diff --git a/docs/APIReference-Errors.md b/docs/APIReference-Errors.md index eccd0934ce..dd28900c11 100644 --- a/docs/APIReference-Errors.md +++ b/docs/APIReference-Errors.md @@ -3,7 +3,7 @@ title: graphql/error layout: ../_core/GraphQLJSLayout category: API Reference permalink: /graphql-js/error/ -sublinks: formatError,GraphQLError,locatedError,syntaxError +sublinks: formatError,GraphQLError,locatedError,syntaxError,GraphQLAggregateError next: /graphql-js/execution/ --- @@ -107,3 +107,20 @@ type GraphQLErrorLocation = { Given a GraphQLError, format it according to the rules described by the Response Format, Errors section of the GraphQL Specification. + +### GraphQLAggregateError + +```js +class GraphQLAggregateError extends Error { + constructor( + errors: Array, + message?: string + ) +} +``` + +A helper class for bundling multiple distinct errors. When a +GraphQLAggregateError is thrown during execution of a GraphQL operation, +a GraphQLError will be produced from each individual errors and will be +reported separately, according to the rules described by the Response +Format, Errors section of the GraphQL Specification. diff --git a/src/README.md b/src/README.md index 7a67bcb569..53b9a06f76 100644 --- a/src/README.md +++ b/src/README.md @@ -20,4 +20,3 @@ Each sub directory within is a sub-module of graphql-js: - [`graphql/error`](error/README.md): Creating and formatting GraphQL errors. - [`graphql/utilities`](utilities/README.md): Common useful computations upon the GraphQL language and type objects. -- [`graphql/subscription`](subscription/README.md): Subscribe to data updates. diff --git a/src/error/GraphQLAggregateError.ts b/src/error/GraphQLAggregateError.ts new file mode 100644 index 0000000000..915e0f0cca --- /dev/null +++ b/src/error/GraphQLAggregateError.ts @@ -0,0 +1,47 @@ +import { GraphQLError } from './GraphQLError'; + +/** + * A GraphQLAggregateError is a container for multiple errors. + * + * This helper can be used to report multiple distinct errors simultaneously. + * Note that error handlers must be aware aggregated errors may be reported so as to + * properly handle the contained errors. + * + * See also: + * https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError + * https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/es.aggregate-error.js + * https://github.com/sindresorhus/aggregate-error + * + */ +export class GraphQLAggregateError extends Error { + readonly errors!: ReadonlyArray; + + constructor(errors: ReadonlyArray, message?: string) { + super(message); + + Object.defineProperties(this, { + name: { value: 'GraphQLAggregateError' }, + message: { + value: message, + writable: true, + }, + errors: { + value: errors, + }, + }); + } + + get [Symbol.toStringTag](): string { + return 'GraphQLAggregateError'; + } +} + +export function isAggregateOfGraphQLErrors( + error: unknown, +): error is GraphQLAggregateError { + return ( + error instanceof GraphQLAggregateError && + error.errors.every((subError) => subError instanceof GraphQLError) + ); +} diff --git a/src/error/__tests__/GraphQLAggregateError-test.ts b/src/error/__tests__/GraphQLAggregateError-test.ts new file mode 100644 index 0000000000..cbde1895b8 --- /dev/null +++ b/src/error/__tests__/GraphQLAggregateError-test.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { GraphQLAggregateError } from '../GraphQLAggregateError'; + +describe('GraphQLAggregateError', () => { + it('is a class and is a subclass of Error', () => { + const errors = [new Error('Error1'), new Error('Error2')]; + expect(new GraphQLAggregateError(errors)).to.be.instanceof(Error); + expect(new GraphQLAggregateError(errors)).to.be.instanceof( + GraphQLAggregateError, + ); + }); + + it('has a name, errors, and a message (if provided)', () => { + const errors = [new Error('Error1'), new Error('Error2')]; + const e = new GraphQLAggregateError(errors, 'msg'); + + expect(e).to.include({ + name: 'GraphQLAggregateError', + errors, + message: 'msg', + }); + }); +}); diff --git a/src/error/index.ts b/src/error/index.ts index b947c93fc7..48cdb5fd15 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1,3 +1,8 @@ +export { + GraphQLAggregateError, + isAggregateOfGraphQLErrors, +} from './GraphQLAggregateError'; + export { GraphQLError, printError, formatError } from './GraphQLError'; export type { GraphQLFormattedError } from './GraphQLError'; diff --git a/src/execution/README.md b/src/execution/README.md index 6540f323fe..10bf0e8866 100644 --- a/src/execution/README.md +++ b/src/execution/README.md @@ -7,3 +7,8 @@ fulfilling a GraphQL request. import { execute } from 'graphql/execution'; // ES6 var GraphQLExecution = require('graphql/execution'); // CommonJS ``` + +```js +import { subscribe, createSourceEventStream } from 'graphql/execution'; // ES6 +var GraphQLSubscription = require('graphql/execution'); // CommonJS +``` diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index f5e55b9ef8..005d408dea 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -9,6 +9,7 @@ import { invariant } from '../../jsutils/invariant'; import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; +import { GraphQLAggregateError } from '../../error/GraphQLAggregateError'; import { GraphQLSchema } from '../../type/schema'; import { GraphQLInt, GraphQLBoolean, GraphQLString } from '../../type/scalars'; import { @@ -37,6 +38,22 @@ describe('Execute: Handles basic execution tasks', () => { expect(() => executeSync({ schema })).to.throw('Must provide document.'); }); + it('throws if invalid document is provided', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Type', + fields: { + a: { type: GraphQLString }, + }, + }), + }); + + // @ts-expect-error + expect(() => executeSync({ schema, document: {} })).to.throw( + 'document.definitions is not iterable', + ); + }); + it('throws if no schema is provided', () => { const document = parse('{ field }'); @@ -403,17 +420,22 @@ describe('Execute: Handles basic execution tasks', () => { fields: { sync: { type: GraphQLString }, syncError: { type: GraphQLString }, + syncAggregateError: { type: GraphQLString }, syncRawError: { type: GraphQLString }, syncReturnError: { type: GraphQLString }, + syncReturnAggregateError: { type: GraphQLString }, syncReturnErrorList: { type: new GraphQLList(GraphQLString) }, async: { type: GraphQLString }, asyncReject: { type: GraphQLString }, + asyncRejectAggregate: { type: GraphQLString }, asyncRejectWithExtensions: { type: GraphQLString }, asyncRawReject: { type: GraphQLString }, asyncEmptyReject: { type: GraphQLString }, asyncError: { type: GraphQLString }, + asyncAggregateError: { type: GraphQLString }, asyncRawError: { type: GraphQLString }, asyncReturnError: { type: GraphQLString }, + asyncReturnAggregateError: { type: GraphQLString }, asyncReturnErrorWithExtensions: { type: GraphQLString }, }, }), @@ -423,16 +445,22 @@ describe('Execute: Handles basic execution tasks', () => { { sync syncError + syncAggregateError syncRawError syncReturnError + syncReturnAggregateError syncReturnErrorList async asyncReject + asyncRejectAggregate asyncRawReject + asyncRawRejectAggregate asyncEmptyReject asyncError + asyncAggregateError asyncRawError asyncReturnError + asyncReturnAggregateError asyncReturnErrorWithExtensions } `); @@ -444,6 +472,12 @@ describe('Execute: Handles basic execution tasks', () => { syncError() { throw new Error('Error getting syncError'); }, + syncAggregateError() { + throw new GraphQLAggregateError([ + new Error('Error1 getting syncAggregateError'), + new Error('Error2 getting syncAggregateError'), + ]); + }, syncRawError() { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw 'Error getting syncRawError'; @@ -451,6 +485,12 @@ describe('Execute: Handles basic execution tasks', () => { syncReturnError() { return new Error('Error getting syncReturnError'); }, + syncReturnAggregateError() { + return new GraphQLAggregateError([ + new Error('Error1 getting syncReturnAggregateError'), + new Error('Error2 getting syncReturnAggregateError'), + ]); + }, syncReturnErrorList() { return [ 'sync0', @@ -467,6 +507,16 @@ describe('Execute: Handles basic execution tasks', () => { reject(new Error('Error getting asyncReject')), ); }, + asyncRejectAggregate() { + return new Promise((_, reject) => + reject( + new GraphQLAggregateError([ + new Error('Error1 getting asyncRejectAggregate'), + new Error('Error2 getting asyncRejectAggregate'), + ]), + ), + ); + }, asyncRawReject() { // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject('Error getting asyncRawReject'); @@ -480,6 +530,14 @@ describe('Execute: Handles basic execution tasks', () => { throw new Error('Error getting asyncError'); }); }, + asyncAggregateError() { + return new Promise(() => { + throw new GraphQLAggregateError([ + new Error('Error1 getting asyncAggregateError'), + new Error('Error2 getting asyncAggregateError'), + ]); + }); + }, asyncRawError() { return new Promise(() => { // eslint-disable-next-line @typescript-eslint/no-throw-literal @@ -489,6 +547,14 @@ describe('Execute: Handles basic execution tasks', () => { asyncReturnError() { return Promise.resolve(new Error('Error getting asyncReturnError')); }, + asyncReturnAggregateError() { + return Promise.resolve( + new GraphQLAggregateError([ + new Error('Error1 getting asyncReturnAggregateError'), + new Error('Error2 getting asyncReturnAggregateError'), + ]), + ); + }, asyncReturnErrorWithExtensions() { const error = new Error('Error getting asyncReturnErrorWithExtensions'); // @ts-expect-error @@ -503,16 +569,21 @@ describe('Execute: Handles basic execution tasks', () => { data: { sync: 'sync', syncError: null, + syncAggregateError: null, syncRawError: null, syncReturnError: null, + syncReturnAggregateError: null, syncReturnErrorList: ['sync0', null, 'sync2', null], async: 'async', asyncReject: null, + asyncRejectAggregate: null, asyncRawReject: null, asyncEmptyReject: null, asyncError: null, + asyncAggregateError: null, asyncRawError: null, asyncReturnError: null, + asyncReturnAggregateError: null, asyncReturnErrorWithExtensions: null, }, errors: [ @@ -522,58 +593,108 @@ describe('Execute: Handles basic execution tasks', () => { path: ['syncError'], }, { - message: 'Unexpected error value: "Error getting syncRawError"', + message: 'Error1 getting syncAggregateError', + locations: [{ line: 5, column: 9 }], + path: ['syncAggregateError'], + }, + { + message: 'Error2 getting syncAggregateError', locations: [{ line: 5, column: 9 }], + path: ['syncAggregateError'], + }, + { + message: 'Unexpected error value: "Error getting syncRawError"', + locations: [{ line: 6, column: 9 }], path: ['syncRawError'], }, { message: 'Error getting syncReturnError', - locations: [{ line: 6, column: 9 }], + locations: [{ line: 7, column: 9 }], path: ['syncReturnError'], }, + { + message: 'Error1 getting syncReturnAggregateError', + locations: [{ line: 8, column: 9 }], + path: ['syncReturnAggregateError'], + }, + { + message: 'Error2 getting syncReturnAggregateError', + locations: [{ line: 8, column: 9 }], + path: ['syncReturnAggregateError'], + }, { message: 'Error getting syncReturnErrorList1', - locations: [{ line: 7, column: 9 }], + locations: [{ line: 9, column: 9 }], path: ['syncReturnErrorList', 1], }, { message: 'Error getting syncReturnErrorList3', - locations: [{ line: 7, column: 9 }], + locations: [{ line: 9, column: 9 }], path: ['syncReturnErrorList', 3], }, { message: 'Error getting asyncReject', - locations: [{ line: 9, column: 9 }], + locations: [{ line: 11, column: 9 }], path: ['asyncReject'], }, + { + message: 'Error1 getting asyncRejectAggregate', + locations: [{ line: 12, column: 9 }], + path: ['asyncRejectAggregate'], + }, + { + message: 'Error2 getting asyncRejectAggregate', + locations: [{ line: 12, column: 9 }], + path: ['asyncRejectAggregate'], + }, { message: 'Unexpected error value: "Error getting asyncRawReject"', - locations: [{ line: 10, column: 9 }], + locations: [{ line: 13, column: 9 }], path: ['asyncRawReject'], }, { message: 'Unexpected error value: undefined', - locations: [{ line: 11, column: 9 }], + locations: [{ line: 15, column: 9 }], path: ['asyncEmptyReject'], }, { message: 'Error getting asyncError', - locations: [{ line: 12, column: 9 }], + locations: [{ line: 16, column: 9 }], path: ['asyncError'], }, + { + message: 'Error1 getting asyncAggregateError', + locations: [{ line: 17, column: 9 }], + path: ['asyncAggregateError'], + }, + { + message: 'Error2 getting asyncAggregateError', + locations: [{ line: 17, column: 9 }], + path: ['asyncAggregateError'], + }, { message: 'Unexpected error value: "Error getting asyncRawError"', - locations: [{ line: 13, column: 9 }], + locations: [{ line: 18, column: 9 }], path: ['asyncRawError'], }, { message: 'Error getting asyncReturnError', - locations: [{ line: 14, column: 9 }], + locations: [{ line: 19, column: 9 }], path: ['asyncReturnError'], }, + { + message: 'Error1 getting asyncReturnAggregateError', + locations: [{ line: 20, column: 9 }], + path: ['asyncReturnAggregateError'], + }, + { + message: 'Error2 getting asyncReturnAggregateError', + locations: [{ line: 20, column: 9 }], + path: ['asyncReturnAggregateError'], + }, { message: 'Error getting asyncReturnErrorWithExtensions', - locations: [{ line: 15, column: 9 }], + locations: [{ line: 21, column: 9 }], path: ['asyncReturnErrorWithExtensions'], extensions: { foo: 'bar' }, }, diff --git a/src/subscription/__tests__/mapAsyncIterator-test.ts b/src/execution/__tests__/mapAsyncIterator-test.ts similarity index 100% rename from src/subscription/__tests__/mapAsyncIterator-test.ts rename to src/execution/__tests__/mapAsyncIterator-test.ts diff --git a/src/subscription/__tests__/simplePubSub-test.ts b/src/execution/__tests__/simplePubSub-test.ts similarity index 100% rename from src/subscription/__tests__/simplePubSub-test.ts rename to src/execution/__tests__/simplePubSub-test.ts diff --git a/src/subscription/__tests__/simplePubSub.ts b/src/execution/__tests__/simplePubSub.ts similarity index 100% rename from src/subscription/__tests__/simplePubSub.ts rename to src/execution/__tests__/simplePubSub.ts diff --git a/src/subscription/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts similarity index 93% rename from src/subscription/__tests__/subscribe-test.ts rename to src/execution/__tests__/subscribe-test.ts index 004429630e..39a6592bf4 100644 --- a/src/subscription/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -13,7 +13,10 @@ import { GraphQLSchema } from '../../type/schema'; import { GraphQLList, GraphQLObjectType } from '../../type/definition'; import { GraphQLInt, GraphQLString, GraphQLBoolean } from '../../type/scalars'; -import { createSourceEventStream, subscribe } from '../subscribe'; +import { GraphQLAggregateError } from '../../error/GraphQLAggregateError'; + +import { Executor } from '../executor'; +import { subscribe } from '../subscribe'; import { SimplePubSub } from './simplePubSub'; @@ -408,13 +411,12 @@ describe('Subscription Initialization Phase', () => { const document = parse('subscription { foo }'); const result = await subscribe({ schema, document }); - expect(await createSourceEventStream(schema, document)).to.deep.equal( - result, - ); + const executor = new Executor({ schema, document }); + expect(await executor.createSourceEventStream()).to.deep.equal(result); return result; } - const expectedResult = { + const singleErrorResult = { errors: [ { message: 'test error', @@ -427,24 +429,66 @@ describe('Subscription Initialization Phase', () => { expectJSON( // Returning an error await subscribeWithFn(() => new Error('test error')), - ).to.deep.equal(expectedResult); + ).to.deep.equal(singleErrorResult); expectJSON( // Throwing an error await subscribeWithFn(() => { throw new Error('test error'); }), - ).to.deep.equal(expectedResult); + ).to.deep.equal(singleErrorResult); expectJSON( // Resolving to an error await subscribeWithFn(() => Promise.resolve(new Error('test error'))), - ).to.deep.equal(expectedResult); + ).to.deep.equal(singleErrorResult); expectJSON( // Rejecting with an error await subscribeWithFn(() => Promise.reject(new Error('test error'))), - ).to.deep.equal(expectedResult); + ).to.deep.equal(singleErrorResult); + + const multipleErrorsResult = { + errors: [ + { + message: 'test error1', + locations: [{ line: 1, column: 16 }], + path: ['foo'], + }, + { + message: 'test error2', + locations: [{ line: 1, column: 16 }], + path: ['foo'], + }, + ], + }; + + const aggregateError = new GraphQLAggregateError([ + new Error('test error1'), + new Error('test error2'), + ]); + + expectJSON( + // Returning an error + await subscribeWithFn(() => aggregateError), + ).to.deep.equal(multipleErrorsResult); + + expectJSON( + // Throwing an error + await subscribeWithFn(() => { + throw aggregateError; + }), + ).to.deep.equal(multipleErrorsResult); + + expectJSON( + // Resolving to an error + await subscribeWithFn(() => Promise.resolve(aggregateError)), + ).to.deep.equal(multipleErrorsResult); + + expectJSON( + // Rejecting with an error + await subscribeWithFn(() => Promise.reject(aggregateError)), + ).to.deep.equal(multipleErrorsResult); }); it('resolves to an error if variables were wrong type', async () => { diff --git a/src/execution/execute.ts b/src/execution/execute.ts index d1ed7d3bc5..e089eac15f 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1,84 +1,23 @@ -import type { Path } from '../jsutils/Path'; import type { ObjMap } from '../jsutils/ObjMap'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; import type { Maybe } from '../jsutils/Maybe'; -import { inspect } from '../jsutils/inspect'; -import { memoize3 } from '../jsutils/memoize3'; -import { invariant } from '../jsutils/invariant'; -import { devAssert } from '../jsutils/devAssert'; import { isPromise } from '../jsutils/isPromise'; -import { isObjectLike } from '../jsutils/isObjectLike'; -import { promiseReduce } from '../jsutils/promiseReduce'; -import { promiseForObject } from '../jsutils/promiseForObject'; -import { addPath, pathToArray } from '../jsutils/Path'; -import { isIterableObject } from '../jsutils/isIterableObject'; - -import type { GraphQLFormattedError } from '../error/GraphQLError'; -import { GraphQLError } from '../error/GraphQLError'; -import { locatedError } from '../error/locatedError'; import type { - DocumentNode, - OperationDefinitionNode, - FieldNode, - FragmentDefinitionNode, - ASTNode, -} from '../language/ast'; -import { Kind } from '../language/kinds'; + GraphQLError, + GraphQLFormattedError, +} from '../error/GraphQLError'; +import { isAggregateOfGraphQLErrors } from '../error/GraphQLAggregateError'; + +import type { DocumentNode } from '../language/ast'; import type { GraphQLSchema } from '../type/schema'; import type { - GraphQLObjectType, - GraphQLOutputType, - GraphQLLeafType, - GraphQLAbstractType, - GraphQLField, GraphQLFieldResolver, - GraphQLResolveInfo, GraphQLTypeResolver, - GraphQLList, } from '../type/definition'; -import { assertValidSchema } from '../type/validate'; -import { - SchemaMetaFieldDef, - TypeMetaFieldDef, - TypeNameMetaFieldDef, -} from '../type/introspection'; -import { - isObjectType, - isAbstractType, - isLeafType, - isListType, - isNonNullType, -} from '../type/definition'; - -import { getOperationRootType } from '../utilities/getOperationRootType'; -import { getVariableValues, getArgumentValues } from './values'; -import { - collectFields, - collectSubfields as _collectSubfields, -} from './collectFields'; - -/** - * 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, - ), -); +import { Executor } from './executor'; /** * Terminology @@ -100,24 +39,6 @@ const collectSubfields = memoize3( * 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; - errors: Array; -} - /** * The result of GraphQL execution. * @@ -165,47 +86,21 @@ export interface ExecutionArgs { * a GraphQLError will be thrown immediately explaining the invalid input. */ export function execute(args: ExecutionArgs): PromiseOrValue { - const { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - } = args; - - // If arguments are missing or incorrect, throw an error. - assertValidExecutionArguments(schema, document, variableValues); - // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. - const exeContext = buildExecutionContext( - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - ); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; + let executor: Executor; + try { + executor = new Executor(args); + } catch (error) { + // Note: if the Executor constructor throws a GraphQLAggregateError, it will be + // of type GraphQLAggregateError, but this is checked explicitly. + if (isAggregateOfGraphQLErrors(error)) { + return { errors: error.errors }; + } + throw error; } - // 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. - const data = executeOperation(exeContext, exeContext.operation, rootValue); - return buildResponse(exeContext, data); + return executor.executeQueryOrMutation(); } /** @@ -223,822 +118,3 @@ export function executeSync(args: ExecutionArgs): ExecutionResult { return result; } - -/** - * Given a completed execution context and data, build the `{ errors, data }` - * response defined by the "Response" section of the GraphQL specification. - */ -function buildResponse( - exeContext: ExecutionContext, - data: PromiseOrValue | null>, -): PromiseOrValue { - if (isPromise(data)) { - return data.then((resolved) => buildResponse(exeContext, resolved)); - } - return exeContext.errors.length === 0 - ? { data } - : { errors: exeContext.errors, data }; -} - -/** - * Essential assertions before executing to provide developer feedback for - * improper use of the GraphQL library. - * - * @internal - */ -export function assertValidExecutionArguments( - schema: GraphQLSchema, - document: DocumentNode, - rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, -): void { - devAssert(document, 'Must provide document.'); - - // If the schema used for execution is invalid, throw an error. - assertValidSchema(schema); - - // Variables, if provided, must be an object. - devAssert( - rawVariableValues == null || isObjectLike(rawVariableValues), - 'Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.', - ); -} - -/** - * 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. - * - * @internal - */ -export function buildExecutionContext( - schema: GraphQLSchema, - document: DocumentNode, - rootValue: unknown, - contextValue: unknown, - rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, - operationName: Maybe, - fieldResolver: Maybe>, - typeResolver?: Maybe>, -): ReadonlyArray | ExecutionContext { - 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; - } - } - - if (!operation) { - if (operationName != null) { - return [new GraphQLError(`Unknown operation named "${operationName}".`)]; - } - return [new GraphQLError('Must provide an operation.')]; - } - - // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') - const variableDefinitions = operation.variableDefinitions ?? []; - - const coercedVariableValues = getVariableValues( - schema, - variableDefinitions, - rawVariableValues ?? {}, - { maxErrors: 50 }, - ); - - if (coercedVariableValues.errors) { - return coercedVariableValues.errors; - } - - return { - schema, - fragments, - rootValue, - contextValue, - operation, - variableValues: coercedVariableValues.coerced, - fieldResolver: fieldResolver ?? defaultFieldResolver, - typeResolver: typeResolver ?? defaultTypeResolver, - errors: [], - }; -} - -/** - * Implements the "Executing operations" section of the spec. - */ -function executeOperation( - exeContext: ExecutionContext, - operation: OperationDefinitionNode, - rootValue: unknown, -): PromiseOrValue | null> { - const type = getOperationRootType(exeContext.schema, operation); - const fields = collectFields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - type, - operation.selectionSet, - ); - - const path = undefined; - - // 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 = - operation.operation === 'mutation' - ? executeFieldsSerially(exeContext, type, rootValue, path, fields) - : executeFields(exeContext, type, rootValue, path, fields); - if (isPromise(result)) { - return result.then(undefined, (error) => { - exeContext.errors.push(error); - return Promise.resolve(null); - }); - } - return result; - } catch (error) { - exeContext.errors.push(error); - return null; - } -} - -/** - * 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 field" 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 fieldDef = getFieldDef(exeContext.schema, parentType, fieldNodes[0]); - 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) => - handleRawError(exeContext, returnType, rawError, fieldNodes, path), - ); - } - return completed; - } catch (rawError) { - return handleRawError(exeContext, returnType, rawError, fieldNodes, path); - } -} - -/** - * @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 handleRawError( - exeContext: ExecutionContext, - returnType: GraphQLOutputType, - rawError: unknown, - fieldNodes: ReadonlyArray, - path?: Maybe>, -): null { - const error = locatedError(rawError, fieldNodes, pathToArray(path)); - - // 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 - * "Field entries" 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, - ); - } - - // If field type is Object, execute and complete all sub-selections. - // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') - if (isObjectType(returnType)) { - return completeObjectValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); - } - - // istanbul ignore next (Not reachable. All possible output types have been considered) - invariant( - false, - 'Cannot complete value of unexpected output type: ' + inspect(returnType), - ); -} - -/** - * 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> { - 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. - const itemType = returnType.ofType; - 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) => - handleRawError(exeContext, itemType, rawError, fieldNodes, itemPath), - ); - } - return completedItem; - } catch (rawError) { - return handleRawError( - exeContext, - itemType, - rawError, - fieldNodes, - itemPath, - ); - } - }); - - 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.`, - 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.`, - fieldNodes, - ); - } - - if (!isObjectType(runtimeType)) { - throw new GraphQLError( - `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, - fieldNodes, - ); - } - - if (!exeContext.schema.isSubType(returnType, runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, - fieldNodes, - ); - } - - return runtimeType; -} - -/** - * Complete an Object value by executing all sub-selections. - */ -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, - ); - }); - } - - if (!isTypeOf) { - throw invalidReturnTypeError(returnType, result, fieldNodes); - } - } - - 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)}.`, - fieldNodes, - ); -} - -/** - * 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; - } - }; - -/** - * This method looks up the field on the given type definition. - * It has special casing for the three introspection fields, - * __schema, __type and __typename. __typename is special because - * it can always be queried as a field, even in situations where no - * other fields are allowed, like on a Union. __schema and __type - * could get automatically added to the query type, but that would - * require mutating type definitions, which would cause issues. - * - * @internal - */ -export function getFieldDef( - schema: GraphQLSchema, - parentType: GraphQLObjectType, - fieldNode: FieldNode, -): Maybe> { - const fieldName = fieldNode.name.value; - - if ( - fieldName === SchemaMetaFieldDef.name && - schema.getQueryType() === parentType - ) { - return SchemaMetaFieldDef; - } else if ( - fieldName === TypeMetaFieldDef.name && - schema.getQueryType() === parentType - ) { - return TypeMetaFieldDef; - } else if (fieldName === TypeNameMetaFieldDef.name) { - return TypeNameMetaFieldDef; - } - return parentType.getFields()[fieldName]; -} diff --git a/src/execution/executor.ts b/src/execution/executor.ts new file mode 100644 index 0000000000..112d0ab3db --- /dev/null +++ b/src/execution/executor.ts @@ -0,0 +1,1131 @@ +import type { Path } from '../jsutils/Path'; +import type { ObjMap } from '../jsutils/ObjMap'; +import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; +import type { Maybe } from '../jsutils/Maybe'; +import { inspect } from '../jsutils/inspect'; +import { memoize2 } from '../jsutils/memoize2'; +import { invariant } from '../jsutils/invariant'; +import { devAssert } from '../jsutils/devAssert'; +import { isPromise } from '../jsutils/isPromise'; +import { isObjectLike } from '../jsutils/isObjectLike'; +import { promiseReduce } from '../jsutils/promiseReduce'; +import { promiseForObject } from '../jsutils/promiseForObject'; +import { addPath, pathToArray } from '../jsutils/Path'; +import { isIterableObject } from '../jsutils/isIterableObject'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; + +import { GraphQLAggregateError } from '../error/GraphQLAggregateError'; +import { GraphQLError } from '../error/GraphQLError'; +import { locatedError } from '../error/locatedError'; + +import type { + DocumentNode, + OperationDefinitionNode, + FieldNode, + FragmentDefinitionNode, + ASTNode, +} from '../language/ast'; +import { Kind } from '../language/kinds'; + +import type { GraphQLSchema } from '../type/schema'; +import type { + GraphQLObjectType, + GraphQLOutputType, + GraphQLLeafType, + GraphQLAbstractType, + GraphQLField, + GraphQLFieldResolver, + GraphQLResolveInfo, + GraphQLTypeResolver, + GraphQLList, +} from '../type/definition'; +import { assertValidSchema } from '../type/validate'; +import { + SchemaMetaFieldDef, + TypeMetaFieldDef, + TypeNameMetaFieldDef, +} from '../type/introspection'; +import { + isObjectType, + isAbstractType, + isLeafType, + isListType, + isNonNullType, +} from '../type/definition'; + +import { getOperationRootType } from '../utilities/getOperationRootType'; + +import type { ExecutionResult } from './execute'; +import { getVariableValues, getArgumentValues } from './values'; +import { + collectFields, + collectSubfields as _collectSubfields, +} from './collectFields'; +import { mapAsyncIterator } from './mapAsyncIterator'; + +export interface ExecutorArgs { + schema: GraphQLSchema; + document: DocumentNode; + rootValue?: unknown; + contextValue?: unknown; + variableValues?: Maybe<{ readonly [variable: string]: unknown }>; + operationName?: Maybe; + fieldResolver?: Maybe>; + typeResolver?: Maybe>; + subscribeFieldResolver?: Maybe>; +} + +/** + * 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: Maybe>; + errors: Array; +} + +/** + * Executor class responsible for implementing the Execution section of the GraphQL spec. + * + * This class is exported only to assist people in implementing their own executors + * without duplicating too much code and should be used only as last resort for cases + * such as experimental syntax or if certain features could not be contributed upstream. + * + * It is still part of the internal API and is versioned, so any changes to it are never + * considered breaking changes. If you still need to support multiple versions of the + * library, please use the `versionInfo` variable for version detection. + * + * @internal + */ +export class Executor { + /** + * 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 = memoize2( + (returnType: GraphQLObjectType, fieldNodes: ReadonlyArray) => { + const { _schema, _fragments, _variableValues } = this; + return _collectSubfields( + _schema, + _fragments, + _variableValues, + returnType, + fieldNodes, + ); + }, + ); + + protected _schema: GraphQLSchema; + protected _fragments: ObjMap; + protected _rootValue: unknown; + protected _contextValue: unknown; + protected _operation: OperationDefinitionNode; + protected _variableValues: { [variable: string]: unknown }; + protected _fieldResolver: GraphQLFieldResolver; + protected _typeResolver: GraphQLTypeResolver; + protected _subscribeFieldResolver: Maybe>; + protected _errors: Array; + + constructor(argsOrExecutionContext: ExecutorArgs | ExecutionContext) { + const executionContext = + 'fragments' in argsOrExecutionContext + ? argsOrExecutionContext + : this.buildExecutionContext(argsOrExecutionContext); + + const { + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues, + fieldResolver, + typeResolver, + subscribeFieldResolver, + errors, + } = executionContext; + + this._schema = schema; + this._fragments = fragments; + this._rootValue = rootValue; + this._contextValue = contextValue; + this._operation = operation; + this._variableValues = variableValues; + this._fieldResolver = fieldResolver; + this._typeResolver = typeResolver; + this._subscribeFieldResolver = subscribeFieldResolver; + this._errors = errors; + } + + /** + * Implements the "Executing operations" section of the spec for queries and + * mutations. + */ + executeQueryOrMutation(): PromiseOrValue { + const data = this.executeQueryOrMutationRootFields(); + + if (isPromise(data)) { + return data.then((resolved) => this.buildResponse(resolved)); + } + + return this.buildResponse(data); + } + + /** + * 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): ExecutionResult { + return this._errors.length === 0 + ? { data } + : { errors: this._errors, data }; + } + + /** + * Essential assertions before executing to provide developer feedback for + * improper use of the GraphQL library. + * + * @internal + */ + assertValidArguments( + schema: GraphQLSchema, + document: DocumentNode, + rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>, + ): void { + devAssert(document, 'Must provide document.'); + + // If the schema used for execution is invalid, throw an error. + assertValidSchema(schema); + + // Variables, if provided, must be an object. + devAssert( + rawVariableValues == null || isObjectLike(rawVariableValues), + 'Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.', + ); + } + + /** + * 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. + * + * @internal + */ + buildExecutionContext(args: ExecutorArgs): ExecutionContext { + const { + schema, + document, + rootValue, + contextValue, + variableValues: rawVariableValues, + operationName, + fieldResolver, + typeResolver, + subscribeFieldResolver, + } = args; + + // If arguments are missing or incorrect, throw an error. + this.assertValidArguments(schema, document, rawVariableValues); + + 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) { + throw new GraphQLAggregateError([ + 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; + } + } + + if (!operation) { + if (operationName != null) { + throw new GraphQLAggregateError([ + new GraphQLError(`Unknown operation named "${operationName}".`), + ]); + } + throw new GraphQLAggregateError([ + new GraphQLError('Must provide an operation.'), + ]); + } + + // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') + const variableDefinitions = operation.variableDefinitions ?? []; + + const coercedVariableValues = getVariableValues( + schema, + variableDefinitions, + rawVariableValues ?? {}, + { maxErrors: 50 }, + ); + + if (coercedVariableValues.errors) { + throw new GraphQLAggregateError(coercedVariableValues.errors); + } + + return { + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues: coercedVariableValues.coerced, + fieldResolver: fieldResolver ?? defaultFieldResolver, + typeResolver: typeResolver ?? defaultTypeResolver, + subscribeFieldResolver, + errors: [], + }; + } + + /** + * Return the data (or 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 + * returned value or resolved Promise. + * */ + executeQueryOrMutationRootFields(): PromiseOrValue | null> { + const { _schema, _fragments, _rootValue, _operation, _variableValues } = + this; + const type = getOperationRootType(_schema, _operation); + const fields = collectFields( + _schema, + _fragments, + _variableValues, + type, + _operation.selectionSet, + ); + + const path = undefined; + + // 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 = + _operation.operation === 'mutation' + ? this.executeFieldsSerially(type, _rootValue, path, fields) + : this.executeFields(type, _rootValue, path, fields); + if (isPromise(result)) { + return result.then(undefined, (error) => { + this._errors.push(error); + return Promise.resolve(null); + }); + } + return result; + } catch (error) { + this._errors.push(error); + return null; + } + } + + /** + * Implements the "Executing selection sets" section of the spec + * for fields that must be executed serially. + */ + executeFieldsSerially( + 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( + 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( + 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( + 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 field" 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. + */ + executeField( + parentType: GraphQLObjectType, + source: unknown, + fieldNodes: ReadonlyArray, + path: Path, + ): PromiseOrValue { + const fieldDef = this.getFieldDef(this._schema, parentType, fieldNodes[0]); + if (!fieldDef) { + return; + } + + const returnType = fieldDef.type; + const resolveFn = fieldDef.resolve ?? this._fieldResolver; + + const info = this.buildResolveInfo(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 function 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(returnType, fieldNodes, info, path, resolved), + ); + } else { + completed = this.completeValue( + 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) => + this.handleRawError(returnType, rawError, fieldNodes, path), + ); + } + return completed; + } catch (rawError) { + return this.handleRawError(returnType, rawError, fieldNodes, path); + } + } + + /** + * @internal + */ + buildResolveInfo( + fieldDef: GraphQLField, + fieldNodes: ReadonlyArray, + parentType: GraphQLObjectType, + path: Path, + ): GraphQLResolveInfo { + const { _schema, _fragments, _rootValue, _operation, _variableValues } = + this; + + // 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: _schema, + fragments: _fragments, + rootValue: _rootValue, + operation: _operation, + variableValues: _variableValues, + }; + } + + handleRawError( + returnType: GraphQLOutputType, + rawError: unknown, + fieldNodes: ReadonlyArray, + path?: Maybe>, + ): null { + const pathAsArray = pathToArray(path); + const error = + rawError instanceof GraphQLAggregateError + ? new GraphQLAggregateError( + rawError.errors.map((subError) => + locatedError(subError, fieldNodes, pathAsArray), + ), + rawError.message, + ) + : locatedError(rawError, fieldNodes, pathAsArray); + + // 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. + if (error instanceof GraphQLAggregateError) { + this._errors.push(...error.errors); + return null; + } + + this._errors.push(error); + return null; + } + + /** + * Implements the instructions for completeValue as defined in the + * "Field entries" 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( + 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( + 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(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( + returnType, + fieldNodes, + info, + path, + result, + ); + } + + // If field type is Object, execute and complete all sub-selections. + // istanbul ignore else (See: 'https://github.com/graphql/graphql-js/issues/2618') + if (isObjectType(returnType)) { + return this.completeObjectValue( + returnType, + fieldNodes, + info, + path, + result, + ); + } + + // istanbul ignore next (Not reachable. All possible output types have been considered) + invariant( + false, + 'Cannot complete value of unexpected output type: ' + inspect(returnType), + ); + } + + /** + * Complete a list value by completing each item in the list with the + * inner type + */ + completeListValue( + returnType: GraphQLList, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + 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. + const itemType = returnType.ofType; + 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(itemType, fieldNodes, info, itemPath, resolved), + ); + } else { + completedItem = this.completeValue( + 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) => + this.handleRawError(itemType, rawError, fieldNodes, itemPath), + ); + } + return completedItem; + } catch (rawError) { + return this.handleRawError(itemType, rawError, fieldNodes, itemPath); + } + }); + + 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( + returnType: GraphQLAbstractType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + const resolveTypeFn = returnType.resolveType ?? this._typeResolver; + const contextValue = this._contextValue; + const runtimeType = resolveTypeFn(result, contextValue, info, returnType); + + if (isPromise(runtimeType)) { + return runtimeType.then((resolvedRuntimeType) => + this.completeObjectValue( + this.ensureValidRuntimeType( + resolvedRuntimeType, + returnType, + fieldNodes, + info, + result, + ), + fieldNodes, + info, + path, + result, + ), + ); + } + + return this.completeObjectValue( + 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.`, + 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._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.`, + fieldNodes, + ); + } + + if (!isObjectType(runtimeType)) { + throw new GraphQLError( + `Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, + fieldNodes, + ); + } + + if (!this._schema.isSubType(returnType, runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, + fieldNodes, + ); + } + + return runtimeType; + } + + /** + * Complete an Object value by executing all sub-selections. + */ + completeObjectValue( + returnType: GraphQLObjectType, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, + ): PromiseOrValue> { + // Collect sub-fields to execute to complete this value. + const subFieldNodes = this.collectSubfields(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(returnType, result, path, subFieldNodes); + }); + } + + if (!isTypeOf) { + throw this.invalidReturnTypeError(returnType, result, fieldNodes); + } + } + + return this.executeFields(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, + )}.`, + fieldNodes, + ); + } + + /** + * This method looks up the field on the given type definition. + * It has special casing for the three introspection fields, + * __schema, __type and __typename. __typename is special because + * it can always be queried as a field, even in situations where no + * other fields are allowed, like on a Union. __schema and __type + * could get automatically added to the query type, but that would + * require mutating type definitions, which would cause issues. + * + * @internal + */ + getFieldDef( + schema: GraphQLSchema, + parentType: GraphQLObjectType, + fieldNode: FieldNode, + ): Maybe> { + const fieldName = fieldNode.name.value; + + if ( + fieldName === SchemaMetaFieldDef.name && + schema.getQueryType() === parentType + ) { + return SchemaMetaFieldDef; + } else if ( + fieldName === TypeMetaFieldDef.name && + schema.getQueryType() === parentType + ) { + return TypeMetaFieldDef; + } else if (fieldName === TypeNameMetaFieldDef.name) { + return TypeNameMetaFieldDef; + } + return parentType.getFields()[fieldName]; + } + + /** + * Implements the "Executing operations" section of the spec for subscriptions + */ + async executeSubscription(): Promise< + AsyncGenerator | ExecutionResult + > { + const resultOrStream = await this.createSourceEventStream(); + + 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 and with + // an empty set of errors. + // 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. + const mapSourceToResponse = (payload: unknown) => { + const { + _schema, + _fragments, + _contextValue, + _operation, + _variableValues, + _fieldResolver, + _typeResolver, + _subscribeFieldResolver, + } = this; + + const executor = new Executor({ + schema: _schema, + fragments: _fragments, + rootValue: payload, + contextValue: _contextValue, + operation: _operation, + variableValues: _variableValues, + fieldResolver: _fieldResolver, + typeResolver: _typeResolver, + subscribeFieldResolver: _subscribeFieldResolver, + errors: [], + }); + + return executor.executeQueryOrMutation(); + }; + + // Map every source value to a ExecutionResult value as described above. + return mapAsyncIterator(resultOrStream, mapSourceToResponse); + } + + /** + * 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. + */ + async createSourceEventStream(): Promise< + AsyncIterable | ExecutionResult + > { + const eventStream = await this.executeSubscriptionRootField(); + + if (this._errors.length !== 0) { + return { errors: this._errors }; + } + + // Assert field returned an event stream, otherwise yield an error. + if (!isAsyncIterable(eventStream)) { + throw new Error( + 'Subscription field must return Async Iterable. ' + + `Received: ${inspect(eventStream)}.`, + ); + } + + return eventStream; + } + + async executeSubscriptionRootField(): Promise { + const { _schema, _fragments, _operation, _variableValues, _rootValue } = + this; + const type = getOperationRootType(_schema, _operation); + const fields = collectFields( + _schema, + _fragments, + _variableValues, + type, + _operation.selectionSet, + ); + const [responseName, fieldNodes] = [...fields.entries()][0]; + const fieldDef = this.getFieldDef(_schema, type, fieldNodes[0]); + + if (!fieldDef) { + const fieldName = fieldNodes[0].name.value; + this._errors.push( + new GraphQLError( + `The subscription field "${fieldName}" is not defined.`, + fieldNodes, + ), + ); + return null; + } + + const path = addPath(undefined, responseName, type.name); + const info = this.buildResolveInfo(fieldDef, fieldNodes, type, 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 = this._contextValue; + + // Call the `subscribe()` resolver or the default resolver to produce an + // AsyncIterable yielding raw payloads. + const resolveFn = fieldDef.subscribe ?? this._fieldResolver; + const eventStream = await resolveFn(_rootValue, args, contextValue, info); + + if (eventStream instanceof Error) { + throw eventStream; + } + return eventStream; + } catch (rawError) { + return this.handleRawError(fieldDef.type, rawError, fieldNodes, path); + } + } +} + +/** + * 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/index.ts b/src/execution/index.ts index 5ae0706ec9..c36a5cfc5f 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -1,11 +1,13 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; +export type { ExecutorArgs, ExecutionContext } from './executor'; export { - execute, - executeSync, + Executor, defaultFieldResolver, defaultTypeResolver, -} from './execute'; +} from './executor'; + +export { execute, executeSync } from './execute'; export type { ExecutionArgs, @@ -14,3 +16,7 @@ export type { } from './execute'; export { getDirectiveValues } from './values'; + +export { subscribe } from './subscribe'; + +export type { SubscriptionArgs } from './subscribe'; diff --git a/src/subscription/mapAsyncIterator.ts b/src/execution/mapAsyncIterator.ts similarity index 100% rename from src/subscription/mapAsyncIterator.ts rename to src/execution/mapAsyncIterator.ts diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts new file mode 100644 index 0000000000..29097b0b06 --- /dev/null +++ b/src/execution/subscribe.ts @@ -0,0 +1,67 @@ +import type { Maybe } from '../jsutils/Maybe'; + +import type { DocumentNode } from '../language/ast'; + +import { isAggregateOfGraphQLErrors } from '../error/GraphQLAggregateError'; + +import type { GraphQLSchema } from '../type/schema'; +import type { + GraphQLFieldResolver, + GraphQLTypeResolver, +} from '../type/definition'; + +import { Executor } from './executor'; +import type { ExecutionResult } from './execute'; + +export interface SubscriptionArgs { + schema: GraphQLSchema; + document: DocumentNode; + rootValue?: unknown; + contextValue?: unknown; + variableValues?: Maybe<{ readonly [variable: string]: unknown }>; + operationName?: Maybe; + fieldResolver?: Maybe>; + typeResolver?: Maybe>; + subscribeFieldResolver?: Maybe>; +} + +/** + * Implements the "Subscribe" algorithm described in the GraphQL specification. + * + * Returns a Promise which resolves to either an AsyncIterator (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 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 an AsyncIterator, which + * yields a stream of ExecutionResults representing the response stream. + * + * Accepts either an object with named arguments, or individual arguments. + */ +export async function subscribe( + args: SubscriptionArgs, +): Promise | ExecutionResult> { + // If a valid execution context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + let executor: Executor; + try { + executor = new Executor(args); + } catch (error) { + // Note: if the Executor constructor throws a GraphQLAggregateError, it will be + // of type GraphQLAggregateError, but this is checked explicitly. + if (isAggregateOfGraphQLErrors(error)) { + return { errors: error.errors }; + } + throw error; + } + + return executor.executeSubscription(); +} diff --git a/src/index.ts b/src/index.ts index c09efb0456..db2ffaa054 100644 --- a/src/index.ts +++ b/src/index.ts @@ -303,6 +303,7 @@ export type { /** Execute GraphQL queries. */ export { + Executor, execute, executeSync, defaultFieldResolver, @@ -312,13 +313,15 @@ export { } from './execution/index'; export type { + ExecutorArgs, ExecutionArgs, + ExecutionContext, ExecutionResult, FormattedExecutionResult, } from './execution/index'; -export { subscribe, createSourceEventStream } from './subscription/index'; -export type { SubscriptionArgs } from './subscription/index'; +export { subscribe } from './execution/index'; +export type { SubscriptionArgs } from './execution/index'; /** Validate GraphQL documents. */ export { @@ -370,6 +373,7 @@ export type { ValidationRule } from './validation/index'; /** Create, format, and print GraphQL errors. */ export { + GraphQLAggregateError, GraphQLError, syntaxError, locatedError, diff --git a/src/jsutils/memoize2.ts b/src/jsutils/memoize2.ts new file mode 100644 index 0000000000..d15d400159 --- /dev/null +++ b/src/jsutils/memoize2.ts @@ -0,0 +1,28 @@ +/** + * Memoizes the provided two-argument function. + */ +export function memoize2( + fn: (a1: A1, a2: A2) => R, +): (a1: A1, a2: A2) => R { + let cache0: WeakMap>; + + return function memoized(a1, a2) { + if (cache0 === undefined) { + cache0 = new WeakMap(); + } + + let cache1 = cache0.get(a1); + if (cache1 === undefined) { + cache1 = new WeakMap(); + cache0.set(a1, cache1); + } + + let fnResult = cache1.get(a2); + if (fnResult === undefined) { + fnResult = fn(a1, a2); + cache1.set(a2, fnResult); + } + + return fnResult; + }; +} diff --git a/src/jsutils/memoize3.ts b/src/jsutils/memoize3.ts deleted file mode 100644 index 213cb95d10..0000000000 --- a/src/jsutils/memoize3.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Memoizes the provided three-argument function. - */ -export function memoize3< - A1 extends object, - A2 extends object, - A3 extends object, - R, ->(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R { - let cache0: WeakMap>>; - - return function memoized(a1, a2, a3) { - if (cache0 === undefined) { - cache0 = new WeakMap(); - } - - let cache1 = cache0.get(a1); - if (cache1 === undefined) { - cache1 = new WeakMap(); - cache0.set(a1, cache1); - } - - let cache2 = cache1.get(a2); - if (cache2 === undefined) { - cache2 = new WeakMap(); - cache1.set(a2, cache2); - } - - let fnResult = cache2.get(a3); - if (fnResult === undefined) { - fnResult = fn(a1, a2, a3); - cache2.set(a3, fnResult); - } - - return fnResult; - }; -} diff --git a/src/subscription/README.md b/src/subscription/README.md deleted file mode 100644 index c938354c2b..0000000000 --- a/src/subscription/README.md +++ /dev/null @@ -1,8 +0,0 @@ -## GraphQL Subscription - -The `graphql/subscription` module is responsible for subscribing to updates on specific data. - -```js -import { subscribe, createSourceEventStream } from 'graphql/subscription'; // ES6 -var GraphQLSubscription = require('graphql/subscription'); // CommonJS -``` diff --git a/src/subscription/index.ts b/src/subscription/index.ts deleted file mode 100644 index 899e443b6b..0000000000 --- a/src/subscription/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { subscribe, createSourceEventStream } from './subscribe'; -export type { SubscriptionArgs } from './subscribe'; diff --git a/src/subscription/subscribe.ts b/src/subscription/subscribe.ts deleted file mode 100644 index be6b517e7b..0000000000 --- a/src/subscription/subscribe.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { inspect } from '../jsutils/inspect'; -import { isAsyncIterable } from '../jsutils/isAsyncIterable'; -import { addPath, pathToArray } from '../jsutils/Path'; -import type { Maybe } from '../jsutils/Maybe'; - -import { GraphQLError } from '../error/GraphQLError'; -import { locatedError } from '../error/locatedError'; - -import type { DocumentNode } from '../language/ast'; - -import type { ExecutionResult, ExecutionContext } from '../execution/execute'; -import { collectFields } from '../execution/collectFields'; -import { getArgumentValues } from '../execution/values'; -import { - assertValidExecutionArguments, - buildExecutionContext, - buildResolveInfo, - execute, - getFieldDef, -} from '../execution/execute'; - -import type { GraphQLSchema } from '../type/schema'; -import type { GraphQLFieldResolver } from '../type/definition'; - -import { getOperationRootType } from '../utilities/getOperationRootType'; - -import { mapAsyncIterator } from './mapAsyncIterator'; - -export interface SubscriptionArgs { - schema: GraphQLSchema; - document: DocumentNode; - rootValue?: unknown; - contextValue?: unknown; - variableValues?: Maybe<{ readonly [variable: string]: unknown }>; - operationName?: Maybe; - fieldResolver?: Maybe>; - subscribeFieldResolver?: Maybe>; -} - -/** - * Implements the "Subscribe" algorithm described in the GraphQL specification. - * - * Returns a Promise which resolves to either an AsyncIterator (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 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 an AsyncIterator, which - * yields a stream of ExecutionResults representing the response stream. - * - * Accepts either an object with named arguments, or individual arguments. - */ -export async function subscribe( - args: SubscriptionArgs, -): Promise | ExecutionResult> { - const { - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - subscribeFieldResolver, - } = args; - - const resultOrStream = await createSourceEventStream( - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - subscribeFieldResolver, - ); - - 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. - const mapSourceToResponse = (payload: unknown) => - execute({ - schema, - document, - rootValue: payload, - contextValue, - variableValues, - operationName, - fieldResolver, - }); - - // Map every source value to a ExecutionResult value as described above. - return mapAsyncIterator(resultOrStream, mapSourceToResponse); -} - -/** - * 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. - */ -export async function createSourceEventStream( - schema: GraphQLSchema, - document: DocumentNode, - rootValue?: unknown, - contextValue?: unknown, - variableValues?: Maybe<{ readonly [variable: string]: unknown }>, - operationName?: Maybe, - fieldResolver?: Maybe>, -): Promise | ExecutionResult> { - // If arguments are missing or incorrectly typed, this is an internal - // developer mistake which should throw an early error. - assertValidExecutionArguments(schema, document, variableValues); - - try { - // If a valid context cannot be created due to incorrect arguments, this will throw an error. - const exeContext = buildExecutionContext( - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - ); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - const eventStream = await executeSubscription(exeContext); - - // Assert field returned an event stream, otherwise yield an error. - if (!isAsyncIterable(eventStream)) { - throw new Error( - 'Subscription field must return Async Iterable. ' + - `Received: ${inspect(eventStream)}.`, - ); - } - - return eventStream; - } catch (error) { - // If it GraphQLError, report it as an ExecutionResult, containing only errors and no data. - // Otherwise treat the error as a system-class error and re-throw it. - if (error instanceof GraphQLError) { - return { errors: [error] }; - } - throw error; - } -} - -async function executeSubscription( - exeContext: ExecutionContext, -): Promise { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; - const type = getOperationRootType(schema, operation); - const fields = collectFields( - schema, - fragments, - variableValues, - type, - operation.selectionSet, - ); - const [responseName, fieldNodes] = [...fields.entries()][0]; - const fieldDef = getFieldDef(schema, type, fieldNodes[0]); - - if (!fieldDef) { - const fieldName = fieldNodes[0].name.value; - throw new GraphQLError( - `The subscription field "${fieldName}" is not defined.`, - fieldNodes, - ); - } - - const path = addPath(undefined, responseName, type.name); - const info = buildResolveInfo(exeContext, fieldDef, fieldNodes, type, 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.fieldResolver; - const eventStream = await resolveFn(rootValue, args, contextValue, info); - - if (eventStream instanceof Error) { - throw eventStream; - } - return eventStream; - } catch (error) { - throw locatedError(error, fieldNodes, pathToArray(path)); - } -}