use persisted operations for app (#2649)

This commit is contained in:
Laurin Quast 2023-07-25 09:12:08 +02:00 committed by GitHub
parent 350d123028
commit c8b6e88a06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 174 additions and 10 deletions

1
.gitignore vendored
View file

@ -129,6 +129,7 @@ cypress
packages/web/app/next.config.mjs
packages/web/app/environment-*.mjs
packages/web/app/src/gql/*.ts
packages/web/app/src/gql/*.json
packages/web/app/src/graphql/*.ts
_redirects

View file

@ -1,7 +1,7 @@
// @ts-check
import { type CodegenConfig } from '@graphql-codegen/cli';
import { addTypenameDocumentTransform } from './configs/graphql-code-generator/add-typename-document-transform.mjs';
/** @type {import('@graphql-codegen/cli').CodegenConfig} */
const config = {
const config: CodegenConfig = {
schema: './packages/services/api/src/modules/*/module.graphql.ts',
emitLegacyCommonJSImports: true,
generates: {
@ -157,7 +157,11 @@ const config = {
JSONSchemaObject: 'json-schema-typed#JSONSchema',
},
},
presetConfig: {
persistedDocuments: true,
},
plugins: [],
documentTransforms: [addTypenameDocumentTransform],
},
// CLI
'./packages/libraries/cli/src/gql/': {

View file

@ -0,0 +1,36 @@
/* eslint-disable import/no-extraneous-dependencies */
import { Kind, visit } from 'graphql';
import { Types } from '@graphql-codegen/plugin-helpers';
export const addTypenameDocumentTransform: Types.DocumentTransformObject = {
transform({ documents }) {
return documents.map(document => ({
...document,
document: document.document
? visit(document.document, {
SelectionSet(node) {
if (
!node.selections.find(
selection => selection.kind === 'Field' && selection.name.value === '__typename',
)
) {
return {
...node,
selections: [
{
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: '__typename',
},
},
...node.selections,
],
};
}
},
})
: undefined,
}));
},
};

View file

@ -120,6 +120,11 @@ export function deployApp({
value: 'https://the-guild.dev/graphql/hive/docs',
},
{
name: 'GRAPHQL_PERSISTED_OPERATIONS',
value: '1',
},
//
// AUTH
//

View file

@ -164,6 +164,8 @@ export function deployGraphQL({
AUTH_LEGACY_AUTH0: '1',
AUTH_LEGACY_AUTH0_INTERNAL_API_KEY: auth0Config.internalApiKey,
AUTH_ORGANIZATION_OIDC: '1',
// Various
GRAPHQL_PERSISTED_OPERATIONS_PATH: './persisted-operations.json',
},
exposesMetrics: true,
port: 4000,

View file

@ -27,7 +27,7 @@
"docker:build": "docker buildx bake -f docker/docker.hcl --load build",
"env:sync": "tsx scripts/sync-env-files.ts",
"generate": "pnpm --filter @hive/storage db:generate && pnpm graphql:generate",
"graphql:generate": "graphql-codegen --config codegen.cjs",
"graphql:generate": "graphql-codegen --config codegen.mts",
"integration:prepare": "cd integration-tests && ./local.sh",
"lint": "eslint --cache --ignore-path .gitignore \"{packages,cypress}/**/*.{ts,tsx}\"",
"lint:env-template": "tsx scripts/check-env-template.ts",

View file

@ -48,6 +48,7 @@ The GraphQL API for GraphQL Hive.
| `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) |
| `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `server` |
| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) |
| `GRAPHQL_PERSISTED_OPERATIONS_PATH` | No | The path to a file of persisted operations to | `./persisted-operations.json` |
## Hive Hosted Configuration

View file

@ -5,7 +5,7 @@
"license": "MIT",
"private": true,
"scripts": {
"build": "tsx ../../../scripts/runify.ts",
"build": "tsx ../../../scripts/runify.ts && tsx ./scripts/copy-persisted-operations.mts",
"dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/dev.ts",
"typecheck": "tsc --noEmit"
},
@ -19,6 +19,7 @@
"@escape.tech/graphql-armor-max-depth": "2.1.0",
"@escape.tech/graphql-armor-max-directives": "2.0.0",
"@escape.tech/graphql-armor-max-tokens": "2.1.0",
"@graphql-yoga/plugin-persisted-operations": "2.0.3",
"@graphql-yoga/plugin-response-cache": "2.1.0",
"@sentry/integrations": "7.59.3",
"@sentry/node": "7.59.3",

View file

@ -0,0 +1,34 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import * as url from 'url';
import graphql, { Kind, parse, visit } from 'graphql';
const dirname = url.fileURLToPath(new URL('.', import.meta.url));
const persistedOperationsAppPath = path.join(
dirname,
'..',
'..',
'..',
'web',
'app',
'src',
'gql',
'persisted-documents.json',
);
const persistedOperationsDistPath = path.join(dirname, '..', 'dist', 'persisted-operations.json');
const persistedOperations: Record<string, string> = JSON.parse(
await fs.readFile(persistedOperationsAppPath, 'utf-8'),
);
await fs.writeFile(
persistedOperationsDistPath,
JSON.stringify(
Object.fromEntries(
Object.entries(persistedOperations).map(([hash, document]) => [hash, parse(document)]),
),
null,
2,
),
);

View file

@ -32,6 +32,7 @@ const EnvironmentModel = zod.object({
WEBHOOKS_ENDPOINT: zod.string().url(),
SCHEMA_ENDPOINT: zod.string().url(),
AUTH_ORGANIZATION_OIDC: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
GRAPHQL_PERSISTED_OPERATIONS_PATH: emptyString(zod.string().optional()),
});
const SentryModel = zod.union([
@ -350,4 +351,7 @@ export const env = {
}
: null,
hive: hiveConfig,
graphql: {
persistedOperationsPath: base.GRAPHQL_PERSISTED_OPERATIONS_PATH ?? null,
},
} as const;

View file

@ -5,7 +5,7 @@ import type {
FastifyRequest,
RouteHandlerMethod,
} from 'fastify';
import { GraphQLError, print, ValidationContext, ValidationRule } from 'graphql';
import { DocumentNode, GraphQLError, print, ValidationContext, ValidationRule } from 'graphql';
import { createYoga, Plugin, useErrorHandler } from 'graphql-yoga';
import hyperid from 'hyperid';
import zod from 'zod';
@ -14,6 +14,7 @@ import { useGenericAuth } from '@envelop/generic-auth';
import { useGraphQLModules } from '@envelop/graphql-modules';
import { useSentry } from '@envelop/sentry';
import { useYogaHive } from '@graphql-hive/client';
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations';
import { useResponseCache } from '@graphql-yoga/plugin-response-cache';
import { HiveError, Registry, RegistryContext } from '@hive/api';
import { cleanRequestId } from '@hive/service-common';
@ -54,6 +55,7 @@ export interface GraphQLHandlerOptions {
hiveConfig: HiveConfig;
release: string;
logger: FastifyLoggerInstance;
persistedOperations: Record<string, DocumentNode | string> | null;
}
export type SuperTokenSessionPayload = zod.TypeOf<typeof SuperTokenAccessTokenModel>;
@ -98,9 +100,19 @@ function useNoIntrospection(params: {
}
export const graphqlHandler = (options: GraphQLHandlerOptions): RouteHandlerMethod => {
const { persistedOperations } = options;
const server = createYoga<Context>({
logging: options.logger,
plugins: [
persistedOperations
? usePersistedOperations({
allowArbitraryOperations: true,
skipDocumentValidation: true,
getPersistedOperation(key) {
return persistedOperations[key] ?? null;
},
})
: {},
useArmor(),
useSentry({
startTransaction: false,

View file

@ -1,6 +1,7 @@
#!/usr/bin/env node
import * as fs from 'fs';
import got from 'got';
import { GraphQLError, stripIgnoredCharacters } from 'graphql';
import { DocumentNode, GraphQLError, stripIgnoredCharacters } from 'graphql';
import 'reflect-metadata';
import zod from 'zod';
import { createRegistry, CryptoProvider, LogFn, Logger } from '@hive/api';
@ -244,6 +245,13 @@ export async function main() {
organizationOIDC: env.organizationOIDC,
});
let persistedOperations: Record<string, DocumentNode | string> | null = null;
if (env.graphql.persistedOperationsPath) {
persistedOperations = JSON.parse(
fs.readFileSync(env.graphql.persistedOperationsPath, 'utf-8'),
);
}
const graphqlPath = '/graphql';
const port = env.http.port;
const signature = Math.random().toString(16).substr(2);
@ -259,6 +267,7 @@ export async function main() {
release: env.release,
hiveConfig: env.hive,
logger: graphqlLogger as any,
persistedOperations,
});
server.route({

View file

@ -38,6 +38,7 @@ The following environment variables configure the application.
| `NODE_ENV` | No | The `NODE_ENV` value. | `production` |
| `GA_TRACKING_ID` | No | The token for Google Analytics in order to track user actions. | `g6aff8102efda5e1d12e` |
| `CRISP_WEBSITE_ID` | No | The Crisp Website ID | `g6aff8102efda5e1d12e` |
| `GRAPHQL_PERSISTED_OPERATIONS` | No | Send persisted oepration hashes instead of documents to the server. | `1` (enabled) or `0` (disabled) |
## Hive Hosted Configuration

View file

@ -38,6 +38,9 @@ const BaseSchema = zod.object({
AUTH_REQUIRE_EMAIL_VERIFICATION: emptyString(
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
),
GRAPHQL_PERSISTED_OPERATIONS: emptyString(
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
),
});
const IntegrationSlackSchema = zod.union([
@ -231,6 +234,9 @@ const config = {
},
sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null,
stripePublicKey: base.STRIPE_PUBLIC_KEY ?? null,
graphql: {
persistedOperations: base.GRAPHQL_PERSISTED_OPERATIONS === '1',
},
} as const;
declare global {

View file

@ -47,6 +47,7 @@
"@urql/core": "4.0.10",
"@urql/devtools": "2.0.3",
"@urql/exchange-graphcache": "6.1.4",
"@urql/exchange-persisted": "4.0.1",
"@whatwg-node/fetch": "0.9.9",
"class-variance-authority": "0.6.0",
"clsx": "1.2.1",

View file

@ -33,6 +33,9 @@ export const env = {
release: backendEnv.release,
environment: backendEnv.environment,
nodeEnv: backendEnv.nodeEnv,
graphql: {
persistedOperations: backendEnv.graphql.persistedOperations,
},
} as const;
declare global {

View file

@ -1,5 +1,7 @@
import { createClient, errorExchange, fetchExchange } from 'urql';
import { env } from '@/env/frontend';
import { cacheExchange } from '@urql/exchange-graphcache';
import { persistedExchange } from '@urql/exchange-persisted';
import { Mutation } from './urql-cache';
import { networkStatusExchange } from './urql-exchanges/state';
@ -7,6 +9,8 @@ const noKey = (): null => null;
const SERVER_BASE_PATH = '/api/proxy';
const isSome = <T>(value: T | null | undefined): value is T => value != null;
export const urqlClient = createClient({
url: SERVER_BASE_PATH,
exchanges: [
@ -45,6 +49,7 @@ export const urqlClient = createClient({
},
globalIDs: ['SuccessfulSchemaCheck', 'FailedSchemaCheck'],
}),
networkStatusExchange,
errorExchange({
onError(error) {
if (error.response?.status === 401) {
@ -52,7 +57,16 @@ export const urqlClient = createClient({
}
},
}),
networkStatusExchange,
env.graphql.persistedOperations
? persistedExchange({
enforcePersistedQueries: true,
enableForMutation: true,
generateHash: (_, document) => {
// TODO: improve types here
return Promise.resolve((document as any)?.['__meta__']?.['hash'] ?? '');
},
})
: null,
fetchExchange,
].filter(Boolean),
].filter(isSome),
});

View file

@ -1018,6 +1018,9 @@ importers:
'@escape.tech/graphql-armor-max-tokens':
specifier: 2.1.0
version: 2.1.0
'@graphql-yoga/plugin-persisted-operations':
specifier: 2.0.3
version: 2.0.3(@graphql-tools/utils@10.0.3)(graphql-yoga@4.0.3)(graphql@16.6.0)
'@graphql-yoga/plugin-response-cache':
specifier: 2.1.0
version: 2.1.0(@envelop/core@4.0.0)(graphql-yoga@4.0.3)(graphql@16.6.0)
@ -1546,6 +1549,9 @@ importers:
'@urql/exchange-graphcache':
specifier: 6.1.4
version: 6.1.4(graphql@16.6.0)
'@urql/exchange-persisted':
specifier: 4.0.1
version: 4.0.1(graphql@16.6.0)
'@whatwg-node/fetch':
specifier: 0.9.9
version: 0.9.9
@ -7615,6 +7621,19 @@ packages:
dependencies:
tslib: 2.5.3
/@graphql-yoga/plugin-persisted-operations@2.0.3(@graphql-tools/utils@10.0.3)(graphql-yoga@4.0.3)(graphql@16.6.0):
resolution: {integrity: sha512-2MWcNiwetxlfpUuwGpf9YvYfWalOHckRA81DRdWQtys4qRpozPJ5Zt+ucEEmrCtpqhloudbmXjW2aXnrLVUcQQ==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@graphql-tools/utils': ^10.0.0
graphql: ^15.2.0 || ^16.0.0
graphql-yoga: ^4.0.3
dependencies:
'@graphql-tools/utils': 10.0.3(graphql@16.6.0)
graphql: 16.6.0
graphql-yoga: 4.0.3(graphql@16.6.0)
dev: false
/@graphql-yoga/plugin-response-cache@2.1.0(@envelop/core@4.0.0)(graphql-yoga@4.0.3)(graphql@16.6.0):
resolution: {integrity: sha512-QMmVL5Sx2mEQ6pejj0pQ6F2QcfIiMYibE1xSdhWAJkDfQlImAcd3uv1smMBbUMp3GNRF3vs1K5SpIzX/UHva4g==}
engines: {node: '>=16.0.0'}
@ -13009,6 +13028,15 @@ packages:
- graphql
dev: false
/@urql/exchange-persisted@4.0.1(graphql@16.6.0):
resolution: {integrity: sha512-EG0835VoNC4a1Tjv58z0V01/jX1L3+TEEEglI8rTMfE0KUdGKpqel6vqz2W6L52RdGiZgh0cY+wgfy/xAZT2ug==}
dependencies:
'@urql/core': 4.0.10(graphql@16.6.0)
wonka: 6.3.2
transitivePeerDependencies:
- graphql
dev: false
/@vitest/expect@0.31.1:
resolution: {integrity: sha512-BV1LyNvhnX+eNYzJxlHIGPWZpwJFZaCcOIzp2CNG0P+bbetenTupk6EO0LANm4QFt0TTit+yqx7Rxd1qxi/SQA==}
dependencies:

View file

@ -15,7 +15,9 @@
".eslintrc.cjs",
"vitest.config.ts",
"scripts/serializer.ts",
"packages/web/app/.babelrc.cjs"
"packages/web/app/.babelrc.cjs",
"configs",
"codegen.mts"
],
"exclude": ["**/node_modules/**", "**/dist", "**/temp", "**/tmp"]
}