diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts index 95c54479c2..2989e21c3b 100644 --- a/src/execution/__tests__/defer-test.ts +++ b/src/execution/__tests__/defer-test.ts @@ -1,8 +1,6 @@ -import { expect } from 'chai'; import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON.js'; -import { expectPromise } from '../../__testUtils__/expectPromise.js'; import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js'; import type { DocumentNode } from '../../language/ast.js'; @@ -20,7 +18,7 @@ import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, } from '../execute.js'; -import { execute, experimentalExecuteIncrementally } from '../execute.js'; +import { execute } from '../execute.js'; const friendType = new GraphQLObjectType({ fields: { @@ -84,7 +82,7 @@ const query = new GraphQLObjectType({ const schema = new GraphQLSchema({ query }); async function complete(document: DocumentNode) { - const result = await experimentalExecuteIncrementally({ + const result = await execute({ schema, document, rootValue: {}, @@ -656,46 +654,4 @@ describe('Execute: defer directive', () => { }, ]); }); - - it('original execute function throws error if anything is deferred and everything else is sync', () => { - const doc = ` - query Deferred { - ... @defer { hero { id } } - } - `; - expect(() => - execute({ - schema, - document: parse(doc), - rootValue: {}, - }), - ).to.throw( - 'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)', - ); - }); - - it('original execute function resolves to error if anything is deferred and something else is async', async () => { - const doc = ` - query Deferred { - hero { slowField } - ... @defer { hero { id } } - } - `; - expectJSON( - await expectPromise( - execute({ - schema, - document: parse(doc), - rootValue: {}, - }), - ).toResolve(), - ).toDeepEqual({ - errors: [ - { - message: - 'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)', - }, - ], - }); - }); }); diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 628f71d139..cd45b3a219 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -807,7 +807,12 @@ describe('Execute: Handles basic execution tasks', () => { const rootValue = { a: 'b', c: 'd' }; const operationName = 'Q'; - const result = executeSync({ schema, document, rootValue, operationName }); + const result = executeSync({ + schema, + document, + rootValue, + operationName, + }); expect(result).to.deep.equal({ data: { a: 'b' } }); }); diff --git a/src/execution/__tests__/mutations-test.ts b/src/execution/__tests__/mutations-test.ts index fa533c75ea..c1db1e4726 100644 --- a/src/execution/__tests__/mutations-test.ts +++ b/src/execution/__tests__/mutations-test.ts @@ -10,11 +10,7 @@ import { GraphQLObjectType } from '../../type/definition.js'; import { GraphQLInt } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { - execute, - executeSync, - experimentalExecuteIncrementally, -} from '../execute.js'; +import { execute, executeSync } from '../execute.js'; class NumberHolder { theNumber: number; @@ -218,7 +214,7 @@ describe('Execute: Handles mutation execution ordering', () => { `); const rootValue = new Root(6); - const mutationResult = await experimentalExecuteIncrementally({ + const mutationResult = await execute({ schema, document, rootValue, @@ -294,7 +290,7 @@ describe('Execute: Handles mutation execution ordering', () => { `); const rootValue = new Root(6); - const mutationResult = await experimentalExecuteIncrementally({ + const mutationResult = await execute({ schema, document, rootValue, diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index df25d6e855..7d0ee4a0b0 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -20,7 +20,7 @@ import type { InitialIncrementalExecutionResult, SubsequentIncrementalExecutionResult, } from '../execute.js'; -import { experimentalExecuteIncrementally } from '../execute.js'; +import { execute } from '../execute.js'; const friendType = new GraphQLObjectType({ fields: { @@ -83,7 +83,7 @@ const query = new GraphQLObjectType({ const schema = new GraphQLSchema({ query }); async function complete(document: DocumentNode, rootValue: unknown = {}) { - const result = await experimentalExecuteIncrementally({ + const result = await execute({ schema, document, rootValue, @@ -106,7 +106,7 @@ async function completeAsync( numCalls: number, rootValue: unknown = {}, ) { - const result = await experimentalExecuteIncrementally({ + const result = await execute({ schema, document, rootValue, @@ -1187,7 +1187,7 @@ describe('Execute: stream directive', () => { } `); - const executeResult = await experimentalExecuteIncrementally({ + const executeResult = await execute({ schema, document, rootValue: { @@ -1312,7 +1312,7 @@ describe('Execute: stream directive', () => { } } `); - const executeResult = await experimentalExecuteIncrementally({ + const executeResult = await execute({ schema, document, rootValue: { @@ -1405,7 +1405,7 @@ describe('Execute: stream directive', () => { } `); - const executeResult = await experimentalExecuteIncrementally({ + const executeResult = await execute({ schema, document, rootValue: { @@ -1491,7 +1491,7 @@ describe('Execute: stream directive', () => { } `); - const executeResult = await experimentalExecuteIncrementally({ + const executeResult = await execute({ schema, document, rootValue: { @@ -1595,7 +1595,7 @@ describe('Execute: stream directive', () => { } `); - const executeResult = await experimentalExecuteIncrementally({ + const executeResult = await execute({ schema, document, rootValue: { @@ -1649,7 +1649,7 @@ describe('Execute: stream directive', () => { } `); - const executeResult = await experimentalExecuteIncrementally({ + const executeResult = await execute({ schema, document, rootValue: { @@ -1709,7 +1709,7 @@ describe('Execute: stream directive', () => { } `); - const executeResult = await experimentalExecuteIncrementally({ + const executeResult = await execute({ schema, document, rootValue: { diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 0dc2e3140c..846f1f9557 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -21,11 +21,7 @@ import { import { GraphQLSchema } from '../../type/schema.js'; import type { ExecutionArgs, ExecutionResult } from '../execute.js'; -import { - createSourceEventStream, - experimentalSubscribeIncrementally, - subscribe, -} from '../execute.js'; +import { createSourceEventStream, subscribe } from '../execute.js'; import { SimplePubSub } from './simplePubSub.js'; @@ -99,7 +95,6 @@ const emailSchema = new GraphQLSchema({ function createSubscription( pubsub: SimplePubSub, variableValues?: { readonly [variable: string]: unknown }, - originalSubscribe: boolean = false, ) { const document = parse(` subscription ($priority: Int = 0, $shouldDefer: Boolean = false, $asyncResolver: Boolean = false) { @@ -145,7 +140,7 @@ function createSubscription( }), }; - return (originalSubscribe ? subscribe : experimentalSubscribeIncrementally)({ + return subscribe({ schema: emailSchema, document, rootValue: data, @@ -841,75 +836,6 @@ describe('Subscription Publish Phase', () => { }); }); - it('original subscribe function returns errors with @defer', async () => { - const pubsub = new SimplePubSub(); - const subscription = await createSubscription( - pubsub, - { - shouldDefer: true, - }, - true, - ); - assert(isAsyncIterable(subscription)); - // Wait for the next subscription payload. - const payload = subscription.next(); - - // A new email arrives! - expect( - pubsub.emit({ - from: 'yuzhi@graphql.org', - subject: 'Alright', - message: 'Tests are good', - unread: true, - }), - ).to.equal(true); - - const errorPayload = { - done: false, - value: { - errors: [ - { - message: - 'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)', - }, - ], - }, - }; - - // The previously waited on payload now has a value. - expectJSON(await payload).toDeepEqual(errorPayload); - - // Wait for the next payload from @defer - expectJSON(await subscription.next()).toDeepEqual(errorPayload); - - // Another new email arrives, after all incrementally delivered payloads are received. - expect( - pubsub.emit({ - from: 'hyo@graphql.org', - subject: 'Tools', - message: 'I <3 making things', - unread: true, - }), - ).to.equal(true); - - // The next waited on payload will have a value. - expectJSON(await subscription.next()).toDeepEqual(errorPayload); - // The next waited on payload will have a value. - expectJSON(await subscription.next()).toDeepEqual(errorPayload); - - // The client disconnects before the deferred payload is consumed. - expectJSON(await subscription.return()).toDeepEqual({ - done: true, - value: undefined, - }); - - // Awaiting a subscription after closing it results in completed results. - expectJSON(await subscription.next()).toDeepEqual({ - done: true, - value: undefined, - }); - }); - it('produces a payload when there are multiple events', async () => { const pubsub = new SimplePubSub(); const subscription = createSubscription(pubsub); diff --git a/src/execution/__tests__/sync-test.ts b/src/execution/__tests__/sync-test.ts index f5efa4097c..d32474c0e9 100644 --- a/src/execution/__tests__/sync-test.ts +++ b/src/execution/__tests__/sync-test.ts @@ -114,6 +114,16 @@ describe('Execute: synchronously when possible', () => { }).to.throw('GraphQL execution failed to complete synchronously.'); }); + it('return successfully if incremental delivery is enabled but async iterable is not returned', () => { + const doc = 'query Example { syncField }'; + const result = executeSync({ + schema, + document: parse(doc), + rootValue: 'rootValue', + }); + expect(result).to.deep.equal({ data: { syncField: 'rootValue' } }); + }); + it('throws if encountering async iterable execution', () => { const doc = ` query Example { diff --git a/src/execution/execute.ts b/src/execution/execute.ts index c1f025eb19..2cc2325ca7 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -263,8 +263,9 @@ export interface ExecutionArgs { subscribeFieldResolver?: Maybe>; } -const UNEXPECTED_MULTIPLE_PAYLOADS = - 'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)'; +type MaybeIncrementalMap = TMaybeIncremental extends true + ? ExecutionResult | ExperimentalIncrementalExecutionResults + : ExecutionResult; /** * Implements the "Executing requests" section of the GraphQL specification. @@ -276,56 +277,34 @@ const UNEXPECTED_MULTIPLE_PAYLOADS = * If the arguments to this function do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. * - * This function does not support incremental delivery (`@defer` and `@stream`). - * If an operation which would defer or stream data is executed with this - * function, it will throw or resolve to an object containing an error instead. - * Use `experimentalExecuteIncrementally` if you want to support incremental - * delivery. - */ -export function execute(args: ExecutionArgs): PromiseOrValue { - const result = experimentalExecuteIncrementally(args); - if (!isPromise(result)) { - if ('initialResult' in result) { - throw new Error(UNEXPECTED_MULTIPLE_PAYLOADS); - } - return result; - } - - return result.then((incrementalResult) => { - if ('initialResult' in incrementalResult) { - return { - errors: [new GraphQLError(UNEXPECTED_MULTIPLE_PAYLOADS)], - }; - } - return incrementalResult; - }); -} - -/** - * Implements the "Executing requests" section of the GraphQL specification, - * including `@defer` and `@stream` as proposed in - * https://github.com/graphql/graphql-spec/pull/742 + * This function also supports experimental incremental delivery directives + * (`@defer` and `@stream`). To use these directives, they should be added to + * the schema and TS generic parameter TMaybeIncremental should be set to `true` + * (default: false). * - * This function returns a Promise of an ExperimentalIncrementalExecutionResults - * object. This object either consists of a single ExecutionResult, or an - * object containing an `initialResult` and a stream of `subsequentResults`. + * Note: At runtime, the schema is not checked as to whether these directives are + * actually present; all operations passed to execute are assumed to be valid. * - * If the arguments to this function do not result in a legal execution context, - * a GraphQLError will be thrown immediately explaining the invalid input. + * If the incremental delivery directives are used the return type of this + * function may also be an ExperimentalIncrementalExecutionResults object (or a + * Promise thereof), This object either consists of a an `initialResult` and a + * stream of `subsequentResults`. */ -export function experimentalExecuteIncrementally( +export function execute( args: ExecutionArgs, -): PromiseOrValue { +): PromiseOrValue> { // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. const exeContext = buildExecutionContext(args); // Return early errors if execution context failed. if (!('schema' in exeContext)) { - return { errors: exeContext }; + return { errors: exeContext } as MaybeIncrementalMap; } - return executeImpl(exeContext); + return executeImpl(exeContext) as PromiseOrValue< + MaybeIncrementalMap + >; } function executeImpl( @@ -388,7 +367,7 @@ function executeImpl( * that all field resolvers are also synchronous. */ export function executeSync(args: ExecutionArgs): ExecutionResult { - const result = experimentalExecuteIncrementally(args); + const result = execute(args); // Assert that the execution was synchronous. if (isPromise(result) || 'initialResult' in result) { @@ -1457,6 +1436,13 @@ export const defaultFieldResolver: GraphQLFieldResolver = } }; +type MaybeIncrementalPayload = TMaybeIncremental extends true + ? + | ExecutionResult + | InitialIncrementalExecutionResult + | SubsequentIncrementalExecutionResult + : ExecutionResult; + /** * Implements the "Subscribe" algorithm described in the GraphQL specification. * @@ -1476,71 +1462,16 @@ export const defaultFieldResolver: GraphQLFieldResolver = * If the operation succeeded, the promise resolves to an AsyncIterator, which * yields a stream of ExecutionResults representing the response stream. * - * This function does not support incremental delivery (`@defer` and `@stream`). - * If an operation which would defer or stream data is executed with this - * function, each `InitialIncrementalExecutionResult` and - * `SubsequentIncrementalExecutionResult` in the result stream will be replaced - * with an `ExecutionResult` with a single error stating that defer/stream is - * not supported. Use `experimentalSubscribeIncrementally` if you want to - * support incremental delivery. + * This function also supports experimental incremental delivery directives + * (`@defer` and `@stream`). To use these directives, they should be added to + * the schema and TS generic parameter TMaybeIncremental should be set to `true` + * (default: false). * - * Accepts an object with named arguments. - */ -export function subscribe( - args: ExecutionArgs, -): PromiseOrValue< - AsyncGenerator | ExecutionResult -> { - const maybePromise = experimentalSubscribeIncrementally(args); - if (isPromise(maybePromise)) { - return maybePromise.then((resultOrIterable) => - isAsyncIterable(resultOrIterable) - ? mapAsyncIterable(resultOrIterable, ensureSingleExecutionResult) - : resultOrIterable, - ); - } - return isAsyncIterable(maybePromise) - ? mapAsyncIterable(maybePromise, ensureSingleExecutionResult) - : maybePromise; -} - -function ensureSingleExecutionResult( - result: - | ExecutionResult - | InitialIncrementalExecutionResult - | SubsequentIncrementalExecutionResult, -): ExecutionResult { - if ('hasNext' in result) { - return { - errors: [new GraphQLError(UNEXPECTED_MULTIPLE_PAYLOADS)], - }; - } - return result; -} - -/** - * Implements the "Subscribe" algorithm described in the GraphQL specification, - * including `@defer` and `@stream` as proposed in - * https://github.com/graphql/graphql-spec/pull/742 + * Note: At runtime, the schema is not checked as to whether these directives are + * actually present; all operations passed to execute are assumed to be valid. * - * 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 result representing the response stream. - * - * Each result may be an ExecutionResult with no `hasNext` (if executing the - * event did not use `@defer` or `@stream`), or an + * In that case, each payload may be an ExecutionResult with no `hasNext` + * (if executing the event did not use `@defer` or `@stream`), or an * `InitialIncrementalExecutionResult` or `SubsequentIncrementalExecutionResult` * (if executing the event used `@defer` or `@stream`). In the case of * incremental execution results, each event produces a single @@ -1551,16 +1482,10 @@ function ensureSingleExecutionResult( * * Accepts an object with named arguments. */ -export function experimentalSubscribeIncrementally( +export function subscribe( args: ExecutionArgs, ): PromiseOrValue< - | AsyncGenerator< - | ExecutionResult - | InitialIncrementalExecutionResult - | SubsequentIncrementalExecutionResult, - void, - void - > + | AsyncGenerator, void, void> | ExecutionResult > { // If a valid execution context cannot be created due to incorrect arguments, diff --git a/src/execution/index.ts b/src/execution/index.ts index 46a4688d59..4c7d222fdb 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -3,12 +3,10 @@ export { pathToArray as responsePathAsArray } from '../jsutils/Path.js'; export { createSourceEventStream, execute, - experimentalExecuteIncrementally, executeSync, defaultFieldResolver, defaultTypeResolver, subscribe, - experimentalSubscribeIncrementally, } from './execute.js'; export type { diff --git a/src/index.ts b/src/index.ts index 5e05627fbe..bb7ec16eff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -319,7 +319,6 @@ export type { // Execute GraphQL queries. export { execute, - experimentalExecuteIncrementally, executeSync, defaultFieldResolver, defaultTypeResolver, @@ -328,7 +327,6 @@ export { getVariableValues, getDirectiveValues, subscribe, - experimentalSubscribeIncrementally, createSourceEventStream, } from './execution/index.js'; diff --git a/website/docs/tutorials/defer-stream.md b/website/docs/tutorials/defer-stream.md index 7f1c9ceb95..a712368f51 100644 --- a/website/docs/tutorials/defer-stream.md +++ b/website/docs/tutorials/defer-stream.md @@ -29,3 +29,12 @@ const result = experimentalExecuteIncrementally({ ``` If the `directives` option is passed to `GraphQLSchema`, the default directives will not be included. `specifiedDirectives` must be passed to ensure all standard directives are added in addition to `defer` & `stream`. + +When using TypeScript, remember to set the `TMaybeIncremental` generic parameter of `execute` to `true`: + +```ts +const result = execute({ + schema, + document, +}); +```