mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
use persisted operations for app (#2649)
This commit is contained in:
parent
350d123028
commit
c8b6e88a06
19 changed files with 174 additions and 10 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/': {
|
||||
|
|
@ -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,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
|
@ -120,6 +120,11 @@ export function deployApp({
|
|||
value: 'https://the-guild.dev/graphql/hive/docs',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'GRAPHQL_PERSISTED_OPERATIONS',
|
||||
value: '1',
|
||||
},
|
||||
|
||||
//
|
||||
// AUTH
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue