From 63b78d5a7f6f7fd1d5939e92ede2574fda9d08dd Mon Sep 17 00:00:00 2001 From: Valentin Cocaud Date: Wed, 5 Mar 2025 15:56:00 +0100 Subject: [PATCH] feat(core): Add new Instruments API (#3793) * feat(core): Add new Tracer API * changeset * fix root exports * use latest envelope snapshot * rename to instruments and use instruments utils * chore(dependencies): updated changesets for modified dependencies * refactor request parser to use promise utils * add tests * chore(dependencies): updated changesets for modified dependencies * fix `requestParse` and `resultProcess` instruments can be async * remove request from operation instruments payload * add documentation * use @envelop/instruments released version * chore(dependencies): updated changesets for modified dependencies * changeset * re-export instruments utils * update whatwg-node alpha * More refactor * chore(dependencies): updated changesets for modified dependencies * Lets go * Bump versions * Bump versions * Bump versions * Deduped lockfile * chore(dependencies): updated changesets for modified dependencies * Fix tests * Fix * Deduped lockfile * chore(dependencies): updated changesets for modified dependencies --------- Co-authored-by: github-actions[bot] Co-authored-by: Arda TANRIKULU --- ...oga_nestjs-federation-3793-dependencies.md | 6 + ...n-apollo-inline-trace-3793-dependencies.md | 5 + ...disable-introspection-3793-dependencies.md | 5 + ...oga_plugin-prometheus-3793-dependencies.md | 5 + ...plugin-response-cache-3793-dependencies.md | 6 + .changeset/afraid-jars-burn.md | 90 ++++ .changeset/graphql-yoga-3793-dependencies.md | 7 + .gitignore | 1 + examples/error-handling/src/yoga.ts | 4 +- .../__integration-tests__/sveltekit.spec.ts | 2 +- package.json | 19 +- .../__tests__/instruments.spec.ts | 113 +++++ packages/graphql-yoga/package.json | 4 +- packages/graphql-yoga/src/index.ts | 5 +- .../request-parser/post-form-url-encoded.ts | 5 +- .../request-parser/post-graphql-string.ts | 7 +- .../src/plugins/request-parser/post-json.ts | 96 ++-- .../plugins/request-parser/post-multipart.ts | 100 ++--- .../src/plugins/result-processor/multipart.ts | 59 +-- .../src/plugins/result-processor/sse.ts | 48 +- packages/graphql-yoga/src/plugins/types.ts | 29 +- .../graphql-yoga/src/plugins/use-graphiql.ts | 41 +- .../src/plugins/use-readiness-check.ts | 41 +- .../graphql-yoga/src/plugins/use-schema.ts | 29 +- packages/graphql-yoga/src/process-request.ts | 99 +++-- packages/graphql-yoga/src/server.ts | 375 ++++++++++------ packages/nestjs-federation/package.json | 4 +- .../plugins/apollo-inline-trace/package.json | 4 +- .../plugins/apollo-usage-report/package.json | 2 +- packages/plugins/csrf-prevention/src/index.ts | 2 +- .../disable-introspection/package.json | 3 + .../disable-introspection/src/index.ts | 17 +- packages/plugins/prometheus/package.json | 2 +- packages/plugins/response-cache/package.json | 4 +- pnpm-lock.yaml | 416 ++++-------------- .../content/docs/features/envelop-plugins.mdx | 117 +++++ 36 files changed, 1045 insertions(+), 727 deletions(-) create mode 100644 .changeset/@graphql-yoga_nestjs-federation-3793-dependencies.md create mode 100644 .changeset/@graphql-yoga_plugin-apollo-inline-trace-3793-dependencies.md create mode 100644 .changeset/@graphql-yoga_plugin-disable-introspection-3793-dependencies.md create mode 100644 .changeset/@graphql-yoga_plugin-prometheus-3793-dependencies.md create mode 100644 .changeset/@graphql-yoga_plugin-response-cache-3793-dependencies.md create mode 100644 .changeset/afraid-jars-burn.md create mode 100644 .changeset/graphql-yoga-3793-dependencies.md create mode 100644 packages/graphql-yoga/__tests__/instruments.spec.ts diff --git a/.changeset/@graphql-yoga_nestjs-federation-3793-dependencies.md b/.changeset/@graphql-yoga_nestjs-federation-3793-dependencies.md new file mode 100644 index 0000000000..b775e9b673 --- /dev/null +++ b/.changeset/@graphql-yoga_nestjs-federation-3793-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-yoga/nestjs-federation": patch +--- +dependencies updates: + - Updated dependency [`@envelop/apollo-federation@^6.1.1` ↗︎](https://www.npmjs.com/package/@envelop/apollo-federation/v/6.1.1) (from `^6.0.0`, in `dependencies`) + - Updated dependency [`@envelop/core@^5.2.1` ↗︎](https://www.npmjs.com/package/@envelop/core/v/5.2.1) (from `^5.0.0`, in `dependencies`) diff --git a/.changeset/@graphql-yoga_plugin-apollo-inline-trace-3793-dependencies.md b/.changeset/@graphql-yoga_plugin-apollo-inline-trace-3793-dependencies.md new file mode 100644 index 0000000000..605b740e84 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-apollo-inline-trace-3793-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-yoga/plugin-apollo-inline-trace": patch +--- +dependencies updates: + - Updated dependency [`@envelop/on-resolve@^5.1.1` ↗︎](https://www.npmjs.com/package/@envelop/on-resolve/v/5.1.1) (from `^5.0.0`, in `dependencies`) diff --git a/.changeset/@graphql-yoga_plugin-disable-introspection-3793-dependencies.md b/.changeset/@graphql-yoga_plugin-disable-introspection-3793-dependencies.md new file mode 100644 index 0000000000..4e136a4b9c --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-disable-introspection-3793-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-yoga/plugin-disable-introspection": patch +--- +dependencies updates: + - Added dependency [`@whatwg-node/promise-helpers@^1.2.4` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.2.4) (to `dependencies`) diff --git a/.changeset/@graphql-yoga_plugin-prometheus-3793-dependencies.md b/.changeset/@graphql-yoga_plugin-prometheus-3793-dependencies.md new file mode 100644 index 0000000000..9afef09c7d --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-prometheus-3793-dependencies.md @@ -0,0 +1,5 @@ +--- +"@graphql-yoga/plugin-prometheus": patch +--- +dependencies updates: + - Updated dependency [`@envelop/prometheus@^12.1.1` ↗︎](https://www.npmjs.com/package/@envelop/prometheus/v/12.1.1) (from `^12.0.0`, in `dependencies`) diff --git a/.changeset/@graphql-yoga_plugin-response-cache-3793-dependencies.md b/.changeset/@graphql-yoga_plugin-response-cache-3793-dependencies.md new file mode 100644 index 0000000000..9cebbfa804 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-response-cache-3793-dependencies.md @@ -0,0 +1,6 @@ +--- +"@graphql-yoga/plugin-response-cache": patch +--- +dependencies updates: + - Updated dependency [`@envelop/core@^5.2.1` ↗︎](https://www.npmjs.com/package/@envelop/core/v/5.2.1) (from `^5.0.2`, in `dependencies`) + - Updated dependency [`@envelop/response-cache@^7.1.1` ↗︎](https://www.npmjs.com/package/@envelop/response-cache/v/7.1.1) (from `^7.0.0`, in `dependencies`) diff --git a/.changeset/afraid-jars-burn.md b/.changeset/afraid-jars-burn.md new file mode 100644 index 0000000000..46a0ac3e0b --- /dev/null +++ b/.changeset/afraid-jars-burn.md @@ -0,0 +1,90 @@ +--- +'graphql-yoga': minor +--- + +Add new Instruments API + +Introduction of a new API allowing to instrument the graphql pipeline. + +This new API differs from already existing Hooks by not having access to input/output of phases. The +goal of `Instruments` is to run allow running code before, after or around the **whole process of a +phase**, including plugins hooks executions. + +The main use case of this new API is observability (monitoring, tracing, etc...). + +### Basic usage + +```ts +import { createYoga } from 'graphql-yoga' +import Sentry from '@sentry/node' +import schema from './schema' + +const server = createYoga({ + schema, + plugins: [ + { + instruments: { + request: ({ request }, wrapped) => + Sentry.startSpan({ name: 'Graphql Operation' }, async () => { + try { + await wrapped() + } catch (err) { + Sentry.captureException(err) + } + }) + } + } + ] +}) +``` + +### Multiple instruments plugins + +It is possible to have multiple instruments plugins (Prometheus and Sentry for example), they will +be automatically composed by envelop in the same order than the plugin array (first is outermost, +last is inner most). + +```ts +import { createYoga } from 'graphql-yoga' +import schema from './schema' + +const server = createYoga({ + schema, + plugins: [useSentry(), useOpentelemetry()] +}) +``` + +```mermaid +sequenceDiagram + Sentry->>Opentelemetry: ; + Opentelemetry->>Server Adapter: ; + Server Adapter->>Opentelemetry: ; + Opentelemetry->>Sentry: ; +``` + +### Custom instruments ordering + +If the default composition ordering doesn't suite your need, you can manually compose instruments. +This allows to have a different execution order of hooks and instruments. + +```ts +import { composeInstruments, createYoga } from 'graphql-yoga' +import schema from './schema' + +const { instruments: sentryInstruments, ...sentryPlugin } = useSentry() +const { instruments: otelInstruments, ...otelPlugin } = useOpentelemetry() +const instruments = composeInstruments([otelInstruments, sentryInstruments]) + +const server = createYoga({ + schema, + plugins: [{ instruments }, useSentry(), useOpentelemetry()] +}) +``` + +```mermaid +sequenceDiagram + Opentelemetry->>Sentry: ; + Sentry->>Server Adapter: ; + Server Adapter->>Sentry: ; + Sentry->>Opentelemetry: ; +``` diff --git a/.changeset/graphql-yoga-3793-dependencies.md b/.changeset/graphql-yoga-3793-dependencies.md new file mode 100644 index 0000000000..1c452bc5db --- /dev/null +++ b/.changeset/graphql-yoga-3793-dependencies.md @@ -0,0 +1,7 @@ +--- +"graphql-yoga": patch +--- +dependencies updates: + - Updated dependency [`@envelop/core@^5.2.1` ↗︎](https://www.npmjs.com/package/@envelop/core/v/5.2.1) (from `^5.0.2`, in `dependencies`) + - Added dependency [`@envelop/instruments@^1.0.0` ↗︎](https://www.npmjs.com/package/@envelop/instruments/v/1.0.0) (to `dependencies`) + - Added dependency [`@whatwg-node/promise-helpers@^1.2.4` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.2.4) (to `dependencies`) diff --git a/.gitignore b/.gitignore index 53dcc92601..5d4cdd326d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ packages/graphql-yoga/src/landing-page-html.ts packages/graphql-yoga/src/graphiql-html.ts run/ website/public/_pagefind/ +.helix/languages.toml diff --git a/examples/error-handling/src/yoga.ts b/examples/error-handling/src/yoga.ts index 6a7986b901..e2961a1912 100644 --- a/examples/error-handling/src/yoga.ts +++ b/examples/error-handling/src/yoga.ts @@ -33,7 +33,9 @@ export const yoga = createYoga({ Query: { greeting: async () => { // This service does not exist - const greeting = await fetch('http://localhost:9999/greeting').then(res => res.text()); + const greeting = await fetch('http://0.0.0.0:9999/greeting', { + signal: AbortSignal.timeout(1000), + }).then(res => res.text()); return greeting; }, diff --git a/examples/sveltekit/__integration-tests__/sveltekit.spec.ts b/examples/sveltekit/__integration-tests__/sveltekit.spec.ts index 508d47e586..4326a8db3e 100644 --- a/examples/sveltekit/__integration-tests__/sveltekit.spec.ts +++ b/examples/sveltekit/__integration-tests__/sveltekit.spec.ts @@ -50,7 +50,7 @@ describe('SvelteKit integration', () => { }); afterAll(async () => { - await browser.close(); + await browser?.close(); sveltekitProcess.kill(); }); diff --git a/package.json b/package.json index f29c1e9f64..ea19ecbb0d 100644 --- a/package.json +++ b/package.json @@ -105,9 +105,24 @@ }, "overrides": { "graphql": "16.10.0", - "@envelop/core": "5.2.1", "@changesets/assemble-release-plan": "5.2.3", "@types/react": "19.0.10" - } + }, + "onlyBuiltDependencies": [ + "@nestjs/core", + "@playwright/browser-chromium", + "@prisma/client", + "@prisma/engines", + "@pulumi/docker", + "@pulumi/docker-build", + "@sveltejs/kit", + "@swc/core", + "egg-bin", + "esbuild", + "netlify-cli", + "prisma", + "svelte-preprocess", + "workerd" + ] } } diff --git a/packages/graphql-yoga/__tests__/instruments.spec.ts b/packages/graphql-yoga/__tests__/instruments.spec.ts new file mode 100644 index 0000000000..342a93fc00 --- /dev/null +++ b/packages/graphql-yoga/__tests__/instruments.spec.ts @@ -0,0 +1,113 @@ +import { createSchema, createYoga, Plugin } from '../src'; + +describe('instruments', () => { + const schema = createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String! + } + type Subscription { + greetings: String! + } + `, + resolvers: { + Query: { + hello: () => 'world', + }, + Subscription: { + greetings: { + async *subscribe() { + yield { greetings: 'Hi' }; + }, + }, + }, + }, + }); + + it('should wrap all the phases with the default composition order', async () => { + const result: string[] = []; + const make = (name: string): Plugin => ({ + instruments: { + context: (_, w) => { + result.push(`pre-context-${name}`); + w(); + result.push(`post-context-${name}`); + }, + execute: async (_, w) => { + result.push(`pre-execute-${name}`); + await w(); + result.push(`post-execute-${name}`); + }, + init: (_, w) => { + result.push(`pre-init-${name}`); + w(); + result.push(`post-init-${name}`); + }, + parse: (_, w) => { + result.push(`pre-parse-${name}`); + w(); + result.push(`post-parse-${name}`); + }, + request: async (_, w) => { + result.push(`pre-request-${name}`); + await w(); + result.push(`post-request-${name}`); + }, + subscribe: async (_, w) => { + result.push(`pre-subscribe-${name}`); + await w(); + result.push('post-subscribe-${name}'); + }, + validate: (_, w) => { + result.push(`pre-validate-${name}`); + w(); + result.push(`post-validate-${name}`); + }, + operation: async (_, w) => { + result.push(`pre-operation-${name}`); + await w(); + result.push(`post-operation-${name}`); + }, + requestParse: async (_, w) => { + result.push(`pre-request-parse-${name}`); + await w(); + result.push(`post-request-parse-${name}`); + }, + resultProcess: async (_, w) => { + result.push(`pre-result-process-${name}`); + await w(); + result.push(`post-result-process-${name}`); + }, + }, + }); + + const yoga = createYoga({ + schema, + plugins: [make('1'), make('2'), make('3')], + }); + + await yoga.fetch('http://yoga/graphql?query={hello}'); + + const withPrefix = (prefix: string) => [`${prefix}-1`, `${prefix}-2`, `${prefix}-3`]; + expect(result).toEqual([ + ...withPrefix('pre-request'), + ...withPrefix('pre-request-parse'), + ...withPrefix('post-request-parse').reverse(), + ...withPrefix('pre-operation'), + ...withPrefix('pre-init'), + ...withPrefix('post-init').reverse(), + ...withPrefix('pre-parse'), + ...withPrefix('post-parse').reverse(), + ...withPrefix('pre-validate'), + ...withPrefix('post-validate').reverse(), + ...withPrefix('pre-context'), + ...withPrefix('post-context').reverse(), + ...withPrefix('pre-execute'), + ...withPrefix('post-execute').reverse(), + ...withPrefix('post-operation').reverse(), + ...withPrefix('pre-result-process'), + ...withPrefix('post-result-process').reverse(), + ...withPrefix('post-request').reverse(), + ]); + }); +}); diff --git a/packages/graphql-yoga/package.json b/packages/graphql-yoga/package.json index 65fbeb5d2a..6923ad9b4f 100644 --- a/packages/graphql-yoga/package.json +++ b/packages/graphql-yoga/package.json @@ -49,13 +49,15 @@ "graphql": "^15.2.0 || ^16.0.0" }, "dependencies": { - "@envelop/core": "^5.0.2", + "@envelop/core": "^5.2.1", + "@envelop/instruments": "^1.0.0", "@graphql-tools/executor": "^1.4.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^10.6.2", "@graphql-yoga/logger": "workspace:^", "@graphql-yoga/subscription": "workspace:^", "@whatwg-node/fetch": "^0.10.5", + "@whatwg-node/promise-helpers": "^1.2.4", "@whatwg-node/server": "^0.10.0", "dset": "^3.1.4", "lru-cache": "^10.0.0", diff --git a/packages/graphql-yoga/src/index.ts b/packages/graphql-yoga/src/index.ts index a886a53ba9..b41203a63e 100644 --- a/packages/graphql-yoga/src/index.ts +++ b/packages/graphql-yoga/src/index.ts @@ -1,5 +1,4 @@ export * from '@graphql-yoga/logger'; -export { type Plugin } from './plugins/types.js'; export { type GraphiQLOptions } from './plugins/use-graphiql.js'; export { renderGraphiQL, shouldRenderGraphiQL } from './plugins/use-graphiql.js'; export { useReadinessCheck } from './plugins/use-readiness-check.js'; @@ -9,7 +8,7 @@ export * from './server.js'; export * from './subscription.js'; export * from './types.js'; export { maskError } from './utils/mask-error.js'; -export { type OnParamsEventPayload } from './plugins/types.js'; +export { type OnParamsEventPayload, type Plugin, type Instruments } from './plugins/types.js'; export { _createLRUCache, createLRUCache } from './utils/create-lru-cache.js'; export { mergeSchemas } from '@graphql-tools/schema'; export { @@ -36,6 +35,7 @@ export { useLogger, usePayloadFormatter, } from '@envelop/core'; +export { getInstrumentsAndPlugins, chain, composeInstruments } from '@envelop/instruments'; export { createGraphQLError, isPromise, mapMaybePromise } from '@graphql-tools/utils'; export { getSSEProcessor } from './plugins/result-processor/sse.js'; export { processRegularResult } from './plugins/result-processor/regular.js'; @@ -45,3 +45,4 @@ export { type LandingPageRendererOpts, } from './plugins/use-unhandled-route.js'; export { DisposableSymbols } from '@whatwg-node/server'; +export * from '@envelop/instruments'; diff --git a/packages/graphql-yoga/src/plugins/request-parser/post-form-url-encoded.ts b/packages/graphql-yoga/src/plugins/request-parser/post-form-url-encoded.ts index 778f9e0462..fda175c3e8 100644 --- a/packages/graphql-yoga/src/plugins/request-parser/post-form-url-encoded.ts +++ b/packages/graphql-yoga/src/plugins/request-parser/post-form-url-encoded.ts @@ -7,7 +7,6 @@ export function isPOSTFormUrlEncodedRequest(request: Request) { ); } -export async function parsePOSTFormUrlEncodedRequest(request: Request): Promise { - const requestBody = await request.text(); - return parseURLSearchParams(requestBody); +export function parsePOSTFormUrlEncodedRequest(request: Request): Promise { + return request.text().then(parseURLSearchParams); } diff --git a/packages/graphql-yoga/src/plugins/request-parser/post-graphql-string.ts b/packages/graphql-yoga/src/plugins/request-parser/post-graphql-string.ts index e0421a20ce..ab78a0792b 100644 --- a/packages/graphql-yoga/src/plugins/request-parser/post-graphql-string.ts +++ b/packages/graphql-yoga/src/plugins/request-parser/post-graphql-string.ts @@ -5,9 +5,6 @@ export function isPOSTGraphQLStringRequest(request: Request) { return request.method === 'POST' && isContentTypeMatch(request, 'application/graphql'); } -export async function parsePOSTGraphQLStringRequest(request: Request): Promise { - const requestBody = await request.text(); - return { - query: requestBody, - }; +export function parsePOSTGraphQLStringRequest(request: Request): Promise { + return request.text().then(query => ({ query })); } diff --git a/packages/graphql-yoga/src/plugins/request-parser/post-json.ts b/packages/graphql-yoga/src/plugins/request-parser/post-json.ts index d59b33d74f..4a995cdcfd 100644 --- a/packages/graphql-yoga/src/plugins/request-parser/post-json.ts +++ b/packages/graphql-yoga/src/plugins/request-parser/post-json.ts @@ -1,5 +1,6 @@ -import { GraphQLErrorExtensions } from 'graphql'; +import { GraphQLError, GraphQLErrorExtensions } from 'graphql'; import { createGraphQLError } from '@graphql-tools/utils'; +import { handleMaybePromise, MaybePromise } from '@whatwg-node/promise-helpers'; import { GraphQLParams } from '../../types.js'; import { isContentTypeMatch } from './utils.js'; @@ -11,54 +12,57 @@ export function isPOSTJsonRequest(request: Request) { ); } -export async function parsePOSTJsonRequest(request: Request): Promise { - let requestBody: GraphQLParams; - try { - requestBody = await request.json(); - } catch (err) { - const extensions: GraphQLErrorExtensions = { - http: { - spec: true, - status: 400, - }, - code: 'BAD_REQUEST', - }; - if (err instanceof Error) { - extensions['originalError'] = { - name: err.name, - message: err.message, - }; - } - throw createGraphQLError('POST body sent invalid JSON.', { - extensions, - }); - } +export function parsePOSTJsonRequest(request: Request): MaybePromise { + return handleMaybePromise( + () => request.json(), + (requestBody: GraphQLParams) => { + if (requestBody == null) { + throw createGraphQLError(`POST body is expected to be object but received ${requestBody}`, { + extensions: { + http: { + status: 400, + }, + code: 'BAD_REQUEST', + }, + }); + } - if (requestBody == null) { - throw createGraphQLError(`POST body is expected to be object but received ${requestBody}`, { - extensions: { + const requestBodyTypeof = typeof requestBody; + if (requestBodyTypeof !== 'object') { + throw createGraphQLError( + `POST body is expected to be object but received ${requestBodyTypeof}`, + { + extensions: { + http: { + status: 400, + }, + code: 'BAD_REQUEST', + }, + }, + ); + } + return requestBody; + }, + err => { + if (err instanceof GraphQLError) { + throw err; + } + const extensions: GraphQLErrorExtensions = { http: { + spec: true, status: 400, }, code: 'BAD_REQUEST', - }, - }); - } - - const requestBodyTypeof = typeof requestBody; - if (requestBodyTypeof !== 'object') { - throw createGraphQLError( - `POST body is expected to be object but received ${requestBodyTypeof}`, - { - extensions: { - http: { - status: 400, - }, - code: 'BAD_REQUEST', - }, - }, - ); - } - - return requestBody; + }; + if (err instanceof Error) { + extensions['originalError'] = { + name: err.name, + message: err.message, + }; + } + throw createGraphQLError('POST body sent invalid JSON.', { + extensions, + }); + }, + ); } diff --git a/packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts b/packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts index 45eee8bec3..b30f95198e 100644 --- a/packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts +++ b/packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts @@ -1,5 +1,6 @@ import { dset } from 'dset'; import { createGraphQLError } from '@graphql-tools/utils'; +import { handleMaybePromise, MaybePromise } from '@whatwg-node/promise-helpers'; import { GraphQLParams } from '../../types.js'; import { isContentTypeMatch } from './utils.js'; @@ -7,63 +8,64 @@ export function isPOSTMultipartRequest(request: Request): boolean { return request.method === 'POST' && isContentTypeMatch(request, 'multipart/form-data'); } -export async function parsePOSTMultipartRequest(request: Request): Promise { - let requestBody: FormData; - try { - requestBody = await request.formData(); - } catch (e) { - if (e instanceof Error && e.message.startsWith('File size limit exceeded: ')) { - throw createGraphQLError(e.message, { - extensions: { - http: { - status: 413, - }, - }, - }); - } - throw e; - } - - const operationsStr = requestBody.get('operations'); +export function parsePOSTMultipartRequest(request: Request): MaybePromise { + return handleMaybePromise( + () => request.formData(), + (requestBody: FormData) => { + const operationsStr = requestBody.get('operations'); - if (!operationsStr) { - throw createGraphQLError('Missing multipart form field "operations"'); - } + if (!operationsStr) { + throw createGraphQLError('Missing multipart form field "operations"'); + } - if (typeof operationsStr !== 'string') { - throw createGraphQLError('Multipart form field "operations" must be a string'); - } + if (typeof operationsStr !== 'string') { + throw createGraphQLError('Multipart form field "operations" must be a string'); + } - let operations: GraphQLParams; + let operations: GraphQLParams; - try { - operations = JSON.parse(operationsStr); - } catch { - throw createGraphQLError('Multipart form field "operations" must be a valid JSON string'); - } + try { + operations = JSON.parse(operationsStr); + } catch { + throw createGraphQLError('Multipart form field "operations" must be a valid JSON string'); + } - const mapStr = requestBody.get('map'); + const mapStr = requestBody.get('map'); - if (mapStr != null) { - if (typeof mapStr !== 'string') { - throw createGraphQLError('Multipart form field "map" must be a string'); - } + if (mapStr != null) { + if (typeof mapStr !== 'string') { + throw createGraphQLError('Multipart form field "map" must be a string'); + } - let map: Record; + let map: Record; - try { - map = JSON.parse(mapStr); - } catch { - throw createGraphQLError('Multipart form field "map" must be a valid JSON string'); - } - for (const fileIndex in map) { - const file = requestBody.get(fileIndex); - const keys = map[fileIndex]!; - for (const key of keys) { - dset(operations, key, file); + try { + map = JSON.parse(mapStr); + } catch { + throw createGraphQLError('Multipart form field "map" must be a valid JSON string'); + } + for (const fileIndex in map) { + const file = requestBody.get(fileIndex); + const keys = map[fileIndex]!; + for (const key of keys) { + dset(operations, key, file); + } + } } - } - } - return operations; + return operations; + }, + e => { + if (e instanceof Error && e.message.startsWith('File size limit exceeded: ')) { + throw createGraphQLError(e.message, { + extensions: { + http: { + status: 413, + }, + }, + }); + } + throw e; + }, + ); } diff --git a/packages/graphql-yoga/src/plugins/result-processor/multipart.ts b/packages/graphql-yoga/src/plugins/result-processor/multipart.ts index 5a55782795..a25688fb3a 100644 --- a/packages/graphql-yoga/src/plugins/result-processor/multipart.ts +++ b/packages/graphql-yoga/src/plugins/result-processor/multipart.ts @@ -1,5 +1,6 @@ import { ExecutionResult } from 'graphql'; import { isAsyncIterable } from '@envelop/core'; +import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { fakePromise } from '@whatwg-node/server'; import { getResponseInitByRespectingErrors } from '../../error.js'; import { FetchAPI, MaybeArray } from '../../types.js'; @@ -38,38 +39,46 @@ export function processMultipartResult(result: ResultProcessorInput, fetchAPI: F controller.enqueue(textEncoder.encode('\r\n')); controller.enqueue(textEncoder.encode(`---`)); }, - async pull(controller) { - try { - const { done, value } = await iterator.next(); - if (value != null) { - controller.enqueue(textEncoder.encode('\r\n')); + pull(controller) { + return handleMaybePromise( + () => iterator.next(), + ({ done, value }) => { + if (value != null) { + controller.enqueue(textEncoder.encode('\r\n')); - controller.enqueue(textEncoder.encode('Content-Type: application/json; charset=utf-8')); - controller.enqueue(textEncoder.encode('\r\n')); + controller.enqueue(textEncoder.encode('Content-Type: application/json; charset=utf-8')); + controller.enqueue(textEncoder.encode('\r\n')); - const chunk = jsonStringifyResultWithoutInternals(value); - const encodedChunk = textEncoder.encode(chunk); + const chunk = jsonStringifyResultWithoutInternals(value); + const encodedChunk = textEncoder.encode(chunk); - controller.enqueue(textEncoder.encode('Content-Length: ' + encodedChunk.byteLength)); - controller.enqueue(textEncoder.encode('\r\n')); + controller.enqueue(textEncoder.encode('Content-Length: ' + encodedChunk.byteLength)); + controller.enqueue(textEncoder.encode('\r\n')); - controller.enqueue(textEncoder.encode('\r\n')); - controller.enqueue(encodedChunk); - controller.enqueue(textEncoder.encode('\r\n')); + controller.enqueue(textEncoder.encode('\r\n')); + controller.enqueue(encodedChunk); + controller.enqueue(textEncoder.encode('\r\n')); - controller.enqueue(textEncoder.encode('---')); - } + controller.enqueue(textEncoder.encode('---')); + } - if (done) { - controller.enqueue(textEncoder.encode('--\r\n')); - controller.close(); - } - } catch (err) { - controller.error(err); - } + if (done) { + controller.enqueue(textEncoder.encode('--\r\n')); + controller.close(); + } + }, + err => { + controller.error(err); + }, + ); }, - async cancel(e) { - await iterator.return?.(e); + cancel(e) { + if (iterator.return) { + return handleMaybePromise( + () => iterator.return?.(e), + () => {}, + ); + } }, }); diff --git a/packages/graphql-yoga/src/plugins/result-processor/sse.ts b/packages/graphql-yoga/src/plugins/result-processor/sse.ts index 52d28748cb..d56909b52d 100644 --- a/packages/graphql-yoga/src/plugins/result-processor/sse.ts +++ b/packages/graphql-yoga/src/plugins/result-processor/sse.ts @@ -1,5 +1,6 @@ import { ExecutionResult } from 'graphql'; import { isAsyncIterable } from '@envelop/core'; +import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { fakePromise } from '@whatwg-node/server'; import { getResponseInitByRespectingErrors } from '../../error.js'; import { FetchAPI, MaybeArray } from '../../types.js'; @@ -58,28 +59,35 @@ export function getSSEProcessor(): ResultProcessor { }; } }, - async pull(controller) { - try { - const result = await iterator.next(); - - if (result.value != null) { - controller.enqueue(textEncoder.encode(`event: next\n`)); - const chunk = jsonStringifyResultWithoutInternals(result.value); - controller.enqueue(textEncoder.encode(`data: ${chunk}\n\n`)); - } - if (result.done) { - controller.enqueue(textEncoder.encode(`event: complete\n`)); - controller.enqueue(textEncoder.encode(`data:\n\n`)); - clearInterval(pingInterval); - controller.close(); - } - } catch (err) { - controller.error(err); - } + pull(controller) { + return handleMaybePromise( + () => iterator.next(), + result => { + if (result.value != null) { + controller.enqueue(textEncoder.encode(`event: next\n`)); + const chunk = jsonStringifyResultWithoutInternals(result.value); + controller.enqueue(textEncoder.encode(`data: ${chunk}\n\n`)); + } + if (result.done) { + controller.enqueue(textEncoder.encode(`event: complete\n`)); + controller.enqueue(textEncoder.encode(`data:\n\n`)); + clearInterval(pingInterval); + controller.close(); + } + }, + err => { + controller.error(err); + }, + ); }, - async cancel(e) { + cancel(e) { clearInterval(pingInterval); - await iterator.return?.(e); + if (iterator.return) { + return handleMaybePromise( + () => iterator.return?.(e), + () => {}, + ); + } }, }); return new fetchAPI.Response(readableStream, responseInit); diff --git a/packages/graphql-yoga/src/plugins/types.ts b/packages/graphql-yoga/src/plugins/types.ts index 2e957e8469..9937ece3b7 100644 --- a/packages/graphql-yoga/src/plugins/types.ts +++ b/packages/graphql-yoga/src/plugins/types.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { + Instruments as EnvelopInstruments, Plugin as EnvelopPlugin, OnExecuteHook, OnSubscribeHook, @@ -8,7 +9,12 @@ import { SetSchemaFn, } from '@envelop/core'; import { ExecutionResult } from '@graphql-tools/utils'; -import { ServerAdapterPlugin, type ServerAdapterInitialContext } from '@whatwg-node/server'; +import { MaybePromise } from '@whatwg-node/promise-helpers'; +import { + ServerAdapterPlugin, + type ServerAdapterInitialContext, + type Instruments as ServerAdapterInstruments, +} from '@whatwg-node/server'; import { YogaServer } from '../server.js'; import { FetchAPI, @@ -41,6 +47,11 @@ export type Plugin< */ onPluginInit?: OnPluginInitHook; } & { + /** + * A Tracer instance that will wrap each phases of the request pipeline. + * This should be used primarly as an observability tool (for monitoring, tracing, etc...). + */ + instruments?: Instruments; /** * This hook is invoked at Yoga Server initialization, before it starts. * Here you can setup long running resources (like monitoring or caching clients) @@ -81,6 +92,22 @@ export type Plugin< onResultProcess?: OnResultProcess; }; +export type Instruments> = EnvelopInstruments & + ServerAdapterInstruments & { + operation?: ( + payload: { context: TContext }, + wrapped: () => PromiseOrValue, + ) => PromiseOrValue; + requestParse?: ( + payload: { request: Request }, + wrapped: () => MaybePromise, + ) => MaybePromise; + resultProcess?: ( + payload: { request: Request }, + wrapped: () => MaybePromise, + ) => MaybePromise; + }; + export type OnYogaInitHook> = ( payload: OnYogaInitEventPayload, ) => void; diff --git a/packages/graphql-yoga/src/plugins/use-graphiql.ts b/packages/graphql-yoga/src/plugins/use-graphiql.ts index ed8a845e33..1ae502ddac 100644 --- a/packages/graphql-yoga/src/plugins/use-graphiql.ts +++ b/packages/graphql-yoga/src/plugins/use-graphiql.ts @@ -1,5 +1,6 @@ import { PromiseOrValue } from '@envelop/core'; import { YogaLogger } from '@graphql-yoga/logger'; +import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import graphiqlHTML from '../graphiql-html.js'; import { FetchAPI } from '../types.js'; import { Plugin } from './types.js'; @@ -181,7 +182,7 @@ export function useGraphiQL>( return urlPattern; }; return { - async onRequest({ request, serverContext, fetchAPI, endResponse, url }) { + onRequest({ request, serverContext, fetchAPI, endResponse, url }) { if ( shouldRenderGraphiQL(request) && (request.url.endsWith(config.graphqlEndpoint) || @@ -191,24 +192,28 @@ export function useGraphiQL>( getUrlPattern(fetchAPI).test(url)) ) { logger.debug(`Rendering GraphiQL`); - const graphiqlOptions = await graphiqlOptionsFactory( - request, - serverContext as TServerContext, + return handleMaybePromise( + () => graphiqlOptionsFactory(request, serverContext as TServerContext), + graphiqlOptions => { + if (graphiqlOptions) { + return handleMaybePromise( + () => + renderer({ + ...(graphiqlOptions === true ? {} : graphiqlOptions), + }), + graphiqlBody => { + const response = new fetchAPI.Response(graphiqlBody, { + headers: { + 'Content-Type': 'text/html', + }, + status: 200, + }); + endResponse(response); + }, + ); + } + }, ); - - if (graphiqlOptions) { - const graphiQLBody = await renderer({ - ...(graphiqlOptions === true ? {} : graphiqlOptions), - }); - - const response = new fetchAPI.Response(graphiQLBody, { - headers: { - 'Content-Type': 'text/html', - }, - status: 200, - }); - endResponse(response); - } } }, }; diff --git a/packages/graphql-yoga/src/plugins/use-readiness-check.ts b/packages/graphql-yoga/src/plugins/use-readiness-check.ts index 0b70b29d0d..7a096014e6 100644 --- a/packages/graphql-yoga/src/plugins/use-readiness-check.ts +++ b/packages/graphql-yoga/src/plugins/use-readiness-check.ts @@ -1,3 +1,4 @@ +import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { FetchAPI } from '../types.js'; import { Plugin } from './types.js'; @@ -39,26 +40,30 @@ export function useReadinessCheck({ onYogaInit({ yoga }) { urlPattern = new yoga.fetchAPI.URLPattern({ pathname: endpoint }); }, - async onRequest({ request, endResponse, fetchAPI, url }) { + onRequest({ request, endResponse, fetchAPI, url }) { if (request.url.endsWith(endpoint) || url.pathname === endpoint || urlPattern.test(url)) { - let response: Response; - try { - const readyOrResponse = await check({ request, fetchAPI }); - if (typeof readyOrResponse === 'object') { - response = readyOrResponse; - } else { - response = new fetchAPI.Response(null, { - status: readyOrResponse === false ? 503 : 200, + return handleMaybePromise( + () => check({ request, fetchAPI }), + readyOrResponse => { + let response: Response; + if (typeof readyOrResponse === 'object') { + response = readyOrResponse; + } else { + response = new fetchAPI.Response(null, { + status: readyOrResponse === false ? 503 : 200, + }); + } + endResponse(response); + }, + err => { + const isError = err instanceof Error; + const response = new fetchAPI.Response(isError ? err.message : null, { + status: 503, + headers: isError ? { 'content-type': 'text/plain; charset=utf-8' } : {}, }); - } - } catch (err) { - const isError = err instanceof Error; - response = new fetchAPI.Response(isError ? err.message : null, { - status: 503, - headers: isError ? { 'content-type': 'text/plain; charset=utf-8' } : {}, - }); - } - endResponse(response); + endResponse(response); + }, + ); } }, }; diff --git a/packages/graphql-yoga/src/plugins/use-schema.ts b/packages/graphql-yoga/src/plugins/use-schema.ts index 5c3f74082b..ccc1685225 100644 --- a/packages/graphql-yoga/src/plugins/use-schema.ts +++ b/packages/graphql-yoga/src/plugins/use-schema.ts @@ -1,5 +1,6 @@ import { GraphQLSchema, isSchema } from 'graphql'; import { PromiseOrValue } from '@envelop/core'; +import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import type { GraphQLSchemaWithContext, YogaInitialContext } from '../types.js'; import type { Plugin } from './types.js'; @@ -34,8 +35,15 @@ export const useSchema = < return { onRequestParse() { return { - async onRequestParseDone() { - schema ||= await schemaDef; + onRequestParseDone() { + if (!schema) { + return handleMaybePromise( + () => schemaDef, + schemaDef => { + schema = schemaDef; + }, + ); + } }, }; }, @@ -53,12 +61,17 @@ export const useSchema = < return { onRequestParse({ request, serverContext }) { return { - async onRequestParseDone() { - const schema = await schemaDef({ - ...(serverContext as TServerContext), - request, - }); - schemaByRequest.set(request, schema); + onRequestParseDone() { + return handleMaybePromise( + () => + schemaDef({ + ...(serverContext as TServerContext), + request, + }), + schemaDef => { + schemaByRequest.set(request, schemaDef); + }, + ); }, }; }, diff --git a/packages/graphql-yoga/src/process-request.ts b/packages/graphql-yoga/src/process-request.ts index cd3bb2e107..4e1bec96b1 100644 --- a/packages/graphql-yoga/src/process-request.ts +++ b/packages/graphql-yoga/src/process-request.ts @@ -1,11 +1,12 @@ import { getOperationAST } from 'graphql'; import { GetEnvelopedFn } from '@envelop/core'; import { ExecutionArgs } from '@graphql-tools/executor'; +import { handleMaybePromise, iterateAsync } from '@whatwg-node/promise-helpers'; import { ServerAdapterInitialContext } from '@whatwg-node/server'; import { OnResultProcess, ResultProcessor, ResultProcessorInput } from './plugins/types.js'; import { FetchAPI, GraphQLParams } from './types.js'; -export async function processResult({ +export function processResult({ request, result, fetchAPI, @@ -26,38 +27,42 @@ export async function processResult({ const acceptableMediaTypes: string[] = []; let acceptedMediaType = '*/*'; - for (const onResultProcessHook of onResultProcessHooks) { - await onResultProcessHook({ - request, - acceptableMediaTypes, - result, - setResult(newResult) { - result = newResult; - }, - resultProcessor, - setResultProcessor(newResultProcessor, newAcceptedMimeType) { - resultProcessor = newResultProcessor; - acceptedMediaType = newAcceptedMimeType; - }, - serverContext, - }); - } - - // If no result processor found for this result, return an error - if (!resultProcessor) { - return new fetchAPI.Response(null, { - status: 406, - statusText: 'Not Acceptable', - headers: { - accept: acceptableMediaTypes.join('; charset=utf-8, '), - }, - }); - } + return handleMaybePromise( + () => + iterateAsync(onResultProcessHooks, onResultProcessHook => + onResultProcessHook({ + request, + acceptableMediaTypes, + result, + setResult(newResult) { + result = newResult; + }, + resultProcessor, + setResultProcessor(newResultProcessor, newAcceptedMimeType) { + resultProcessor = newResultProcessor; + acceptedMediaType = newAcceptedMimeType; + }, + serverContext, + }), + ), + () => { + // If no result processor found for this result, return an error + if (!resultProcessor) { + return new fetchAPI.Response(null, { + status: 406, + statusText: 'Not Acceptable', + headers: { + accept: acceptableMediaTypes.join('; charset=utf-8, '), + }, + }); + } - return resultProcessor(result, fetchAPI, acceptedMediaType); + return resultProcessor(result, fetchAPI, acceptedMediaType); + }, + ); } -export async function processRequest({ +export function processRequest({ params, enveloped, }: { @@ -76,22 +81,26 @@ export async function processRequest({ } // Build the context for the execution - const contextValue = await enveloped.contextFactory(); - const executionArgs: ExecutionArgs = { - schema: enveloped.schema, - document, - contextValue, - variableValues: params.variables, - operationName: params.operationName, - }; + return handleMaybePromise( + () => enveloped.contextFactory(), + contextValue => { + const executionArgs: ExecutionArgs = { + schema: enveloped.schema, + document, + contextValue, + variableValues: params.variables, + operationName: params.operationName, + }; - // Get the actual operation - const operation = getOperationAST(document, params.operationName); + // Get the actual operation + const operation = getOperationAST(document, params.operationName); - // Choose the right executor - const executeFn = - operation?.operation === 'subscription' ? enveloped.subscribe : enveloped.execute; + // Choose the right executor + const executeFn = + operation?.operation === 'subscription' ? enveloped.subscribe : enveloped.execute; - // Get the result to be processed - return executeFn(executionArgs); + // Get the result to be processed + return executeFn(executionArgs); + }, + ); } diff --git a/packages/graphql-yoga/src/server.ts b/packages/graphql-yoga/src/server.ts index dca5b08498..8076ae526a 100644 --- a/packages/graphql-yoga/src/server.ts +++ b/packages/graphql-yoga/src/server.ts @@ -9,10 +9,17 @@ import { useExtendContext, useMaskedErrors, } from '@envelop/core'; +import { chain, getInstrumented } from '@envelop/instruments'; import { normalizedExecutor } from '@graphql-tools/executor'; import { mapAsyncIterator } from '@graphql-tools/utils'; import { createLogger, LogLevel, YogaLogger } from '@graphql-yoga/logger'; import * as defaultFetchAPI from '@whatwg-node/fetch'; +import { + handleMaybePromise, + iterateAsync, + iterateAsyncVoid, + MaybePromise, +} from '@whatwg-node/promise-helpers'; import { createServerAdapter, ServerAdapter, @@ -44,6 +51,7 @@ import { useHTTPValidationError } from './plugins/request-validation/use-http-va import { useLimitBatching } from './plugins/request-validation/use-limit-batching.js'; import { usePreventMutationViaGET } from './plugins/request-validation/use-prevent-mutation-via-get.js'; import { + Instruments, OnExecutionResultHook, OnParamsHook, OnRequestParseDoneHook, @@ -216,6 +224,7 @@ export class YogaServer< protected plugins: Array< Plugin >; + private instruments: Instruments | undefined; private onRequestParseHooks: OnRequestParseHook[]; private onParamsHooks: OnParamsHook[]; private onExecutionResultHooks: OnExecutionResultHook[]; @@ -459,61 +468,71 @@ export class YogaServer< if (plugin.onResultProcess) { this.onResultProcessHooks.push(plugin.onResultProcess); } + if (plugin.instruments) { + this.instruments = this.instruments + ? chain(this.instruments, plugin.instruments) + : plugin.instruments; + } } } } - handleParams: ParamsHandler = async ({ request, context, params }) => { - let result: ExecutionResult | AsyncIterable; - try { - const additionalContext = - context['request'] === request - ? { - params, - } - : { - request, - params, - }; + handleParams: ParamsHandler = ({ request, context, params }) => { + const additionalContext = + context['request'] === request + ? { + params, + } + : { + request, + params, + }; - Object.assign(context, additionalContext); + Object.assign(context, additionalContext); - const enveloped = this.getEnveloped(context); + const enveloped = this.getEnveloped(context); - this.logger.debug(`Processing GraphQL Parameters`); - result = await processGraphQLParams({ - params, - enveloped, - }); - this.logger.debug(`Processing GraphQL Parameters done.`); - } catch (error) { - const errors = handleError(error, this.maskedErrorsOpts, this.logger); + this.logger.debug(`Processing GraphQL Parameters`); + return handleMaybePromise( + () => + handleMaybePromise( + () => processGraphQLParams({ params, enveloped }), + result => { + this.logger.debug(`Processing GraphQL Parameters done.`); + return result; + }, + error => { + const errors = handleError(error, this.maskedErrorsOpts, this.logger); - result = { - errors, - }; - } - if (isAsyncIterable(result)) { - result = mapAsyncIterator( - result, - v => v, - (error: Error) => { - if (error.name === 'AbortError') { - this.logger.debug(`Request aborted`); - throw error; - } + return { + errors, + }; + }, + ), + result => { + if (isAsyncIterable(result)) { + result = mapAsyncIterator( + result, + v => v, + (error: Error) => { + if (error.name === 'AbortError') { + this.logger.debug(`Request aborted`); + throw error; + } - const errors = handleError(error, this.maskedErrorsOpts, this.logger); - return { - errors, - }; - }, - ); - } - return result; + const errors = handleError(error, this.maskedErrorsOpts, this.logger); + return { + errors, + }; + }, + ); + } + return result; + }, + ); }; - async getResultForParams( + getResultForParams = ( { params, request, @@ -522,53 +541,70 @@ export class YogaServer< request: Request; }, context: TServerContext, - ) { + ) => { let result: ExecutionResult | AsyncIterable | undefined; let paramsHandler = this.handleParams; - for (const onParamsHook of this.onParamsHooks) { - await onParamsHook({ - params, - request, - setParams(newParams) { - params = newParams; - }, - paramsHandler, - setParamsHandler(newHandler) { - paramsHandler = newHandler; - }, - setResult(newResult) { - result = newResult; - }, - fetchAPI: this.fetchAPI, - context, - }); - } - - result ??= await paramsHandler({ - request, - params, - context: context as TServerContext & YogaInitialContext, - }); - - for (const onExecutionResult of this.onExecutionResultHooks) { - await onExecutionResult({ - result, - setResult(newResult) { - result = newResult; - }, - request, - context: context as TServerContext & YogaInitialContext, - }); - } - - return result; - } + return handleMaybePromise( + () => + iterateAsync(this.onParamsHooks, onParamsHook => + onParamsHook({ + params, + request, + setParams(newParams) { + params = newParams; + }, + paramsHandler, + setParamsHandler(newHandler) { + paramsHandler = newHandler; + }, + setResult(newResult) { + result = newResult; + }, + fetchAPI: this.fetchAPI, + context, + }), + ), + () => + handleMaybePromise( + () => + result || + paramsHandler({ + request, + params, + context: context as TServerContext & YogaInitialContext, + }), + result => + handleMaybePromise( + () => + iterateAsync(this.onExecutionResultHooks, onExecutionResult => + onExecutionResult({ + result, + setResult(newResult) { + result = newResult; + }, + request, + context: context as TServerContext & YogaInitialContext, + }), + ), + () => result, + ), + ), + ); + }; - handle: ServerAdapterRequestHandler = async ( + parseRequest = ( request: Request, serverContext: TServerContext & ServerAdapterInitialContext, - ) => { + ): MaybePromise< + | { + requestParserResult: + | GraphQLParams, Record> + | GraphQLParams, Record>[]; + response?: never; + } + | { requestParserResult?: never; response: Response } + > => { let url = new Proxy({} as URL, { get: (_target, prop, _receiver) => { url = new this.fetchAPI.URL(request.url, 'http://localhost'); @@ -578,68 +614,125 @@ export class YogaServer< let requestParser: RequestParser | undefined; const onRequestParseDoneList: OnRequestParseDoneHook[] = []; - for (const onRequestParse of this.onRequestParseHooks) { - const onRequestParseResult = await onRequestParse({ - request, - url, - requestParser, - serverContext, - setRequestParser(parser: RequestParser) { - requestParser = parser; - }, - }); - if (onRequestParseResult?.onRequestParseDone != null) { - onRequestParseDoneList.push(onRequestParseResult.onRequestParseDone); - } - } - this.logger.debug(`Parsing request to extract GraphQL parameters`); + return handleMaybePromise( + () => + iterateAsync( + this.onRequestParseHooks, + onRequestParse => + handleMaybePromise( + () => + onRequestParse({ + request, + url, + requestParser, + serverContext, + setRequestParser(parser: RequestParser) { + requestParser = parser; + }, + }), + requestParseHookResult => requestParseHookResult?.onRequestParseDone, + ), + onRequestParseDoneList, + ), + () => { + this.logger.debug(`Parsing request to extract GraphQL parameters`); - if (!requestParser) { - return new this.fetchAPI.Response(null, { - status: 415, - statusText: 'Unsupported Media Type', - }); - } + if (!requestParser) { + return { + response: new this.fetchAPI.Response(null, { + status: 415, + statusText: 'Unsupported Media Type', + }), + }; + } - let requestParserResult = await requestParser(request); + return handleMaybePromise( + () => requestParser!(request), + requestParserResult => { + return handleMaybePromise( + () => + iterateAsyncVoid(onRequestParseDoneList, onRequestParseDone => + onRequestParseDone({ + requestParserResult, + setRequestParserResult(newParams: GraphQLParams | GraphQLParams[]) { + requestParserResult = newParams; + }, + }), + ), + () => ({ + requestParserResult, + }), + ); + }, + ); + }, + ); + }; - for (const onRequestParseDone of onRequestParseDoneList) { - await onRequestParseDone({ - requestParserResult, - setRequestParserResult(newParams: GraphQLParams | GraphQLParams[]) { - requestParserResult = newParams; - }, - }); - } + handle: ServerAdapterRequestHandler = ( + request: Request, + serverContext: TServerContext & ServerAdapterInitialContext, + ) => { + const instrumented = this.instruments && getInstrumented({ request }); - const result = (await (Array.isArray(requestParserResult) - ? Promise.all( - requestParserResult.map(params => - this.getResultForParams( - { - params, - request, - }, - Object.create(serverContext), - ), - ), - ) - : this.getResultForParams( - { - params: requestParserResult, - request, - }, - serverContext, - ))) as ResultProcessorInput; + const parseRequest = this.instruments?.requestParse + ? instrumented!.asyncFn(this.instruments?.requestParse, this.parseRequest) + : this.parseRequest; - return processResult({ - request, - result, - fetchAPI: this.fetchAPI, - onResultProcessHooks: this.onResultProcessHooks, - serverContext, - }); + return handleMaybePromise( + () => parseRequest(request, serverContext), + ({ response, requestParserResult }) => { + if (response) { + return response; + } + const getResultForParams = this.instruments?.operation + ? (payload: { request: Request; params: GraphQLParams }, context: any) => { + const instrumented = getInstrumented({ context }); + const tracedHandler = instrumented.asyncFn( + this.instruments?.operation, + this.getResultForParams, + ); + return tracedHandler(payload, context); + } + : this.getResultForParams; + return handleMaybePromise( + () => + (Array.isArray(requestParserResult) + ? Promise.all( + requestParserResult.map(params => + getResultForParams( + { + params, + request, + }, + Object.create(serverContext), + ), + ), + ) + : getResultForParams( + { + params: requestParserResult, + request, + }, + serverContext, + )) as ResultProcessorInput, + result => { + const tracedProcessResult = this.instruments?.resultProcess + ? instrumented!.asyncFn(this.instruments.resultProcess, processResult) + : processResult; + + return tracedProcessResult({ + request, + result, + fetchAPI: this.fetchAPI, + onResultProcessHooks: this.onResultProcessHooks, + serverContext, + }); + }, + ); + }, + ); }; } diff --git a/packages/nestjs-federation/package.json b/packages/nestjs-federation/package.json index be8e1f49be..132e34f014 100644 --- a/packages/nestjs-federation/package.json +++ b/packages/nestjs-federation/package.json @@ -55,8 +55,8 @@ "dependencies": { "@apollo/gateway": "^2.4.0", "@apollo/subgraph": "^2.4.0", - "@envelop/apollo-federation": "^6.0.0", - "@envelop/core": "^5.0.0", + "@envelop/apollo-federation": "^6.1.1", + "@envelop/core": "^5.2.1", "@graphql-yoga/nestjs": "workspace:*", "@graphql-yoga/plugin-apollo-inline-trace": "workspace:*" }, diff --git a/packages/plugins/apollo-inline-trace/package.json b/packages/plugins/apollo-inline-trace/package.json index f75c999f7c..bee97f3426 100644 --- a/packages/plugins/apollo-inline-trace/package.json +++ b/packages/plugins/apollo-inline-trace/package.json @@ -42,13 +42,13 @@ }, "dependencies": { "@apollo/usage-reporting-protobuf": "^4.1.1", - "@envelop/on-resolve": "^5.0.0", + "@envelop/on-resolve": "^5.1.1", "tslib": "^2.8.1" }, "devDependencies": { "@apollo/gateway": "^2.9.3", "@apollo/subgraph": "^2.9.3", - "@envelop/core": "^5.0.2", + "@envelop/core": "^5.2.1", "@graphql-tools/delegate": "^10.1.1", "@graphql-tools/federation": "^3.0.0", "@whatwg-node/fetch": "^0.10.1", diff --git a/packages/plugins/apollo-usage-report/package.json b/packages/plugins/apollo-usage-report/package.json index f5d74aa97d..3bfec78335 100644 --- a/packages/plugins/apollo-usage-report/package.json +++ b/packages/plugins/apollo-usage-report/package.json @@ -46,7 +46,7 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@envelop/on-resolve": "^5.0.0", + "@envelop/on-resolve": "^5.1.1", "@whatwg-node/fetch": "^0.10.1", "graphql": "16.10.0", "graphql-yoga": "workspace:*" diff --git a/packages/plugins/csrf-prevention/src/index.ts b/packages/plugins/csrf-prevention/src/index.ts index 99a4eff84c..6a066c0f29 100644 --- a/packages/plugins/csrf-prevention/src/index.ts +++ b/packages/plugins/csrf-prevention/src/index.ts @@ -31,7 +31,7 @@ export function useCSRFPrevention( ): Plugin { const { requestHeaders = ['x-graphql-yoga-csrf'] } = options; return { - async onRequestParse({ request }) { + onRequestParse({ request }) { if (wasTheRequestAlreadyPreflightChecked(request.headers?.get('content-type'))) { return; } diff --git a/packages/plugins/disable-introspection/package.json b/packages/plugins/disable-introspection/package.json index be841abc8f..008cea9b23 100644 --- a/packages/plugins/disable-introspection/package.json +++ b/packages/plugins/disable-introspection/package.json @@ -40,6 +40,9 @@ "graphql": "^15.2.0 || ^16.0.0", "graphql-yoga": "workspace:^" }, + "dependencies": { + "@whatwg-node/promise-helpers": "^1.2.4" + }, "devDependencies": { "graphql": "16.10.0", "graphql-yoga": "workspace:*" diff --git a/packages/plugins/disable-introspection/src/index.ts b/packages/plugins/disable-introspection/src/index.ts index 74077d5865..9533112bed 100644 --- a/packages/plugins/disable-introspection/src/index.ts +++ b/packages/plugins/disable-introspection/src/index.ts @@ -1,5 +1,6 @@ import { NoSchemaIntrospectionCustomRule } from 'graphql'; import type { Plugin, PromiseOrValue } from 'graphql-yoga'; +import { handleMaybePromise } from '@whatwg-node/promise-helpers'; type UseDisableIntrospectionArgs = { isDisabled?: (request: Request, context: Record) => PromiseOrValue; @@ -8,14 +9,20 @@ type UseDisableIntrospectionArgs = { export function useDisableIntrospection>( props?: UseDisableIntrospectionArgs, ): Plugin { - const store = new WeakMap(); + const disabledIntrospection = new WeakSet(); return { - async onRequestParse({ request, serverContext }) { - const isDisabled = props?.isDisabled ? await props.isDisabled(request, serverContext) : true; - store.set(request, isDisabled); + onRequestParse({ request, serverContext }) { + return handleMaybePromise( + () => (props?.isDisabled ? props.isDisabled(request, serverContext) : true), + result => { + if (result) { + disabledIntrospection.add(request); + } + }, + ); }, onValidate({ addValidationRule, context }) { - const isDisabled = store.get(context.request) ?? true; + const isDisabled = disabledIntrospection.has(context.request); if (isDisabled) { addValidationRule(NoSchemaIntrospectionCustomRule); } diff --git a/packages/plugins/prometheus/package.json b/packages/plugins/prometheus/package.json index 623e45ab64..2bbccabf59 100644 --- a/packages/plugins/prometheus/package.json +++ b/packages/plugins/prometheus/package.json @@ -42,7 +42,7 @@ "prom-client": "^15.0.0" }, "dependencies": { - "@envelop/prometheus": "^12.0.0" + "@envelop/prometheus": "^12.1.1" }, "devDependencies": { "@types/ws": "^8.5.13", diff --git a/packages/plugins/response-cache/package.json b/packages/plugins/response-cache/package.json index 760b2c1167..a2f4886a2d 100644 --- a/packages/plugins/response-cache/package.json +++ b/packages/plugins/response-cache/package.json @@ -47,8 +47,8 @@ "graphql-yoga": "workspace:^" }, "dependencies": { - "@envelop/core": "^5.0.2", - "@envelop/response-cache": "^7.0.0" + "@envelop/core": "^5.2.1", + "@envelop/response-cache": "^7.1.1" }, "devDependencies": { "graphql": "16.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ea27f0cf0..4a79a52b34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,6 @@ settings: overrides: graphql: 16.10.0 - '@envelop/core': 5.2.1 '@changesets/assemble-release-plan': 5.2.3 '@types/react': 19.0.10 @@ -78,10 +77,10 @@ importers: version: 6.0.2 '@typescript-eslint/eslint-plugin': specifier: ^8.18.0 - version: 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) + version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) '@typescript-eslint/parser': specifier: ^8.18.0 - version: 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) + version: 8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) babel-jest: specifier: ^29.0.0 version: 29.7.0(@babel/core@7.26.9) @@ -363,7 +362,7 @@ importers: dependencies: '@cloudflare/workers-types': specifier: ^4.0.0 - version: 4.20250224.0 + version: 4.20250303.0 graphql: specifier: 16.10.0 version: 16.10.0 @@ -382,7 +381,7 @@ importers: version: 5.98.0(@swc/core@1.11.5(@swc/helpers@0.5.15))(esbuild@0.17.19) wrangler: specifier: 3.112.0 - version: 3.112.0(@cloudflare/workers-types@4.20250224.0) + version: 3.112.0(@cloudflare/workers-types@4.20250303.0) examples/cloudflare-modules: dependencies: @@ -558,7 +557,7 @@ importers: devDependencies: '@graphql-tools/utils': specifier: ^10.6.1 - version: 10.8.3(graphql@16.10.0) + version: 10.8.4(graphql@16.10.0) '@types/node': specifier: 22.13.9 version: 22.13.9 @@ -761,7 +760,7 @@ importers: devDependencies: '@types/ws': specifier: ^8.5.13 - version: 8.5.14 + version: 8.18.0 cross-env: specifier: 7.0.3 version: 7.0.3 @@ -1463,7 +1462,7 @@ importers: dependencies: '@types/node': specifier: ^22.0.0 - version: 22.13.8 + version: 22.13.9 graphql: specifier: 16.10.0 version: 16.10.0 @@ -1475,7 +1474,7 @@ importers: version: link:../../packages/graphql-yoga/dist ts-node-dev: specifier: ^2.0.0 - version: 2.0.0(@swc/core@1.11.5(@swc/helpers@0.5.15))(@types/node@22.13.8)(typescript@5.8.2) + version: 2.0.0(@swc/core@1.11.5(@swc/helpers@0.5.15))(@types/node@22.13.9)(typescript@5.8.2) typescript: specifier: ^5.0.0 version: 5.8.2 @@ -1621,8 +1620,11 @@ importers: packages/graphql-yoga: dependencies: '@envelop/core': - specifier: 5.2.1 + specifier: ^5.2.1 version: 5.2.1 + '@envelop/instruments': + specifier: ^1.0.0 + version: 1.0.0 '@graphql-tools/executor': specifier: ^1.4.0 version: 1.4.2(graphql@16.10.0) @@ -1631,7 +1633,7 @@ importers: version: 10.0.20(graphql@16.10.0) '@graphql-tools/utils': specifier: ^10.6.2 - version: 10.8.3(graphql@16.10.0) + version: 10.8.4(graphql@16.10.0) '@graphql-yoga/logger': specifier: workspace:^ version: link:../logger/dist @@ -1641,6 +1643,9 @@ importers: '@whatwg-node/fetch': specifier: ^0.10.5 version: 0.10.5 + '@whatwg-node/promise-helpers': + specifier: ^1.2.4 + version: 1.2.4 '@whatwg-node/server': specifier: ^0.10.0 version: 0.10.0 @@ -1748,7 +1753,7 @@ importers: version: 8.1.0 '@types/ws': specifier: ^8.5.4 - version: 8.5.14 + version: 8.18.0 '@whatwg-node/fetch': specifier: ^0.10.1 version: 0.10.5 @@ -1801,10 +1806,10 @@ importers: specifier: ^2.4.0 version: 2.10.0(graphql@16.10.0) '@envelop/apollo-federation': - specifier: ^6.0.0 - version: 6.0.1(@envelop/core@5.2.1)(graphql@16.10.0) + specifier: ^6.1.1 + version: 6.1.1(@envelop/core@5.2.1)(graphql@16.10.0) '@envelop/core': - specifier: 5.2.1 + specifier: ^5.2.1 version: 5.2.1 '@graphql-yoga/nestjs': specifier: workspace:* @@ -1835,8 +1840,8 @@ importers: specifier: ^4.1.1 version: 4.1.1 '@envelop/on-resolve': - specifier: ^5.0.0 - version: 5.0.1(@envelop/core@5.2.1)(graphql@16.10.0) + specifier: ^5.1.1 + version: 5.1.1(@envelop/core@5.2.1)(graphql@16.10.0) tslib: specifier: ^2.8.1 version: 2.8.1 @@ -1848,7 +1853,7 @@ importers: specifier: ^2.9.3 version: 2.10.0(graphql@16.10.0) '@envelop/core': - specifier: 5.2.1 + specifier: ^5.2.1 version: 5.2.1 '@graphql-tools/delegate': specifier: ^10.1.1 @@ -1909,8 +1914,8 @@ importers: version: 2.8.1 devDependencies: '@envelop/on-resolve': - specifier: ^5.0.0 - version: 5.0.1(@envelop/core@5.2.1)(graphql@16.10.0) + specifier: ^5.1.1 + version: 5.1.1(@envelop/core@5.2.1)(graphql@16.10.0) '@whatwg-node/fetch': specifier: ^0.10.1 version: 0.10.5 @@ -1949,7 +1954,7 @@ importers: dependencies: '@graphql-tools/utils': specifier: ^10.6.1 - version: 10.8.3(graphql@16.10.0) + version: 10.8.4(graphql@16.10.0) devDependencies: '@graphql-tools/executor-http': specifier: ^1.1.9 @@ -1975,6 +1980,10 @@ importers: publishDirectory: dist packages/plugins/disable-introspection: + dependencies: + '@whatwg-node/promise-helpers': + specifier: ^1.2.4 + version: 1.2.4 devDependencies: graphql: specifier: 16.10.0 @@ -2021,7 +2030,7 @@ importers: version: 9.0.9 '@types/ws': specifier: ^8.5.13 - version: 8.5.14 + version: 8.18.0 graphql: specifier: 16.10.0 version: 16.10.0 @@ -2052,15 +2061,15 @@ importers: packages/plugins/prometheus: dependencies: '@envelop/prometheus': - specifier: ^12.0.0 - version: 12.0.1(@envelop/core@5.2.1)(graphql@16.10.0)(prom-client@15.1.3) + specifier: ^12.1.1 + version: 12.1.1(@envelop/core@5.2.1)(graphql@16.10.0)(prom-client@15.1.3) graphql: specifier: 16.10.0 version: 16.10.0 devDependencies: '@types/ws': specifier: ^8.5.13 - version: 8.5.14 + version: 8.18.0 graphql-ws: specifier: ^6.0.0 version: 6.0.4(graphql@16.10.0)(uWebSockets.js@https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/6609a88ffa9a16ac5158046761356ce03250a0df)(ws@8.18.1) @@ -2078,11 +2087,11 @@ importers: packages/plugins/response-cache: dependencies: '@envelop/core': - specifier: 5.2.1 + specifier: ^5.2.1 version: 5.2.1 '@envelop/response-cache': - specifier: ^7.0.0 - version: 7.0.1(@envelop/core@5.2.1)(graphql@16.10.0) + specifier: ^7.1.1 + version: 7.1.1(@envelop/core@5.2.1)(graphql@16.10.0) devDependencies: graphql: specifier: 16.10.0 @@ -2099,7 +2108,7 @@ importers: dependencies: '@graphql-tools/utils': specifier: ^10.3.2 - version: 10.8.3(graphql@16.10.0) + version: 10.8.4(graphql@16.10.0) graphql: specifier: 16.10.0 version: 16.10.0 @@ -3901,9 +3910,6 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20250224.0': - resolution: {integrity: sha512-j6ZwQ5G2moQRaEtGI2u5TBQhVXv/XwOS5jfBAheZHcpCM07zm8j0i8jZHHLq/6VA8e6VRjKohOyj5j6tZ1KHLQ==} - '@cloudflare/workers-types@4.20250303.0': resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==} @@ -3974,18 +3980,11 @@ packages: '@emotion/memoize@0.7.4': resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} - '@envelop/apollo-federation@6.0.1': - resolution: {integrity: sha512-MqjbO1M973VuV9QrloqMMNXxojrN+5F2zOR8mLXOpJ5QUKnXHt+ZVH7O0CH/pVDdKm7GVCcVEfQyew2Bw9zvQQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@envelop/core': 5.2.1 - graphql: 16.10.0 - '@envelop/apollo-federation@6.1.1': resolution: {integrity: sha512-j1qQachKv5cQ01XD1ZKTBkxOCNhCscGiLPaniWUTgUa+Bb14rNAaBQuAqmdUkkw6H73gvC5O6yZug92S0vnxUg==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 '@envelop/core@5.2.1': @@ -3996,35 +3995,35 @@ packages: resolution: {integrity: sha512-PMz7qFxHFLOOzWArEApFJFRtuPsjGJI2XjZBoOc8D9V0+XCH525S7Tw8ILCAUfHc8ogUO38wyx7omZp1dBYg0g==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 '@envelop/extended-validation@5.1.1': resolution: {integrity: sha512-AjVzgLOZn5xSC9+y5du8K0p/GYrPPnrtlCJN3dDJapbdKjf8AuWylZwg9/+OAiN3/UPC9+8wcbwkvXsMr8zViA==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 '@envelop/generic-auth@9.1.1': resolution: {integrity: sha512-ikx8SbiomiBdYWPAX4aQV4pC3BzcOHzr6TRIKCkW+CGj9z688akqsgmkUsTCZMowhCKpd2YBR8YlccoQlDr15Q==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 '@envelop/graphql-jit@9.1.1': resolution: {integrity: sha512-SYKHqwOme4qbQF+A0g0yTkZa/AwLWgwLyCD7OjtamwHXmAGiI/QziLL7sxWYITovdCk3vk5JmFgLbs/wPtJiTQ==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 '@envelop/graphql-modules@7.1.1': resolution: {integrity: sha512-rjRpL71PQ7YFsQWVLj3CzhWqrIL7jR1eCFby20enicblMNywn/wwqmTqLfzerzHgKJFS7RwUyOAKhjHpC9FcoQ==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 graphql-modules: ^1 || ^2.0.0 || ^3.0.0 @@ -4036,29 +4035,29 @@ packages: resolution: {integrity: sha512-XViuCZvWXJ5DkJx7xaehUaf7geH2DQnBcaIkdxOTX3A0cpuKRgifo3oO/POwOgBIX4yxCKW1ZzqQEJCVFk183Q==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 - '@envelop/on-resolve@5.0.1': - resolution: {integrity: sha512-ubYfdwtjD2FerYKmpg2FuyHQkRD1v6YWuu+j+IsIVfTJZtwHBnoX2G0iYSKMrJCbJfKJn4QwdjYnnacR7nVB0w==} + '@envelop/on-resolve@5.1.1': + resolution: {integrity: sha512-JuRgGClc8ABnndPm65Yq73G7Zp3ZPO0n+/ZLjpA9kzdhcum5T0cnJzjOmQOyX6UXeejFctaed6mylYLtbAUrjg==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 - '@envelop/prometheus@12.0.1': - resolution: {integrity: sha512-52HDsNivABVJFDoioQmLXQZ+RXhpktOwMsNojpfDwvJLTj6BfG5CWfiYR6maBQbmm78YfZQylw89kh0ZonbGTw==} + '@envelop/prometheus@12.1.1': + resolution: {integrity: sha512-9WMlJzQKvCvp2ZMwTL3spJOkZMUPngyg0w+9jirx9IYnKmgC0rz9ShbfOL++RT2AI+ZLYWRhA8Q61fjzGd5X1Q==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 prom-client: ^15.0.0 - '@envelop/response-cache@7.0.1': - resolution: {integrity: sha512-esowXKZ8AfFRXQL14F4CYt6rmF53Meg+1ynnbWtFJXGYHwoRzzbVsqqcIhdM4kU5CQlw5Buo1KWme3iGwlQh8w==} + '@envelop/response-cache@7.1.1': + resolution: {integrity: sha512-V4xIT2GdB31DiJezmoeVkEiWfQs3eaIO7qSOFnCYMwDoF3yFy2qd8WUoCLkMZLRQYGsWxyZO5Pz+Qv5otc9+Lg==} engines: {node: '>=18.0.0'} peerDependencies: - '@envelop/core': 5.2.1 + '@envelop/core': ^5.2.1 graphql: 16.10.0 '@envelop/types@5.2.0': @@ -4665,7 +4664,7 @@ packages: engines: {node: '>=18.0.0'} peerDependencies: '@apollo/server': ^4.0.0 - '@envelop/core': 5.2.1 + '@envelop/core': ^5.0.0 '@escape.tech/graphql-armor-types': 0.7.0 peerDependenciesMeta: '@apollo/server': @@ -5098,12 +5097,6 @@ packages: peerDependencies: graphql: 16.10.0 - '@graphql-tools/utils@10.8.3': - resolution: {integrity: sha512-4QCvx3SWRsbH7wnktl51mBek+zE9hsjsv796XVlJlOUdWpAghJmA3ID2P7/Vwuy7BivVNfuAKe4ucUdE1fG7vA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: 16.10.0 - '@graphql-tools/utils@10.8.4': resolution: {integrity: sha512-HpHBgcmLIE79jWk1v5Bm0Eb8MaPiwSJT/Iy5xIJ+GMe7yAKpCYrbjf7wb+UMDMkLkfEryvo3syCx8k+TMAZ9bA==} engines: {node: '>=16.0.0'} @@ -7994,9 +7987,6 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.13.8': - resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==} - '@types/node@22.13.9': resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} @@ -8089,9 +8079,6 @@ packages: '@types/ws@8.18.0': resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} - '@types/ws@8.5.14': - resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} - '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -8115,14 +8102,6 @@ packages: typescript: optional: true - '@typescript-eslint/eslint-plugin@8.25.0': - resolution: {integrity: sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/eslint-plugin@8.26.0': resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8141,13 +8120,6 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.25.0': - resolution: {integrity: sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/parser@8.26.0': resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8159,10 +8131,6 @@ packages: resolution: {integrity: sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.25.0': - resolution: {integrity: sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.26.0': resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8177,13 +8145,6 @@ packages: typescript: optional: true - '@typescript-eslint/type-utils@8.25.0': - resolution: {integrity: sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/type-utils@8.26.0': resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8199,10 +8160,6 @@ packages: resolution: {integrity: sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.25.0': - resolution: {integrity: sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.26.0': resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8225,12 +8182,6 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.25.0': - resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/typescript-estree@8.26.0': resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8247,13 +8198,6 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.25.0': - resolution: {integrity: sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/utils@8.26.0': resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8269,10 +8213,6 @@ packages: resolution: {integrity: sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.25.0': - resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.26.0': resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8368,12 +8308,8 @@ packages: resolution: {integrity: sha512-ec9ZPDImceXD9gShv0VTc6q0waZ7ccpiYXNbAeGMjGQAZ8hkAeAYOXoiJsfaHO5Pt0UR+SbNVTJGP2aeFMYz0Q==} engines: {node: '>=18.0.0'} - '@whatwg-node/promise-helpers@1.2.2': - resolution: {integrity: sha512-aPVTGCs/QEYkSTnYcLKE1wyYZykbGjaXsEwXHc0FKbSlojIpdw72BQMJx9aJXzkCs6qy9WfDV0jhV9C2qIYYOA==} - engines: {node: '>=16.0.0'} - - '@whatwg-node/promise-helpers@1.2.3': - resolution: {integrity: sha512-G2eyO3BwOolVHO0AGBHG/MQHqI/mBATsuoRpIBxSr/VrEz5clA8/O9WxOckt55FVrLcFfxfHY86h8t8bEKvhAw==} + '@whatwg-node/promise-helpers@1.2.4': + resolution: {integrity: sha512-daEUfaHbaMuAcor+FPAVK+pOCSzsAYhK6LN1y81EcakdqQEPQvjm74PTmfwfv8POg8pw4RyCv9LXB1e+mQDwqg==} engines: {node: '>=16.0.0'} '@whatwg-node/server-plugin-cookies@1.0.4': @@ -20253,8 +20189,6 @@ snapshots: '@cloudflare/workerd-windows-64@1.20250214.0': optional: true - '@cloudflare/workers-types@4.20250224.0': {} - '@cloudflare/workers-types@4.20250303.0': {} '@codemirror/language@6.0.0': @@ -20357,13 +20291,6 @@ snapshots: '@emotion/memoize@0.7.4': optional: true - '@envelop/apollo-federation@6.0.1(@envelop/core@5.2.1)(graphql@16.10.0)': - dependencies: - '@apollo/utils.keyvaluecache': 3.1.0 - '@envelop/core': 5.2.1 - graphql: 16.10.0 - tslib: 2.8.1 - '@envelop/apollo-federation@6.1.1(@envelop/core@5.2.1)(graphql@16.10.0)': dependencies: '@apollo/utils.keyvaluecache': 3.1.0 @@ -20375,7 +20302,7 @@ snapshots: dependencies: '@envelop/instruments': 1.0.0 '@envelop/types': 5.2.0 - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 tslib: 2.8.1 '@envelop/disable-introspection@7.1.1(@envelop/core@5.2.1)(graphql@16.10.0)': @@ -20397,14 +20324,14 @@ snapshots: '@envelop/extended-validation': 5.1.1(@envelop/core@5.2.1)(graphql@16.10.0) '@graphql-tools/executor': 1.4.2(graphql@16.10.0) '@graphql-tools/utils': 10.8.4(graphql@16.10.0) - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 graphql: 16.10.0 tslib: 2.8.1 '@envelop/graphql-jit@9.1.1(@envelop/core@5.2.1)(graphql@16.10.0)': dependencies: '@envelop/core': 5.2.1 - '@whatwg-node/promise-helpers': 1.2.3 + '@whatwg-node/promise-helpers': 1.2.4 graphql: 16.10.0 graphql-jit: 0.8.7(graphql@16.10.0) tslib: 2.8.1 @@ -20418,7 +20345,7 @@ snapshots: '@envelop/instruments@1.0.0': dependencies: - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 tslib: 2.8.1 '@envelop/live-query@8.1.1(@envelop/core@5.2.1)(graphql@16.10.0)': @@ -20431,21 +20358,21 @@ snapshots: graphql: 16.10.0 tslib: 2.8.1 - '@envelop/on-resolve@5.0.1(@envelop/core@5.2.1)(graphql@16.10.0)': + '@envelop/on-resolve@5.1.1(@envelop/core@5.2.1)(graphql@16.10.0)': dependencies: '@envelop/core': 5.2.1 - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 graphql: 16.10.0 - '@envelop/prometheus@12.0.1(@envelop/core@5.2.1)(graphql@16.10.0)(prom-client@15.1.3)': + '@envelop/prometheus@12.1.1(@envelop/core@5.2.1)(graphql@16.10.0)(prom-client@15.1.3)': dependencies: '@envelop/core': 5.2.1 - '@envelop/on-resolve': 5.0.1(@envelop/core@5.2.1)(graphql@16.10.0) + '@envelop/on-resolve': 5.1.1(@envelop/core@5.2.1)(graphql@16.10.0) graphql: 16.10.0 prom-client: 15.1.3 tslib: 2.8.1 - '@envelop/response-cache@7.0.1(@envelop/core@5.2.1)(graphql@16.10.0)': + '@envelop/response-cache@7.1.1(@envelop/core@5.2.1)(graphql@16.10.0)': dependencies: '@envelop/core': 5.2.1 '@graphql-tools/utils': 10.8.4(graphql@16.10.0) @@ -20457,7 +20384,7 @@ snapshots: '@envelop/types@5.2.0': dependencies: - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 tslib: 2.8.1 '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': @@ -21042,7 +20969,7 @@ snapshots: '@graphql-tools/load': 8.0.16(graphql@16.10.0) '@graphql-tools/prisma-loader': 8.0.17(@types/node@22.13.9)(encoding@0.1.13)(graphql@16.10.0) '@graphql-tools/url-loader': 8.0.28(@types/node@22.13.9)(graphql@16.10.0) - '@graphql-tools/utils': 10.8.3(graphql@16.10.0) + '@graphql-tools/utils': 10.8.4(graphql@16.10.0) '@whatwg-node/fetch': 0.10.5 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.8.2) @@ -21159,7 +21086,7 @@ snapshots: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) '@graphql-codegen/typescript': 4.1.5(encoding@0.1.13)(graphql@16.10.0) '@graphql-codegen/visitor-plugin-common': 5.7.1(encoding@0.1.13)(graphql@16.10.0) - '@graphql-tools/utils': 10.8.3(graphql@16.10.0) + '@graphql-tools/utils': 10.8.4(graphql@16.10.0) auto-bind: 4.0.0 graphql: 16.10.0 tslib: 2.6.3 @@ -21315,7 +21242,7 @@ snapshots: '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) '@repeaterjs/repeater': 3.0.6 '@whatwg-node/disposablestack': 0.0.6 - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 graphql: 16.10.0 tslib: 2.8.1 @@ -21355,7 +21282,7 @@ snapshots: '@graphql-tools/graphql-tag-pluck': 8.3.16(graphql@16.10.0) '@graphql-tools/utils': 10.8.4(graphql@16.10.0) '@whatwg-node/fetch': 0.10.5 - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 graphql: 16.10.0 sync-fetch: 0.6.0-2 tslib: 2.8.1 @@ -21471,7 +21398,7 @@ snapshots: '@graphql-tools/schema@10.0.18(graphql@16.10.0)': dependencies: - '@graphql-tools/merge': 9.0.19(graphql@16.10.0) + '@graphql-tools/merge': 9.0.21(graphql@16.10.0) '@graphql-tools/utils': 10.8.4(graphql@16.10.0) graphql: 16.10.0 tslib: 2.8.1 @@ -21504,7 +21431,7 @@ snapshots: '@graphql-tools/wrap': 10.0.31(graphql@16.10.0) '@types/ws': 8.18.0 '@whatwg-node/fetch': 0.10.5 - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 graphql: 16.10.0 isomorphic-ws: 5.0.0(ws@8.18.1) sync-fetch: 0.6.0-2 @@ -21525,19 +21452,10 @@ snapshots: graphql: 16.10.0 tslib: 2.8.1 - '@graphql-tools/utils@10.8.3(graphql@16.10.0)': - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) - '@whatwg-node/promise-helpers': 1.2.2 - cross-inspect: 1.0.1 - dset: 3.1.4 - graphql: 16.10.0 - tslib: 2.8.1 - '@graphql-tools/utils@10.8.4(graphql@16.10.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 cross-inspect: 1.0.1 dset: 3.1.4 graphql: 16.10.0 @@ -24763,7 +24681,7 @@ snapshots: eslint: 9.21.0(jiti@2.4.2) eslint-config-prettier: 9.1.0(eslint@9.21.0(jiti@2.4.2)) eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-jsonc: 2.18.2(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-mdx: 3.1.5(eslint@9.21.0(jiti@2.4.2)) @@ -25219,10 +25137,6 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.13.8': - dependencies: - undici-types: 6.20.0 - '@types/node@22.13.9': dependencies: undici-types: 6.20.0 @@ -25317,10 +25231,6 @@ snapshots: dependencies: '@types/node': 22.13.9 - '@types/ws@8.5.14': - dependencies: - '@types/node': 22.13.9 - '@types/yargs-parser@21.0.3': {} '@types/yargs@16.0.9': @@ -25354,23 +25264,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.25.0 - eslint: 9.21.0(jiti@2.4.2) - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -25401,18 +25294,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': - dependencies: - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.25.0 - debug: 4.4.0 - eslint: 9.21.0(jiti@2.4.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': dependencies: '@typescript-eslint/scope-manager': 8.26.0 @@ -25430,11 +25311,6 @@ snapshots: '@typescript-eslint/types': 8.17.0 '@typescript-eslint/visitor-keys': 8.17.0 - '@typescript-eslint/scope-manager@8.25.0': - dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 - '@typescript-eslint/scope-manager@8.26.0': dependencies: '@typescript-eslint/types': 8.26.0 @@ -25452,17 +25328,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': - dependencies: - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) - debug: 4.4.0 - eslint: 9.21.0(jiti@2.4.2) - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': dependencies: '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) @@ -25478,8 +25343,6 @@ snapshots: '@typescript-eslint/types@8.17.0': {} - '@typescript-eslint/types@8.25.0': {} - '@typescript-eslint/types@8.26.0': {} '@typescript-eslint/typescript-estree@5.62.0(supports-color@9.4.0)(typescript@5.8.2)': @@ -25525,20 +25388,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.25.0(typescript@5.8.2)': - dependencies: - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/visitor-keys': 8.25.0 - debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 2.0.1(typescript@5.8.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.2)': dependencies: '@typescript-eslint/types': 8.26.0 @@ -25565,17 +25414,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': - dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.25.0 - '@typescript-eslint/types': 8.25.0 - '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) - eslint: 9.21.0(jiti@2.4.2) - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0(jiti@2.4.2)) @@ -25597,11 +25435,6 @@ snapshots: '@typescript-eslint/types': 8.17.0 eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.25.0': - dependencies: - '@typescript-eslint/types': 8.25.0 - eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.26.0': dependencies: '@typescript-eslint/types': 8.26.0 @@ -25750,7 +25583,7 @@ snapshots: '@whatwg-node/cookie-store@0.2.3': dependencies: - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 tslib: 2.8.1 '@whatwg-node/disposablestack@0.0.5': @@ -25759,7 +25592,7 @@ snapshots: '@whatwg-node/disposablestack@0.0.6': dependencies: - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 tslib: 2.8.1 '@whatwg-node/events@0.1.2': @@ -25774,15 +25607,11 @@ snapshots: '@whatwg-node/node-fetch@0.7.12': dependencies: '@whatwg-node/disposablestack': 0.0.6 - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 busboy: 1.6.0 tslib: 2.8.1 - '@whatwg-node/promise-helpers@1.2.2': - dependencies: - tslib: 2.8.1 - - '@whatwg-node/promise-helpers@1.2.3': + '@whatwg-node/promise-helpers@1.2.4': dependencies: tslib: 2.8.1 @@ -25797,14 +25626,14 @@ snapshots: '@envelop/instruments': 1.0.0 '@whatwg-node/disposablestack': 0.0.6 '@whatwg-node/fetch': 0.10.5 - '@whatwg-node/promise-helpers': 1.2.3 + '@whatwg-node/promise-helpers': 1.2.4 tslib: 2.8.1 '@whatwg-node/server@0.9.71': dependencies: '@whatwg-node/disposablestack': 0.0.6 '@whatwg-node/fetch': 0.10.5 - '@whatwg-node/promise-helpers': 1.2.2 + '@whatwg-node/promise-helpers': 1.2.4 tslib: 2.8.1 '@wry/caches@1.0.1': @@ -28546,7 +28375,7 @@ snapshots: eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.21.0(jiti@2.4.2)) @@ -28585,7 +28414,7 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -28600,7 +28429,7 @@ snapshots: stable-hash: 0.0.4 tinyglobby: 0.2.12 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -28631,16 +28460,6 @@ snapshots: - bluebird - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0(jiti@2.4.2)): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) - eslint: 9.21.0(jiti@2.4.2) - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)): dependencies: debug: 3.2.7 @@ -28659,7 +28478,7 @@ snapshots: eslint: 9.21.0(jiti@2.4.2) eslint-compat-utils: 0.5.1(eslint@9.21.0(jiti@2.4.2)) - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -28670,7 +28489,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -28682,13 +28501,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.25.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -32953,7 +32772,7 @@ snapshots: mqtt@5.10.4: dependencies: '@types/readable-stream': 4.0.18 - '@types/ws': 8.5.14 + '@types/ws': 8.18.0 commist: 3.2.0 concat-stream: 2.0.0 debug: 4.4.0 @@ -36379,24 +36198,6 @@ snapshots: '@ts-morph/common': 0.26.1 code-block-writer: 13.0.3 - ts-node-dev@2.0.0(@swc/core@1.11.5(@swc/helpers@0.5.15))(@types/node@22.13.8)(typescript@5.8.2): - dependencies: - chokidar: 3.6.0 - dynamic-dedupe: 0.3.0 - minimist: 1.2.8 - mkdirp: 1.0.4 - resolve: 1.22.10 - rimraf: 2.7.1 - source-map-support: 0.5.21 - tree-kill: 1.2.2 - ts-node: 10.9.2(@swc/core@1.11.5(@swc/helpers@0.5.15))(@types/node@22.13.8)(typescript@5.8.2) - tsconfig: 7.0.0 - typescript: 5.8.2 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@types/node' - ts-node-dev@2.0.0(@swc/core@1.11.5(@swc/helpers@0.5.15))(@types/node@22.13.9)(typescript@5.8.2): dependencies: chokidar: 3.6.0 @@ -36415,26 +36216,6 @@ snapshots: - '@swc/wasm' - '@types/node' - ts-node@10.9.2(@swc/core@1.11.5(@swc/helpers@0.5.15))(@types/node@22.13.8)(typescript@5.8.2): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 22.13.8 - acorn: 8.14.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.8.2 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optionalDependencies: - '@swc/core': 1.11.5(@swc/helpers@0.5.15) - ts-node@10.9.2(@swc/core@1.11.5(@swc/helpers@0.5.15))(@types/node@22.13.9)(typescript@5.8.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -37327,25 +37108,6 @@ snapshots: workerpool@6.5.1: {} - wrangler@3.112.0(@cloudflare/workers-types@4.20250224.0): - dependencies: - '@cloudflare/kv-asset-handler': 0.3.4 - '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) - '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) - blake3-wasm: 2.1.5 - esbuild: 0.17.19 - miniflare: 3.20250214.2 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.1 - workerd: 1.20250214.0 - optionalDependencies: - '@cloudflare/workers-types': 4.20250224.0 - fsevents: 2.3.3 - sharp: 0.33.5 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - wrangler@3.112.0(@cloudflare/workers-types@4.20250303.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 diff --git a/website/src/content/docs/features/envelop-plugins.mdx b/website/src/content/docs/features/envelop-plugins.mdx index f791f2e9ca..800be969ed 100644 --- a/website/src/content/docs/features/envelop-plugins.mdx +++ b/website/src/content/docs/features/envelop-plugins.mdx @@ -395,3 +395,120 @@ const plugin = { But for this kind of purposes, `waitUntil` can be a better choice. [Learn more about explicit resource management here](/docs/features/explicit-resource-management). + +### `instruments` + +An optional `instruments` instance can be present in the plugin. + +This `Instruments` instance allows to wrap an entire phase execution (including all plugin hooks), +meaning running code just before, just after and around the execution of the phase. + +Instruments doesn't have access to input/output of a phase, use hooks to have access to those data. +If needed, we recommend to share data between instruments and hooks with a `WeakMap` and the given +`context` as the key. + +All instruments takes 2 parameters: + +- `payload`: an object containing the graphql context or the http request depending on the + instrument. +- `wrapped`: The function representing the execution of the phase. It takes no parameters, and + returns `void` (or `Promise` for asynchrone phases). **This function must always be + called**. If this function returns a `Promise`, the instrument should return a `Promise` resolving + after it. + +#### Instruments composition + +If multiple plugins have `instruments`, they are composed in the same order they are defined the +plugin array (the first is outtermost call, the last is inner most call). + +It is possible to customize this composition if it doesn't suite your need (ie. you need hooks and +instruments to have a different oreder of execution). + +```ts +import { composeInstruments, envelop } from '@envelop/core' + +const { instruments: instruments1, ...plugin1 } = usePlugin1() +const { instruments: instruments2, ...plugin2 } = usePlugin2() + +const instruments = composeInstruments([instruments2, instruments1]) + +const getEnveloped = envelop({ + plugin: [{ insturments }, plugin1, plugin2] +}) +``` + +#### `request` + +Wraps the HTTP request handling. This includes all the plugins `onRequest` and `onResponse` hooks. + +This instrument can be asynchronous, the wrapped funcion **can be** asynchronous. Be sure to return +a `Promise` if `wrapped()` returned a `Promise`. + +#### `requestParse` + +Wraps the parsing of the request phase to extract grapqhl params. This include all the plugins +`onRequestParse` hooks. + +This insturment can be asynchronous, the wrapped function **can be** asynchrounous. Be sure to +return a `Promise` if `wrapped()` returns a `Promise`. + +#### `operation` + +Wraps the Graphql operation execution pipeline. This is called for each graphql operation, meaning +it can be called mutliple time for the same HTTP request if batching is enabled. + +This instrument can be asynchronous, the wrapped function **can be** asynchronous. Be sur to return +a `Promise` if `wrapped()` returnd a `Promise`. + +#### `init` + +Wraps the envelop (the call to `envelop` function) initialisation. + +This includes all the plugins `onEnveloped` hooks, and the creation of the Envelop Orchestrator. + +This instruments must be synchrone, the wrapped function is always synchrone. + +#### `parse` + +Wraps the parse phase. This includes all the plugins `onParse` hooks and the actual engine `parse`. + +This instrument must be synchrone, the wrapped function is always synchrone. + +#### `validate` + +Wraps the validate phase. This includes all the plugins `onValidate` hooks and the actual engine +`validate`. + +This instrument must be synchrone, the wrapped function is always synchrone. + +#### `context` + +Wraps the context building phase. This includes all the plugins `onContextBuilding` hooks. + +This instrument must be synchrone, the wrapped function is always synchrone. + +#### `execute` + +Wraps the execute phase. This includes all the plugins `onExecute` hooks. + +This instrument can be asynchrone, the wrapped function **can be** asynchrone. Be sure to `await` or +use `.then` on the result of the `wrapped` function to run code after the `execute` phase. + +Note that `wrapped` is not guaranted to return a promise. + +#### `subscribe` + +Wraps the subscribe phase. This includes all the plugins `onSubsribe` hooks. Note that it doesn't +wrap the entire lifetime of the subscription, but only it's intialisation. + +This instrument can be asynchrone, the wrapped function **can be** asynchrone. Be sure to `await` or +use `.then` on the result of the `wrapped` function to run code after the `subsribe` phase. + +Note that `wrapped` is not guaranted to return a promise. + +#### `resultProcess` + +Wraps the context result processing phase. This includes all the plugins `onResultProcess` hooks. + +This instruments can be asynchrone, the wrapped function **can be** asynchronous. Be sure to return +a `Promise` if `wrapped()` returns a `Promise`.