diff --git a/integrationTests/ts/basic-test.ts b/integrationTests/ts/basic-test.ts index a28bd840e7..bfbca75c3d 100644 --- a/integrationTests/ts/basic-test.ts +++ b/integrationTests/ts/basic-test.ts @@ -23,12 +23,13 @@ const queryType: GraphQLObjectType = new GraphQLObjectType({ const schema: GraphQLSchema = new GraphQLSchema({ query: queryType }); -const result: ExecutionResult = graphqlSync({ - schema, - source: ` +const result: ExecutionResult | AsyncGenerator = + graphqlSync({ + schema, + source: ` query helloWho($who: String){ test(who: $who) } `, - variableValues: { who: 'Dolly' }, -}); + variableValues: { who: 'Dolly' }, + }); diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index d637787c4a..840d246f64 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -1,6 +1,8 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; + import { graphqlSync } from '../graphql'; import { StarWarsSchema } from './starWarsSchema'; @@ -8,6 +10,7 @@ import { StarWarsSchema } from './starWarsSchema'; function queryStarWars(source: string) { const result = graphqlSync({ schema: StarWarsSchema, source }); expect(Object.keys(result)).to.deep.equal(['data']); + assert(!isAsyncIterable(result)); return result.data; } diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 60b203dc05..173906bf64 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -4,6 +4,7 @@ import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; import { inspect } from '../../jsutils/inspect'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; import { Kind } from '../../language/kinds'; import { parse } from '../../language/parser'; @@ -833,7 +834,7 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { c: 'd' } }); }); - it('uses the subscription schema for subscriptions', () => { + it('uses the subscription schema for subscriptions', async () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Q', @@ -852,11 +853,22 @@ describe('Execute: Handles basic execution tasks', () => { query Q { a } subscription S { a } `); - const rootValue = { a: 'b', c: 'd' }; + const rootValue = { + // eslint-disable-next-line @typescript-eslint/require-await + async *a() { + yield { a: 'b' }; /* c8 ignore start */ + } /* c8 ignore stop */, + c: 'd', + }; const operationName = 'S'; const result = executeSync({ schema, document, rootValue, operationName }); - expect(result).to.deep.equal({ data: { a: 'b' } }); + + assert(isAsyncIterable(result)); + expect(await result.next()).to.deep.equal({ + value: { data: { a: 'b' } }, + done: false, + }); }); it('resolves to an error if schema does not support operation', () => { @@ -895,7 +907,6 @@ describe('Execute: Handles basic execution tasks', () => { expectJSON( executeSync({ schema, document, operationName: 'S' }), ).toDeepEqual({ - data: null, errors: [ { message: diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts index 3fdd77ab56..d6c4ba8408 100644 --- a/src/execution/__tests__/lists-test.ts +++ b/src/execution/__tests__/lists-test.ts @@ -85,7 +85,9 @@ describe('Execute: Accepts async iterables as list value', () => { function completeObjectList( resolve: GraphQLFieldResolver<{ index: number }, unknown>, - ): PromiseOrValue { + ): PromiseOrValue< + ExecutionResult | AsyncGenerator + > { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index 427f2a64d6..ce3d0dcef5 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -1,8 +1,11 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; +import type { PromiseOrValue } from '../../jsutils/PromiseOrValue'; + import { parse } from '../../language/parser'; import { GraphQLNonNull, GraphQLObjectType } from '../../type/definition'; @@ -109,7 +112,9 @@ const schema = buildSchema(` function executeQuery( query: string, rootValue: unknown, -): ExecutionResult | Promise { +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { return execute({ schema, document: parse(query), rootValue }); } @@ -132,6 +137,7 @@ async function executeSyncAndAsync(query: string, rootValue: unknown) { rootValue, }); + assert(!isAsyncIterable(syncResult)); expectJSON(asyncResult).toDeepEqual(patchData(syncResult)); return syncResult; } diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 5f256ca868..5adad73503 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -14,8 +14,8 @@ import { GraphQLList, GraphQLObjectType } from '../../type/definition'; import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; -import type { ExecutionArgs, ExecutionResult } from '../execute'; -import { createSourceEventStream, subscribe } from '../execute'; +import type { ExecutionResult } from '../execute'; +import { execute } from '../execute'; import { SimplePubSub } from './simplePubSub'; @@ -122,7 +122,7 @@ function createSubscription(pubsub: SimplePubSub) { }), }; - return subscribe({ schema: emailSchema, document, rootValue: data }); + return execute({ schema: emailSchema, document, rootValue: data }); } // TODO: consider adding this method to testUtils (with tests) @@ -150,24 +150,6 @@ function expectPromise(maybePromise: unknown) { }; } -// TODO: consider adding this method to testUtils (with tests) -function expectEqualPromisesOrValues( - value1: PromiseOrValue, - value2: PromiseOrValue, -): PromiseOrValue { - if (isPromise(value1)) { - assert(isPromise(value2)); - return Promise.all([value1, value2]).then((resolved) => { - expectJSON(resolved[1]).toDeepEqual(resolved[0]); - return resolved[0]; - }); - } - - assert(!isPromise(value2)); - expectJSON(value2).toDeepEqual(value1); - return value1; -} - const DummyQueryType = new GraphQLObjectType({ name: 'Query', fields: { @@ -189,16 +171,7 @@ function subscribeWithBadFn( }); const document = parse('subscription { foo }'); - return subscribeWithBadArgs({ schema, document }); -} - -function subscribeWithBadArgs( - args: ExecutionArgs, -): PromiseOrValue> { - return expectEqualPromisesOrValues( - subscribe(args), - createSourceEventStream(args), - ); + return execute({ schema, document }); } /* eslint-disable @typescript-eslint/require-await */ @@ -220,7 +193,7 @@ describe('Subscription Initialization Phase', () => { yield { foo: 'FooValue' }; } - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), rootValue: { foo: fooGenerator }, @@ -256,7 +229,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), }); @@ -294,7 +267,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const promise = subscribe({ + const promise = execute({ schema, document: parse('subscription { foo }'), }); @@ -329,7 +302,7 @@ describe('Subscription Initialization Phase', () => { yield { foo: 'FooValue' }; } - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo }'), rootValue: { customFoo: fooGenerator }, @@ -379,7 +352,7 @@ describe('Subscription Initialization Phase', () => { }), }); - const subscription = subscribe({ + const subscription = execute({ schema, document: parse('subscription { foo bar }'), }); @@ -400,7 +373,7 @@ describe('Subscription Initialization Phase', () => { const schema = new GraphQLSchema({ query: DummyQueryType }); const document = parse('subscription { unknownField }'); - const result = subscribeWithBadArgs({ schema, document }); + const result = execute({ schema, document }); expectJSON(result).toDeepEqual({ errors: [ { @@ -424,7 +397,7 @@ describe('Subscription Initialization Phase', () => { }); const document = parse('subscription { unknownField }'); - const result = subscribeWithBadArgs({ schema, document }); + const result = execute({ schema, document }); expectJSON(result).toDeepEqual({ errors: [ { @@ -447,7 +420,7 @@ describe('Subscription Initialization Phase', () => { }); // @ts-expect-error - expect(() => subscribeWithBadArgs({ schema, document: {} })).to.throw(); + expect(() => execute({ schema, document: {} })).to.throw(); }); it('throws an error if subscribe does not return an iterator', async () => { @@ -530,9 +503,9 @@ describe('Subscription Initialization Phase', () => { } `); - // If we receive variables that cannot be coerced correctly, subscribe() will + // If we receive variables that cannot be coerced correctly, execute() will // resolve to an ExecutionResult that contains an informative error description. - const result = subscribeWithBadArgs({ schema, document, variableValues }); + const result = execute({ schema, document, variableValues }); expectJSON(result).toDeepEqual({ errors: [ { @@ -945,7 +918,7 @@ describe('Subscription Publish Phase', () => { }); const document = parse('subscription { newMessage }'); - const subscription = subscribe({ schema, document }); + const subscription = execute({ schema, document }); assert(isAsyncIterable(subscription)); expect(await subscription.next()).to.deep.equal({ @@ -1006,7 +979,7 @@ describe('Subscription Publish Phase', () => { }); const document = parse('subscription { newMessage }'); - const subscription = subscribe({ schema, document }); + const subscription = execute({ schema, document }); assert(isAsyncIterable(subscription)); expect(await subscription.next()).to.deep.equal({ diff --git a/src/execution/execute.ts b/src/execution/execute.ts index cc3cbb9cbf..7ad93ed869 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -22,6 +22,7 @@ import type { FieldNode, FragmentDefinitionNode, OperationDefinitionNode, + VariableDefinitionNode, } from '../language/ast'; import { OperationTypeNode } from '../language/ast'; import { Kind } from '../language/kinds'; @@ -52,6 +53,7 @@ import { collectSubfields as _collectSubfields, } from './collectFields'; import { mapAsyncIterator } from './mapAsyncIterator'; +import type { CoercedVariableValues } from './values'; import { getArgumentValues, getVariableValues } from './values'; /* eslint-disable max-params */ @@ -65,14 +67,14 @@ import { getArgumentValues, getVariableValues } from './values'; */ const collectSubfields = memoize3( ( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, returnType: GraphQLObjectType, fieldNodes: ReadonlyArray, ) => _collectSubfields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, + exeInfo.schema, + exeInfo.fragments, + exeInfo.variableValues, returnType, fieldNodes, ), @@ -101,19 +103,38 @@ const collectSubfields = memoize3( /** * 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 + * Namely, the fragments defined in the query document and the + * original operation. + * + * This interface is provided separately in the expectation that + * advanced clients may cache this information separately. */ -export interface ExecutionContext { - schema: GraphQLSchema; +export interface DocumentInfo { fragments: ObjMap; + operation: OperationDefinitionNode; +} + +/** + * Data that must be available at all points during query execution. + */ +export interface ExecutionInfo extends DocumentInfo { + schema: GraphQLSchema; rootValue: unknown; contextValue: unknown; - operation: OperationDefinitionNode; variableValues: { [variable: string]: unknown }; fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; + subscriptionEventExecutor: ( + exeInfo: ExecutionInfo, + payload: unknown, + ) => PromiseOrValue; +} + +/** + * Internal data representing the current state of execution. + */ +export interface ExecutionContext { errors: Array; } @@ -152,8 +173,23 @@ export interface ExecutionArgs { fieldResolver?: Maybe>; typeResolver?: Maybe>; subscribeFieldResolver?: Maybe>; + subscriptionEventExecutor?: Maybe< + ( + exeInfo: ExecutionInfo, + payload: unknown, + ) => PromiseOrValue + >; } +type FieldsExecutor = ( + exeInfo: ExecutionInfo, + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: unknown, + path: Path | undefined, + fields: Map>, +) => PromiseOrValue>; + /** * Implements the "Executing requests" section of the GraphQL specification. * @@ -164,21 +200,60 @@ export interface ExecutionArgs { * If the arguments to this function do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. */ -export function execute(args: ExecutionArgs): PromiseOrValue { - // If a valid execution context cannot be created due to incorrect arguments, +export function execute( + args: ExecutionArgs, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { + // If a valid execution info object cannot be created due to incorrect arguments, // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); + const exeInfo = buildExecutionInfo(args); // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; + if (!('schema' in exeInfo)) { + return { errors: exeInfo }; } - return executeImpl(exeContext); + return executeOperation(exeInfo); } -function executeImpl( +/** + * Implements the "Executing operations" section of the spec. + */ +export function executeOperation( + exeInfo: ExecutionInfo, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { + const operationType = exeInfo.operation.operation; + + if (operationType === OperationTypeNode.QUERY) { + return executeQuery(exeInfo); + } + + if (operationType === OperationTypeNode.MUTATION) { + return executeMutation(exeInfo); + } + + return executeSubscription(exeInfo); +} + +export function executeQuery( + exeInfo: ExecutionInfo, +): PromiseOrValue { + return executeQueryOrMutation(exeInfo, { errors: [] }, executeFields); +} + +export function executeMutation( + exeInfo: ExecutionInfo, +): PromiseOrValue { + return executeQueryOrMutation(exeInfo, { errors: [] }, executeFieldsSerially); +} + +function executeQueryOrMutation( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, + fieldsExecutor: FieldsExecutor, ): PromiseOrValue { // Return a Promise that will eventually resolve to the data described by // The "Response" section of the GraphQL specification. @@ -192,7 +267,11 @@ function executeImpl( // at which point we still log the error and null the parent field, which // in this case is the entire response. try { - const result = executeOperation(exeContext); + const result = executeQueryOrMutationRootFields( + exeInfo, + exeContext, + fieldsExecutor, + ); if (isPromise(result)) { return result.then( (data) => buildResponse(data, exeContext.errors), @@ -214,7 +293,9 @@ function executeImpl( * However, it guarantees to complete synchronously (or throw an error) assuming * that all field resolvers are also synchronous. */ -export function executeSync(args: ExecutionArgs): ExecutionResult { +export function executeSync( + args: ExecutionArgs, +): ExecutionResult | AsyncGenerator { const result = execute(args); // Assert that the execution was synchronous. @@ -237,7 +318,7 @@ function buildResponse( } /** - * Constructs a ExecutionContext object from the arguments passed to + * Constructs a ExecutionInfo 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. @@ -245,9 +326,9 @@ function buildResponse( * TODO: consider no longer exporting this function * @internal */ -export function buildExecutionContext( +export function buildExecutionInfo( args: ExecutionArgs, -): ReadonlyArray | ExecutionContext { +): ReadonlyArray | ExecutionInfo { const { schema, document, @@ -258,11 +339,49 @@ export function buildExecutionContext( fieldResolver, typeResolver, subscribeFieldResolver, + subscriptionEventExecutor, } = args; // If the schema used for execution is invalid, throw an error. assertValidSchema(schema); + const documentInfo = buildDocumentInfo(document, operationName); + + if (!('fragments' in documentInfo)) { + return documentInfo; + } + + const { fragments, operation } = documentInfo; + + const coercedVariableValues = coerceVariableValues( + schema, + operation.variableDefinitions, + rawVariableValues, + ); + + if (coercedVariableValues.errors) { + return coercedVariableValues.errors; + } + + return { + schema, + fragments, + rootValue, + contextValue, + operation, + variableValues: coercedVariableValues.coerced, + fieldResolver: fieldResolver ?? defaultFieldResolver, + typeResolver: typeResolver ?? defaultTypeResolver, + subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, + subscriptionEventExecutor: + subscriptionEventExecutor ?? defaultSubscriptionEventExecutor, + }; +} + +function buildDocumentInfo( + document: DocumentNode, + operationName: Maybe, +): ReadonlyArray | DocumentInfo { let operation: OperationDefinitionNode | undefined; const fragments: ObjMap = Object.create(null); for (const definition of document.definitions) { @@ -296,54 +415,30 @@ export function buildExecutionContext( return [new GraphQLError('Must provide an operation.')]; } + return { fragments, operation }; +} + +export function coerceVariableValues( + schema: GraphQLSchema, // FIXME: https://github.com/graphql/graphql-js/issues/2203 /* c8 ignore next */ - const variableDefinitions = operation.variableDefinitions ?? []; - - const coercedVariableValues = getVariableValues( + variableDefinitions: ReadonlyArray = [], + rawVariableValues?: Maybe<{ readonly [variable: string]: unknown }>, +): CoercedVariableValues { + return 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, - subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - errors: [], - }; } -function buildPerEventExecutionContext( - exeContext: ExecutionContext, - payload: unknown, -): ExecutionContext { - return { - ...exeContext, - rootValue: payload, - errors: [], - }; -} - -/** - * Implements the "Executing operations" section of the spec. - */ -function executeOperation( +function executeQueryOrMutationRootFields( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, + fieldsExecutor: FieldsExecutor, ): PromiseOrValue> { - const { operation, schema, fragments, variableValues, rootValue } = - exeContext; + const { operation, schema, fragments, variableValues, rootValue } = exeInfo; const rootType = schema.getRootType(operation.operation); if (rootType == null) { throw new GraphQLError( @@ -361,22 +456,14 @@ function executeOperation( ); const path = undefined; - switch (operation.operation) { - case OperationTypeNode.QUERY: - return executeFields(exeContext, rootType, rootValue, path, rootFields); - case OperationTypeNode.MUTATION: - return executeFieldsSerially( - exeContext, - rootType, - rootValue, - path, - rootFields, - ); - case OperationTypeNode.SUBSCRIPTION: - // TODO: deprecate `subscribe` and move all logic here - // Temporary solution until we finish merging execute and subscribe together - return executeFields(exeContext, rootType, rootValue, path, rootFields); - } + return fieldsExecutor( + exeInfo, + exeContext, + rootType, + rootValue, + path, + rootFields, + ); } /** @@ -384,6 +471,7 @@ function executeOperation( * for fields that must be executed serially. */ function executeFieldsSerially( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: unknown, @@ -395,6 +483,7 @@ function executeFieldsSerially( (results, [responseName, fieldNodes]) => { const fieldPath = addPath(path, responseName, parentType.name); const result = executeField( + exeInfo, exeContext, parentType, sourceValue, @@ -422,6 +511,7 @@ function executeFieldsSerially( * for fields that may be executed in parallel. */ function executeFields( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: unknown, @@ -434,6 +524,7 @@ function executeFields( for (const [responseName, fieldNodes] of fields.entries()) { const fieldPath = addPath(path, responseName, parentType.name); const result = executeField( + exeInfo, exeContext, parentType, sourceValue, @@ -467,6 +558,7 @@ function executeFields( * serialize scalars, or execute the sub-selection-set for objects. */ function executeField( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, parentType: GraphQLObjectType, source: unknown, @@ -474,16 +566,16 @@ function executeField( path: Path, ): PromiseOrValue { const fieldName = fieldNodes[0].name.value; - const fieldDef = exeContext.schema.getField(parentType, fieldName); + const fieldDef = exeInfo.schema.getField(parentType, fieldName); if (!fieldDef) { return; } const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver; + const resolveFn = fieldDef.resolve ?? exeInfo.fieldResolver; const info = buildResolveInfo( - exeContext, + exeInfo, fieldDef, fieldNodes, parentType, @@ -498,23 +590,32 @@ function executeField( const args = getArgumentValues( fieldDef, fieldNodes[0], - exeContext.variableValues, + exeInfo.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 contextValue = exeInfo.contextValue; const result = resolveFn(source, args, contextValue, info); let completed; if (isPromise(result)) { completed = result.then((resolved) => - completeValue(exeContext, returnType, fieldNodes, info, path, resolved), + completeValue( + exeInfo, + exeContext, + returnType, + fieldNodes, + info, + path, + resolved, + ), ); } else { completed = completeValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -544,7 +645,7 @@ function executeField( * @internal */ export function buildResolveInfo( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, fieldDef: GraphQLField, fieldNodes: ReadonlyArray, parentType: GraphQLObjectType, @@ -558,11 +659,11 @@ export function buildResolveInfo( returnType: fieldDef.type, parentType, path, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, + schema: exeInfo.schema, + fragments: exeInfo.fragments, + rootValue: exeInfo.rootValue, + operation: exeInfo.operation, + variableValues: exeInfo.variableValues, }; } @@ -605,6 +706,7 @@ function handleFieldError( * value by executing all sub-selections. */ function completeValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLOutputType, fieldNodes: ReadonlyArray, @@ -621,6 +723,7 @@ function completeValue( // if result is null. if (isNonNullType(returnType)) { const completed = completeValue( + exeInfo, exeContext, returnType.ofType, fieldNodes, @@ -644,6 +747,7 @@ function completeValue( // If field type is List, complete each item in the list with the inner type if (isListType(returnType)) { return completeListValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -663,6 +767,7 @@ function completeValue( // runtime Object type and complete for that type. if (isAbstractType(returnType)) { return completeAbstractValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -675,6 +780,7 @@ function completeValue( // If field type is Object, execute and complete all sub-selections. if (isObjectType(returnType)) { return completeObjectValue( + exeInfo, exeContext, returnType, fieldNodes, @@ -696,6 +802,7 @@ function completeValue( * recursively until all the results are completed. */ async function completeAsyncIteratorValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, itemType: GraphQLOutputType, fieldNodes: ReadonlyArray, @@ -719,6 +826,7 @@ async function completeAsyncIteratorValue( try { // TODO can the error checking logic be consolidated with completeListValue? const completedItem = completeValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -755,6 +863,7 @@ async function completeAsyncIteratorValue( * inner type */ function completeListValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLList, fieldNodes: ReadonlyArray, @@ -768,6 +877,7 @@ function completeListValue( const iterator = result[Symbol.asyncIterator](); return completeAsyncIteratorValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -795,6 +905,7 @@ function completeListValue( if (isPromise(item)) { completedItem = item.then((resolved) => completeValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -805,6 +916,7 @@ function completeListValue( ); } else { completedItem = completeValue( + exeInfo, exeContext, itemType, fieldNodes, @@ -860,6 +972,7 @@ function completeLeafValue( * of that value, then complete the value for that type. */ function completeAbstractValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLAbstractType, fieldNodes: ReadonlyArray, @@ -867,17 +980,18 @@ function completeAbstractValue( path: Path, result: unknown, ): PromiseOrValue> { - const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; - const contextValue = exeContext.contextValue; + const resolveTypeFn = returnType.resolveType ?? exeInfo.typeResolver; + const contextValue = exeInfo.contextValue; const runtimeType = resolveTypeFn(result, contextValue, info, returnType); if (isPromise(runtimeType)) { return runtimeType.then((resolvedRuntimeType) => completeObjectValue( + exeInfo, exeContext, ensureValidRuntimeType( resolvedRuntimeType, - exeContext, + exeInfo, returnType, fieldNodes, info, @@ -892,10 +1006,11 @@ function completeAbstractValue( } return completeObjectValue( + exeInfo, exeContext, ensureValidRuntimeType( runtimeType, - exeContext, + exeInfo, returnType, fieldNodes, info, @@ -910,7 +1025,7 @@ function completeAbstractValue( function ensureValidRuntimeType( runtimeTypeName: unknown, - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, returnType: GraphQLAbstractType, fieldNodes: ReadonlyArray, info: GraphQLResolveInfo, @@ -938,7 +1053,7 @@ function ensureValidRuntimeType( ); } - const runtimeType = exeContext.schema.getType(runtimeTypeName); + const runtimeType = exeInfo.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.`, @@ -953,7 +1068,7 @@ function ensureValidRuntimeType( ); } - if (!exeContext.schema.isSubType(returnType, runtimeType)) { + if (!exeInfo.schema.isSubType(returnType, runtimeType)) { throw new GraphQLError( `Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, { nodes: fieldNodes }, @@ -967,6 +1082,7 @@ function ensureValidRuntimeType( * Complete an Object value by executing all sub-selections. */ function completeObjectValue( + exeInfo: ExecutionInfo, exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldNodes: ReadonlyArray, @@ -975,13 +1091,13 @@ function completeObjectValue( result: unknown, ): PromiseOrValue> { // Collect sub-fields to execute to complete this value. - const subFieldNodes = collectSubfields(exeContext, returnType, fieldNodes); + const subFieldNodes = collectSubfields(exeInfo, 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); + const isTypeOf = returnType.isTypeOf(result, exeInfo.contextValue, info); if (isPromise(isTypeOf)) { return isTypeOf.then((resolvedIsTypeOf) => { @@ -989,6 +1105,7 @@ function completeObjectValue( throw invalidReturnTypeError(returnType, result, fieldNodes); } return executeFields( + exeInfo, exeContext, returnType, result, @@ -1003,7 +1120,14 @@ function completeObjectValue( } } - return executeFields(exeContext, returnType, result, path, subFieldNodes); + return executeFields( + exeInfo, + exeContext, + returnType, + result, + path, + subFieldNodes, + ); } function invalidReturnTypeError( @@ -1099,36 +1223,25 @@ export const defaultFieldResolver: GraphQLFieldResolver = * * 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 function subscribe( - args: ExecutionArgs, +export function executeSubscription( + exeInfo: ExecutionInfo, ): PromiseOrValue< - AsyncGenerator | ExecutionResult + ExecutionResult | AsyncGenerator > { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - const resultOrStream = createSourceEventStreamImpl(exeContext); + const resultOrStream = createSourceEventStream(exeInfo); if (isPromise(resultOrStream)) { return resultOrStream.then((resolvedResultOrStream) => - mapSourceToResponse(exeContext, resolvedResultOrStream), + mapSourceToResponse(exeInfo, resolvedResultOrStream), ); } - return mapSourceToResponse(exeContext, resultOrStream); + return mapSourceToResponse(exeInfo, resultOrStream); } function mapSourceToResponse( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, resultOrStream: ExecutionResult | AsyncIterable, ): PromiseOrValue< AsyncGenerator | ExecutionResult @@ -1144,10 +1257,19 @@ function mapSourceToResponse( // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. return mapAsyncIterator(resultOrStream, (payload: unknown) => - executeImpl(buildPerEventExecutionContext(exeContext, payload)), + exeInfo.subscriptionEventExecutor(exeInfo, payload), ); } +export const defaultSubscriptionEventExecutor = ( + exeInfo: ExecutionInfo, + payload: unknown, +): PromiseOrValue => + executeQuery({ + ...exeInfo, + rootValue: payload, + }); + /** * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. @@ -1177,25 +1299,10 @@ function mapSourceToResponse( * "Supporting Subscriptions at Scale" information in the GraphQL specification. */ export function createSourceEventStream( - args: ExecutionArgs, -): PromiseOrValue | ExecutionResult> { - // If a valid execution context cannot be created due to incorrect arguments, - // a "Response" with only errors is returned. - const exeContext = buildExecutionContext(args); - - // Return early errors if execution context failed. - if (!('schema' in exeContext)) { - return { errors: exeContext }; - } - - return createSourceEventStreamImpl(exeContext); -} - -function createSourceEventStreamImpl( - exeContext: ExecutionContext, + exeInfo: ExecutionInfo, ): PromiseOrValue | ExecutionResult> { try { - const eventStream = executeSubscription(exeContext); + const eventStream = executeSubscriptionRootField(exeInfo); if (isPromise(eventStream)) { return eventStream.then(undefined, (error) => ({ errors: [error] })); } @@ -1206,11 +1313,10 @@ function createSourceEventStreamImpl( } } -function executeSubscription( - exeContext: ExecutionContext, +function executeSubscriptionRootField( + exeInfo: ExecutionInfo, ): PromiseOrValue> { - const { schema, fragments, operation, variableValues, rootValue } = - exeContext; + const { schema, fragments, operation, variableValues, rootValue } = exeInfo; const rootType = schema.getSubscriptionType(); if (rootType == null) { @@ -1239,13 +1345,7 @@ function executeSubscription( } const path = addPath(undefined, responseName, rootType.name); - const info = buildResolveInfo( - exeContext, - fieldDef, - fieldNodes, - rootType, - path, - ); + const info = buildResolveInfo(exeInfo, fieldDef, fieldNodes, rootType, path); try { // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. @@ -1258,11 +1358,11 @@ function executeSubscription( // 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 contextValue = exeInfo.contextValue; // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. - const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver; + const resolveFn = fieldDef.subscribe ?? exeInfo.subscribeFieldResolver; const result = resolveFn(rootValue, args, contextValue, info); if (isPromise(result)) { diff --git a/src/execution/index.ts b/src/execution/index.ts index b27a2c291c..f17e4bc5d7 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -1,12 +1,11 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path'; export { - createSourceEventStream, execute, executeSync, defaultFieldResolver, defaultTypeResolver, - subscribe, + defaultSubscriptionEventExecutor, } from './execute'; export type { diff --git a/src/execution/values.ts b/src/execution/values.ts index 023e028109..7efbdc6b45 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -23,7 +23,7 @@ import { coerceInputValue } from '../utilities/coerceInputValue'; import { typeFromAST } from '../utilities/typeFromAST'; import { valueFromAST } from '../utilities/valueFromAST'; -type CoercedVariableValues = +export type CoercedVariableValues = | { errors: ReadonlyArray; coerced?: never } | { coerced: { [variable: string]: unknown }; errors?: never }; diff --git a/src/graphql.ts b/src/graphql.ts index ffad9123c1..8cc5ff19fa 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -67,7 +67,9 @@ export interface GraphQLArgs { typeResolver?: Maybe>; } -export function graphql(args: GraphQLArgs): Promise { +export function graphql( + args: GraphQLArgs, +): Promise> { // Always return a Promise for a consistent API. return new Promise((resolve) => resolve(graphqlImpl(args))); } @@ -78,7 +80,9 @@ export function graphql(args: GraphQLArgs): Promise { * However, it guarantees to complete synchronously (or throw an error) assuming * that all field resolvers are also synchronous. */ -export function graphqlSync(args: GraphQLArgs): ExecutionResult { +export function graphqlSync( + args: GraphQLArgs, +): ExecutionResult | AsyncGenerator { const result = graphqlImpl(args); // Assert that the execution was synchronous. @@ -89,7 +93,11 @@ export function graphqlSync(args: GraphQLArgs): ExecutionResult { return result; } -function graphqlImpl(args: GraphQLArgs): PromiseOrValue { +function graphqlImpl( + args: GraphQLArgs, +): PromiseOrValue< + ExecutionResult | AsyncGenerator +> { const { schema, source, diff --git a/src/index.ts b/src/index.ts index bce254f808..32a0b25974 100644 --- a/src/index.ts +++ b/src/index.ts @@ -315,12 +315,11 @@ export { executeSync, defaultFieldResolver, defaultTypeResolver, + defaultSubscriptionEventExecutor, responsePathAsArray, getArgumentValues, getVariableValues, getDirectiveValues, - subscribe, - createSourceEventStream, } from './execution/index'; export type { diff --git a/src/type/__tests__/enumType-test.ts b/src/type/__tests__/enumType-test.ts index 35e9f94b00..cf33869e3a 100644 --- a/src/type/__tests__/enumType-test.ts +++ b/src/type/__tests__/enumType-test.ts @@ -1,8 +1,10 @@ -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; + import { introspectionFromSchema } from '../../utilities/introspectionFromSchema'; import { graphqlSync } from '../../graphql'; @@ -104,6 +106,10 @@ const SubscriptionType = new GraphQLObjectType({ subscribeToEnum: { type: ColorType, args: { color: { type: ColorType } }, + // eslint-disable-next-line @typescript-eslint/require-await + async *subscribe(_source, { color }) { + yield { subscribeToEnum: color }; /* c8 ignore start */ + } /* c8 ignore stop */, resolve: (_source, { color }) => color, }, }, @@ -248,13 +254,15 @@ describe('Type System: Enum Values', () => { }); }); - it('accepts enum literals as input arguments to subscriptions', () => { + it('accepts enum literals as input arguments to subscriptions', async () => { const doc = 'subscription ($color: Color!) { subscribeToEnum(color: $color) }'; const result = executeQuery(doc, { color: 'GREEN' }); - expect(result).to.deep.equal({ - data: { subscribeToEnum: 'GREEN' }, + assert(isAsyncIterable(result)); + expect(await result.next()).to.deep.equal({ + value: { data: { subscribeToEnum: 'GREEN' } }, + done: false, }); }); diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 435abc2d7a..e7945c80e5 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -3,6 +3,7 @@ import { describe, it } from 'mocha'; import { dedent } from '../../__testUtils__/dedent'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; import type { Maybe } from '../../jsutils/Maybe'; import type { ASTNode } from '../../language/ast'; @@ -76,6 +77,7 @@ describe('Schema Builder', () => { source: '{ str }', rootValue: { str: 123 }, }); + assert(!isAsyncIterable(result)); expect(result.data).to.deep.equal({ str: '123' }); }); diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 8c043f0e77..cb90567974 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -3,6 +3,8 @@ import { describe, it } from 'mocha'; import { dedent } from '../../__testUtils__/dedent'; +import { isAsyncIterable } from '../../jsutils/isAsyncIterable'; + import { assertEnumType, GraphQLEnumType, @@ -591,6 +593,7 @@ describe('Type System: build schema from introspection', () => { variableValues: { v: 'baz' }, }); + assert(!isAsyncIterable(result)); expect(result.data).to.deep.equal({ foo: 'bar' }); }); diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index 78c1b30244..5daed3fb28 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -1,4 +1,5 @@ import { invariant } from '../jsutils/invariant'; +import { isAsyncIterable } from '../jsutils/isAsyncIterable'; import { parse } from '../language/parser'; @@ -35,6 +36,7 @@ export function introspectionFromSchema( const document = parse(getIntrospectionQuery(optionsWithDefaults)); const result = executeSync({ schema, document }); + invariant(!isAsyncIterable(result)); invariant(result.errors == null && result.data != null); return result.data as any; }