From bdd523c59c867c4ab21242b2ad6f8659105df701 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Tue, 10 Mar 2020 21:31:35 -0400 Subject: [PATCH 1/9] Initial commit, implement contextValueExecution for #894 --- src/subscription/subscribe.d.ts | 3 +++ src/subscription/subscribe.js | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/subscription/subscribe.d.ts b/src/subscription/subscribe.d.ts index a2a2d0820e..c6d97772c9 100644 --- a/src/subscription/subscribe.d.ts +++ b/src/subscription/subscribe.d.ts @@ -16,6 +16,7 @@ export interface SubscriptionArgs { operationName?: Maybe; fieldResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + contextValueExecution?: Maybe<(contextValue: any) => any>; } /** @@ -53,6 +54,7 @@ export function subscribe( operationName?: Maybe, fieldResolver?: Maybe>, subscribeFieldResolver?: Maybe>, + contextValueExecution?: Maybe<(contextValue: any) => any>, ): Promise< AsyncIterableIterator> | ExecutionResult >; @@ -83,4 +85,5 @@ export function createSourceEventStream( variableValues?: { [key: string]: any }, operationName?: Maybe, fieldResolver?: Maybe>, + contextValueExecution?: Maybe<(contextValue: any) => any>, ): Promise | ExecutionResult>; diff --git a/src/subscription/subscribe.js b/src/subscription/subscribe.js index 1b170800c6..f8f5c18620 100644 --- a/src/subscription/subscribe.js +++ b/src/subscription/subscribe.js @@ -37,6 +37,7 @@ export type SubscriptionArgs = {| operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, subscribeFieldResolver?: ?GraphQLFieldResolver, + contextValueExecution?: ?(contextValue: mixed) => mixed, |}; /** @@ -74,6 +75,7 @@ declare function subscribe( operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, subscribeFieldResolver?: ?GraphQLFieldResolver, + contextValueExecution?: ?(contextValue: mixed) => mixed, ): Promise | ExecutionResult>; export function subscribe( argsOrSchema, @@ -84,6 +86,7 @@ export function subscribe( operationName, fieldResolver, subscribeFieldResolver, + contextValueExecution, ) { /* eslint-enable no-redeclare */ // Extract arguments from object args if provided. @@ -98,6 +101,7 @@ export function subscribe( operationName, fieldResolver, subscribeFieldResolver, + contextValueExecution, }); } @@ -125,6 +129,7 @@ function subscribeImpl( operationName, fieldResolver, subscribeFieldResolver, + contextValueExecution, } = args; const sourcePromise = createSourceEventStream( @@ -143,16 +148,21 @@ function subscribeImpl( // the GraphQL specification. The `execute` function provides the // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. - const mapSourceToResponse = payload => - execute({ + const mapSourceToResponse = payload => { + const executeContextValue = + typeof contextValueExecution === 'function' + ? contextValueExecution(contextValue) + : contextValue; + return execute({ schema, document, rootValue: payload, - contextValue, + contextValue: executeContextValue, variableValues, operationName, fieldResolver, }); + }; // Resolve the Source Stream, then map every source value to a // ExecutionResult value as described above. From 8b4b0f78b4e161cf8699407044867567796e39fe Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Tue, 10 Mar 2020 21:58:20 -0400 Subject: [PATCH 2/9] Add test for contextValueExecution --- src/subscription/__tests__/subscribe-test.js | 68 +++++++++++++++++++- src/subscription/subscribe.js | 4 +- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index 8586d95469..85e9f6a83e 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -100,6 +100,7 @@ async function createSubscription( pubsub, schema = emailSchema, document = defaultSubscriptionAST, + contextValueExecution = undefined, ) { const data = { inbox: { @@ -132,7 +133,12 @@ async function createSubscription( return { sendImportantEmail, // $FlowFixMe - subscription: await subscribe({ schema, document, rootValue: data }), + subscription: await subscribe({ + schema, + document, + rootValue: data, + contextValueExecution, + }), }; } @@ -1107,4 +1113,64 @@ describe('Subscription Publish Phase', () => { value: undefined, }); }); + + it('should produce a unique context per execution via contextValueExecution', async () => { + const contextExecutionSchema = emailSchemaWithResolvers( + async function*() { + yield { email: { subject: 'Hello' } }; + yield { email: { subject: 'Hello' } }; + }, + (data, _args, ctx) => ({ + email: { subject: `${data.email.subject} ${ctx.contextIndex}` }, + }), + ); + + let contextIndex = 0; + const subscription = await subscribe({ + schema: contextExecutionSchema, + document: parse(` + subscription { + importantEmail { + email { + subject + } + } + } + `), + contextValue: { test: true }, + contextValueExecution: ctx => { + expect(ctx.test).to.equal(true); + return { ...ctx, contextIndex: contextIndex++ }; + }, + }); + + // $FlowFixMe + const payload1 = await subscription.next(); + expect(payload1).to.deep.equal({ + done: false, + value: { + data: { + importantEmail: { + email: { + subject: 'Hello 0', + }, + }, + }, + }, + }); + + const payload2 = await subscription.next(); + expect(payload2).to.deep.equal({ + done: false, + value: { + data: { + importantEmail: { + email: { + subject: 'Hello 1', + }, + }, + }, + }, + }); + }); }); diff --git a/src/subscription/subscribe.js b/src/subscription/subscribe.js index f8f5c18620..7c02c04218 100644 --- a/src/subscription/subscribe.js +++ b/src/subscription/subscribe.js @@ -37,7 +37,7 @@ export type SubscriptionArgs = {| operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, subscribeFieldResolver?: ?GraphQLFieldResolver, - contextValueExecution?: ?(contextValue: mixed) => mixed, + contextValueExecution?: ?(contextValue: any) => mixed, |}; /** @@ -75,7 +75,7 @@ declare function subscribe( operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, subscribeFieldResolver?: ?GraphQLFieldResolver, - contextValueExecution?: ?(contextValue: mixed) => mixed, + contextValueExecution?: ?(contextValue: any) => mixed, ): Promise | ExecutionResult>; export function subscribe( argsOrSchema, From 878330d15960d324bf7e7e65b5366b57c86223f2 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Mon, 16 Mar 2020 11:24:59 -0400 Subject: [PATCH 3/9] contextValueExecution -> perEventContextResolver --- src/subscription/__tests__/subscribe-test.js | 8 ++++---- src/subscription/subscribe.d.ts | 5 ++--- src/subscription/subscribe.js | 14 +++++++------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index 85e9f6a83e..5d1f54ed61 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -100,7 +100,7 @@ async function createSubscription( pubsub, schema = emailSchema, document = defaultSubscriptionAST, - contextValueExecution = undefined, + perEventContextResolver = undefined, ) { const data = { inbox: { @@ -137,7 +137,7 @@ async function createSubscription( schema, document, rootValue: data, - contextValueExecution, + perEventContextResolver, }), }; } @@ -1114,7 +1114,7 @@ describe('Subscription Publish Phase', () => { }); }); - it('should produce a unique context per execution via contextValueExecution', async () => { + it('should produce a unique context per event via perEventContextResolver', async () => { const contextExecutionSchema = emailSchemaWithResolvers( async function*() { yield { email: { subject: 'Hello' } }; @@ -1138,7 +1138,7 @@ describe('Subscription Publish Phase', () => { } `), contextValue: { test: true }, - contextValueExecution: ctx => { + perEventContextResolver: ctx => { expect(ctx.test).to.equal(true); return { ...ctx, contextIndex: contextIndex++ }; }, diff --git a/src/subscription/subscribe.d.ts b/src/subscription/subscribe.d.ts index c6d97772c9..1359a6116c 100644 --- a/src/subscription/subscribe.d.ts +++ b/src/subscription/subscribe.d.ts @@ -16,7 +16,7 @@ export interface SubscriptionArgs { operationName?: Maybe; fieldResolver?: Maybe>; subscribeFieldResolver?: Maybe>; - contextValueExecution?: Maybe<(contextValue: any) => any>; + perEventContextResolver?: Maybe<(contextValue: any) => any>; } /** @@ -54,7 +54,7 @@ export function subscribe( operationName?: Maybe, fieldResolver?: Maybe>, subscribeFieldResolver?: Maybe>, - contextValueExecution?: Maybe<(contextValue: any) => any>, + perEventContextResolver?: Maybe<(contextValue: any) => any>, ): Promise< AsyncIterableIterator> | ExecutionResult >; @@ -85,5 +85,4 @@ export function createSourceEventStream( variableValues?: { [key: string]: any }, operationName?: Maybe, fieldResolver?: Maybe>, - contextValueExecution?: Maybe<(contextValue: any) => any>, ): Promise | ExecutionResult>; diff --git a/src/subscription/subscribe.js b/src/subscription/subscribe.js index 7c02c04218..2540179a98 100644 --- a/src/subscription/subscribe.js +++ b/src/subscription/subscribe.js @@ -37,7 +37,7 @@ export type SubscriptionArgs = {| operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, subscribeFieldResolver?: ?GraphQLFieldResolver, - contextValueExecution?: ?(contextValue: any) => mixed, + perEventContextResolver?: ?(contextValue: any) => mixed, |}; /** @@ -75,7 +75,7 @@ declare function subscribe( operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, subscribeFieldResolver?: ?GraphQLFieldResolver, - contextValueExecution?: ?(contextValue: any) => mixed, + perEventContextResolver?: ?(contextValue: any) => mixed, ): Promise | ExecutionResult>; export function subscribe( argsOrSchema, @@ -86,7 +86,7 @@ export function subscribe( operationName, fieldResolver, subscribeFieldResolver, - contextValueExecution, + perEventContextResolver, ) { /* eslint-enable no-redeclare */ // Extract arguments from object args if provided. @@ -101,7 +101,7 @@ export function subscribe( operationName, fieldResolver, subscribeFieldResolver, - contextValueExecution, + perEventContextResolver, }); } @@ -129,7 +129,7 @@ function subscribeImpl( operationName, fieldResolver, subscribeFieldResolver, - contextValueExecution, + perEventContextResolver, } = args; const sourcePromise = createSourceEventStream( @@ -150,8 +150,8 @@ function subscribeImpl( // "ExecuteQuery" algorithm, for which `execute` is also used. const mapSourceToResponse = payload => { const executeContextValue = - typeof contextValueExecution === 'function' - ? contextValueExecution(contextValue) + typeof perEventContextResolver === 'function' + ? perEventContextResolver(contextValue) : contextValue; return execute({ schema, From 92929055025a1c677e8d701a504fc802d94103ed Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Mon, 16 Mar 2020 11:25:22 -0400 Subject: [PATCH 4/9] Add note about perEventContextResolver in JSDoc for subscribe --- src/subscription/subscribe.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/subscription/subscribe.d.ts b/src/subscription/subscribe.d.ts index 1359a6116c..8f952bd9c7 100644 --- a/src/subscription/subscribe.d.ts +++ b/src/subscription/subscribe.d.ts @@ -37,6 +37,9 @@ export interface SubscriptionArgs { * If the operation succeeded, the promise resolves to an AsyncIterator, which * yields a stream of ExecutionResults representing the response stream. * + * If a `perEventContextResolver` argument is provided, it will be invoked for + * each event, and return a new context value specific to that event's execution. + * * Accepts either an object with named arguments, or individual arguments. */ export function subscribe( From 1d6b1505b6809bdba80d2663b525667a28b207d0 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Mon, 16 Mar 2020 11:32:51 -0400 Subject: [PATCH 5/9] Remove comma --- src/subscription/subscribe.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subscription/subscribe.d.ts b/src/subscription/subscribe.d.ts index 8f952bd9c7..264ff73a33 100644 --- a/src/subscription/subscribe.d.ts +++ b/src/subscription/subscribe.d.ts @@ -38,7 +38,7 @@ export interface SubscriptionArgs { * yields a stream of ExecutionResults representing the response stream. * * If a `perEventContextResolver` argument is provided, it will be invoked for - * each event, and return a new context value specific to that event's execution. + * each event and return a new context value specific to that event's execution. * * Accepts either an object with named arguments, or individual arguments. */ From 4461d7fc6548756f3b9829786790d7e913a2d141 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Mon, 16 Mar 2020 16:48:35 -0400 Subject: [PATCH 6/9] Add docs --- src/subscription/subscribe.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/subscription/subscribe.js b/src/subscription/subscribe.js index 2540179a98..8873c5a2c0 100644 --- a/src/subscription/subscribe.js +++ b/src/subscription/subscribe.js @@ -59,6 +59,9 @@ export type SubscriptionArgs = {| * If the operation succeeded, the promise resolves to an AsyncIterator, which * yields a stream of ExecutionResults representing the response stream. * + * If a `perEventContextResolver` argument is provided, it will be invoked for + * each event and return a new context value specific to the event's execution. + * * Accepts either an object with named arguments, or individual arguments. */ declare function subscribe( @@ -148,6 +151,8 @@ function subscribeImpl( // the GraphQL specification. The `execute` function provides the // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. + // If `perEventContextResolver` is provided, it is invoked with the original + // `contextValue` to return a new context unique to this `execute`. const mapSourceToResponse = payload => { const executeContextValue = typeof perEventContextResolver === 'function' From f759882a10d018d24498791e1be8db8670903743 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Wed, 18 Mar 2020 10:02:03 -0400 Subject: [PATCH 7/9] Update, ensure perEventContextResolverFn per stream --- src/subscription/subscribe.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/subscription/subscribe.js b/src/subscription/subscribe.js index 8873c5a2c0..c47602298c 100644 --- a/src/subscription/subscribe.js +++ b/src/subscription/subscribe.js @@ -153,21 +153,21 @@ function subscribeImpl( // "ExecuteQuery" algorithm, for which `execute` is also used. // If `perEventContextResolver` is provided, it is invoked with the original // `contextValue` to return a new context unique to this `execute`. - const mapSourceToResponse = payload => { - const executeContextValue = - typeof perEventContextResolver === 'function' - ? perEventContextResolver(contextValue) - : contextValue; - return execute({ + const perEventContextResolverFn = + typeof perEventContextResolver === 'function' + ? perEventContextResolver + : ctx => ctx; + + const mapSourceToResponse = payload => + execute({ schema, document, rootValue: payload, - contextValue: executeContextValue, + contextValue: perEventContextResolverFn(contextValue), variableValues, operationName, fieldResolver, }); - }; // Resolve the Source Stream, then map every source value to a // ExecutionResult value as described above. From 1d0b6af7abf9c8e2962e95b810cc53fcf08eac60 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Mon, 23 Mar 2020 09:27:47 -0400 Subject: [PATCH 8/9] Use ?? for the perEventContextResolver check --- src/subscription/subscribe.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/subscription/subscribe.js b/src/subscription/subscribe.js index c47602298c..5436255533 100644 --- a/src/subscription/subscribe.js +++ b/src/subscription/subscribe.js @@ -153,10 +153,7 @@ function subscribeImpl( // "ExecuteQuery" algorithm, for which `execute` is also used. // If `perEventContextResolver` is provided, it is invoked with the original // `contextValue` to return a new context unique to this `execute`. - const perEventContextResolverFn = - typeof perEventContextResolver === 'function' - ? perEventContextResolver - : ctx => ctx; + const perEventContextResolverFn = perEventContextResolver ?? (ctx => ctx); const mapSourceToResponse = payload => execute({ From 1fb09314a64d885a21f4b9198a549f60f3eb5c53 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Fri, 13 Nov 2020 12:00:34 -0500 Subject: [PATCH 9/9] Fix flow types --- src/subscription/__tests__/subscribe-test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/subscription/__tests__/subscribe-test.js b/src/subscription/__tests__/subscribe-test.js index 919614649e..7ee7395424 100644 --- a/src/subscription/__tests__/subscribe-test.js +++ b/src/subscription/__tests__/subscribe-test.js @@ -13,6 +13,7 @@ import { GraphQLError } from '../../error/GraphQLError'; import { GraphQLSchema } from '../../type/schema'; import { GraphQLList, GraphQLObjectType } from '../../type/definition'; +import type { GraphQLFieldResolver } from '../../type/definition'; import { GraphQLInt, GraphQLString, GraphQLBoolean } from '../../type/scalars'; import { createSourceEventStream, subscribe } from '../subscribe'; @@ -68,9 +69,9 @@ const EmailEventType = new GraphQLObjectType({ const emailSchema = emailSchemaWithResolvers(); -function emailSchemaWithResolvers( +function emailSchemaWithResolvers( subscribeFn?: (T) => mixed, - resolveFn?: (T) => mixed, + resolveFn?: GraphQLFieldResolver, ) { return new GraphQLSchema({ query: QueryType, @@ -1133,8 +1134,8 @@ describe('Subscription Publish Phase', () => { return { ...ctx, contextIndex: contextIndex++ }; }, }); + invariant(isAsyncIterable(subscription)); - // $FlowFixMe const payload1 = await subscription.next(); expect(payload1).to.deep.equal({ done: false,