From ebc06c95c4209a58be3401ce3511c3d4d34cbc26 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 7 Nov 2024 15:41:15 +0100 Subject: [PATCH 1/3] feat: batching index in phases --- .../graphql-yoga/__tests__/batching.spec.ts | 96 ++++++++++++++++++- packages/graphql-yoga/src/plugins/types.ts | 2 + packages/graphql-yoga/src/server.ts | 18 ++-- 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/packages/graphql-yoga/__tests__/batching.spec.ts b/packages/graphql-yoga/__tests__/batching.spec.ts index a141c2393f..ea5741888a 100644 --- a/packages/graphql-yoga/__tests__/batching.spec.ts +++ b/packages/graphql-yoga/__tests__/batching.spec.ts @@ -1,5 +1,4 @@ -import { createSchema } from '../src/schema'; -import { createYoga } from '../src/server'; +import { createSchema, createYoga, Plugin } from '../src'; describe('Batching', () => { const schema = createSchema({ @@ -287,4 +286,97 @@ describe('Batching', () => { ], }); }); + it('batching index is forwarded into hooks (single batch)', async () => { + const yoga = createYoga({ + schema, + batching: true, + plugins: [ + { + onParams(params) { + expect(params.batchedRequestIndex).toEqual(0); + }, + onParse(context) { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + }, + onValidate(context) { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + }, + onExecute(context) { + // @ts-expect-error not in types + expect(context.args.contextValue[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + }, + } satisfies Plugin, + ], + }); + + const response = await yoga.fetch('http://yoga.com/graphql', { + method: 'POST', + headers: { + accept: 'application/graphql-response+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify([{ query: '{hello}' }]), + }); + expect(await response.text()).toEqual(`[{"data":{"hello":"hello"}}]`); + }); + it('batching index is forwarded into hooks (multiple batches)', async () => { + const yoga = createYoga({ + schema, + batching: true, + plugins: [ + { + onParams(context) { + const params = JSON.stringify(context.params); + if (params === '{"source":"{hello}"}') { + expect(context.batchedRequestIndex).toEqual(0); + } else if (params === '{"source":"{bye}"}') { + expect(context.batchedRequestIndex).toEqual(1); + } + }, + onParse(context) { + const params = JSON.stringify(context.params); + if (params === '{"source":"{hello}"}') { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + } else if (params === '{"source":"{bye}"}') { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(1); + } + }, + onValidate(context) { + const params = JSON.stringify(context.params); + if (params === '{"source":"{hello}"}') { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + } else if (params === '{"source":"{bye}"}') { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(1); + } + }, + onExecute(context) { + const params = JSON.stringify(context.args.contextValue.params); + if (params === '{"source":"{hello}"}') { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + } else if (params === '{"source":"{bye}"}') { + // @ts-expect-error not in types + expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(1); + } + }, + } satisfies Plugin, + ], + }); + + const response = await yoga.fetch('http://yoga.com/graphql', { + method: 'POST', + headers: { + accept: 'application/graphql-response+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify([{ query: '{hello}' }, { query: '{bye}' }]), + }); + expect(await response.text()).toEqual(`[{"data":{"hello":"hello"}},{"data":{"bye":"bye"}}]`); + }); }); diff --git a/packages/graphql-yoga/src/plugins/types.ts b/packages/graphql-yoga/src/plugins/types.ts index 660afc1a5f..aaf141e1ec 100644 --- a/packages/graphql-yoga/src/plugins/types.ts +++ b/packages/graphql-yoga/src/plugins/types.ts @@ -113,6 +113,8 @@ export interface OnParamsEventPayload { setParams: (params: GraphQLParams) => void; setResult: (result: ExecutionResult | AsyncIterable) => void; fetchAPI: FetchAPI; + /** Index of the batched request if it is a batched request */ + batchedRequestIndex?: number; } export type OnResultProcess = ( diff --git a/packages/graphql-yoga/src/server.ts b/packages/graphql-yoga/src/server.ts index cd04883ad6..29f4f11419 100644 --- a/packages/graphql-yoga/src/server.ts +++ b/packages/graphql-yoga/src/server.ts @@ -462,13 +462,12 @@ export class YogaServer< { params, request, - batched, }: { params: GraphQLParams; request: Request; - batched: boolean; }, serverContext: TServerContext, + batchedRequestIndex?: number, ) { let result: ExecutionResult | AsyncIterable | undefined; let context = serverContext as TServerContext & YogaInitialContext; @@ -484,6 +483,7 @@ export class YogaServer< result = newResult; }, fetchAPI: this.fetchAPI, + batchedRequestIndex, }); } @@ -498,9 +498,16 @@ export class YogaServer< params, }; + let batchIndexPartial: object = {}; + + if (batchedRequestIndex !== undefined) { + batchIndexPartial = { [Symbol.for('yogaBatchedRequestIndex')]: batchedRequestIndex }; + } + context = Object.assign( - batched ? Object.create(serverContext) : serverContext, + batchedRequestIndex === undefined ? serverContext : Object.create(serverContext), additionalContext, + batchIndexPartial, ); const enveloped = this.getEnveloped(context); @@ -605,14 +612,14 @@ export class YogaServer< const result = (await (Array.isArray(requestParserResult) ? Promise.all( - requestParserResult.map(params => + requestParserResult.map((params, index) => this.getResultForParams( { params, request, - batched: true, }, serverContext, + index, ), ), ) @@ -620,7 +627,6 @@ export class YogaServer< { params: requestParserResult, request, - batched: false, }, serverContext, ))) as ResultProcessorInput; From 4dbacb49f638631511bbb8c0e55231fc7643f02a Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 7 Nov 2024 16:15:55 +0100 Subject: [PATCH 2/3] refactor: use weak map --- .../graphql-yoga/__tests__/batching.spec.ts | 29 +++++++------------ packages/graphql-yoga/src/index.ts | 1 + packages/graphql-yoga/src/server.ts | 12 ++++---- .../src/utils/batch-request-index.ts | 5 ++++ 4 files changed, 21 insertions(+), 26 deletions(-) create mode 100644 packages/graphql-yoga/src/utils/batch-request-index.ts diff --git a/packages/graphql-yoga/__tests__/batching.spec.ts b/packages/graphql-yoga/__tests__/batching.spec.ts index ea5741888a..9edb7dc991 100644 --- a/packages/graphql-yoga/__tests__/batching.spec.ts +++ b/packages/graphql-yoga/__tests__/batching.spec.ts @@ -1,4 +1,4 @@ -import { createSchema, createYoga, Plugin } from '../src'; +import { createSchema, createYoga, getBatchRequestIndexFromContext, Plugin } from '../src/index.js'; describe('Batching', () => { const schema = createSchema({ @@ -296,16 +296,13 @@ describe('Batching', () => { expect(params.batchedRequestIndex).toEqual(0); }, onParse(context) { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + expect(getBatchRequestIndexFromContext(context.context)).toEqual(0); }, onValidate(context) { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + expect(getBatchRequestIndexFromContext(context.context)).toEqual(0); }, onExecute(context) { - // @ts-expect-error not in types - expect(context.args.contextValue[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + expect(getBatchRequestIndexFromContext(context.args.contextValue)).toEqual(0); }, } satisfies Plugin, ], @@ -338,31 +335,25 @@ describe('Batching', () => { onParse(context) { const params = JSON.stringify(context.params); if (params === '{"source":"{hello}"}') { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + expect(getBatchRequestIndexFromContext(context.context)).toEqual(0); } else if (params === '{"source":"{bye}"}') { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(1); + expect(getBatchRequestIndexFromContext(context.context)).toEqual(1); } }, onValidate(context) { const params = JSON.stringify(context.params); if (params === '{"source":"{hello}"}') { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + expect(getBatchRequestIndexFromContext(context.context)).toEqual(0); } else if (params === '{"source":"{bye}"}') { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(1); + expect(getBatchRequestIndexFromContext(context.context)).toEqual(1); } }, onExecute(context) { const params = JSON.stringify(context.args.contextValue.params); if (params === '{"source":"{hello}"}') { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(0); + expect(getBatchRequestIndexFromContext(context.args.contextValue)).toEqual(0); } else if (params === '{"source":"{bye}"}') { - // @ts-expect-error not in types - expect(context.context[Symbol.for('yogaBatchedRequestIndex')]).toEqual(1); + expect(getBatchRequestIndexFromContext(context.args.contextValue)).toEqual(1); } }, } satisfies Plugin, diff --git a/packages/graphql-yoga/src/index.ts b/packages/graphql-yoga/src/index.ts index 5c34b831af..d3a8844c7d 100644 --- a/packages/graphql-yoga/src/index.ts +++ b/packages/graphql-yoga/src/index.ts @@ -12,6 +12,7 @@ export * from './types.js'; export { maskError } from './utils/mask-error.js'; export { type OnParamsEventPayload } from './plugins/types.js'; export { createLRUCache } from './utils/create-lru-cache.js'; +export { getBatchRequestIndexFromContext } from './utils/batch-request-index.js'; export { mergeSchemas } from '@graphql-tools/schema'; export { // Handy type utils diff --git a/packages/graphql-yoga/src/server.ts b/packages/graphql-yoga/src/server.ts index 29f4f11419..3bb1088730 100644 --- a/packages/graphql-yoga/src/server.ts +++ b/packages/graphql-yoga/src/server.ts @@ -70,6 +70,7 @@ import { YogaInitialContext, YogaMaskedErrorOpts, } from './types.js'; +import { batchRequestIndexMap } from './utils/batch-request-index.js'; import { maskError } from './utils/mask-error.js'; /** @@ -498,18 +499,15 @@ export class YogaServer< params, }; - let batchIndexPartial: object = {}; - - if (batchedRequestIndex !== undefined) { - batchIndexPartial = { [Symbol.for('yogaBatchedRequestIndex')]: batchedRequestIndex }; - } - context = Object.assign( batchedRequestIndex === undefined ? serverContext : Object.create(serverContext), additionalContext, - batchIndexPartial, ); + if (batchedRequestIndex !== undefined) { + batchRequestIndexMap.set(context, batchedRequestIndex); + } + const enveloped = this.getEnveloped(context); this.logger.debug(`Processing GraphQL Parameters`); diff --git a/packages/graphql-yoga/src/utils/batch-request-index.ts b/packages/graphql-yoga/src/utils/batch-request-index.ts new file mode 100644 index 0000000000..ba5bca787d --- /dev/null +++ b/packages/graphql-yoga/src/utils/batch-request-index.ts @@ -0,0 +1,5 @@ +export const batchRequestIndexMap = new WeakMap(); + +export function getBatchRequestIndexFromContext(context: object): number | null { + return batchRequestIndexMap.get(context) ?? null; +} From 1a3fdc1c0cd92f974e4ed1175243aa54c7572f77 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 7 Nov 2024 16:33:15 +0100 Subject: [PATCH 3/3] changeset --- .changeset/young-lamps-juggle.md | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .changeset/young-lamps-juggle.md diff --git a/.changeset/young-lamps-juggle.md b/.changeset/young-lamps-juggle.md new file mode 100644 index 0000000000..0d63c17548 --- /dev/null +++ b/.changeset/young-lamps-juggle.md @@ -0,0 +1,36 @@ +--- +'graphql-yoga': minor +--- + +Support accessing the index of a batched request within the plugin phases. + +The helper function `getBatchRequestIndexFromContext` can be used for getting the current batch +requests index for the ongoing execution. + +```ts +import { createYoga, getBatchRequestIndexFromContext, Plugin } from 'graphql-yoga' + +const yoga = createYoga({ + batched: true, + plugins: [ + { + onParams(params) { + // undefined or number + console.log(params.batchedRequestIndex) + }, + onParse(context) { + // undefined or number + console.log(getBatchRequestIndexFromContext(context.context)) + }, + onValidate(context) { + // undefined or number + console.log(getBatchRequestIndexFromContext(context.context)) + }, + onExecute(context) { + // undefined or number + console.log(getBatchRequestIndexFromContext(context.args.contextValue)) + } + } satisfies Plugin + ] +}) +```