diff --git a/integrationTests/ts/basic-test.ts b/integrationTests/ts/basic-test.ts index a28bd840e7..bfbca75c3d 100644 --- a/integrationTests/ts/basic-test.ts +++ b/integrationTests/ts/basic-test.ts @@ -23,12 +23,13 @@ const queryType: GraphQLObjectType = new GraphQLObjectType({ const schema: GraphQLSchema = new GraphQLSchema({ query: queryType }); -const result: ExecutionResult = graphqlSync({ - schema, - source: ` +const result: ExecutionResult | AsyncGenerator = + graphqlSync({ + schema, + source: ` query helloWho($who: String){ test(who: $who) } `, - variableValues: { who: 'Dolly' }, -}); + variableValues: { who: 'Dolly' }, + }); diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index d637787c4a..840d246f64 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -1,6 +1,8 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; + import { graphqlSync } from '../graphql'; import { StarWarsSchema } from './starWarsSchema'; @@ -8,6 +10,7 @@ import { StarWarsSchema } from './starWarsSchema'; function queryStarWars(source: string) { const result = graphqlSync({ schema: StarWarsSchema, source }); expect(Object.keys(result)).to.deep.equal(['data']); + assert(!isAsyncIterable(result)); return result.data; } diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 60b203dc05..8e8a16774a 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -4,6 +4,7 @@ import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; import { inspect } from '../../jsutils/inspect'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; @@ -19,7 +20,7 @@ import { import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; -import { execute, executeSync } from '../execute'; +import { execute, executeSubscriptionEvent, executeSync } from '../execute'; describe('Execute: Handles basic execution tasks', () => { it('executes arbitrary code', async () => { @@ -833,7 +834,7 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { c: 'd' } }); }); - it('uses the subscription schema for subscriptions', () => { + it('uses the subscription schema for subscriptions', async () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Q', @@ -852,11 +853,22 @@ describe('Execute: Handles basic execution tasks', () => { query Q { a } subscription S { a } `); - const rootValue = { a: 'b', c: 'd' }; + const rootValue = { + // eslint-disable-next-line @typescript-eslint/require-await + async *a() { + yield { a: 'b' }; /* c8 ignore start */ + } /* c8 ignore stop */, + c: 'd', + }; const operationName = 'S'; const result = executeSync({ schema, document, rootValue, operationName }); - expect(result).to.deep.equal({ data: { a: 'b' } }); + + assert(isAsyncIterable(result)); + expect(await result.next()).to.deep.equal({ + value: { data: { a: 'b' } }, + done: false, + }); }); it('resolves to an error if schema does not support operation', () => { @@ -894,6 +906,18 @@ describe('Execute: Handles basic execution tasks', () => { expectJSON( executeSync({ schema, document, operationName: 'S' }), + ).toDeepEqual({ + errors: [ + { + message: + 'Schema is not configured to execute subscription operation.', + locations: [{ line: 4, column: 7 }], + }, + ], + }); + + expectJSON( + executeSubscriptionEvent({ schema, document, operationName: 'S' }), ).toDeepEqual({ data: null, errors: [ diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts index 3fdd77ab56..d6c4ba8408 100644 --- a/src/execution/__tests__/lists-test.ts +++ b/src/execution/__tests__/lists-test.ts @@ -85,7 +85,9 @@ describe('Execute: Accepts async iterables as list value', () => { function completeObjectList( resolve: GraphQLFieldResolver<{ index: number }, unknown>, - ): PromiseOrValue { + ): PromiseOrValue< + ExecutionResult | AsyncGenerator + > { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index 427f2a64d6..ce3d0dcef5 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -1,8 +1,11 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; +import type { PromiseOrValue } from '../../jsutils/PromiseOrValue'; + import { parse } from '../../language/parser'; import { GraphQLNonNull, GraphQLObjectType } from '../../type/definition'; @@ -109,7 +112,9 @@ const schema = buildSchema(` function executeQuery( query: string, rootValue: unknown, -): ExecutionResult | Promise { +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { return execute({ schema, document: parse(query), rootValue }); } @@ -132,6 +137,7 @@ async function executeSyncAndAsync(query: string, rootValue: unknown) { rootValue, }); + assert(!isAsyncIterable(syncResult)); expectJSON(asyncResult).toDeepEqual(patchData(syncResult)); return syncResult; } diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 5f256ca868..902a99b47b 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -15,7 +15,7 @@ import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; import type { ExecutionArgs, ExecutionResult } from '../execute'; -import { createSourceEventStream, subscribe } from '../execute'; +import { createSourceEventStream, execute, subscribe } from '../execute'; import { SimplePubSub } from './simplePubSub'; @@ -122,7 +122,7 @@ function createSubscription(pubsub: SimplePubSub) { }), }; - return subscribe({ schema: emailSchema, document, rootValue: data }); + return execute({ schema: emailSchema, document, rootValue: data }); } // TODO: consider adding this method to testUtils (with tests) @@ -150,22 +150,46 @@ function expectPromise(maybePromise: unknown) { }; } -// TODO: consider adding this method to testUtils (with tests) +// TODO: consider adding these method to testUtils (with tests) function expectEqualPromisesOrValues( - value1: PromiseOrValue, - value2: PromiseOrValue, + items: ReadonlyArray>, ): PromiseOrValue { - if (isPromise(value1)) { - assert(isPromise(value2)); - return Promise.all([value1, value2]).then((resolved) => { - expectJSON(resolved[1]).toDeepEqual(resolved[0]); - return resolved[0]; - }); + if (isPromise(items[0])) { + if (assertAllPromises(items)) { + return Promise.all(items).then(expectMatchingValues); + } + } else if (assertNoPromises(items)) { + return expectMatchingValues(items); } + /* c8 ignore next 3 */ + // Not reachable, all possible output types have been considered. + assert(false, 'Receives mixture of promises and values.'); +} - assert(!isPromise(value2)); - expectJSON(value2).toDeepEqual(value1); - return value1; +function expectMatchingValues(values: ReadonlyArray): T { + const remainingValues = values.slice(1); + for (const value of remainingValues) { + expectJSON(value).toDeepEqual(values[0]); + } + return values[0]; +} + +function assertAllPromises( + items: ReadonlyArray>, +): items is ReadonlyArray> { + for (const item of items) { + assert(isPromise(item)); + } + return true; +} + +function assertNoPromises( + items: ReadonlyArray>, +): items is ReadonlyArray { + for (const item of items) { + assert(!isPromise(item)); + } + return true; } const DummyQueryType = new GraphQLObjectType({ @@ -195,10 +219,11 @@ function subscribeWithBadFn( function subscribeWithBadArgs( args: ExecutionArgs, ): PromiseOrValue> { - return expectEqualPromisesOrValues( - subscribe(args), + return expectEqualPromisesOrValues([ + execute(args), createSourceEventStream(args), - ); + subscribe(args), + ]); } /* eslint-disable @typescript-eslint/require-await */ @@ -220,7 +245,7 @@ describe('Subscription Initialization Phase', () => { yield { foo: 'FooValue' }; } - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), rootValue: { foo: fooGenerator }, @@ -256,7 +281,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), }); @@ -294,7 +319,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const promise = subscribe({ + const promise = execute({ schema, document: parse('subscription { foo }'), }); @@ -329,7 +354,7 @@ describe('Subscription Initialization Phase', () => { yield { foo: 'FooValue' }; } - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), rootValue: { customFoo: fooGenerator }, @@ -379,7 +404,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo bar }'), }); @@ -530,7 +555,7 @@ describe('Subscription Initialization Phase', () => { } `); - // If we receive variables that cannot be coerced correctly, subscribe() will + // If we receive variables that cannot be coerced correctly, execute() will // resolve to an ExecutionResult that contains an informative error description. const result = subscribeWithBadArgs({ schema, document, variableValues }); expectJSON(result).toDeepEqual({ @@ -945,7 +970,7 @@ describe('Subscription Publish Phase', () => { }); const document = parse('subscription { newMessage }'); - const subscription = subscribe({ schema, document }); + const subscription = execute({ schema, document }); assert(isAsyncIterable(subscription)); expect(await subscription.next()).to.deep.equal({ @@ -1006,7 +1031,7 @@ describe('Subscription Publish Phase', () => { }); const document = parse('subscription { newMessage }'); - const subscription = subscribe({ schema, document }); + const subscription = execute({ schema, document }); assert(isAsyncIterable(subscription)); expect(await subscription.next()).to.deep.equal({ diff --git a/src/execution/compiledDocument.ts b/src/execution/compiledDocument.ts new file mode 100644 index 0000000000..dbd2af2cf3 --- /dev/null +++ b/src/execution/compiledDocument.ts @@ -0,0 +1,80 @@ +import type { ObjMap } from '../jsutils/ObjMap'; + +import { GraphQLError } from '../error/GraphQLError'; + +import type { + FragmentDefinitionNode, + OperationDefinitionNode, +} from '../language/ast'; +import { Kind } from '../language/kinds'; + +import { assertValidSchema } from '../type/validate'; + +import type { ExecutionArgs } from './execute'; + +/** + * Data that must be available at all points during query execution. + * + * Namely, schema of the type system that is currently executing, + * and the fragments defined in the query document + */ +export interface ExecutionContext { + fragments: ObjMap; + operation: OperationDefinitionNode; +} + +/** + * Constructs a ExecutionContext object from the arguments passed to + * execute, which we will pass throughout the other execution methods. + * + * Throws a GraphQLError if a valid execution context cannot be created. + * + * TODO: consider no longer exporting this function + * @internal + */ +export function buildExecutionContext( + args: ExecutionArgs, +): ReadonlyArray | ExecutionContext { + const { schema, document, operationName } = args; + + // If the schema used for execution is invalid, throw an error. + assertValidSchema(schema); + + let operation: OperationDefinitionNode | undefined; + const fragments: ObjMap = Object.create(null); + for (const definition of document.definitions) { + switch (definition.kind) { + case Kind.OPERATION_DEFINITION: + if (operationName == null) { + if (operation !== undefined) { + return [ + new GraphQLError( + 'Must provide operation name if query contains multiple operations.', + ), + ]; + } + operation = definition; + } else if (definition.name?.value === operationName) { + operation = definition; + } + break; + case Kind.FRAGMENT_DEFINITION: + fragments[definition.name.value] = definition; + break; + default: + // ignore non-executable definitions + } + } + + if (!operation) { + if (operationName != null) { + return [new GraphQLError(`Unknown operation named "${operationName}".`)]; + } + return [new GraphQLError('Must provide an operation.')]; + } + + return { + fragments, + operation, + }; +} diff --git a/src/execution/compiledSchema.ts b/src/execution/compiledSchema.ts new file mode 100644 index 0000000000..5b85be84e0 --- /dev/null +++ b/src/execution/compiledSchema.ts @@ -0,0 +1,106 @@ +import { isObjectLike } from '../jsutils/isObjectLike'; +import { isPromise } from '../jsutils/isPromise'; +import type { Maybe } from '../jsutils/Maybe'; + +import type { + GraphQLFieldResolver, + GraphQLTypeResolver, +} from '../type/definition'; +import type { GraphQLSchema } from '../type/schema'; + +/* eslint-disable max-params */ +// This file contains a lot of such errors but we plan to refactor it anyway +// so just disable it for entire file. + +export interface GraphQLCompiledSchemaConfig { + schema: GraphQLSchema; + fieldResolver?: Maybe>; + typeResolver?: Maybe>; + subscribeFieldResolver?: Maybe>; +} + +/** + * @internal + */ +export class GraphQLCompiledSchema { + schema: GraphQLSchema; + fieldResolver: GraphQLFieldResolver; + typeResolver: GraphQLTypeResolver; + subscribeFieldResolver: GraphQLFieldResolver; + + constructor(config: GraphQLCompiledSchemaConfig) { + this.schema = config.schema; + this.fieldResolver = config.fieldResolver ?? defaultFieldResolver; + this.typeResolver = config.typeResolver ?? defaultTypeResolver; + this.subscribeFieldResolver = + config.subscribeFieldResolver ?? defaultFieldResolver; + } + + get [Symbol.toStringTag]() { + return 'GraphQLCompiledSchema'; + } +} + +/** + * If a resolveType function is not given, then a default resolve behavior is + * used which attempts two strategies: + * + * First, See if the provided value has a `__typename` field defined, if so, use + * that value as name of the resolved type. + * + * Otherwise, test each possible type for the abstract type by calling + * isTypeOf for the object being coerced, returning the first type that matches. + */ +export const defaultTypeResolver: GraphQLTypeResolver = + function (value, contextValue, info, abstractType) { + // First, look for `__typename`. + if (isObjectLike(value) && typeof value.__typename === 'string') { + return value.__typename; + } + + // Otherwise, test each possible type. + const possibleTypes = info.schema.getPossibleTypes(abstractType); + const promisedIsTypeOfResults = []; + + for (let i = 0; i < possibleTypes.length; i++) { + const type = possibleTypes[i]; + + if (type.isTypeOf) { + const isTypeOfResult = type.isTypeOf(value, contextValue, info); + + if (isPromise(isTypeOfResult)) { + promisedIsTypeOfResults[i] = isTypeOfResult; + } else if (isTypeOfResult) { + return type.name; + } + } + } + + if (promisedIsTypeOfResults.length) { + return Promise.all(promisedIsTypeOfResults).then((isTypeOfResults) => { + for (let i = 0; i < isTypeOfResults.length; i++) { + if (isTypeOfResults[i]) { + return possibleTypes[i].name; + } + } + }); + } + }; + +/** + * If a resolve function is not given, then a default resolve behavior is used + * which takes the property of the source object of the same name as the field + * and returns it as the result, or if it's a function, returns the result + * of calling that function while passing along args and context value. + */ +export const defaultFieldResolver: GraphQLFieldResolver = + function (source: any, args, contextValue, info) { + // ensure source is a value for which property access is acceptable. + if (isObjectLike(source) || typeof source === 'function') { + const property = source[info.fieldName]; + if (typeof property === 'function') { + return source[info.fieldName](args, contextValue, info); + } + return property; + } + }; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index cc3cbb9cbf..67ad2776f5 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1,122 +1,31 @@ -import { inspect } from '../jsutils/inspect'; -import { invariant } from '../jsutils/invariant'; -import { isAsyncIterable } from '../jsutils/isAsyncIterable'; -import { isIterableObject } from '../jsutils/isIterableObject'; -import { isObjectLike } from '../jsutils/isObjectLike'; import { isPromise } from '../jsutils/isPromise'; import type { Maybe } from '../jsutils/Maybe'; -import { memoize3 } from '../jsutils/memoize3'; import type { ObjMap } from '../jsutils/ObjMap'; -import type { Path } from '../jsutils/Path'; -import { addPath, pathToArray } from '../jsutils/Path'; -import { promiseForObject } from '../jsutils/promiseForObject'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; -import { promiseReduce } from '../jsutils/promiseReduce'; - -import type { GraphQLFormattedError } from '../error/GraphQLError'; -import { GraphQLError } from '../error/GraphQLError'; -import { locatedError } from '../error/locatedError'; import type { - DocumentNode, - FieldNode, - FragmentDefinitionNode, - OperationDefinitionNode, -} from '../language/ast'; + GraphQLError, + GraphQLFormattedError, +} from '../error/GraphQLError'; + +import type { DocumentNode } from '../language/ast'; import { OperationTypeNode } from '../language/ast'; -import { Kind } from '../language/kinds'; import type { - GraphQLAbstractType, - GraphQLField, GraphQLFieldResolver, - GraphQLLeafType, - GraphQLList, - GraphQLObjectType, - GraphQLOutputType, - GraphQLResolveInfo, GraphQLTypeResolver, } from '../type/definition'; -import { - isAbstractType, - isLeafType, - isListType, - isNonNullType, - isObjectType, -} from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; -import { assertValidSchema } from '../type/validate'; -import { - collectFields, - collectSubfields as _collectSubfields, -} from './collectFields'; -import { mapAsyncIterator } from './mapAsyncIterator'; -import { getArgumentValues, getVariableValues } from './values'; +import type { ExecutionContext } from './compiledDocument'; +import { buildExecutionContext } from './compiledDocument'; +import { GraphQLCompiledSchema } from './compiledSchema'; +import { GraphQLExecution } from './execution'; +import { getVariableValues } from './values'; -/* eslint-disable max-params */ // This file contains a lot of such errors but we plan to refactor it anyway // so just disable it for entire file. -/** - * A memoized collection of relevant subfields with regard to the return - * type. Memoizing ensures the subfields are not repeatedly calculated, which - * saves overhead when resolving lists of values. - */ -const collectSubfields = memoize3( - ( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, - ) => - _collectSubfields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - returnType, - fieldNodes, - ), -); - -/** - * Terminology - * - * "Definitions" are the generic name for top-level statements in the document. - * Examples of this include: - * 1) Operations (such as a query) - * 2) Fragments - * - * "Operations" are a generic name for requests in the document. - * Examples of this include: - * 1) query, - * 2) mutation - * - * "Selections" are the definitions that can appear legally and at - * single level of the query. These include: - * 1) field references e.g `a` - * 2) fragment "spreads" e.g. `...c` - * 3) inline fragment "spreads" e.g. `...on Type { a }` - */ - -/** - * Data that must be available at all points during query execution. - * - * Namely, schema of the type system that is currently executing, - * and the fragments defined in the query document - */ -export interface ExecutionContext { - schema: GraphQLSchema; - fragments: ObjMap; - rootValue: unknown; - contextValue: unknown; - operation: OperationDefinitionNode; - variableValues: { [variable: string]: unknown }; - fieldResolver: GraphQLFieldResolver; - typeResolver: GraphQLTypeResolver; - subscribeFieldResolver: GraphQLFieldResolver; - errors: Array; -} - /** * The result of GraphQL execution. * @@ -164,923 +73,95 @@ export interface ExecutionArgs { * If the arguments to this function do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. */ -export function execute(args: ExecutionArgs): PromiseOrValue { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - return executeImpl(exeContext); -} - -function executeImpl( - exeContext: ExecutionContext, -): PromiseOrValue { - // Return a Promise that will eventually resolve to the data described by - // The "Response" section of the GraphQL specification. - // - // If errors are encountered while executing a GraphQL field, only that - // field and its descendants will be omitted, and sibling fields will still - // be executed. An execution which encounters errors will still result in a - // resolved Promise. - // - // Errors from sub-fields of a NonNull type may propagate to the top level, - // at which point we still log the error and null the parent field, which - // in this case is the entire response. - try { - const result = executeOperation(exeContext); - if (isPromise(result)) { - return result.then( - (data) => buildResponse(data, exeContext.errors), - (error) => { - exeContext.errors.push(error); - return buildResponse(null, exeContext.errors); - }, - ); - } - return buildResponse(result, exeContext.errors); - } catch (error) { - exeContext.errors.push(error); - return buildResponse(null, exeContext.errors); - } -} - -/** - * Also implements the "Executing requests" section of the GraphQL specification. - * However, it guarantees to complete synchronously (or throw an error) assuming - * that all field resolvers are also synchronous. - */ -export function executeSync(args: ExecutionArgs): ExecutionResult { - const result = execute(args); +export function execute( + args: ExecutionArgs, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { + return prepareContextAndRunFn( + args, + ( + compiledSchema: GraphQLCompiledSchema, + exeContext: ExecutionContext, + coercedVariableValues: { [variable: string]: unknown }, + ) => { + const execution = new GraphQLExecution({ + compiledSchema, + exeContext, + rootValue: args.rootValue, + contextValue: args.contextValue, + variableValues: coercedVariableValues, + }); - // Assert that the execution was synchronous. - if (isPromise(result)) { - throw new Error('GraphQL execution failed to complete synchronously.'); - } + const operationType = exeContext.operation.operation; + if (operationType === OperationTypeNode.QUERY) { + return execution.executeQuery(exeContext); + } - return result; -} + if (operationType === OperationTypeNode.MUTATION) { + return execution.executeMutation(exeContext); + } -/** - * Given a completed execution context and data, build the `{ errors, data }` - * response defined by the "Response" section of the GraphQL specification. - */ -function buildResponse( - data: ObjMap | null, - errors: ReadonlyArray, -): ExecutionResult { - return errors.length === 0 ? { data } : { errors, data }; + return execution.executeSubscription(exeContext); + }, + ); } -/** - * Constructs a ExecutionContext object from the arguments passed to - * execute, which we will pass throughout the other execution methods. - * - * Throws a GraphQLError if a valid execution context cannot be created. - * - * TODO: consider no longer exporting this function - * @internal - */ -export function buildExecutionContext( +function prepareContextAndRunFn( args: ExecutionArgs, -): ReadonlyArray | ExecutionContext { - const { - schema, - document, - rootValue, - contextValue, - variableValues: rawVariableValues, - operationName, - fieldResolver, - typeResolver, - subscribeFieldResolver, - } = args; - - // If the schema used for execution is invalid, throw an error. - assertValidSchema(schema); - - let operation: OperationDefinitionNode | undefined; - const fragments: ObjMap = Object.create(null); - for (const definition of document.definitions) { - switch (definition.kind) { - case Kind.OPERATION_DEFINITION: - if (operationName == null) { - if (operation !== undefined) { - return [ - new GraphQLError( - 'Must provide operation name if query contains multiple operations.', - ), - ]; - } - operation = definition; - } else if (definition.name?.value === operationName) { - operation = definition; - } - break; - case Kind.FRAGMENT_DEFINITION: - fragments[definition.name.value] = definition; - break; - default: - // ignore non-executable definitions - } - } + fn: ( + compiledSchema: GraphQLCompiledSchema, + exeContext: ExecutionContext, + coercedVariableValues: { [variable: string]: unknown }, + ) => T, +): ExecutionResult | T { + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + const exeContext = buildExecutionContext(args); - if (!operation) { - if (operationName != null) { - return [new GraphQLError(`Unknown operation named "${operationName}".`)]; - } - return [new GraphQLError('Must provide an operation.')]; + // Return early errors if execution context failed. + if (!('fragments' in exeContext)) { + return { errors: exeContext }; } // FIXME: https://github.com/graphql/graphql-js/issues/2203 /* c8 ignore next */ - const variableDefinitions = operation.variableDefinitions ?? []; + const variableDefinitions = exeContext.operation.variableDefinitions ?? []; + + const compiledSchema = new GraphQLCompiledSchema(args); const coercedVariableValues = getVariableValues( - schema, + compiledSchema.schema, variableDefinitions, - rawVariableValues ?? {}, + args.variableValues ?? {}, { maxErrors: 50 }, ); if (coercedVariableValues.errors) { - return coercedVariableValues.errors; - } - - return { - schema, - fragments, - rootValue, - contextValue, - operation, - variableValues: coercedVariableValues.coerced, - fieldResolver: fieldResolver ?? defaultFieldResolver, - typeResolver: typeResolver ?? defaultTypeResolver, - subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - errors: [], - }; -} - -function buildPerEventExecutionContext( - exeContext: ExecutionContext, - payload: unknown, -): ExecutionContext { - return { - ...exeContext, - rootValue: payload, - errors: [], - }; -} - -/** - * Implements the "Executing operations" section of the spec. - */ -function executeOperation( - exeContext: ExecutionContext, -): PromiseOrValue> { - const { operation, schema, fragments, variableValues, rootValue } = - exeContext; - const rootType = schema.getRootType(operation.operation); - if (rootType == null) { - throw new GraphQLError( - `Schema is not configured to execute ${operation.operation} operation.`, - { nodes: operation }, - ); - } - - const rootFields = collectFields( - schema, - fragments, - variableValues, - rootType, - operation.selectionSet, - ); - const path = undefined; - - switch (operation.operation) { - case OperationTypeNode.QUERY: - return executeFields(exeContext, rootType, rootValue, path, rootFields); - case OperationTypeNode.MUTATION: - return executeFieldsSerially( - exeContext, - rootType, - rootValue, - path, - rootFields, - ); - case OperationTypeNode.SUBSCRIPTION: - // TODO: deprecate `subscribe` and move all logic here - // Temporary solution until we finish merging execute and subscribe together - return executeFields(exeContext, rootType, rootValue, path, rootFields); - } -} - -/** - * Implements the "Executing selection sets" section of the spec - * for fields that must be executed serially. - */ -function executeFieldsSerially( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - fields: Map>, -): PromiseOrValue> { - return promiseReduce( - fields.entries(), - (results, [responseName, fieldNodes]) => { - const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( - exeContext, - parentType, - sourceValue, - fieldNodes, - fieldPath, - ); - if (result === undefined) { - return results; - } - if (isPromise(result)) { - return result.then((resolvedResult) => { - results[responseName] = resolvedResult; - return results; - }); - } - results[responseName] = result; - return results; - }, - Object.create(null), - ); -} - -/** - * Implements the "Executing selection sets" section of the spec - * for fields that may be executed in parallel. - */ -function executeFields( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: unknown, - path: Path | undefined, - fields: Map>, -): PromiseOrValue> { - const results = Object.create(null); - let containsPromise = false; - - for (const [responseName, fieldNodes] of fields.entries()) { - const fieldPath = addPath(path, responseName, parentType.name); - const result = executeField( - exeContext, - parentType, - sourceValue, - fieldNodes, - fieldPath, - ); - - if (result !== undefined) { - results[responseName] = result; - if (isPromise(result)) { - containsPromise = true; - } - } - } - - // If there are no promises, we can just return the object - if (!containsPromise) { - return results; - } - - // Otherwise, results is a map from field name to the result of resolving that - // field, which is possibly a promise. Return a promise that will return this - // same map, but with any promises replaced with the values they resolved to. - return promiseForObject(results); -} - -/** - * Implements the "Executing fields" section of the spec - * In particular, this function figures out the value that the field returns by - * calling its resolve function, then calls completeValue to complete promises, - * serialize scalars, or execute the sub-selection-set for objects. - */ -function executeField( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - source: unknown, - fieldNodes: ReadonlyArray, - path: Path, -): PromiseOrValue { - const fieldName = fieldNodes[0].name.value; - const fieldDef = exeContext.schema.getField(parentType, fieldName); - if (!fieldDef) { - return; - } - - const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver; - - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - parentType, - path, - ); - - // Get the resolve function, regardless of if its result is normal or abrupt (error). - try { - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - // TODO: find a way to memoize, in case this field is within a List type. - const args = getArgumentValues( - fieldDef, - fieldNodes[0], - exeContext.variableValues, - ); - - // The resolve function's optional third argument is a context value that - // is provided to every resolve function within an execution. It is commonly - // used to represent an authenticated user, or request-specific caches. - const contextValue = exeContext.contextValue; - - const result = resolveFn(source, args, contextValue, info); - - let completed; - if (isPromise(result)) { - completed = result.then((resolved) => - completeValue(exeContext, returnType, fieldNodes, info, path, resolved), - ); - } else { - completed = completeValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); - } - - if (isPromise(completed)) { - // Note: we don't rely on a `catch` method, but we do expect "thenable" - // to take a second callback for the error case. - return completed.then(undefined, (rawError) => { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - return handleFieldError(error, returnType, exeContext); - }); - } - return completed; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - return handleFieldError(error, returnType, exeContext); - } -} - -/** - * TODO: consider no longer exporting this function - * @internal - */ -export function buildResolveInfo( - exeContext: ExecutionContext, - fieldDef: GraphQLField, - fieldNodes: ReadonlyArray, - parentType: GraphQLObjectType, - path: Path, -): GraphQLResolveInfo { - // The resolve function's optional fourth argument is a collection of - // information about the current execution state. - return { - fieldName: fieldDef.name, - fieldNodes, - returnType: fieldDef.type, - parentType, - path, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - }; -} - -function handleFieldError( - error: GraphQLError, - returnType: GraphQLOutputType, - exeContext: ExecutionContext, -): null { - // If the field type is non-nullable, then it is resolved without any - // protection from errors, however it still properly locates the error. - if (isNonNullType(returnType)) { - throw error; - } - - // Otherwise, error protection is applied, logging the error and resolving - // a null value for this field if one is encountered. - exeContext.errors.push(error); - return null; -} - -/** - * Implements the instructions for completeValue as defined in the - * "Value Completion" section of the spec. - * - * If the field type is Non-Null, then this recursively completes the value - * for the inner type. It throws a field error if that completion returns null, - * as per the "Nullability" section of the spec. - * - * If the field type is a List, then this recursively completes the value - * for the inner type on each item in the list. - * - * If the field type is a Scalar or Enum, ensures the completed value is a legal - * value of the type by calling the `serialize` method of GraphQL type - * definition. - * - * If the field is an abstract type, determine the runtime type of the value - * and then complete based on that type - * - * Otherwise, the field type expects a sub-selection set, and will complete the - * value by executing all sub-selections. - */ -function completeValue( - exeContext: ExecutionContext, - returnType: GraphQLOutputType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue { - // If result is an Error, throw a located error. - if (result instanceof Error) { - throw result; - } - - // If field type is NonNull, complete for inner type, and throw field error - // if result is null. - if (isNonNullType(returnType)) { - const completed = completeValue( - exeContext, - returnType.ofType, - fieldNodes, - info, - path, - result, - ); - if (completed === null) { - throw new Error( - `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, - ); - } - return completed; - } - - // If result value is null or undefined then return null. - if (result == null) { - return null; - } - - // If field type is List, complete each item in the list with the inner type - if (isListType(returnType)) { - return completeListValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); - } - - // If field type is a leaf type, Scalar or Enum, serialize to a valid value, - // returning null if serialization is not possible. - if (isLeafType(returnType)) { - return completeLeafValue(returnType, result); - } - - // If field type is an abstract type, Interface or Union, determine the - // runtime Object type and complete for that type. - if (isAbstractType(returnType)) { - return completeAbstractValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); + return { errors: coercedVariableValues.errors }; } - // If field type is Object, execute and complete all sub-selections. - if (isObjectType(returnType)) { - return completeObjectValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); - } - /* c8 ignore next 6 */ - // Not reachable, all possible output types have been considered. - invariant( - false, - 'Cannot complete value of unexpected output type: ' + inspect(returnType), - ); -} - -/** - * Complete a async iterator value by completing the result and calling - * recursively until all the results are completed. - */ -async function completeAsyncIteratorValue( - exeContext: ExecutionContext, - itemType: GraphQLOutputType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - iterator: AsyncIterator, -): Promise> { - let containsPromise = false; - const completedResults = []; - let index = 0; - // eslint-disable-next-line no-constant-condition - while (true) { - const fieldPath = addPath(path, index, undefined); - try { - // eslint-disable-next-line no-await-in-loop - const { value, done } = await iterator.next(); - if (done) { - break; - } - - try { - // TODO can the error checking logic be consolidated with completeListValue? - const completedItem = completeValue( - exeContext, - itemType, - fieldNodes, - info, - fieldPath, - value, - ); - if (isPromise(completedItem)) { - containsPromise = true; - } - completedResults.push(completedItem); - } catch (rawError) { - completedResults.push(null); - const error = locatedError( - rawError, - fieldNodes, - pathToArray(fieldPath), - ); - handleFieldError(error, itemType, exeContext); - } - } catch (rawError) { - completedResults.push(null); - const error = locatedError(rawError, fieldNodes, pathToArray(fieldPath)); - handleFieldError(error, itemType, exeContext); - break; - } - index += 1; - } - return containsPromise ? Promise.all(completedResults) : completedResults; -} - -/** - * Complete a list value by completing each item in the list with the - * inner type - */ -function completeListValue( - exeContext: ExecutionContext, - returnType: GraphQLList, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue> { - const itemType = returnType.ofType; - - if (isAsyncIterable(result)) { - const iterator = result[Symbol.asyncIterator](); - - return completeAsyncIteratorValue( - exeContext, - itemType, - fieldNodes, - info, - path, - iterator, - ); - } - - if (!isIterableObject(result)) { - throw new GraphQLError( - `Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`, - ); - } - - // This is specified as a simple map, however we're optimizing the path - // where the list contains no Promises by avoiding creating another Promise. - let containsPromise = false; - const completedResults = Array.from(result, (item, index) => { - // No need to modify the info object containing the path, - // since from here on it is not ever accessed by resolver functions. - const itemPath = addPath(path, index, undefined); - try { - let completedItem; - if (isPromise(item)) { - completedItem = item.then((resolved) => - completeValue( - exeContext, - itemType, - fieldNodes, - info, - itemPath, - resolved, - ), - ); - } else { - completedItem = completeValue( - exeContext, - itemType, - fieldNodes, - info, - itemPath, - item, - ); - } - - if (isPromise(completedItem)) { - containsPromise = true; - // Note: we don't rely on a `catch` method, but we do expect "thenable" - // to take a second callback for the error case. - return completedItem.then(undefined, (rawError) => { - const error = locatedError( - rawError, - fieldNodes, - pathToArray(itemPath), - ); - return handleFieldError(error, itemType, exeContext); - }); - } - return completedItem; - } catch (rawError) { - const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); - return handleFieldError(error, itemType, exeContext); - } - }); - - return containsPromise ? Promise.all(completedResults) : completedResults; -} - -/** - * Complete a Scalar or Enum by serializing to a valid value, returning - * null if serialization is not possible. - */ -function completeLeafValue( - returnType: GraphQLLeafType, - result: unknown, -): unknown { - const serializedResult = returnType.serialize(result); - if (serializedResult == null) { - throw new Error( - `Expected \`${inspect(returnType)}.serialize(${inspect(result)})\` to ` + - `return non-nullable value, returned: ${inspect(serializedResult)}`, - ); - } - return serializedResult; -} - -/** - * Complete a value of an abstract type by determining the runtime object type - * of that value, then complete the value for that type. - */ -function completeAbstractValue( - exeContext: ExecutionContext, - returnType: GraphQLAbstractType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue> { - const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; - const contextValue = exeContext.contextValue; - const runtimeType = resolveTypeFn(result, contextValue, info, returnType); - - if (isPromise(runtimeType)) { - return runtimeType.then((resolvedRuntimeType) => - completeObjectValue( - exeContext, - ensureValidRuntimeType( - resolvedRuntimeType, - exeContext, - returnType, - fieldNodes, - info, - result, - ), - fieldNodes, - info, - path, - result, - ), - ); - } - - return completeObjectValue( - exeContext, - ensureValidRuntimeType( - runtimeType, - exeContext, - returnType, - fieldNodes, - info, - result, - ), - fieldNodes, - info, - path, - result, - ); -} - -function ensureValidRuntimeType( - runtimeTypeName: unknown, - exeContext: ExecutionContext, - returnType: GraphQLAbstractType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - result: unknown, -): GraphQLObjectType { - if (runtimeTypeName == null) { - throw new GraphQLError( - `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, - { nodes: fieldNodes }, - ); - } - - // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` - // TODO: remove in 17.0.0 release - if (isObjectType(runtimeTypeName)) { - throw new GraphQLError( - 'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.', - ); - } - - if (typeof runtimeTypeName !== 'string') { - throw new GraphQLError( - `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + - `value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`, - ); - } - - const runtimeType = exeContext.schema.getType(runtimeTypeName); - if (runtimeType == null) { - throw new GraphQLError( - `Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, - { nodes: fieldNodes }, - ); - } - - if (!isObjectType(runtimeType)) { - throw new GraphQLError( - `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, - { nodes: fieldNodes }, - ); - } - - if (!exeContext.schema.isSubType(returnType, runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, - { nodes: fieldNodes }, - ); - } - - return runtimeType; + return fn(compiledSchema, exeContext, coercedVariableValues.coerced); } /** - * Complete an Object value by executing all sub-selections. + * Also implements the "Executing requests" section of the GraphQL specification. + * However, it guarantees to complete synchronously (or throw an error) assuming + * that all field resolvers are also synchronous. */ -function completeObjectValue( - exeContext: ExecutionContext, - returnType: GraphQLObjectType, - fieldNodes: ReadonlyArray, - info: GraphQLResolveInfo, - path: Path, - result: unknown, -): PromiseOrValue> { - // Collect sub-fields to execute to complete this value. - const subFieldNodes = collectSubfields(exeContext, returnType, fieldNodes); - - // If there is an isTypeOf predicate function, call it with the - // current result. If isTypeOf returns false, then raise an error rather - // than continuing execution. - if (returnType.isTypeOf) { - const isTypeOf = returnType.isTypeOf(result, exeContext.contextValue, info); - - if (isPromise(isTypeOf)) { - return isTypeOf.then((resolvedIsTypeOf) => { - if (!resolvedIsTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); - } - return executeFields( - exeContext, - returnType, - result, - path, - subFieldNodes, - ); - }); - } +export function executeSync( + args: ExecutionArgs, +): ExecutionResult | AsyncGenerator { + const result = execute(args); - if (!isTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); - } + // Assert that the execution was synchronous. + if (isPromise(result)) { + throw new Error('GraphQL execution failed to complete synchronously.'); } - return executeFields(exeContext, returnType, result, path, subFieldNodes); -} - -function invalidReturnTypeError( - returnType: GraphQLObjectType, - result: unknown, - fieldNodes: ReadonlyArray, -): GraphQLError { - return new GraphQLError( - `Expected value of type "${returnType.name}" but got: ${inspect(result)}.`, - { nodes: fieldNodes }, - ); + return result; } -/** - * If a resolveType function is not given, then a default resolve behavior is - * used which attempts two strategies: - * - * First, See if the provided value has a `__typename` field defined, if so, use - * that value as name of the resolved type. - * - * Otherwise, test each possible type for the abstract type by calling - * isTypeOf for the object being coerced, returning the first type that matches. - */ -export const defaultTypeResolver: GraphQLTypeResolver = - function (value, contextValue, info, abstractType) { - // First, look for `__typename`. - if (isObjectLike(value) && typeof value.__typename === 'string') { - return value.__typename; - } - - // Otherwise, test each possible type. - const possibleTypes = info.schema.getPossibleTypes(abstractType); - const promisedIsTypeOfResults = []; - - for (let i = 0; i < possibleTypes.length; i++) { - const type = possibleTypes[i]; - - if (type.isTypeOf) { - const isTypeOfResult = type.isTypeOf(value, contextValue, info); - - if (isPromise(isTypeOfResult)) { - promisedIsTypeOfResults[i] = isTypeOfResult; - } else if (isTypeOfResult) { - return type.name; - } - } - } - - if (promisedIsTypeOfResults.length) { - return Promise.all(promisedIsTypeOfResults).then((isTypeOfResults) => { - for (let i = 0; i < isTypeOfResults.length; i++) { - if (isTypeOfResults[i]) { - return possibleTypes[i].name; - } - } - }); - } - }; - -/** - * If a resolve function is not given, then a default resolve behavior is used - * which takes the property of the source object of the same name as the field - * and returns it as the result, or if it's a function, returns the result - * of calling that function while passing along args and context value. - */ -export const defaultFieldResolver: GraphQLFieldResolver = - function (source: any, args, contextValue, info) { - // ensure source is a value for which property access is acceptable. - if (isObjectLike(source) || typeof source === 'function') { - const property = source[info.fieldName]; - if (typeof property === 'function') { - return source[info.fieldName](args, contextValue, info); - } - return property; - } - }; - /** * Implements the "Subscribe" algorithm described in the GraphQL specification. * @@ -1101,51 +182,15 @@ export const defaultFieldResolver: GraphQLFieldResolver = * yields a stream of ExecutionResults representing the response stream. * * Accepts either an object with named arguments, or individual arguments. + * + * @deprecated subscribe will be removed in v18; use execute instead */ export function subscribe( args: ExecutionArgs, ): PromiseOrValue< AsyncGenerator | ExecutionResult > { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - const resultOrStream = createSourceEventStreamImpl(exeContext); - - if (isPromise(resultOrStream)) { - return resultOrStream.then((resolvedResultOrStream) => - mapSourceToResponse(exeContext, resolvedResultOrStream), - ); - } - - return mapSourceToResponse(exeContext, resultOrStream); -} - -function mapSourceToResponse( - exeContext: ExecutionContext, - resultOrStream: ExecutionResult | AsyncIterable, -): PromiseOrValue< - AsyncGenerator | ExecutionResult -> { - if (!isAsyncIterable(resultOrStream)) { - return resultOrStream; - } - - // 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. - return mapAsyncIterator(resultOrStream, (payload: unknown) => - executeImpl(buildPerEventExecutionContext(exeContext, payload)), - ); + return execute(args); } /** @@ -1179,116 +224,49 @@ function mapSourceToResponse( export function createSourceEventStream( args: ExecutionArgs, ): PromiseOrValue | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - return createSourceEventStreamImpl(exeContext); -} - -function createSourceEventStreamImpl( - exeContext: ExecutionContext, -): PromiseOrValue | ExecutionResult> { - try { - const eventStream = executeSubscription(exeContext); - if (isPromise(eventStream)) { - return eventStream.then(undefined, (error) => ({ errors: [error] })); - } - - return eventStream; - } catch (error) { - return { errors: [error] }; - } -} - -function executeSubscription( - exeContext: ExecutionContext, -): PromiseOrValue> { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; - - const rootType = schema.getSubscriptionType(); - if (rootType == null) { - throw new GraphQLError( - 'Schema is not configured to execute subscription operation.', - { nodes: operation }, - ); - } - - const rootFields = collectFields( - schema, - fragments, - variableValues, - rootType, - operation.selectionSet, - ); - const [responseName, fieldNodes] = [...rootFields.entries()][0]; - const fieldName = fieldNodes[0].name.value; - const fieldDef = schema.getField(rootType, fieldName); - - if (!fieldDef) { - throw new GraphQLError( - `The subscription field "${fieldName}" is not defined.`, - { nodes: fieldNodes }, - ); - } - - const path = addPath(undefined, responseName, rootType.name); - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - rootType, - path, - ); - - try { - // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. - // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. - - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues); - - // The resolve function's optional third argument is a context value that - // is provided to every resolve function within an execution. It is commonly - // used to represent an authenticated user, or request-specific caches. - const contextValue = exeContext.contextValue; - - // Call the `subscribe()` resolver or the default resolver to produce an - // AsyncIterable yielding raw payloads. - const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver; - const result = resolveFn(rootValue, args, contextValue, info); - - if (isPromise(result)) { - return result.then(assertEventStream).then(undefined, (error) => { - throw locatedError(error, fieldNodes, pathToArray(path)); + return prepareContextAndRunFn( + args, + ( + compiledSchema: GraphQLCompiledSchema, + exeContext: ExecutionContext, + coercedVariableValues: { [variable: string]: unknown }, + ) => { + const execution = new GraphQLExecution({ + compiledSchema, + exeContext, + rootValue: args.rootValue, + contextValue: args.contextValue, + variableValues: coercedVariableValues, }); - } - return assertEventStream(result); - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); - } + return execution.createSourceEventStreamImpl(exeContext); + }, + ); } -function assertEventStream(result: unknown): AsyncIterable { - if (result instanceof Error) { - throw result; - } - - // Assert field returned an event stream, otherwise yield an error. - if (!isAsyncIterable(result)) { - throw new GraphQLError( - 'Subscription field must return Async Iterable. ' + - `Received: ${inspect(result)}.`, - ); - } +/** + * Implements the "ExecuteSubscriptionEvent" algorithm described in the + * GraphQL specification. + */ +export function executeSubscriptionEvent( + args: ExecutionArgs, +): PromiseOrValue { + return prepareContextAndRunFn( + args, + ( + compiledSchema: GraphQLCompiledSchema, + exeContext: ExecutionContext, + coercedVariableValues: { [variable: string]: unknown }, + ) => { + const execution = new GraphQLExecution({ + compiledSchema, + exeContext, + rootValue: args.rootValue, + contextValue: args.contextValue, + variableValues: coercedVariableValues, + }); - return result; + return execution.executeQuery(exeContext); + }, + ); } diff --git a/src/execution/execution.ts b/src/execution/execution.ts new file mode 100644 index 0000000000..b5671247e5 --- /dev/null +++ b/src/execution/execution.ts @@ -0,0 +1,1005 @@ +import { inspect } from '../jsutils/inspect'; +import { invariant } from '../jsutils/invariant'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; +import { isIterableObject } from '../jsutils/isIterableObject'; +import { isPromise } from '../jsutils/isPromise'; +import { memoize3 } from '../jsutils/memoize3'; +import type { ObjMap } from '../jsutils/ObjMap'; +import type { Path } from '../jsutils/Path'; +import { addPath, pathToArray } from '../jsutils/Path'; +import { promiseForObject } from '../jsutils/promiseForObject'; +import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; +import { promiseReduce } from '../jsutils/promiseReduce'; + +import { GraphQLError } from '../error/GraphQLError'; +import { locatedError } from '../error/locatedError'; + +import type { FieldNode } from '../language/ast'; + +import type { + GraphQLAbstractType, + GraphQLField, + GraphQLLeafType, + GraphQLList, + GraphQLObjectType, + GraphQLOutputType, + GraphQLResolveInfo, +} from '../type/definition'; +import { + isAbstractType, + isLeafType, + isListType, + isNonNullType, + isObjectType, +} from '../type/definition'; + +import { + collectFields, + collectSubfields as _collectSubfields, +} from './collectFields'; +import type { ExecutionContext } from './compiledDocument'; +import type { GraphQLCompiledSchema } from './compiledSchema'; +import type { ExecutionResult } from './execute'; +import { mapAsyncIterator } from './mapAsyncIterator'; +import { getArgumentValues } from './values'; + +/* eslint-disable max-params */ +// This file contains a lot of such errors but we plan to refactor it anyway +// so just disable it for entire file. + +type FieldsExecutor = ( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + fields: Map>, +) => PromiseOrValue>; + +export interface GraphQLExecutionConfig { + compiledSchema: GraphQLCompiledSchema; + exeContext: ExecutionContext; + rootValue: unknown; + contextValue: unknown; + variableValues: { [variable: string]: unknown }; +} + +/** + * @internal + */ +export class GraphQLExecution { + compiledSchema: GraphQLCompiledSchema; + exeContext: ExecutionContext; + rootValue: unknown; + contextValue: unknown; + variableValues: { [variable: string]: unknown }; + errors: Array; + + /** + * A memoized collection of relevant subfields with regard to the return + * type. Memoizing ensures the subfields are not repeatedly calculated, which + * saves overhead when resolving lists of values. + */ + collectSubfields = memoize3( + ( + exeContext: ExecutionContext, + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + ) => + _collectSubfields( + this.compiledSchema.schema, + exeContext.fragments, + this.variableValues, + returnType, + fieldNodes, + ), + ); + + constructor(config: GraphQLExecutionConfig) { + this.compiledSchema = config.compiledSchema; + this.exeContext = config.exeContext; + this.rootValue = config.rootValue; + this.contextValue = config.contextValue; + this.variableValues = config.variableValues; + this.errors = []; + } + + get [Symbol.toStringTag]() { + return 'GraphQLExecution'; + } + + executeQuery(exeContext: ExecutionContext): PromiseOrValue { + return this.executeQueryOrMutation( + exeContext, + this.executeFields.bind(this), + ); + } + + executeMutation( + exeContext: ExecutionContext, + ): PromiseOrValue { + return this.executeQueryOrMutation( + exeContext, + this.executeFieldsSerially.bind(this), + ); + } + + executeQueryOrMutation( + exeContext: ExecutionContext, + fieldsExecutor: FieldsExecutor, + ): PromiseOrValue { + // Return a Promise that will eventually resolve to the data described by + // The "Response" section of the GraphQL specification. + // + // If errors are encountered while executing a GraphQL field, only that + // field and its descendants will be omitted, and sibling fields will still + // be executed. An execution which encounters errors will still result in a + // resolved Promise. + // + // Errors from sub-fields of a NonNull type may propagate to the top level, + // at which point we still log the error and null the parent field, which + // in this case is the entire response. + try { + const result = this.executeQueryOrMutationRootFields( + exeContext, + fieldsExecutor, + ); + if (isPromise(result)) { + return result.then( + (data) => this.buildResponse(data, this.errors), + (error) => { + this.errors.push(error); + return this.buildResponse(null, this.errors); + }, + ); + } + return this.buildResponse(result, this.errors); + } catch (error) { + this.errors.push(error); + return this.buildResponse(null, this.errors); + } + } + + /** + * Given a completed execution context and data, build the `{ errors, data }` + * response defined by the "Response" section of the GraphQL specification. + */ + buildResponse( + data: ObjMap | null, + errors: ReadonlyArray, + ): ExecutionResult { + return errors.length === 0 ? { data } : { errors, data }; + } + + executeQueryOrMutationRootFields( + exeContext: ExecutionContext, + fieldsExecutor: FieldsExecutor, + ): PromiseOrValue> { + const { operation, fragments } = exeContext; + const rootType = this.compiledSchema.schema.getRootType( + operation.operation, + ); + if (rootType == null) { + throw new GraphQLError( + `Schema is not configured to execute ${operation.operation} operation.`, + { nodes: operation }, + ); + } + + const rootFields = collectFields( + this.compiledSchema.schema, + fragments, + this.variableValues, + rootType, + operation.selectionSet, + ); + const path = undefined; + + return fieldsExecutor( + exeContext, + rootType, + this.rootValue, + path, + rootFields, + ); + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that must be executed serially. + */ + executeFieldsSerially( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + fields: Map>, + ): PromiseOrValue> { + return promiseReduce( + fields.entries(), + (results, [responseName, fieldNodes]) => { + const fieldPath = addPath(path, responseName, parentType.name); + const result = this.executeField( + exeContext, + parentType, + sourceValue, + fieldNodes, + fieldPath, + ); + if (result === undefined) { + return results; + } + if (isPromise(result)) { + return result.then((resolvedResult) => { + results[responseName] = resolvedResult; + return results; + }); + } + results[responseName] = result; + return results; + }, + Object.create(null), + ); + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that may be executed in parallel. + */ + executeFields( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + fields: Map>, + ): PromiseOrValue> { + const results = Object.create(null); + let containsPromise = false; + + for (const [responseName, fieldNodes] of fields.entries()) { + const fieldPath = addPath(path, responseName, parentType.name); + const result = this.executeField( + exeContext, + parentType, + sourceValue, + fieldNodes, + fieldPath, + ); + + if (result !== undefined) { + results[responseName] = result; + if (isPromise(result)) { + containsPromise = true; + } + } + } + + // If there are no promises, we can just return the object + if (!containsPromise) { + return results; + } + + // Otherwise, results is a map from field name to the result of resolving that + // field, which is possibly a promise. Return a promise that will return this + // same map, but with any promises replaced with the values they resolved to. + return promiseForObject(results); + } + + /** + * Implements the "Executing fields" section of the spec + * In particular, this figures out the value that the field returns by + * calling its resolve function, then calls completeValue to complete promises, + * serialize scalars, or execute the sub-selection-set for objects. + */ + executeField( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + source: unknown, + fieldNodes: ReadonlyArray, + path: Path, + ): PromiseOrValue { + const fieldName = fieldNodes[0].name.value; + const fieldDef = this.compiledSchema.schema.getField(parentType, fieldName); + if (!fieldDef) { + return; + } + + const returnType = fieldDef.type; + const resolveFn = fieldDef.resolve ?? this.compiledSchema.fieldResolver; + + const info = this.buildResolveInfo( + exeContext, + fieldDef, + fieldNodes, + parentType, + path, + ); + + // Get the resolve function, regardless of if its result is normal or abrupt (error). + try { + // Build a JS object of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + // TODO: find a way to memoize, in case this field is within a List type. + const args = getArgumentValues( + fieldDef, + fieldNodes[0], + this.variableValues, + ); + + // The resolve function's optional third argument is a context value that + // is provided to every resolve within an execution. It is commonly + // used to represent an authenticated user, or request-specific caches. + const contextValue = this.contextValue; + + const result = resolveFn(source, args, contextValue, info); + + let completed; + if (isPromise(result)) { + completed = result.then((resolved) => + this.completeValue( + exeContext, + returnType, + fieldNodes, + info, + path, + resolved, + ), + ); + } else { + completed = this.completeValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); + } + + if (isPromise(completed)) { + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + return completed.then(undefined, (rawError) => { + const error = locatedError(rawError, fieldNodes, pathToArray(path)); + return this.handleFieldError(error, returnType); + }); + } + return completed; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(path)); + return this.handleFieldError(error, returnType); + } + } + + buildResolveInfo( + exeContext: ExecutionContext, + fieldDef: GraphQLField, + fieldNodes: ReadonlyArray, + parentType: GraphQLObjectType, + path: Path, + ): GraphQLResolveInfo { + // The resolve function's optional fourth argument is a collection of + // information about the current execution state. + return { + fieldName: fieldDef.name, + fieldNodes, + returnType: fieldDef.type, + parentType, + path, + schema: this.compiledSchema.schema, + fragments: exeContext.fragments, + rootValue: this.rootValue, + operation: exeContext.operation, + variableValues: this.variableValues, + }; + } + + handleFieldError(error: GraphQLError, returnType: GraphQLOutputType): null { + // If the field type is non-nullable, then it is resolved without any + // protection from errors, however it still properly locates the error. + if (isNonNullType(returnType)) { + throw error; + } + + // Otherwise, error protection is applied, logging the error and resolving + // a null value for this field if one is encountered. + this.errors.push(error); + return null; + } + + /** + * Implements the instructions for completeValue as defined in the + * "Value Completion" section of the spec. + * + * If the field type is Non-Null, then this recursively completes the value + * for the inner type. It throws a field error if that completion returns null, + * as per the "Nullability" section of the spec. + * + * If the field type is a List, then this recursively completes the value + * for the inner type on each item in the list. + * + * If the field type is a Scalar or Enum, ensures the completed value is a legal + * value of the type by calling the `serialize` method of GraphQL type + * definition. + * + * If the field is an abstract type, determine the runtime type of the value + * and then complete based on that type + * + * Otherwise, the field type expects a sub-selection set, and will complete the + * value by executing all sub-selections. + */ + completeValue( + exeContext: ExecutionContext, + returnType: GraphQLOutputType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue { + // If result is an Error, throw a located error. + if (result instanceof Error) { + throw result; + } + + // If field type is NonNull, complete for inner type, and throw field error + // if result is null. + if (isNonNullType(returnType)) { + const completed = this.completeValue( + exeContext, + returnType.ofType, + fieldNodes, + info, + path, + result, + ); + if (completed === null) { + throw new Error( + `Cannot return null for non-nullable field ${info.parentType.name}.${info.fieldName}.`, + ); + } + return completed; + } + + // If result value is null or undefined then return null. + if (result == null) { + return null; + } + + // If field type is List, complete each item in the list with the inner type + if (isListType(returnType)) { + return this.completeListValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); + } + + // If field type is a leaf type, Scalar or Enum, serialize to a valid value, + // returning null if serialization is not possible. + if (isLeafType(returnType)) { + return this.completeLeafValue(returnType, result); + } + + // If field type is an abstract type, Interface or Union, determine the + // runtime Object type and complete for that type. + if (isAbstractType(returnType)) { + return this.completeAbstractValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); + } + + // If field type is Object, execute and complete all sub-selections. + if (isObjectType(returnType)) { + return this.completeObjectValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); + } + /* c8 ignore next 6 */ + // Not reachable, all possible output types have been considered. + invariant( + false, + 'Cannot complete value of unexpected output type: ' + inspect(returnType), + ); + } + + /** + * Complete a async iterator value by completing the result and calling + * recursively until all the results are completed. + */ + async completeAsyncIteratorValue( + exeContext: ExecutionContext, + itemType: GraphQLOutputType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + iterator: AsyncIterator, + ): Promise> { + let containsPromise = false; + const completedResults = []; + let index = 0; + // eslint-disable-next-line no-constant-condition + while (true) { + const fieldPath = addPath(path, index, undefined); + try { + // eslint-disable-next-line no-await-in-loop + const { value, done } = await iterator.next(); + if (done) { + break; + } + + try { + // TODO can the error checking logic be consolidated with completeListValue? + const completedItem = this.completeValue( + exeContext, + itemType, + fieldNodes, + info, + fieldPath, + value, + ); + if (isPromise(completedItem)) { + containsPromise = true; + } + completedResults.push(completedItem); + } catch (rawError) { + completedResults.push(null); + const error = locatedError( + rawError, + fieldNodes, + pathToArray(fieldPath), + ); + this.handleFieldError(error, itemType); + } + } catch (rawError) { + completedResults.push(null); + const error = locatedError( + rawError, + fieldNodes, + pathToArray(fieldPath), + ); + this.handleFieldError(error, itemType); + break; + } + index += 1; + } + return containsPromise ? Promise.all(completedResults) : completedResults; + } + + /** + * Complete a list value by completing each item in the list with the + * inner type + */ + completeListValue( + exeContext: ExecutionContext, + returnType: GraphQLList, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + const itemType = returnType.ofType; + + if (isAsyncIterable(result)) { + const iterator = result[Symbol.asyncIterator](); + + return this.completeAsyncIteratorValue( + exeContext, + itemType, + fieldNodes, + info, + path, + iterator, + ); + } + + if (!isIterableObject(result)) { + throw new GraphQLError( + `Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`, + ); + } + + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another Promise. + let containsPromise = false; + const completedResults = Array.from(result, (item, index) => { + // No need to modify the info object containing the path, + // since from here on it is not ever accessed by resolver functions. + const itemPath = addPath(path, index, undefined); + try { + let completedItem; + if (isPromise(item)) { + completedItem = item.then((resolved) => + this.completeValue( + exeContext, + itemType, + fieldNodes, + info, + itemPath, + resolved, + ), + ); + } else { + completedItem = this.completeValue( + exeContext, + itemType, + fieldNodes, + info, + itemPath, + item, + ); + } + + if (isPromise(completedItem)) { + containsPromise = true; + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + return completedItem.then(undefined, (rawError) => { + const error = locatedError( + rawError, + fieldNodes, + pathToArray(itemPath), + ); + return this.handleFieldError(error, itemType); + }); + } + return completedItem; + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + return this.handleFieldError(error, itemType); + } + }); + + return containsPromise ? Promise.all(completedResults) : completedResults; + } + + /** + * Complete a Scalar or Enum by serializing to a valid value, returning + * null if serialization is not possible. + */ + completeLeafValue(returnType: GraphQLLeafType, result: unknown): unknown { + const serializedResult = returnType.serialize(result); + if (serializedResult == null) { + throw new Error( + `Expected \`${inspect(returnType)}.serialize(${inspect( + result, + )})\` to ` + + `return non-nullable value, returned: ${inspect(serializedResult)}`, + ); + } + return serializedResult; + } + + /** + * Complete a value of an abstract type by determining the runtime object type + * of that value, then complete the value for that type. + */ + completeAbstractValue( + exeContext: ExecutionContext, + returnType: GraphQLAbstractType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + const resolveTypeFn = + returnType.resolveType ?? this.compiledSchema.typeResolver; + const contextValue = this.contextValue; + const runtimeType = resolveTypeFn(result, contextValue, info, returnType); + + if (isPromise(runtimeType)) { + return runtimeType.then((resolvedRuntimeType) => + this.completeObjectValue( + exeContext, + this.ensureValidRuntimeType( + resolvedRuntimeType, + returnType, + fieldNodes, + info, + result, + ), + fieldNodes, + info, + path, + result, + ), + ); + } + + return this.completeObjectValue( + exeContext, + this.ensureValidRuntimeType( + runtimeType, + returnType, + fieldNodes, + info, + result, + ), + fieldNodes, + info, + path, + result, + ); + } + + ensureValidRuntimeType( + runtimeTypeName: unknown, + returnType: GraphQLAbstractType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + result: unknown, + ): GraphQLObjectType { + if (runtimeTypeName == null) { + throw new GraphQLError( + `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, + { nodes: fieldNodes }, + ); + } + + // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` + // TODO: remove in 17.0.0 release + if (isObjectType(runtimeTypeName)) { + throw new GraphQLError( + 'Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.', + ); + } + + if (typeof runtimeTypeName !== 'string') { + throw new GraphQLError( + `Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + + `value ${inspect(result)}, received "${inspect(runtimeTypeName)}".`, + ); + } + + const runtimeType = this.compiledSchema.schema.getType(runtimeTypeName); + if (runtimeType == null) { + throw new GraphQLError( + `Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, + { nodes: fieldNodes }, + ); + } + + if (!isObjectType(runtimeType)) { + throw new GraphQLError( + `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, + { nodes: fieldNodes }, + ); + } + + if (!this.compiledSchema.schema.isSubType(returnType, runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, + { nodes: fieldNodes }, + ); + } + + return runtimeType; + } + + /** + * Complete an Object value by executing all sub-selections. + */ + completeObjectValue( + exeContext: ExecutionContext, + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + // Collect sub-fields to execute to complete this value. + const subFieldNodes = this.collectSubfields( + exeContext, + returnType, + fieldNodes, + ); + + // If there is an isTypeOf predicate function, call it with the + // current result. If isTypeOf returns false, then raise an error rather + // than continuing execution. + if (returnType.isTypeOf) { + const isTypeOf = returnType.isTypeOf(result, this.contextValue, info); + + if (isPromise(isTypeOf)) { + return isTypeOf.then((resolvedIsTypeOf) => { + if (!resolvedIsTypeOf) { + throw this.invalidReturnTypeError(returnType, result, fieldNodes); + } + return this.executeFields( + exeContext, + returnType, + result, + path, + subFieldNodes, + ); + }); + } + + if (!isTypeOf) { + throw this.invalidReturnTypeError(returnType, result, fieldNodes); + } + } + + return this.executeFields( + exeContext, + returnType, + result, + path, + subFieldNodes, + ); + } + + invalidReturnTypeError( + returnType: GraphQLObjectType, + result: unknown, + fieldNodes: ReadonlyArray, + ): GraphQLError { + return new GraphQLError( + `Expected value of type "${returnType.name}" but got: ${inspect( + result, + )}.`, + { nodes: fieldNodes }, + ); + } + + executeSubscription( + exeContext: ExecutionContext, + ): PromiseOrValue< + ExecutionResult | AsyncGenerator + > { + const resultOrStream = this.createSourceEventStreamImpl(exeContext); + + if (isPromise(resultOrStream)) { + return resultOrStream.then((resolvedResultOrStream) => + this.mapSourceToResponse(exeContext, resolvedResultOrStream), + ); + } + + return this.mapSourceToResponse(exeContext, resultOrStream); + } + + mapSourceToResponse( + exeContext: ExecutionContext, + resultOrStream: ExecutionResult | AsyncIterable, + ): PromiseOrValue< + AsyncGenerator | ExecutionResult + > { + if (!isAsyncIterable(resultOrStream)) { + return resultOrStream; + } + + // 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` provides the + // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the + // "this.executeQuery" algorithm, for which `execute` is also used. + return mapAsyncIterator(resultOrStream, (payload: unknown) => { + const eventExecution = new GraphQLExecution({ + compiledSchema: this.compiledSchema, + exeContext: this.exeContext, + rootValue: payload, + contextValue: this.contextValue, + variableValues: this.variableValues, + }); + return eventExecution.executeQuery(exeContext); + }); + } + + createSourceEventStreamImpl( + exeContext: ExecutionContext, + ): PromiseOrValue | ExecutionResult> { + try { + const eventStream = this.executeSubscriptionRootField(exeContext); + if (isPromise(eventStream)) { + return eventStream.then(undefined, (error) => ({ errors: [error] })); + } + + return eventStream; + } catch (error) { + return { errors: [error] }; + } + } + + executeSubscriptionRootField( + exeContext: ExecutionContext, + ): PromiseOrValue> { + const { fragments, operation } = exeContext; + + const rootType = this.compiledSchema.schema.getSubscriptionType(); + if (rootType == null) { + throw new GraphQLError( + 'Schema is not configured to execute subscription operation.', + { nodes: operation }, + ); + } + + const rootFields = collectFields( + this.compiledSchema.schema, + fragments, + this.variableValues, + rootType, + operation.selectionSet, + ); + const [responseName, fieldNodes] = [...rootFields.entries()][0]; + const fieldName = fieldNodes[0].name.value; + const fieldDef = this.compiledSchema.schema.getField(rootType, fieldName); + + if (!fieldDef) { + throw new GraphQLError( + `The subscription field "${fieldName}" is not defined.`, + { nodes: fieldNodes }, + ); + } + + const path = addPath(undefined, responseName, rootType.name); + const info = this.buildResolveInfo( + exeContext, + fieldDef, + fieldNodes, + rootType, + path, + ); + + try { + // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. + // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. + + // Build a JS object of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + const args = getArgumentValues( + fieldDef, + fieldNodes[0], + this.variableValues, + ); + + // The resolve function's optional third argument is a context value that + // is provided to every resolve within an execution. It is commonly + // used to represent an authenticated user, or request-specific caches. + const contextValue = this.contextValue; + + // Call the `subscribe()` resolver or the default resolver to produce an + // AsyncIterable yielding raw payloads. + const resolveFn = + fieldDef.subscribe ?? this.compiledSchema.subscribeFieldResolver; + const result = resolveFn(this.rootValue, args, contextValue, info); + + if (isPromise(result)) { + return result.then(this.assertEventStream).then(undefined, (error) => { + throw locatedError(error, fieldNodes, pathToArray(path)); + }); + } + + return this.assertEventStream(result); + } catch (error) { + throw locatedError(error, fieldNodes, pathToArray(path)); + } + } + + assertEventStream(result: unknown): AsyncIterable { + if (result instanceof Error) { + throw result; + } + + // Assert field returned an event stream, otherwise yield an error. + if (!isAsyncIterable(result)) { + throw new GraphQLError( + 'Subscription field must return Async Iterable. ' + + `Received: ${inspect(result)}.`, + ); + } + + return result; + } +} diff --git a/src/execution/index.ts b/src/execution/index.ts index b27a2c291c..68e110c595 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -1,11 +1,12 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; +export { defaultFieldResolver, defaultTypeResolver } from './compiledDocument'; + export { createSourceEventStream, execute, + executeSubscriptionEvent, executeSync, - defaultFieldResolver, - defaultTypeResolver, subscribe, } from './execute'; diff --git a/src/graphql.ts b/src/graphql.ts index ffad9123c1..8cc5ff19fa 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -67,7 +67,9 @@ export interface GraphQLArgs { typeResolver?: Maybe>; } -export function graphql(args: GraphQLArgs): Promise { +export function graphql( + args: GraphQLArgs, +): Promise> { // Always return a Promise for a consistent API. return new Promise((resolve) => resolve(graphqlImpl(args))); } @@ -78,7 +80,9 @@ export function graphql(args: GraphQLArgs): Promise { * However, it guarantees to complete synchronously (or throw an error) assuming * that all field resolvers are also synchronous. */ -export function graphqlSync(args: GraphQLArgs): ExecutionResult { +export function graphqlSync( + args: GraphQLArgs, +): ExecutionResult | AsyncGenerator { const result = graphqlImpl(args); // Assert that the execution was synchronous. @@ -89,7 +93,11 @@ export function graphqlSync(args: GraphQLArgs): ExecutionResult { return result; } -function graphqlImpl(args: GraphQLArgs): PromiseOrValue { +function graphqlImpl( + args: GraphQLArgs, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { const { schema, source, diff --git a/src/index.ts b/src/index.ts index bce254f808..9e7c439e83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -321,6 +321,7 @@ export { getDirectiveValues, subscribe, createSourceEventStream, + executeSubscriptionEvent, } from './execution/index'; export type { diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index 35e9f94b00..cf33869e3a 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -1,8 +1,10 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; + import { introspectionFromSchema } from '../../utilities/introspectionFromSchema'; import { graphqlSync } from '../../graphql'; @@ -104,6 +106,10 @@ const SubscriptionType = new GraphQLObjectType({ subscribeToEnum: { type: ColorType, args: { color: { type: ColorType } }, + // eslint-disable-next-line @typescript-eslint/require-await + async *subscribe(_source, { color }) { + yield { subscribeToEnum: color }; /* c8 ignore start */ + } /* c8 ignore stop */, resolve: (_source, { color }) => color, }, }, @@ -248,13 +254,15 @@ describe('Type System: Enum Values', () => { }); }); - it('accepts enum literals as input arguments to subscriptions', () => { + it('accepts enum literals as input arguments to subscriptions', async () => { const doc = 'subscription ($color: Color!) { subscribeToEnum(color: $color) }'; const result = executeQuery(doc, { color: 'GREEN' }); - expect(result).to.deep.equal({ - data: { subscribeToEnum: 'GREEN' }, + assert(isAsyncIterable(result)); + expect(await result.next()).to.deep.equal({ + value: { data: { subscribeToEnum: 'GREEN' } }, + done: false, }); }); diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 435abc2d7a..e7945c80e5 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -3,6 +3,7 @@ import { describe, it } from 'mocha'; import { dedent } from '../../__testUtils__/dedent'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; import type { Maybe } from '../../jsutils/Maybe'; import type { ASTNode } from '../../language/ast'; @@ -76,6 +77,7 @@ describe('Schema Builder', () => { source: '{ str }', rootValue: { str: 123 }, }); + assert(!isAsyncIterable(result)); expect(result.data).to.deep.equal({ str: '123' }); }); diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 8c043f0e77..cb90567974 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -3,6 +3,8 @@ import { describe, it } from 'mocha'; import { dedent } from '../../__testUtils__/dedent'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; + import { assertEnumType, GraphQLEnumType, @@ -591,6 +593,7 @@ describe('Type System: build schema from introspection', () => { variableValues: { v: 'baz' }, }); + assert(!isAsyncIterable(result)); expect(result.data).to.deep.equal({ foo: 'bar' }); }); diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index 78c1b30244..5daed3fb28 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -1,4 +1,5 @@ import { invariant } from '../jsutils/invariant'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; import { parse } from '../language/parser'; @@ -35,6 +36,7 @@ export function introspectionFromSchema( const document = parse(getIntrospectionQuery(optionsWithDefaults)); const result = executeSync({ schema, document }); + invariant(!isAsyncIterable(result)); invariant(result.errors == null && result.data != null); return result.data as any; }