diff --git a/.changeset/slow-files-act.md b/.changeset/slow-files-act.md new file mode 100644 index 0000000000..c5f6eba651 --- /dev/null +++ b/.changeset/slow-files-act.md @@ -0,0 +1,5 @@ +--- +'graphql-yoga': patch +--- + +Use the same context object in the entire pipeline diff --git a/examples/apollo-federation/service/typeDefs.js b/examples/apollo-federation/service/typeDefs.js new file mode 100644 index 0000000000..f6ea6b3ada --- /dev/null +++ b/examples/apollo-federation/service/typeDefs.js @@ -0,0 +1,14 @@ +/* eslint-disable */ +const { parse } = require('graphql'); + +module.exports = parse(/* GraphQL */ ` + type Query { + me: User + throw: String + } + + type User @key(fields: "id") { + id: ID! + username: String + } +`); diff --git a/examples/service-worker/src/index.ts b/examples/service-worker/src/index.ts index e05aedcb7f..8c01dd7e5a 100644 --- a/examples/service-worker/src/index.ts +++ b/examples/service-worker/src/index.ts @@ -1,10 +1,7 @@ import { createSchema, createYoga, Repeater } from 'graphql-yoga'; -// We can define GraphQL Route dynamically using env vars. -declare let GRAPHQL_ROUTE: string; - const yoga = createYoga({ - graphqlEndpoint: GRAPHQL_ROUTE || '/graphql', + graphqlEndpoint: globalThis.GRAPHQL_ROUTE || '/graphql', schema: createSchema({ typeDefs: /* GraphQL */ ` type Query { diff --git a/packages/graphql-yoga/__tests__/context.spec.ts b/packages/graphql-yoga/__tests__/context.spec.ts index 5e99d35130..7848d36e7e 100644 --- a/packages/graphql-yoga/__tests__/context.spec.ts +++ b/packages/graphql-yoga/__tests__/context.spec.ts @@ -137,4 +137,137 @@ describe('Context', () => { expect(onContextBuildingFn.mock.lastCall?.[0].context.params).toEqual(params); expect(onContextBuildingFn.mock.lastCall?.[0].context.request).toBeDefined(); }); + + it('share the same context object', async () => { + const contextObjects = new Set(); + const plugin = { + onContextBuilding: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onEnveloped: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onParse: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onValidate: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onExecute: jest.fn(({ args }) => { + contextObjects.add(args.contextValue); + }), + onRequest: jest.fn(({ serverContext }) => { + contextObjects.add(serverContext); + }), + onRequestParse: jest.fn(({ serverContext }) => { + contextObjects.add(serverContext); + }), + onResponse: jest.fn(({ serverContext }) => { + contextObjects.add(serverContext); + }), + }; + const yoga = createYoga({ + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String! + } + `, + resolvers: { + Query: { + hello: () => 'world', + }, + }, + }), + plugins: [plugin], + }); + const queryRes = await yoga.fetch('http://yoga/graphql?query={hello}', { + myExtraContext: 'myExtraContext', + }); + expect(queryRes.status).toBe(200); + const queryResult = await queryRes.json(); + expect(queryResult.data.hello).toBe('world'); + expect(contextObjects.size).toBe(1); + for (const hook of Object.keys(plugin) as (keyof typeof plugin)[]) { + expect(plugin[hook]).toHaveBeenCalledTimes(1); + } + const contextObject = contextObjects.values().next().value; + expect(contextObject).toBeDefined(); + expect(contextObject.myExtraContext).toBe('myExtraContext'); + }); + it('share different context objects for batched requests', async () => { + const contextObjects = new Set(); + const plugin = { + onContextBuilding: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onEnveloped: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onParse: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onValidate: jest.fn(({ context }) => { + contextObjects.add(context); + }), + onExecute: jest.fn(({ args }) => { + contextObjects.add(args.contextValue); + }), + onRequest: jest.fn(({ serverContext }) => { + contextObjects.add(serverContext); + }), + onRequestParse: jest.fn(({ serverContext }) => { + contextObjects.add(serverContext); + }), + onResponse: jest.fn(({ serverContext }) => { + contextObjects.add(serverContext); + }), + }; + const yoga = createYoga({ + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String! + } + `, + resolvers: { + Query: { + hello: () => 'world', + }, + }, + }), + plugins: [plugin], + batching: true, + }); + const queryRes = await yoga.fetch( + 'http://yoga/graphql', + { + method: 'POST', + body: JSON.stringify([ + { query: '{hello}' }, + { query: '{__typename hello}' }, + { query: '{__typename}' }, + ]), + headers: { + 'Content-Type': 'application/json', + }, + }, + { + myExtraContext: 'myExtraContext', + }, + ); + expect(queryRes.status).toBe(200); + const queryResult = await queryRes.json(); + expect(queryResult.length).toBe(3); + expect(queryResult[0].data.hello).toBe('world'); + expect(queryResult[1].data.__typename).toBe('Query'); + expect(queryResult[1].data.hello).toBe('world'); + expect(queryResult[2].data.__typename).toBe('Query'); + // One for server context, one for each request + expect(contextObjects.size).toBe(4); + for (const contextObject of contextObjects) { + expect(contextObject).toBeDefined(); + expect((contextObject as { myExtraContext: string }).myExtraContext).toBe('myExtraContext'); + } + }); }); diff --git a/packages/graphql-yoga/src/server.ts b/packages/graphql-yoga/src/server.ts index 85cdb1ddfb..031e988624 100644 --- a/packages/graphql-yoga/src/server.ts +++ b/packages/graphql-yoga/src/server.ts @@ -419,9 +419,11 @@ export class YogaServer< { params, request, + batched, }: { params: GraphQLParams; request: Request; + batched: boolean; }, // eslint-disable-next-line @typescript-eslint/ban-types ...args: {} extends TServerContext @@ -446,12 +448,20 @@ export class YogaServer< } if (result == null) { - const serverContext = args[0]; - const initialContext = { - ...serverContext, - request, - params, - }; + const additionalContext = args[0]?.request + ? { + params, + } + : { + request, + params, + }; + + const initialContext = args[0] + ? batched + ? Object.assign({}, args[0], additionalContext) + : Object.assign(args[0], additionalContext) + : additionalContext; const enveloped = this.getEnveloped(initialContext); @@ -532,6 +542,7 @@ export class YogaServer< { params, request, + batched: true, }, serverContext, ), @@ -541,6 +552,7 @@ export class YogaServer< { params: requestParserResult, request, + batched: false, }, serverContext, ))) as ResultProcessorInput;