From 32500bfe18815f50f1f53175b4d2b910deaaf1e0 Mon Sep 17 00:00:00 2001 From: morrys Date: Mon, 30 Dec 2019 13:33:49 +0100 Subject: [PATCH 01/10] added GraphQLDeferDirective --- src/index.d.ts | 1 + src/index.js | 1 + src/type/directives.d.ts | 5 +++++ src/type/directives.js | 21 +++++++++++++++++++++ src/type/index.d.ts | 1 + src/type/index.js | 1 + 6 files changed, 30 insertions(+) diff --git a/src/index.d.ts b/src/index.d.ts index 214f7a1371..f2f3cf4a68 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -54,6 +54,7 @@ export { specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, GraphQLDeprecatedDirective, // "Enum" of Type Kinds TypeKind, diff --git a/src/index.js b/src/index.js index 9ea2be9858..850cb47efd 100644 --- a/src/index.js +++ b/src/index.js @@ -55,6 +55,7 @@ export { specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, GraphQLDeprecatedDirective, // "Enum" of Type Kinds TypeKind, diff --git a/src/type/directives.d.ts b/src/type/directives.d.ts index b133c66483..17c0839725 100644 --- a/src/type/directives.d.ts +++ b/src/type/directives.d.ts @@ -54,6 +54,11 @@ export const GraphQLIncludeDirective: GraphQLDirective; */ export const GraphQLSkipDirective: GraphQLDirective; +/** + * Used to conditionally defer fragments. + */ +export const GraphQLDeferDirective: GraphQLDirective; + /** * Constant string used for default reason for a deprecation. */ diff --git a/src/type/directives.js b/src/type/directives.js index 13f579c8d9..d3894a9f60 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -167,6 +167,26 @@ export const GraphQLSkipDirective = new GraphQLDirective({ }, }); +/** + * Used to conditionally defer fragments. + */ +export const GraphQLDeferDirective = new GraphQLDirective({ + name: 'defer', + description: + 'Directs the executor to defer fragment when the `if` argument is true.', + locations: [DirectiveLocation.FRAGMENT_SPREAD], + args: { + if: { + type: GraphQLBoolean, + description: 'Deferred when true.', + }, + label: { + type: GraphQLNonNull(GraphQLString), + description: 'label', + }, + }, +}); + /** * Constant string used for default reason for a deprecation. */ @@ -195,6 +215,7 @@ export const GraphQLDeprecatedDirective = new GraphQLDirective({ export const specifiedDirectives = Object.freeze([ GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, GraphQLDeprecatedDirective, ]); diff --git a/src/type/index.d.ts b/src/type/index.d.ts index b6780d8171..f7d904daa5 100644 --- a/src/type/index.d.ts +++ b/src/type/index.d.ts @@ -114,6 +114,7 @@ export { specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, GraphQLDeprecatedDirective, // Constant Deprecation Reason DEFAULT_DEPRECATION_REASON, diff --git a/src/type/index.js b/src/type/index.js index ec87a1c7b0..de843599c9 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -78,6 +78,7 @@ export { specifiedDirectives, GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, GraphQLDeprecatedDirective, // Constant Deprecation Reason DEFAULT_DEPRECATION_REASON, From ad31bb16b1878acfca42181bcccfb66775223c8f Mon Sep 17 00:00:00 2001 From: morrys Date: Mon, 30 Dec 2019 13:34:34 +0100 Subject: [PATCH 02/10] created getDirective function in values.js --- src/execution/values.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/execution/values.js b/src/execution/values.js index 1d09672e5f..998db4b810 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -230,6 +230,22 @@ export function getArgumentValues( return coercedValues; } +/** + * If the directive does not exist on the node, returns undefined otherwise returns a DirectiveNode. + */ +export function getDirective( + directiveDef: GraphQLDirective, + node: { +directives?: $ReadOnlyArray, ... }, +): void | DirectiveNode { + return ( + node.directives && + find( + node.directives, + directive => directive.name.value === directiveDef.name, + ) + ); +} + /** * Prepares an object map of argument values given a directive definition * and a AST node which may contain directives. Optionally also accepts a map @@ -246,12 +262,7 @@ export function getDirectiveValues( node: { +directives?: $ReadOnlyArray, ... }, variableValues?: ?ObjMap, ): void | { [argument: string]: mixed, ... } { - const directiveNode = - node.directives && - find( - node.directives, - directive => directive.name.value === directiveDef.name, - ); + const directiveNode = getDirective(directiveDef, node); if (directiveNode) { return getArgumentValues(directiveDef, directiveNode, variableValues); From 6daf03b8412e648f4cab9e0dbd43019fd9841689 Mon Sep 17 00:00:00 2001 From: morrys Date: Mon, 30 Dec 2019 13:35:40 +0100 Subject: [PATCH 03/10] added defer execution --- src/execution/execute.js | 271 ++++++++++++++++++++++++++++---- src/utilities/buildASTSchema.js | 5 + 2 files changed, 247 insertions(+), 29 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index fe80d6c980..02ec700088 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -1,6 +1,6 @@ // @flow strict -import { forEach, isCollection } from 'iterall'; +import { $$asyncIterator, forEach, isCollection } from 'iterall'; import inspect from '../jsutils/inspect'; import memoize3 from '../jsutils/memoize3'; @@ -22,6 +22,7 @@ import { locatedError } from '../error/locatedError'; import { Kind } from '../language/kinds'; import { type DocumentNode, + type DirectiveNode, type OperationDefinitionNode, type SelectionSetNode, type FieldNode, @@ -40,6 +41,7 @@ import { import { GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, } from '../type/directives'; import { type GraphQLObjectType, @@ -65,6 +67,7 @@ import { getVariableValues, getArgumentValues, getDirectiveValues, + getDirective, } from './values'; /** @@ -103,6 +106,7 @@ export type ExecutionContext = {| fieldResolver: GraphQLFieldResolver, typeResolver: GraphQLTypeResolver, errors: Array, + resultResolver: ResultResolver, |}; /** @@ -114,6 +118,13 @@ export type ExecutionContext = {| export type ExecutionResult = {| errors?: $ReadOnlyArray, data?: ObjMap | null, + label?: string, + path?: Array, +|}; + +export type AsyncExecutionResult = {| + value: ExecutionResult, + done: boolean, |}; export type ExecutionArgs = {| @@ -127,6 +138,161 @@ export type ExecutionArgs = {| typeResolver?: ?GraphQLTypeResolver, |}; +/** + * Helper class that allows us to dispatch patches dynamically, and obtain an + * AsyncIterable that yields each patch in the order that they get resolved. + */ +class ResultResolver { + executionResults: Array>; + deferResults: { + path: Path | void, + label: string, + resolvers: Array<{ + responseName: string, + resolver: () => PromiseOrValue, + }>, + }; + + constructor() { + this.executionResults = []; + this.deferResults = Object.create(null); + } + + addDeferResolver( + path: Path | void, + label: string, + responseName: string, + resolver: () => PromiseOrValue, + ): void { + const pathArray = pathToArray(path); + const keyDeferResult = pathArray.toString() + label; + const deferResult = this.deferResults[keyDeferResult] || { + resolvers: [], + label, + path: pathArray, + }; + deferResult.resolvers.push({ resolver, responseName }); + this.deferResults[keyDeferResult] = deferResult; + } + + addDeferResults(exeContext: ExecutionContext) { + const deferResultsArray = Object.values(this.deferResults); + this.deferResults = Object.create(null); + + // avoid multiple resolve for same responseName in same path + const resultsResolver = Object.create({}); + for (const deferResult of deferResultsArray) { + const resolveResult = () => { + const { label, path, resolvers } = deferResult; + const results = Object.create({}); + let containsPromise = false; + resolvers.forEach(({ resolver, responseName }) => { + const keyResultResolve = path.toString() + responseName; + const result = resultsResolver[keyResultResolve] + ? resultsResolver[keyResultResolve] + : resolver(); + + resultsResolver[keyResultResolve] = result; + if (result !== undefined) { + results[responseName] = result; + if (!containsPromise && isPromise(result)) { + containsPromise = true; + } + } + }); + + // If there are no promises, we can just return the object + if (!containsPromise) { + // defer's result must always be a Promise + return this.buildResponse(exeContext, Promise.resolve(results), { + label, + path, + }); + } + // 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 this.buildResponse(exeContext, promiseForObject(results), { + label, + path, + }); + }; + + this.executionResults.push(resolveResult()); + //this.addDeferResults(exeContext); + // change questo con una promise dopo la prima esecuzione then (add deferResult return data) + } + } + + /** + * Given a completed execution context and data, build the { errors, data } + * response defined by the "Response" section of the GraphQL specification. + */ + buildResponse( + exeContext: ExecutionContext, + data: PromiseOrValue | null>, + iterable?: { + label?: string, + path?: Array, + }, + ): PromiseOrValue | Promise { + if (isPromise(data)) { + return data.then(resolved => { + const value = + exeContext.errors.length === 0 + ? { data: resolved, ...iterable } + : { errors: exeContext.errors, data: resolved, ...iterable }; + if (iterable) { + // defer results are added only after the parent's response has been resolved + this.addDeferResults(exeContext); + return { + value, + done: false, + }; + } + return value; + }); + } + return exeContext.errors.length === 0 + ? { data } + : { errors: exeContext.errors, data }; + } + + getResult( + exeContext: ExecutionContext, + data: PromiseOrValue | null>, + ): AsyncIterable> | PromiseOrValue { + if (!Object.keys(this.deferResults).length) { + return this.buildResponse(exeContext, data); + } + // defer's result must always be a Promise + const promiseData = isPromise(data) ? data : Promise.resolve(data); + this.executionResults.push(this.buildResponse(exeContext, promiseData, {})); + return this.getAsyncIterable(); + } + + getAsyncIterable(): AsyncIterable> { + const self = this; + return ({ + [$$asyncIterator]() { + return { + next() { + return ( + self.executionResults.shift() || + Promise.resolve({ + done: true, + }) + ); + }, + [$$asyncIterator]() { + return this; + }, + }; + }, + }: any); + } +} + /** * Implements the "Evaluating requests" section of the GraphQL specification. * @@ -142,7 +308,7 @@ export type ExecutionArgs = {| declare function execute( ExecutionArgs, ..._: [] -): PromiseOrValue; +): AsyncIterable> | PromiseOrValue; /* eslint-disable no-redeclare */ declare function execute( schema: GraphQLSchema, @@ -153,7 +319,7 @@ declare function execute( operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, typeResolver?: ?GraphQLTypeResolver, -): PromiseOrValue; +): AsyncIterable> | PromiseOrValue; export function execute( argsOrSchema, document, @@ -180,7 +346,9 @@ export function execute( }); } -function executeImpl(args: ExecutionArgs): PromiseOrValue { +function executeImpl( + args: ExecutionArgs, +): AsyncIterable> | PromiseOrValue { const { schema, document, @@ -221,23 +389,7 @@ function executeImpl(args: ExecutionArgs): PromiseOrValue { // 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); -} - -/** - * 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 }; + return exeContext.resultResolver.getResult(exeContext, data); } /** @@ -333,6 +485,7 @@ export function buildExecutionContext( fieldResolver: fieldResolver || defaultFieldResolver, typeResolver: typeResolver || defaultTypeResolver, errors: [], + resultResolver: new ResultResolver(), }; } @@ -417,6 +570,19 @@ function executeFieldsSerially( ); } +function getDeferredInfo(exeContext, fieldNodes) { + let every = true; + const deferLabels = []; + for (const node of fieldNodes) { + const lastDefer = getDeferDirectiveValues(exeContext, node); + every = every && lastDefer; + if (lastDefer && !deferLabels.includes(lastDefer.label)) { + deferLabels.push(lastDefer.label); + } + } + return [every, deferLabels]; +} + /** * Implements the "Evaluating selection sets" section of the spec * for "read" mode. @@ -430,17 +596,24 @@ function executeFields( ): PromiseOrValue> { const results = Object.create(null); let containsPromise = false; - for (const responseName of Object.keys(fields)) { const fieldNodes = fields[responseName]; const fieldPath = addPath(path, responseName); - const result = resolveField( - exeContext, - parentType, - sourceValue, - fieldNodes, - fieldPath, - ); + const [every, deferLabels] = getDeferredInfo(exeContext, fieldNodes); + + const resolve = () => + resolveField(exeContext, parentType, sourceValue, fieldNodes, fieldPath); + + const result = every ? null : resolve(); + + for (const label of deferLabels) { + exeContext.resultResolver.addDeferResolver( + path, + label, + responseName, + () => (every ? resolve() : result), + ); + } if (result !== undefined) { results[responseName] = result; @@ -477,6 +650,7 @@ export function collectFields( selectionSet: SelectionSetNode, fields: ObjMap>, visitedFragmentNames: ObjMap, + deferDirective?: DirectiveNode, ): ObjMap> { for (const selection of selectionSet.selections) { switch (selection.kind) { @@ -488,6 +662,14 @@ export function collectFields( if (!fields[name]) { fields[name] = []; } + /* + in order to support defer on field + const isFieldDefer = shouldDeferNode(exeContext, selection); + if (deferDirective && !isFieldDefer) { + */ + if (deferDirective) { + selection.directives.push(deferDirective); + } fields[name].push(selection); break; } @@ -504,6 +686,7 @@ export function collectFields( selection.selectionSet, fields, visitedFragmentNames, + deferDirective, ); break; } @@ -523,12 +706,15 @@ export function collectFields( ) { continue; } + const fragmentDeferDirective = + shouldDeferNode(exeContext, selection) || deferDirective; collectFields( exeContext, runtimeType, fragment.selectionSet, fields, visitedFragmentNames, + fragmentDeferDirective, ); break; } @@ -565,6 +751,33 @@ function shouldIncludeNode( return true; } +/** + * Determines if a field should be deferred. @skip and @include has higher + * precedence than @defer. + */ +function shouldDeferNode( + exeContext: ExecutionContext, + node: FragmentSpreadNode, +): DirectiveNode | void { + const shouldDefer = getDeferDirectiveValues(exeContext, node); + return shouldDefer && getDirective(GraphQLDeferDirective, node); +} + +function getDeferDirectiveValues( + exeContext: ExecutionContext, + node: FragmentSpreadNode | FieldNode, +): null | { [argument: string]: mixed, ... } { + const defer = getDirectiveValues( + GraphQLDeferDirective, + node, + exeContext.variableValues, + ); + if (!defer || defer.if === false) { + return null; + } + return defer; +} + /** * Determines if a fragment is applicable to the given type. */ diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 0259e2c1d2..1c30ddcf34 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -15,6 +15,7 @@ import { } from '../type/schema'; import { GraphQLSkipDirective, + GraphQLDeferDirective, GraphQLIncludeDirective, GraphQLDeprecatedDirective, } from '../type/directives'; @@ -101,6 +102,10 @@ export function buildASTSchema( directives.push(GraphQLSkipDirective); } + if (!directives.some(directive => directive.name === 'defer')) { + directives.push(GraphQLDeferDirective); + } + if (!directives.some(directive => directive.name === 'include')) { directives.push(GraphQLIncludeDirective); } From 8b669b82f13a65cb1821030ac4b4c7f7118656f8 Mon Sep 17 00:00:00 2001 From: morrys Date: Mon, 30 Dec 2019 13:35:50 +0100 Subject: [PATCH 04/10] added tests for defer directive --- src/execution/__tests__/directives-test.js | 389 +++++++++++++++++- src/type/__tests__/introspection-test.js | 28 ++ .../__tests__/buildASTSchema-test.js | 11 +- .../__tests__/findBreakingChanges-test.js | 7 +- src/utilities/__tests__/schemaPrinter-test.js | 18 + .../__tests__/KnownDirectives-test.js | 9 +- .../ProvidedRequiredArguments-test.js | 8 + src/validation/__tests__/harness.js | 2 + 8 files changed, 466 insertions(+), 6 deletions(-) diff --git a/src/execution/__tests__/directives-test.js b/src/execution/__tests__/directives-test.js index 810cd51b68..2aa651604c 100644 --- a/src/execution/__tests__/directives-test.js +++ b/src/execution/__tests__/directives-test.js @@ -10,6 +10,35 @@ import { GraphQLString } from '../../type/scalars'; import { GraphQLObjectType } from '../../type/definition'; import { execute } from '../execute'; +import { forAwaitEach, isAsyncIterable, createAsyncIterator } from 'iterall'; + +class Data { + d: string; + e: string; + + constructor(d) { + this.d = d; + this.e = 'e'; + } +} + +const DataType = new GraphQLObjectType({ + name: 'DataType', + fields: { + d: { + type: GraphQLString, + resolve(obj) { + return Promise.resolve(obj.d); + }, + }, + e: { + type: GraphQLString, + resolve(obj) { + return obj.e; + }, + }, + }, +}); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -17,6 +46,7 @@ const schema = new GraphQLSchema({ fields: { a: { type: GraphQLString }, b: { type: GraphQLString }, + c: { type: DataType }, }, }), }); @@ -28,6 +58,9 @@ const rootValue = { b() { return 'b'; }, + c() { + return new Data('d'); + }, }; function executeTestQuery(query) { @@ -144,8 +177,362 @@ describe('Execute: handles directives', () => { data: { a: 'a' }, }); }); - }); + describe('defer fragment spread', () => { + it('without if', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(label: "Frag_b_defer") + } + fragment Frag on TestType { + b + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const resultsIterator = createAsyncIterator(result); + + const results = []; + await forAwaitEach(resultsIterator, value => { + results.push(value); + }); + + expect(results.length).to.equal(2); + expect(results[0]).to.deep.equal({ + data: { a: 'a', b: null }, + }); + expect(results[1]).to.deep.equal({ + data: { b: 'b' }, + label: 'Frag_b_defer', + path: [], + }); + }); + + it('if true', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: true, label: "Frag_b_defer") + } + fragment Frag on TestType { + b + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const resultsIterator = createAsyncIterator(result); + + const results = []; + await forAwaitEach(resultsIterator, value => { + results.push(value); + }); + + expect(results.length).to.equal(2); + expect(results[0]).to.deep.equal({ + data: { a: 'a', b: null }, + }); + expect(results[1]).to.deep.equal({ + data: { b: 'b' }, + label: 'Frag_b_defer', + path: [], + }); + }); + + it('if false', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: false, label: "Frag_b_defer") + } + fragment Frag on TestType { + b + } + `); + expect(isAsyncIterable(result)).to.equal(false); + const data = await result; + expect(data).to.deep.equal({ + data: { a: 'a', b: 'b' }, + }); + }); + describe('defer defer fragment spread with DataType', () => { + it('without defer', async () => { + const result = executeTestQuery(` + query { + a + ...Frag + } + fragment FragData on DataType { + d + } + fragment Frag on TestType { + c { + ...FragData + } + } + `); + expect(isAsyncIterable(result)).to.equal(false); + const data = await result; + expect(data).to.deep.equal({ + data: { + a: 'a', + c: { + d: 'd', + }, + }, + }); + }); + + it('if false not defer', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: false, label: "Frag_c_defer") + } + fragment Frag on TestType { + c { + ...FragData @defer(if: false, label: "Frag_d_defer") + } + } + fragment FragData on DataType { + d + } + `); + expect(isAsyncIterable(result)).to.equal(false); + const data = await result; + expect(data).to.deep.equal({ + data: { + a: 'a', + c: { + d: 'd', + }, + }, + }); + }); + it('two fragments with two field equals', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: true, label: "Frag_c_defer") + } + fragment Frag on TestType { + c { + ...FragData @defer(if: true, label: "FragData_d_defer") + ...FragDeData @defer(if: true, label: "FragDeData_de_defer") + } + } + fragment FragData on DataType { + d + } + fragment FragDeData on DataType { + d + e + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const resultsIterator = createAsyncIterator(result); + + const results = []; + await forAwaitEach(resultsIterator, value => { + results.push(value); + }); + + expect(results.length).to.equal(4); + expect(results[0]).to.deep.equal({ + data: { a: 'a', c: null }, + }); + expect(results[1]).to.deep.equal({ + data: { + c: { + d: null, + e: null, + }, + }, + label: 'Frag_c_defer', + path: [], + }); + expect(results[2]).to.deep.equal({ + data: { d: 'd' }, + label: 'FragData_d_defer', + path: ['c'], + }); + expect(results[3]).to.deep.equal({ + data: { d: 'd', e: 'e' }, + label: 'FragDeData_de_defer', + path: ['c'], + }); + }); + + it('with two equals fragments', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: true, label: "Frag_c_defer") + } + fragment Frag on TestType { + c { + ...FragData @defer(if: true, label: "FragData_d_defer") + ...FragDeData @defer(if: true, label: "FragDeData_de_defer") + } + } + fragment FragData on DataType { + d + } + fragment FragDeData on DataType { + d + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const resultsIterator = createAsyncIterator(result); + + const results = []; + await forAwaitEach(resultsIterator, value => { + results.push(value); + }); + + expect(results.length).to.equal(4); + expect(results[0]).to.deep.equal({ + data: { a: 'a', c: null }, + }); + expect(results[1]).to.deep.equal({ + data: { + c: { + d: null, + }, + }, + label: 'Frag_c_defer', + path: [], + }); + expect(results[2]).to.deep.equal({ + data: { d: 'd' }, + label: 'FragData_d_defer', + path: ['c'], + }); + expect(results[3]).to.deep.equal({ + data: { d: 'd' }, + label: 'FragDeData_de_defer', + path: ['c'], + }); + }); + + it('mixed if true & if false', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: true, label: "Frag_c_defer") + } + fragment Frag on TestType { + c { + ...FragData @defer(if: false, label: "FragData_d_defer") + } + } + fragment FragData on DataType { + d + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const resultsIterator = createAsyncIterator(result); + + const results = []; + await forAwaitEach(resultsIterator, value => { + results.push(value); + }); + + expect(results.length).to.equal(2); + expect(results[0]).to.deep.equal({ + data: { a: 'a', c: null }, + }); + expect(results[1]).to.deep.equal({ + data: { + c: { d: 'd' }, + }, + label: 'Frag_c_defer', + path: [], + }); + }); + + it('mixed if false & if true', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: false, label: "Frag_c_defer") + } + fragment Frag on TestType { + c { + ...FragData @defer(if: true, label: "FragData_d_defer") + } + } + fragment FragData on DataType { + d + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const resultsIterator = createAsyncIterator(result); + + const results = []; + await forAwaitEach(resultsIterator, value => { + results.push(value); + }); + + expect(results.length).to.equal(2); + expect(results[0]).to.deep.equal({ + data: { + a: 'a', + c: { + d: null, + }, + }, + }); + expect(results[1]).to.deep.equal({ + data: { d: 'd' }, + label: 'FragData_d_defer', + path: ['c'], + }); + }); + + it('if true', async () => { + const result = executeTestQuery(` + query { + a + ...Frag @defer(if: true, label: "Frag_c_defer") + } + fragment Frag on TestType { + c { + ...FragData @defer(if: true, label: "FragData_d_defer") + } + } + fragment FragData on DataType { + d + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const resultsIterator = createAsyncIterator(result); + + const results = []; + await forAwaitEach(resultsIterator, value => { + results.push(value); + }); + + expect(results.length).to.equal(3); + expect(results[0]).to.deep.equal({ + data: { a: 'a', c: null }, + }); + expect(results[1]).to.deep.equal({ + data: { + c: { + d: null, + }, + }, + label: 'Frag_c_defer', + path: [], + }); + expect(results[2]).to.deep.equal({ + data: { d: 'd' }, + label: 'FragData_d_defer', + path: ['c'], + }); + }); + }); + }); + }); describe('works on inline fragment', () => { it('if false omits inline fragment', () => { const result = executeTestQuery(` diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 4a57cda970..f4afadff5f 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -845,6 +845,34 @@ describe('Introspection', () => { }, ], }, + { + name: 'defer', + locations: ['FRAGMENT_SPREAD'], + args: [ + { + defaultValue: null, + name: 'if', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + { + defaultValue: null, + name: 'label', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + }, + ], + }, { name: 'deprecated', locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 47fe0bab8c..e43d63223e 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -14,6 +14,7 @@ import { validateSchema } from '../../type/validate'; import { assertDirective, GraphQLSkipDirective, + GraphQLDeferDirective, GraphQLIncludeDirective, GraphQLDeprecatedDirective, } from '../../type/directives'; @@ -211,9 +212,10 @@ describe('Schema Builder', () => { it('Maintains @skip & @include', () => { const schema = buildSchema('type Query'); - expect(schema.getDirectives()).to.have.lengthOf(3); + expect(schema.getDirectives()).to.have.lengthOf(4); expect(schema.getDirective('skip')).to.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.equal(GraphQLIncludeDirective); + expect(schema.getDirective('defer')).to.equal(GraphQLDeferDirective); expect(schema.getDirective('deprecated')).to.equal( GraphQLDeprecatedDirective, ); @@ -224,9 +226,10 @@ describe('Schema Builder', () => { directive @skip on FIELD directive @include on FIELD directive @deprecated on FIELD_DEFINITION + directive @defer on FRAGMENT_SPREAD `); - expect(schema.getDirectives()).to.have.lengthOf(3); + expect(schema.getDirectives()).to.have.lengthOf(4); expect(schema.getDirective('skip')).to.not.equal(GraphQLSkipDirective); expect(schema.getDirective('include')).to.not.equal( GraphQLIncludeDirective, @@ -234,6 +237,7 @@ describe('Schema Builder', () => { expect(schema.getDirective('deprecated')).to.not.equal( GraphQLDeprecatedDirective, ); + expect(schema.getDirective('defer')).to.not.equal(GraphQLDeferDirective); }); it('Adding directives maintains @skip & @include', () => { @@ -241,10 +245,11 @@ describe('Schema Builder', () => { directive @foo(arg: Int) on FIELD `); - expect(schema.getDirectives()).to.have.lengthOf(4); + expect(schema.getDirectives()).to.have.lengthOf(5); expect(schema.getDirective('skip')).to.not.equal(undefined); expect(schema.getDirective('include')).to.not.equal(undefined); expect(schema.getDirective('deprecated')).to.not.equal(undefined); + expect(schema.getDirective('defer')).to.not.equal(undefined); }); it('Type modifiers', () => { diff --git a/src/utilities/__tests__/findBreakingChanges-test.js b/src/utilities/__tests__/findBreakingChanges-test.js index b3fd7dae6e..c46e3568f1 100644 --- a/src/utilities/__tests__/findBreakingChanges-test.js +++ b/src/utilities/__tests__/findBreakingChanges-test.js @@ -7,6 +7,7 @@ import { GraphQLSchema } from '../../type/schema'; import { GraphQLSkipDirective, GraphQLIncludeDirective, + GraphQLDeferDirective, GraphQLDeprecatedDirective, } from '../../type/directives'; @@ -790,7 +791,11 @@ describe('findBreakingChanges', () => { const oldSchema = new GraphQLSchema({}); const newSchema = new GraphQLSchema({ - directives: [GraphQLSkipDirective, GraphQLIncludeDirective], + directives: [ + GraphQLSkipDirective, + GraphQLIncludeDirective, + GraphQLDeferDirective, + ], }); expect(findBreakingChanges(oldSchema, newSchema)).to.deep.equal([ diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 14edfdca62..839799bc84 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -590,6 +590,15 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + """Directs the executor to defer fragment when the \`if\` argument is true.""" + directive @defer( + """Deferred when true.""" + if: Boolean + + """label""" + label: String! + ) on FRAGMENT_SPREAD + """Marks an element of a GraphQL schema as no longer supported.""" directive @deprecated( """ @@ -803,6 +812,15 @@ describe('Type System Printer', () => { if: Boolean! ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + # Directs the executor to defer fragment when the \`if\` argument is true. + directive @defer( + # Deferred when true. + if: Boolean + + # label + label: String! + ) on FRAGMENT_SPREAD + # Marks an element of a GraphQL schema as no longer supported. directive @deprecated( # Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/). diff --git a/src/validation/__tests__/KnownDirectives-test.js b/src/validation/__tests__/KnownDirectives-test.js index d875cbc4a1..59129f99a4 100644 --- a/src/validation/__tests__/KnownDirectives-test.js +++ b/src/validation/__tests__/KnownDirectives-test.js @@ -61,6 +61,7 @@ describe('Validate: Known directives', () => { human @skip(if: false) { name } + ...DeferFrag @defer(if: true) } `); }); @@ -116,6 +117,8 @@ describe('Validate: Known directives', () => { ...Frag @include(if: true) skippedField @skip(if: true) ...SkippedFrag @skip(if: true) + ...DeferVarFrag @defer(if: $var) + ...DeferFrag @defer(if: true) } mutation Bar @onMutation { @@ -135,7 +138,7 @@ describe('Validate: Known directives', () => { it('with misplaced directives', () => { expectErrors(` query Foo($var: Boolean) @include(if: true) { - name @onQuery @include(if: $var) + name @onQuery @include(if: $var) @defer(if: $var) ...Frag @onQuery } @@ -151,6 +154,10 @@ describe('Validate: Known directives', () => { message: 'Directive "@onQuery" may not be used on FIELD.', locations: [{ line: 3, column: 14 }], }, + { + message: 'Directive "@defer" may not be used on FIELD.', + locations: [{ line: 3, column: 42 }], + }, { message: 'Directive "@onQuery" may not be used on FRAGMENT_SPREAD.', locations: [{ line: 4, column: 17 }], diff --git a/src/validation/__tests__/ProvidedRequiredArguments-test.js b/src/validation/__tests__/ProvidedRequiredArguments-test.js index d969838169..f6130f0068 100644 --- a/src/validation/__tests__/ProvidedRequiredArguments-test.js +++ b/src/validation/__tests__/ProvidedRequiredArguments-test.js @@ -227,6 +227,8 @@ describe('Validate: Provided required arguments', () => { human @skip(if: false) { name } + ...DeferFrag @defer(label: "DeferFrag_defer") + ...DeferIfFrag @defer(if: true, label: "DeferFrag_defer") } `); }); @@ -237,6 +239,7 @@ describe('Validate: Provided required arguments', () => { dog @include { name @skip } + ...DeferFrag @defer } `).to.deep.equal([ { @@ -249,6 +252,11 @@ describe('Validate: Provided required arguments', () => { 'Directive "@skip" argument "if" of type "Boolean!" is required, but it was not provided.', locations: [{ line: 4, column: 18 }], }, + { + message: + 'Directive "@defer" argument "label" of type "String!" is required, but it was not provided.', + locations: [{ line: 6, column: 24 }], + }, ]); }); }); diff --git a/src/validation/__tests__/harness.js b/src/validation/__tests__/harness.js index d84d7520e5..d81c137f96 100644 --- a/src/validation/__tests__/harness.js +++ b/src/validation/__tests__/harness.js @@ -11,6 +11,7 @@ import { GraphQLDirective, GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, } from '../../type/directives'; import { GraphQLInt, @@ -362,6 +363,7 @@ export const testSchema = new GraphQLSchema({ directives: [ GraphQLIncludeDirective, GraphQLSkipDirective, + GraphQLDeferDirective, new GraphQLDirective({ name: 'onQuery', locations: ['QUERY'], From 4226f231f74ad30c5e20a3ca3289417af8058ba1 Mon Sep 17 00:00:00 2001 From: morrys Date: Mon, 30 Dec 2019 14:30:27 +0100 Subject: [PATCH 05/10] remove comments --- src/execution/execute.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 02ec700088..8316a8dd1d 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -219,8 +219,6 @@ class ResultResolver { }; this.executionResults.push(resolveResult()); - //this.addDeferResults(exeContext); - // change questo con una promise dopo la prima esecuzione then (add deferResult return data) } } From 97e3e792b91e6013bb2dc5301790a9957bac1c23 Mon Sep 17 00:00:00 2001 From: morrys Date: Mon, 30 Dec 2019 18:56:31 +0100 Subject: [PATCH 06/10] small refactor for fix flow --- src/execution/__tests__/directives-test.js | 24 ++---- src/execution/execute.js | 92 +++++++++++++--------- src/graphql.js | 11 ++- src/utilities/introspectionFromSchema.js | 2 +- 4 files changed, 70 insertions(+), 59 deletions(-) diff --git a/src/execution/__tests__/directives-test.js b/src/execution/__tests__/directives-test.js index 2aa651604c..75f0a8510f 100644 --- a/src/execution/__tests__/directives-test.js +++ b/src/execution/__tests__/directives-test.js @@ -10,7 +10,7 @@ import { GraphQLString } from '../../type/scalars'; import { GraphQLObjectType } from '../../type/definition'; import { execute } from '../execute'; -import { forAwaitEach, isAsyncIterable, createAsyncIterator } from 'iterall'; +import { forAwaitEach, isAsyncIterable } from 'iterall'; class Data { d: string; @@ -190,10 +190,8 @@ describe('Execute: handles directives', () => { } `); expect(isAsyncIterable(result)).to.equal(true); - const resultsIterator = createAsyncIterator(result); - const results = []; - await forAwaitEach(resultsIterator, value => { + await forAwaitEach(((result: any): AsyncIterable), value => { results.push(value); }); @@ -219,10 +217,9 @@ describe('Execute: handles directives', () => { } `); expect(isAsyncIterable(result)).to.equal(true); - const resultsIterator = createAsyncIterator(result); const results = []; - await forAwaitEach(resultsIterator, value => { + await forAwaitEach(((result: any): AsyncIterable), value => { results.push(value); }); @@ -328,10 +325,9 @@ describe('Execute: handles directives', () => { } `); expect(isAsyncIterable(result)).to.equal(true); - const resultsIterator = createAsyncIterator(result); const results = []; - await forAwaitEach(resultsIterator, value => { + await forAwaitEach(((result: any): AsyncIterable), value => { results.push(value); }); @@ -381,10 +377,9 @@ describe('Execute: handles directives', () => { } `); expect(isAsyncIterable(result)).to.equal(true); - const resultsIterator = createAsyncIterator(result); const results = []; - await forAwaitEach(resultsIterator, value => { + await forAwaitEach(((result: any): AsyncIterable), value => { results.push(value); }); @@ -429,10 +424,9 @@ describe('Execute: handles directives', () => { } `); expect(isAsyncIterable(result)).to.equal(true); - const resultsIterator = createAsyncIterator(result); const results = []; - await forAwaitEach(resultsIterator, value => { + await forAwaitEach(((result: any): AsyncIterable), value => { results.push(value); }); @@ -465,10 +459,9 @@ describe('Execute: handles directives', () => { } `); expect(isAsyncIterable(result)).to.equal(true); - const resultsIterator = createAsyncIterator(result); const results = []; - await forAwaitEach(resultsIterator, value => { + await forAwaitEach(((result: any): AsyncIterable), value => { results.push(value); }); @@ -504,10 +497,9 @@ describe('Execute: handles directives', () => { } `); expect(isAsyncIterable(result)).to.equal(true); - const resultsIterator = createAsyncIterator(result); const results = []; - await forAwaitEach(resultsIterator, value => { + await forAwaitEach(((result: any): AsyncIterable), value => { results.push(value); }); diff --git a/src/execution/execute.js b/src/execution/execute.js index 8316a8dd1d..bc629d1ab0 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -63,6 +63,8 @@ import { import { typeFromAST } from '../utilities/typeFromAST'; import { getOperationRootType } from '../utilities/getOperationRootType'; +import objectValues from '../polyfills/objectValues'; + import { getVariableValues, getArgumentValues, @@ -139,19 +141,20 @@ export type ExecutionArgs = {| |}; /** - * Helper class that allows us to dispatch patches dynamically, and obtain an - * AsyncIterable that yields each patch in the order that they get resolved. + * ResultResolver class that allows us to create the result of the execution: + * AsyncIterable > when there are deferred results + * PromiseOrValue for standard executions */ class ResultResolver { executionResults: Array>; - deferResults: { - path: Path | void, + deferResults: ObjMap<{| + path: Array, label: string, - resolvers: Array<{ + resolvers: Array<{| responseName: string, resolver: () => PromiseOrValue, - }>, - }; + |}>, + |}>; constructor() { this.executionResults = []; @@ -176,13 +179,13 @@ class ResultResolver { } addDeferResults(exeContext: ExecutionContext) { - const deferResultsArray = Object.values(this.deferResults); + const deferResultsArray = objectValues(this.deferResults); this.deferResults = Object.create(null); // avoid multiple resolve for same responseName in same path const resultsResolver = Object.create({}); for (const deferResult of deferResultsArray) { - const resolveResult = () => { + const resolveResult = (): Promise => { const { label, path, resolvers } = deferResult; const results = Object.create({}); let containsPromise = false; @@ -203,8 +206,8 @@ class ResultResolver { // If there are no promises, we can just return the object if (!containsPromise) { - // defer's result must always be a Promise - return this.buildResponse(exeContext, Promise.resolve(results), { + // deferred result must always be a Promise + return this.buildAsyncResponse(exeContext, Promise.resolve(results), { label, path, }); @@ -212,7 +215,7 @@ class ResultResolver { // 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 this.buildResponse(exeContext, promiseForObject(results), { + return this.buildAsyncResponse(exeContext, promiseForObject(results), { label, path, }); @@ -222,6 +225,32 @@ class ResultResolver { } } + /** + * Given a completed execution context, data and iterable object , build the { value, done } + * response defined by the "AsyncIterable" + */ + buildAsyncResponse( + exeContext: ExecutionContext, + data: Promise | null>, + iterable?: {| + label?: string, + path?: Array, + |}, + ): Promise { + return data.then(resolved => { + const value = + exeContext.errors.length === 0 + ? { data: resolved, ...iterable } + : { errors: exeContext.errors, data: resolved, ...iterable }; + // defer results are added only after the parent's response has been resolved + this.addDeferResults(exeContext); + return { + value, + done: false, + }; + }); + } + /** * Given a completed execution context and data, build the { errors, data } * response defined by the "Response" section of the GraphQL specification. @@ -229,27 +258,9 @@ class ResultResolver { buildResponse( exeContext: ExecutionContext, data: PromiseOrValue | null>, - iterable?: { - label?: string, - path?: Array, - }, - ): PromiseOrValue | Promise { + ): PromiseOrValue { if (isPromise(data)) { - return data.then(resolved => { - const value = - exeContext.errors.length === 0 - ? { data: resolved, ...iterable } - : { errors: exeContext.errors, data: resolved, ...iterable }; - if (iterable) { - // defer results are added only after the parent's response has been resolved - this.addDeferResults(exeContext); - return { - value, - done: false, - }; - } - return value; - }); + return data.then(resolved => this.buildResponse(exeContext, resolved)); } return exeContext.errors.length === 0 ? { data } @@ -263,9 +274,11 @@ class ResultResolver { if (!Object.keys(this.deferResults).length) { return this.buildResponse(exeContext, data); } - // defer's result must always be a Promise + // deferred result must always be a Promise const promiseData = isPromise(data) ? data : Promise.resolve(data); - this.executionResults.push(this.buildResponse(exeContext, promiseData, {})); + this.executionResults.push( + this.buildAsyncResponse(exeContext, promiseData), + ); return this.getAsyncIterable(); } @@ -570,11 +583,12 @@ function executeFieldsSerially( function getDeferredInfo(exeContext, fieldNodes) { let every = true; - const deferLabels = []; + const deferLabels: Array = []; for (const node of fieldNodes) { const lastDefer = getDeferDirectiveValues(exeContext, node); every = every && lastDefer; if (lastDefer && !deferLabels.includes(lastDefer.label)) { + // $FlowFixMe(>=0.90.0) deferLabels.push(lastDefer.label); } } @@ -666,6 +680,7 @@ export function collectFields( if (deferDirective && !isFieldDefer) { */ if (deferDirective) { + // $FlowFixMe(>=0.90.0) selection.directives.push(deferDirective); } fields[name].push(selection); @@ -764,16 +779,15 @@ function shouldDeferNode( function getDeferDirectiveValues( exeContext: ExecutionContext, node: FragmentSpreadNode | FieldNode, -): null | { [argument: string]: mixed, ... } { +): void | { [argument: string]: mixed, ... } { const defer = getDirectiveValues( GraphQLDeferDirective, node, exeContext.variableValues, ); - if (!defer || defer.if === false) { - return null; + if (defer && defer.if !== false) { + return defer; } - return defer; } /** diff --git a/src/graphql.js b/src/graphql.js index a49f032166..273c641f4b 100644 --- a/src/graphql.js +++ b/src/graphql.js @@ -66,7 +66,10 @@ export type GraphQLArgs = {| fieldResolver?: ?GraphQLFieldResolver, typeResolver?: ?GraphQLTypeResolver, |}; -declare function graphql(GraphQLArgs, ..._: []): Promise; +declare function graphql( + GraphQLArgs, + ..._: [] +): AsyncIterable> | PromiseOrValue; /* eslint-disable no-redeclare */ declare function graphql( schema: GraphQLSchema, @@ -77,7 +80,7 @@ declare function graphql( operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, typeResolver?: ?GraphQLTypeResolver, -): Promise; +): AsyncIterable> | PromiseOrValue; export function graphql( argsOrSchema, source, @@ -161,7 +164,9 @@ export function graphqlSync( return result; } -function graphqlImpl(args: GraphQLArgs): PromiseOrValue { +function graphqlImpl( + args: GraphQLArgs, +): AsyncIterable> | PromiseOrValue { const { schema, source, diff --git a/src/utilities/introspectionFromSchema.js b/src/utilities/introspectionFromSchema.js index b3ff3d8e60..a1c340a3b3 100644 --- a/src/utilities/introspectionFromSchema.js +++ b/src/utilities/introspectionFromSchema.js @@ -28,6 +28,6 @@ export function introspectionFromSchema( ): IntrospectionQuery { const document = parse(getIntrospectionQuery(options)); const result = execute({ schema, document }); - invariant(!isPromise(result) && !result.errors && result.data); + invariant(!isPromise(result) && result.errors == null && result.data != null); return (result.data: any); } From d63438c1b8f45d926b282c7ad27199c386373c7b Mon Sep 17 00:00:00 2001 From: morrys Date: Mon, 30 Dec 2019 20:08:12 +0100 Subject: [PATCH 07/10] remove not necessary code --- src/execution/execute.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index bc629d1ab0..db00f9df1c 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -295,9 +295,6 @@ class ResultResolver { }) ); }, - [$$asyncIterator]() { - return this; - }, }; }, }: any); From 40de590e09cb22f44d4758afebfff5024155a75e Mon Sep 17 00:00:00 2001 From: morrys Date: Tue, 31 Dec 2019 12:23:49 +0100 Subject: [PATCH 08/10] feat added support defer for inline fragments --- src/execution/__tests__/directives-test.js | 97 ++++++++++++++++++- src/execution/execute.js | 8 +- src/type/__tests__/introspection-test.js | 2 +- src/type/directives.js | 5 +- src/utilities/__tests__/schemaPrinter-test.js | 4 +- 5 files changed, 108 insertions(+), 8 deletions(-) diff --git a/src/execution/__tests__/directives-test.js b/src/execution/__tests__/directives-test.js index 75f0a8510f..3e41afb920 100644 --- a/src/execution/__tests__/directives-test.js +++ b/src/execution/__tests__/directives-test.js @@ -250,7 +250,7 @@ describe('Execute: handles directives', () => { data: { a: 'a', b: 'b' }, }); }); - describe('defer defer fragment spread with DataType', () => { + describe('defer fragment spread with DataType', () => { it('without defer', async () => { const result = executeTestQuery(` query { @@ -583,6 +583,101 @@ describe('Execute: handles directives', () => { data: { a: 'a' }, }); }); + + describe('defer on inline fragment', () => { + it('without if', async () => { + const result = executeTestQuery(` + query { + a + ... on TestType @defer(label: "Frag_b_defer") { + b + } + } + `); + expect(isAsyncIterable(result)).to.equal(true); + const results = []; + await forAwaitEach(((result: any): AsyncIterable), value => { + results.push(value); + }); + + expect(results.length).to.equal(2); + expect(results[0]).to.deep.equal({ + data: { a: 'a', b: null }, + }); + expect(results[1]).to.deep.equal({ + data: { b: 'b' }, + label: 'Frag_b_defer', + path: [], + }); + }); + + it('if true', async () => { + const result = executeTestQuery(` + query { + a + ... on TestType @defer(label: "Frag_b_defer") { + b + } + } + `); + expect(isAsyncIterable(result)).to.equal(true); + + const results = []; + await forAwaitEach(((result: any): AsyncIterable), value => { + results.push(value); + }); + + expect(results.length).to.equal(2); + expect(results[0]).to.deep.equal({ + data: { a: 'a', b: null }, + }); + expect(results[1]).to.deep.equal({ + data: { b: 'b' }, + label: 'Frag_b_defer', + path: [], + }); + }); + + it('if true DataType', async () => { + const result = executeTestQuery(` + query { + a + ... on TestType @defer(if: true, label: "Frag_c_defer") { + c { + ... on DataType @defer(if: true, label: "FragData_d_defer") { + d + } + } + } + } + `); + expect(isAsyncIterable(result)).to.equal(true); + + const results = []; + await forAwaitEach(((result: any): AsyncIterable), value => { + results.push(value); + }); + + expect(results.length).to.equal(3); + expect(results[0]).to.deep.equal({ + data: { a: 'a', c: null }, + }); + expect(results[1]).to.deep.equal({ + data: { + c: { + d: null, + }, + }, + label: 'Frag_c_defer', + path: [], + }); + expect(results[2]).to.deep.equal({ + data: { d: 'd' }, + label: 'FragData_d_defer', + path: ['c'], + }); + }); + }); }); describe('works on anonymous inline fragment', () => { diff --git a/src/execution/execute.js b/src/execution/execute.js index db00f9df1c..6e14a39d75 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -690,13 +690,15 @@ export function collectFields( ) { continue; } + const fragmentDeferDirective = + shouldDeferNode(exeContext, selection) || deferDirective; collectFields( exeContext, runtimeType, selection.selectionSet, fields, visitedFragmentNames, - deferDirective, + fragmentDeferDirective, ); break; } @@ -767,7 +769,7 @@ function shouldIncludeNode( */ function shouldDeferNode( exeContext: ExecutionContext, - node: FragmentSpreadNode, + node: FragmentSpreadNode | InlineFragmentNode, ): DirectiveNode | void { const shouldDefer = getDeferDirectiveValues(exeContext, node); return shouldDefer && getDirective(GraphQLDeferDirective, node); @@ -775,7 +777,7 @@ function shouldDeferNode( function getDeferDirectiveValues( exeContext: ExecutionContext, - node: FragmentSpreadNode | FieldNode, + node: FragmentSpreadNode | InlineFragmentNode | FieldNode, ): void | { [argument: string]: mixed, ... } { const defer = getDirectiveValues( GraphQLDeferDirective, diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index f4afadff5f..e93c9d67ac 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -847,7 +847,7 @@ describe('Introspection', () => { }, { name: 'defer', - locations: ['FRAGMENT_SPREAD'], + locations: ['FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], args: [ { defaultValue: null, diff --git a/src/type/directives.js b/src/type/directives.js index d3894a9f60..0bae9b5228 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -174,7 +174,10 @@ export const GraphQLDeferDirective = new GraphQLDirective({ name: 'defer', description: 'Directs the executor to defer fragment when the `if` argument is true.', - locations: [DirectiveLocation.FRAGMENT_SPREAD], + locations: [ + DirectiveLocation.FRAGMENT_SPREAD, + DirectiveLocation.INLINE_FRAGMENT, + ], args: { if: { type: GraphQLBoolean, diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index 839799bc84..474156d798 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -597,7 +597,7 @@ describe('Type System Printer', () => { """label""" label: String! - ) on FRAGMENT_SPREAD + ) on FRAGMENT_SPREAD | INLINE_FRAGMENT """Marks an element of a GraphQL schema as no longer supported.""" directive @deprecated( @@ -819,7 +819,7 @@ describe('Type System Printer', () => { # label label: String! - ) on FRAGMENT_SPREAD + ) on FRAGMENT_SPREAD | INLINE_FRAGMENT # Marks an element of a GraphQL schema as no longer supported. directive @deprecated( From 892c5f641082a1510c96bd24a40df79a2e29c719 Mon Sep 17 00:00:00 2001 From: morrys Date: Sun, 5 Jan 2020 11:00:46 +0100 Subject: [PATCH 09/10] added some optimizations and test performances --- .DS_Store | Bin 0 -> 6148 bytes package.json | 1 + .../__tests__/directives-perf-test.js | 80 +++++++++++ src/execution/__tests__/directives-test.js | 125 ++++++++++++------ src/execution/execute.js | 105 ++++++++------- src/graphql.js | 6 +- src/subscription/subscribe.js | 5 +- 7 files changed, 230 insertions(+), 92 deletions(-) create mode 100644 .DS_Store create mode 100644 src/execution/__tests__/directives-perf-test.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9498e50bca478ef0f2ba7e5825a2b5c01a58d1cb GIT binary patch literal 6148 zcmeH~F^&@<^#q<|Dyh64V5D0F8{w$Au;FvJKz4lIXp9kT>kyg=4u>tuyyIXzgm zT8trHk9M-;bv4;Kdpj(L56e57Pcby>?XbdxW;LK71*E`CfmP2(KmYgizvlm0i&7~d z1>Q^n8+M1?mM@iO>&xqT{g_o>H#!-YGd%qSF!7^!Ll5JA@da6vt& { + const index = 100; + let fragString = ''; + let fragElementString = ''; + for (const i of Array.from(Array(index).keys())) { + fragString += ` + ...Frag${i} @defer(label: \"Frag_b_defer${i}\") + `; + fragElementString += ` + fragment Frag${i} on TestType { + b + } + `; + } + const result = await executeTestQuery(` + query { + a + ${fragString} + } + ${fragElementString} + `); + expect(isAsyncIterable(result)).to.equal(true); + const results = []; + await forAwaitEach(((result: any): AsyncIterable), value => { + results.push(value); + }); + + expect(results.length).to.equal(index + 1); + expect(results[0]).to.deep.equal({ + data: { a: 'a' }, + }); + for (const i of Array.from(Array(index).keys())) { + expect(results[i + 1]).to.deep.equal({ + data: { b: 'b1' }, + label: `Frag_b_defer${i}`, + path: [], + }); + } +}); diff --git a/src/execution/__tests__/directives-test.js b/src/execution/__tests__/directives-test.js index 3e41afb920..1fc889866f 100644 --- a/src/execution/__tests__/directives-test.js +++ b/src/execution/__tests__/directives-test.js @@ -22,13 +22,19 @@ class Data { } } +function delay(t, v) { + return new Promise(function(resolve) { + setTimeout(resolve.bind(null, v), t); + }); +} + const DataType = new GraphQLObjectType({ name: 'DataType', fields: { d: { type: GraphQLString, resolve(obj) { - return Promise.resolve(obj.d); + return delay(5).then(() => obj.d); }, }, e: { @@ -59,7 +65,7 @@ const rootValue = { return 'b'; }, c() { - return new Data('d'); + return Promise.resolve(new Data('d')); }, }; @@ -180,7 +186,7 @@ describe('Execute: handles directives', () => { describe('defer fragment spread', () => { it('without if', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(label: "Frag_b_defer") @@ -197,7 +203,7 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(2); expect(results[0]).to.deep.equal({ - data: { a: 'a', b: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { b: 'b' }, @@ -207,7 +213,7 @@ describe('Execute: handles directives', () => { }); it('if true', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: true, label: "Frag_b_defer") @@ -225,7 +231,7 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(2); expect(results[0]).to.deep.equal({ - data: { a: 'a', b: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { b: 'b' }, @@ -235,7 +241,7 @@ describe('Execute: handles directives', () => { }); it('if false', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: false, label: "Frag_b_defer") @@ -252,7 +258,7 @@ describe('Execute: handles directives', () => { }); describe('defer fragment spread with DataType', () => { it('without defer', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @@ -279,7 +285,7 @@ describe('Execute: handles directives', () => { }); it('if false not defer', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: false, label: "Frag_c_defer") @@ -305,7 +311,7 @@ describe('Execute: handles directives', () => { }); }); it('two fragments with two field equals', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: true, label: "Frag_c_defer") @@ -333,14 +339,11 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(4); expect(results[0]).to.deep.equal({ - data: { a: 'a', c: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { - c: { - d: null, - e: null, - }, + c: {}, }, label: 'Frag_c_defer', path: [], @@ -357,8 +360,58 @@ describe('Execute: handles directives', () => { }); }); + it('race condition', async () => { + const result = await executeTestQuery(` + query { + a + ...Frag @defer(if: true, label: "Frag_c_defer") + } + fragment Frag on TestType { + c { + ...FragData @defer(if: true, label: "FragData_d_defer") + ...FragDeData @defer(if: true, label: "FragDeData_de_defer") + } + } + fragment FragData on DataType { + d + } + fragment FragDeData on DataType { + e + } + `); + expect(isAsyncIterable(result)).to.equal(true); + + const results = []; + await forAwaitEach(((result: any): AsyncIterable), value => { + results.push(value); + }); + + expect(results.length).to.equal(4); + expect(results[0]).to.deep.equal({ + data: { a: 'a' }, + }); + expect(results[1]).to.deep.equal({ + data: { + c: {}, + }, + label: 'Frag_c_defer', + path: [], + }); + + expect(results[2]).to.deep.equal({ + data: { e: 'e' }, + label: 'FragDeData_de_defer', + path: ['c'], + }); + expect(results[3]).to.deep.equal({ + data: { d: 'd' }, + label: 'FragData_d_defer', + path: ['c'], + }); + }); + it('with two equals fragments', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: true, label: "Frag_c_defer") @@ -385,13 +438,11 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(4); expect(results[0]).to.deep.equal({ - data: { a: 'a', c: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { - c: { - d: null, - }, + c: {}, }, label: 'Frag_c_defer', path: [], @@ -409,7 +460,7 @@ describe('Execute: handles directives', () => { }); it('mixed if true & if false', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: true, label: "Frag_c_defer") @@ -432,7 +483,7 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(2); expect(results[0]).to.deep.equal({ - data: { a: 'a', c: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { @@ -444,7 +495,7 @@ describe('Execute: handles directives', () => { }); it('mixed if false & if true', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: false, label: "Frag_c_defer") @@ -469,9 +520,7 @@ describe('Execute: handles directives', () => { expect(results[0]).to.deep.equal({ data: { a: 'a', - c: { - d: null, - }, + c: {}, }, }); expect(results[1]).to.deep.equal({ @@ -482,7 +531,7 @@ describe('Execute: handles directives', () => { }); it('if true', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ...Frag @defer(if: true, label: "Frag_c_defer") @@ -505,13 +554,11 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(3); expect(results[0]).to.deep.equal({ - data: { a: 'a', c: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { - c: { - d: null, - }, + c: {}, }, label: 'Frag_c_defer', path: [], @@ -586,7 +633,7 @@ describe('Execute: handles directives', () => { describe('defer on inline fragment', () => { it('without if', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ... on TestType @defer(label: "Frag_b_defer") { @@ -602,7 +649,7 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(2); expect(results[0]).to.deep.equal({ - data: { a: 'a', b: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { b: 'b' }, @@ -612,7 +659,7 @@ describe('Execute: handles directives', () => { }); it('if true', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ... on TestType @defer(label: "Frag_b_defer") { @@ -629,7 +676,7 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(2); expect(results[0]).to.deep.equal({ - data: { a: 'a', b: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { b: 'b' }, @@ -639,7 +686,7 @@ describe('Execute: handles directives', () => { }); it('if true DataType', async () => { - const result = executeTestQuery(` + const result = await executeTestQuery(` query { a ... on TestType @defer(if: true, label: "Frag_c_defer") { @@ -660,13 +707,11 @@ describe('Execute: handles directives', () => { expect(results.length).to.equal(3); expect(results[0]).to.deep.equal({ - data: { a: 'a', c: null }, + data: { a: 'a' }, }); expect(results[1]).to.deep.equal({ data: { - c: { - d: null, - }, + c: {}, }, label: 'Frag_c_defer', path: [], diff --git a/src/execution/execute.js b/src/execution/execute.js index 6e14a39d75..7843af295d 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -64,6 +64,7 @@ import { typeFromAST } from '../utilities/typeFromAST'; import { getOperationRootType } from '../utilities/getOperationRootType'; import objectValues from '../polyfills/objectValues'; +import objectEntries from '../polyfills/objectEntries'; import { getVariableValues, @@ -146,7 +147,8 @@ export type ExecutionArgs = {| * PromiseOrValue for standard executions */ class ResultResolver { - executionResults: Array>; + initialResult: Promise | void; + executionResults: ObjMap>; deferResults: ObjMap<{| path: Array, label: string, @@ -157,11 +159,11 @@ class ResultResolver { |}>; constructor() { - this.executionResults = []; + this.executionResults = Object.create(null); this.deferResults = Object.create(null); } - addDeferResolver( + addDeferredResult( path: Path | void, label: string, responseName: string, @@ -178,15 +180,15 @@ class ResultResolver { this.deferResults[keyDeferResult] = deferResult; } - addDeferResults(exeContext: ExecutionContext) { - const deferResultsArray = objectValues(this.deferResults); + resolveResults(exeContext: ExecutionContext) { + const deferResultsArray = objectEntries(this.deferResults); this.deferResults = Object.create(null); // avoid multiple resolve for same responseName in same path const resultsResolver = Object.create({}); - for (const deferResult of deferResultsArray) { - const resolveResult = (): Promise => { - const { label, path, resolvers } = deferResult; + for (const [keyDeferResult, deferResult] of deferResultsArray) { + const { label, path, resolvers } = deferResult; + const resolve = () => { const results = Object.create({}); let containsPromise = false; resolvers.forEach(({ resolver, responseName }) => { @@ -207,21 +209,22 @@ class ResultResolver { // If there are no promises, we can just return the object if (!containsPromise) { // deferred result must always be a Promise - return this.buildAsyncResponse(exeContext, Promise.resolve(results), { - label, - path, - }); + 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 this.buildAsyncResponse(exeContext, promiseForObject(results), { - label, - path, - }); + return promiseForObject(results); }; - this.executionResults.push(resolveResult()); + this.executionResults[keyDeferResult] = this.buildAsyncResponse( + exeContext, + resolve(), + { + label, + path, + }, + ); } } @@ -231,19 +234,20 @@ class ResultResolver { */ buildAsyncResponse( exeContext: ExecutionContext, - data: Promise | null>, + data: PromiseOrValue | null>, iterable?: {| label?: string, path?: Array, |}, ): Promise { - return data.then(resolved => { + const result = isPromise(data) ? data : Promise.resolve(data); + return result.then(resolved => { const value = exeContext.errors.length === 0 ? { data: resolved, ...iterable } : { errors: exeContext.errors, data: resolved, ...iterable }; // defer results are added only after the parent's response has been resolved - this.addDeferResults(exeContext); + this.resolveResults(exeContext); return { value, done: false, @@ -258,27 +262,16 @@ class ResultResolver { buildResponse( exeContext: ExecutionContext, data: PromiseOrValue | null>, - ): PromiseOrValue { + ): PromiseOrValue> | ExecutionResult> { if (isPromise(data)) { return data.then(resolved => this.buildResponse(exeContext, resolved)); } - return exeContext.errors.length === 0 - ? { data } - : { errors: exeContext.errors, data }; - } - - getResult( - exeContext: ExecutionContext, - data: PromiseOrValue | null>, - ): AsyncIterable> | PromiseOrValue { if (!Object.keys(this.deferResults).length) { - return this.buildResponse(exeContext, data); + return exeContext.errors.length === 0 + ? { data } + : { errors: exeContext.errors, data }; } - // deferred result must always be a Promise - const promiseData = isPromise(data) ? data : Promise.resolve(data); - this.executionResults.push( - this.buildAsyncResponse(exeContext, promiseData), - ); + this.initialResult = this.buildAsyncResponse(exeContext, data); return this.getAsyncIterable(); } @@ -288,12 +281,28 @@ class ResultResolver { [$$asyncIterator]() { return { next() { - return ( - self.executionResults.shift() || - Promise.resolve({ - done: true, - }) - ); + if (self.initialResult) { + return ( + self.initialResult && + self.initialResult.then(value => { + self.initialResult = undefined; + return value; + }) + ); + } + const promises = objectValues(self.executionResults); + if (promises.length === 0) { + return Promise.resolve({ value: undefined, done: true }); + } + return Promise.race(promises) + .then(r => r) + .then(response => { + const { path, label } = response.value; + if (path) { + delete self.executionResults[path.toString() + label]; + } + return response; + }); }, }; }, @@ -316,7 +325,7 @@ class ResultResolver { declare function execute( ExecutionArgs, ..._: [] -): AsyncIterable> | PromiseOrValue; +): PromiseOrValue> | ExecutionResult>; /* eslint-disable no-redeclare */ declare function execute( schema: GraphQLSchema, @@ -327,7 +336,7 @@ declare function execute( operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, typeResolver?: ?GraphQLTypeResolver, -): AsyncIterable> | PromiseOrValue; +): PromiseOrValue> | ExecutionResult>; export function execute( argsOrSchema, document, @@ -356,7 +365,7 @@ export function execute( function executeImpl( args: ExecutionArgs, -): AsyncIterable> | PromiseOrValue { +): PromiseOrValue> | ExecutionResult> { const { schema, document, @@ -397,7 +406,7 @@ function executeImpl( // be executed. An execution which encounters errors will still result in a // resolved Promise. const data = executeOperation(exeContext, exeContext.operation, rootValue); - return exeContext.resultResolver.getResult(exeContext, data); + return exeContext.resultResolver.buildResponse(exeContext, data); } /** @@ -613,10 +622,10 @@ function executeFields( const resolve = () => resolveField(exeContext, parentType, sourceValue, fieldNodes, fieldPath); - const result = every ? null : resolve(); + const result = every ? undefined : resolve(); for (const label of deferLabels) { - exeContext.resultResolver.addDeferResolver( + exeContext.resultResolver.addDeferredResult( path, label, responseName, diff --git a/src/graphql.js b/src/graphql.js index 273c641f4b..c13fb2b82b 100644 --- a/src/graphql.js +++ b/src/graphql.js @@ -69,7 +69,7 @@ export type GraphQLArgs = {| declare function graphql( GraphQLArgs, ..._: [] -): AsyncIterable> | PromiseOrValue; +): PromiseOrValue> | ExecutionResult>; /* eslint-disable no-redeclare */ declare function graphql( schema: GraphQLSchema, @@ -80,7 +80,7 @@ declare function graphql( operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, typeResolver?: ?GraphQLTypeResolver, -): AsyncIterable> | PromiseOrValue; +): PromiseOrValue> | ExecutionResult>; export function graphql( argsOrSchema, source, @@ -166,7 +166,7 @@ export function graphqlSync( function graphqlImpl( args: GraphQLArgs, -): AsyncIterable> | PromiseOrValue { +): PromiseOrValue> | ExecutionResult> { const { schema, source, diff --git a/src/subscription/subscribe.js b/src/subscription/subscribe.js index ac90c3e1c5..12667afee1 100644 --- a/src/subscription/subscribe.js +++ b/src/subscription/subscribe.js @@ -4,6 +4,7 @@ import { isAsyncIterable } from 'iterall'; import inspect from '../jsutils/inspect'; import { addPath, pathToArray } from '../jsutils/Path'; +import { type PromiseOrValue } from '../jsutils/PromiseOrValue'; import { GraphQLError } from '../error/GraphQLError'; import { locatedError } from '../error/locatedError'; @@ -161,7 +162,9 @@ function subscribeImpl( isAsyncIterable(resultOrStream) ? mapAsyncIterator( ((resultOrStream: any): AsyncIterable), - mapSourceToResponse, + ((mapSourceToResponse: any): ( + payload: any, + ) => PromiseOrValue), reportGraphQLError, ) : ((resultOrStream: any): ExecutionResult), From 6f28a7b11a4c9845c78ebc48326d93fec44d4f1d Mon Sep 17 00:00:00 2001 From: morrys Date: Sun, 5 Jan 2020 11:18:29 +0100 Subject: [PATCH 10/10] fix ci --- package.json | 1 - src/execution/__tests__/directives-perf-test.js | 6 +++--- src/execution/__tests__/directives-test.js | 4 +--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 6225d5b323..a503e9fa35 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@babel/register": "7.6.2", "babel-eslint": "10.0.3", "chai": "4.2.0", - "chai-spies": "1.0.0", "cspell": "4.0.43", "dtslint": "2.0.2", "eslint": "6.8.0", diff --git a/src/execution/__tests__/directives-perf-test.js b/src/execution/__tests__/directives-perf-test.js index dc59160469..669b7fb2dc 100644 --- a/src/execution/__tests__/directives-perf-test.js +++ b/src/execution/__tests__/directives-perf-test.js @@ -1,7 +1,7 @@ // @flow strict import { expect } from 'chai'; -import { describe, it } from 'mocha'; +import { it } from 'mocha'; import { parse } from '../../language/parser'; @@ -39,13 +39,13 @@ function executeTestQuery(query) { return execute({ schema, document, rootValue }); } -it('perfomance test, if the same field is deferred several times, its resolve is called only once', async () => { +it('performance test, if the same field is deferred several times, its resolve is called only once', async () => { const index = 100; let fragString = ''; let fragElementString = ''; for (const i of Array.from(Array(index).keys())) { fragString += ` - ...Frag${i} @defer(label: \"Frag_b_defer${i}\") + ...Frag${i} @defer(label: "Frag_b_defer${i}") `; fragElementString += ` fragment Frag${i} on TestType { diff --git a/src/execution/__tests__/directives-test.js b/src/execution/__tests__/directives-test.js index 1fc889866f..96f6f74453 100644 --- a/src/execution/__tests__/directives-test.js +++ b/src/execution/__tests__/directives-test.js @@ -23,9 +23,7 @@ class Data { } function delay(t, v) { - return new Promise(function(resolve) { - setTimeout(resolve.bind(null, v), t); - }); + return new Promise(resolve => setTimeout(resolve.bind(null, v), t)); } const DataType = new GraphQLObjectType({