From 19ed013b20630f3ce807fc9e5f829e4d3118ac2d Mon Sep 17 00:00:00 2001 From: Hoang Date: Sun, 25 Oct 2020 14:04:56 -0400 Subject: [PATCH 01/13] Implement subscription --- src/execution.ts | 244 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 240 insertions(+), 4 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 99655c7..b928b64 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -26,15 +26,19 @@ import { isObjectType, isSpecifiedScalarType, Kind, - TypeNameMetaFieldDef + TypeNameMetaFieldDef, + locatedError } from "graphql"; import { collectFields, - ExecutionContext as GraphQLContext + ExecutionContext as GraphQLContext, + getFieldDef } from "graphql/execution/execute"; import { FieldNode, OperationDefinitionNode } from "graphql/language/ast"; import Maybe from "graphql/tsutils/Maybe"; import { GraphQLTypeResolver } from "graphql/type/definition"; +import mapAsyncIterator from "graphql/subscription/mapAsyncIterator"; + import { addPath, Arguments, @@ -58,6 +62,7 @@ import { compileVariableParsing, failToParseVariables } from "./variables"; +import { pathToArray } from "graphql/jsutils/Path"; const inspect = createInspect(); @@ -166,6 +171,11 @@ export interface CompiledQuery { context: any, variables: Maybe<{ [key: string]: any }> ) => Promise | ExecutionResult; + subscribe?: ( + root: any, + context: any, + variables: Maybe<{ [key: string]: any }> + ) => Promise | ExecutionResult>; stringify: (v: any) => string; } @@ -181,6 +191,7 @@ interface InternalCompiledQuery extends CompiledQuery { * @param partialOptions compilation options to tune the compiler features * @returns {CompiledQuery} the cacheable result */ + export function compileQuery( schema: GraphQLSchema, document: DocumentNode, @@ -201,6 +212,7 @@ export function compileQuery( ) { throw new Error("resolverInfoEnricher must be a function"); } + try { const options = { disablingCapturingStackErrors: false, @@ -226,17 +238,29 @@ export function compileQuery( } else { stringify = JSON.stringify; } + const getVariables = compileVariableParsing( schema, context.operation.variableDefinitions || [] ); const functionBody = compileOperation(context); + + const query = createBoundQuery( + context, + document, + new Function("return " + functionBody)(), + getVariables, + context.operation.name != null ? context.operation.name.value : undefined + ); + + // Subscription const compiledQuery: InternalCompiledQuery = { - query: createBoundQuery( + query, + subscribe: createBoundSubscribe( context, document, - new Function("return " + functionBody)(), + query, getVariables, context.operation.name != null ? context.operation.name.value @@ -264,6 +288,217 @@ export function isCompiledQuery< return "query" in query && typeof query.query === "function"; } +/** + * Subscription + * Implements the "CreateSourceEventStream" algorithm described in the + * GraphQL specification, resolving the subscription source event stream. + * + * Returns a Promise which resolves to either an AsyncIterable (if successful) + * or an ExecutionResult (error). The promise will be rejected if the schema or + * other arguments to this function are invalid, or if the resolved event stream + * is not an async iterable. + * + * If the client-provided arguments to this function do not result in a + * compliant subscription, a GraphQL Response (ExecutionResult) with + * descriptive errors and no data will be returned. + * + * If the the source stream could not be created due to faulty subscription + * resolver logic or underlying systems, the promise will resolve to a single + * ExecutionResult containing `errors` and no `data`. + * + * If the operation succeeded, the promise resolves to the AsyncIterable for the + * event stream returned by the resolver. + * + * A Source Event Stream represents a sequence of events, each of which triggers + * a GraphQL execution for that event. + * + * This may be useful when hosting the stateful subscription service in a + * different process or machine than the stateless GraphQL execution engine, + * or otherwise separating these two steps. For more on this, see the + * "Supporting Subscriptions at Scale" information in the GraphQL specification. + * + * Since createSourceEventStream only builds execution context and reports errors + * in doing so, which we did, we simply call directly the later called + * executeSubscription. + */ + +function isAsyncIterable( + val: unknown +): val is AsyncIterableIterator { + return typeof Object(val)[Symbol.asyncIterator] === "function"; +} + +async function executeSubscription( + context: ExecutionContext, + compileContext: CompilationContext +): Promise> { + // TODO: We are doing the same thing in compileOperation, but since + // it does not expose any of its sideeffect, we have to do it again + const type = getOperationRootType( + compileContext.schema, + compileContext.operation + ); + + const fields = collectFields( + compileContext, + type, + compileContext.operation.selectionSet, + Object.create(null), + Object.create(null) + ); + + const responseNames = Object.keys(fields); + const responseName = responseNames[0]; + const fieldNodes = fields[responseName]; + const fieldNode = fieldNodes[0]; + const fieldName = fieldNode.name.value; + const fieldDef = getFieldDef(compileContext.schema, type, fieldName); + + if (!fieldDef) { + throw new GraphQLError( + `The subscription field "${fieldName}" is not defined.`, + fieldNodes + ); + } + + const responsePath = addPath(undefined, fieldName); + + const resolveInfo = createResolveInfoThunk({ + schema: compileContext.schema, + fragments: compileContext.fragments, + operation: compileContext.operation, + parentType: type, + fieldName, + fieldType: fieldDef.type, + fieldNodes + })(context.rootValue, context.variables, serializeResponsePath(responsePath)); + + // Call the `subscribe()` resolver or the default resolver to produce an + // AsyncIterable yielding raw payloads. + + // TODO: rootValue resolver and value is not supported + const subscriber = fieldDef.subscribe; + + let eventStream; + + try { + eventStream = + subscriber && + (await subscriber( + context.rootValue, + context.variables, + context.context, + resolveInfo + )); + if (eventStream instanceof Error) throw eventStream; + } catch (error) { + throw locatedError(error, fieldNodes, pathToArray(responsePath)); + } + + if (!isAsyncIterable(eventStream)) { + throw new Error( + "Subscription field must return Async Iterable. " + + `Received: ${inspect(eventStream)}.` + ); + } + return eventStream; +} + +function createBoundSubscribe( + compilationContext: CompilationContext, + document: DocumentNode, + queryFn: CompiledQuery["query"], + getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, + operationName: string | undefined +): CompiledQuery["subscribe"] | undefined { + if (compilationContext.operation.operation !== "subscription") + return undefined; + + const { + resolvers, + typeResolvers, + isTypeOfs, + serializers, + resolveInfos + } = compilationContext; + const trimmer = createNullTrimmer(compilationContext); + const fnName = operationName ? operationName : "subscribe"; + + const ret = { + async [fnName]( + rootValue: any, + context: any, + variables: Maybe<{ [key: string]: any }> + ): Promise | ExecutionResult> { + // this can be shared across in a batch request + const parsedVariables = getVariableValues(variables || {}); + + // Return early errors if variable coercing failed. + if (failToParseVariables(parsedVariables)) { + return { errors: parsedVariables.errors }; + } + + const executionContext: ExecutionContext = { + rootValue, + context, + variables: parsedVariables.coerced, + safeMap, + inspect, + GraphQLError: GraphqlJitError, + resolvers, + typeResolvers, + isTypeOfs, + serializers, + resolveInfos, + trimmer, + promiseCounter: 0, + nullErrors: [], + errors: [], + data: {} + }; + + function reportGraphQLError(error: any): ExecutionResult { + if (error instanceof GraphQLError) { + return { + errors: [error] + }; + } + throw error; + } + + let resultOrStream: AsyncIterableIterator; + + try { + resultOrStream = await executeSubscription( + executionContext, + compilationContext + ); + } catch (e) { + return reportGraphQLError(e); + } + + // For each payload yielded from a subscription, map it over the normal + // GraphQL `execute` function, with `payload` as the rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // 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. + // We use our `query` function in place of `execute` + + const mapSourceToResponse = (payload: any) => + queryFn(payload, context, variables); + + return mapAsyncIterator( + resultOrStream, + mapSourceToResponse, + reportGraphQLError + ); + } + }; + + return ret[fnName]; +} + // Exported only for an error test export function createBoundQuery( compilationContext: CompilationContext, @@ -389,6 +624,7 @@ function compileOperation(context: CompilationContext) { fieldMap, true ); + let body = `function query (${GLOBAL_EXECUTION_CONTEXT}) { "use strict"; `; From a4218791efc80afccaa7eb05c06643da4a19a28c Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sun, 25 Oct 2020 14:12:59 -0400 Subject: [PATCH 02/13] Add tests --- src/__tests__/execution.test.ts | 20 + src/__tests__/subscription.test.ts | 1208 ++++++++++++++++++++++++++++ 2 files changed, 1228 insertions(+) create mode 100644 src/__tests__/subscription.test.ts diff --git a/src/__tests__/execution.test.ts b/src/__tests__/execution.test.ts index cb60a3b..9a986e9 100644 --- a/src/__tests__/execution.test.ts +++ b/src/__tests__/execution.test.ts @@ -1258,4 +1258,24 @@ describe("dx", () => { expect(isCompiledQuery(compiledQuery)).toBe(true); expect(compiledQuery.query.name).toBe("mockOperationName"); }); + test("function name of the bound query (subscription)", async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: "TypeZ", + fields: { + a: { type: GraphQLString } + } + }), + subscription: new GraphQLObjectType({ + name: "Type", + fields: { + a: { type: GraphQLString } + } + }) + }); + const document = parse(`subscription mockOperationName { a }`); + const compiledQuery = compileQuery(schema, document) as CompiledQuery; + expect(isCompiledQuery(compiledQuery)).toBe(true); + expect(compiledQuery.subscribe!.name).toBe("mockOperationName"); + }); }); diff --git a/src/__tests__/subscription.test.ts b/src/__tests__/subscription.test.ts new file mode 100644 index 0000000..f8cc8aa --- /dev/null +++ b/src/__tests__/subscription.test.ts @@ -0,0 +1,1208 @@ +/** + * Based on https://github.com/graphql/graphql-js/blob/master/src/subscription/subscribe.js + * This test suite makes an addition denoted by "*" comments: + * graphql-jit does not support the root resolver pattern that this test uses + * so the part must be rewritten to include that root resolver in `subscribe` of + * the GraphQLObject in the schema. + */ + +import { EventEmitter } from "events"; +import { + GraphQLObjectType, + GraphQLString, + GraphQLBoolean, + GraphQLInt, + GraphQLList, + GraphQLSchema, + parse, + DocumentNode, + GraphQLError, + SubscriptionArgs, + ExecutionResult +} from "graphql"; +import { compileQuery, isCompiledQuery } from "../execution"; + +const deepStrictEqual = (actual: any, expected: any) => { + expect(actual).toEqual(expected); +}; + +const strictEqual = (actual: any, expected: any) => { + return expect(actual).toBe(expected); +}; + +function eventEmitterAsyncIterator( + eventEmitter: EventEmitter, + eventName: string +): AsyncIterableIterator { + const pullQueue = [] as any; + const pushQueue = [] as any; + let listening = true; + eventEmitter.addListener(eventName, pushValue); + + function pushValue(event: any) { + if (pullQueue.length !== 0) { + pullQueue.shift()({ value: event, done: false }); + } else { + pushQueue.push(event); + } + } + + function pullValue() { + return new Promise(resolve => { + if (pushQueue.length !== 0) { + resolve({ value: pushQueue.shift(), done: false }); + } else { + pullQueue.push(resolve); + } + }); + } + + function emptyQueue() { + if (listening) { + listening = false; + eventEmitter.removeListener(eventName, pushValue); + for (const resolve of pullQueue) { + resolve({ value: undefined, done: true }); + } + pullQueue.length = 0; + pushQueue.length = 0; + } + } + + return { + next() { + return listening ? pullValue() : this.return(); + }, + return() { + emptyQueue(); + return Promise.resolve({ value: undefined, done: true }); + }, + throw(error: any) { + emptyQueue(); + return Promise.reject(error); + }, + [Symbol.asyncIterator]() { + return this; + } + } as any; +} + +async function subscribe({ + schema, + document, + operationName, + rootValue, + contextValue, + variableValues +}: SubscriptionArgs): Promise< + AsyncIterableIterator | ExecutionResult +> { + const prepared = compileQuery(schema, document, operationName || ""); + if (!isCompiledQuery(prepared)) return prepared; + return prepared.subscribe!(rootValue, contextValue, variableValues || {}); +} + +const EmailType = new GraphQLObjectType({ + name: "Email", + fields: { + from: { type: GraphQLString }, + subject: { type: GraphQLString }, + message: { type: GraphQLString }, + unread: { type: GraphQLBoolean } + } +}); + +const InboxType = new GraphQLObjectType({ + name: "Inbox", + fields: { + total: { + type: GraphQLInt, + resolve: inbox => inbox.emails.length + }, + unread: { + type: GraphQLInt, + resolve: inbox => inbox.emails.filter((email: any) => email.unread).length + }, + emails: { type: new GraphQLList(EmailType) } + } +}); + +const QueryType = new GraphQLObjectType({ + name: "Query", + fields: { + inbox: { type: InboxType } + } +}); + +const EmailEventType = new GraphQLObjectType({ + name: "EmailEvent", + fields: { + email: { type: EmailType }, + inbox: { type: InboxType } + } +}); + +const emailSchema = emailSchemaWithResolvers(); + +function emailSchemaWithResolvers( + subscribeFn?: (arg: T) => any, + resolveFn?: (arg: T) => any +) { + return new GraphQLSchema({ + query: QueryType, + subscription: new GraphQLObjectType({ + name: "Subscription", + fields: { + importantEmail: { + type: EmailEventType, + resolve: resolveFn, + subscribe: subscribeFn, + args: { + priority: { type: GraphQLInt } + } + } + } + }) + }); +} + +const defaultSubscriptionAST = parse(` + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } +`); + +async function createSubscription( + pubsub: EventEmitter, + schema?: GraphQLSchema, + document: DocumentNode = defaultSubscriptionAST +) { + const data = { + inbox: { + emails: [ + { + from: "joe@graphql.org", + subject: "Hello", + message: "Hello World", + unread: false + } + ] + }, + importantEmail() { + return eventEmitterAsyncIterator(pubsub, "importantEmail"); + } + }; + + if (!schema) { + // * + schema = emailSchemaWithResolvers(() => + eventEmitterAsyncIterator(pubsub, "importantEmail") + ); + } + + function sendImportantEmail(newEmail: any) { + data.inbox.emails.push(newEmail); + // Returns true if the event was consumed by a subscriber. + return pubsub.emit("importantEmail", { + importantEmail: { + email: newEmail, + inbox: data.inbox + } + }); + } + + // `subscribe` returns Promise + return { + sendImportantEmail, + subscription: await subscribe({ schema, document, rootValue: data }) + }; +} + +async function expectPromiseToThrow( + promise: () => Promise, + message: string +) { + try { + await promise(); + throw new Error("promise should have thrown but did not"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual(message); + } +} + +// Check all error cases when initializing the subscription. +describe("Subscription Initialization Phase", () => { + it("accepts positional arguments", async () => { + const document = parse(` + subscription { + importantEmail + } + `); + + async function* emptyAsyncIterator() { + // Empty + } + + const ai = await subscribe({ + // * + schema: emailSchemaWithResolvers(emptyAsyncIterator), + document, + rootValue: { + importantEmail: emptyAsyncIterator + } + }); + + // @ts-ignore + ai.next(); + // @ts-ignore + ai.return(); + }); + + it("accepts multiple subscription fields defined in schema", async () => { + const pubsub = new EventEmitter(); + const SubscriptionTypeMultiple = new GraphQLObjectType({ + name: "Subscription", + fields: { + // * + importantEmail: { + type: EmailEventType, + subscribe: () => eventEmitterAsyncIterator(pubsub, "importantEmail") + }, + nonImportantEmail: { + type: EmailEventType, + subscribe: () => + eventEmitterAsyncIterator(pubsub, "nonImportantEmail") + } + } + }); + + const testSchema = new GraphQLSchema({ + query: QueryType, + subscription: SubscriptionTypeMultiple + }); + + const { subscription, sendImportantEmail } = await createSubscription( + pubsub, + testSchema + ); + + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + }); + + // @ts-ignore + await subscription.next(); + }); + + it("accepts type definition with sync subscribe function", async () => { + const pubsub = new EventEmitter(); + const schema = new GraphQLSchema({ + query: QueryType, + subscription: new GraphQLObjectType({ + name: "Subscription", + fields: { + importantEmail: { + type: GraphQLString, + subscribe: () => eventEmitterAsyncIterator(pubsub, "importantEmail") + } + } + }) + }); + + const subscription = await subscribe({ + schema, + document: parse(` + subscription { + importantEmail + } + `) + }); + + pubsub.emit("importantEmail", { + importantEmail: {} + }); + + // @ts-ignore + await subscription.next(); + }); + + it("accepts type definition with async subscribe function", async () => { + const pubsub = new EventEmitter(); + const schema = new GraphQLSchema({ + query: QueryType, + subscription: new GraphQLObjectType({ + name: "Subscription", + fields: { + importantEmail: { + type: GraphQLString, + subscribe: async () => { + await new Promise(setImmediate); + return eventEmitterAsyncIterator(pubsub, "importantEmail"); + } + } + } + }) + }); + + const subscription = await subscribe({ + schema, + document: parse(` + subscription { + importantEmail + } + `) + }); + + pubsub.emit("importantEmail", { + importantEmail: {} + }); + + // @ts-ignore + await subscription.next(); + }); + + it("should only resolve the first field of invalid multi-field", async () => { + let didResolveImportantEmail = false; + let didResolveNonImportantEmail = false; + + const SubscriptionTypeMultiple = new GraphQLObjectType({ + name: "Subscription", + fields: { + importantEmail: { + type: EmailEventType, + subscribe() { + didResolveImportantEmail = true; + return eventEmitterAsyncIterator(new EventEmitter(), "event"); + } + }, + nonImportantEmail: { + type: EmailEventType, + // istanbul ignore next (Shouldn't be called) + subscribe() { + didResolveNonImportantEmail = true; + return eventEmitterAsyncIterator(new EventEmitter(), "event"); + } + } + } + }); + + const schema = new GraphQLSchema({ + query: QueryType, + subscription: SubscriptionTypeMultiple + }); + + const subscription = await subscribe({ + schema, + document: parse(` + subscription { + importantEmail + nonImportantEmail + } + `) + }); + + // @ts-ignore + subscription.next(); // Ask for a result, but ignore it. + + strictEqual(didResolveImportantEmail, true); + strictEqual(didResolveNonImportantEmail, false); + + // Close subscription + // @ts-ignore + subscription.return(); + }); + + it("resolves to an error for unknown subscription field", async () => { + const ast = parse(` + subscription { + unknownField + } + `); + + const pubsub = new EventEmitter(); + + const { subscription } = await createSubscription(pubsub, emailSchema, ast); + + deepStrictEqual(subscription, { + errors: [ + { + message: 'The subscription field "unknownField" is not defined.', + locations: [{ line: 3, column: 9 }] + } + ] + }); + }); + + it("throws an error if subscribe does not return an iterator", async () => { + const invalidEmailSchema = new GraphQLSchema({ + query: QueryType, + subscription: new GraphQLObjectType({ + name: "Subscription", + fields: { + importantEmail: { + type: GraphQLString, + subscribe: () => "test" + } + } + }) + }); + + const pubsub = new EventEmitter(); + + await expectPromiseToThrow( + () => createSubscription(pubsub, invalidEmailSchema), + 'Subscription field must return Async Iterable. Received: "test".' + ); + }); + + it("resolves to an error for subscription resolver errors", async () => { + // Returning an error + const subscriptionReturningErrorSchema = emailSchemaWithResolvers( + () => new Error("test error") + ); + await testReportsError(subscriptionReturningErrorSchema); + + // Throwing an error + const subscriptionThrowingErrorSchema = emailSchemaWithResolvers(() => { + throw new Error("test error"); + }); + await testReportsError(subscriptionThrowingErrorSchema); + + // Resolving to an error + const subscriptionResolvingErrorSchema = emailSchemaWithResolvers(() => + Promise.resolve(new Error("test error")) + ); + await testReportsError(subscriptionResolvingErrorSchema); + + // Rejecting with an error + const subscriptionRejectingErrorSchema = emailSchemaWithResolvers(() => + Promise.reject(new Error("test error")) + ); + await testReportsError(subscriptionRejectingErrorSchema); + + async function testReportsError(schema: GraphQLSchema) { + // Promise | ExecutionResult> + const result = await subscribe({ + schema, + document: parse(` + subscription { + importantEmail + } + `) + }); + + deepStrictEqual(result, { + errors: [ + { + message: "test error", + locations: [{ line: 3, column: 13 }], + path: ["importantEmail"] + } + ] + }); + } + }); + + it("resolves to an error for source event stream resolver errors", async () => { + // Returning an error + const subscriptionReturningErrorSchema = emailSchemaWithResolvers( + () => new Error("test error") + ); + await testReportsError(subscriptionReturningErrorSchema); + + // Throwing an error + const subscriptionThrowingErrorSchema = emailSchemaWithResolvers(() => { + throw new Error("test error"); + }); + await testReportsError(subscriptionThrowingErrorSchema); + + // Resolving to an error + const subscriptionResolvingErrorSchema = emailSchemaWithResolvers(() => + Promise.resolve(new Error("test error")) + ); + await testReportsError(subscriptionResolvingErrorSchema); + + // Rejecting with an error + const subscriptionRejectingErrorSchema = emailSchemaWithResolvers(() => + Promise.reject(new Error("test error")) + ); + await testReportsError(subscriptionRejectingErrorSchema); + + async function testReportsError(schema: GraphQLSchema) { + // Promise | ExecutionResult> + const result = await subscribe({ + schema, + document: parse(` + subscription { + importantEmail + } + `) + }); + + deepStrictEqual(result, { + errors: [ + { + message: "test error", + locations: [{ column: 13, line: 3 }], + path: ["importantEmail"] + } + ] + }); + } + }); + + it("resolves to an error if variables were wrong type", async () => { + // If we receive variables that cannot be coerced correctly, subscribe() + // will resolve to an ExecutionResult that contains an informative error + // description. + const ast = parse(` + subscription ($priority: Int) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + `); + + const result = await subscribe({ + schema: emailSchema, + document: ast, + variableValues: { priority: "meow" } + }); + + deepStrictEqual(result, { + errors: [ + { + // Different + message: + 'Variable "$priority" got invalid value "meow"; Expected type Int; Int cannot represent non-integer value: "meow"', + locations: [{ line: 2, column: 21 }] + } + ] + }); + }); +}); + +// Once a subscription returns a valid AsyncIterator, it can still yield +// errors. +describe("Subscription Publish Phase", () => { + it("produces a payload for multiple subscribe in same subscription", async () => { + const pubsub = new EventEmitter(); + const { sendImportantEmail, subscription } = await createSubscription( + pubsub + ); + const second = await createSubscription(pubsub); + + // @ts-ignore + const payload1 = subscription.next(); + // @ts-ignore + const payload2 = second.subscription.next(); + + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + }), + true + ); + + const expectedPayload = { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Alright" + }, + inbox: { + unread: 1, + total: 2 + } + } + } + } + }; + + deepStrictEqual(await payload1, expectedPayload); + deepStrictEqual(await payload2, expectedPayload); + }); + + it("produces a payload per subscription event", async () => { + const pubsub = new EventEmitter(); + const { sendImportantEmail, subscription } = await createSubscription( + pubsub + ); + + // Wait for the next subscription payload. + // @ts-ignore + const payload = subscription.next(); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + }), + true + ); + + // The previously waited on payload now has a value. + deepStrictEqual(await payload, { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Alright" + }, + inbox: { + unread: 1, + total: 2 + } + } + } + } + }); + + // Another new email arrives, before subscription.next() is called. + strictEqual( + sendImportantEmail({ + from: "hyo@graphql.org", + subject: "Tools", + message: "I <3 making things", + unread: true + }), + true + ); + + // The next waited on payload will have a value. + // @ts-ignore + deepStrictEqual(await subscription.next(), { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "hyo@graphql.org", + subject: "Tools" + }, + inbox: { + unread: 2, + total: 3 + } + } + } + } + }); + + // The client decides to disconnect. + // @ts-ignore + deepStrictEqual(await subscription.return(), { + done: true, + value: undefined + }); + + // Which may result in disconnecting upstream services as well. + strictEqual( + sendImportantEmail({ + from: "adam@graphql.org", + subject: "Important", + message: "Read me please", + unread: true + }), + false + ); // No more listeners. + + // Awaiting a subscription after closing it results in completed results. + // @ts-ignore + deepStrictEqual(await subscription.next(), { + done: true, + value: undefined + }); + }); + + it("produces a payload when there are multiple events", async () => { + const pubsub = new EventEmitter(); + const { sendImportantEmail, subscription } = await createSubscription( + pubsub + ); + // @ts-ignore + let payload = subscription.next(); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + }), + true + ); + + deepStrictEqual(await payload, { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Alright" + }, + inbox: { + unread: 1, + total: 2 + } + } + } + } + }); + + // @ts-ignore + payload = subscription.next(); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright 2", + message: "Tests are good 2", + unread: true + }), + true + ); + + deepStrictEqual(await payload, { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Alright 2" + }, + inbox: { + unread: 2, + total: 3 + } + } + } + } + }); + }); + + it("should not trigger when subscription is already done", async () => { + const pubsub = new EventEmitter(); + const { sendImportantEmail, subscription } = await createSubscription( + pubsub + ); + // @ts-ignore + let payload = subscription.next(); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + }), + true + ); + + deepStrictEqual(await payload, { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Alright" + }, + inbox: { + unread: 1, + total: 2 + } + } + } + } + }); + + // @ts-ignore + payload = subscription.next(); + // @ts-ignore + subscription.return(); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright 2", + message: "Tests are good 2", + unread: true + }), + false + ); + + deepStrictEqual(await payload, { + done: true, + value: undefined + }); + }); + + it("should not trigger when subscription is thrown", async () => { + const pubsub = new EventEmitter(); + const { sendImportantEmail, subscription } = await createSubscription( + pubsub + ); + // @ts-ignore + let payload = subscription.next(); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + }), + true + ); + + deepStrictEqual(await payload, { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Alright" + }, + inbox: { + unread: 1, + total: 2 + } + } + } + } + }); + + // @ts-ignore + payload = subscription.next(); + + // Throw error + let caughtError; + try { + // @ts-ignore + await subscription.throw("ouch"); + } catch (e) { + caughtError = e; + } + strictEqual(caughtError, "ouch"); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Alright 2", + message: "Tests are good 2", + unread: true + }), + false + ); + + deepStrictEqual(await payload, { + done: true, + value: undefined + }); + }); + + it("event order is correct for multiple publishes", async () => { + const pubsub = new EventEmitter(); + const { sendImportantEmail, subscription } = await createSubscription( + pubsub + ); + // @ts-ignore + let payload = subscription.next(); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Message", + message: "Tests are good", + unread: true + }), + true + ); + + // A new email arrives! + strictEqual( + sendImportantEmail({ + from: "yuzhi@graphql.org", + subject: "Message 2", + message: "Tests are good 2", + unread: true + }), + true + ); + + deepStrictEqual(await payload, { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Message" + }, + inbox: { + unread: 2, + total: 3 + } + } + } + } + }); + + // @ts-ignore + payload = subscription.next(); + + deepStrictEqual(await payload, { + done: false, + value: { + data: { + importantEmail: { + email: { + from: "yuzhi@graphql.org", + subject: "Message 2" + }, + inbox: { + unread: 2, + total: 3 + } + } + } + } + }); + }); + + it("should handle error during execution of source event", async () => { + const erroringEmailSchema = emailSchemaWithResolvers( + async function*() { + yield { email: { subject: "Hello" } }; + yield { email: { subject: "Goodbye" } }; + yield { email: { subject: "Bonjour" } }; + }, + event => { + if ((event as any).email.subject === "Goodbye") { + throw new Error("Never leave."); + } + return event; + } + ); + + const subscription = await subscribe({ + schema: erroringEmailSchema, + document: parse(` + subscription { + importantEmail { + email { + subject + } + } + } + `) + }); + + // @ts-ignore + const payload1 = await subscription.next(); + deepStrictEqual(payload1, { + done: false, + value: { + data: { + importantEmail: { + email: { + subject: "Hello" + } + } + } + } + }); + + // An error in execution is presented as such. + // @ts-ignore + const payload2 = await subscription.next(); + deepStrictEqual(payload2, { + done: false, + value: { + errors: [ + { + message: "Never leave.", + locations: [{ line: 3, column: 11 }], + path: ["importantEmail"] + } + ], + data: { + importantEmail: null + } + } + }); + + // However that does not close the response event stream. Subsequent + // events are still executed. + // @ts-ignore + const payload3 = await subscription.next(); + deepStrictEqual(payload3, { + done: false, + value: { + data: { + importantEmail: { + email: { + subject: "Bonjour" + } + } + } + } + }); + }); + + it("should pass through error thrown in source event stream", async () => { + const erroringEmailSchema = emailSchemaWithResolvers( + async function*() { + yield { email: { subject: "Hello" } }; + throw new Error("test error"); + }, + email => email + ); + + const subscription = await subscribe({ + schema: erroringEmailSchema, + document: parse(` + subscription { + importantEmail { + email { + subject + } + } + } + `) + }); + + // @ts-ignore + const payload1 = await subscription.next(); + deepStrictEqual(payload1, { + done: false, + value: { + data: { + importantEmail: { + email: { + subject: "Hello" + } + } + } + } + }); + + let expectedError; + try { + // @ts-ignore + await subscription.next(); + } catch (error) { + expectedError = error; + } + + strictEqual(expectedError instanceof Error, true); + strictEqual("message" in expectedError, true); + + // @ts-ignore + const payload2 = await subscription.next(); + deepStrictEqual(payload2, { + done: true, + value: undefined + }); + }); + + it("should resolve GraphQL error from source event stream", async () => { + const erroringEmailSchema = emailSchemaWithResolvers( + async function*() { + yield { email: { subject: "Hello" } }; + throw new GraphQLError("test error"); + }, + email => email + ); + + const subscription = await subscribe({ + schema: erroringEmailSchema, + document: parse(` + subscription { + importantEmail { + email { + subject + } + } + } + `) + }); + + // @ts-ignore + const payload1 = await subscription.next(); + deepStrictEqual(payload1, { + done: false, + value: { + data: { + importantEmail: { + email: { + subject: "Hello" + } + } + } + } + }); + + // @ts-ignore + const payload2 = await subscription.next(); + deepStrictEqual(payload2, { + done: false, + value: { + errors: [ + { + message: "test error" + } + ] + } + }); + + // @ts-ignore + const payload3 = await subscription.next(); + deepStrictEqual(payload3, { + done: true, + value: undefined + }); + }); +}); From a692f25c94e28e31e80d52008f9adc4bc0994fc8 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sun, 25 Oct 2020 14:13:58 -0400 Subject: [PATCH 03/13] run lint --- src/execution.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index b928b64..a82f372 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -26,8 +26,8 @@ import { isObjectType, isSpecifiedScalarType, Kind, - TypeNameMetaFieldDef, - locatedError + locatedError, + TypeNameMetaFieldDef } from "graphql"; import { collectFields, @@ -35,10 +35,11 @@ import { getFieldDef } from "graphql/execution/execute"; import { FieldNode, OperationDefinitionNode } from "graphql/language/ast"; +import mapAsyncIterator from "graphql/subscription/mapAsyncIterator"; import Maybe from "graphql/tsutils/Maybe"; import { GraphQLTypeResolver } from "graphql/type/definition"; -import mapAsyncIterator from "graphql/subscription/mapAsyncIterator"; +import { pathToArray } from "graphql/jsutils/Path"; import { addPath, Arguments, @@ -62,7 +63,6 @@ import { compileVariableParsing, failToParseVariables } from "./variables"; -import { pathToArray } from "graphql/jsutils/Path"; const inspect = createInspect(); @@ -390,7 +390,7 @@ async function executeSubscription( context.context, resolveInfo )); - if (eventStream instanceof Error) throw eventStream; + if (eventStream instanceof Error) { throw eventStream; } } catch (error) { throw locatedError(error, fieldNodes, pathToArray(responsePath)); } @@ -411,8 +411,9 @@ function createBoundSubscribe( getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, operationName: string | undefined ): CompiledQuery["subscribe"] | undefined { - if (compilationContext.operation.operation !== "subscription") + if (compilationContext.operation.operation !== "subscription") { return undefined; + } const { resolvers, From 6f102f1ee9cb3faf7a462c812080f1fd30b8d4a0 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sun, 25 Oct 2020 14:17:17 -0400 Subject: [PATCH 04/13] Run format --- src/execution.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/execution.ts b/src/execution.ts index a82f372..6d481e4 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -390,7 +390,9 @@ async function executeSubscription( context.context, resolveInfo )); - if (eventStream instanceof Error) { throw eventStream; } + if (eventStream instanceof Error) { + throw eventStream; + } } catch (error) { throw locatedError(error, fieldNodes, pathToArray(responsePath)); } From c5973f1cf8df5509e2e7377dffb2c037ca6964a8 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sun, 25 Oct 2020 14:28:17 -0400 Subject: [PATCH 05/13] Update test --- src/__tests__/subscription.test.ts | 154 +++++++++++++---------------- 1 file changed, 67 insertions(+), 87 deletions(-) diff --git a/src/__tests__/subscription.test.ts b/src/__tests__/subscription.test.ts index f8cc8aa..e9aad11 100644 --- a/src/__tests__/subscription.test.ts +++ b/src/__tests__/subscription.test.ts @@ -22,14 +22,6 @@ import { } from "graphql"; import { compileQuery, isCompiledQuery } from "../execution"; -const deepStrictEqual = (actual: any, expected: any) => { - expect(actual).toEqual(expected); -}; - -const strictEqual = (actual: any, expected: any) => { - return expect(actual).toBe(expected); -}; - function eventEmitterAsyncIterator( eventEmitter: EventEmitter, eventName: string @@ -417,8 +409,8 @@ describe("Subscription Initialization Phase", () => { // @ts-ignore subscription.next(); // Ask for a result, but ignore it. - strictEqual(didResolveImportantEmail, true); - strictEqual(didResolveNonImportantEmail, false); + expect(didResolveImportantEmail).toBe(true); + expect(didResolveNonImportantEmail).toBe(false); // Close subscription // @ts-ignore @@ -436,7 +428,7 @@ describe("Subscription Initialization Phase", () => { const { subscription } = await createSubscription(pubsub, emailSchema, ast); - deepStrictEqual(subscription, { + expect(subscription).toEqual({ errors: [ { message: 'The subscription field "unknownField" is not defined.', @@ -504,7 +496,7 @@ describe("Subscription Initialization Phase", () => { `) }); - deepStrictEqual(result, { + expect(result).toEqual({ errors: [ { message: "test error", @@ -552,7 +544,7 @@ describe("Subscription Initialization Phase", () => { `) }); - deepStrictEqual(result, { + expect(result).toEqual({ errors: [ { message: "test error", @@ -589,7 +581,7 @@ describe("Subscription Initialization Phase", () => { variableValues: { priority: "meow" } }); - deepStrictEqual(result, { + expect(result).toEqual({ errors: [ { // Different @@ -617,15 +609,14 @@ describe("Subscription Publish Phase", () => { // @ts-ignore const payload2 = second.subscription.next(); - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true - }), - true - ); + }) + ).toBe(true); const expectedPayload = { done: false, @@ -645,8 +636,8 @@ describe("Subscription Publish Phase", () => { } }; - deepStrictEqual(await payload1, expectedPayload); - deepStrictEqual(await payload2, expectedPayload); + expect(await payload1).toEqual(expectedPayload); + expect(await payload2).toEqual(expectedPayload); }); it("produces a payload per subscription event", async () => { @@ -660,18 +651,17 @@ describe("Subscription Publish Phase", () => { const payload = subscription.next(); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true - }), - true - ); + }) + ).toBe(true); // The previously waited on payload now has a value. - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: false, value: { data: { @@ -690,19 +680,18 @@ describe("Subscription Publish Phase", () => { }); // Another new email arrives, before subscription.next() is called. - strictEqual( + expect( sendImportantEmail({ from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", unread: true - }), - true - ); + }) + ).toBe(true); // The next waited on payload will have a value. // @ts-ignore - deepStrictEqual(await subscription.next(), { + expect(await subscription.next()).toEqual({ done: false, value: { data: { @@ -722,25 +711,24 @@ describe("Subscription Publish Phase", () => { // The client decides to disconnect. // @ts-ignore - deepStrictEqual(await subscription.return(), { + expect(await subscription.return()).toEqual({ done: true, value: undefined }); // Which may result in disconnecting upstream services as well. - strictEqual( + expect( sendImportantEmail({ from: "adam@graphql.org", subject: "Important", message: "Read me please", unread: true - }), - false - ); // No more listeners. + }) + ).toBe(false); // No more listeners. // Awaiting a subscription after closing it results in completed results. // @ts-ignore - deepStrictEqual(await subscription.next(), { + expect(await subscription.next()).toEqual({ done: true, value: undefined }); @@ -755,17 +743,16 @@ describe("Subscription Publish Phase", () => { let payload = subscription.next(); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true - }), - true - ); + }) + ).toBe(true); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: false, value: { data: { @@ -787,17 +774,16 @@ describe("Subscription Publish Phase", () => { payload = subscription.next(); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright 2", message: "Tests are good 2", unread: true - }), - true - ); + }) + ).toBe(true); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: false, value: { data: { @@ -825,17 +811,16 @@ describe("Subscription Publish Phase", () => { let payload = subscription.next(); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true - }), - true - ); + }) + ).toBe(true); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: false, value: { data: { @@ -859,17 +844,16 @@ describe("Subscription Publish Phase", () => { subscription.return(); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright 2", message: "Tests are good 2", unread: true - }), - false - ); + }) + ).toBe(false); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: true, value: undefined }); @@ -884,17 +868,16 @@ describe("Subscription Publish Phase", () => { let payload = subscription.next(); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true - }), - true - ); + }) + ).toBe(true); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: false, value: { data: { @@ -923,20 +906,19 @@ describe("Subscription Publish Phase", () => { } catch (e) { caughtError = e; } - strictEqual(caughtError, "ouch"); + expect(caughtError).toBe("ouch"); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Alright 2", message: "Tests are good 2", unread: true - }), - false - ); + }) + ).toBe(false); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: true, value: undefined }); @@ -951,28 +933,26 @@ describe("Subscription Publish Phase", () => { let payload = subscription.next(); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Message", message: "Tests are good", unread: true - }), - true - ); + }) + ).toBe(true); // A new email arrives! - strictEqual( + expect( sendImportantEmail({ from: "yuzhi@graphql.org", subject: "Message 2", message: "Tests are good 2", unread: true - }), - true - ); + }) + ).toBe(true); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: false, value: { data: { @@ -993,7 +973,7 @@ describe("Subscription Publish Phase", () => { // @ts-ignore payload = subscription.next(); - deepStrictEqual(await payload, { + expect(await payload).toEqual({ done: false, value: { data: { @@ -1042,7 +1022,7 @@ describe("Subscription Publish Phase", () => { // @ts-ignore const payload1 = await subscription.next(); - deepStrictEqual(payload1, { + expect(payload1).toEqual({ done: false, value: { data: { @@ -1058,7 +1038,7 @@ describe("Subscription Publish Phase", () => { // An error in execution is presented as such. // @ts-ignore const payload2 = await subscription.next(); - deepStrictEqual(payload2, { + expect(payload2).toEqual({ done: false, value: { errors: [ @@ -1078,7 +1058,7 @@ describe("Subscription Publish Phase", () => { // events are still executed. // @ts-ignore const payload3 = await subscription.next(); - deepStrictEqual(payload3, { + expect(payload3).toEqual({ done: false, value: { data: { @@ -1116,7 +1096,7 @@ describe("Subscription Publish Phase", () => { // @ts-ignore const payload1 = await subscription.next(); - deepStrictEqual(payload1, { + expect(payload1).toEqual({ done: false, value: { data: { @@ -1137,12 +1117,12 @@ describe("Subscription Publish Phase", () => { expectedError = error; } - strictEqual(expectedError instanceof Error, true); - strictEqual("message" in expectedError, true); + expect(expectedError instanceof Error).toBe(true); + expect("message" in expectedError).toBe(true); // @ts-ignore const payload2 = await subscription.next(); - deepStrictEqual(payload2, { + expect(payload2).toEqual({ done: true, value: undefined }); @@ -1172,7 +1152,7 @@ describe("Subscription Publish Phase", () => { // @ts-ignore const payload1 = await subscription.next(); - deepStrictEqual(payload1, { + expect(payload1).toEqual({ done: false, value: { data: { @@ -1187,7 +1167,7 @@ describe("Subscription Publish Phase", () => { // @ts-ignore const payload2 = await subscription.next(); - deepStrictEqual(payload2, { + expect(payload2).toEqual({ done: false, value: { errors: [ @@ -1200,7 +1180,7 @@ describe("Subscription Publish Phase", () => { // @ts-ignore const payload3 = await subscription.next(); - deepStrictEqual(payload3, { + expect(payload3).toEqual({ done: true, value: undefined }); From 465d8f511c5f60ac7f3f9f4c24dcb10288b12e7f Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sat, 30 Jan 2021 12:55:49 +0700 Subject: [PATCH 06/13] Only set subscribe property if operation is a subscription --- src/__tests__/execution.test.ts | 20 --------------- src/__tests__/subscription.test.ts | 32 +++++++++++++++++++++++- src/execution.ts | 40 ++++++++++++++---------------- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/__tests__/execution.test.ts b/src/__tests__/execution.test.ts index f7933f1..4e15b8d 100644 --- a/src/__tests__/execution.test.ts +++ b/src/__tests__/execution.test.ts @@ -1258,24 +1258,4 @@ describe("dx", () => { expect(isCompiledQuery(compiledQuery)).toBe(true); expect(compiledQuery.query.name).toBe("mockOperationName"); }); - test("function name of the bound query (subscription)", async () => { - const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - name: "TypeZ", - fields: { - a: { type: GraphQLString } - } - }), - subscription: new GraphQLObjectType({ - name: "Type", - fields: { - a: { type: GraphQLString } - } - }) - }); - const document = parse(`subscription mockOperationName { a }`); - const compiledQuery = compileQuery(schema, document) as CompiledQuery; - expect(isCompiledQuery(compiledQuery)).toBe(true); - expect(compiledQuery.subscribe!.name).toBe("mockOperationName"); - }); }); diff --git a/src/__tests__/subscription.test.ts b/src/__tests__/subscription.test.ts index e9aad11..9f935c6 100644 --- a/src/__tests__/subscription.test.ts +++ b/src/__tests__/subscription.test.ts @@ -20,7 +20,7 @@ import { SubscriptionArgs, ExecutionResult } from "graphql"; -import { compileQuery, isCompiledQuery } from "../execution"; +import { CompiledQuery, compileQuery, isCompiledQuery } from "../execution"; function eventEmitterAsyncIterator( eventEmitter: EventEmitter, @@ -1186,3 +1186,33 @@ describe("Subscription Publish Phase", () => { }); }); }); + +describe("dx", () => { + test("function name of the bound query", async () => { + const schema = new GraphQLSchema({ + subscription: new GraphQLObjectType({ + name: "Type", + fields: { + a: { type: GraphQLString } + } + }) + }); + const document = parse(`subscription mockOperationName { a }`); + const compiledQuery = compileQuery(schema, document) as CompiledQuery; + expect(isCompiledQuery(compiledQuery)).toBe(true); + expect(compiledQuery.subscribe!.name).toBe("mockOperationName"); + }); + test("sets subscribe property only if operation is a subscription", () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: "TypeZ", + fields: { + a: { type: GraphQLString } + } + }) + }); + const document = parse(`query mockOperationName { a }`); + const compiledQuery = compileQuery(schema, document) as CompiledQuery; + expect(compiledQuery).not.toHaveProperty("subscribe"); + }); +}); diff --git a/src/execution.ts b/src/execution.ts index bed3a5e..279bb20 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -249,28 +249,30 @@ export function compileQuery( const functionBody = compileOperation(context); - const query = createBoundQuery( - context, - document, - new Function("return " + functionBody)(), - getVariables, - context.operation.name != null ? context.operation.name.value : undefined - ); - // Subscription const compiledQuery: InternalCompiledQuery = { - query, - subscribe: createBoundSubscribe( + query: createBoundQuery( context, document, - query, + new Function("return " + functionBody)(), getVariables, - context.operation.name != null - ? context.operation.name.value - : undefined + context.operation.name != null ? context.operation.name.value : undefined ), stringify }; + + if (context.operation.operation === "subscription") { + compiledQuery.subscribe = createBoundSubscribe( + context, + document, + compiledQuery.query, + getVariables, + context.operation.name != null + ? context.operation.name.value + : undefined + ); + } + if ((options as any).debug) { // result of the compilation useful for debugging issues // and visualization tools like try-jit. @@ -335,8 +337,8 @@ async function executeSubscription( context: ExecutionContext, compileContext: CompilationContext ): Promise> { - // TODO: We are doing the same thing in compileOperation, but since - // it does not expose any of its sideeffect, we have to do it again + // TODO: We are doing the same thing in compileOperation, so we + // should find a way to reuse those results const type = getOperationRootType( compileContext.schema, compileContext.operation @@ -415,11 +417,7 @@ function createBoundSubscribe( queryFn: CompiledQuery["query"], getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, operationName: string | undefined -): CompiledQuery["subscribe"] | undefined { - if (compilationContext.operation.operation !== "subscription") { - return undefined; - } - +): CompiledQuery["subscribe"] { const { resolvers, typeResolvers, From eaa4dcecdf0025bc73e31da71de0bb334ce1c0d1 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sat, 30 Jan 2021 14:49:45 +0700 Subject: [PATCH 07/13] format and use our getFieldDef impl --- src/execution.ts | 425 +++++++++++++++++++++++------------------------ 1 file changed, 209 insertions(+), 216 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 279bb20..03b0894 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -30,10 +30,7 @@ import { locatedError, TypeNameMetaFieldDef } from "graphql"; -import { - ExecutionContext as GraphQLContext, - getFieldDef -} from "graphql/execution/execute"; +import { ExecutionContext as GraphQLContext } from "graphql/execution/execute"; import { FieldNode, OperationDefinitionNode } from "graphql/language/ast"; import mapAsyncIterator from "graphql/subscription/mapAsyncIterator"; import { GraphQLTypeResolver } from "graphql/type/definition"; @@ -249,14 +246,15 @@ export function compileQuery( const functionBody = compileOperation(context); - // Subscription const compiledQuery: InternalCompiledQuery = { query: createBoundQuery( context, document, new Function("return " + functionBody)(), getVariables, - context.operation.name != null ? context.operation.name.value : undefined + context.operation.name != null + ? context.operation.name.value + : undefined ), stringify }; @@ -293,216 +291,6 @@ export function isCompiledQuery< return "query" in query && typeof query.query === "function"; } -/** - * Subscription - * Implements the "CreateSourceEventStream" algorithm described in the - * GraphQL specification, resolving the subscription source event stream. - * - * Returns a Promise which resolves to either an AsyncIterable (if successful) - * or an ExecutionResult (error). The promise will be rejected if the schema or - * other arguments to this function are invalid, or if the resolved event stream - * is not an async iterable. - * - * If the client-provided arguments to this function do not result in a - * compliant subscription, a GraphQL Response (ExecutionResult) with - * descriptive errors and no data will be returned. - * - * If the the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the promise will resolve to a single - * ExecutionResult containing `errors` and no `data`. - * - * If the operation succeeded, the promise resolves to the AsyncIterable for the - * event stream returned by the resolver. - * - * A Source Event Stream represents a sequence of events, each of which triggers - * a GraphQL execution for that event. - * - * This may be useful when hosting the stateful subscription service in a - * different process or machine than the stateless GraphQL execution engine, - * or otherwise separating these two steps. For more on this, see the - * "Supporting Subscriptions at Scale" information in the GraphQL specification. - * - * Since createSourceEventStream only builds execution context and reports errors - * in doing so, which we did, we simply call directly the later called - * executeSubscription. - */ - -function isAsyncIterable( - val: unknown -): val is AsyncIterableIterator { - return typeof Object(val)[Symbol.asyncIterator] === "function"; -} - -async function executeSubscription( - context: ExecutionContext, - compileContext: CompilationContext -): Promise> { - // TODO: We are doing the same thing in compileOperation, so we - // should find a way to reuse those results - const type = getOperationRootType( - compileContext.schema, - compileContext.operation - ); - - const fields = collectFields( - compileContext, - type, - compileContext.operation.selectionSet, - Object.create(null), - Object.create(null) - ); - - const responseNames = Object.keys(fields); - const responseName = responseNames[0]; - const fieldNodes = fields[responseName]; - const fieldNode = fieldNodes[0]; - const fieldName = fieldNode.name.value; - const fieldDef = getFieldDef(compileContext.schema, type, fieldName); - - if (!fieldDef) { - throw new GraphQLError( - `The subscription field "${fieldName}" is not defined.`, - fieldNodes - ); - } - - const responsePath = addPath(undefined, fieldName); - - const resolveInfo = createResolveInfoThunk({ - schema: compileContext.schema, - fragments: compileContext.fragments, - operation: compileContext.operation, - parentType: type, - fieldName, - fieldType: fieldDef.type, - fieldNodes - })(context.rootValue, context.variables, serializeResponsePath(responsePath)); - - // Call the `subscribe()` resolver or the default resolver to produce an - // AsyncIterable yielding raw payloads. - - // TODO: rootValue resolver and value is not supported - const subscriber = fieldDef.subscribe; - - let eventStream; - - try { - eventStream = - subscriber && - (await subscriber( - context.rootValue, - context.variables, - context.context, - resolveInfo - )); - if (eventStream instanceof Error) { - throw eventStream; - } - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(responsePath)); - } - - if (!isAsyncIterable(eventStream)) { - throw new Error( - "Subscription field must return Async Iterable. " + - `Received: ${inspect(eventStream)}.` - ); - } - return eventStream; -} - -function createBoundSubscribe( - compilationContext: CompilationContext, - document: DocumentNode, - queryFn: CompiledQuery["query"], - getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, - operationName: string | undefined -): CompiledQuery["subscribe"] { - const { - resolvers, - typeResolvers, - isTypeOfs, - serializers, - resolveInfos - } = compilationContext; - const trimmer = createNullTrimmer(compilationContext); - const fnName = operationName ? operationName : "subscribe"; - - const ret = { - async [fnName]( - rootValue: any, - context: any, - variables: Maybe<{ [key: string]: any }> - ): Promise | ExecutionResult> { - // this can be shared across in a batch request - const parsedVariables = getVariableValues(variables || {}); - - // Return early errors if variable coercing failed. - if (failToParseVariables(parsedVariables)) { - return { errors: parsedVariables.errors }; - } - - const executionContext: ExecutionContext = { - rootValue, - context, - variables: parsedVariables.coerced, - safeMap, - inspect, - GraphQLError: GraphqlJitError, - resolvers, - typeResolvers, - isTypeOfs, - serializers, - resolveInfos, - trimmer, - promiseCounter: 0, - nullErrors: [], - errors: [], - data: {} - }; - - function reportGraphQLError(error: any): ExecutionResult { - if (error instanceof GraphQLError) { - return { - errors: [error] - }; - } - throw error; - } - - let resultOrStream: AsyncIterableIterator; - - try { - resultOrStream = await executeSubscription( - executionContext, - compilationContext - ); - } catch (e) { - return reportGraphQLError(e); - } - - // For each payload yielded from a subscription, map it over the normal - // GraphQL `execute` function, with `payload` as the rootValue. - // This implements the "MapSourceToResponseEvent" algorithm described in - // 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. - // We use our `query` function in place of `execute` - - const mapSourceToResponse = (payload: any) => - queryFn(payload, context, variables); - - return mapAsyncIterator( - resultOrStream, - mapSourceToResponse, - reportGraphQLError - ); - } - }; - - return ret[fnName]; -} - // Exported only for an error test export function createBoundQuery( compilationContext: CompilationContext, @@ -617,6 +405,7 @@ function compileOperation(context: CompilationContext) { Object.create(null), Object.create(null) ); + const topLevel = compileObjectType( context, type, @@ -1876,3 +1665,207 @@ function getParentArgIndexes(context: CompilationContext) { function getJsFieldName(fieldName: string) { return `${LOCAL_JS_FIELD_NAME_PREFIX}${fieldName}`; } + +/** + * Subscription + * Implements the "CreateSourceEventStream" algorithm described in the + * GraphQL specification, resolving the subscription source event stream. + * + * Returns a Promise which resolves to either an AsyncIterable (if successful) + * or an ExecutionResult (error). The promise will be rejected if the schema or + * other arguments to this function are invalid, or if the resolved event stream + * is not an async iterable. + * + * If the client-provided arguments to this function do not result in a + * compliant subscription, a GraphQL Response (ExecutionResult) with + * descriptive errors and no data will be returned. + * + * If the the source stream could not be created due to faulty subscription + * resolver logic or underlying systems, the promise will resolve to a single + * ExecutionResult containing `errors` and no `data`. + * + * If the operation succeeded, the promise resolves to the AsyncIterable for the + * event stream returned by the resolver. + * + * A Source Event Stream represents a sequence of events, each of which triggers + * a GraphQL execution for that event. + * + * This may be useful when hosting the stateful subscription service in a + * different process or machine than the stateless GraphQL execution engine, + * or otherwise separating these two steps. For more on this, see the + * "Supporting Subscriptions at Scale" information in the GraphQL specification. + * + * Since createSourceEventStream only builds execution context and reports errors + * in doing so, which we did, we simply call directly the later called + * executeSubscription. + */ + +function isAsyncIterable( + val: unknown +): val is AsyncIterableIterator { + return typeof Object(val)[Symbol.asyncIterator] === "function"; +} + +async function executeSubscription( + context: ExecutionContext, + compileContext: CompilationContext +): Promise> { + // TODO: We are doing the same thing in compileOperation, so we + // should find a way to reuse those results + const type = getOperationRootType( + compileContext.schema, + compileContext.operation + ); + + const fieldMap = collectFields( + compileContext, + type, + compileContext.operation.selectionSet, + Object.create(null), + Object.create(null) + ); + + const fieldNodes = Object.values(fieldMap)[0]; + const fieldNode = fieldNodes[0]; + const fieldName = fieldNode.name.value; + + const field = resolveFieldDef(compileContext, type, fieldNodes); + + if (!field) { + throw new GraphQLError( + `The subscription field "${fieldName}" is not defined.`, + fieldNodes + ); + } + + const responsePath = addPath(undefined, fieldName); + + const resolveInfoName = createResolveInfoName(responsePath); + const resolveInfo = context.resolveInfos[resolveInfoName]( + context.rootValue, + context.variables, + responsePath + ); + + // Call the `subscribe()` resolver or the default resolver to produce an + // AsyncIterable yielding raw payloads. + const subscriber = field.subscribe; + + let eventStream; + + try { + eventStream = + subscriber && + (await subscriber( + context.rootValue, + context.variables, + context.context, + resolveInfo + )); + if (eventStream instanceof Error) { + throw eventStream; + } + } catch (error) { + throw locatedError(error, fieldNodes, pathToArray(responsePath)); + } + + if (!isAsyncIterable(eventStream)) { + throw new Error( + "Subscription field must return Async Iterable. " + + `Received: ${inspect(eventStream)}.` + ); + } + return eventStream; +} + +function createBoundSubscribe( + compilationContext: CompilationContext, + document: DocumentNode, + queryFn: CompiledQuery["query"], + getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, + operationName: string | undefined +): CompiledQuery["subscribe"] { + const { + resolvers, + typeResolvers, + isTypeOfs, + serializers, + resolveInfos + } = compilationContext; + const trimmer = createNullTrimmer(compilationContext); + const fnName = operationName ? operationName : "subscribe"; + + const ret = { + async [fnName]( + rootValue: any, + context: any, + variables: Maybe<{ [key: string]: any }> + ): Promise | ExecutionResult> { + // this can be shared across in a batch request + const parsedVariables = getVariableValues(variables || {}); + + // Return early errors if variable coercing failed. + if (failToParseVariables(parsedVariables)) { + return { errors: parsedVariables.errors }; + } + + const executionContext: ExecutionContext = { + rootValue, + context, + variables: parsedVariables.coerced, + safeMap, + inspect, + GraphQLError: GraphqlJitError, + resolvers, + typeResolvers, + isTypeOfs, + serializers, + resolveInfos, + trimmer, + promiseCounter: 0, + nullErrors: [], + errors: [], + data: {} + }; + + function reportGraphQLError(error: any): ExecutionResult { + if (error instanceof GraphQLError) { + return { + errors: [error] + }; + } + throw error; + } + + let resultOrStream: AsyncIterableIterator; + + try { + resultOrStream = await executeSubscription( + executionContext, + compilationContext + ); + } catch (e) { + return reportGraphQLError(e); + } + + // For each payload yielded from a subscription, map it over the normal + // GraphQL `execute` function, with `payload` as the rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // 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. + // We use our `query` function in place of `execute` + + const mapSourceToResponse = (payload: any) => + queryFn(payload, context, variables); + + return mapAsyncIterator( + resultOrStream, + mapSourceToResponse, + reportGraphQLError + ); + } + }; + + return ret[fnName]; +} From ede12f26e960fbbef87b7a1eeac01a234838f296 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sat, 30 Jan 2021 15:28:29 +0700 Subject: [PATCH 08/13] use toMatchObject to expose difference --- src/__tests__/subscription.test.ts | 4 ++-- src/execution.ts | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/__tests__/subscription.test.ts b/src/__tests__/subscription.test.ts index 9f935c6..6d44ffe 100644 --- a/src/__tests__/subscription.test.ts +++ b/src/__tests__/subscription.test.ts @@ -1,5 +1,5 @@ /** - * Based on https://github.com/graphql/graphql-js/blob/master/src/subscription/subscribe.js + * Based on https://github.com/graphql/graphql-js/blob/main/src/subscription/__tests__/subscribe-test.js * This test suite makes an addition denoted by "*" comments: * graphql-jit does not support the root resolver pattern that this test uses * so the part must be rewritten to include that root resolver in `subscribe` of @@ -544,7 +544,7 @@ describe("Subscription Initialization Phase", () => { `) }); - expect(result).toEqual({ + expect(result).toMatchObject({ errors: [ { message: "test error", diff --git a/src/execution.ts b/src/execution.ts index 03b0894..7615c27 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -31,11 +31,10 @@ import { TypeNameMetaFieldDef } from "graphql"; import { ExecutionContext as GraphQLContext } from "graphql/execution/execute"; +import { pathToArray } from "graphql/jsutils/Path"; import { FieldNode, OperationDefinitionNode } from "graphql/language/ast"; import mapAsyncIterator from "graphql/subscription/mapAsyncIterator"; import { GraphQLTypeResolver } from "graphql/type/definition"; - -import { pathToArray } from "graphql/jsutils/Path"; import { addPath, Arguments, @@ -191,7 +190,6 @@ interface InternalCompiledQuery extends CompiledQuery { * @param partialOptions compilation options to tune the compiler features * @returns {CompiledQuery} the cacheable result */ - export function compileQuery( schema: GraphQLSchema, document: DocumentNode, @@ -212,7 +210,6 @@ export function compileQuery( ) { throw new Error("resolverInfoEnricher must be a function"); } - try { const options = { disablingCapturingStackErrors: false, @@ -238,14 +235,12 @@ export function compileQuery( } else { stringify = JSON.stringify; } - const getVariables = compileVariableParsing( schema, context.operation.variableDefinitions || [] ); const functionBody = compileOperation(context); - const compiledQuery: InternalCompiledQuery = { query: createBoundQuery( context, @@ -405,7 +400,6 @@ function compileOperation(context: CompilationContext) { Object.create(null), Object.create(null) ); - const topLevel = compileObjectType( context, type, @@ -417,7 +411,6 @@ function compileOperation(context: CompilationContext) { fieldMap, true ); - let body = `function query (${GLOBAL_EXECUTION_CONTEXT}) { "use strict"; `; From 9c6fd1e83eb3ed3ee7f055fbff1e6988393e6f55 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sat, 30 Jan 2021 16:27:56 +0700 Subject: [PATCH 09/13] Indent subscription query to match graphql-js test value --- src/__tests__/subscription.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/__tests__/subscription.test.ts b/src/__tests__/subscription.test.ts index 6d44ffe..2c2c8ea 100644 --- a/src/__tests__/subscription.test.ts +++ b/src/__tests__/subscription.test.ts @@ -486,7 +486,6 @@ describe("Subscription Initialization Phase", () => { await testReportsError(subscriptionRejectingErrorSchema); async function testReportsError(schema: GraphQLSchema) { - // Promise | ExecutionResult> const result = await subscribe({ schema, document: parse(` @@ -539,12 +538,12 @@ describe("Subscription Initialization Phase", () => { schema, document: parse(` subscription { - importantEmail + importantEmail } `) }); - expect(result).toMatchObject({ + expect(result).toEqual({ errors: [ { message: "test error", From 28a34e13e6c4ca59f48fc87282e29c6943efb843 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sat, 30 Jan 2021 17:56:53 +0700 Subject: [PATCH 10/13] refactor to reuse rootType and fieldMap --- src/execution.ts | 186 +++++++++++++++++++++++------------------------ 1 file changed, 92 insertions(+), 94 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index 7615c27..f09cd6a 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -240,7 +240,17 @@ export function compileQuery( context.operation.variableDefinitions || [] ); - const functionBody = compileOperation(context); + const type = getOperationRootType(context.schema, context.operation); + const fieldMap = collectFields( + context, + type, + context.operation.selectionSet, + Object.create(null), + Object.create(null) + ); + + const functionBody = compileOperation(context, type, fieldMap); + const compiledQuery: InternalCompiledQuery = { query: createBoundQuery( context, @@ -258,7 +268,12 @@ export function compileQuery( compiledQuery.subscribe = createBoundSubscribe( context, document, - compiledQuery.query, + compileSubscriptionOperation( + context, + type, + fieldMap, + compiledQuery.query + ), getVariables, context.operation.name != null ? context.operation.name.value @@ -390,16 +405,12 @@ function postProcessResult({ * @param {CompilationContext} context compilation context with the execution context * @returns {string} a function body to be instantiated together with the header, footer */ -function compileOperation(context: CompilationContext) { - const type = getOperationRootType(context.schema, context.operation); +function compileOperation( + context: CompilationContext, + type: GraphQLObjectType, + fieldMap: FieldsAndNodes +) { const serialExecution = context.operation.operation === "mutation"; - const fieldMap = collectFields( - context, - type, - context.operation.selectionSet, - Object.create(null), - Object.create(null) - ); const topLevel = compileObjectType( context, type, @@ -1699,30 +1710,26 @@ function isAsyncIterable( return typeof Object(val)[Symbol.asyncIterator] === "function"; } -async function executeSubscription( - context: ExecutionContext, - compileContext: CompilationContext -): Promise> { - // TODO: We are doing the same thing in compileOperation, so we - // should find a way to reuse those results - const type = getOperationRootType( - compileContext.schema, - compileContext.operation - ); - - const fieldMap = collectFields( - compileContext, - type, - compileContext.operation.selectionSet, - Object.create(null), - Object.create(null) - ); +function compileSubscriptionOperation( + context: CompilationContext, + type: GraphQLObjectType, + fieldMap: FieldsAndNodes, + queryFn: CompiledQuery["query"] +) { + function reportGraphQLError(error: any): ExecutionResult { + if (error instanceof GraphQLError) { + return { + errors: [error] + }; + } + throw error; + } const fieldNodes = Object.values(fieldMap)[0]; const fieldNode = fieldNodes[0]; const fieldName = fieldNode.name.value; - const field = resolveFieldDef(compileContext, type, fieldNodes); + const field = resolveFieldDef(context, type, fieldNodes); if (!field) { throw new GraphQLError( @@ -1732,49 +1739,75 @@ async function executeSubscription( } const responsePath = addPath(undefined, fieldName); - const resolveInfoName = createResolveInfoName(responsePath); - const resolveInfo = context.resolveInfos[resolveInfoName]( - context.rootValue, - context.variables, - responsePath - ); // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. const subscriber = field.subscribe; - let eventStream; + return async function subscribe(executionContext: ExecutionContext) { + const resolveInfo = executionContext.resolveInfos[resolveInfoName]( + executionContext.rootValue, + executionContext.variables, + responsePath + ); - try { - eventStream = - subscriber && - (await subscriber( - context.rootValue, - context.variables, - context.context, - resolveInfo - )); - if (eventStream instanceof Error) { - throw eventStream; + let resultOrStream: AsyncIterableIterator; + + try { + try { + resultOrStream = + subscriber && + (await subscriber( + executionContext.rootValue, + executionContext.variables, + executionContext.context, + resolveInfo + )); + if (resultOrStream instanceof Error) { + throw resultOrStream; + } + } catch (error) { + throw locatedError( + error, + resolveInfo.fieldNodes, + pathToArray(resolveInfo.path) + ); + } + if (!isAsyncIterable(resultOrStream)) { + throw new Error( + "Subscription field must return Async Iterable. " + + `Received: ${inspect(resultOrStream)}.` + ); + } + } catch (e) { + return reportGraphQLError(e); } - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(responsePath)); - } - if (!isAsyncIterable(eventStream)) { - throw new Error( - "Subscription field must return Async Iterable. " + - `Received: ${inspect(eventStream)}.` + // For each payload yielded from a subscription, map it over the normal + // GraphQL `execute` function, with `payload` as the rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // 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. + // We use our `query` function in place of `execute` + const mapSourceToResponse = (payload: any) => + queryFn(payload, executionContext.context, executionContext.variables); + + return mapAsyncIterator( + resultOrStream, + mapSourceToResponse, + reportGraphQLError ); - } - return eventStream; + }; } function createBoundSubscribe( compilationContext: CompilationContext, document: DocumentNode, - queryFn: CompiledQuery["query"], + func: ( + context: ExecutionContext + ) => Promise | ExecutionResult>, getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, operationName: string | undefined ): CompiledQuery["subscribe"] { @@ -1821,42 +1854,7 @@ function createBoundSubscribe( data: {} }; - function reportGraphQLError(error: any): ExecutionResult { - if (error instanceof GraphQLError) { - return { - errors: [error] - }; - } - throw error; - } - - let resultOrStream: AsyncIterableIterator; - - try { - resultOrStream = await executeSubscription( - executionContext, - compilationContext - ); - } catch (e) { - return reportGraphQLError(e); - } - - // For each payload yielded from a subscription, map it over the normal - // GraphQL `execute` function, with `payload` as the rootValue. - // This implements the "MapSourceToResponseEvent" algorithm described in - // 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. - // We use our `query` function in place of `execute` - - const mapSourceToResponse = (payload: any) => - queryFn(payload, context, variables); - - return mapAsyncIterator( - resultOrStream, - mapSourceToResponse, - reportGraphQLError - ); + return func.call(null, executionContext); } }; From 04653617576273193313fb571ba0e363cd19f188 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Mon, 8 Mar 2021 03:30:35 +0700 Subject: [PATCH 11/13] test(subscription): Always forward variableValues --- src/__tests__/subscription.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/__tests__/subscription.test.ts b/src/__tests__/subscription.test.ts index 2c2c8ea..65cbd24 100644 --- a/src/__tests__/subscription.test.ts +++ b/src/__tests__/subscription.test.ts @@ -8,17 +8,17 @@ import { EventEmitter } from "events"; import { - GraphQLObjectType, - GraphQLString, + DocumentNode, + ExecutionResult, GraphQLBoolean, + GraphQLError, GraphQLInt, GraphQLList, + GraphQLObjectType, GraphQLSchema, + GraphQLString, parse, - DocumentNode, - GraphQLError, - SubscriptionArgs, - ExecutionResult + SubscriptionArgs } from "graphql"; import { CompiledQuery, compileQuery, isCompiledQuery } from "../execution"; @@ -91,7 +91,7 @@ async function subscribe({ > { const prepared = compileQuery(schema, document, operationName || ""); if (!isCompiledQuery(prepared)) return prepared; - return prepared.subscribe!(rootValue, contextValue, variableValues || {}); + return prepared.subscribe!(rootValue, contextValue, variableValues); } const EmailType = new GraphQLObjectType({ From 924e2e94ce22f6a630111dcbdac17bd88ede22df Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Mon, 8 Mar 2021 03:40:41 +0700 Subject: [PATCH 12/13] Update README --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e52d393..910aa65 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,18 @@ if (!isCompiledQuery(compiledQuery)) { #### Execute the Query ```js -const executionResult = await compiledQuery.query(); +const executionResult = await compiledQuery.query(root, context, variables); console.log(executionResult); ``` +#### Subscribe to the Query + +```js +const result = await compiledQuery.subscribe(root, context, variables); +for await (const value of result) { + console.log(value); +} +``` ## API ### compiledQuery = compileQuery(schema, document, operationName, compilerOptions) @@ -112,14 +120,18 @@ Compiles the `document` AST, using an optional operationName and compiler option for overly expensive serializers - `customJSONSerializer` {boolean, default: false} - Whether to produce also a JSON serializer function using `fast-json-stringify`. The default stringifier function is `JSON.stringify` -#### compiledQuery.compiled(root: any, context: any, variables: Maybe<{ [key: string]: any }>) +#### compiledQuery.query(root: any, context: any, variables: Maybe<{ [key: string]: any }>) + +the compiled function that can be called with a root value, a context and the required variables to produce execution result. + +#### compiledQuery.subscribe(root: any, context: any, variables: Maybe<{ [key: string]: any }>) -the compiled function that can be called with a root value, a context and the required variables. +(available for GraphQL Subscription only) the compiled function that can be called with a root value, a context and the required variables to produce either an AsyncIterator (if successful) or an ExecutionResult (error). #### compiledQuery.stringify(value: any) the compiled function for producing a JSON string. It will be `JSON.stringify` unless `compilerOptions.customJSONSerializer` is true. -The value argument should the return of the compiled GraphQL function. +The value argument should be the return of the compiled GraphQL function. ## LICENSE From 07aac0e474818a7958bf4a270a2d0b9ef90f908e Mon Sep 17 00:00:00 2001 From: Hoang Date: Mon, 8 Mar 2021 21:39:27 +0700 Subject: [PATCH 13/13] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 910aa65..62a03cb 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,7 @@ Done in 141.94s. ### Support for GraphQL spec -The goal is to support the [June 2018 version of the GraphQL spec](https://facebook.github.io/graphql/June2018/). At this moment, -the only missing feature is support for Subscriptions. +The goal is to support the [June 2018 version of the GraphQL spec](https://facebook.github.io/graphql/June2018/). #### Differences to `graphql-js`