From 88c7455d8567e4f289fa0b6b5e1f4ceb35d75271 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 08:42:58 -0800 Subject: [PATCH 01/94] Extract Execution context building to a separate modules so that ExecutionContext type can be exported, enabling plan building to be a separate module. --- src/execution/context.js | 88 ++++++++++++++++++++++++++++++++++++++++ src/execution/execute.js | 73 +-------------------------------- 2 files changed, 90 insertions(+), 71 deletions(-) create mode 100644 src/execution/context.js diff --git a/src/execution/context.js b/src/execution/context.js new file mode 100644 index 0000000000..3b27699c58 --- /dev/null +++ b/src/execution/context.js @@ -0,0 +1,88 @@ +/* @flow */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +import { GraphQLError } from '../error'; +import { GraphQLSchema } from '../type/schema'; +import type { + OperationDefinition, + Document, + FragmentDefinition +} from '../language/ast'; +import { getVariableValues } from './values'; +import { Kind } from '../language'; + +/** + * Data that must be available at all points during query execution. + * + * Namely, schema of the type system that is currently executing, + * and the fragments defined in the query document + */ +export type ExecutionContext = { + schema: GraphQLSchema; + fragments: {[key: string]: FragmentDefinition}; + rootValue: mixed; + operation: OperationDefinition; + variableValues: {[key: string]: mixed}; + errors: Array; +} + +/** + * Constructs a ExecutionContext object from the arguments passed to + * execute, which we will pass throughout the other execution methods. + * + * Throws a GraphQLError if a valid execution context cannot be created. + */ +export function buildExecutionContext( + schema: GraphQLSchema, + documentAST: Document, + rootValue: mixed, + rawVariableValues: ?{[key: string]: mixed}, + operationName: ?string +): ExecutionContext { + const errors: Array = []; + let operation: ?OperationDefinition; + const fragments: {[name: string]: FragmentDefinition} = Object.create(null); + documentAST.definitions.forEach(definition => { + switch (definition.kind) { + case Kind.OPERATION_DEFINITION: + if (!operationName && operation) { + throw new GraphQLError( + 'Must provide operation name if query contains multiple operations.' + ); + } + if (!operationName || + definition.name && definition.name.value === operationName) { + operation = definition; + } + break; + case Kind.FRAGMENT_DEFINITION: + fragments[definition.name.value] = definition; + break; + default: throw new GraphQLError( + `GraphQL cannot execute a request containing a ${definition.kind}.`, + definition + ); + } + }); + if (!operation) { + if (!operationName) { + throw new GraphQLError(`Unknown operation named "${operationName}".`); + } else { + throw new GraphQLError('Must provide an operation.'); + } + } + const variableValues = getVariableValues( + schema, + operation.variableDefinitions || [], + rawVariableValues || {} + ); + const exeContext: ExecutionContext = + { schema, fragments, rootValue, operation, variableValues, errors }; + return exeContext; +} diff --git a/src/execution/execute.js b/src/execution/execute.js index 3fcc86d7a3..3209fb3210 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -14,7 +14,7 @@ import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; import { typeFromAST } from '../utilities/typeFromAST'; import { Kind } from '../language'; -import { getVariableValues, getArgumentValues } from './values'; +import { getArgumentValues } from './values'; import { GraphQLScalarType, GraphQLObjectType, @@ -48,6 +48,7 @@ import type { InlineFragment, FragmentDefinition } from '../language/ast'; +import { ExecutionContext, buildExecutionContext } from './context'; /** @@ -70,21 +71,6 @@ import type { * 3) inline fragment "spreads" e.g. "...on Type { a }" */ -/** - * Data that must be available at all points during query execution. - * - * Namely, schema of the type system that is currently executing, - * and the fragments defined in the query document - */ -type ExecutionContext = { - schema: GraphQLSchema; - fragments: {[key: string]: FragmentDefinition}; - rootValue: mixed; - operation: OperationDefinition; - variableValues: {[key: string]: mixed}; - errors: Array; -} - /** * The result of execution. `data` is the result of executing the * query, `errors` is null if no errors occurred, and is a @@ -150,61 +136,6 @@ export function execute( }); } -/** - * Constructs a ExecutionContext object from the arguments passed to - * execute, which we will pass throughout the other execution methods. - * - * Throws a GraphQLError if a valid execution context cannot be created. - */ -function buildExecutionContext( - schema: GraphQLSchema, - documentAST: Document, - rootValue: mixed, - rawVariableValues: ?{[key: string]: mixed}, - operationName: ?string -): ExecutionContext { - const errors: Array = []; - let operation: ?OperationDefinition; - const fragments: {[name: string]: FragmentDefinition} = Object.create(null); - documentAST.definitions.forEach(definition => { - switch (definition.kind) { - case Kind.OPERATION_DEFINITION: - if (!operationName && operation) { - throw new GraphQLError( - 'Must provide operation name if query contains multiple operations.' - ); - } - if (!operationName || - definition.name && definition.name.value === operationName) { - operation = definition; - } - break; - case Kind.FRAGMENT_DEFINITION: - fragments[definition.name.value] = definition; - break; - default: throw new GraphQLError( - `GraphQL cannot execute a request containing a ${definition.kind}.`, - definition - ); - } - }); - if (!operation) { - if (!operationName) { - throw new GraphQLError(`Unknown operation named "${operationName}".`); - } else { - throw new GraphQLError('Must provide an operation.'); - } - } - const variableValues = getVariableValues( - schema, - operation.variableDefinitions || [], - rawVariableValues || {} - ); - const exeContext: ExecutionContext = - { schema, fragments, rootValue, operation, variableValues, errors }; - return exeContext; -} - /** * Implements the "Evaluating operations" section of the spec. */ From ecf7b8efb92a3c88ed68dc7f36c88c3c10c1b9ff Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 09:17:01 -0800 Subject: [PATCH 02/94] Create a planning module and move some initial functionality there that is only used in the planning phase --- src/execution/execute.js | 152 ++------------------------ src/execution/plan.js | 226 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 145 deletions(-) create mode 100644 src/execution/plan.js diff --git a/src/execution/execute.js b/src/execution/execute.js index 3209fb3210..1794212e0a 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -9,11 +9,8 @@ */ import { GraphQLError, locatedError } from '../error'; -import find from '../jsutils/find'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; -import { typeFromAST } from '../utilities/typeFromAST'; -import { Kind } from '../language'; import { getArgumentValues } from './values'; import { GraphQLScalarType, @@ -35,20 +32,18 @@ import { TypeMetaFieldDef, TypeNameMetaFieldDef } from '../type/introspection'; -import { - GraphQLIncludeDirective, - GraphQLSkipDirective -} from '../type/directives'; import type { - Directive, Document, OperationDefinition, - SelectionSet, Field, - InlineFragment, - FragmentDefinition } from '../language/ast'; -import { ExecutionContext, buildExecutionContext } from './context'; +import { + ExecutionContext, + buildExecutionContext +} from './context'; +import { + collectFields +} from './plan'; /** @@ -275,132 +270,6 @@ function executeFields( return promiseForObject(finalResults); } -/** - * Given a selectionSet, adds all of the fields in that selection to - * the passed in map of fields, and returns it at the end. - * - * CollectFields requires the "runtime type" of an object. For a field which - * returns and Interface or Union type, the "runtime type" will be the actual - * Object type returned by that field. - */ -function collectFields( - exeContext: ExecutionContext, - runtimeType: GraphQLObjectType, - selectionSet: SelectionSet, - fields: {[key: string]: Array}, - visitedFragmentNames: {[key: string]: boolean} -): {[key: string]: Array} { - for (let i = 0; i < selectionSet.selections.length; i++) { - const selection = selectionSet.selections[i]; - switch (selection.kind) { - case Kind.FIELD: - if (!shouldIncludeNode(exeContext, selection.directives)) { - continue; - } - const name = getFieldEntryKey(selection); - if (!fields[name]) { - fields[name] = []; - } - fields[name].push(selection); - break; - case Kind.INLINE_FRAGMENT: - if (!shouldIncludeNode(exeContext, selection.directives) || - !doesFragmentConditionMatch(exeContext, selection, runtimeType)) { - continue; - } - collectFields( - exeContext, - runtimeType, - selection.selectionSet, - fields, - visitedFragmentNames - ); - break; - case Kind.FRAGMENT_SPREAD: - const fragName = selection.name.value; - if (visitedFragmentNames[fragName] || - !shouldIncludeNode(exeContext, selection.directives)) { - continue; - } - visitedFragmentNames[fragName] = true; - const fragment = exeContext.fragments[fragName]; - if (!fragment || - !shouldIncludeNode(exeContext, fragment.directives) || - !doesFragmentConditionMatch(exeContext, fragment, runtimeType)) { - continue; - } - collectFields( - exeContext, - runtimeType, - fragment.selectionSet, - fields, - visitedFragmentNames - ); - break; - } - } - return fields; -} - -/** - * Determines if a field should be included based on the @include and @skip - * directives, where @skip has higher precidence than @include. - */ -function shouldIncludeNode( - exeContext: ExecutionContext, - directives: ?Array -): boolean { - const skipAST = directives && find( - directives, - directive => directive.name.value === GraphQLSkipDirective.name - ); - if (skipAST) { - const { if: skipIf } = getArgumentValues( - GraphQLSkipDirective.args, - skipAST.arguments, - exeContext.variableValues - ); - return !skipIf; - } - - const includeAST = directives && find( - directives, - directive => directive.name.value === GraphQLIncludeDirective.name - ); - if (includeAST) { - const { if: includeIf } = getArgumentValues( - GraphQLIncludeDirective.args, - includeAST.arguments, - exeContext.variableValues - ); - return Boolean(includeIf); - } - - return true; -} - -/** - * Determines if a fragment is applicable to the given type. - */ -function doesFragmentConditionMatch( - exeContext: ExecutionContext, - fragment: FragmentDefinition | InlineFragment, - type: GraphQLObjectType -): boolean { - const typeConditionAST = fragment.typeCondition; - if (!typeConditionAST) { - return true; - } - const conditionalType = typeFromAST(exeContext.schema, typeConditionAST); - if (conditionalType === type) { - return true; - } - if (isAbstractType(conditionalType)) { - return ((conditionalType: any): GraphQLAbstractType).isPossibleType(type); - } - return false; -} - /** * This function transforms a JS object `{[key: string]: Promise}` into * a `Promise<{[key: string]: T}>` @@ -421,13 +290,6 @@ function promiseForObject( ); } -/** - * Implements the logic to compute the key of a given field’s entry - */ -function getFieldEntryKey(node: Field): string { - return node.alias ? node.alias.value : node.name.value; -} - /** * Resolves the field on the given source object. In particular, this * figures out the value that the field returns by calling its resolve function, diff --git a/src/execution/plan.js b/src/execution/plan.js new file mode 100644 index 0000000000..624aef922b --- /dev/null +++ b/src/execution/plan.js @@ -0,0 +1,226 @@ +/* @flow */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +import { GraphQLError } from '../error'; +import { GraphQLSchema } from '../type/schema'; +import { ExecutionContext } from './context'; +import { Kind } from '../language'; +import find from '../jsutils/find'; +import { getArgumentValues } from './values'; +import { typeFromAST } from '../utilities/typeFromAST'; +import { + GraphQLObjectType, + GraphQLAbstractType, + isAbstractType +} from '../type/definition'; +import { + GraphQLIncludeDirective, + GraphQLSkipDirective +} from '../type/directives'; +import type { + Directive, + OperationDefinition, + SelectionSet, + InlineFragment, + Field, + FragmentDefinition +} from '../language/ast'; + +/** + * Create a plan based on the "Evaluating operations" section of the spec. + */ +export function planOperation( + exeContext: ExecutionContext, + operation: OperationDefinition +): Object { + const type = getOperationRootType(exeContext.schema, operation); + const fields = collectFields( + exeContext, + type, + operation.selectionSet, + Object.create(null), + Object.create(null) + ); + /* + if (operation.operation === 'mutation') { + } else { + } + */ + return fields; // @TODO Fix +} + +/** + * Extracts the root type of the operation from the schema. + */ +function getOperationRootType( + schema: GraphQLSchema, + operation: OperationDefinition +): GraphQLObjectType { + switch (operation.operation) { + case 'query': + return schema.getQueryType(); + case 'mutation': + const mutationType = schema.getMutationType(); + if (!mutationType) { + throw new GraphQLError( + 'Schema is not configured for mutations', + [ operation ] + ); + } + return mutationType; + case 'subscription': + const subscriptionType = schema.getSubscriptionType(); + if (!subscriptionType) { + throw new GraphQLError( + 'Schema is not configured for subscriptions', + [ operation ] + ); + } + return subscriptionType; + default: + throw new GraphQLError( + 'Can only execute queries, mutations and subscriptions', + [ operation ] + ); + } +} + +/** + * Given a selectionSet, adds all of the fields in that selection to + * the passed in map of fields, and returns it at the end. + * + * CollectFields requires the "runtime type" of an object. For a field which + * returns and Interface or Union type, the "runtime type" will be the actual + * Object type returned by that field. + */ +export function collectFields( + exeContext: ExecutionContext, + runtimeType: GraphQLObjectType, + selectionSet: SelectionSet, + fields: {[key: string]: Array}, + visitedFragmentNames: {[key: string]: boolean} +): {[key: string]: Array} { + for (let i = 0; i < selectionSet.selections.length; i++) { + const selection = selectionSet.selections[i]; + switch (selection.kind) { + case Kind.FIELD: + if (!shouldIncludeNode(exeContext, selection.directives)) { + continue; + } + const name = getFieldEntryKey(selection); + if (!fields[name]) { + fields[name] = []; + } + fields[name].push(selection); + break; + case Kind.INLINE_FRAGMENT: + if (!shouldIncludeNode(exeContext, selection.directives) || + !doesFragmentConditionMatch(exeContext, selection, runtimeType)) { + continue; + } + collectFields( + exeContext, + runtimeType, + selection.selectionSet, + fields, + visitedFragmentNames + ); + break; + case Kind.FRAGMENT_SPREAD: + const fragName = selection.name.value; + if (visitedFragmentNames[fragName] || + !shouldIncludeNode(exeContext, selection.directives)) { + continue; + } + visitedFragmentNames[fragName] = true; + const fragment = exeContext.fragments[fragName]; + if (!fragment || + !shouldIncludeNode(exeContext, fragment.directives) || + !doesFragmentConditionMatch(exeContext, fragment, runtimeType)) { + continue; + } + collectFields( + exeContext, + runtimeType, + fragment.selectionSet, + fields, + visitedFragmentNames + ); + break; + } + } + return fields; +} + +/** + * Determines if a field should be included based on the @include and @skip + * directives, where @skip has higher precidence than @include. + */ +function shouldIncludeNode( + exeContext: ExecutionContext, + directives: ?Array +): boolean { + const skipAST = directives && find( + directives, + directive => directive.name.value === GraphQLSkipDirective.name + ); + if (skipAST) { + const { if: skipIf } = getArgumentValues( + GraphQLSkipDirective.args, + skipAST.arguments, + exeContext.variableValues + ); + return !skipIf; + } + + const includeAST = directives && find( + directives, + directive => directive.name.value === GraphQLIncludeDirective.name + ); + if (includeAST) { + const { if: includeIf } = getArgumentValues( + GraphQLIncludeDirective.args, + includeAST.arguments, + exeContext.variableValues + ); + return Boolean(includeIf); + } + + return true; +} + +/** + * Determines if a fragment is applicable to the given type. + */ +function doesFragmentConditionMatch( + exeContext: ExecutionContext, + fragment: FragmentDefinition | InlineFragment, + type: GraphQLObjectType +): boolean { + const typeConditionAST = fragment.typeCondition; + if (!typeConditionAST) { + return true; + } + const conditionalType = typeFromAST(exeContext.schema, typeConditionAST); + if (conditionalType === type) { + return true; + } + if (isAbstractType(conditionalType)) { + return ((conditionalType: any): GraphQLAbstractType).isPossibleType(type); + } + return false; +} + +/** + * Implements the logic to compute the key of a given field’s entry + */ +function getFieldEntryKey(node: Field): string { + return node.alias ? node.alias.value : node.name.value; +} + From 72bccc2b3ddf5369da87f569ff021cc83e84547c Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 09:21:52 -0800 Subject: [PATCH 03/94] Remove accidentally duplicated code; initial call to planOperation --- src/execution/execute.js | 40 ++++------------------------------------ src/execution/plan.js | 2 +- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 1794212e0a..f6b0bbc439 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -42,6 +42,8 @@ import { buildExecutionContext } from './context'; import { + planOperation, + getOperationRootType, collectFields } from './plan'; @@ -108,6 +110,8 @@ export function execute( operationName ); + planOperation(context, context.operation); + // Return a Promise that will eventually resolve to the data described by // The "Response" section of the GraphQL specification. // @@ -154,42 +158,6 @@ function executeOperation( return executeFields(exeContext, type, rootValue, fields); } -/** - * Extracts the root type of the operation from the schema. - */ -function getOperationRootType( - schema: GraphQLSchema, - operation: OperationDefinition -): GraphQLObjectType { - switch (operation.operation) { - case 'query': - return schema.getQueryType(); - case 'mutation': - const mutationType = schema.getMutationType(); - if (!mutationType) { - throw new GraphQLError( - 'Schema is not configured for mutations', - [ operation ] - ); - } - return mutationType; - case 'subscription': - const subscriptionType = schema.getSubscriptionType(); - if (!subscriptionType) { - throw new GraphQLError( - 'Schema is not configured for subscriptions', - [ operation ] - ); - } - return subscriptionType; - default: - throw new GraphQLError( - 'Can only execute queries, mutations and subscriptions', - [ operation ] - ); - } -} - /** * Implements the "Evaluating selection sets" section of the spec * for "write" mode. diff --git a/src/execution/plan.js b/src/execution/plan.js index 624aef922b..94a3c7b956 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -58,7 +58,7 @@ export function planOperation( /** * Extracts the root type of the operation from the schema. */ -function getOperationRootType( +export function getOperationRootType( schema: GraphQLSchema, operation: OperationDefinition ): GraphQLObjectType { From 08c08f0d9f4c3f5350e141ded5badc13d400c7fa Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 09:34:48 -0800 Subject: [PATCH 04/94] Introduce the OperationExecutionPlan type --- src/execution/plan.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index 94a3c7b956..de781e632e 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -32,13 +32,25 @@ import type { FragmentDefinition } from '../language/ast'; +/** + * Data that must be available at all points during query execution. + * + * Namely, schema of the type system that is currently executing, + * and the fragments defined in the query document + */ +export type OperationExecutionPlan = { + type: GraphQLObjectType; + fields: {[key: string]: Array}; +// strategy: +} + /** * Create a plan based on the "Evaluating operations" section of the spec. */ export function planOperation( exeContext: ExecutionContext, operation: OperationDefinition -): Object { +): OperationExecutionPlan { const type = getOperationRootType(exeContext.schema, operation); const fields = collectFields( exeContext, @@ -47,12 +59,17 @@ export function planOperation( Object.create(null), Object.create(null) ); + + const plan: OperationExecutionPlan = { + type, + fields + }; /* if (operation.operation === 'mutation') { } else { } */ - return fields; // @TODO Fix + return plan; } /** From b8f16a6827c82d283f41e656086d51f4bfb720b8 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 10:22:17 -0800 Subject: [PATCH 05/94] Begin executing based on the plan --- src/execution/execute.js | 25 +++++++++---------------- src/execution/plan.js | 12 +++++------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index f6b0bbc439..50dde26918 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -34,7 +34,6 @@ import { } from '../type/introspection'; import type { Document, - OperationDefinition, Field, } from '../language/ast'; import { @@ -43,7 +42,7 @@ import { } from './context'; import { planOperation, - getOperationRootType, + OperationExecutionPlan, collectFields } from './plan'; @@ -110,7 +109,7 @@ export function execute( operationName ); - planOperation(context, context.operation); + const plan = planOperation(context, context.operation); // Return a Promise that will eventually resolve to the data described by // The "Response" section of the GraphQL specification. @@ -120,7 +119,7 @@ export function execute( // be executed. An execution which encounters errors will still result in a // resolved Promise. return new Promise(resolve => { - resolve(executeOperation(context, context.operation, rootValue)); + resolve(executeOperation(context, plan, rootValue)); }).catch(error => { // Errors from sub-fields of a NonNull type may propagate to the top level, // at which point we still log the error and null the parent field, which @@ -140,22 +139,16 @@ export function execute( */ function executeOperation( exeContext: ExecutionContext, - operation: OperationDefinition, + plan: OperationExecutionPlan, rootValue: mixed ): Object { - const type = getOperationRootType(exeContext.schema, operation); - const fields = collectFields( - exeContext, - type, - operation.selectionSet, - Object.create(null), - Object.create(null) - ); - if (operation.operation === 'mutation') { - return executeFieldsSerially(exeContext, type, rootValue, fields); + invariant(plan.strategy === 'serial' || plan.strategy === 'parallel'); + + if (plan.strategy === 'serial') { + return executeFieldsSerially(exeContext, plan.type, rootValue, plan.fields); } - return executeFields(exeContext, type, rootValue, fields); + return executeFields(exeContext, plan.type, rootValue, plan.fields); } /** diff --git a/src/execution/plan.js b/src/execution/plan.js index de781e632e..3e17939adf 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -41,7 +41,7 @@ import type { export type OperationExecutionPlan = { type: GraphQLObjectType; fields: {[key: string]: Array}; -// strategy: + strategy: string; } /** @@ -59,16 +59,14 @@ export function planOperation( Object.create(null), Object.create(null) ); + const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; const plan: OperationExecutionPlan = { type, - fields + fields, + strategy }; - /* - if (operation.operation === 'mutation') { - } else { - } - */ + return plan; } From 4c78283aaecf99eafc857e34abe8280d6307a534 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 12:04:52 -0800 Subject: [PATCH 06/94] Introduce a FieldResolvingPlan --- src/execution/execute.js | 48 +---------- src/execution/plan.js | 167 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 164 insertions(+), 51 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 50dde26918..19611edefd 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -23,15 +23,9 @@ import { import type { GraphQLType, GraphQLAbstractType, - GraphQLFieldDefinition, GraphQLResolveInfo, } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; -import { - SchemaMetaFieldDef, - TypeMetaFieldDef, - TypeNameMetaFieldDef -} from '../type/introspection'; import type { Document, Field, @@ -43,6 +37,8 @@ import { import { planOperation, OperationExecutionPlan, + defaultResolveFn, + getFieldDef, collectFields } from './plan'; @@ -529,20 +525,6 @@ function completeValue( return executeFields(exeContext, runtimeType, result, subFieldASTs); } -/** - * If a resolve function is not given, then a default resolve behavior is used - * which takes the property of the source object of the same name as the field - * and returns it as the result, or if it's a function, returns the result - * of calling that function. - */ -function defaultResolveFn(source, args, { fieldName }) { - // ensure source is a value for which property access is acceptable. - if (typeof source !== 'number' && typeof source !== 'string' && source) { - const property = (source: any)[fieldName]; - return typeof property === 'function' ? property.call(source) : property; - } -} - /** * Checks to see if this object acts like a Promise, i.e. has a "then" * function. @@ -552,29 +534,3 @@ function isThenable(value: mixed): boolean { value !== null && typeof value.then === 'function'; } - -/** - * This method looks up the field on the given type defintion. - * It has special casing for the two introspection fields, __schema - * and __typename. __typename is special because it can always be - * queried as a field, even in situations where no other fields - * are allowed, like on a Union. __schema could get automatically - * added to the query type, but that would require mutating type - * definitions, which would cause issues. - */ -function getFieldDef( - schema: GraphQLSchema, - parentType: GraphQLObjectType, - fieldName: string -): ?GraphQLFieldDefinition { - if (fieldName === SchemaMetaFieldDef.name && - schema.getQueryType() === parentType) { - return SchemaMetaFieldDef; - } else if (fieldName === TypeMetaFieldDef.name && - schema.getQueryType() === parentType) { - return TypeMetaFieldDef; - } else if (fieldName === TypeNameMetaFieldDef.name) { - return TypeNameMetaFieldDef; - } - return parentType.getFields()[fieldName]; -} diff --git a/src/execution/plan.js b/src/execution/plan.js index 3e17939adf..5290e21410 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -17,6 +17,9 @@ import { typeFromAST } from '../utilities/typeFromAST'; import { GraphQLObjectType, GraphQLAbstractType, + GraphQLResolveInfo, + GraphQLFieldDefinition, + GraphQLOutputType, isAbstractType } from '../type/definition'; import { @@ -31,17 +34,29 @@ import type { Field, FragmentDefinition } from '../language/ast'; +import { + SchemaMetaFieldDef, + TypeMetaFieldDef, + TypeNameMetaFieldDef +} from '../type/introspection'; + +/** + */ +export type FieldResolvingPlan = { + resolveFn: mixed; + args: { [key: string]: mixed }, + info: GraphQLResolveInfo; + returnType: GraphQLOutputType; + fieldASTs: Array; +} /** - * Data that must be available at all points during query execution. - * - * Namely, schema of the type system that is currently executing, - * and the fragments defined in the query document */ export type OperationExecutionPlan = { type: GraphQLObjectType; fields: {[key: string]: Array}; strategy: string; + fieldPlans: {[key: string]: FieldResolvingPlan}; } /** @@ -61,15 +76,112 @@ export function planOperation( ); const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; + const fieldPlans = planFields(exeContext, type, fields); + const plan: OperationExecutionPlan = { type, fields, - strategy + strategy, + fieldPlans }; return plan; } +/** + * Implements the "Evaluating selection sets" section of the spec + */ +function planFields( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + fields: {[key: string]: Array} +): {[key: string]: FieldResolvingPlan} { +/* + const finalResults = Object.keys(fields).reduce( + (results, responseName) => { + const fieldASTs = fields[responseName]; + const result = planResolveField( + exeContext, + parentType, + fieldASTs + ); + results[responseName] = result; + }, + Object.create(null) + ); + return finalResults; +*/ + const results = Object.create(null); + Object.keys(fields).forEach( + responseName => { + const fieldASTs = fields[responseName]; + const result = planResolveField( + exeContext, + parentType, + fieldASTs + ); + if (result !== undefined) { + results[responseName] = result; + } + } + ); + return results; +} + +/** + * Plan how to a field. + */ +function planResolveField( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + fieldASTs: Array +): ?FieldResolvingPlan { + const fieldAST = fieldASTs[0]; + const fieldName = fieldAST.name.value; + + const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName); + if (!fieldDef) { + // @TODO is it intentional that this fails silently? Should note that + return; + } + + const returnType = fieldDef.type; + const resolveFn = fieldDef.resolve || defaultResolveFn; + + // Build a JS object of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + const args = getArgumentValues( + fieldDef.args, + fieldAST.arguments, + exeContext.variableValues + ); + + // The resolve function's optional third argument is a collection of + // information about the current execution state. + const info: GraphQLResolveInfo = { + fieldName, + fieldASTs, + returnType, + parentType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, + }; + + const plan: FieldResolvingPlan = { + resolveFn, + args, + info, + returnType, + fieldASTs + }; + + return plan; +} + + /** * Extracts the root type of the operation from the schema. */ @@ -239,3 +351,48 @@ function getFieldEntryKey(node: Field): string { return node.alias ? node.alias.value : node.name.value; } +/** + * If a resolve function is not given, then a default resolve behavior is used + * which takes the property of the source object of the same name as the field + * and returns it as the result, or if it's a function, returns the result + * of calling that function. + */ +export function defaultResolveFn( + source:mixed, + args:{ [key: string]: mixed }, + info: GraphQLResolveInfo +) { + const fieldName = info.fieldName; + + // ensure source is a value for which property access is acceptable. + if (typeof source !== 'number' && typeof source !== 'string' && source) { + const property = (source: any)[fieldName]; + return typeof property === 'function' ? property.call(source) : property; + } +} + +/** + * This method looks up the field on the given type defintion. + * It has special casing for the two introspection fields, __schema + * and __typename. __typename is special because it can always be + * queried as a field, even in situations where no other fields + * are allowed, like on a Union. __schema could get automatically + * added to the query type, but that would require mutating type + * definitions, which would cause issues. + */ +export function getFieldDef( + schema: GraphQLSchema, + parentType: GraphQLObjectType, + fieldName: string +): ?GraphQLFieldDefinition { + if (fieldName === SchemaMetaFieldDef.name && + schema.getQueryType() === parentType) { + return SchemaMetaFieldDef; + } else if (fieldName === TypeMetaFieldDef.name && + schema.getQueryType() === parentType) { + return TypeMetaFieldDef; + } else if (fieldName === TypeNameMetaFieldDef.name) { + return TypeNameMetaFieldDef; + } + return parentType.getFields()[fieldName]; +} From 9b598b00753b3efa8d70bc68ffe21770d8ff828f Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 13:17:11 -0800 Subject: [PATCH 07/94] Execute based on the FieldResolvingPlan as far forward as it has been calculated --- src/execution/execute.js | 88 +++++++++++++++++++++++++++++++++++++--- src/execution/plan.js | 8 +++- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 19611edefd..6adcebe298 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -37,6 +37,7 @@ import { import { planOperation, OperationExecutionPlan, + FieldResolvingPlan, defaultResolveFn, getFieldDef, collectFields @@ -141,10 +142,16 @@ function executeOperation( invariant(plan.strategy === 'serial' || plan.strategy === 'parallel'); + if (plan.strategy === 'serial') { - return executeFieldsSerially(exeContext, plan.type, rootValue, plan.fields); + return executeFieldsSerially( + exeContext, + plan.type, + rootValue, + plan.fieldPlans + ); } - return executeFields(exeContext, plan.type, rootValue, plan.fields); + return executeFieldsPlan(exeContext, plan.type, rootValue, plan.fieldPlans); } /** @@ -155,16 +162,15 @@ function executeFieldsSerially( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, - fields: {[key: string]: Array} + fields: {[key: string]: FieldResolvingPlan} ): Promise { return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { - const fieldASTs = fields[responseName]; - const result = resolveField( + const result = resolveFieldPlan( exeContext, parentType, sourceValue, - fieldASTs + fields[responseName] ); if (result === undefined) { return results; @@ -182,6 +188,50 @@ function executeFieldsSerially( ); } +/** + * Implements the "Evaluating selection sets" section of the spec + * for "read" mode. + */ +function executeFieldsPlan( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: mixed, + fields: {[key: string]: FieldResolvingPlan} +): Object { + let containsPromise = false; + + const finalResults = Object.keys(fields).reduce( + (results, responseName) => { + const result = resolveFieldPlan( + exeContext, + parentType, + sourceValue, + fields[responseName] + ); + if (result === undefined) { + return results; + } + results[responseName] = result; + if (isThenable(result)) { + containsPromise = true; + } + return results; + }, + Object.create(null) + ); + + // If there are no promises, we can just return the object + if (!containsPromise) { + return finalResults; + } + + // Otherwise, results is a map from field name to the result + // of resolving that field, which is possibly a promise. Return + // a promise that will return this same map, but with any + // promises replaced with the values they resolved to. + return promiseForObject(finalResults); +} + /** * Implements the "Evaluating selection sets" section of the spec * for "read" mode. @@ -247,6 +297,32 @@ function promiseForObject( ); } +/** + * Resolves the field on the given source object. In particular, this + * figures out the value that the field returns by calling its resolve function, + * then calls completeValue to complete promises, serialize scalars, or execute + * the sub-selection-set for objects. + */ +function resolveFieldPlan( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + source: mixed, + plan: FieldResolvingPlan +): mixed { + + // Get the resolve function, regardless of if its result is normal + // or abrupt (error). + const result = resolveOrError(plan.resolveFn, source, plan.args, plan.info); + + return completeValueCatchingError( + exeContext, + plan.returnType, + plan.fieldASTs, + plan.info, + result + ); +} + /** * Resolves the field on the given source object. In particular, this * figures out the value that the field returns by calling its resolve function, diff --git a/src/execution/plan.js b/src/execution/plan.js index 5290e21410..c129f48f92 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -43,8 +43,12 @@ import { /** */ export type FieldResolvingPlan = { - resolveFn: mixed; - args: { [key: string]: mixed }, + resolveFn: ( + source: mixed, + args: { [key: string]: mixed }, + info: GraphQLResolveInfo + ) => mixed; + args: { [key: string]: mixed }; info: GraphQLResolveInfo; returnType: GraphQLOutputType; fieldASTs: Array; From 4ffca0a92833cd1516214772831b145677e4f2d9 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 16:28:37 -0800 Subject: [PATCH 08/94] Create plans for CompleteValue execution, rename plan names to be more consistent --- src/execution/execute.js | 6 +- src/execution/plan.js | 182 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 180 insertions(+), 8 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 6adcebe298..4d8921b3d0 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -36,7 +36,7 @@ import { } from './context'; import { planOperation, - OperationExecutionPlan, + SelectionExecutionPlan, FieldResolvingPlan, defaultResolveFn, getFieldDef, @@ -136,7 +136,7 @@ export function execute( */ function executeOperation( exeContext: ExecutionContext, - plan: OperationExecutionPlan, + plan: SelectionExecutionPlan, rootValue: mixed ): Object { @@ -314,6 +314,8 @@ function resolveFieldPlan( // or abrupt (error). const result = resolveOrError(plan.resolveFn, source, plan.args, plan.info); + // @TODO: CompleteValuePlan() + return completeValueCatchingError( exeContext, plan.returnType, diff --git a/src/execution/plan.js b/src/execution/plan.js index c129f48f92..62e3a8f010 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -12,14 +12,19 @@ import { GraphQLSchema } from '../type/schema'; import { ExecutionContext } from './context'; import { Kind } from '../language'; import find from '../jsutils/find'; +import invariant from '../jsutils/invariant'; import { getArgumentValues } from './values'; import { typeFromAST } from '../utilities/typeFromAST'; import { + GraphQLScalarType, + GraphQLEnumType, GraphQLObjectType, GraphQLAbstractType, + GraphQLList, GraphQLResolveInfo, GraphQLFieldDefinition, GraphQLOutputType, + GraphQLType, isAbstractType } from '../type/definition'; import { @@ -40,6 +45,36 @@ import { TypeNameMetaFieldDef } from '../type/introspection'; +/** + */ +export type MappingExecutionPlan = { + innerCompletionPlan: ExecutionPlan; +} + +/** + */ +export type SerializationExecutionPlan = { +} + +/** + */ +export type CoersionExecutionPlan = { + typePlans: Array; +} + +/** + */ +export type IgnoringExecutionPlan = { +} + +/** + */ +export type ExecutionPlan = + SerializationExecutionPlan | + MappingExecutionPlan | + CoersionExecutionPlan | + IgnoringExecutionPlan; + /** */ export type FieldResolvingPlan = { @@ -52,11 +87,12 @@ export type FieldResolvingPlan = { info: GraphQLResolveInfo; returnType: GraphQLOutputType; fieldASTs: Array; + completionPlan: ExecutionPlan; } /** */ -export type OperationExecutionPlan = { +export type SelectionExecutionPlan = { type: GraphQLObjectType; fields: {[key: string]: Array}; strategy: string; @@ -69,20 +105,70 @@ export type OperationExecutionPlan = { export function planOperation( exeContext: ExecutionContext, operation: OperationDefinition -): OperationExecutionPlan { +): SelectionExecutionPlan { const type = getOperationRootType(exeContext.schema, operation); + const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; + + return planSelection(exeContext, type, operation.selectionSet, strategy); +} + +/** + * Create a plan based on the "Evaluating operations" section of the spec. + */ +function planSelection( + exeContext: ExecutionContext, + type: GraphQLObjectType, + selectionSet: SelectionSet, + strategy: string = 'parallel' +): SelectionExecutionPlan { const fields = collectFields( exeContext, type, - operation.selectionSet, + selectionSet, Object.create(null), Object.create(null) ); - const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; const fieldPlans = planFields(exeContext, type, fields); - const plan: OperationExecutionPlan = { + const plan: SelectionExecutionPlan = { + type, + fields, + strategy, + fieldPlans + }; + + return plan; +} + +/** + * Create a plan based on the "Evaluating operations" section of the spec. + */ +function planSelectionToo( + exeContext: ExecutionContext, + type: GraphQLObjectType, + fieldASTs: Array, + strategy: string = 'parallel' +): SelectionExecutionPlan { + + let fields = Object.create(null); + const visitedFragmentNames = Object.create(null); + for (let i = 0; i < fieldASTs.length; i++) { + const selectionSet = fieldASTs[i].selectionSet; + if (selectionSet) { + fields = collectFields( + exeContext, + type, + selectionSet, + fields, + visitedFragmentNames + ); + } + } + + const fieldPlans = planFields(exeContext, type, fields); + + const plan: SelectionExecutionPlan = { type, fields, strategy, @@ -174,17 +260,101 @@ function planResolveField( variableValues: exeContext.variableValues, }; + const completionPlan = planCompleteValue(exeContext, returnType, fieldASTs); + const plan: FieldResolvingPlan = { resolveFn, args, info, returnType, - fieldASTs + fieldASTs, + completionPlan }; return plan; } +/** + * Implements the instructions for completeValue as defined in the + * "Field entries" section of the spec. + * + * If the field type is Non-Null, then this recursively completes the value + * for the inner type. It throws a field error if that completion returns null, + * as per the "Nullability" section of the spec. + * + * If the field type is a List, then this recursively completes the value + * for the inner type on each item in the list. + * + * If the field type is a Scalar or Enum, ensures the completed value is a legal + * value of the type by calling the `serialize` method of GraphQL type + * definition. + * + * Otherwise, the field type expects a sub-selection set, and will complete the + * value by evaluating all sub-selections. + */ +function planCompleteValue( + exeContext: ExecutionContext, + returnType: GraphQLType, + fieldASTs: Array +): ExecutionPlan { + + // If field type is List, complete each item in the list with the inner type + if (returnType instanceof GraphQLList) { + const innerType = returnType.ofType; + + const innerCompletionPlan = + planCompleteValue(exeContext, innerType, fieldASTs); + + const plan: MappingExecutionPlan = { + innerCompletionPlan + }; + + return plan; + } + + // If field type is Scalar or Enum, serialize to a valid value, returning + // null if serialization is not possible. + if (returnType instanceof GraphQLScalarType || + returnType instanceof GraphQLEnumType) { + invariant(returnType.serialize, 'Missing serialize method on type'); + + const plan: SerializationExecutionPlan = { + }; + + return plan; + } + + if (returnType instanceof GraphQLObjectType) { + return planSelectionToo( + exeContext, + returnType, + fieldASTs + ); + } + + if (isAbstractType(returnType)) { + const abstractType = ((returnType: any): GraphQLAbstractType); + const possibleTypes = abstractType.getPossibleTypes(); + const typePlans = possibleTypes.map(possibleType => { + return planSelectionToo( + exeContext, + possibleType, + fieldASTs + ); + }); + + const plan: CoersionExecutionPlan = { + typePlans + }; + + return plan; + } + + const plan: IgnoringExecutionPlan = { + }; + + return plan; +} /** * Extracts the root type of the operation from the schema. From f4f561acbe289d71bc955916a5e0cb463aa84eb6 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 16:35:52 -0800 Subject: [PATCH 09/94] Propigate execution plan into CompleteValue --- src/execution/execute.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 4d8921b3d0..b61f0eef95 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -37,6 +37,7 @@ import { import { planOperation, SelectionExecutionPlan, + ExecutionPlan, FieldResolvingPlan, defaultResolveFn, getFieldDef, @@ -321,7 +322,8 @@ function resolveFieldPlan( plan.returnType, plan.fieldASTs, plan.info, - result + result, + plan.completionPlan ); } @@ -412,12 +414,13 @@ function completeValueCatchingError( returnType: GraphQLType, fieldASTs: Array, info: GraphQLResolveInfo, - result: mixed + result: mixed, + plan: ?ExecutionPlan ): mixed { // If the field type is non-nullable, then it is resolved without any // protection from errors. if (returnType instanceof GraphQLNonNull) { - return completeValue(exeContext, returnType, fieldASTs, info, result); + return completeValue(exeContext, returnType, fieldASTs, info, result, plan); } // Otherwise, error protection is applied, logging the error and resolving @@ -428,7 +431,8 @@ function completeValueCatchingError( returnType, fieldASTs, info, - result + result, + plan ); if (isThenable(completed)) { // If `completeValue` returned a rejected promise, log the rejection @@ -472,7 +476,8 @@ function completeValue( returnType: GraphQLType, fieldASTs: Array, info: GraphQLResolveInfo, - result: mixed + result: mixed, + plan: ?ExecutionPlan ): mixed { // If result is a Promise, apply-lift over completeValue. if (isThenable(result)) { @@ -483,7 +488,8 @@ function completeValue( returnType, fieldASTs, info, - resolved + resolved, + plan ), // If rejected, create a located error, and continue to reject. error => Promise.reject(locatedError(error, fieldASTs)) From 0fca85b14ddb7db59017ddc2e7e5ee15794cc2fc Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 16:43:54 -0800 Subject: [PATCH 10/94] Correct misspelling add kind field to Execution Plans --- src/execution/plan.js | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index 62e3a8f010..51613bd82b 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -48,46 +48,52 @@ import { /** */ export type MappingExecutionPlan = { - innerCompletionPlan: ExecutionPlan; + kind: string; + innerCompletionPlan: ExecutionPlan; } /** */ export type SerializationExecutionPlan = { + kind: string; } /** */ -export type CoersionExecutionPlan = { - typePlans: Array; +export type CoercionExecutionPlan = { + kind: string; + typePlans: Array; } /** */ export type IgnoringExecutionPlan = { + kind: string; } /** */ export type ExecutionPlan = + SelectionExecutionPlan | SerializationExecutionPlan | MappingExecutionPlan | - CoersionExecutionPlan | + CoercionExecutionPlan | IgnoringExecutionPlan; /** */ export type FieldResolvingPlan = { - resolveFn: ( + kind: string; + resolveFn: ( source: mixed, args: { [key: string]: mixed }, info: GraphQLResolveInfo - ) => mixed; - args: { [key: string]: mixed }; - info: GraphQLResolveInfo; - returnType: GraphQLOutputType; - fieldASTs: Array; - completionPlan: ExecutionPlan; + ) => mixed; + args: { [key: string]: mixed }; + info: GraphQLResolveInfo; + returnType: GraphQLOutputType; + fieldASTs: Array; + completionPlan: ExecutionPlan; } /** @@ -132,6 +138,7 @@ function planSelection( const fieldPlans = planFields(exeContext, type, fields); const plan: SelectionExecutionPlan = { + kind: 'select', type, fields, strategy, @@ -169,6 +176,7 @@ function planSelectionToo( const fieldPlans = planFields(exeContext, type, fields); const plan: SelectionExecutionPlan = { + kind: 'select', type, fields, strategy, @@ -263,6 +271,7 @@ function planResolveField( const completionPlan = planCompleteValue(exeContext, returnType, fieldASTs); const plan: FieldResolvingPlan = { + kind: 'resolve', resolveFn, args, info, @@ -306,6 +315,7 @@ function planCompleteValue( planCompleteValue(exeContext, innerType, fieldASTs); const plan: MappingExecutionPlan = { + kind: 'map', innerCompletionPlan }; @@ -319,6 +329,7 @@ function planCompleteValue( invariant(returnType.serialize, 'Missing serialize method on type'); const plan: SerializationExecutionPlan = { + kind: 'serialize', }; return plan; @@ -343,7 +354,8 @@ function planCompleteValue( ); }); - const plan: CoersionExecutionPlan = { + const plan: CoercionExecutionPlan = { + kind: 'coerce', typePlans }; @@ -351,6 +363,7 @@ function planCompleteValue( } const plan: IgnoringExecutionPlan = { + kind: 'ignore', }; return plan; From 15f48cdb1ee3808dd27821989456d4e60aa63c1f Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 17:32:29 -0800 Subject: [PATCH 11/94] Checkpoint in the middle of converting CompleteValue to use Execution Plans --- src/execution/execute.js | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/execution/execute.js b/src/execution/execute.js index b61f0eef95..ad8550a300 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -526,6 +526,53 @@ function completeValue( return null; } + if (plan && plan.kind) { + switch (plan.kind) { + case 'serialize': + invariant(returnType.serialize, 'Missing serialize method on type'); + const serializedResult = returnType.serialize(result); + return isNullish(serializedResult) ? null : serializedResult; + case 'map': + invariant( + Array.isArray(result), + 'User Error: expected iterable, but did not find one ' + + `for field ${info.parentType}.${info.fieldName}.` + ); + + invariant(plan.innerCompletionPlan !== null); + invariant(typeof plan.innerCompletionPlan === 'object'); + + const innerCompletionPlan = plan.innerCompletionPlan; + + invariant(returnType instanceof GraphQLList); + + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another + // Promise. + const itemType = returnType.ofType; + let containsPromise = false; + const completedResults = result.map(item => { + const completedItem = + completeValueCatchingError( + exeContext, + itemType, + fieldASTs, + info, + item, + innerCompletionPlan + ); + if (!containsPromise && isThenable(completedItem)) { + containsPromise = true; + } + return completedItem; + }); + + return containsPromise ? + Promise.all(completedResults) : completedResults; + case 'select': + } + } + // If field type is List, complete each item in the list with the inner type if (returnType instanceof GraphQLList) { invariant( From a79d4e360e09ff3f72a15ee5f42352a613ae8043 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 17:45:50 -0800 Subject: [PATCH 12/94] Checkpoint in the middle of converting CompleteValue to use Execution Plans, completed select support --- src/execution/execute.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/execution/execute.js b/src/execution/execute.js index ad8550a300..eb92a51ad6 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -570,6 +570,29 @@ function completeValue( return containsPromise ? Promise.all(completedResults) : completedResults; case 'select': + invariant(plan.fieldPlans !== null); + invariant(typeof plan.fieldPlans === 'object'); + + const fieldPlans = plan.fieldPlans; + + invariant(returnType instanceof GraphQLObjectType); + + // 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 && !returnType.isTypeOf(result, info)) { + throw new GraphQLError( + `Expected value of type "${returnType}" but got: ${result}.`, + fieldASTs + ); + } + + return executeFieldsPlan( + exeContext, + returnType, + result, + fieldPlans + ); } } From b033bf8f273d23df4376ff9b23582f4e8584fbdd Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 19:14:54 -0800 Subject: [PATCH 13/94] Checkpoint converting completeValue over to execution plans --- src/execution/execute.js | 240 +++++++++------------------------------ src/execution/plan.js | 35 ++++-- 2 files changed, 80 insertions(+), 195 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index eb92a51ad6..3f21db6546 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -11,11 +11,9 @@ import { GraphQLError, locatedError } from '../error'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; -import { getArgumentValues } from './values'; + import { - GraphQLScalarType, GraphQLObjectType, - GraphQLEnumType, GraphQLList, GraphQLNonNull, isAbstractType @@ -38,10 +36,7 @@ import { planOperation, SelectionExecutionPlan, ExecutionPlan, - FieldResolvingPlan, - defaultResolveFn, - getFieldDef, - collectFields + FieldResolvingPlan } from './plan'; @@ -233,51 +228,6 @@ function executeFieldsPlan( return promiseForObject(finalResults); } -/** - * Implements the "Evaluating selection sets" section of the spec - * for "read" mode. - */ -function executeFields( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - sourceValue: mixed, - fields: {[key: string]: Array} -): Object { - let containsPromise = false; - - const finalResults = Object.keys(fields).reduce( - (results, responseName) => { - const fieldASTs = fields[responseName]; - const result = resolveField( - exeContext, - parentType, - sourceValue, - fieldASTs - ); - if (result === undefined) { - return results; - } - results[responseName] = result; - if (isThenable(result)) { - containsPromise = true; - } - return results; - }, - Object.create(null) - ); - - // If there are no promises, we can just return the object - if (!containsPromise) { - return finalResults; - } - - // Otherwise, results is a map from field name to the result - // of resolving that field, which is possibly a promise. Return - // a promise that will return this same map, but with any - // promises replaced with the values they resolved to. - return promiseForObject(finalResults); -} - /** * This function transforms a JS object `{[key: string]: Promise}` into * a `Promise<{[key: string]: T}>` @@ -315,8 +265,6 @@ function resolveFieldPlan( // or abrupt (error). const result = resolveOrError(plan.resolveFn, source, plan.args, plan.info); - // @TODO: CompleteValuePlan() - return completeValueCatchingError( exeContext, plan.returnType, @@ -327,65 +275,6 @@ function resolveFieldPlan( ); } -/** - * Resolves the field on the given source object. In particular, this - * figures out the value that the field returns by calling its resolve function, - * then calls completeValue to complete promises, serialize scalars, or execute - * the sub-selection-set for objects. - */ -function resolveField( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - source: mixed, - fieldASTs: Array -): mixed { - const fieldAST = fieldASTs[0]; - const fieldName = fieldAST.name.value; - - const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName); - if (!fieldDef) { - return; - } - - const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve || defaultResolveFn; - - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - // TODO: find a way to memoize, in case this field is within a List type. - const args = getArgumentValues( - fieldDef.args, - fieldAST.arguments, - exeContext.variableValues - ); - - // The resolve function's optional third argument is a collection of - // information about the current execution state. - const info: GraphQLResolveInfo = { - fieldName, - fieldASTs, - returnType, - parentType, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - }; - - // Get the resolve function, regardless of if its result is normal - // or abrupt (error). - const result = resolveOrError(resolveFn, source, args, info); - - return completeValueCatchingError( - exeContext, - returnType, - fieldASTs, - info, - result - ); -} - // Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` // function. Returns the result of resolveFn or the abrupt-return Error object. function resolveOrError( @@ -509,7 +398,8 @@ function completeValue( returnType.ofType, fieldASTs, info, - result + result, + plan ); if (completed === null) { throw new GraphQLError( @@ -593,90 +483,66 @@ function completeValue( result, fieldPlans ); - } - } + case 'coerce': + // Field type must be Object, Interface or Union and expect + // sub-selections. + let runtimeType: ?GraphQLObjectType; - // If field type is List, complete each item in the list with the inner type - if (returnType instanceof GraphQLList) { - invariant( - Array.isArray(result), - 'User Error: expected iterable, but did not find one ' + - `for field ${info.parentType}.${info.fieldName}.` - ); + invariant(isAbstractType(returnType)); - // This is specified as a simple map, however we're optimizing the path - // where the list contains no Promises by avoiding creating another Promise. - const itemType = returnType.ofType; - let containsPromise = false; - const completedResults = result.map(item => { - const completedItem = - completeValueCatchingError(exeContext, itemType, fieldASTs, info, item); - if (!containsPromise && isThenable(completedItem)) { - containsPromise = true; - } - return completedItem; - }); + const abstractType = ((returnType: any): GraphQLAbstractType); + runtimeType = abstractType.getObjectType(result, info); - return containsPromise ? Promise.all(completedResults) : completedResults; - } + if (!runtimeType) { + return null; + } - // If field type is Scalar or Enum, serialize to a valid value, returning - // null if serialization is not possible. - if (returnType instanceof GraphQLScalarType || - returnType instanceof GraphQLEnumType) { - invariant(returnType.serialize, 'Missing serialize method on type'); - const serializedResult = returnType.serialize(result); - return isNullish(serializedResult) ? null : serializedResult; - } + // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, info)) { + throw new GraphQLError( + `Expected value of type "${runtimeType}" but got: ${result}.`, + fieldASTs + ); + } - // Field type must be Object, Interface or Union and expect sub-selections. - let runtimeType: ?GraphQLObjectType; + if (runtimeType && !abstractType.isPossibleType(runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType}" is not a possible type ` + + `for "${abstractType}".`, + fieldASTs + ); + } - if (returnType instanceof GraphQLObjectType) { - runtimeType = returnType; - } else if (isAbstractType(returnType)) { - const abstractType = ((returnType: any): GraphQLAbstractType); - runtimeType = abstractType.getObjectType(result, info); - if (runtimeType && !abstractType.isPossibleType(runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType}" is not a possible type ` + - `for "${abstractType}".`, - fieldASTs - ); - } - } + invariant(plan.typePlans !== null); + invariant(typeof plan.typePlans === 'object'); - if (!runtimeType) { - return null; - } + const typePlans = plan.typePlans; - // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, info)) { - throw new GraphQLError( - `Expected value of type "${runtimeType}" but got: ${result}.`, - fieldASTs - ); - } + const typePlan = typePlans[runtimeType.name]; + if (!typePlan) { + // console.log(runtimeType.name, ':', Array.keys(typePlans)); + throw new GraphQLError( + `Runtime Object type "${runtimeType}" ` + + `is not a possible coercion type for "${abstractType}".`, + fieldASTs + ); + } - // Collect sub-fields to execute to complete this value. - let subFieldASTs = Object.create(null); - const visitedFragmentNames = Object.create(null); - for (let i = 0; i < fieldASTs.length; i++) { - const selectionSet = fieldASTs[i].selectionSet; - if (selectionSet) { - subFieldASTs = collectFields( - exeContext, - runtimeType, - selectionSet, - subFieldASTs, - visitedFragmentNames - ); + invariant(typePlan.fieldPlans !== null); + invariant(typeof typePlan.fieldPlans === 'object'); + + const typeFieldPlans = typePlan.fieldPlans; + + return executeFieldsPlan( + exeContext, + runtimeType, + result, + typeFieldPlans + ); } } - - return executeFields(exeContext, runtimeType, result, subFieldASTs); } /** diff --git a/src/execution/plan.js b/src/execution/plan.js index 51613bd82b..c7e13ab098 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -17,6 +17,7 @@ import { getArgumentValues } from './values'; import { typeFromAST } from '../utilities/typeFromAST'; import { GraphQLScalarType, + GraphQLNonNull, GraphQLEnumType, GraphQLObjectType, GraphQLAbstractType, @@ -62,7 +63,7 @@ export type SerializationExecutionPlan = { */ export type CoercionExecutionPlan = { kind: string; - typePlans: Array; + typePlans: {[key: string]:Array}; } /** @@ -307,6 +308,15 @@ function planCompleteValue( fieldASTs: Array ): ExecutionPlan { + // If field type is NonNull, complete for inner type + if (returnType instanceof GraphQLNonNull) { + return planCompleteValue( + exeContext, + returnType.ofType, + fieldASTs + ); + } + // If field type is List, complete each item in the list with the inner type if (returnType instanceof GraphQLList) { const innerType = returnType.ofType; @@ -346,8 +356,20 @@ function planCompleteValue( if (isAbstractType(returnType)) { const abstractType = ((returnType: any): GraphQLAbstractType); const possibleTypes = abstractType.getPossibleTypes(); - const typePlans = possibleTypes.map(possibleType => { - return planSelectionToo( + + const typePlans = Object.create(null); + /** + const typePlans = possibleTypes.reduce((result, possibleType) => { + result[possibleType.name] = planSelectionToo( + exeContext, + possibleType, + fieldASTs + ); + }, Object.create(null)); + */ + + possibleTypes.forEach(possibleType => { + typePlans[possibleType.name] = planSelectionToo( exeContext, possibleType, fieldASTs @@ -362,11 +384,8 @@ function planCompleteValue( return plan; } - const plan: IgnoringExecutionPlan = { - kind: 'ignore', - }; - - return plan; + // We have handled all possibilities. Not reachable + invariant(false); } /** From 371448b14016399ea93ebb91524d9fcbb6b7bd04 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 19:16:59 -0800 Subject: [PATCH 14/94] Remove function name suffixes now that we have collapsed into only using execution plans --- src/execution/execute.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 3f21db6546..db3019722a 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -147,7 +147,7 @@ function executeOperation( plan.fieldPlans ); } - return executeFieldsPlan(exeContext, plan.type, rootValue, plan.fieldPlans); + return executeFields(exeContext, plan.type, rootValue, plan.fieldPlans); } /** @@ -162,7 +162,7 @@ function executeFieldsSerially( ): Promise { return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { - const result = resolveFieldPlan( + const result = resolveField( exeContext, parentType, sourceValue, @@ -188,7 +188,7 @@ function executeFieldsSerially( * Implements the "Evaluating selection sets" section of the spec * for "read" mode. */ -function executeFieldsPlan( +function executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, @@ -198,7 +198,7 @@ function executeFieldsPlan( const finalResults = Object.keys(fields).reduce( (results, responseName) => { - const result = resolveFieldPlan( + const result = resolveField( exeContext, parentType, sourceValue, @@ -254,7 +254,7 @@ function promiseForObject( * then calls completeValue to complete promises, serialize scalars, or execute * the sub-selection-set for objects. */ -function resolveFieldPlan( +function resolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, source: mixed, @@ -477,7 +477,7 @@ function completeValue( ); } - return executeFieldsPlan( + return executeFields( exeContext, returnType, result, @@ -535,7 +535,7 @@ function completeValue( const typeFieldPlans = typePlan.fieldPlans; - return executeFieldsPlan( + return executeFields( exeContext, runtimeType, result, From 6a06fe1c4627dcb0303fd01115250cf1fe906cad Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 19:36:24 -0800 Subject: [PATCH 15/94] Execution Plans are no longer optional --- src/execution/execute.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index db3019722a..2ea6891133 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -304,7 +304,7 @@ function completeValueCatchingError( fieldASTs: Array, info: GraphQLResolveInfo, result: mixed, - plan: ?ExecutionPlan + plan: ExecutionPlan ): mixed { // If the field type is non-nullable, then it is resolved without any // protection from errors. @@ -366,7 +366,7 @@ function completeValue( fieldASTs: Array, info: GraphQLResolveInfo, result: mixed, - plan: ?ExecutionPlan + plan: ExecutionPlan ): mixed { // If result is a Promise, apply-lift over completeValue. if (isThenable(result)) { From 4b017d707b89d6e24aaba9ada9270e2588204ca6 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 20:21:32 -0800 Subject: [PATCH 16/94] Remove ignoring execution plan --- src/execution/plan.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index c7e13ab098..8ccb20d40c 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -66,20 +66,13 @@ export type CoercionExecutionPlan = { typePlans: {[key: string]:Array}; } -/** - */ -export type IgnoringExecutionPlan = { - kind: string; -} - /** */ export type ExecutionPlan = SelectionExecutionPlan | SerializationExecutionPlan | MappingExecutionPlan | - CoercionExecutionPlan | - IgnoringExecutionPlan; + CoercionExecutionPlan; /** */ From 93d8091f8b2f470fbaeea82e69258b3977643f94 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 20:23:41 -0800 Subject: [PATCH 17/94] Harmonize names of execution plans --- src/execution/execute.js | 8 ++++---- src/execution/plan.js | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 2ea6891133..d00be2645a 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -36,7 +36,7 @@ import { planOperation, SelectionExecutionPlan, ExecutionPlan, - FieldResolvingPlan + ResolvingExecutionPlan } from './plan'; @@ -158,7 +158,7 @@ function executeFieldsSerially( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, - fields: {[key: string]: FieldResolvingPlan} + fields: {[key: string]: ResolvingExecutionPlan} ): Promise { return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { @@ -192,7 +192,7 @@ function executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, - fields: {[key: string]: FieldResolvingPlan} + fields: {[key: string]: ResolvingExecutionPlan} ): Object { let containsPromise = false; @@ -258,7 +258,7 @@ function resolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, source: mixed, - plan: FieldResolvingPlan + plan: ResolvingExecutionPlan ): mixed { // Get the resolve function, regardless of if its result is normal diff --git a/src/execution/plan.js b/src/execution/plan.js index 8ccb20d40c..e1fe040b57 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -76,7 +76,7 @@ export type ExecutionPlan = /** */ -export type FieldResolvingPlan = { +export type ResolvingExecutionPlan = { kind: string; resolveFn: ( source: mixed, @@ -96,7 +96,7 @@ export type SelectionExecutionPlan = { type: GraphQLObjectType; fields: {[key: string]: Array}; strategy: string; - fieldPlans: {[key: string]: FieldResolvingPlan}; + fieldPlans: {[key: string]: ResolvingExecutionPlan}; } /** @@ -187,7 +187,7 @@ function planFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, fields: {[key: string]: Array} -): {[key: string]: FieldResolvingPlan} { +): {[key: string]: ResolvingExecutionPlan} { /* const finalResults = Object.keys(fields).reduce( (results, responseName) => { @@ -227,7 +227,7 @@ function planResolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, fieldASTs: Array -): ?FieldResolvingPlan { +): ?ResolvingExecutionPlan { const fieldAST = fieldASTs[0]; const fieldName = fieldAST.name.value; @@ -264,7 +264,7 @@ function planResolveField( const completionPlan = planCompleteValue(exeContext, returnType, fieldASTs); - const plan: FieldResolvingPlan = { + const plan: ResolvingExecutionPlan = { kind: 'resolve', resolveFn, args, From a1f8aa374e05b3ef127f2f41e32b49896c02ad08 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 20:35:18 -0800 Subject: [PATCH 18/94] Fix imports of execution plan types --- src/execution/execute.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index d00be2645a..1d48af1d85 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -33,9 +33,12 @@ import { buildExecutionContext } from './context'; import { - planOperation, - SelectionExecutionPlan, + planOperation +} from './plan'; + +import type { ExecutionPlan, + SelectionExecutionPlan, ResolvingExecutionPlan } from './plan'; From db8ad59274e541f1af3c9a7d178797e1acb8ddb0 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 20:35:56 -0800 Subject: [PATCH 19/94] Indeed, it is in intional to omit fields, change comment to reflect that --- src/execution/plan.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index e1fe040b57..4453750a06 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -212,7 +212,7 @@ function planFields( parentType, fieldASTs ); - if (result !== undefined) { + if (result) { results[responseName] = result; } } @@ -233,7 +233,7 @@ function planResolveField( const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName); if (!fieldDef) { - // @TODO is it intentional that this fails silently? Should note that + // Omit requested fields that are not in our schema return; } From a1f6c3e2e49ea8affd7a3656c3ea20c345e232a0 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 20:41:37 -0800 Subject: [PATCH 20/94] Remove wrapper around switch statement --- src/execution/execute.js | 219 ++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 109 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 1d48af1d85..0e39a75b0a 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -419,132 +419,133 @@ function completeValue( return null; } - if (plan && plan.kind) { - switch (plan.kind) { - case 'serialize': - invariant(returnType.serialize, 'Missing serialize method on type'); - const serializedResult = returnType.serialize(result); - return isNullish(serializedResult) ? null : serializedResult; - case 'map': - invariant( - Array.isArray(result), - 'User Error: expected iterable, but did not find one ' + - `for field ${info.parentType}.${info.fieldName}.` - ); - - invariant(plan.innerCompletionPlan !== null); - invariant(typeof plan.innerCompletionPlan === 'object'); - - const innerCompletionPlan = plan.innerCompletionPlan; - - invariant(returnType instanceof GraphQLList); - - // This is specified as a simple map, however we're optimizing the path - // where the list contains no Promises by avoiding creating another - // Promise. - const itemType = returnType.ofType; - let containsPromise = false; - const completedResults = result.map(item => { - const completedItem = - completeValueCatchingError( - exeContext, - itemType, - fieldASTs, - info, - item, - innerCompletionPlan - ); - if (!containsPromise && isThenable(completedItem)) { - containsPromise = true; - } - return completedItem; - }); + // @TODO: This doesn't say much; need typeflow lessons + invariant(plan && plan.kind); + + switch (plan.kind) { + case 'serialize': + invariant(returnType.serialize, 'Missing serialize method on type'); + const serializedResult = returnType.serialize(result); + return isNullish(serializedResult) ? null : serializedResult; + case 'map': + invariant( + Array.isArray(result), + 'User Error: expected iterable, but did not find one ' + + `for field ${info.parentType}.${info.fieldName}.` + ); - return containsPromise ? - Promise.all(completedResults) : completedResults; - case 'select': - invariant(plan.fieldPlans !== null); - invariant(typeof plan.fieldPlans === 'object'); + invariant(plan.innerCompletionPlan !== null); + invariant(typeof plan.innerCompletionPlan === 'object'); + + const innerCompletionPlan = plan.innerCompletionPlan; + + invariant(returnType instanceof GraphQLList); + + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another + // Promise. + const itemType = returnType.ofType; + let containsPromise = false; + const completedResults = result.map(item => { + const completedItem = + completeValueCatchingError( + exeContext, + itemType, + fieldASTs, + info, + item, + innerCompletionPlan + ); + if (!containsPromise && isThenable(completedItem)) { + containsPromise = true; + } + return completedItem; + }); - const fieldPlans = plan.fieldPlans; + return containsPromise ? + Promise.all(completedResults) : completedResults; + case 'select': + invariant(plan.fieldPlans !== null); + invariant(typeof plan.fieldPlans === 'object'); - invariant(returnType instanceof GraphQLObjectType); + const fieldPlans = plan.fieldPlans; - // 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 && !returnType.isTypeOf(result, info)) { - throw new GraphQLError( - `Expected value of type "${returnType}" but got: ${result}.`, - fieldASTs - ); - } + invariant(returnType instanceof GraphQLObjectType); - return executeFields( - exeContext, - returnType, - result, - fieldPlans + // 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 && !returnType.isTypeOf(result, info)) { + throw new GraphQLError( + `Expected value of type "${returnType}" but got: ${result}.`, + fieldASTs ); - case 'coerce': - // Field type must be Object, Interface or Union and expect - // sub-selections. - let runtimeType: ?GraphQLObjectType; + } - invariant(isAbstractType(returnType)); + return executeFields( + exeContext, + returnType, + result, + fieldPlans + ); + case 'coerce': + // Field type must be Object, Interface or Union and expect + // sub-selections. + let runtimeType: ?GraphQLObjectType; - const abstractType = ((returnType: any): GraphQLAbstractType); - runtimeType = abstractType.getObjectType(result, info); + invariant(isAbstractType(returnType)); - if (!runtimeType) { - return null; - } + const abstractType = ((returnType: any): GraphQLAbstractType); + runtimeType = abstractType.getObjectType(result, info); - // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, info)) { - throw new GraphQLError( - `Expected value of type "${runtimeType}" but got: ${result}.`, - fieldASTs - ); - } + if (!runtimeType) { + return null; + } - if (runtimeType && !abstractType.isPossibleType(runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType}" is not a possible type ` + - `for "${abstractType}".`, - fieldASTs - ); - } + // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, info)) { + throw new GraphQLError( + `Expected value of type "${runtimeType}" but got: ${result}.`, + fieldASTs + ); + } - invariant(plan.typePlans !== null); - invariant(typeof plan.typePlans === 'object'); + if (runtimeType && !abstractType.isPossibleType(runtimeType)) { + throw new GraphQLError( + `Runtime Object type "${runtimeType}" is not a possible type ` + + `for "${abstractType}".`, + fieldASTs + ); + } - const typePlans = plan.typePlans; + invariant(plan.typePlans !== null); + invariant(typeof plan.typePlans === 'object'); - const typePlan = typePlans[runtimeType.name]; - if (!typePlan) { - // console.log(runtimeType.name, ':', Array.keys(typePlans)); - throw new GraphQLError( - `Runtime Object type "${runtimeType}" ` + - `is not a possible coercion type for "${abstractType}".`, - fieldASTs - ); - } + const typePlans = plan.typePlans; - invariant(typePlan.fieldPlans !== null); - invariant(typeof typePlan.fieldPlans === 'object'); + const typePlan = typePlans[runtimeType.name]; + if (!typePlan) { + // console.log(runtimeType.name, ':', Array.keys(typePlans)); + throw new GraphQLError( + `Runtime Object type "${runtimeType}" ` + + `is not a possible coercion type for "${abstractType}".`, + fieldASTs + ); + } - const typeFieldPlans = typePlan.fieldPlans; + invariant(typePlan.fieldPlans !== null); + invariant(typeof typePlan.fieldPlans === 'object'); - return executeFields( - exeContext, - runtimeType, - result, - typeFieldPlans - ); - } + const typeFieldPlans = typePlan.fieldPlans; + + return executeFields( + exeContext, + runtimeType, + result, + typeFieldPlans + ); } } From 8405ebbc217f48924fb6343ba07f3b0ac1849b57 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 20:50:59 -0800 Subject: [PATCH 21/94] Add kind as a constant on execution plan type definitions --- src/execution/execute.js | 3 --- src/execution/plan.js | 11 ++++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 0e39a75b0a..72a3eb9a2d 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -419,9 +419,6 @@ function completeValue( return null; } - // @TODO: This doesn't say much; need typeflow lessons - invariant(plan && plan.kind); - switch (plan.kind) { case 'serialize': invariant(returnType.serialize, 'Missing serialize method on type'); diff --git a/src/execution/plan.js b/src/execution/plan.js index 4453750a06..06af4667b0 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -49,20 +49,20 @@ import { /** */ export type MappingExecutionPlan = { - kind: string; + kind: 'map'; innerCompletionPlan: ExecutionPlan; } /** */ export type SerializationExecutionPlan = { - kind: string; + kind: 'serialize'; } /** */ export type CoercionExecutionPlan = { - kind: string; + kind: 'coerce', typePlans: {[key: string]:Array}; } @@ -77,7 +77,7 @@ export type ExecutionPlan = /** */ export type ResolvingExecutionPlan = { - kind: string; + kind: 'resolve'; resolveFn: ( source: mixed, args: { [key: string]: mixed }, @@ -93,6 +93,7 @@ export type ResolvingExecutionPlan = { /** */ export type SelectionExecutionPlan = { + kind: 'select'; type: GraphQLObjectType; fields: {[key: string]: Array}; strategy: string; @@ -332,7 +333,7 @@ function planCompleteValue( invariant(returnType.serialize, 'Missing serialize method on type'); const plan: SerializationExecutionPlan = { - kind: 'serialize', + kind: 'serialize' }; return plan; From 5f8a43f4c31b97034660ce0559e40dd7c8e91f88 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 21:30:04 -0800 Subject: [PATCH 22/94] Fix incorrectly imported type definitions --- src/execution/plan.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index 06af4667b0..8bddf5b3d1 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -17,16 +17,18 @@ import { getArgumentValues } from './values'; import { typeFromAST } from '../utilities/typeFromAST'; import { GraphQLScalarType, - GraphQLNonNull, - GraphQLEnumType, GraphQLObjectType, - GraphQLAbstractType, + GraphQLEnumType, GraphQLList, - GraphQLResolveInfo, + GraphQLNonNull, + isAbstractType +} from '../type/definition'; +import type { GraphQLFieldDefinition, - GraphQLOutputType, + GraphQLAbstractType, GraphQLType, - isAbstractType + GraphQLOutputType, + GraphQLResolveInfo } from '../type/definition'; import { GraphQLIncludeDirective, From d3bd76a45ee23a5a250252fad97fe999167d6c77 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 22:23:22 -0800 Subject: [PATCH 23/94] Fix broken CoercionExecutionPlan definition --- src/execution/execute.js | 3 --- src/execution/plan.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 72a3eb9a2d..085d7dc96b 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -532,9 +532,6 @@ function completeValue( ); } - invariant(typePlan.fieldPlans !== null); - invariant(typeof typePlan.fieldPlans === 'object'); - const typeFieldPlans = typePlan.fieldPlans; return executeFields( diff --git a/src/execution/plan.js b/src/execution/plan.js index 8bddf5b3d1..8d4be3c5d3 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -64,8 +64,8 @@ export type SerializationExecutionPlan = { /** */ export type CoercionExecutionPlan = { - kind: 'coerce', - typePlans: {[key: string]:Array}; + kind: 'coerce'; + typePlans: {[key: string]:SelectionExecutionPlan}; } /** From d7e34b793dfce142dc2056690f3d10a57f5efd9c Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 22:26:18 -0800 Subject: [PATCH 24/94] Remove unnecessary invariant declarations --- src/execution/execute.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 085d7dc96b..bc9e1a1ce1 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -432,7 +432,6 @@ function completeValue( ); invariant(plan.innerCompletionPlan !== null); - invariant(typeof plan.innerCompletionPlan === 'object'); const innerCompletionPlan = plan.innerCompletionPlan; @@ -463,7 +462,6 @@ function completeValue( Promise.all(completedResults) : completedResults; case 'select': invariant(plan.fieldPlans !== null); - invariant(typeof plan.fieldPlans === 'object'); const fieldPlans = plan.fieldPlans; @@ -518,7 +516,6 @@ function completeValue( } invariant(plan.typePlans !== null); - invariant(typeof plan.typePlans === 'object'); const typePlans = plan.typePlans; From a4beecf87b8fbd2b9fa2377fa4e8c89615a45e62 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 22:38:14 -0800 Subject: [PATCH 25/94] Change ExecuteFields to accept a plan instead of a list of fields --- src/execution/execute.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index bc9e1a1ce1..5859db729d 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -147,10 +147,10 @@ function executeOperation( exeContext, plan.type, rootValue, - plan.fieldPlans + plan ); } - return executeFields(exeContext, plan.type, rootValue, plan.fieldPlans); + return executeFields(exeContext, plan.type, rootValue, plan); } /** @@ -161,8 +161,9 @@ function executeFieldsSerially( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, - fields: {[key: string]: ResolvingExecutionPlan} + plan: SelectionExecutionPlan ): Promise { + const fields = plan.fieldPlans; return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { const result = resolveField( @@ -195,8 +196,9 @@ function executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, - fields: {[key: string]: ResolvingExecutionPlan} + plan: SelectionExecutionPlan ): Object { + const fields = plan.fieldPlans; let containsPromise = false; const finalResults = Object.keys(fields).reduce( @@ -461,9 +463,6 @@ function completeValue( return containsPromise ? Promise.all(completedResults) : completedResults; case 'select': - invariant(plan.fieldPlans !== null); - - const fieldPlans = plan.fieldPlans; invariant(returnType instanceof GraphQLObjectType); @@ -481,7 +480,7 @@ function completeValue( exeContext, returnType, result, - fieldPlans + plan ); case 'coerce': // Field type must be Object, Interface or Union and expect @@ -529,13 +528,11 @@ function completeValue( ); } - const typeFieldPlans = typePlan.fieldPlans; - return executeFields( exeContext, runtimeType, result, - typeFieldPlans + typePlan ); } } From f34ccac500410901707356a3193cd036dd19966b Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 23:22:36 -0800 Subject: [PATCH 26/94] No need to capture fields, this wasy a temporary artifact of transition --- src/execution/plan.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index 8d4be3c5d3..6dde9f49d2 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -97,7 +97,6 @@ export type ResolvingExecutionPlan = { export type SelectionExecutionPlan = { kind: 'select'; type: GraphQLObjectType; - fields: {[key: string]: Array}; strategy: string; fieldPlans: {[key: string]: ResolvingExecutionPlan}; } @@ -137,7 +136,6 @@ function planSelection( const plan: SelectionExecutionPlan = { kind: 'select', type, - fields, strategy, fieldPlans }; @@ -175,7 +173,6 @@ function planSelectionToo( const plan: SelectionExecutionPlan = { kind: 'select', type, - fields, strategy, fieldPlans }; From d841bdc9115e27c0a86b36c047ec10a76d520e3c Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 23:28:29 -0800 Subject: [PATCH 27/94] Rename ExecutionPlan to CompletionExecutionPlan. That's more indicative of what it is. Frees up ExecutionPlan for more generic uses --- src/execution/execute.js | 6 +++--- src/execution/plan.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 5859db729d..f4d544cfb1 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -37,7 +37,7 @@ import { } from './plan'; import type { - ExecutionPlan, + CompletionExecutionPlan, SelectionExecutionPlan, ResolvingExecutionPlan } from './plan'; @@ -309,7 +309,7 @@ function completeValueCatchingError( fieldASTs: Array, info: GraphQLResolveInfo, result: mixed, - plan: ExecutionPlan + plan: CompletionExecutionPlan ): mixed { // If the field type is non-nullable, then it is resolved without any // protection from errors. @@ -371,7 +371,7 @@ function completeValue( fieldASTs: Array, info: GraphQLResolveInfo, result: mixed, - plan: ExecutionPlan + plan: CompletionExecutionPlan ): mixed { // If result is a Promise, apply-lift over completeValue. if (isThenable(result)) { diff --git a/src/execution/plan.js b/src/execution/plan.js index 6dde9f49d2..ddf155d3d8 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -52,7 +52,7 @@ import { */ export type MappingExecutionPlan = { kind: 'map'; - innerCompletionPlan: ExecutionPlan; + innerCompletionPlan: CompletionExecutionPlan; // Is this really so broad? } /** @@ -70,7 +70,7 @@ export type CoercionExecutionPlan = { /** */ -export type ExecutionPlan = +export type CompletionExecutionPlan = SelectionExecutionPlan | SerializationExecutionPlan | MappingExecutionPlan | @@ -89,7 +89,7 @@ export type ResolvingExecutionPlan = { info: GraphQLResolveInfo; returnType: GraphQLOutputType; fieldASTs: Array; - completionPlan: ExecutionPlan; + completionPlan: CompletionExecutionPlan; } /** @@ -299,7 +299,7 @@ function planCompleteValue( exeContext: ExecutionContext, returnType: GraphQLType, fieldASTs: Array -): ExecutionPlan { +): CompletionExecutionPlan { // If field type is NonNull, complete for inner type if (returnType instanceof GraphQLNonNull) { From 36fb2d1123c528fb266220cd803e846cbb86efb5 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 25 Feb 2016 23:38:27 -0800 Subject: [PATCH 28/94] Capture type information in the Execution Plans --- src/execution/plan.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index ddf155d3d8..bd83e8a8c2 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -52,6 +52,8 @@ import { */ export type MappingExecutionPlan = { kind: 'map'; + type: GraphQLType; + innerType: GraphQLType; innerCompletionPlan: CompletionExecutionPlan; // Is this really so broad? } @@ -59,12 +61,14 @@ export type MappingExecutionPlan = { */ export type SerializationExecutionPlan = { kind: 'serialize'; + type: GraphQLType; } /** */ export type CoercionExecutionPlan = { kind: 'coerce'; + type: GraphQLType; typePlans: {[key: string]:SelectionExecutionPlan}; } @@ -319,6 +323,8 @@ function planCompleteValue( const plan: MappingExecutionPlan = { kind: 'map', + type: returnType, + innerType, innerCompletionPlan }; @@ -332,7 +338,8 @@ function planCompleteValue( invariant(returnType.serialize, 'Missing serialize method on type'); const plan: SerializationExecutionPlan = { - kind: 'serialize' + kind: 'serialize', + type: returnType }; return plan; @@ -371,6 +378,7 @@ function planCompleteValue( const plan: CoercionExecutionPlan = { kind: 'coerce', + type: returnType, typePlans }; From cdbb5d5bba6c7850a0f7ca355e23718918d8ab1b Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 00:00:19 -0800 Subject: [PATCH 29/94] move fieldASTs information into the Execution Plans and out of the calling chain. --- src/execution/execute.js | 26 +++++++++----------------- src/execution/plan.js | 11 ++++++++++- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index f4d544cfb1..5a934abfbf 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -25,8 +25,7 @@ import type { } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; import type { - Document, - Field, + Document } from '../language/ast'; import { ExecutionContext, @@ -273,7 +272,6 @@ function resolveField( return completeValueCatchingError( exeContext, plan.returnType, - plan.fieldASTs, plan.info, result, plan.completionPlan @@ -306,7 +304,6 @@ function resolveOrError( function completeValueCatchingError( exeContext: ExecutionContext, returnType: GraphQLType, - fieldASTs: Array, info: GraphQLResolveInfo, result: mixed, plan: CompletionExecutionPlan @@ -314,7 +311,7 @@ function completeValueCatchingError( // If the field type is non-nullable, then it is resolved without any // protection from errors. if (returnType instanceof GraphQLNonNull) { - return completeValue(exeContext, returnType, fieldASTs, info, result, plan); + return completeValue(exeContext, returnType, info, result, plan); } // Otherwise, error protection is applied, logging the error and resolving @@ -323,7 +320,6 @@ function completeValueCatchingError( const completed = completeValue( exeContext, returnType, - fieldASTs, info, result, plan @@ -368,7 +364,6 @@ function completeValueCatchingError( function completeValue( exeContext: ExecutionContext, returnType: GraphQLType, - fieldASTs: Array, info: GraphQLResolveInfo, result: mixed, plan: CompletionExecutionPlan @@ -380,19 +375,18 @@ function completeValue( resolved => completeValue( exeContext, returnType, - fieldASTs, info, resolved, plan ), // If rejected, create a located error, and continue to reject. - error => Promise.reject(locatedError(error, fieldASTs)) + error => Promise.reject(locatedError(error, plan.fieldASTs)) ); } // If result is an Error, throw a located error. if (result instanceof Error) { - throw locatedError(result, fieldASTs); + throw locatedError(result, plan.fieldASTs); } // If field type is NonNull, complete for inner type, and throw field error @@ -401,7 +395,6 @@ function completeValue( const completed = completeValue( exeContext, returnType.ofType, - fieldASTs, info, result, plan @@ -410,7 +403,7 @@ function completeValue( throw new GraphQLError( `Cannot return null for non-nullable ` + `field ${info.parentType}.${info.fieldName}.`, - fieldASTs + plan.fieldASTs ); } return completed; @@ -449,7 +442,6 @@ function completeValue( completeValueCatchingError( exeContext, itemType, - fieldASTs, info, item, innerCompletionPlan @@ -472,7 +464,7 @@ function completeValue( if (returnType.isTypeOf && !returnType.isTypeOf(result, info)) { throw new GraphQLError( `Expected value of type "${returnType}" but got: ${result}.`, - fieldASTs + plan.fieldASTs ); } @@ -502,7 +494,7 @@ function completeValue( if (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, info)) { throw new GraphQLError( `Expected value of type "${runtimeType}" but got: ${result}.`, - fieldASTs + plan.fieldASTs ); } @@ -510,7 +502,7 @@ function completeValue( throw new GraphQLError( `Runtime Object type "${runtimeType}" is not a possible type ` + `for "${abstractType}".`, - fieldASTs + plan.fieldASTs ); } @@ -524,7 +516,7 @@ function completeValue( throw new GraphQLError( `Runtime Object type "${runtimeType}" ` + `is not a possible coercion type for "${abstractType}".`, - fieldASTs + plan.fieldASTs ); } diff --git a/src/execution/plan.js b/src/execution/plan.js index bd83e8a8c2..90cc14b0dc 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -53,6 +53,7 @@ import { export type MappingExecutionPlan = { kind: 'map'; type: GraphQLType; + fieldASTs: Array; innerType: GraphQLType; innerCompletionPlan: CompletionExecutionPlan; // Is this really so broad? } @@ -62,6 +63,7 @@ export type MappingExecutionPlan = { export type SerializationExecutionPlan = { kind: 'serialize'; type: GraphQLType; + fieldASTs: Array; } /** @@ -69,6 +71,7 @@ export type SerializationExecutionPlan = { export type CoercionExecutionPlan = { kind: 'coerce'; type: GraphQLType; + fieldASTs: Array; typePlans: {[key: string]:SelectionExecutionPlan}; } @@ -101,6 +104,7 @@ export type ResolvingExecutionPlan = { export type SelectionExecutionPlan = { kind: 'select'; type: GraphQLObjectType; + fieldASTs: Array; strategy: string; fieldPlans: {[key: string]: ResolvingExecutionPlan}; } @@ -140,6 +144,7 @@ function planSelection( const plan: SelectionExecutionPlan = { kind: 'select', type, + fieldASTs: [], // I don't know what to pass here strategy, fieldPlans }; @@ -177,6 +182,7 @@ function planSelectionToo( const plan: SelectionExecutionPlan = { kind: 'select', type, + fieldASTs, strategy, fieldPlans }; @@ -324,6 +330,7 @@ function planCompleteValue( const plan: MappingExecutionPlan = { kind: 'map', type: returnType, + fieldASTs, innerType, innerCompletionPlan }; @@ -339,7 +346,8 @@ function planCompleteValue( const plan: SerializationExecutionPlan = { kind: 'serialize', - type: returnType + type: returnType, + fieldASTs }; return plan; @@ -379,6 +387,7 @@ function planCompleteValue( const plan: CoercionExecutionPlan = { kind: 'coerce', type: returnType, + fieldASTs, typePlans }; From 787c4e6461da286d4bb8366ba8fb0cbe42cffacb Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 00:05:55 -0800 Subject: [PATCH 30/94] Narrow the width of the executeFields methods by removing the type parameter, which is also on the plan --- src/execution/execute.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 5a934abfbf..297b02a500 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -140,16 +140,10 @@ function executeOperation( invariant(plan.strategy === 'serial' || plan.strategy === 'parallel'); - if (plan.strategy === 'serial') { - return executeFieldsSerially( - exeContext, - plan.type, - rootValue, - plan - ); + return executeFieldsSerially(exeContext, rootValue, plan); } - return executeFields(exeContext, plan.type, rootValue, plan); + return executeFields(exeContext, rootValue, plan); } /** @@ -158,11 +152,11 @@ function executeOperation( */ function executeFieldsSerially( exeContext: ExecutionContext, - parentType: GraphQLObjectType, sourceValue: mixed, plan: SelectionExecutionPlan ): Promise { const fields = plan.fieldPlans; + const parentType = plan.type; return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { const result = resolveField( @@ -193,11 +187,11 @@ function executeFieldsSerially( */ function executeFields( exeContext: ExecutionContext, - parentType: GraphQLObjectType, sourceValue: mixed, plan: SelectionExecutionPlan ): Object { const fields = plan.fieldPlans; + const parentType = plan.type; let containsPromise = false; const finalResults = Object.keys(fields).reduce( @@ -470,7 +464,6 @@ function completeValue( return executeFields( exeContext, - returnType, result, plan ); @@ -522,7 +515,6 @@ function completeValue( return executeFields( exeContext, - runtimeType, result, typePlan ); From 031bc4498d351b84094e20cd19940cca41f5cba5 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 00:10:28 -0800 Subject: [PATCH 31/94] Rename returnType to type to harmonize the name of the type field on Execution plans --- src/execution/execute.js | 2 +- src/execution/plan.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 297b02a500..63c64cd2e1 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -265,7 +265,7 @@ function resolveField( return completeValueCatchingError( exeContext, - plan.returnType, + plan.type, plan.info, result, plan.completionPlan diff --git a/src/execution/plan.js b/src/execution/plan.js index 90cc14b0dc..4209010c47 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -87,6 +87,7 @@ export type CompletionExecutionPlan = */ export type ResolvingExecutionPlan = { kind: 'resolve'; + type: GraphQLOutputType; resolveFn: ( source: mixed, args: { [key: string]: mixed }, @@ -94,7 +95,6 @@ export type ResolvingExecutionPlan = { ) => mixed; args: { [key: string]: mixed }; info: GraphQLResolveInfo; - returnType: GraphQLOutputType; fieldASTs: Array; completionPlan: CompletionExecutionPlan; } @@ -279,7 +279,7 @@ function planResolveField( resolveFn, args, info, - returnType, + type: returnType, fieldASTs, completionPlan }; From 8eb0381b623a7394496303389e6f47331e5b634b Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 00:14:02 -0800 Subject: [PATCH 32/94] Remove more caller type propigation in favor of the Execution plan --- src/execution/execute.js | 5 ----- src/execution/plan.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 63c64cd2e1..e30abe18e3 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -156,12 +156,10 @@ function executeFieldsSerially( plan: SelectionExecutionPlan ): Promise { const fields = plan.fieldPlans; - const parentType = plan.type; return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { const result = resolveField( exeContext, - parentType, sourceValue, fields[responseName] ); @@ -191,14 +189,12 @@ function executeFields( plan: SelectionExecutionPlan ): Object { const fields = plan.fieldPlans; - const parentType = plan.type; let containsPromise = false; const finalResults = Object.keys(fields).reduce( (results, responseName) => { const result = resolveField( exeContext, - parentType, sourceValue, fields[responseName] ); @@ -254,7 +250,6 @@ function promiseForObject( */ function resolveField( exeContext: ExecutionContext, - parentType: GraphQLObjectType, source: mixed, plan: ResolvingExecutionPlan ): mixed { diff --git a/src/execution/plan.js b/src/execution/plan.js index 4209010c47..6086d945f3 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -276,10 +276,10 @@ function planResolveField( const plan: ResolvingExecutionPlan = { kind: 'resolve', + type: returnType, resolveFn, args, info, - type: returnType, fieldASTs, completionPlan }; From cae4343873b4515376d1cc9d5930020526074366 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 00:34:32 -0800 Subject: [PATCH 33/94] Brain dump on what is left to complete --- src/execution/execute.js | 13 +++++++++++++ src/execution/plan.js | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index e30abe18e3..d765e3c461 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,6 +8,19 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +// @TODO: Eliminate the propagation of info and type to CompleteValue +// @TODO: Merge GraphQLResolveInfo and ResolvingExecutionPlan +// @TODO: Document parallels between CompleteValue and planCompletion +// @TODO: Move Execution Plan Types to co-locate with GraphQLResolveInfo +// @TODO: Create an example of prefetching based on Execution plan +// @TODO: Add error messages for unreachable Conditions +// @TODO: Refactor away planSelectionToo +// @TODO: Debug the reduce code in plan.js +// @TODO: Declare a resolveFn type +// @TODO: Review against the specification +// @TODO: Add invariants to execution for conditions tested during planning +// @TODO: Review null bails to eliminate flowtype boilerplate + import { GraphQLError, locatedError } from '../error'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; diff --git a/src/execution/plan.js b/src/execution/plan.js index 6086d945f3..a61a4105d0 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -55,7 +55,8 @@ export type MappingExecutionPlan = { type: GraphQLType; fieldASTs: Array; innerType: GraphQLType; - innerCompletionPlan: CompletionExecutionPlan; // Is this really so broad? + // @TODO Is this really so broad? + innerCompletionPlan: CompletionExecutionPlan; } /** @@ -144,7 +145,7 @@ function planSelection( const plan: SelectionExecutionPlan = { kind: 'select', type, - fieldASTs: [], // I don't know what to pass here + fieldASTs: [], // @TODO: I don't know what to pass here strategy, fieldPlans }; From 4b3db81546b2055d8a4bdf4e2226780d7beb2f7d Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 09:28:03 -0800 Subject: [PATCH 34/94] Document parallels between CompleteValue and planCompletion and Add invariants to execution for conditions tested during planning --- src/execution/execute.js | 46 ++++++++++++++++++++++++++++++++++------ src/execution/plan.js | 44 ++++++++++++++++++++++++-------------- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index d765e3c461..20ef1e52d5 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -10,7 +10,6 @@ // @TODO: Eliminate the propagation of info and type to CompleteValue // @TODO: Merge GraphQLResolveInfo and ResolvingExecutionPlan -// @TODO: Document parallels between CompleteValue and planCompletion // @TODO: Move Execution Plan Types to co-locate with GraphQLResolveInfo // @TODO: Create an example of prefetching based on Execution plan // @TODO: Add error messages for unreachable Conditions @@ -18,15 +17,20 @@ // @TODO: Debug the reduce code in plan.js // @TODO: Declare a resolveFn type // @TODO: Review against the specification -// @TODO: Add invariants to execution for conditions tested during planning // @TODO: Review null bails to eliminate flowtype boilerplate +// The Execution Plan Hierarchy mirrors the schema herarchy, not the +// query result set, exactly what you would want when trying to pre-fetch +// from a resolver. + import { GraphQLError, locatedError } from '../error'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; import { + GraphQLScalarType, GraphQLObjectType, + GraphQLEnumType, GraphQLList, GraphQLNonNull, isAbstractType @@ -370,6 +374,8 @@ function completeValue( result: mixed, plan: CompletionExecutionPlan ): mixed { + + // --- CASE A: Promise (Execution time only, no plan) // If result is a Promise, apply-lift over completeValue. if (isThenable(result)) { return ((result: any): Promise).then( @@ -386,11 +392,13 @@ function completeValue( ); } + // --- CASE B: Error (Execution time only, no plan) // If result is an Error, throw a located error. if (result instanceof Error) { throw locatedError(result, plan.fieldASTs); } + // --- CASE C: GraphQLNonNull (No plan, structural. See planCompleteValue) // If field type is NonNull, complete for inner type, and throw field error // if result is null. if (returnType instanceof GraphQLNonNull) { @@ -411,17 +419,32 @@ function completeValue( return completed; } + // --- CASE D: Nullish (Execution time only, no plan) // If result is null-like, return null. if (isNullish(result)) { return null; } switch (plan.kind) { + + // --- CASE E: Serialize (run SerializationExecutionPlan) + // If result is null-like, return null. case 'serialize': + // Tested in planCompleteValue + invariant(returnType instanceof GraphQLScalarType || + returnType instanceof GraphQLEnumType); + invariant(returnType.serialize, 'Missing serialize method on type'); + const serializedResult = returnType.serialize(result); return isNullish(serializedResult) ? null : serializedResult; + + // --- CASE F: GraphQLList (run MappingExecutionPlan) + // If result is null-like, return null. case 'map': + // Tested in planCompleteValue + invariant(returnType instanceof GraphQLList); + invariant( Array.isArray(result), 'User Error: expected iterable, but did not find one ' + @@ -432,8 +455,6 @@ function completeValue( const innerCompletionPlan = plan.innerCompletionPlan; - invariant(returnType instanceof GraphQLList); - // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another // Promise. @@ -456,8 +477,10 @@ function completeValue( return containsPromise ? Promise.all(completedResults) : completedResults; - case 'select': + // --- CASE G: GraphQLObjectType (run SelectionExecutionPlan) + case 'select': + // Tested in planCompleteValue invariant(returnType instanceof GraphQLObjectType); // If there is an isTypeOf predicate function, call it with the @@ -475,13 +498,16 @@ function completeValue( result, plan ); + + // --- CASE H: isAbstractType (run CoercionExecutionPlan) case 'coerce': + // Tested in planCompleteValue + invariant(isAbstractType(returnType)); + // Field type must be Object, Interface or Union and expect // sub-selections. let runtimeType: ?GraphQLObjectType; - invariant(isAbstractType(returnType)); - const abstractType = ((returnType: any): GraphQLAbstractType); runtimeType = abstractType.getObjectType(result, info); @@ -526,7 +552,13 @@ function completeValue( result, typePlan ); + + // --- CASE Z: Unreachable + // We have handled all possibilities. Not reachable + default: + invariant(false); } + } /** diff --git a/src/execution/plan.js b/src/execution/plan.js index a61a4105d0..a736dab91e 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -289,7 +289,7 @@ function planResolveField( } /** - * Implements the instructions for completeValue as defined in the + * Plans the Execution of completeValue as defined in the * "Field entries" section of the spec. * * If the field type is Non-Null, then this recursively completes the value @@ -312,6 +312,11 @@ function planCompleteValue( fieldASTs: Array ): CompletionExecutionPlan { + // --- CASE A: Promise (Execution time only, see completeValue) + + // --- CASE B: Error (Execution time only, see completeValue) + + // --- CASE C: GraphQLNonNull (Structural, see completeValue) // If field type is NonNull, complete for inner type if (returnType instanceof GraphQLNonNull) { return planCompleteValue( @@ -321,6 +326,25 @@ function planCompleteValue( ); } + // --- CASE D: Nullish (Execution time only, see completeValue) + + // --- CASE E: Serialize (See completeValue for plan Execution) + // If field type is Scalar or Enum, serialize to a valid value, returning + // null if serialization is not possible. + if (returnType instanceof GraphQLScalarType || + returnType instanceof GraphQLEnumType) { + invariant(returnType.serialize, 'Missing serialize method on type'); + + const plan: SerializationExecutionPlan = { + kind: 'serialize', + type: returnType, + fieldASTs + }; + + return plan; + } + + // --- CASE F: GraphQLList (See completeValue for plan Execution) // If field type is List, complete each item in the list with the inner type if (returnType instanceof GraphQLList) { const innerType = returnType.ofType; @@ -339,21 +363,7 @@ function planCompleteValue( return plan; } - // If field type is Scalar or Enum, serialize to a valid value, returning - // null if serialization is not possible. - if (returnType instanceof GraphQLScalarType || - returnType instanceof GraphQLEnumType) { - invariant(returnType.serialize, 'Missing serialize method on type'); - - const plan: SerializationExecutionPlan = { - kind: 'serialize', - type: returnType, - fieldASTs - }; - - return plan; - } - + // --- CASE G: GraphQLObjectType (See completeValue for plan Execution) if (returnType instanceof GraphQLObjectType) { return planSelectionToo( exeContext, @@ -362,6 +372,7 @@ function planCompleteValue( ); } + // --- CASE H: isAbstractType (run CoercionExecutionPlan) if (isAbstractType(returnType)) { const abstractType = ((returnType: any): GraphQLAbstractType); const possibleTypes = abstractType.getPossibleTypes(); @@ -395,6 +406,7 @@ function planCompleteValue( return plan; } + // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable invariant(false); } From bef596b04dbd05cba1e75574cd4164ef61aedbb8 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 09:51:22 -0800 Subject: [PATCH 35/94] Remove innerType, it does not provide useful additional information --- src/execution/plan.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index a736dab91e..18d094c8fe 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -54,7 +54,6 @@ export type MappingExecutionPlan = { kind: 'map'; type: GraphQLType; fieldASTs: Array; - innerType: GraphQLType; // @TODO Is this really so broad? innerCompletionPlan: CompletionExecutionPlan; } @@ -192,7 +191,7 @@ function planSelectionToo( } /** - * Implements the "Evaluating selection sets" section of the spec + * Plan the "Evaluating selection sets" section of the spec */ function planFields( exeContext: ExecutionContext, @@ -232,7 +231,7 @@ function planFields( } /** - * Plan how to a field. + * Plan how to resolve a field. */ function planResolveField( exeContext: ExecutionContext, @@ -356,7 +355,6 @@ function planCompleteValue( kind: 'map', type: returnType, fieldASTs, - innerType, innerCompletionPlan }; From 7fe185e209a9e8a473566de38599a28884e29014 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 10:04:18 -0800 Subject: [PATCH 36/94] Additional Documentation --- src/execution/execute.js | 7 ++-- src/execution/plan.js | 72 +++++++++++++++++++++++----------------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 20ef1e52d5..807ff2c6a2 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -18,8 +18,10 @@ // @TODO: Declare a resolveFn type // @TODO: Review against the specification // @TODO: Review null bails to eliminate flowtype boilerplate +// @TODO: Resolve select and coerce discrepancy around calling isTypeOf +// @TODO: Can Execution Context be eliminated in favor of only Plans -// The Execution Plan Hierarchy mirrors the schema herarchy, not the +// The Execution Plan Hierarchy mirrors the schema hierarchy, not the // query result set, exactly what you would want when trying to pre-fetch // from a resolver. @@ -58,7 +60,6 @@ import type { ResolvingExecutionPlan } from './plan'; - /** * Terminology * @@ -121,6 +122,8 @@ export function execute( operationName ); + // Create an Execution plan which will describe how we intend to execute + // The query. const plan = planOperation(context, context.operation); // Return a Promise that will eventually resolve to the data described by diff --git a/src/execution/plan.js b/src/execution/plan.js index 18d094c8fe..9aad78e359 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -49,16 +49,38 @@ import { } from '../type/introspection'; /** + * Describes how the execution engine plans to interact with the schema's + * resolve function to fetch field values indicated by the query */ -export type MappingExecutionPlan = { - kind: 'map'; - type: GraphQLType; +export type ResolvingExecutionPlan = { + kind: 'resolve'; + type: GraphQLOutputType; + resolveFn: ( + source: mixed, + args: { [key: string]: mixed }, + info: GraphQLResolveInfo + ) => mixed; + args: { [key: string]: mixed }; + info: GraphQLResolveInfo; fieldASTs: Array; - // @TODO Is this really so broad? - innerCompletionPlan: CompletionExecutionPlan; + completionPlan: CompletionExecutionPlan; } /** + * Describes how the execution engine plans to perform a selection + * on a resolved value. + */ +export type SelectionExecutionPlan = { + kind: 'select'; + type: GraphQLObjectType; + fieldASTs: Array; + strategy: string; + fieldPlans: {[key: string]: ResolvingExecutionPlan}; +} + +/** + * Indicates that the execution engine plans to call + * serialize for a value. */ export type SerializationExecutionPlan = { kind: 'serialize'; @@ -67,6 +89,19 @@ export type SerializationExecutionPlan = { } /** + * Describes how the execution engine plans to map list values + */ +export type MappingExecutionPlan = { + kind: 'map'; + type: GraphQLType; + fieldASTs: Array; + // @TODO Is this really so broad? + innerCompletionPlan: CompletionExecutionPlan; +} + +/** + * Describes plans which the execution engine may take + * based on the run time type of a value. */ export type CoercionExecutionPlan = { kind: 'coerce'; @@ -76,6 +111,7 @@ export type CoercionExecutionPlan = { } /** + * Execution plans which might be executed to Complete a value. */ export type CompletionExecutionPlan = SelectionExecutionPlan | @@ -83,32 +119,6 @@ export type CompletionExecutionPlan = MappingExecutionPlan | CoercionExecutionPlan; -/** - */ -export type ResolvingExecutionPlan = { - kind: 'resolve'; - type: GraphQLOutputType; - resolveFn: ( - source: mixed, - args: { [key: string]: mixed }, - info: GraphQLResolveInfo - ) => mixed; - args: { [key: string]: mixed }; - info: GraphQLResolveInfo; - fieldASTs: Array; - completionPlan: CompletionExecutionPlan; -} - -/** - */ -export type SelectionExecutionPlan = { - kind: 'select'; - type: GraphQLObjectType; - fieldASTs: Array; - strategy: string; - fieldPlans: {[key: string]: ResolvingExecutionPlan}; -} - /** * Create a plan based on the "Evaluating operations" section of the spec. */ From f6f035c1e6c713601dd006f799cfc5f0648f45e5 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 10:29:03 -0800 Subject: [PATCH 37/94] Add fields from GraphQLResolveInfo to ResolvingExecutionPlan --- src/execution/plan.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index 9aad78e359..8f0c852d56 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -28,6 +28,7 @@ import type { GraphQLAbstractType, GraphQLType, GraphQLOutputType, + GraphQLCompositeType, GraphQLResolveInfo } from '../type/definition'; import { @@ -54,7 +55,16 @@ import { */ export type ResolvingExecutionPlan = { kind: 'resolve'; - type: GraphQLOutputType; + fieldName: string, + fieldASTs: Array; + returnType: GraphQLOutputType, + parentType: GraphQLCompositeType, + schema: GraphQLSchema, + fragments: { [fragmentName: string]: FragmentDefinition }, + rootValue: mixed, + operation: OperationDefinition, + variableValues: { [variableName: string]: mixed }, + resolveFn: ( source: mixed, args: { [key: string]: mixed }, @@ -62,8 +72,10 @@ export type ResolvingExecutionPlan = { ) => mixed; args: { [key: string]: mixed }; info: GraphQLResolveInfo; - fieldASTs: Array; + completionPlan: CompletionExecutionPlan; + + type: GraphQLOutputType; } /** @@ -286,11 +298,19 @@ function planResolveField( const plan: ResolvingExecutionPlan = { kind: 'resolve', + fieldName, + fieldASTs, + returnType, + parentType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, type: returnType, resolveFn, args, info, - fieldASTs, completionPlan }; From 4538e1970dd6e9fabe28a16f593f296507105ef9 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 10:34:26 -0800 Subject: [PATCH 38/94] Remove duplicate type information --- src/execution/execute.js | 2 +- src/execution/plan.js | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 807ff2c6a2..9c99f2e9fa 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -280,7 +280,7 @@ function resolveField( return completeValueCatchingError( exeContext, - plan.type, + plan.returnType, plan.info, result, plan.completionPlan diff --git a/src/execution/plan.js b/src/execution/plan.js index 8f0c852d56..a142f63560 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -64,7 +64,6 @@ export type ResolvingExecutionPlan = { rootValue: mixed, operation: OperationDefinition, variableValues: { [variableName: string]: mixed }, - resolveFn: ( source: mixed, args: { [key: string]: mixed }, @@ -72,10 +71,7 @@ export type ResolvingExecutionPlan = { ) => mixed; args: { [key: string]: mixed }; info: GraphQLResolveInfo; - completionPlan: CompletionExecutionPlan; - - type: GraphQLOutputType; } /** @@ -307,7 +303,6 @@ function planResolveField( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - type: returnType, resolveFn, args, info, From a8aef5f3365cda6e97e66b0e8c6d0b58ff5b17e5 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 10:37:17 -0800 Subject: [PATCH 39/94] There is already a resolver signature defined --- src/execution/execute.js | 1 - src/execution/plan.js | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 9c99f2e9fa..5a0a6343c4 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -15,7 +15,6 @@ // @TODO: Add error messages for unreachable Conditions // @TODO: Refactor away planSelectionToo // @TODO: Debug the reduce code in plan.js -// @TODO: Declare a resolveFn type // @TODO: Review against the specification // @TODO: Review null bails to eliminate flowtype boilerplate // @TODO: Resolve select and coerce discrepancy around calling isTypeOf diff --git a/src/execution/plan.js b/src/execution/plan.js index a142f63560..a50ced0a0f 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -29,6 +29,7 @@ import type { GraphQLType, GraphQLOutputType, GraphQLCompositeType, + GraphQLFieldResolveFn, GraphQLResolveInfo } from '../type/definition'; import { @@ -64,11 +65,7 @@ export type ResolvingExecutionPlan = { rootValue: mixed, operation: OperationDefinition, variableValues: { [variableName: string]: mixed }, - resolveFn: ( - source: mixed, - args: { [key: string]: mixed }, - info: GraphQLResolveInfo - ) => mixed; + resolveFn: GraphQLFieldResolveFn; args: { [key: string]: mixed }; info: GraphQLResolveInfo; completionPlan: CompletionExecutionPlan; From 6ae2dba52fc68a52de099d56d9d9cee05711e48f Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 11:17:24 -0800 Subject: [PATCH 40/94] Organize TODO list --- src/execution/execute.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 5a0a6343c4..68e900d320 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,17 +8,22 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +// @TODO: Remove resolveFn from ResolvingExecutionPlan +// @TODO: Extract CheckType, ResolveType, CompleteList, CompleteSelect +// @TODO: Merge GraphQLResolveInfo fields into CoercionExecutionPlan +// @TODO: Merge GraphQLResolveInfo fields into SelectionExecutionPlan // @TODO: Eliminate the propagation of info and type to CompleteValue -// @TODO: Merge GraphQLResolveInfo and ResolvingExecutionPlan // @TODO: Move Execution Plan Types to co-locate with GraphQLResolveInfo -// @TODO: Create an example of prefetching based on Execution plan // @TODO: Add error messages for unreachable Conditions // @TODO: Refactor away planSelectionToo // @TODO: Debug the reduce code in plan.js -// @TODO: Review against the specification // @TODO: Review null bails to eliminate flowtype boilerplate -// @TODO: Resolve select and coerce discrepancy around calling isTypeOf // @TODO: Can Execution Context be eliminated in favor of only Plans +// @TODO: Review against the specification +// @TODO: Create an example of prefetching based on Execution plan +// @TODO: Undo file split? +// @TODO: Distinction without a difference: +// @TODO: Make the final pull diff easier to read // The Execution Plan Hierarchy mirrors the schema hierarchy, not the // query result set, exactly what you would want when trying to pre-fetch @@ -427,6 +432,7 @@ function completeValue( return null; } + // Execution Completion Plan switch (plan.kind) { // --- CASE E: Serialize (run SerializationExecutionPlan) From 52cc6230782f550e9ae1e0d307ba1388f50f71b3 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 15:29:34 -0800 Subject: [PATCH 41/94] Harmonize and document plan terminology --- src/execution/execute.js | 80 ++++++++++++++++++++++++++++++++-------- src/execution/plan.js | 66 +++++++++++++++++++-------------- 2 files changed, 103 insertions(+), 43 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 68e900d320..ccaee10ab3 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,10 +8,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Remove resolveFn from ResolvingExecutionPlan +// @TODO: Remove resolveFn from GraphQLFieldResolvingPlan // @TODO: Extract CheckType, ResolveType, CompleteList, CompleteSelect -// @TODO: Merge GraphQLResolveInfo fields into CoercionExecutionPlan -// @TODO: Merge GraphQLResolveInfo fields into SelectionExecutionPlan +// @TODO: Merge GraphQLResolveInfo fields into GraphQLTypeResolvingPlan +// @TODO: Merge GraphQLResolveInfo fields into GraphQLSelectionCompletionPlan // @TODO: Eliminate the propagation of info and type to CompleteValue // @TODO: Move Execution Plan Types to co-locate with GraphQLResolveInfo // @TODO: Add error messages for unreachable Conditions @@ -59,9 +59,9 @@ import { } from './plan'; import type { - CompletionExecutionPlan, - SelectionExecutionPlan, - ResolvingExecutionPlan + GraphQLCompletionPlan, + GraphQLSelectionCompletionPlan, + GraphQLFieldResolvingPlan } from './plan'; /** @@ -84,6 +84,32 @@ import type { * 3) inline fragment "spreads" e.g. "...on Type { a }" */ +/** + * Queries are evaluated in two phases: planning and execution. + * The goal of planning is to precompute data needed for execution + * and to give resolvers insight into how the query will be + * executed + * + * Execution uses the following terms: + * + * "execute" indicates evaluating an operation + * "resolve" indicates resolving a field or type value + * "complete" indicates processing return values from the resolving process. + * + * The planning and execution phases are coupled in this way: + * +------------------+-------------------+------------------------------------+ + * | Execution | Planning | Plan | + * +------------------+-------------------+------------------------------------+ + * | executeOperation | planOperation | GraphQLOperationExecutionPlan | + * | resolveField | planFields | GraphQLFieldResolvingPlan | + * | completeValue | planCompleteValue | GraphQLCompletionPlan | + * | resolveType | planCompleteValue | GraphQLTypeResolvingPlan | + * | completeList | planCompleteValue | GraphQLListCompletionPlan | + * | completeValue | planCompleteValue | GraphQLSerializationCompletionPlan | + * | completeValue | planSelection | GraphQLSelectionCompletionPlan | + * +------------------+-------------------+------------------------------------+ + */ + /** * The result of execution. `data` is the result of executing the * query, `errors` is null if no errors occurred, and is a @@ -158,7 +184,7 @@ export function execute( */ function executeOperation( exeContext: ExecutionContext, - plan: SelectionExecutionPlan, + plan: GraphQLSelectionCompletionPlan, rootValue: mixed ): Object { @@ -177,7 +203,7 @@ function executeOperation( function executeFieldsSerially( exeContext: ExecutionContext, sourceValue: mixed, - plan: SelectionExecutionPlan + plan: GraphQLSelectionCompletionPlan ): Promise { const fields = plan.fieldPlans; return Object.keys(fields).reduce( @@ -210,7 +236,7 @@ function executeFieldsSerially( function executeFields( exeContext: ExecutionContext, sourceValue: mixed, - plan: SelectionExecutionPlan + plan: GraphQLSelectionCompletionPlan ): Object { const fields = plan.fieldPlans; let containsPromise = false; @@ -275,7 +301,7 @@ function promiseForObject( function resolveField( exeContext: ExecutionContext, source: mixed, - plan: ResolvingExecutionPlan + plan: GraphQLFieldResolvingPlan ): mixed { // Get the resolve function, regardless of if its result is normal @@ -319,7 +345,7 @@ function completeValueCatchingError( returnType: GraphQLType, info: GraphQLResolveInfo, result: mixed, - plan: CompletionExecutionPlan + plan: GraphQLCompletionPlan ): mixed { // If the field type is non-nullable, then it is resolved without any // protection from errors. @@ -356,6 +382,26 @@ function completeValueCatchingError( } } +/** + * Check that a value is of the expected type, raise an error if not + */ +/* +function CheckType( + expectedType:GraphQLType, + value: mixed, + plan: GraphQLCompletionPlan +) { + // 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 (expectedType.isTypeOf && !expectedType.isTypeOf(value, plan.info)) { + throw new GraphQLError( + `Expected value of type "${expectedType}" but got: ${value}.`, + plan.fieldASTs + ); + } +} +*/ /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -379,7 +425,7 @@ function completeValue( returnType: GraphQLType, info: GraphQLResolveInfo, result: mixed, - plan: CompletionExecutionPlan + plan: GraphQLCompletionPlan ): mixed { // --- CASE A: Promise (Execution time only, no plan) @@ -435,9 +481,11 @@ function completeValue( // Execution Completion Plan switch (plan.kind) { - // --- CASE E: Serialize (run SerializationExecutionPlan) + // --- CASE E: Serialize (run GraphQLSerializationCompletionPlan) // If result is null-like, return null. case 'serialize': + // Intentionally first; will be evaluated often + // Tested in planCompleteValue invariant(returnType instanceof GraphQLScalarType || returnType instanceof GraphQLEnumType); @@ -447,7 +495,7 @@ function completeValue( const serializedResult = returnType.serialize(result); return isNullish(serializedResult) ? null : serializedResult; - // --- CASE F: GraphQLList (run MappingExecutionPlan) + // --- CASE F: GraphQLList (run GraphQLListCompletionPlan) // If result is null-like, return null. case 'map': // Tested in planCompleteValue @@ -486,7 +534,7 @@ function completeValue( return containsPromise ? Promise.all(completedResults) : completedResults; - // --- CASE G: GraphQLObjectType (run SelectionExecutionPlan) + // --- CASE G: GraphQLObjectType (run GraphQLSelectionCompletionPlan) case 'select': // Tested in planCompleteValue invariant(returnType instanceof GraphQLObjectType); @@ -507,7 +555,7 @@ function completeValue( plan ); - // --- CASE H: isAbstractType (run CoercionExecutionPlan) + // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) case 'coerce': // Tested in planCompleteValue invariant(isAbstractType(returnType)); diff --git a/src/execution/plan.js b/src/execution/plan.js index a50ced0a0f..97bdfee370 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -54,7 +54,7 @@ import { * Describes how the execution engine plans to interact with the schema's * resolve function to fetch field values indicated by the query */ -export type ResolvingExecutionPlan = { +export type GraphQLFieldResolvingPlan = { kind: 'resolve'; fieldName: string, fieldASTs: Array; @@ -68,26 +68,38 @@ export type ResolvingExecutionPlan = { resolveFn: GraphQLFieldResolveFn; args: { [key: string]: mixed }; info: GraphQLResolveInfo; - completionPlan: CompletionExecutionPlan; + completionPlan: GraphQLCompletionPlan; } /** * Describes how the execution engine plans to perform a selection * on a resolved value. */ -export type SelectionExecutionPlan = { +export type GraphQLOperationExecutionPlan = { + kind: 'operation'; + type: GraphQLObjectType; + fieldASTs: Array; + strategy: string; + fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; +} + +/** + * Describes how the execution engine plans to perform a selection + * on a resolved value. + */ +export type GraphQLSelectionCompletionPlan = { kind: 'select'; type: GraphQLObjectType; fieldASTs: Array; strategy: string; - fieldPlans: {[key: string]: ResolvingExecutionPlan}; + fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; } /** * Indicates that the execution engine plans to call * serialize for a value. */ -export type SerializationExecutionPlan = { +export type GraphQLSerializationCompletionPlan = { kind: 'serialize'; type: GraphQLType; fieldASTs: Array; @@ -96,33 +108,33 @@ export type SerializationExecutionPlan = { /** * Describes how the execution engine plans to map list values */ -export type MappingExecutionPlan = { +export type GraphQLListCompletionPlan = { kind: 'map'; type: GraphQLType; fieldASTs: Array; // @TODO Is this really so broad? - innerCompletionPlan: CompletionExecutionPlan; + innerCompletionPlan: GraphQLCompletionPlan; } /** * Describes plans which the execution engine may take * based on the run time type of a value. */ -export type CoercionExecutionPlan = { +export type GraphQLTypeResolvingPlan = { kind: 'coerce'; type: GraphQLType; fieldASTs: Array; - typePlans: {[key: string]:SelectionExecutionPlan}; + typePlans: {[key: string]:GraphQLSelectionCompletionPlan}; } /** * Execution plans which might be executed to Complete a value. */ -export type CompletionExecutionPlan = - SelectionExecutionPlan | - SerializationExecutionPlan | - MappingExecutionPlan | - CoercionExecutionPlan; +export type GraphQLCompletionPlan = + GraphQLSelectionCompletionPlan | + GraphQLSerializationCompletionPlan | + GraphQLListCompletionPlan | + GraphQLTypeResolvingPlan; /** * Create a plan based on the "Evaluating operations" section of the spec. @@ -130,7 +142,7 @@ export type CompletionExecutionPlan = export function planOperation( exeContext: ExecutionContext, operation: OperationDefinition -): SelectionExecutionPlan { +): GraphQLSelectionCompletionPlan { const type = getOperationRootType(exeContext.schema, operation); const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; @@ -145,7 +157,7 @@ function planSelection( type: GraphQLObjectType, selectionSet: SelectionSet, strategy: string = 'parallel' -): SelectionExecutionPlan { +): GraphQLSelectionCompletionPlan { const fields = collectFields( exeContext, type, @@ -156,7 +168,7 @@ function planSelection( const fieldPlans = planFields(exeContext, type, fields); - const plan: SelectionExecutionPlan = { + const plan: GraphQLSelectionCompletionPlan = { kind: 'select', type, fieldASTs: [], // @TODO: I don't know what to pass here @@ -175,7 +187,7 @@ function planSelectionToo( type: GraphQLObjectType, fieldASTs: Array, strategy: string = 'parallel' -): SelectionExecutionPlan { +): GraphQLSelectionCompletionPlan { let fields = Object.create(null); const visitedFragmentNames = Object.create(null); @@ -194,7 +206,7 @@ function planSelectionToo( const fieldPlans = planFields(exeContext, type, fields); - const plan: SelectionExecutionPlan = { + const plan: GraphQLSelectionCompletionPlan = { kind: 'select', type, fieldASTs, @@ -212,7 +224,7 @@ function planFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, fields: {[key: string]: Array} -): {[key: string]: ResolvingExecutionPlan} { +): {[key: string]: GraphQLFieldResolvingPlan} { /* const finalResults = Object.keys(fields).reduce( (results, responseName) => { @@ -252,7 +264,7 @@ function planResolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, fieldASTs: Array -): ?ResolvingExecutionPlan { +): ?GraphQLFieldResolvingPlan { const fieldAST = fieldASTs[0]; const fieldName = fieldAST.name.value; @@ -289,7 +301,7 @@ function planResolveField( const completionPlan = planCompleteValue(exeContext, returnType, fieldASTs); - const plan: ResolvingExecutionPlan = { + const plan: GraphQLFieldResolvingPlan = { kind: 'resolve', fieldName, fieldASTs, @@ -331,7 +343,7 @@ function planCompleteValue( exeContext: ExecutionContext, returnType: GraphQLType, fieldASTs: Array -): CompletionExecutionPlan { +): GraphQLCompletionPlan { // --- CASE A: Promise (Execution time only, see completeValue) @@ -356,7 +368,7 @@ function planCompleteValue( returnType instanceof GraphQLEnumType) { invariant(returnType.serialize, 'Missing serialize method on type'); - const plan: SerializationExecutionPlan = { + const plan: GraphQLSerializationCompletionPlan = { kind: 'serialize', type: returnType, fieldASTs @@ -373,7 +385,7 @@ function planCompleteValue( const innerCompletionPlan = planCompleteValue(exeContext, innerType, fieldASTs); - const plan: MappingExecutionPlan = { + const plan: GraphQLListCompletionPlan = { kind: 'map', type: returnType, fieldASTs, @@ -392,7 +404,7 @@ function planCompleteValue( ); } - // --- CASE H: isAbstractType (run CoercionExecutionPlan) + // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) if (isAbstractType(returnType)) { const abstractType = ((returnType: any): GraphQLAbstractType); const possibleTypes = abstractType.getPossibleTypes(); @@ -416,7 +428,7 @@ function planCompleteValue( ); }); - const plan: CoercionExecutionPlan = { + const plan: GraphQLTypeResolvingPlan = { kind: 'coerce', type: returnType, fieldASTs, From a61e3723aa7bd81c13bcba280799f6c19ffe89d3 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 15:39:20 -0800 Subject: [PATCH 42/94] Wire up the OperationExecutionPlan --- src/execution/execute.js | 18 ++++++++---------- src/execution/plan.js | 28 ++++++++-------------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index ccaee10ab3..38e43114f1 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -60,7 +60,7 @@ import { import type { GraphQLCompletionPlan, - GraphQLSelectionCompletionPlan, + GraphQLOperationExecutionPlan, GraphQLFieldResolvingPlan } from './plan'; @@ -184,16 +184,16 @@ export function execute( */ function executeOperation( exeContext: ExecutionContext, - plan: GraphQLSelectionCompletionPlan, + plan: GraphQLOperationExecutionPlan, rootValue: mixed ): Object { invariant(plan.strategy === 'serial' || plan.strategy === 'parallel'); if (plan.strategy === 'serial') { - return executeFieldsSerially(exeContext, rootValue, plan); + return executeFieldsSerially(exeContext, rootValue, plan.fieldPlans); } - return executeFields(exeContext, rootValue, plan); + return executeFields(exeContext, rootValue, plan.fieldPlans); } /** @@ -203,9 +203,8 @@ function executeOperation( function executeFieldsSerially( exeContext: ExecutionContext, sourceValue: mixed, - plan: GraphQLSelectionCompletionPlan + fields: {[key: string]: GraphQLFieldResolvingPlan} ): Promise { - const fields = plan.fieldPlans; return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { const result = resolveField( @@ -236,9 +235,8 @@ function executeFieldsSerially( function executeFields( exeContext: ExecutionContext, sourceValue: mixed, - plan: GraphQLSelectionCompletionPlan + fields: {[key: string]: GraphQLFieldResolvingPlan} ): Object { - const fields = plan.fieldPlans; let containsPromise = false; const finalResults = Object.keys(fields).reduce( @@ -552,7 +550,7 @@ function completeValue( return executeFields( exeContext, result, - plan + plan.fieldPlans ); // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) @@ -606,7 +604,7 @@ function completeValue( return executeFields( exeContext, result, - typePlan + typePlan.fieldPlans ); // --- CASE Z: Unreachable diff --git a/src/execution/plan.js b/src/execution/plan.js index 97bdfee370..11b0aba013 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -142,34 +142,22 @@ export type GraphQLCompletionPlan = export function planOperation( exeContext: ExecutionContext, operation: OperationDefinition -): GraphQLSelectionCompletionPlan { +): GraphQLOperationExecutionPlan { const type = getOperationRootType(exeContext.schema, operation); const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; - return planSelection(exeContext, type, operation.selectionSet, strategy); -} - -/** - * Create a plan based on the "Evaluating operations" section of the spec. - */ -function planSelection( - exeContext: ExecutionContext, - type: GraphQLObjectType, - selectionSet: SelectionSet, - strategy: string = 'parallel' -): GraphQLSelectionCompletionPlan { const fields = collectFields( exeContext, type, - selectionSet, + operation.selectionSet, Object.create(null), Object.create(null) ); const fieldPlans = planFields(exeContext, type, fields); - const plan: GraphQLSelectionCompletionPlan = { - kind: 'select', + const plan: GraphQLOperationExecutionPlan = { + kind: 'operation', type, fieldASTs: [], // @TODO: I don't know what to pass here strategy, @@ -182,7 +170,7 @@ function planSelection( /** * Create a plan based on the "Evaluating operations" section of the spec. */ -function planSelectionToo( +function planSelection( exeContext: ExecutionContext, type: GraphQLObjectType, fieldASTs: Array, @@ -397,7 +385,7 @@ function planCompleteValue( // --- CASE G: GraphQLObjectType (See completeValue for plan Execution) if (returnType instanceof GraphQLObjectType) { - return planSelectionToo( + return planSelection( exeContext, returnType, fieldASTs @@ -412,7 +400,7 @@ function planCompleteValue( const typePlans = Object.create(null); /** const typePlans = possibleTypes.reduce((result, possibleType) => { - result[possibleType.name] = planSelectionToo( + result[possibleType.name] = planSelection( exeContext, possibleType, fieldASTs @@ -421,7 +409,7 @@ function planCompleteValue( */ possibleTypes.forEach(possibleType => { - typePlans[possibleType.name] = planSelectionToo( + typePlans[possibleType.name] = planSelection( exeContext, possibleType, fieldASTs From d984c2812b1c484aa63628b7ccf5246a16da1eed Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 15:40:49 -0800 Subject: [PATCH 43/94] Remove unnecessary strategy in selection plan --- src/execution/execute.js | 1 - src/execution/plan.js | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 38e43114f1..f465dd0a06 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -15,7 +15,6 @@ // @TODO: Eliminate the propagation of info and type to CompleteValue // @TODO: Move Execution Plan Types to co-locate with GraphQLResolveInfo // @TODO: Add error messages for unreachable Conditions -// @TODO: Refactor away planSelectionToo // @TODO: Debug the reduce code in plan.js // @TODO: Review null bails to eliminate flowtype boilerplate // @TODO: Can Execution Context be eliminated in favor of only Plans diff --git a/src/execution/plan.js b/src/execution/plan.js index 11b0aba013..b83ab503e4 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -91,7 +91,6 @@ export type GraphQLSelectionCompletionPlan = { kind: 'select'; type: GraphQLObjectType; fieldASTs: Array; - strategy: string; fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; } @@ -173,8 +172,7 @@ export function planOperation( function planSelection( exeContext: ExecutionContext, type: GraphQLObjectType, - fieldASTs: Array, - strategy: string = 'parallel' + fieldASTs: Array ): GraphQLSelectionCompletionPlan { let fields = Object.create(null); @@ -198,7 +196,6 @@ function planSelection( kind: 'select', type, fieldASTs, - strategy, fieldPlans }; From 2a816db92b0ee49f8b1624fc38669f4b0feebc79 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 16:41:50 -0800 Subject: [PATCH 44/94] Merge GraphQLResolveInfo fields into GraphQLTypeResolvingPlan --- src/execution/execute.js | 4 ++-- src/execution/plan.js | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index f465dd0a06..bef36c2ae5 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,10 +8,9 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Remove resolveFn from GraphQLFieldResolvingPlan -// @TODO: Extract CheckType, ResolveType, CompleteList, CompleteSelect // @TODO: Merge GraphQLResolveInfo fields into GraphQLTypeResolvingPlan // @TODO: Merge GraphQLResolveInfo fields into GraphQLSelectionCompletionPlan +// @TODO: Extract CheckType, ResolveType, CompleteList, CompleteSelect // @TODO: Eliminate the propagation of info and type to CompleteValue // @TODO: Move Execution Plan Types to co-locate with GraphQLResolveInfo // @TODO: Add error messages for unreachable Conditions @@ -23,6 +22,7 @@ // @TODO: Undo file split? // @TODO: Distinction without a difference: // @TODO: Make the final pull diff easier to read +// @TODO: Remove resolveFn from GraphQLFieldResolvingPlan // The Execution Plan Hierarchy mirrors the schema hierarchy, not the // query result set, exactly what you would want when trying to pre-fetch diff --git a/src/execution/plan.js b/src/execution/plan.js index b83ab503e4..7e5aebeb3b 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -58,13 +58,13 @@ export type GraphQLFieldResolvingPlan = { kind: 'resolve'; fieldName: string, fieldASTs: Array; - returnType: GraphQLOutputType, - parentType: GraphQLCompositeType, - schema: GraphQLSchema, - fragments: { [fragmentName: string]: FragmentDefinition }, - rootValue: mixed, - operation: OperationDefinition, - variableValues: { [variableName: string]: mixed }, + returnType: GraphQLOutputType; + parentType: GraphQLCompositeType; + schema: GraphQLSchema; + fragments: { [fragmentName: string]: FragmentDefinition }; + rootValue: mixed; + operation: OperationDefinition; + variableValues: { [variableName: string]: mixed }; resolveFn: GraphQLFieldResolveFn; args: { [key: string]: mixed }; info: GraphQLResolveInfo; @@ -121,8 +121,17 @@ export type GraphQLListCompletionPlan = { */ export type GraphQLTypeResolvingPlan = { kind: 'coerce'; - type: GraphQLType; +// @TODO +// fieldName: string, fieldASTs: Array; + returnType: GraphQLCompositeType; +// @TODO +// parentType: GraphQLCompositeType, + schema: GraphQLSchema; + fragments: { [fragmentName: string]: FragmentDefinition }; + rootValue: mixed; + operation: OperationDefinition; + variableValues: { [variableName: string]: mixed }; typePlans: {[key: string]:GraphQLSelectionCompletionPlan}; } @@ -413,10 +422,21 @@ function planCompleteValue( ); }); +// @TODO: Still not sure what the right type is to return here +// maybe returnType is actually Parent type? const plan: GraphQLTypeResolvingPlan = { kind: 'coerce', - type: returnType, +// @TODO +// fieldName, fieldASTs, + returnType: abstractType, +// @TODO +// parentType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, typePlans }; From b7fcb3cede3fe4690fac0a5f566e77710bce635b Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 16:56:47 -0800 Subject: [PATCH 45/94] Merge GraphQLResolveInfo fields into GraphQLSelectionCompletionPlan --- src/execution/plan.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/execution/plan.js b/src/execution/plan.js index 7e5aebeb3b..c6f66ae10c 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -89,8 +89,18 @@ export type GraphQLOperationExecutionPlan = { */ export type GraphQLSelectionCompletionPlan = { kind: 'select'; - type: GraphQLObjectType; +// fieldName: string, fieldASTs: Array; +// returnType: GraphQLOutputType; +// parentType: GraphQLCompositeType; + schema: GraphQLSchema; + fragments: { [fragmentName: string]: FragmentDefinition }; + rootValue: mixed; + operation: OperationDefinition; + variableValues: { [variableName: string]: mixed }; + +// type: GraphQLObjectType; + fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; } @@ -203,8 +213,22 @@ function planSelection( const plan: GraphQLSelectionCompletionPlan = { kind: 'select', - type, +// @TODO +// fieldName, fieldASTs, + +// @TODO +// returnType, +// @TODO +// parentType, +// @TODO +// type, + + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, fieldPlans }; From 7a921cdae09ee6e2faaad6b391d07f9445dd3894 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 17:05:39 -0800 Subject: [PATCH 46/94] Move plan type definitions to definitions.js --- src/execution/execute.js | 10 ++-- src/execution/plan.js | 114 +++------------------------------------ src/type/definition.js | 104 +++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 114 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index bef36c2ae5..81b1fb93c8 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -12,7 +12,6 @@ // @TODO: Merge GraphQLResolveInfo fields into GraphQLSelectionCompletionPlan // @TODO: Extract CheckType, ResolveType, CompleteList, CompleteSelect // @TODO: Eliminate the propagation of info and type to CompleteValue -// @TODO: Move Execution Plan Types to co-locate with GraphQLResolveInfo // @TODO: Add error messages for unreachable Conditions // @TODO: Debug the reduce code in plan.js // @TODO: Review null bails to eliminate flowtype boilerplate @@ -44,6 +43,9 @@ import type { GraphQLType, GraphQLAbstractType, GraphQLResolveInfo, + GraphQLCompletionPlan, + GraphQLOperationExecutionPlan, + GraphQLFieldResolvingPlan } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; import type { @@ -57,12 +59,6 @@ import { planOperation } from './plan'; -import type { - GraphQLCompletionPlan, - GraphQLOperationExecutionPlan, - GraphQLFieldResolvingPlan -} from './plan'; - /** * Terminology * diff --git a/src/execution/plan.js b/src/execution/plan.js index c6f66ae10c..5c1d9e0b45 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -27,9 +27,13 @@ import type { GraphQLFieldDefinition, GraphQLAbstractType, GraphQLType, - GraphQLOutputType, - GraphQLCompositeType, - GraphQLFieldResolveFn, + GraphQLOperationExecutionPlan, + GraphQLSelectionCompletionPlan, + GraphQLFieldResolvingPlan, + GraphQLCompletionPlan, + GraphQLSerializationCompletionPlan, + GraphQLListCompletionPlan, + GraphQLTypeResolvingPlan, GraphQLResolveInfo } from '../type/definition'; import { @@ -50,110 +54,6 @@ import { TypeNameMetaFieldDef } from '../type/introspection'; -/** - * Describes how the execution engine plans to interact with the schema's - * resolve function to fetch field values indicated by the query - */ -export type GraphQLFieldResolvingPlan = { - kind: 'resolve'; - fieldName: string, - fieldASTs: Array; - returnType: GraphQLOutputType; - parentType: GraphQLCompositeType; - schema: GraphQLSchema; - fragments: { [fragmentName: string]: FragmentDefinition }; - rootValue: mixed; - operation: OperationDefinition; - variableValues: { [variableName: string]: mixed }; - resolveFn: GraphQLFieldResolveFn; - args: { [key: string]: mixed }; - info: GraphQLResolveInfo; - completionPlan: GraphQLCompletionPlan; -} - -/** - * Describes how the execution engine plans to perform a selection - * on a resolved value. - */ -export type GraphQLOperationExecutionPlan = { - kind: 'operation'; - type: GraphQLObjectType; - fieldASTs: Array; - strategy: string; - fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; -} - -/** - * Describes how the execution engine plans to perform a selection - * on a resolved value. - */ -export type GraphQLSelectionCompletionPlan = { - kind: 'select'; -// fieldName: string, - fieldASTs: Array; -// returnType: GraphQLOutputType; -// parentType: GraphQLCompositeType; - schema: GraphQLSchema; - fragments: { [fragmentName: string]: FragmentDefinition }; - rootValue: mixed; - operation: OperationDefinition; - variableValues: { [variableName: string]: mixed }; - -// type: GraphQLObjectType; - - fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; -} - -/** - * Indicates that the execution engine plans to call - * serialize for a value. - */ -export type GraphQLSerializationCompletionPlan = { - kind: 'serialize'; - type: GraphQLType; - fieldASTs: Array; -} - -/** - * Describes how the execution engine plans to map list values - */ -export type GraphQLListCompletionPlan = { - kind: 'map'; - type: GraphQLType; - fieldASTs: Array; - // @TODO Is this really so broad? - innerCompletionPlan: GraphQLCompletionPlan; -} - -/** - * Describes plans which the execution engine may take - * based on the run time type of a value. - */ -export type GraphQLTypeResolvingPlan = { - kind: 'coerce'; -// @TODO -// fieldName: string, - fieldASTs: Array; - returnType: GraphQLCompositeType; -// @TODO -// parentType: GraphQLCompositeType, - schema: GraphQLSchema; - fragments: { [fragmentName: string]: FragmentDefinition }; - rootValue: mixed; - operation: OperationDefinition; - variableValues: { [variableName: string]: mixed }; - typePlans: {[key: string]:GraphQLSelectionCompletionPlan}; -} - -/** - * Execution plans which might be executed to Complete a value. - */ -export type GraphQLCompletionPlan = - GraphQLSelectionCompletionPlan | - GraphQLSerializationCompletionPlan | - GraphQLListCompletionPlan | - GraphQLTypeResolvingPlan; - /** * Create a plan based on the "Evaluating operations" section of the spec. */ diff --git a/src/type/definition.js b/src/type/definition.js index 2fee4566be..228132f0c8 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -485,6 +485,110 @@ export type GraphQLResolveInfo = { variableValues: { [variableName: string]: mixed }, } +/** + * Describes how the execution engine plans to interact with the schema's + * resolve function to fetch field values indicated by the query + */ +export type GraphQLFieldResolvingPlan = { + kind: 'resolve'; + fieldName: string, + fieldASTs: Array; + returnType: GraphQLOutputType; + parentType: GraphQLCompositeType; + schema: GraphQLSchema; + fragments: { [fragmentName: string]: FragmentDefinition }; + rootValue: mixed; + operation: OperationDefinition; + variableValues: { [variableName: string]: mixed }; + resolveFn: GraphQLFieldResolveFn; + args: { [key: string]: mixed }; + info: GraphQLResolveInfo; + completionPlan: GraphQLCompletionPlan; +} + +/** + * Describes how the execution engine plans to perform a selection + * on a resolved value. + */ +export type GraphQLOperationExecutionPlan = { + kind: 'operation'; + type: GraphQLObjectType; + fieldASTs: Array; + strategy: string; + fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; +} + +/** + * Describes how the execution engine plans to perform a selection + * on a resolved value. + */ +export type GraphQLSelectionCompletionPlan = { + kind: 'select'; +// fieldName: string, + fieldASTs: Array; +// returnType: GraphQLOutputType; +// parentType: GraphQLCompositeType; + schema: GraphQLSchema; + fragments: { [fragmentName: string]: FragmentDefinition }; + rootValue: mixed; + operation: OperationDefinition; + variableValues: { [variableName: string]: mixed }; + +// type: GraphQLObjectType; + + fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; +} + +/** + * Indicates that the execution engine plans to call + * serialize for a value. + */ +export type GraphQLSerializationCompletionPlan = { + kind: 'serialize'; + type: GraphQLType; + fieldASTs: Array; +} + +/** + * Describes how the execution engine plans to map list values + */ +export type GraphQLListCompletionPlan = { + kind: 'map'; + type: GraphQLType; + fieldASTs: Array; + // @TODO Is this really so broad? + innerCompletionPlan: GraphQLCompletionPlan; +} + +/** + * Describes plans which the execution engine may take + * based on the run time type of a value. + */ +export type GraphQLTypeResolvingPlan = { + kind: 'coerce'; +// @TODO +// fieldName: string, + fieldASTs: Array; + returnType: GraphQLCompositeType; +// @TODO +// parentType: GraphQLCompositeType, + schema: GraphQLSchema; + fragments: { [fragmentName: string]: FragmentDefinition }; + rootValue: mixed; + operation: OperationDefinition; + variableValues: { [variableName: string]: mixed }; + typePlans: {[key: string]:GraphQLSelectionCompletionPlan}; +} + +/** + * Execution plans which might be executed to Complete a value. + */ +export type GraphQLCompletionPlan = + GraphQLSelectionCompletionPlan | + GraphQLSerializationCompletionPlan | + GraphQLListCompletionPlan | + GraphQLTypeResolvingPlan; + export type GraphQLFieldConfig = { type: GraphQLOutputType; args?: GraphQLFieldConfigArgumentMap; From fe8246756015679fb12f746c97509ab3d8862747 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 18:19:00 -0800 Subject: [PATCH 47/94] Use the plan instead of GraphQLResolveInfo as the parameter to resolve and source of error information data --- src/execution/execute.js | 31 +++++++--------- src/execution/plan.js | 77 +++++++++++++++++++++------------------- src/type/definition.js | 37 ++++++++----------- 3 files changed, 67 insertions(+), 78 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 81b1fb93c8..3a47a5e6b2 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,10 +8,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Merge GraphQLResolveInfo fields into GraphQLTypeResolvingPlan -// @TODO: Merge GraphQLResolveInfo fields into GraphQLSelectionCompletionPlan // @TODO: Extract CheckType, ResolveType, CompleteList, CompleteSelect -// @TODO: Eliminate the propagation of info and type to CompleteValue // @TODO: Add error messages for unreachable Conditions // @TODO: Debug the reduce code in plan.js // @TODO: Review null bails to eliminate flowtype boilerplate @@ -299,12 +296,11 @@ function resolveField( // Get the resolve function, regardless of if its result is normal // or abrupt (error). - const result = resolveOrError(plan.resolveFn, source, plan.args, plan.info); + const result = resolveOrError(plan.resolveFn, source, plan.args, plan); return completeValueCatchingError( exeContext, plan.returnType, - plan.info, result, plan.completionPlan ); @@ -320,10 +316,10 @@ function resolveOrError( ) => T, source: mixed, args: { [key: string]: mixed }, - info: GraphQLResolveInfo + plan: GraphQLFieldResolvingPlan ): Error | T { try { - return resolveFn(source, args, info); + return resolveFn(source, args, plan); } catch (error) { // Sometimes a non-error is thrown, wrap it as an Error for a // consistent interface. @@ -336,14 +332,13 @@ function resolveOrError( function completeValueCatchingError( exeContext: ExecutionContext, returnType: GraphQLType, - info: GraphQLResolveInfo, result: mixed, plan: GraphQLCompletionPlan ): mixed { // If the field type is non-nullable, then it is resolved without any // protection from errors. if (returnType instanceof GraphQLNonNull) { - return completeValue(exeContext, returnType, info, result, plan); + return completeValue(exeContext, returnType, result, plan); } // Otherwise, error protection is applied, logging the error and resolving @@ -352,7 +347,6 @@ function completeValueCatchingError( const completed = completeValue( exeContext, returnType, - info, result, plan ); @@ -416,7 +410,6 @@ function CheckType( function completeValue( exeContext: ExecutionContext, returnType: GraphQLType, - info: GraphQLResolveInfo, result: mixed, plan: GraphQLCompletionPlan ): mixed { @@ -429,7 +422,6 @@ function completeValue( resolved => completeValue( exeContext, returnType, - info, resolved, plan ), @@ -451,14 +443,13 @@ function completeValue( const completed = completeValue( exeContext, returnType.ofType, - info, result, plan ); if (completed === null) { throw new GraphQLError( `Cannot return null for non-nullable ` + - `field ${info.parentType}.${info.fieldName}.`, + `field ${plan.parentType}.${plan.fieldName}.`, plan.fieldASTs ); } @@ -494,10 +485,13 @@ function completeValue( // Tested in planCompleteValue invariant(returnType instanceof GraphQLList); + invariant(plan.parentType !== null); + invariant(plan.fieldName !== null); + invariant( Array.isArray(result), 'User Error: expected iterable, but did not find one ' + - `for field ${info.parentType}.${info.fieldName}.` + `for field ${plan.parentType}.${plan.fieldName}.` ); invariant(plan.innerCompletionPlan !== null); @@ -514,7 +508,6 @@ function completeValue( completeValueCatchingError( exeContext, itemType, - info, item, innerCompletionPlan ); @@ -535,7 +528,7 @@ function completeValue( // 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 && !returnType.isTypeOf(result, info)) { + if (returnType.isTypeOf && !returnType.isTypeOf(result, plan)) { throw new GraphQLError( `Expected value of type "${returnType}" but got: ${result}.`, plan.fieldASTs @@ -558,7 +551,7 @@ function completeValue( let runtimeType: ?GraphQLObjectType; const abstractType = ((returnType: any): GraphQLAbstractType); - runtimeType = abstractType.getObjectType(result, info); + runtimeType = abstractType.getObjectType(result, plan); if (!runtimeType) { return null; @@ -567,7 +560,7 @@ function completeValue( // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, info)) { + if (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, plan)) { throw new GraphQLError( `Expected value of type "${runtimeType}" but got: ${result}.`, plan.fieldASTs diff --git a/src/execution/plan.js b/src/execution/plan.js index 5c1d9e0b45..b58b5e3d9a 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -21,6 +21,7 @@ import { GraphQLEnumType, GraphQLList, GraphQLNonNull, + GraphQLCompositeType, isAbstractType } from '../type/definition'; import type { @@ -91,7 +92,9 @@ export function planOperation( function planSelection( exeContext: ExecutionContext, type: GraphQLObjectType, - fieldASTs: Array + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType ): GraphQLSelectionCompletionPlan { let fields = Object.create(null); @@ -113,17 +116,12 @@ function planSelection( const plan: GraphQLSelectionCompletionPlan = { kind: 'select', -// @TODO -// fieldName, + fieldName, fieldASTs, // @TODO // returnType, -// @TODO -// parentType, -// @TODO -// type, - + parentType, schema: exeContext.schema, fragments: exeContext.fragments, rootValue: exeContext.rootValue, @@ -203,21 +201,13 @@ function planResolveField( exeContext.variableValues ); - // The resolve function's optional third argument is a collection of - // information about the current execution state. - const info: GraphQLResolveInfo = { - fieldName, - fieldASTs, + const completionPlan = planCompleteValue( + exeContext, returnType, - parentType, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - }; - - const completionPlan = planCompleteValue(exeContext, returnType, fieldASTs); + fieldASTs, + fieldName, + parentType + ); const plan: GraphQLFieldResolvingPlan = { kind: 'resolve', @@ -232,7 +222,6 @@ function planResolveField( variableValues: exeContext.variableValues, resolveFn, args, - info, completionPlan }; @@ -260,7 +249,9 @@ function planResolveField( function planCompleteValue( exeContext: ExecutionContext, returnType: GraphQLType, - fieldASTs: Array + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType ): GraphQLCompletionPlan { // --- CASE A: Promise (Execution time only, see completeValue) @@ -273,7 +264,9 @@ function planCompleteValue( return planCompleteValue( exeContext, returnType.ofType, - fieldASTs + fieldASTs, + fieldName, + parentType ); } @@ -289,7 +282,9 @@ function planCompleteValue( const plan: GraphQLSerializationCompletionPlan = { kind: 'serialize', type: returnType, - fieldASTs + fieldASTs, + fieldName, + parentType }; return plan; @@ -301,12 +296,20 @@ function planCompleteValue( const innerType = returnType.ofType; const innerCompletionPlan = - planCompleteValue(exeContext, innerType, fieldASTs); + planCompleteValue( + exeContext, + innerType, + fieldASTs, + fieldName, + parentType + ); const plan: GraphQLListCompletionPlan = { kind: 'map', type: returnType, fieldASTs, + fieldName, + parentType, innerCompletionPlan }; @@ -318,7 +321,9 @@ function planCompleteValue( return planSelection( exeContext, returnType, - fieldASTs + fieldASTs, + fieldName, + parentType ); } @@ -333,7 +338,9 @@ function planCompleteValue( result[possibleType.name] = planSelection( exeContext, possibleType, - fieldASTs + fieldASTs, + fieldName, + parentType ); }, Object.create(null)); */ @@ -342,20 +349,18 @@ function planCompleteValue( typePlans[possibleType.name] = planSelection( exeContext, possibleType, - fieldASTs + fieldASTs, + fieldName, + parentType ); }); -// @TODO: Still not sure what the right type is to return here -// maybe returnType is actually Parent type? const plan: GraphQLTypeResolvingPlan = { kind: 'coerce', -// @TODO -// fieldName, + fieldName, fieldASTs, + parentType, returnType: abstractType, -// @TODO -// parentType, schema: exeContext.schema, fragments: exeContext.fragments, rootValue: exeContext.rootValue, diff --git a/src/type/definition.js b/src/type/definition.js index 228132f0c8..a0cca13464 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -473,17 +473,10 @@ export type GraphQLFieldResolveFn = ( info: GraphQLResolveInfo ) => mixed -export type GraphQLResolveInfo = { - fieldName: string, - fieldASTs: Array, - returnType: GraphQLOutputType, - parentType: GraphQLCompositeType, - schema: GraphQLSchema, - fragments: { [fragmentName: string]: FragmentDefinition }, - rootValue: mixed, - operation: OperationDefinition, - variableValues: { [variableName: string]: mixed }, -} +export type GraphQLResolveInfo = + GraphQLFieldResolvingPlan | + GraphQLTypeResolvingPlan | + GraphQLSelectionCompletionPlan; /** * Describes how the execution engine plans to interact with the schema's @@ -502,7 +495,6 @@ export type GraphQLFieldResolvingPlan = { variableValues: { [variableName: string]: mixed }; resolveFn: GraphQLFieldResolveFn; args: { [key: string]: mixed }; - info: GraphQLResolveInfo; completionPlan: GraphQLCompletionPlan; } @@ -524,18 +516,15 @@ export type GraphQLOperationExecutionPlan = { */ export type GraphQLSelectionCompletionPlan = { kind: 'select'; -// fieldName: string, + fieldName: string; fieldASTs: Array; // returnType: GraphQLOutputType; -// parentType: GraphQLCompositeType; + parentType: GraphQLCompositeType; schema: GraphQLSchema; fragments: { [fragmentName: string]: FragmentDefinition }; rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - -// type: GraphQLObjectType; - fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; } @@ -545,8 +534,10 @@ export type GraphQLSelectionCompletionPlan = { */ export type GraphQLSerializationCompletionPlan = { kind: 'serialize'; - type: GraphQLType; + fieldName: string; fieldASTs: Array; + parentType: GraphQLCompositeType; + type: GraphQLType; } /** @@ -554,8 +545,10 @@ export type GraphQLSerializationCompletionPlan = { */ export type GraphQLListCompletionPlan = { kind: 'map'; - type: GraphQLType; + fieldName: string; fieldASTs: Array; + parentType: GraphQLCompositeType; + type: GraphQLType; // @TODO Is this really so broad? innerCompletionPlan: GraphQLCompletionPlan; } @@ -566,12 +559,10 @@ export type GraphQLListCompletionPlan = { */ export type GraphQLTypeResolvingPlan = { kind: 'coerce'; -// @TODO -// fieldName: string, + fieldName: string, fieldASTs: Array; returnType: GraphQLCompositeType; -// @TODO -// parentType: GraphQLCompositeType, + parentType: GraphQLCompositeType, schema: GraphQLSchema; fragments: { [fragmentName: string]: FragmentDefinition }; rootValue: mixed; From 20a693d03eabd490cc831bf1a622a79319feaff0 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 18:26:18 -0800 Subject: [PATCH 48/94] Change order of plan parameters --- src/execution/execute.js | 50 +++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 3a47a5e6b2..4059181ce5 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -12,13 +12,11 @@ // @TODO: Add error messages for unreachable Conditions // @TODO: Debug the reduce code in plan.js // @TODO: Review null bails to eliminate flowtype boilerplate -// @TODO: Can Execution Context be eliminated in favor of only Plans // @TODO: Review against the specification // @TODO: Create an example of prefetching based on Execution plan // @TODO: Undo file split? // @TODO: Distinction without a difference: // @TODO: Make the final pull diff easier to read -// @TODO: Remove resolveFn from GraphQLFieldResolvingPlan // The Execution Plan Hierarchy mirrors the schema hierarchy, not the // query result set, exactly what you would want when trying to pre-fetch @@ -201,8 +199,8 @@ function executeFieldsSerially( (prevPromise, responseName) => prevPromise.then(results => { const result = resolveField( exeContext, - sourceValue, - fields[responseName] + fields[responseName], + sourceValue ); if (result === undefined) { return results; @@ -235,8 +233,8 @@ function executeFields( (results, responseName) => { const result = resolveField( exeContext, - sourceValue, - fields[responseName] + fields[responseName], + sourceValue ); if (result === undefined) { return results; @@ -290,33 +288,33 @@ function promiseForObject( */ function resolveField( exeContext: ExecutionContext, - source: mixed, - plan: GraphQLFieldResolvingPlan + plan: GraphQLFieldResolvingPlan, + source: mixed ): mixed { // Get the resolve function, regardless of if its result is normal // or abrupt (error). - const result = resolveOrError(plan.resolveFn, source, plan.args, plan); + const result = resolveOrError(plan, plan.resolveFn, source, plan.args); return completeValueCatchingError( exeContext, + plan.completionPlan, plan.returnType, - result, - plan.completionPlan + result ); } // Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` // function. Returns the result of resolveFn or the abrupt-return Error object. function resolveOrError( + plan: GraphQLFieldResolvingPlan, resolveFn: ( source: mixed, args: { [key: string]: mixed }, info: GraphQLResolveInfo ) => T, source: mixed, - args: { [key: string]: mixed }, - plan: GraphQLFieldResolvingPlan + args: { [key: string]: mixed } ): Error | T { try { return resolveFn(source, args, plan); @@ -331,14 +329,14 @@ function resolveOrError( // in the execution context. function completeValueCatchingError( exeContext: ExecutionContext, + plan: GraphQLCompletionPlan, returnType: GraphQLType, - result: mixed, - plan: GraphQLCompletionPlan + result: mixed ): mixed { // If the field type is non-nullable, then it is resolved without any // protection from errors. if (returnType instanceof GraphQLNonNull) { - return completeValue(exeContext, returnType, result, plan); + return completeValue(exeContext, plan, returnType, result); } // Otherwise, error protection is applied, logging the error and resolving @@ -346,9 +344,9 @@ function completeValueCatchingError( try { const completed = completeValue( exeContext, + plan, returnType, - result, - plan + result ); if (isThenable(completed)) { // If `completeValue` returned a rejected promise, log the rejection @@ -409,9 +407,9 @@ function CheckType( */ function completeValue( exeContext: ExecutionContext, + plan: GraphQLCompletionPlan, returnType: GraphQLType, - result: mixed, - plan: GraphQLCompletionPlan + result: mixed ): mixed { // --- CASE A: Promise (Execution time only, no plan) @@ -421,9 +419,9 @@ function completeValue( // Once resolved to a value, complete that value. resolved => completeValue( exeContext, + plan, returnType, - resolved, - plan + resolved ), // If rejected, create a located error, and continue to reject. error => Promise.reject(locatedError(error, plan.fieldASTs)) @@ -442,9 +440,9 @@ function completeValue( if (returnType instanceof GraphQLNonNull) { const completed = completeValue( exeContext, + plan, returnType.ofType, - result, - plan + result ); if (completed === null) { throw new GraphQLError( @@ -507,9 +505,9 @@ function completeValue( const completedItem = completeValueCatchingError( exeContext, + innerCompletionPlan, itemType, - item, - innerCompletionPlan + item ); if (!containsPromise && isThenable(completedItem)) { containsPromise = true; From f3ed20ef3149b7cc3d2354d04aca5306b055bfc9 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 18:32:23 -0800 Subject: [PATCH 49/94] Simplify the parameter signature of resolveOrError --- src/execution/execute.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 4059181ce5..6486e1eaad 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -37,7 +37,6 @@ import { import type { GraphQLType, GraphQLAbstractType, - GraphQLResolveInfo, GraphQLCompletionPlan, GraphQLOperationExecutionPlan, GraphQLFieldResolvingPlan @@ -294,7 +293,7 @@ function resolveField( // Get the resolve function, regardless of if its result is normal // or abrupt (error). - const result = resolveOrError(plan, plan.resolveFn, source, plan.args); + const result = resolveOrError(plan, source); return completeValueCatchingError( exeContext, @@ -306,16 +305,12 @@ function resolveField( // Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` // function. Returns the result of resolveFn or the abrupt-return Error object. -function resolveOrError( +function resolveOrError( plan: GraphQLFieldResolvingPlan, - resolveFn: ( - source: mixed, - args: { [key: string]: mixed }, - info: GraphQLResolveInfo - ) => T, - source: mixed, - args: { [key: string]: mixed } -): Error | T { + source: mixed +): Error | mixed { + const resolveFn = plan.resolveFn; + const args = plan.args; try { return resolveFn(source, args, plan); } catch (error) { From 1b61e5e3e5a705d1faea2d7a3b062ac8ae30f08b Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 18:40:48 -0800 Subject: [PATCH 50/94] Add some messaging for unreachable conditions --- src/execution/execute.js | 29 ++++------------------------- src/execution/plan.js | 2 +- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 6486e1eaad..8446a49815 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,8 +8,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Extract CheckType, ResolveType, CompleteList, CompleteSelect -// @TODO: Add error messages for unreachable Conditions // @TODO: Debug the reduce code in plan.js // @TODO: Review null bails to eliminate flowtype boilerplate // @TODO: Review against the specification @@ -17,6 +15,7 @@ // @TODO: Undo file split? // @TODO: Distinction without a difference: // @TODO: Make the final pull diff easier to read +// @TODO: Review Plan structures for consistency // The Execution Plan Hierarchy mirrors the schema hierarchy, not the // query result set, exactly what you would want when trying to pre-fetch @@ -92,8 +91,8 @@ import { * | executeOperation | planOperation | GraphQLOperationExecutionPlan | * | resolveField | planFields | GraphQLFieldResolvingPlan | * | completeValue | planCompleteValue | GraphQLCompletionPlan | - * | resolveType | planCompleteValue | GraphQLTypeResolvingPlan | - * | completeList | planCompleteValue | GraphQLListCompletionPlan | + * | completeValue | planCompleteValue | GraphQLTypeResolvingPlan | + * | completeValue | planCompleteValue | GraphQLListCompletionPlan | * | completeValue | planCompleteValue | GraphQLSerializationCompletionPlan | * | completeValue | planSelection | GraphQLSelectionCompletionPlan | * +------------------+-------------------+------------------------------------+ @@ -362,26 +361,6 @@ function completeValueCatchingError( } } -/** - * Check that a value is of the expected type, raise an error if not - */ -/* -function CheckType( - expectedType:GraphQLType, - value: mixed, - plan: GraphQLCompletionPlan -) { - // 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 (expectedType.isTypeOf && !expectedType.isTypeOf(value, plan.info)) { - throw new GraphQLError( - `Expected value of type "${expectedType}" but got: ${value}.`, - plan.fieldASTs - ); - } -} -*/ /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -591,7 +570,7 @@ function completeValue( // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable default: - invariant(false); + invariant(false, 'No plan covers runtime conditions'); } } diff --git a/src/execution/plan.js b/src/execution/plan.js index b58b5e3d9a..7784ee4b00 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -374,7 +374,7 @@ function planCompleteValue( // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable - invariant(false); + invariant(false, `Cannot form plan for ${parentType}.${fieldName}`); } /** From b408f08cdf7db99918bb2888c9dd88db9ce80355 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 19:00:15 -0800 Subject: [PATCH 51/94] Giving up on getting reduce past typeflow --- src/execution/execute.js | 1 - src/execution/plan.js | 28 ---------------------------- 2 files changed, 29 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 8446a49815..eb4e28bd5b 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,7 +8,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Debug the reduce code in plan.js // @TODO: Review null bails to eliminate flowtype boilerplate // @TODO: Review against the specification // @TODO: Create an example of prefetching based on Execution plan diff --git a/src/execution/plan.js b/src/execution/plan.js index 7784ee4b00..5a6d6b46bf 100644 --- a/src/execution/plan.js +++ b/src/execution/plan.js @@ -141,21 +141,6 @@ function planFields( parentType: GraphQLObjectType, fields: {[key: string]: Array} ): {[key: string]: GraphQLFieldResolvingPlan} { -/* - const finalResults = Object.keys(fields).reduce( - (results, responseName) => { - const fieldASTs = fields[responseName]; - const result = planResolveField( - exeContext, - parentType, - fieldASTs - ); - results[responseName] = result; - }, - Object.create(null) - ); - return finalResults; -*/ const results = Object.create(null); Object.keys(fields).forEach( responseName => { @@ -331,20 +316,7 @@ function planCompleteValue( if (isAbstractType(returnType)) { const abstractType = ((returnType: any): GraphQLAbstractType); const possibleTypes = abstractType.getPossibleTypes(); - const typePlans = Object.create(null); - /** - const typePlans = possibleTypes.reduce((result, possibleType) => { - result[possibleType.name] = planSelection( - exeContext, - possibleType, - fieldASTs, - fieldName, - parentType - ); - }, Object.create(null)); - */ - possibleTypes.forEach(possibleType => { typePlans[possibleType.name] = planSelection( exeContext, From f23a8271405b1360ac3fdf17276975a30cf115b3 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 19:05:09 -0800 Subject: [PATCH 52/94] Remove unnecessary typeflow boilerplate --- src/execution/execute.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index eb4e28bd5b..ad146d6c69 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,7 +8,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Review null bails to eliminate flowtype boilerplate // @TODO: Review against the specification // @TODO: Create an example of prefetching based on Execution plan // @TODO: Undo file split? @@ -456,17 +455,12 @@ function completeValue( // Tested in planCompleteValue invariant(returnType instanceof GraphQLList); - invariant(plan.parentType !== null); - invariant(plan.fieldName !== null); - invariant( Array.isArray(result), 'User Error: expected iterable, but did not find one ' + `for field ${plan.parentType}.${plan.fieldName}.` ); - invariant(plan.innerCompletionPlan !== null); - const innerCompletionPlan = plan.innerCompletionPlan; // This is specified as a simple map, however we're optimizing the path @@ -552,7 +546,6 @@ function completeValue( const typePlan = typePlans[runtimeType.name]; if (!typePlan) { - // console.log(runtimeType.name, ':', Array.keys(typePlans)); throw new GraphQLError( `Runtime Object type "${runtimeType}" ` + `is not a possible coercion type for "${abstractType}".`, From a540d1ecfbdc18837827f0f459bfb59a8f8bed6e Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 19:51:33 -0800 Subject: [PATCH 53/94] Recombine files so Pull request diff is not ridiculous --- src/execution/context.js | 88 ---------------------------------------- src/execution/execute.js | 80 +++++++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 93 deletions(-) delete mode 100644 src/execution/context.js diff --git a/src/execution/context.js b/src/execution/context.js deleted file mode 100644 index 3b27699c58..0000000000 --- a/src/execution/context.js +++ /dev/null @@ -1,88 +0,0 @@ -/* @flow */ -/** - * Copyright (c) 2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -import { GraphQLError } from '../error'; -import { GraphQLSchema } from '../type/schema'; -import type { - OperationDefinition, - Document, - FragmentDefinition -} from '../language/ast'; -import { getVariableValues } from './values'; -import { Kind } from '../language'; - -/** - * Data that must be available at all points during query execution. - * - * Namely, schema of the type system that is currently executing, - * and the fragments defined in the query document - */ -export type ExecutionContext = { - schema: GraphQLSchema; - fragments: {[key: string]: FragmentDefinition}; - rootValue: mixed; - operation: OperationDefinition; - variableValues: {[key: string]: mixed}; - errors: Array; -} - -/** - * Constructs a ExecutionContext object from the arguments passed to - * execute, which we will pass throughout the other execution methods. - * - * Throws a GraphQLError if a valid execution context cannot be created. - */ -export function buildExecutionContext( - schema: GraphQLSchema, - documentAST: Document, - rootValue: mixed, - rawVariableValues: ?{[key: string]: mixed}, - operationName: ?string -): ExecutionContext { - const errors: Array = []; - let operation: ?OperationDefinition; - const fragments: {[name: string]: FragmentDefinition} = Object.create(null); - documentAST.definitions.forEach(definition => { - switch (definition.kind) { - case Kind.OPERATION_DEFINITION: - if (!operationName && operation) { - throw new GraphQLError( - 'Must provide operation name if query contains multiple operations.' - ); - } - if (!operationName || - definition.name && definition.name.value === operationName) { - operation = definition; - } - break; - case Kind.FRAGMENT_DEFINITION: - fragments[definition.name.value] = definition; - break; - default: throw new GraphQLError( - `GraphQL cannot execute a request containing a ${definition.kind}.`, - definition - ); - } - }); - if (!operation) { - if (!operationName) { - throw new GraphQLError(`Unknown operation named "${operationName}".`); - } else { - throw new GraphQLError('Must provide an operation.'); - } - } - const variableValues = getVariableValues( - schema, - operation.variableDefinitions || [], - rawVariableValues || {} - ); - const exeContext: ExecutionContext = - { schema, fragments, rootValue, operation, variableValues, errors }; - return exeContext; -} diff --git a/src/execution/execute.js b/src/execution/execute.js index ad146d6c69..6075cb65d7 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -22,6 +22,8 @@ import { GraphQLError, locatedError } from '../error'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; +import { Kind } from '../language'; +import { getVariableValues } from './values'; import { GraphQLScalarType, @@ -40,12 +42,10 @@ import type { } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; import type { - Document + OperationDefinition, + Document, + FragmentDefinition } from '../language/ast'; -import { - ExecutionContext, - buildExecutionContext -} from './context'; import { planOperation } from './plan'; @@ -96,6 +96,21 @@ import { * +------------------+-------------------+------------------------------------+ */ +/** + * Data that must be available at all points during query execution. + * + * Namely, schema of the type system that is currently executing, + * and the fragments defined in the query document + */ +export type ExecutionContext = { + schema: GraphQLSchema; + fragments: {[key: string]: FragmentDefinition}; + rootValue: mixed; + operation: OperationDefinition; + variableValues: {[key: string]: mixed}; + errors: Array; +} + /** * The result of execution. `data` is the result of executing the * query, `errors` is null if no errors occurred, and is a @@ -165,6 +180,61 @@ export function execute( }); } +/** + * Constructs a ExecutionContext object from the arguments passed to + * execute, which we will pass throughout the other execution methods. + * + * Throws a GraphQLError if a valid execution context cannot be created. + */ +export function buildExecutionContext( + schema: GraphQLSchema, + documentAST: Document, + rootValue: mixed, + rawVariableValues: ?{[key: string]: mixed}, + operationName: ?string +): ExecutionContext { + const errors: Array = []; + let operation: ?OperationDefinition; + const fragments: {[name: string]: FragmentDefinition} = Object.create(null); + documentAST.definitions.forEach(definition => { + switch (definition.kind) { + case Kind.OPERATION_DEFINITION: + if (!operationName && operation) { + throw new GraphQLError( + 'Must provide operation name if query contains multiple operations.' + ); + } + if (!operationName || + definition.name && definition.name.value === operationName) { + operation = definition; + } + break; + case Kind.FRAGMENT_DEFINITION: + fragments[definition.name.value] = definition; + break; + default: throw new GraphQLError( + `GraphQL cannot execute a request containing a ${definition.kind}.`, + definition + ); + } + }); + if (!operation) { + if (!operationName) { + throw new GraphQLError(`Unknown operation named "${operationName}".`); + } else { + throw new GraphQLError('Must provide an operation.'); + } + } + const variableValues = getVariableValues( + schema, + operation.variableDefinitions || [], + rawVariableValues || {} + ); + const exeContext: ExecutionContext = + { schema, fragments, rootValue, operation, variableValues, errors }; + return exeContext; +} + /** * Implements the "Evaluating operations" section of the spec. */ From 2c1839154ab156a922b7cf17122f94e12e62ab74 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 20:07:02 -0800 Subject: [PATCH 54/94] Re-consolidate plan.js into execute.js so that Pull request diffs are cleaner --- src/execution/execute.js | 537 ++++++++++++++++++++++++++++++++++++- src/execution/plan.js | 565 --------------------------------------- 2 files changed, 532 insertions(+), 570 deletions(-) delete mode 100644 src/execution/plan.js diff --git a/src/execution/execute.js b/src/execution/execute.js index 6075cb65d7..f589b3b9dd 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -23,7 +23,9 @@ import { GraphQLError, locatedError } from '../error'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; import { Kind } from '../language'; -import { getVariableValues } from './values'; +import find from '../jsutils/find'; +import { getVariableValues, getArgumentValues } from './values'; +import { typeFromAST } from '../utilities/typeFromAST'; import { GraphQLScalarType, @@ -31,24 +33,41 @@ import { GraphQLEnumType, GraphQLList, GraphQLNonNull, + GraphQLCompositeType, isAbstractType } from '../type/definition'; import type { + GraphQLFieldDefinition, GraphQLType, GraphQLAbstractType, - GraphQLCompletionPlan, GraphQLOperationExecutionPlan, - GraphQLFieldResolvingPlan + GraphQLSelectionCompletionPlan, + GraphQLFieldResolvingPlan, + GraphQLCompletionPlan, + GraphQLSerializationCompletionPlan, + GraphQLListCompletionPlan, + GraphQLTypeResolvingPlan, + GraphQLResolveInfo } from '../type/definition'; +import { + GraphQLIncludeDirective, + GraphQLSkipDirective +} from '../type/directives'; import { GraphQLSchema } from '../type/schema'; import type { + Directive, OperationDefinition, + SelectionSet, + InlineFragment, Document, + Field, FragmentDefinition } from '../language/ast'; import { - planOperation -} from './plan'; + SchemaMetaFieldDef, + TypeMetaFieldDef, + TypeNameMetaFieldDef +} from '../type/introspection'; /** * Terminology @@ -235,6 +254,300 @@ export function buildExecutionContext( return exeContext; } +/** + * Create a plan based on the "Evaluating operations" section of the spec. + */ +export function planOperation( + exeContext: ExecutionContext, + operation: OperationDefinition +): GraphQLOperationExecutionPlan { + const type = getOperationRootType(exeContext.schema, operation); + const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; + + const fields = collectFields( + exeContext, + type, + operation.selectionSet, + Object.create(null), + Object.create(null) + ); + + const fieldPlans = planFields(exeContext, type, fields); + + const plan: GraphQLOperationExecutionPlan = { + kind: 'operation', + type, + fieldASTs: [], // @TODO: I don't know what to pass here + strategy, + fieldPlans + }; + + return plan; +} + +/** + * Create a plan based on the "Evaluating operations" section of the spec. + */ +function planSelection( + exeContext: ExecutionContext, + type: GraphQLObjectType, + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType +): GraphQLSelectionCompletionPlan { + + let fields = Object.create(null); + const visitedFragmentNames = Object.create(null); + for (let i = 0; i < fieldASTs.length; i++) { + const selectionSet = fieldASTs[i].selectionSet; + if (selectionSet) { + fields = collectFields( + exeContext, + type, + selectionSet, + fields, + visitedFragmentNames + ); + } + } + + const fieldPlans = planFields(exeContext, type, fields); + + const plan: GraphQLSelectionCompletionPlan = { + kind: 'select', + fieldName, + fieldASTs, + +// @TODO +// returnType, + parentType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, + fieldPlans + }; + + return plan; +} + +/** + * Plan the "Evaluating selection sets" section of the spec + */ +function planFields( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + fields: {[key: string]: Array} +): {[key: string]: GraphQLFieldResolvingPlan} { + const results = Object.create(null); + Object.keys(fields).forEach( + responseName => { + const fieldASTs = fields[responseName]; + const result = planResolveField( + exeContext, + parentType, + fieldASTs + ); + if (result) { + results[responseName] = result; + } + } + ); + return results; +} + +/** + * Plan how to resolve a field. + */ +function planResolveField( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + fieldASTs: Array +): ?GraphQLFieldResolvingPlan { + const fieldAST = fieldASTs[0]; + const fieldName = fieldAST.name.value; + + const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName); + if (!fieldDef) { + // Omit requested fields that are not in our schema + return; + } + + const returnType = fieldDef.type; + const resolveFn = fieldDef.resolve || defaultResolveFn; + + // Build a JS object of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + const args = getArgumentValues( + fieldDef.args, + fieldAST.arguments, + exeContext.variableValues + ); + + const completionPlan = planCompleteValue( + exeContext, + returnType, + fieldASTs, + fieldName, + parentType + ); + + const plan: GraphQLFieldResolvingPlan = { + kind: 'resolve', + fieldName, + fieldASTs, + returnType, + parentType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, + resolveFn, + args, + completionPlan + }; + + return plan; +} + +/** + * Plans the Execution of completeValue as defined in the + * "Field entries" section of the spec. + * + * If the field type is Non-Null, then this recursively completes the value + * for the inner type. It throws a field error if that completion returns null, + * as per the "Nullability" section of the spec. + * + * If the field type is a List, then this recursively completes the value + * for the inner type on each item in the list. + * + * If the field type is a Scalar or Enum, ensures the completed value is a legal + * value of the type by calling the `serialize` method of GraphQL type + * definition. + * + * Otherwise, the field type expects a sub-selection set, and will complete the + * value by evaluating all sub-selections. + */ +function planCompleteValue( + exeContext: ExecutionContext, + returnType: GraphQLType, + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType +): GraphQLCompletionPlan { + + // --- CASE A: Promise (Execution time only, see completeValue) + + // --- CASE B: Error (Execution time only, see completeValue) + + // --- CASE C: GraphQLNonNull (Structural, see completeValue) + // If field type is NonNull, complete for inner type + if (returnType instanceof GraphQLNonNull) { + return planCompleteValue( + exeContext, + returnType.ofType, + fieldASTs, + fieldName, + parentType + ); + } + + // --- CASE D: Nullish (Execution time only, see completeValue) + + // --- CASE E: Serialize (See completeValue for plan Execution) + // If field type is Scalar or Enum, serialize to a valid value, returning + // null if serialization is not possible. + if (returnType instanceof GraphQLScalarType || + returnType instanceof GraphQLEnumType) { + invariant(returnType.serialize, 'Missing serialize method on type'); + + const plan: GraphQLSerializationCompletionPlan = { + kind: 'serialize', + type: returnType, + fieldASTs, + fieldName, + parentType + }; + + return plan; + } + + // --- CASE F: GraphQLList (See completeValue for plan Execution) + // If field type is List, complete each item in the list with the inner type + if (returnType instanceof GraphQLList) { + const innerType = returnType.ofType; + + const innerCompletionPlan = + planCompleteValue( + exeContext, + innerType, + fieldASTs, + fieldName, + parentType + ); + + const plan: GraphQLListCompletionPlan = { + kind: 'map', + type: returnType, + fieldASTs, + fieldName, + parentType, + innerCompletionPlan + }; + + return plan; + } + + // --- CASE G: GraphQLObjectType (See completeValue for plan Execution) + if (returnType instanceof GraphQLObjectType) { + return planSelection( + exeContext, + returnType, + fieldASTs, + fieldName, + parentType + ); + } + + // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) + if (isAbstractType(returnType)) { + const abstractType = ((returnType: any): GraphQLAbstractType); + const possibleTypes = abstractType.getPossibleTypes(); + const typePlans = Object.create(null); + possibleTypes.forEach(possibleType => { + typePlans[possibleType.name] = planSelection( + exeContext, + possibleType, + fieldASTs, + fieldName, + parentType + ); + }); + + const plan: GraphQLTypeResolvingPlan = { + kind: 'coerce', + fieldName, + fieldASTs, + parentType, + returnType: abstractType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, + typePlans + }; + + return plan; + } + + // --- CASE Z: Unreachable + // We have handled all possibilities. Not reachable + invariant(false, `Cannot form plan for ${parentType}.${fieldName}`); +} + /** * Implements the "Evaluating operations" section of the spec. */ @@ -252,6 +565,42 @@ function executeOperation( return executeFields(exeContext, rootValue, plan.fieldPlans); } +/** + * Extracts the root type of the operation from the schema. + */ +export function getOperationRootType( + schema: GraphQLSchema, + operation: OperationDefinition +): GraphQLObjectType { + switch (operation.operation) { + case 'query': + return schema.getQueryType(); + case 'mutation': + const mutationType = schema.getMutationType(); + if (!mutationType) { + throw new GraphQLError( + 'Schema is not configured for mutations', + [ operation ] + ); + } + return mutationType; + case 'subscription': + const subscriptionType = schema.getSubscriptionType(); + if (!subscriptionType) { + throw new GraphQLError( + 'Schema is not configured for subscriptions', + [ operation ] + ); + } + return subscriptionType; + default: + throw new GraphQLError( + 'Can only execute queries, mutations and subscriptions', + [ operation ] + ); + } +} + /** * Implements the "Evaluating selection sets" section of the spec * for "write" mode. @@ -326,6 +675,132 @@ function executeFields( return promiseForObject(finalResults); } +/** + * Given a selectionSet, adds all of the fields in that selection to + * the passed in map of fields, and returns it at the end. + * + * CollectFields requires the "runtime type" of an object. For a field which + * returns and Interface or Union type, the "runtime type" will be the actual + * Object type returned by that field. + */ +export function collectFields( + exeContext: ExecutionContext, + runtimeType: GraphQLObjectType, + selectionSet: SelectionSet, + fields: {[key: string]: Array}, + visitedFragmentNames: {[key: string]: boolean} +): {[key: string]: Array} { + for (let i = 0; i < selectionSet.selections.length; i++) { + const selection = selectionSet.selections[i]; + switch (selection.kind) { + case Kind.FIELD: + if (!shouldIncludeNode(exeContext, selection.directives)) { + continue; + } + const name = getFieldEntryKey(selection); + if (!fields[name]) { + fields[name] = []; + } + fields[name].push(selection); + break; + case Kind.INLINE_FRAGMENT: + if (!shouldIncludeNode(exeContext, selection.directives) || + !doesFragmentConditionMatch(exeContext, selection, runtimeType)) { + continue; + } + collectFields( + exeContext, + runtimeType, + selection.selectionSet, + fields, + visitedFragmentNames + ); + break; + case Kind.FRAGMENT_SPREAD: + const fragName = selection.name.value; + if (visitedFragmentNames[fragName] || + !shouldIncludeNode(exeContext, selection.directives)) { + continue; + } + visitedFragmentNames[fragName] = true; + const fragment = exeContext.fragments[fragName]; + if (!fragment || + !shouldIncludeNode(exeContext, fragment.directives) || + !doesFragmentConditionMatch(exeContext, fragment, runtimeType)) { + continue; + } + collectFields( + exeContext, + runtimeType, + fragment.selectionSet, + fields, + visitedFragmentNames + ); + break; + } + } + return fields; +} + +/** + * Determines if a field should be included based on the @include and @skip + * directives, where @skip has higher precidence than @include. + */ +function shouldIncludeNode( + exeContext: ExecutionContext, + directives: ?Array +): boolean { + const skipAST = directives && find( + directives, + directive => directive.name.value === GraphQLSkipDirective.name + ); + if (skipAST) { + const { if: skipIf } = getArgumentValues( + GraphQLSkipDirective.args, + skipAST.arguments, + exeContext.variableValues + ); + return !skipIf; + } + + const includeAST = directives && find( + directives, + directive => directive.name.value === GraphQLIncludeDirective.name + ); + if (includeAST) { + const { if: includeIf } = getArgumentValues( + GraphQLIncludeDirective.args, + includeAST.arguments, + exeContext.variableValues + ); + return Boolean(includeIf); + } + + return true; +} + +/** + * Determines if a fragment is applicable to the given type. + */ +function doesFragmentConditionMatch( + exeContext: ExecutionContext, + fragment: FragmentDefinition | InlineFragment, + type: GraphQLObjectType +): boolean { + const typeConditionAST = fragment.typeCondition; + if (!typeConditionAST) { + return true; + } + const conditionalType = typeFromAST(exeContext.schema, typeConditionAST); + if (conditionalType === type) { + return true; + } + if (isAbstractType(conditionalType)) { + return ((conditionalType: any): GraphQLAbstractType).isPossibleType(type); + } + return false; +} + /** * This function transforms a JS object `{[key: string]: Promise}` into * a `Promise<{[key: string]: T}>` @@ -346,6 +821,13 @@ function promiseForObject( ); } +/** + * Implements the logic to compute the key of a given field’s entry + */ +function getFieldEntryKey(node: Field): string { + return node.alias ? node.alias.value : node.name.value; +} + /** * Resolves the field on the given source object. In particular, this * figures out the value that the field returns by calling its resolve function, @@ -634,7 +1116,26 @@ function completeValue( default: invariant(false, 'No plan covers runtime conditions'); } +} + +/** + * If a resolve function is not given, then a default resolve behavior is used + * which takes the property of the source object of the same name as the field + * and returns it as the result, or if it's a function, returns the result + * of calling that function. + */ +export function defaultResolveFn( + source:mixed, + args:{ [key: string]: mixed }, + info: GraphQLResolveInfo +) { + const fieldName = info.fieldName; + // ensure source is a value for which property access is acceptable. + if (typeof source !== 'number' && typeof source !== 'string' && source) { + const property = (source: any)[fieldName]; + return typeof property === 'function' ? property.call(source) : property; + } } /** @@ -646,3 +1147,29 @@ function isThenable(value: mixed): boolean { value !== null && typeof value.then === 'function'; } + +/** + * This method looks up the field on the given type defintion. + * It has special casing for the two introspection fields, __schema + * and __typename. __typename is special because it can always be + * queried as a field, even in situations where no other fields + * are allowed, like on a Union. __schema could get automatically + * added to the query type, but that would require mutating type + * definitions, which would cause issues. + */ +function getFieldDef( + schema: GraphQLSchema, + parentType: GraphQLObjectType, + fieldName: string +): ?GraphQLFieldDefinition { + if (fieldName === SchemaMetaFieldDef.name && + schema.getQueryType() === parentType) { + return SchemaMetaFieldDef; + } else if (fieldName === TypeMetaFieldDef.name && + schema.getQueryType() === parentType) { + return TypeMetaFieldDef; + } else if (fieldName === TypeNameMetaFieldDef.name) { + return TypeNameMetaFieldDef; + } + return parentType.getFields()[fieldName]; +} diff --git a/src/execution/plan.js b/src/execution/plan.js deleted file mode 100644 index 5a6d6b46bf..0000000000 --- a/src/execution/plan.js +++ /dev/null @@ -1,565 +0,0 @@ -/* @flow */ -/** - * Copyright (c) 2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -import { GraphQLError } from '../error'; -import { GraphQLSchema } from '../type/schema'; -import { ExecutionContext } from './context'; -import { Kind } from '../language'; -import find from '../jsutils/find'; -import invariant from '../jsutils/invariant'; -import { getArgumentValues } from './values'; -import { typeFromAST } from '../utilities/typeFromAST'; -import { - GraphQLScalarType, - GraphQLObjectType, - GraphQLEnumType, - GraphQLList, - GraphQLNonNull, - GraphQLCompositeType, - isAbstractType -} from '../type/definition'; -import type { - GraphQLFieldDefinition, - GraphQLAbstractType, - GraphQLType, - GraphQLOperationExecutionPlan, - GraphQLSelectionCompletionPlan, - GraphQLFieldResolvingPlan, - GraphQLCompletionPlan, - GraphQLSerializationCompletionPlan, - GraphQLListCompletionPlan, - GraphQLTypeResolvingPlan, - GraphQLResolveInfo -} from '../type/definition'; -import { - GraphQLIncludeDirective, - GraphQLSkipDirective -} from '../type/directives'; -import type { - Directive, - OperationDefinition, - SelectionSet, - InlineFragment, - Field, - FragmentDefinition -} from '../language/ast'; -import { - SchemaMetaFieldDef, - TypeMetaFieldDef, - TypeNameMetaFieldDef -} from '../type/introspection'; - -/** - * Create a plan based on the "Evaluating operations" section of the spec. - */ -export function planOperation( - exeContext: ExecutionContext, - operation: OperationDefinition -): GraphQLOperationExecutionPlan { - const type = getOperationRootType(exeContext.schema, operation); - const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; - - const fields = collectFields( - exeContext, - type, - operation.selectionSet, - Object.create(null), - Object.create(null) - ); - - const fieldPlans = planFields(exeContext, type, fields); - - const plan: GraphQLOperationExecutionPlan = { - kind: 'operation', - type, - fieldASTs: [], // @TODO: I don't know what to pass here - strategy, - fieldPlans - }; - - return plan; -} - -/** - * Create a plan based on the "Evaluating operations" section of the spec. - */ -function planSelection( - exeContext: ExecutionContext, - type: GraphQLObjectType, - fieldASTs: Array, - fieldName: string, - parentType: GraphQLCompositeType -): GraphQLSelectionCompletionPlan { - - let fields = Object.create(null); - const visitedFragmentNames = Object.create(null); - for (let i = 0; i < fieldASTs.length; i++) { - const selectionSet = fieldASTs[i].selectionSet; - if (selectionSet) { - fields = collectFields( - exeContext, - type, - selectionSet, - fields, - visitedFragmentNames - ); - } - } - - const fieldPlans = planFields(exeContext, type, fields); - - const plan: GraphQLSelectionCompletionPlan = { - kind: 'select', - fieldName, - fieldASTs, - -// @TODO -// returnType, - parentType, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - fieldPlans - }; - - return plan; -} - -/** - * Plan the "Evaluating selection sets" section of the spec - */ -function planFields( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - fields: {[key: string]: Array} -): {[key: string]: GraphQLFieldResolvingPlan} { - const results = Object.create(null); - Object.keys(fields).forEach( - responseName => { - const fieldASTs = fields[responseName]; - const result = planResolveField( - exeContext, - parentType, - fieldASTs - ); - if (result) { - results[responseName] = result; - } - } - ); - return results; -} - -/** - * Plan how to resolve a field. - */ -function planResolveField( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - fieldASTs: Array -): ?GraphQLFieldResolvingPlan { - const fieldAST = fieldASTs[0]; - const fieldName = fieldAST.name.value; - - const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName); - if (!fieldDef) { - // Omit requested fields that are not in our schema - return; - } - - const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve || defaultResolveFn; - - // Build a JS object of arguments from the field.arguments AST, using the - // variables scope to fulfill any variable references. - const args = getArgumentValues( - fieldDef.args, - fieldAST.arguments, - exeContext.variableValues - ); - - const completionPlan = planCompleteValue( - exeContext, - returnType, - fieldASTs, - fieldName, - parentType - ); - - const plan: GraphQLFieldResolvingPlan = { - kind: 'resolve', - fieldName, - fieldASTs, - returnType, - parentType, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - resolveFn, - args, - completionPlan - }; - - return plan; -} - -/** - * Plans the Execution of completeValue as defined in the - * "Field entries" section of the spec. - * - * If the field type is Non-Null, then this recursively completes the value - * for the inner type. It throws a field error if that completion returns null, - * as per the "Nullability" section of the spec. - * - * If the field type is a List, then this recursively completes the value - * for the inner type on each item in the list. - * - * If the field type is a Scalar or Enum, ensures the completed value is a legal - * value of the type by calling the `serialize` method of GraphQL type - * definition. - * - * Otherwise, the field type expects a sub-selection set, and will complete the - * value by evaluating all sub-selections. - */ -function planCompleteValue( - exeContext: ExecutionContext, - returnType: GraphQLType, - fieldASTs: Array, - fieldName: string, - parentType: GraphQLCompositeType -): GraphQLCompletionPlan { - - // --- CASE A: Promise (Execution time only, see completeValue) - - // --- CASE B: Error (Execution time only, see completeValue) - - // --- CASE C: GraphQLNonNull (Structural, see completeValue) - // If field type is NonNull, complete for inner type - if (returnType instanceof GraphQLNonNull) { - return planCompleteValue( - exeContext, - returnType.ofType, - fieldASTs, - fieldName, - parentType - ); - } - - // --- CASE D: Nullish (Execution time only, see completeValue) - - // --- CASE E: Serialize (See completeValue for plan Execution) - // If field type is Scalar or Enum, serialize to a valid value, returning - // null if serialization is not possible. - if (returnType instanceof GraphQLScalarType || - returnType instanceof GraphQLEnumType) { - invariant(returnType.serialize, 'Missing serialize method on type'); - - const plan: GraphQLSerializationCompletionPlan = { - kind: 'serialize', - type: returnType, - fieldASTs, - fieldName, - parentType - }; - - return plan; - } - - // --- CASE F: GraphQLList (See completeValue for plan Execution) - // If field type is List, complete each item in the list with the inner type - if (returnType instanceof GraphQLList) { - const innerType = returnType.ofType; - - const innerCompletionPlan = - planCompleteValue( - exeContext, - innerType, - fieldASTs, - fieldName, - parentType - ); - - const plan: GraphQLListCompletionPlan = { - kind: 'map', - type: returnType, - fieldASTs, - fieldName, - parentType, - innerCompletionPlan - }; - - return plan; - } - - // --- CASE G: GraphQLObjectType (See completeValue for plan Execution) - if (returnType instanceof GraphQLObjectType) { - return planSelection( - exeContext, - returnType, - fieldASTs, - fieldName, - parentType - ); - } - - // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) - if (isAbstractType(returnType)) { - const abstractType = ((returnType: any): GraphQLAbstractType); - const possibleTypes = abstractType.getPossibleTypes(); - const typePlans = Object.create(null); - possibleTypes.forEach(possibleType => { - typePlans[possibleType.name] = planSelection( - exeContext, - possibleType, - fieldASTs, - fieldName, - parentType - ); - }); - - const plan: GraphQLTypeResolvingPlan = { - kind: 'coerce', - fieldName, - fieldASTs, - parentType, - returnType: abstractType, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - typePlans - }; - - return plan; - } - - // --- CASE Z: Unreachable - // We have handled all possibilities. Not reachable - invariant(false, `Cannot form plan for ${parentType}.${fieldName}`); -} - -/** - * Extracts the root type of the operation from the schema. - */ -export function getOperationRootType( - schema: GraphQLSchema, - operation: OperationDefinition -): GraphQLObjectType { - switch (operation.operation) { - case 'query': - return schema.getQueryType(); - case 'mutation': - const mutationType = schema.getMutationType(); - if (!mutationType) { - throw new GraphQLError( - 'Schema is not configured for mutations', - [ operation ] - ); - } - return mutationType; - case 'subscription': - const subscriptionType = schema.getSubscriptionType(); - if (!subscriptionType) { - throw new GraphQLError( - 'Schema is not configured for subscriptions', - [ operation ] - ); - } - return subscriptionType; - default: - throw new GraphQLError( - 'Can only execute queries, mutations and subscriptions', - [ operation ] - ); - } -} - -/** - * Given a selectionSet, adds all of the fields in that selection to - * the passed in map of fields, and returns it at the end. - * - * CollectFields requires the "runtime type" of an object. For a field which - * returns and Interface or Union type, the "runtime type" will be the actual - * Object type returned by that field. - */ -export function collectFields( - exeContext: ExecutionContext, - runtimeType: GraphQLObjectType, - selectionSet: SelectionSet, - fields: {[key: string]: Array}, - visitedFragmentNames: {[key: string]: boolean} -): {[key: string]: Array} { - for (let i = 0; i < selectionSet.selections.length; i++) { - const selection = selectionSet.selections[i]; - switch (selection.kind) { - case Kind.FIELD: - if (!shouldIncludeNode(exeContext, selection.directives)) { - continue; - } - const name = getFieldEntryKey(selection); - if (!fields[name]) { - fields[name] = []; - } - fields[name].push(selection); - break; - case Kind.INLINE_FRAGMENT: - if (!shouldIncludeNode(exeContext, selection.directives) || - !doesFragmentConditionMatch(exeContext, selection, runtimeType)) { - continue; - } - collectFields( - exeContext, - runtimeType, - selection.selectionSet, - fields, - visitedFragmentNames - ); - break; - case Kind.FRAGMENT_SPREAD: - const fragName = selection.name.value; - if (visitedFragmentNames[fragName] || - !shouldIncludeNode(exeContext, selection.directives)) { - continue; - } - visitedFragmentNames[fragName] = true; - const fragment = exeContext.fragments[fragName]; - if (!fragment || - !shouldIncludeNode(exeContext, fragment.directives) || - !doesFragmentConditionMatch(exeContext, fragment, runtimeType)) { - continue; - } - collectFields( - exeContext, - runtimeType, - fragment.selectionSet, - fields, - visitedFragmentNames - ); - break; - } - } - return fields; -} - -/** - * Determines if a field should be included based on the @include and @skip - * directives, where @skip has higher precidence than @include. - */ -function shouldIncludeNode( - exeContext: ExecutionContext, - directives: ?Array -): boolean { - const skipAST = directives && find( - directives, - directive => directive.name.value === GraphQLSkipDirective.name - ); - if (skipAST) { - const { if: skipIf } = getArgumentValues( - GraphQLSkipDirective.args, - skipAST.arguments, - exeContext.variableValues - ); - return !skipIf; - } - - const includeAST = directives && find( - directives, - directive => directive.name.value === GraphQLIncludeDirective.name - ); - if (includeAST) { - const { if: includeIf } = getArgumentValues( - GraphQLIncludeDirective.args, - includeAST.arguments, - exeContext.variableValues - ); - return Boolean(includeIf); - } - - return true; -} - -/** - * Determines if a fragment is applicable to the given type. - */ -function doesFragmentConditionMatch( - exeContext: ExecutionContext, - fragment: FragmentDefinition | InlineFragment, - type: GraphQLObjectType -): boolean { - const typeConditionAST = fragment.typeCondition; - if (!typeConditionAST) { - return true; - } - const conditionalType = typeFromAST(exeContext.schema, typeConditionAST); - if (conditionalType === type) { - return true; - } - if (isAbstractType(conditionalType)) { - return ((conditionalType: any): GraphQLAbstractType).isPossibleType(type); - } - return false; -} - -/** - * Implements the logic to compute the key of a given field’s entry - */ -function getFieldEntryKey(node: Field): string { - return node.alias ? node.alias.value : node.name.value; -} - -/** - * If a resolve function is not given, then a default resolve behavior is used - * which takes the property of the source object of the same name as the field - * and returns it as the result, or if it's a function, returns the result - * of calling that function. - */ -export function defaultResolveFn( - source:mixed, - args:{ [key: string]: mixed }, - info: GraphQLResolveInfo -) { - const fieldName = info.fieldName; - - // ensure source is a value for which property access is acceptable. - if (typeof source !== 'number' && typeof source !== 'string' && source) { - const property = (source: any)[fieldName]; - return typeof property === 'function' ? property.call(source) : property; - } -} - -/** - * This method looks up the field on the given type defintion. - * It has special casing for the two introspection fields, __schema - * and __typename. __typename is special because it can always be - * queried as a field, even in situations where no other fields - * are allowed, like on a Union. __schema could get automatically - * added to the query type, but that would require mutating type - * definitions, which would cause issues. - */ -export function getFieldDef( - schema: GraphQLSchema, - parentType: GraphQLObjectType, - fieldName: string -): ?GraphQLFieldDefinition { - if (fieldName === SchemaMetaFieldDef.name && - schema.getQueryType() === parentType) { - return SchemaMetaFieldDef; - } else if (fieldName === TypeMetaFieldDef.name && - schema.getQueryType() === parentType) { - return TypeMetaFieldDef; - } else if (fieldName === TypeNameMetaFieldDef.name) { - return TypeNameMetaFieldDef; - } - return parentType.getFields()[fieldName]; -} From d79a7ca4c88804996137427f894551b3aebddf0d Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 26 Feb 2016 20:11:55 -0800 Subject: [PATCH 55/94] Distintions without difference: clean up diff --- src/execution/execute.js | 41 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index f589b3b9dd..91821cd920 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -10,7 +10,6 @@ // @TODO: Review against the specification // @TODO: Create an example of prefetching based on Execution plan -// @TODO: Undo file split? // @TODO: Distinction without a difference: // @TODO: Make the final pull diff easier to read // @TODO: Review Plan structures for consistency @@ -20,13 +19,12 @@ // from a resolver. import { GraphQLError, locatedError } from '../error'; +import find from '../jsutils/find'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; +import { typeFromAST } from '../utilities/typeFromAST'; import { Kind } from '../language'; -import find from '../jsutils/find'; import { getVariableValues, getArgumentValues } from './values'; -import { typeFromAST } from '../utilities/typeFromAST'; - import { GraphQLScalarType, GraphQLObjectType, @@ -37,37 +35,38 @@ import { isAbstractType } from '../type/definition'; import type { - GraphQLFieldDefinition, GraphQLType, GraphQLAbstractType, + GraphQLFieldDefinition, + GraphQLResolveInfo, GraphQLOperationExecutionPlan, GraphQLSelectionCompletionPlan, GraphQLFieldResolvingPlan, GraphQLCompletionPlan, GraphQLSerializationCompletionPlan, GraphQLListCompletionPlan, - GraphQLTypeResolvingPlan, - GraphQLResolveInfo + GraphQLTypeResolvingPlan } from '../type/definition'; +import { GraphQLSchema } from '../type/schema'; +import { + SchemaMetaFieldDef, + TypeMetaFieldDef, + TypeNameMetaFieldDef +} from '../type/introspection'; import { GraphQLIncludeDirective, GraphQLSkipDirective } from '../type/directives'; -import { GraphQLSchema } from '../type/schema'; import type { Directive, + Document, OperationDefinition, SelectionSet, - InlineFragment, - Document, Field, + InlineFragment, FragmentDefinition } from '../language/ast'; -import { - SchemaMetaFieldDef, - TypeMetaFieldDef, - TypeNameMetaFieldDef -} from '../type/introspection'; + /** * Terminology @@ -121,7 +120,7 @@ import { * Namely, schema of the type system that is currently executing, * and the fragments defined in the query document */ -export type ExecutionContext = { +type ExecutionContext = { schema: GraphQLSchema; fragments: {[key: string]: FragmentDefinition}; rootValue: mixed; @@ -205,7 +204,7 @@ export function execute( * * Throws a GraphQLError if a valid execution context cannot be created. */ -export function buildExecutionContext( +function buildExecutionContext( schema: GraphQLSchema, documentAST: Document, rootValue: mixed, @@ -257,7 +256,7 @@ export function buildExecutionContext( /** * Create a plan based on the "Evaluating operations" section of the spec. */ -export function planOperation( +function planOperation( exeContext: ExecutionContext, operation: OperationDefinition ): GraphQLOperationExecutionPlan { @@ -568,7 +567,7 @@ function executeOperation( /** * Extracts the root type of the operation from the schema. */ -export function getOperationRootType( +function getOperationRootType( schema: GraphQLSchema, operation: OperationDefinition ): GraphQLObjectType { @@ -683,7 +682,7 @@ function executeFields( * returns and Interface or Union type, the "runtime type" will be the actual * Object type returned by that field. */ -export function collectFields( +function collectFields( exeContext: ExecutionContext, runtimeType: GraphQLObjectType, selectionSet: SelectionSet, @@ -1124,7 +1123,7 @@ function completeValue( * and returns it as the result, or if it's a function, returns the result * of calling that function. */ -export function defaultResolveFn( +function defaultResolveFn( source:mixed, args:{ [key: string]: mixed }, info: GraphQLResolveInfo From 802f50df0cffe20579a7ecaa706a5cf7341d10c4 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 27 Feb 2016 08:53:58 -0800 Subject: [PATCH 56/94] Clean up plans removing items I don't know how to handle --- src/execution/execute.js | 19 +++++++------------ src/type/definition.js | 4 ---- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 91821cd920..672e2edc9d 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -10,13 +10,13 @@ // @TODO: Review against the specification // @TODO: Create an example of prefetching based on Execution plan -// @TODO: Distinction without a difference: -// @TODO: Make the final pull diff easier to read // @TODO: Review Plan structures for consistency - -// The Execution Plan Hierarchy mirrors the schema hierarchy, not the -// query result set, exactly what you would want when trying to pre-fetch -// from a resolver. +// @TODO: Change kind constants +// @TODO: Sort out returnType in the various Plans +// @TODO: Currently NOT providing returnType in GraphQLSelectionCompletionPlan +// @TODO: Currently NOT providing returnType in GraphQLTypeResolvingPlan +// @TODO: Re-approach plan design from the perspective: +// @TODO: What does a resolver author need to know at this point in time import { GraphQLError, locatedError } from '../error'; import find from '../jsutils/find'; @@ -31,7 +31,7 @@ import { GraphQLEnumType, GraphQLList, GraphQLNonNull, - GraphQLCompositeType, + GraphQLCompositeType, isAbstractType } from '../type/definition'; import type { @@ -276,7 +276,6 @@ function planOperation( const plan: GraphQLOperationExecutionPlan = { kind: 'operation', type, - fieldASTs: [], // @TODO: I don't know what to pass here strategy, fieldPlans }; @@ -316,9 +315,6 @@ function planSelection( kind: 'select', fieldName, fieldASTs, - -// @TODO -// returnType, parentType, schema: exeContext.schema, fragments: exeContext.fragments, @@ -530,7 +526,6 @@ function planCompleteValue( fieldName, fieldASTs, parentType, - returnType: abstractType, schema: exeContext.schema, fragments: exeContext.fragments, rootValue: exeContext.rootValue, diff --git a/src/type/definition.js b/src/type/definition.js index a0cca13464..620c54f0c5 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -505,7 +505,6 @@ export type GraphQLFieldResolvingPlan = { export type GraphQLOperationExecutionPlan = { kind: 'operation'; type: GraphQLObjectType; - fieldASTs: Array; strategy: string; fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; } @@ -518,7 +517,6 @@ export type GraphQLSelectionCompletionPlan = { kind: 'select'; fieldName: string; fieldASTs: Array; -// returnType: GraphQLOutputType; parentType: GraphQLCompositeType; schema: GraphQLSchema; fragments: { [fragmentName: string]: FragmentDefinition }; @@ -549,7 +547,6 @@ export type GraphQLListCompletionPlan = { fieldASTs: Array; parentType: GraphQLCompositeType; type: GraphQLType; - // @TODO Is this really so broad? innerCompletionPlan: GraphQLCompletionPlan; } @@ -561,7 +558,6 @@ export type GraphQLTypeResolvingPlan = { kind: 'coerce'; fieldName: string, fieldASTs: Array; - returnType: GraphQLCompositeType; parentType: GraphQLCompositeType, schema: GraphQLSchema; fragments: { [fragmentName: string]: FragmentDefinition }; From 067f1cd29a545aa5d4cc959a8e41215092181cc8 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 27 Feb 2016 08:56:20 -0800 Subject: [PATCH 57/94] Rename kind constants --- src/execution/execute.js | 11 +++++------ src/type/definition.js | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 672e2edc9d..3a5cf647d0 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -11,12 +11,11 @@ // @TODO: Review against the specification // @TODO: Create an example of prefetching based on Execution plan // @TODO: Review Plan structures for consistency -// @TODO: Change kind constants // @TODO: Sort out returnType in the various Plans // @TODO: Currently NOT providing returnType in GraphQLSelectionCompletionPlan // @TODO: Currently NOT providing returnType in GraphQLTypeResolvingPlan // @TODO: Re-approach plan design from the perspective: -// @TODO: What does a resolver author need to know at this point in time +// @TODO: What does a resolver author need to know at this point in time? import { GraphQLError, locatedError } from '../error'; import find from '../jsutils/find'; @@ -274,7 +273,7 @@ function planOperation( const fieldPlans = planFields(exeContext, type, fields); const plan: GraphQLOperationExecutionPlan = { - kind: 'operation', + kind: 'execute', type, strategy, fieldPlans @@ -389,7 +388,7 @@ function planResolveField( ); const plan: GraphQLFieldResolvingPlan = { - kind: 'resolve', + kind: 'resolveField', fieldName, fieldASTs, returnType, @@ -522,7 +521,7 @@ function planCompleteValue( }); const plan: GraphQLTypeResolvingPlan = { - kind: 'coerce', + kind: 'resolveType', fieldName, fieldASTs, parentType, @@ -1053,7 +1052,7 @@ function completeValue( ); // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) - case 'coerce': + case 'resolveType': // Tested in planCompleteValue invariant(isAbstractType(returnType)); diff --git a/src/type/definition.js b/src/type/definition.js index 620c54f0c5..18f4527d90 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -483,7 +483,7 @@ export type GraphQLResolveInfo = * resolve function to fetch field values indicated by the query */ export type GraphQLFieldResolvingPlan = { - kind: 'resolve'; + kind: 'resolveField'; fieldName: string, fieldASTs: Array; returnType: GraphQLOutputType; @@ -503,7 +503,7 @@ export type GraphQLFieldResolvingPlan = { * on a resolved value. */ export type GraphQLOperationExecutionPlan = { - kind: 'operation'; + kind: 'execute'; type: GraphQLObjectType; strategy: string; fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; @@ -555,7 +555,7 @@ export type GraphQLListCompletionPlan = { * based on the run time type of a value. */ export type GraphQLTypeResolvingPlan = { - kind: 'coerce'; + kind: 'resolveType'; fieldName: string, fieldASTs: Array; parentType: GraphQLCompositeType, From eba24dc52cd47453c8bc5be62b24a1eb525c18fd Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 27 Feb 2016 09:14:35 -0800 Subject: [PATCH 58/94] Rename innerCompletionPlan to completionPlan --- src/execution/execute.js | 8 ++++---- src/type/definition.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 3a5cf647d0..8b88d45862 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -473,7 +473,7 @@ function planCompleteValue( if (returnType instanceof GraphQLList) { const innerType = returnType.ofType; - const innerCompletionPlan = + const completionPlan = planCompleteValue( exeContext, innerType, @@ -488,7 +488,7 @@ function planCompleteValue( fieldASTs, fieldName, parentType, - innerCompletionPlan + completionPlan }; return plan; @@ -1006,7 +1006,7 @@ function completeValue( `for field ${plan.parentType}.${plan.fieldName}.` ); - const innerCompletionPlan = plan.innerCompletionPlan; + const completionPlan = plan.completionPlan; // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another @@ -1017,7 +1017,7 @@ function completeValue( const completedItem = completeValueCatchingError( exeContext, - innerCompletionPlan, + completionPlan, itemType, item ); diff --git a/src/type/definition.js b/src/type/definition.js index 18f4527d90..de007b87ef 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -547,7 +547,7 @@ export type GraphQLListCompletionPlan = { fieldASTs: Array; parentType: GraphQLCompositeType; type: GraphQLType; - innerCompletionPlan: GraphQLCompletionPlan; + completionPlan: GraphQLCompletionPlan; } /** From d8a84f8ea7c2444c255d589323e404b5874fc37f Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 27 Feb 2016 10:11:21 -0800 Subject: [PATCH 59/94] Clean up plan naming --- src/execution/execute.js | 103 ++++++++++++++++++++------------------- src/type/definition.js | 40 +++++++-------- 2 files changed, 73 insertions(+), 70 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 8b88d45862..5c152638d0 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,14 +8,17 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Review against the specification // @TODO: Create an example of prefetching based on Execution plan // @TODO: Review Plan structures for consistency // @TODO: Sort out returnType in the various Plans -// @TODO: Currently NOT providing returnType in GraphQLSelectionCompletionPlan -// @TODO: Currently NOT providing returnType in GraphQLTypeResolvingPlan +// @TODO: Currently NOT providing returnType in GraphQLSelectionPlan +// @TODO: Currently NOT providing returnType in GraphQLCoercionPlan // @TODO: Re-approach plan design from the perspective: // @TODO: What does a resolver author need to know at this point in time? +// @TODO: Document plan fields +// @TODO: Collect resolution use cases +// @TODO: Does isTypeOf really need a context parameter? +// @TODO: Examine error handling paths import { GraphQLError, locatedError } from '../error'; import find from '../jsutils/find'; @@ -38,13 +41,13 @@ import type { GraphQLAbstractType, GraphQLFieldDefinition, GraphQLResolveInfo, - GraphQLOperationExecutionPlan, - GraphQLSelectionCompletionPlan, - GraphQLFieldResolvingPlan, + GraphQLOperationPlan, + GraphQLSelectionPlan, + GraphQLResolvingPlan, GraphQLCompletionPlan, - GraphQLSerializationCompletionPlan, - GraphQLListCompletionPlan, - GraphQLTypeResolvingPlan + GraphQLSerializationPlan, + GraphQLMappingPlan, + GraphQLCoercionPlan } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; import { @@ -100,17 +103,17 @@ import type { * "complete" indicates processing return values from the resolving process. * * The planning and execution phases are coupled in this way: - * +------------------+-------------------+------------------------------------+ - * | Execution | Planning | Plan | - * +------------------+-------------------+------------------------------------+ - * | executeOperation | planOperation | GraphQLOperationExecutionPlan | - * | resolveField | planFields | GraphQLFieldResolvingPlan | - * | completeValue | planCompleteValue | GraphQLCompletionPlan | - * | completeValue | planCompleteValue | GraphQLTypeResolvingPlan | - * | completeValue | planCompleteValue | GraphQLListCompletionPlan | - * | completeValue | planCompleteValue | GraphQLSerializationCompletionPlan | - * | completeValue | planSelection | GraphQLSelectionCompletionPlan | - * +------------------+-------------------+------------------------------------+ + * +------------------+-------------------+---------------------------+ + * | Execution | Planning | Plan | + * +------------------+-------------------+---------------------------+ + * | executeOperation | planOperation | GraphQLOperationPlan | + * | resolveField | planFields | GraphQLResolvingPlan | + * | completeValue | planCompleteValue | GraphQLCompletionPlan | + * | completeValue | planCompleteValue | GraphQLCoercionPlan | + * | completeValue | planCompleteValue | GraphQLMappingPlan | + * | completeValue | planCompleteValue | GraphQLSerializationPlan | + * | completeValue | planSelection | GraphQLSelectionPlan | + * +------------------+-------------------+---------------------------+ */ /** @@ -258,7 +261,7 @@ function buildExecutionContext( function planOperation( exeContext: ExecutionContext, operation: OperationDefinition -): GraphQLOperationExecutionPlan { +): GraphQLOperationPlan { const type = getOperationRootType(exeContext.schema, operation); const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; @@ -272,7 +275,7 @@ function planOperation( const fieldPlans = planFields(exeContext, type, fields); - const plan: GraphQLOperationExecutionPlan = { + const plan: GraphQLOperationPlan = { kind: 'execute', type, strategy, @@ -291,7 +294,7 @@ function planSelection( fieldASTs: Array, fieldName: string, parentType: GraphQLCompositeType -): GraphQLSelectionCompletionPlan { +): GraphQLSelectionPlan { let fields = Object.create(null); const visitedFragmentNames = Object.create(null); @@ -310,7 +313,7 @@ function planSelection( const fieldPlans = planFields(exeContext, type, fields); - const plan: GraphQLSelectionCompletionPlan = { + const plan: GraphQLSelectionPlan = { kind: 'select', fieldName, fieldASTs, @@ -333,7 +336,7 @@ function planFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, fields: {[key: string]: Array} -): {[key: string]: GraphQLFieldResolvingPlan} { +): {[key: string]: GraphQLResolvingPlan} { const results = Object.create(null); Object.keys(fields).forEach( responseName => { @@ -358,7 +361,7 @@ function planResolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, fieldASTs: Array -): ?GraphQLFieldResolvingPlan { +): ?GraphQLResolvingPlan { const fieldAST = fieldASTs[0]; const fieldName = fieldAST.name.value; @@ -387,8 +390,8 @@ function planResolveField( parentType ); - const plan: GraphQLFieldResolvingPlan = { - kind: 'resolveField', + const plan: GraphQLResolvingPlan = { + kind: 'resolve', fieldName, fieldASTs, returnType, @@ -457,7 +460,7 @@ function planCompleteValue( returnType instanceof GraphQLEnumType) { invariant(returnType.serialize, 'Missing serialize method on type'); - const plan: GraphQLSerializationCompletionPlan = { + const plan: GraphQLSerializationPlan = { kind: 'serialize', type: returnType, fieldASTs, @@ -482,7 +485,7 @@ function planCompleteValue( parentType ); - const plan: GraphQLListCompletionPlan = { + const plan: GraphQLMappingPlan = { kind: 'map', type: returnType, fieldASTs, @@ -505,13 +508,13 @@ function planCompleteValue( ); } - // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) + // --- CASE H: isAbstractType (run GraphQLCoercionPlan) if (isAbstractType(returnType)) { const abstractType = ((returnType: any): GraphQLAbstractType); const possibleTypes = abstractType.getPossibleTypes(); - const typePlans = Object.create(null); + const selectionPlansByType = Object.create(null); possibleTypes.forEach(possibleType => { - typePlans[possibleType.name] = planSelection( + selectionPlansByType[possibleType.name] = planSelection( exeContext, possibleType, fieldASTs, @@ -520,8 +523,8 @@ function planCompleteValue( ); }); - const plan: GraphQLTypeResolvingPlan = { - kind: 'resolveType', + const plan: GraphQLCoercionPlan = { + kind: 'coerce', fieldName, fieldASTs, parentType, @@ -530,7 +533,7 @@ function planCompleteValue( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - typePlans + selectionPlansByType }; return plan; @@ -546,7 +549,7 @@ function planCompleteValue( */ function executeOperation( exeContext: ExecutionContext, - plan: GraphQLOperationExecutionPlan, + plan: GraphQLOperationPlan, rootValue: mixed ): Object { @@ -601,7 +604,7 @@ function getOperationRootType( function executeFieldsSerially( exeContext: ExecutionContext, sourceValue: mixed, - fields: {[key: string]: GraphQLFieldResolvingPlan} + fields: {[key: string]: GraphQLResolvingPlan} ): Promise { return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { @@ -633,7 +636,7 @@ function executeFieldsSerially( function executeFields( exeContext: ExecutionContext, sourceValue: mixed, - fields: {[key: string]: GraphQLFieldResolvingPlan} + fields: {[key: string]: GraphQLResolvingPlan} ): Object { let containsPromise = false; @@ -829,7 +832,7 @@ function getFieldEntryKey(node: Field): string { */ function resolveField( exeContext: ExecutionContext, - plan: GraphQLFieldResolvingPlan, + plan: GraphQLResolvingPlan, source: mixed ): mixed { @@ -848,7 +851,7 @@ function resolveField( // Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` // function. Returns the result of resolveFn or the abrupt-return Error object. function resolveOrError( - plan: GraphQLFieldResolvingPlan, + plan: GraphQLResolvingPlan, source: mixed ): Error | mixed { const resolveFn = plan.resolveFn; @@ -994,7 +997,7 @@ function completeValue( const serializedResult = returnType.serialize(result); return isNullish(serializedResult) ? null : serializedResult; - // --- CASE F: GraphQLList (run GraphQLListCompletionPlan) + // --- CASE F: GraphQLList (run GraphQLMappingPlan) // If result is null-like, return null. case 'map': // Tested in planCompleteValue @@ -1030,7 +1033,7 @@ function completeValue( return containsPromise ? Promise.all(completedResults) : completedResults; - // --- CASE G: GraphQLObjectType (run GraphQLSelectionCompletionPlan) + // --- CASE G: GraphQLObjectType (run GraphQLSelectionPlan) case 'select': // Tested in planCompleteValue invariant(returnType instanceof GraphQLObjectType); @@ -1051,8 +1054,8 @@ function completeValue( plan.fieldPlans ); - // --- CASE H: isAbstractType (run GraphQLTypeResolvingPlan) - case 'resolveType': + // --- CASE H: isAbstractType (run GraphQLCoercionPlan) + case 'coerce': // Tested in planCompleteValue invariant(isAbstractType(returnType)); @@ -1085,12 +1088,12 @@ function completeValue( ); } - invariant(plan.typePlans !== null); + invariant(plan.selectionPlansByType !== null); - const typePlans = plan.typePlans; + const selectionPlansByType = plan.selectionPlansByType; - const typePlan = typePlans[runtimeType.name]; - if (!typePlan) { + const selectionPlan = selectionPlansByType[runtimeType.name]; + if (!selectionPlan) { throw new GraphQLError( `Runtime Object type "${runtimeType}" ` + `is not a possible coercion type for "${abstractType}".`, @@ -1101,7 +1104,7 @@ function completeValue( return executeFields( exeContext, result, - typePlan.fieldPlans + selectionPlan.fieldPlans ); // --- CASE Z: Unreachable diff --git a/src/type/definition.js b/src/type/definition.js index de007b87ef..dd4d96c7d2 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -474,16 +474,16 @@ export type GraphQLFieldResolveFn = ( ) => mixed export type GraphQLResolveInfo = - GraphQLFieldResolvingPlan | - GraphQLTypeResolvingPlan | - GraphQLSelectionCompletionPlan; + GraphQLResolvingPlan | + GraphQLSelectionPlan | + GraphQLCoercionPlan; /** * Describes how the execution engine plans to interact with the schema's * resolve function to fetch field values indicated by the query */ -export type GraphQLFieldResolvingPlan = { - kind: 'resolveField'; +export type GraphQLResolvingPlan = { + kind: 'resolve'; fieldName: string, fieldASTs: Array; returnType: GraphQLOutputType; @@ -499,21 +499,21 @@ export type GraphQLFieldResolvingPlan = { } /** - * Describes how the execution engine plans to perform a selection - * on a resolved value. + * Describes how the execution engine plans to perform + * an operation. */ -export type GraphQLOperationExecutionPlan = { +export type GraphQLOperationPlan = { kind: 'execute'; type: GraphQLObjectType; strategy: string; - fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; + fieldPlans: {[key: string]: GraphQLResolvingPlan}; } /** * Describes how the execution engine plans to perform a selection * on a resolved value. */ -export type GraphQLSelectionCompletionPlan = { +export type GraphQLSelectionPlan = { kind: 'select'; fieldName: string; fieldASTs: Array; @@ -523,14 +523,14 @@ export type GraphQLSelectionCompletionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - fieldPlans: {[key: string]: GraphQLFieldResolvingPlan}; + fieldPlans: {[key: string]: GraphQLResolvingPlan}; } /** * Indicates that the execution engine plans to call * serialize for a value. */ -export type GraphQLSerializationCompletionPlan = { +export type GraphQLSerializationPlan = { kind: 'serialize'; fieldName: string; fieldASTs: Array; @@ -541,7 +541,7 @@ export type GraphQLSerializationCompletionPlan = { /** * Describes how the execution engine plans to map list values */ -export type GraphQLListCompletionPlan = { +export type GraphQLMappingPlan = { kind: 'map'; fieldName: string; fieldASTs: Array; @@ -554,8 +554,8 @@ export type GraphQLListCompletionPlan = { * Describes plans which the execution engine may take * based on the run time type of a value. */ -export type GraphQLTypeResolvingPlan = { - kind: 'resolveType'; +export type GraphQLCoercionPlan = { + kind: 'coerce'; fieldName: string, fieldASTs: Array; parentType: GraphQLCompositeType, @@ -564,17 +564,17 @@ export type GraphQLTypeResolvingPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - typePlans: {[key: string]:GraphQLSelectionCompletionPlan}; + selectionPlansByType: {[key: string]:GraphQLSelectionPlan}; } /** * Execution plans which might be executed to Complete a value. */ export type GraphQLCompletionPlan = - GraphQLSelectionCompletionPlan | - GraphQLSerializationCompletionPlan | - GraphQLListCompletionPlan | - GraphQLTypeResolvingPlan; + GraphQLSelectionPlan | + GraphQLSerializationPlan | + GraphQLMappingPlan | + GraphQLCoercionPlan; export type GraphQLFieldConfig = { type: GraphQLOutputType; From b8aa7715c0455ef66b8f51a4b6743c714c7d9a24 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 27 Feb 2016 11:23:15 -0800 Subject: [PATCH 60/94] Notes --- src/execution/execute.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/execution/execute.js b/src/execution/execute.js index 5c152638d0..5f69c6fb39 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -19,6 +19,7 @@ // @TODO: Collect resolution use cases // @TODO: Does isTypeOf really need a context parameter? // @TODO: Examine error handling paths +// @TODO: Should we really be returning fieldASTs in error messages? import { GraphQLError, locatedError } from '../error'; import find from '../jsutils/find'; From 81302b82d71c7529312004c5abc9645fefcdf541 Mon Sep 17 00:00:00 2001 From: Jeff Moore Date: Sat, 27 Feb 2016 11:41:50 -0800 Subject: [PATCH 61/94] Create NOTES.md --- src/execution/NOTES.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/execution/NOTES.md diff --git a/src/execution/NOTES.md b/src/execution/NOTES.md new file mode 100644 index 0000000000..13e8e0eea8 --- /dev/null +++ b/src/execution/NOTES.md @@ -0,0 +1,16 @@ +Split Execution into two phases +=============================== +This is an attempt to address optmization concerns during query evaluation. + +This approach splits execution into two phases, a planning phase and an evaluation phase. + +In the planning phase the AST in analyzed and a heirarchical plan structure is created indicating +how the executor will evaluate the query. Precalculating this information serves two purposes: + +1. Provides a reliable and simple indication to resolving functions what evaulations will occur next. +2. Avoids re-calculating some data when evaluating list results + +There is no attempt to optimize the plan. This is out of scope, although it would be possible to write +optimizing functions that accepted a plan and output a different plan before evaluation. + +Evaluation order is not changed. From 344ae1e5abdd7fb157c6c10c19fac1e9a13123f0 Mon Sep 17 00:00:00 2001 From: Jeff Moore Date: Sat, 27 Feb 2016 12:14:25 -0800 Subject: [PATCH 62/94] Update NOTES.md --- src/execution/NOTES.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/execution/NOTES.md b/src/execution/NOTES.md index 13e8e0eea8..11b1988642 100644 --- a/src/execution/NOTES.md +++ b/src/execution/NOTES.md @@ -2,6 +2,15 @@ Split Execution into two phases =============================== This is an attempt to address optmization concerns during query evaluation. +https://github.com/graphql/graphql-js/issues/26 +https://github.com/graphql/graphql-js/issues/161 +https://github.com/graphql-dotnet/graphql-dotnet/issues/21 +https://github.com/graphql/graphql-js/issues/149 +https://github.com/graphql/graphql-js/issues/111 +https://github.com/graphql/graphql-js/pull/39 +https://github.com/graphql/graphql-js/issues/19 +https://github.com/rmosolgo/graphql-ruby/issues/6 + This approach splits execution into two phases, a planning phase and an evaluation phase. In the planning phase the AST in analyzed and a heirarchical plan structure is created indicating @@ -14,3 +23,27 @@ There is no attempt to optimize the plan. This is out of scope, although it wou optimizing functions that accepted a plan and output a different plan before evaluation. Evaluation order is not changed. + +The current interface for resolver authors +------------------------------------------ + + +GraphQLObjectType allows defining an isTypeOf function +` isTypeOf: ?(value: mixed, info?: GraphQLResolveInfo) => boolean;` + +A Default resolveType function is available in getTypeOf which may call isTypeOf +`function getTypeOf(value: mixed, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType): ?GraphQLObjectType` + +GraphQLInterfaceType and GraphQLUnionType allow defining a resolveType function +`resolveType: ?(value: mixed, info?: GraphQLResolveInfo) => ?GraphQLObjectType;` + +GraphQLInterfaceType and GraphQLUnionType defines a getObjectType function which calls the resolveType or getTypeOf function +``getObjectType(value: mixed, info: GraphQLResolveInfo): ?GraphQLObjectType` + +A field definition defines a resolve function +`resolve?: GraphQLFieldResolveFn;` + +(Why do GraphQLObjectType, GraphQLInterfaceType and GraphQLInterfaceType take their functions as config parameters, which field definition declares it directly? + +A GraphQLScalarType allows defining a serialize function +`serialize: (value: mixed) => ?InternalType;` From 224cce80f3d1dce40be31c8d1d3b9e000e91d1fb Mon Sep 17 00:00:00 2001 From: Jeff Moore Date: Sat, 27 Feb 2016 12:15:02 -0800 Subject: [PATCH 63/94] Update NOTES.md --- src/execution/NOTES.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/execution/NOTES.md b/src/execution/NOTES.md index 11b1988642..c0a331f903 100644 --- a/src/execution/NOTES.md +++ b/src/execution/NOTES.md @@ -29,21 +29,27 @@ The current interface for resolver authors GraphQLObjectType allows defining an isTypeOf function + ` isTypeOf: ?(value: mixed, info?: GraphQLResolveInfo) => boolean;` A Default resolveType function is available in getTypeOf which may call isTypeOf + `function getTypeOf(value: mixed, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType): ?GraphQLObjectType` GraphQLInterfaceType and GraphQLUnionType allow defining a resolveType function + `resolveType: ?(value: mixed, info?: GraphQLResolveInfo) => ?GraphQLObjectType;` GraphQLInterfaceType and GraphQLUnionType defines a getObjectType function which calls the resolveType or getTypeOf function -``getObjectType(value: mixed, info: GraphQLResolveInfo): ?GraphQLObjectType` + +`getObjectType(value: mixed, info: GraphQLResolveInfo): ?GraphQLObjectType` A field definition defines a resolve function + `resolve?: GraphQLFieldResolveFn;` (Why do GraphQLObjectType, GraphQLInterfaceType and GraphQLInterfaceType take their functions as config parameters, which field definition declares it directly? A GraphQLScalarType allows defining a serialize function + `serialize: (value: mixed) => ?InternalType;` From 76be755a2746a188eaa57356d8b7fe097066090f Mon Sep 17 00:00:00 2001 From: Jeff Moore Date: Sat, 27 Feb 2016 12:17:34 -0800 Subject: [PATCH 64/94] Update NOTES.md --- src/execution/NOTES.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/execution/NOTES.md b/src/execution/NOTES.md index c0a331f903..e27894a32a 100644 --- a/src/execution/NOTES.md +++ b/src/execution/NOTES.md @@ -2,14 +2,14 @@ Split Execution into two phases =============================== This is an attempt to address optmization concerns during query evaluation. -https://github.com/graphql/graphql-js/issues/26 -https://github.com/graphql/graphql-js/issues/161 -https://github.com/graphql-dotnet/graphql-dotnet/issues/21 -https://github.com/graphql/graphql-js/issues/149 -https://github.com/graphql/graphql-js/issues/111 -https://github.com/graphql/graphql-js/pull/39 -https://github.com/graphql/graphql-js/issues/19 -https://github.com/rmosolgo/graphql-ruby/issues/6 +- https://github.com/graphql/graphql-js/issues/26 +- https://github.com/graphql/graphql-js/issues/161 +- https://github.com/graphql-dotnet/graphql-dotnet/issues/21 +- https://github.com/graphql/graphql-js/issues/149 +- https://github.com/graphql/graphql-js/issues/111 +- https://github.com/graphql/graphql-js/pull/39 +- https://github.com/graphql/graphql-js/issues/19 +- https://github.com/rmosolgo/graphql-ruby/issues/6 This approach splits execution into two phases, a planning phase and an evaluation phase. From 1b5e2bb9c00f5011ff77f835415f271465fab7bf Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 11:55:34 -0800 Subject: [PATCH 65/94] Factor out duplicate code, so that isTypeOf will only receive one type of plan --- src/execution/execute.js | 76 +++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 5f69c6fb39..d2f72b7103 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -981,6 +981,11 @@ function completeValue( return null; } + // Field type must be Object, Interface or Union and expect + // sub-selections. + let runtimeType: ?GraphQLObjectType; + let selectionPlan: ?GraphQLSelectionPlan; + // Execution Completion Plan switch (plan.kind) { @@ -1039,80 +1044,63 @@ function completeValue( // Tested in planCompleteValue invariant(returnType instanceof GraphQLObjectType); - // 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 && !returnType.isTypeOf(result, plan)) { - throw new GraphQLError( - `Expected value of type "${returnType}" but got: ${result}.`, - plan.fieldASTs - ); - } + selectionPlan = plan; + runtimeType = returnType; - return executeFields( - exeContext, - result, - plan.fieldPlans - ); + break; // --- CASE H: isAbstractType (run GraphQLCoercionPlan) case 'coerce': // Tested in planCompleteValue invariant(isAbstractType(returnType)); - // Field type must be Object, Interface or Union and expect - // sub-selections. - let runtimeType: ?GraphQLObjectType; - const abstractType = ((returnType: any): GraphQLAbstractType); runtimeType = abstractType.getObjectType(result, plan); if (!runtimeType) { + // The type could not be resolved so omit from the results return null; } - // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, plan)) { - throw new GraphQLError( - `Expected value of type "${runtimeType}" but got: ${result}.`, - plan.fieldASTs - ); - } - - if (runtimeType && !abstractType.isPossibleType(runtimeType)) { - throw new GraphQLError( - `Runtime Object type "${runtimeType}" is not a possible type ` + - `for "${abstractType}".`, - plan.fieldASTs - ); - } - invariant(plan.selectionPlansByType !== null); const selectionPlansByType = plan.selectionPlansByType; - const selectionPlan = selectionPlansByType[runtimeType.name]; + selectionPlan = selectionPlansByType[runtimeType.name]; if (!selectionPlan) { throw new GraphQLError( `Runtime Object type "${runtimeType}" ` + - `is not a possible coercion type for "${abstractType}".`, + `is not a possible type for "${abstractType}".`, plan.fieldASTs ); } - - return executeFields( - exeContext, - result, - selectionPlan.fieldPlans - ); + break; // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable default: invariant(false, 'No plan covers runtime conditions'); } + + invariant(selectionPlan); + invariant(runtimeType); + + // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, selectionPlan)) { + throw new GraphQLError( + `Expected value of type "${runtimeType}" but got: ${result}.`, + selectionPlan.fieldASTs + ); + } + + return executeFields( + exeContext, + result, + selectionPlan.fieldPlans + ); + } /** From fa2becb85c7524025c053e24ac1a399550904882 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 13:14:23 -0800 Subject: [PATCH 66/94] Fix spacing --- src/type/definition.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/type/definition.js b/src/type/definition.js index dd4d96c7d2..c8bf383894 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -504,9 +504,9 @@ export type GraphQLResolvingPlan = { */ export type GraphQLOperationPlan = { kind: 'execute'; - type: GraphQLObjectType; - strategy: string; - fieldPlans: {[key: string]: GraphQLResolvingPlan}; + type: GraphQLObjectType; + strategy: string; + fieldPlans: {[key: string]: GraphQLResolvingPlan}; } /** @@ -523,7 +523,7 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - fieldPlans: {[key: string]: GraphQLResolvingPlan}; + fieldPlans: {[key: string]: GraphQLResolvingPlan}; } /** From b87c2bc9d8d4ae44c6a4f4821d36abb900301816 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 19:26:00 -0800 Subject: [PATCH 67/94] Move getObjectType logic from definitions to execution, setting up isTypeOf to have a simpler signature in future refactoring --- src/execution/execute.js | 49 ++++++++++++++++++++++++++++++++++++---- src/type/definition.js | 25 +------------------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index d2f72b7103..0a4ff86213 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,15 +8,12 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Create an example of prefetching based on Execution plan // @TODO: Review Plan structures for consistency // @TODO: Sort out returnType in the various Plans -// @TODO: Currently NOT providing returnType in GraphQLSelectionPlan // @TODO: Currently NOT providing returnType in GraphQLCoercionPlan // @TODO: Re-approach plan design from the perspective: // @TODO: What does a resolver author need to know at this point in time? // @TODO: Document plan fields -// @TODO: Collect resolution use cases // @TODO: Does isTypeOf really need a context parameter? // @TODO: Examine error handling paths // @TODO: Should we really be returning fieldASTs in error messages? @@ -318,6 +315,7 @@ function planSelection( kind: 'select', fieldName, fieldASTs, + returnType: type, parentType, schema: exeContext.schema, fragments: exeContext.fragments, @@ -1055,7 +1053,12 @@ function completeValue( invariant(isAbstractType(returnType)); const abstractType = ((returnType: any): GraphQLAbstractType); - runtimeType = abstractType.getObjectType(result, plan); + + if (abstractType.resolveType) { + runtimeType = findTypeWithResolveType(abstractType, plan, result); + } else { + runtimeType = findTypeWithIsTypeOf(plan, result); + } if (!runtimeType) { // The type could not be resolved so omit from the results @@ -1065,7 +1068,6 @@ function completeValue( invariant(plan.selectionPlansByType !== null); const selectionPlansByType = plan.selectionPlansByType; - selectionPlan = selectionPlansByType[runtimeType.name]; if (!selectionPlan) { throw new GraphQLError( @@ -1074,6 +1076,7 @@ function completeValue( plan.fieldASTs ); } + break; // --- CASE Z: Unreachable @@ -1123,6 +1126,42 @@ function defaultResolveFn( } } +/** + * Determine which type in a GraphQLCoercionPlan matches the type of result. + */ +function findTypeWithResolveType( + type:GraphQLAbstractType, + plan: GraphQLCoercionPlan, + result: mixed +): ?GraphQLObjectType { + if (!type.resolveType) { + return null; + } + return type.resolveType(result, plan); +} + +/** + * Determine which type in a GraphQLCoercionPlan matches the type of result. + */ +function findTypeWithIsTypeOf( + plan: GraphQLCoercionPlan, + result: mixed +): ?GraphQLObjectType { + const plansByType = plan.selectionPlansByType; + // We constructed plansByType without a prototype + /* eslint guard-for-in:0 */ + for (const typeName in plansByType) { + const candidatePlan = plansByType[typeName]; + const type = candidatePlan.returnType; + if (type.isTypeOf && type.isTypeOf(result, candidatePlan)) { + return type; + } + } + + // Not found + return null; +} + /** * Checks to see if this object acts like a Promise, i.e. has a "then" * function. diff --git a/src/type/definition.js b/src/type/definition.js index c8bf383894..04dd04caa0 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -517,6 +517,7 @@ export type GraphQLSelectionPlan = { kind: 'select'; fieldName: string; fieldASTs: Array; + returnType: GraphQLObjectType; parentType: GraphQLCompositeType; schema: GraphQLSchema; fragments: { [fragmentName: string]: FragmentDefinition }; @@ -680,30 +681,11 @@ export class GraphQLInterfaceType { return Boolean(possibleTypes[type.name]); } - getObjectType(value: mixed, info: GraphQLResolveInfo): ?GraphQLObjectType { - const resolver = this.resolveType; - return resolver ? resolver(value, info) : getTypeOf(value, info, this); - } - toString(): string { return this.name; } } -function getTypeOf( - value: mixed, - info: GraphQLResolveInfo, - abstractType: GraphQLAbstractType -): ?GraphQLObjectType { - const possibleTypes = abstractType.getPossibleTypes(); - for (let i = 0; i < possibleTypes.length; i++) { - const type = possibleTypes[i]; - if (typeof type.isTypeOf === 'function' && type.isTypeOf(value, info)) { - return type; - } - } -} - export type GraphQLInterfaceTypeConfig = { name: string, fields: GraphQLFieldConfigMapThunk | GraphQLFieldConfigMap, @@ -801,11 +783,6 @@ export class GraphQLUnionType { return possibleTypeNames[type.name] === true; } - getObjectType(value: mixed, info: GraphQLResolveInfo): ?GraphQLObjectType { - const resolver = this._typeConfig.resolveType; - return resolver ? resolver(value, info) : getTypeOf(value, info, this); - } - toString(): string { return this.name; } From 9b766d6b0dd32b606a13c567457f8e9db1e8233d Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 19:38:26 -0800 Subject: [PATCH 68/94] For each of the three resolving functions, we are now able to specify which concrete plan they will receive in their info parameter --- src/execution/execute.js | 3 +-- src/type/definition.js | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 0a4ff86213..fd6c77ad28 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -38,7 +38,6 @@ import type { GraphQLType, GraphQLAbstractType, GraphQLFieldDefinition, - GraphQLResolveInfo, GraphQLOperationPlan, GraphQLSelectionPlan, GraphQLResolvingPlan, @@ -1115,7 +1114,7 @@ function completeValue( function defaultResolveFn( source:mixed, args:{ [key: string]: mixed }, - info: GraphQLResolveInfo + info: GraphQLResolvingPlan ) { const fieldName = info.fieldName; diff --git a/src/type/definition.js b/src/type/definition.js index 04dd04caa0..3049f1e872 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -301,7 +301,7 @@ export type GraphQLScalarTypeConfig = { export class GraphQLObjectType { name: string; description: ?string; - isTypeOf: ?(value: mixed, info?: GraphQLResolveInfo) => boolean; + isTypeOf: ?GraphQLIsTypeOfFn; _typeConfig: GraphQLObjectTypeConfig; _fields: GraphQLFieldDefinitionMap; @@ -459,7 +459,7 @@ export type GraphQLObjectTypeConfig = { name: string; interfaces?: GraphQLInterfacesThunk | Array; fields: GraphQLFieldConfigMapThunk | GraphQLFieldConfigMap; - isTypeOf?: (value: mixed, info?: GraphQLResolveInfo) => boolean; + isTypeOf?: GraphQLIsTypeOfFn; description?: ?string } @@ -470,9 +470,19 @@ type GraphQLFieldConfigMapThunk = () => GraphQLFieldConfigMap; export type GraphQLFieldResolveFn = ( source: mixed, args: {[argName: string]: mixed}, - info: GraphQLResolveInfo + info: GraphQLResolvingPlan ) => mixed +export type GraphQLTypeResolveFn = ( + value: mixed, + info?: GraphQLCoercionPlan +) => ?GraphQLObjectType + +export type GraphQLIsTypeOfFn = ( + value: mixed, + info?: GraphQLSelectionPlan +) => boolean + export type GraphQLResolveInfo = GraphQLResolvingPlan | GraphQLSelectionPlan | @@ -642,7 +652,7 @@ export type GraphQLFieldDefinitionMap = { export class GraphQLInterfaceType { name: string; description: ?string; - resolveType: ?(value: mixed, info?: GraphQLResolveInfo) => ?GraphQLObjectType; + resolveType: ?GraphQLTypeResolveFn; _typeConfig: GraphQLInterfaceTypeConfig; _fields: GraphQLFieldDefinitionMap; @@ -694,7 +704,7 @@ export type GraphQLInterfaceTypeConfig = { * the default implementation will call `isTypeOf` on each implementing * Object type. */ - resolveType?: (value: mixed, info?: GraphQLResolveInfo) => ?GraphQLObjectType, + resolveType?: GraphQLTypeResolveFn, description?: ?string }; @@ -726,7 +736,7 @@ export type GraphQLInterfaceTypeConfig = { export class GraphQLUnionType { name: string; description: ?string; - resolveType: ?(value: mixed, info?: GraphQLResolveInfo) => ?GraphQLObjectType; + resolveType: ?GraphQLTypeResolveFn; _typeConfig: GraphQLUnionTypeConfig; _types: Array; @@ -796,7 +806,7 @@ export type GraphQLUnionTypeConfig = { * the default implementation will call `isTypeOf` on each implementing * Object type. */ - resolveType?: (value: mixed, info?: GraphQLResolveInfo) => ?GraphQLObjectType; + resolveType?: GraphQLTypeResolveFn; description?: ?string; }; From 43c0c276a75d914e1222145449c791519cc48e1d Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 19:45:00 -0800 Subject: [PATCH 69/94] Add returnType back to GraphQLCoercionPlan --- src/execution/execute.js | 9 +++------ src/type/definition.js | 5 +++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index fd6c77ad28..7a4db025c7 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,12 +8,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: Review Plan structures for consistency -// @TODO: Sort out returnType in the various Plans -// @TODO: Currently NOT providing returnType in GraphQLCoercionPlan -// @TODO: Re-approach plan design from the perspective: -// @TODO: What does a resolver author need to know at this point in time? -// @TODO: Document plan fields +// @TODO: type vs returnType in non-resolving plans +// @TODO: remove resolveFn from plan? // @TODO: Does isTypeOf really need a context parameter? // @TODO: Examine error handling paths // @TODO: Should we really be returning fieldASTs in error messages? @@ -525,6 +521,7 @@ function planCompleteValue( kind: 'coerce', fieldName, fieldASTs, + returnType: abstractType, parentType, schema: exeContext.schema, fragments: exeContext.fragments, diff --git a/src/type/definition.js b/src/type/definition.js index 3049f1e872..39d8541e67 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -494,7 +494,7 @@ export type GraphQLResolveInfo = */ export type GraphQLResolvingPlan = { kind: 'resolve'; - fieldName: string, + fieldName: string; fieldASTs: Array; returnType: GraphQLOutputType; parentType: GraphQLCompositeType; @@ -567,8 +567,9 @@ export type GraphQLMappingPlan = { */ export type GraphQLCoercionPlan = { kind: 'coerce'; - fieldName: string, + fieldName: string; fieldASTs: Array; + returnType: GraphQLAbstractType; parentType: GraphQLCompositeType, schema: GraphQLSchema; fragments: { [fragmentName: string]: FragmentDefinition }; From 5a71e1d75eff04fb21df30df2cabe9e32bc6102b Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 20:13:07 -0800 Subject: [PATCH 70/94] Update comments with evaluate terminology --- src/execution/execute.js | 52 +++++++++++++--------------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 7a4db025c7..2945bdb703 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -84,29 +84,23 @@ import type { */ /** - * Queries are evaluated in two phases: planning and execution. - * The goal of planning is to precompute data needed for execution + * Queries are execcuted in two phases: planning and evaluation. + * The goal of planning is to precompute data needed for evaluation * and to give resolvers insight into how the query will be * executed * - * Execution uses the following terms: - * - * "execute" indicates evaluating an operation - * "resolve" indicates resolving a field or type value - * "complete" indicates processing return values from the resolving process. - * - * The planning and execution phases are coupled in this way: - * +------------------+-------------------+---------------------------+ - * | Execution | Planning | Plan | - * +------------------+-------------------+---------------------------+ - * | executeOperation | planOperation | GraphQLOperationPlan | - * | resolveField | planFields | GraphQLResolvingPlan | - * | completeValue | planCompleteValue | GraphQLCompletionPlan | - * | completeValue | planCompleteValue | GraphQLCoercionPlan | - * | completeValue | planCompleteValue | GraphQLMappingPlan | - * | completeValue | planCompleteValue | GraphQLSerializationPlan | - * | completeValue | planSelection | GraphQLSelectionPlan | - * +------------------+-------------------+---------------------------+ + * The planning and evaluation phases are coupled in this way: + * +-------------------+---------------------------+------------------+ + * | Planning | Plan | Evaluation | + * +-------------------+---------------------------+------------------+ + * | planOperation | GraphQLOperationPlan | executeOperation | + * | planFields | GraphQLResolvingPlan | resolveField | + * | planCompleteValue | GraphQLCompletionPlan | completeValue | + * | planCompleteValue | GraphQLCoercionPlan | completeValue | + * | planCompleteValue | GraphQLMappingPlan | completeValue | + * | planCompleteValue | GraphQLSerializationPlan | completeValue | + * | planSelection | GraphQLSelectionPlan | completeValue | + * +-------------------+---------------------------+------------------+ */ /** @@ -166,7 +160,7 @@ export function execute( operationName ); - // Create an Execution plan which will describe how we intend to execute + // Create an execution plan which will describe how we intend to evaluate // The query. const plan = planOperation(context, context.operation); @@ -404,22 +398,8 @@ function planResolveField( } /** - * Plans the Execution of completeValue as defined in the + * Plans the evaluation of completeValue as defined in the * "Field entries" section of the spec. - * - * If the field type is Non-Null, then this recursively completes the value - * for the inner type. It throws a field error if that completion returns null, - * as per the "Nullability" section of the spec. - * - * If the field type is a List, then this recursively completes the value - * for the inner type on each item in the list. - * - * If the field type is a Scalar or Enum, ensures the completed value is a legal - * value of the type by calling the `serialize` method of GraphQL type - * definition. - * - * Otherwise, the field type expects a sub-selection set, and will complete the - * value by evaluating all sub-selections. */ function planCompleteValue( exeContext: ExecutionContext, From d94c4cdb1e9bad08517f66edb2618dbef2c5384e Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 20:27:14 -0800 Subject: [PATCH 71/94] Clean up invariant messages --- src/execution/execute.js | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 2945bdb703..669bc1f2fb 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -8,12 +8,6 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -// @TODO: type vs returnType in non-resolving plans -// @TODO: remove resolveFn from plan? -// @TODO: Does isTypeOf really need a context parameter? -// @TODO: Examine error handling paths -// @TODO: Should we really be returning fieldASTs in error messages? - import { GraphQLError, locatedError } from '../error'; import find from '../jsutils/find'; import invariant from '../jsutils/invariant'; @@ -969,8 +963,10 @@ function completeValue( // Intentionally first; will be evaluated often // Tested in planCompleteValue - invariant(returnType instanceof GraphQLScalarType || - returnType instanceof GraphQLEnumType); + invariant( + returnType instanceof GraphQLScalarType || + returnType instanceof GraphQLEnumType, + 'Type detected at runtime does not match type expected in planning'); invariant(returnType.serialize, 'Missing serialize method on type'); @@ -981,7 +977,10 @@ function completeValue( // If result is null-like, return null. case 'map': // Tested in planCompleteValue - invariant(returnType instanceof GraphQLList); + invariant( + returnType instanceof GraphQLList, + 'Type detected at runtime does not match type expected in planning' + ); invariant( Array.isArray(result), @@ -1016,7 +1015,10 @@ function completeValue( // --- CASE G: GraphQLObjectType (run GraphQLSelectionPlan) case 'select': // Tested in planCompleteValue - invariant(returnType instanceof GraphQLObjectType); + invariant( + returnType instanceof GraphQLObjectType, + 'Type detected at runtime does not match type expected in planning' + ); selectionPlan = plan; runtimeType = returnType; @@ -1026,7 +1028,10 @@ function completeValue( // --- CASE H: isAbstractType (run GraphQLCoercionPlan) case 'coerce': // Tested in planCompleteValue - invariant(isAbstractType(returnType)); + invariant( + isAbstractType(returnType), + 'Type detected at runtime does not match type expected in planning' + ); const abstractType = ((returnType: any): GraphQLAbstractType); @@ -1036,10 +1041,11 @@ function completeValue( runtimeType = findTypeWithIsTypeOf(plan, result); } - if (!runtimeType) { - // The type could not be resolved so omit from the results - return null; - } + invariant( + runtimeType, + 'Could not resolve type,' + + 'probably an error in a resolveType or isTypeOf function in the schema' + ); invariant(plan.selectionPlansByType !== null); From c7a629632498505940cb7bf74e3d8e01f786cab2 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Wed, 2 Mar 2016 20:49:45 -0800 Subject: [PATCH 72/94] Remove NOTES.md file --- src/execution/NOTES.md | 55 ------------------------------------------ 1 file changed, 55 deletions(-) delete mode 100644 src/execution/NOTES.md diff --git a/src/execution/NOTES.md b/src/execution/NOTES.md deleted file mode 100644 index e27894a32a..0000000000 --- a/src/execution/NOTES.md +++ /dev/null @@ -1,55 +0,0 @@ -Split Execution into two phases -=============================== -This is an attempt to address optmization concerns during query evaluation. - -- https://github.com/graphql/graphql-js/issues/26 -- https://github.com/graphql/graphql-js/issues/161 -- https://github.com/graphql-dotnet/graphql-dotnet/issues/21 -- https://github.com/graphql/graphql-js/issues/149 -- https://github.com/graphql/graphql-js/issues/111 -- https://github.com/graphql/graphql-js/pull/39 -- https://github.com/graphql/graphql-js/issues/19 -- https://github.com/rmosolgo/graphql-ruby/issues/6 - -This approach splits execution into two phases, a planning phase and an evaluation phase. - -In the planning phase the AST in analyzed and a heirarchical plan structure is created indicating -how the executor will evaluate the query. Precalculating this information serves two purposes: - -1. Provides a reliable and simple indication to resolving functions what evaulations will occur next. -2. Avoids re-calculating some data when evaluating list results - -There is no attempt to optimize the plan. This is out of scope, although it would be possible to write -optimizing functions that accepted a plan and output a different plan before evaluation. - -Evaluation order is not changed. - -The current interface for resolver authors ------------------------------------------- - - -GraphQLObjectType allows defining an isTypeOf function - -` isTypeOf: ?(value: mixed, info?: GraphQLResolveInfo) => boolean;` - -A Default resolveType function is available in getTypeOf which may call isTypeOf - -`function getTypeOf(value: mixed, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType): ?GraphQLObjectType` - -GraphQLInterfaceType and GraphQLUnionType allow defining a resolveType function - -`resolveType: ?(value: mixed, info?: GraphQLResolveInfo) => ?GraphQLObjectType;` - -GraphQLInterfaceType and GraphQLUnionType defines a getObjectType function which calls the resolveType or getTypeOf function - -`getObjectType(value: mixed, info: GraphQLResolveInfo): ?GraphQLObjectType` - -A field definition defines a resolve function - -`resolve?: GraphQLFieldResolveFn;` - -(Why do GraphQLObjectType, GraphQLInterfaceType and GraphQLInterfaceType take their functions as config parameters, which field definition declares it directly? - -A GraphQLScalarType allows defining a serialize function - -`serialize: (value: mixed) => ?InternalType;` From 68772242580b5e406bb3f46f35fe1aad9a1145b1 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 09:56:06 -0800 Subject: [PATCH 73/94] Change the order of Plan type declarations to match documentation and evaluation order --- src/type/definition.js | 56 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/type/definition.js b/src/type/definition.js index 39d8541e67..9cd618e9a2 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -488,6 +488,17 @@ export type GraphQLResolveInfo = GraphQLSelectionPlan | GraphQLCoercionPlan; +/** + * Describes how the execution engine plans to perform + * an operation. + */ +export type GraphQLOperationPlan = { + kind: 'execute'; + type: GraphQLObjectType; + strategy: string; + fieldPlans: {[key: string]: GraphQLResolvingPlan}; +} + /** * Describes how the execution engine plans to interact with the schema's * resolve function to fetch field values indicated by the query @@ -509,14 +520,24 @@ export type GraphQLResolvingPlan = { } /** - * Describes how the execution engine plans to perform - * an operation. + * Execution plans which might be executed to Complete a value. */ -export type GraphQLOperationPlan = { - kind: 'execute'; - type: GraphQLObjectType; - strategy: string; - fieldPlans: {[key: string]: GraphQLResolvingPlan}; +export type GraphQLCompletionPlan = + GraphQLSerializationPlan | + GraphQLMappingPlan | + GraphQLSelectionPlan | + GraphQLCoercionPlan; + +/** + * Indicates that the execution engine plans to call + * serialize for a value. + */ +export type GraphQLSerializationPlan = { + kind: 'serialize'; + fieldName: string; + fieldASTs: Array; + parentType: GraphQLCompositeType; + type: GraphQLType; } /** @@ -537,18 +558,6 @@ export type GraphQLSelectionPlan = { fieldPlans: {[key: string]: GraphQLResolvingPlan}; } -/** - * Indicates that the execution engine plans to call - * serialize for a value. - */ -export type GraphQLSerializationPlan = { - kind: 'serialize'; - fieldName: string; - fieldASTs: Array; - parentType: GraphQLCompositeType; - type: GraphQLType; -} - /** * Describes how the execution engine plans to map list values */ @@ -579,15 +588,6 @@ export type GraphQLCoercionPlan = { selectionPlansByType: {[key: string]:GraphQLSelectionPlan}; } -/** - * Execution plans which might be executed to Complete a value. - */ -export type GraphQLCompletionPlan = - GraphQLSelectionPlan | - GraphQLSerializationPlan | - GraphQLMappingPlan | - GraphQLCoercionPlan; - export type GraphQLFieldConfig = { type: GraphQLOutputType; args?: GraphQLFieldConfigArgumentMap; From 95e33b34affe3671fd5c4d5899d98245e3393422 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 09:59:17 -0800 Subject: [PATCH 74/94] Rename generic key portion of type to more explainatory alias or typeName --- src/type/definition.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/type/definition.js b/src/type/definition.js index 9cd618e9a2..418abee17b 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -496,7 +496,7 @@ export type GraphQLOperationPlan = { kind: 'execute'; type: GraphQLObjectType; strategy: string; - fieldPlans: {[key: string]: GraphQLResolvingPlan}; + fieldPlans: {[alias: string]: GraphQLResolvingPlan}; } /** @@ -555,7 +555,7 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - fieldPlans: {[key: string]: GraphQLResolvingPlan}; + fieldPlans: {[alias: string]: GraphQLResolvingPlan}; } /** @@ -585,7 +585,7 @@ export type GraphQLCoercionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - selectionPlansByType: {[key: string]:GraphQLSelectionPlan}; + selectionPlansByType: {[typeName: string]:GraphQLSelectionPlan}; } export type GraphQLFieldConfig = { From 3d9fa60c5dc36744768723ce693f53af09fb2167 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 10:01:59 -0800 Subject: [PATCH 75/94] Introduce term alias instead of generic key for more descriptive typing --- src/execution/execute.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 669bc1f2fb..f44afd7a42 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -317,8 +317,8 @@ function planSelection( function planFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, - fields: {[key: string]: Array} -): {[key: string]: GraphQLResolvingPlan} { + fields: {[alias: string]: Array} +): {[alias: string]: GraphQLResolvingPlan} { const results = Object.create(null); Object.keys(fields).forEach( responseName => { @@ -573,7 +573,7 @@ function getOperationRootType( function executeFieldsSerially( exeContext: ExecutionContext, sourceValue: mixed, - fields: {[key: string]: GraphQLResolvingPlan} + fields: {[alias: string]: GraphQLResolvingPlan} ): Promise { return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { @@ -605,7 +605,7 @@ function executeFieldsSerially( function executeFields( exeContext: ExecutionContext, sourceValue: mixed, - fields: {[key: string]: GraphQLResolvingPlan} + fields: {[alias: string]: GraphQLResolvingPlan} ): Object { let containsPromise = false; @@ -652,9 +652,9 @@ function collectFields( exeContext: ExecutionContext, runtimeType: GraphQLObjectType, selectionSet: SelectionSet, - fields: {[key: string]: Array}, + fields: {[alias: string]: Array}, visitedFragmentNames: {[key: string]: boolean} -): {[key: string]: Array} { +): {[alias: string]: Array} { for (let i = 0; i < selectionSet.selections.length; i++) { const selection = selectionSet.selections[i]; switch (selection.kind) { From 1b75c3af14b79eb01ad33fefa589760531a7c029 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 10:12:38 -0800 Subject: [PATCH 76/94] Remove resolveFn and store fieldDefinition instead which is more useful, bringing resolve in line with the other plans, so that I don't have to document why its different --- src/execution/execute.js | 23 +++++++++++++++-------- src/type/definition.js | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index f44afd7a42..8c5ee8d635 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -354,7 +354,6 @@ function planResolveField( } const returnType = fieldDef.type; - const resolveFn = fieldDef.resolve || defaultResolveFn; // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. @@ -383,7 +382,7 @@ function planResolveField( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - resolveFn, + fieldDefinition: fieldDef, args, completionPlan }; @@ -805,9 +804,13 @@ function resolveField( source: mixed ): mixed { + const fieldDef = plan.fieldDefinition; + const resolveFn = fieldDef.resolve || defaultResolveFn; + const args = plan.args; + // Get the resolve function, regardless of if its result is normal // or abrupt (error). - const result = resolveOrError(plan, source); + const result = resolveOrError(resolveFn, source, args, plan); return completeValueCatchingError( exeContext, @@ -819,12 +822,16 @@ function resolveField( // Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` // function. Returns the result of resolveFn or the abrupt-return Error object. -function resolveOrError( - plan: GraphQLResolvingPlan, - source: mixed +function resolveOrError( + resolveFn: ( + source: mixed, + args: { [key: string]: mixed }, + info: GraphQLResolvingPlan + ) => T, + source: mixed, + args: { [key: string]: mixed }, + plan: GraphQLResolvingPlan ): Error | mixed { - const resolveFn = plan.resolveFn; - const args = plan.args; try { return resolveFn(source, args, plan); } catch (error) { diff --git a/src/type/definition.js b/src/type/definition.js index 418abee17b..c1b4e962e0 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -514,7 +514,7 @@ export type GraphQLResolvingPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - resolveFn: GraphQLFieldResolveFn; + fieldDefinition: GraphQLFieldDefinition; args: { [key: string]: mixed }; completionPlan: GraphQLCompletionPlan; } From 53a6e1570ad32551fc3f945c973f899ba8b1bc4f Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 10:15:00 -0800 Subject: [PATCH 77/94] Incorrectly restored method signature --- src/execution/execute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 8c5ee8d635..30f0e5f77c 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -831,7 +831,7 @@ function resolveOrError( source: mixed, args: { [key: string]: mixed }, plan: GraphQLResolvingPlan -): Error | mixed { +): Error | T { try { return resolveFn(source, args, plan); } catch (error) { From 7949a59b43064aebf1bb966a815847516e2603bf Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 13:05:49 -0800 Subject: [PATCH 78/94] Add precomputed fieldList property to selection plans, making the simple resolver cases simple --- src/execution/execute.js | 29 +++++++++++++++++++++-------- src/type/definition.js | 2 ++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 30f0e5f77c..0e4aecbc0b 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -254,12 +254,13 @@ function planOperation( Object.create(null) ); - const fieldPlans = planFields(exeContext, type, fields); + const {fieldPlans, fieldList} = planFields(exeContext, type, fields); const plan: GraphQLOperationPlan = { kind: 'execute', type, strategy, + fieldList, fieldPlans }; @@ -292,7 +293,7 @@ function planSelection( } } - const fieldPlans = planFields(exeContext, type, fields); + const {fieldPlans, fieldList} = planFields(exeContext, type, fields); const plan: GraphQLSelectionPlan = { kind: 'select', @@ -305,12 +306,18 @@ function planSelection( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, + fieldList, fieldPlans }; return plan; } +type planFieldsResult = { + fieldPlans: {[alias: string]: GraphQLResolvingPlan}; + fieldList: {[fieldName: string]: [ string ]}; +} + /** * Plan the "Evaluating selection sets" section of the spec */ @@ -318,22 +325,28 @@ function planFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, fields: {[alias: string]: Array} -): {[alias: string]: GraphQLResolvingPlan} { - const results = Object.create(null); +): planFieldsResult { + const fieldPlans = Object.create(null); + const fieldList = Object.create(null); Object.keys(fields).forEach( responseName => { const fieldASTs = fields[responseName]; - const result = planResolveField( + const fieldPlan = planResolveField( exeContext, parentType, fieldASTs ); - if (result) { - results[responseName] = result; + if (fieldPlan) { + fieldPlans[responseName] = fieldPlan; + if (fieldList[fieldPlan.fieldName]) { + fieldList[fieldPlan.fieldName].push(responseName); + } else { + fieldList[fieldPlan.fieldName] = [ responseName ]; + } } } ); - return results; + return {fieldPlans, fieldList}; } /** diff --git a/src/type/definition.js b/src/type/definition.js index c1b4e962e0..ded84ee351 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -496,6 +496,7 @@ export type GraphQLOperationPlan = { kind: 'execute'; type: GraphQLObjectType; strategy: string; + fieldList: {[fieldName: string]: [ string ]}; fieldPlans: {[alias: string]: GraphQLResolvingPlan}; } @@ -555,6 +556,7 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; + fieldList: {[fieldName: string]: [ string ]}; fieldPlans: {[alias: string]: GraphQLResolvingPlan}; } From 65c774eb89a6143aba7729e51643644d87d67fb9 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 14:36:02 -0800 Subject: [PATCH 79/94] Rename completionPlan to elementPlan for GraphQLMappingPlan to avoid confusion when chaining fields. --- src/execution/execute.js | 8 ++++---- src/type/definition.js | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 0e4aecbc0b..488d303b46 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -456,7 +456,7 @@ function planCompleteValue( if (returnType instanceof GraphQLList) { const innerType = returnType.ofType; - const completionPlan = + const elementPlan = planCompleteValue( exeContext, innerType, @@ -471,7 +471,7 @@ function planCompleteValue( fieldASTs, fieldName, parentType, - completionPlan + elementPlan }; return plan; @@ -1008,7 +1008,7 @@ function completeValue( `for field ${plan.parentType}.${plan.fieldName}.` ); - const completionPlan = plan.completionPlan; + const elementPlan = plan.elementPlan; // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another @@ -1019,7 +1019,7 @@ function completeValue( const completedItem = completeValueCatchingError( exeContext, - completionPlan, + elementPlan, itemType, item ); diff --git a/src/type/definition.js b/src/type/definition.js index ded84ee351..6bfb711a6e 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -541,6 +541,18 @@ export type GraphQLSerializationPlan = { type: GraphQLType; } +/** + * Describes how the execution engine plans to map list values + */ +export type GraphQLMappingPlan = { + kind: 'map'; + fieldName: string; + fieldASTs: Array; + parentType: GraphQLCompositeType; + type: GraphQLType; + elementPlan: GraphQLCompletionPlan; +} + /** * Describes how the execution engine plans to perform a selection * on a resolved value. @@ -560,18 +572,6 @@ export type GraphQLSelectionPlan = { fieldPlans: {[alias: string]: GraphQLResolvingPlan}; } -/** - * Describes how the execution engine plans to map list values - */ -export type GraphQLMappingPlan = { - kind: 'map'; - fieldName: string; - fieldASTs: Array; - parentType: GraphQLCompositeType; - type: GraphQLType; - completionPlan: GraphQLCompletionPlan; -} - /** * Describes plans which the execution engine may take * based on the run time type of a value. From 0efee5b2d7861ce5ac055adb081077c4cf4cd072 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 14:45:37 -0800 Subject: [PATCH 80/94] Rename type to returnType for documentation consistency --- src/execution/execute.js | 4 ++-- src/type/definition.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 488d303b46..eed8fe02e7 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -442,8 +442,8 @@ function planCompleteValue( const plan: GraphQLSerializationPlan = { kind: 'serialize', - type: returnType, fieldASTs, + returnType, fieldName, parentType }; @@ -467,8 +467,8 @@ function planCompleteValue( const plan: GraphQLMappingPlan = { kind: 'map', - type: returnType, fieldASTs, + returnType, fieldName, parentType, elementPlan diff --git a/src/type/definition.js b/src/type/definition.js index 6bfb711a6e..44768aa5ce 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -537,8 +537,8 @@ export type GraphQLSerializationPlan = { kind: 'serialize'; fieldName: string; fieldASTs: Array; + returnType: GraphQLType; parentType: GraphQLCompositeType; - type: GraphQLType; } /** @@ -548,8 +548,8 @@ export type GraphQLMappingPlan = { kind: 'map'; fieldName: string; fieldASTs: Array; + returnType: GraphQLType; parentType: GraphQLCompositeType; - type: GraphQLType; elementPlan: GraphQLCompletionPlan; } From 73321f273bed6001155c75fac4ded08175dd17cf Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 16:46:30 -0800 Subject: [PATCH 81/94] Differences without distinction: restore original order of operation to make diff easier to understand --- src/execution/execute.js | 80 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index eed8fe02e7..4dbdfe5632 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -433,25 +433,7 @@ function planCompleteValue( // --- CASE D: Nullish (Execution time only, see completeValue) - // --- CASE E: Serialize (See completeValue for plan Execution) - // If field type is Scalar or Enum, serialize to a valid value, returning - // null if serialization is not possible. - if (returnType instanceof GraphQLScalarType || - returnType instanceof GraphQLEnumType) { - invariant(returnType.serialize, 'Missing serialize method on type'); - - const plan: GraphQLSerializationPlan = { - kind: 'serialize', - fieldASTs, - returnType, - fieldName, - parentType - }; - - return plan; - } - - // --- CASE F: GraphQLList (See completeValue for plan Execution) + // --- CASE E: GraphQLList (See completeValue for plan Execution) // If field type is List, complete each item in the list with the inner type if (returnType instanceof GraphQLList) { const innerType = returnType.ofType; @@ -477,6 +459,25 @@ function planCompleteValue( return plan; } + // --- CASE F: Serialize (See completeValue for plan Execution) + // If field type is Scalar or Enum, serialize to a valid value, returning + // null if serialization is not possible. + if (returnType instanceof GraphQLScalarType || + returnType instanceof GraphQLEnumType) { + invariant(returnType.serialize, 'Missing serialize method on type'); + + const plan: GraphQLSerializationPlan = { + kind: 'serialize', + fieldASTs, + returnType, + fieldName, + parentType + }; + + return plan; + } + + // --- CASE G: GraphQLObjectType (See completeValue for plan Execution) if (returnType instanceof GraphQLObjectType) { return planSelection( @@ -664,9 +665,9 @@ function collectFields( exeContext: ExecutionContext, runtimeType: GraphQLObjectType, selectionSet: SelectionSet, - fields: {[alias: string]: Array}, + fields: {[key: string]: Array}, visitedFragmentNames: {[key: string]: boolean} -): {[alias: string]: Array} { +): {[key: string]: Array} { for (let i = 0; i < selectionSet.selections.length; i++) { const selection = selectionSet.selections[i]; switch (selection.kind) { @@ -977,23 +978,7 @@ function completeValue( // Execution Completion Plan switch (plan.kind) { - // --- CASE E: Serialize (run GraphQLSerializationCompletionPlan) - // If result is null-like, return null. - case 'serialize': - // Intentionally first; will be evaluated often - - // Tested in planCompleteValue - invariant( - returnType instanceof GraphQLScalarType || - returnType instanceof GraphQLEnumType, - 'Type detected at runtime does not match type expected in planning'); - - invariant(returnType.serialize, 'Missing serialize method on type'); - - const serializedResult = returnType.serialize(result); - return isNullish(serializedResult) ? null : serializedResult; - - // --- CASE F: GraphQLList (run GraphQLMappingPlan) + // --- CASE E: GraphQLList (run GraphQLMappingPlan) // If result is null-like, return null. case 'map': // Tested in planCompleteValue @@ -1032,6 +1017,21 @@ function completeValue( return containsPromise ? Promise.all(completedResults) : completedResults; + // --- CASE F: Serialize (run GraphQLSerializationCompletionPlan) + // If result is null-like, return null. + case 'serialize': + + // Tested in planCompleteValue + invariant( + returnType instanceof GraphQLScalarType || + returnType instanceof GraphQLEnumType, + 'Type detected at runtime does not match type expected in planning'); + + invariant(returnType.serialize, 'Missing serialize method on type'); + + const serializedResult = returnType.serialize(result); + return isNullish(serializedResult) ? null : serializedResult; + // --- CASE G: GraphQLObjectType (run GraphQLSelectionPlan) case 'select': // Tested in planCompleteValue @@ -1095,8 +1095,8 @@ function completeValue( // than continuing execution. if (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, selectionPlan)) { throw new GraphQLError( - `Expected value of type "${runtimeType}" but got: ${result}.`, - selectionPlan.fieldASTs + `Expected value of type "${runtimeType}" but got: ${result}.`, + selectionPlan.fieldASTs ); } From f8fd24543a597ee0b2c889d896eb7fc301a56d53 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 16:49:02 -0800 Subject: [PATCH 82/94] Move GraphQLOperationPlan to execute.js since it is not exposed publicly --- src/execution/execute.js | 13 ++++++++++++- src/type/definition.js | 12 ------------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 4dbdfe5632..c9639db39c 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -28,7 +28,6 @@ import type { GraphQLType, GraphQLAbstractType, GraphQLFieldDefinition, - GraphQLOperationPlan, GraphQLSelectionPlan, GraphQLResolvingPlan, GraphQLCompletionPlan, @@ -122,6 +121,18 @@ type ExecutionResult = { errors?: Array; } +/** + * Describes how the execution engine plans to perform + * an operation. + */ +type GraphQLOperationPlan = { + kind: 'execute'; + type: GraphQLObjectType; + strategy: string; + fieldList: {[fieldName: string]: [ string ]}; + fieldPlans: {[alias: string]: GraphQLResolvingPlan}; +} + /** * Implements the "Evaluating requests" section of the GraphQL specification. * diff --git a/src/type/definition.js b/src/type/definition.js index 44768aa5ce..036d5d47bb 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -488,18 +488,6 @@ export type GraphQLResolveInfo = GraphQLSelectionPlan | GraphQLCoercionPlan; -/** - * Describes how the execution engine plans to perform - * an operation. - */ -export type GraphQLOperationPlan = { - kind: 'execute'; - type: GraphQLObjectType; - strategy: string; - fieldList: {[fieldName: string]: [ string ]}; - fieldPlans: {[alias: string]: GraphQLResolvingPlan}; -} - /** * Describes how the execution engine plans to interact with the schema's * resolve function to fetch field values indicated by the query From 6b6ecb89e283a4f511ce0f92bfda0713d05327b8 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 16:51:08 -0800 Subject: [PATCH 83/94] Rename strategy to concurrencyStrategy --- src/execution/execute.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index c9639db39c..27387852d3 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -128,7 +128,7 @@ type ExecutionResult = { type GraphQLOperationPlan = { kind: 'execute'; type: GraphQLObjectType; - strategy: string; + concurrencyStrategy: string; fieldList: {[fieldName: string]: [ string ]}; fieldPlans: {[alias: string]: GraphQLResolvingPlan}; } @@ -255,7 +255,8 @@ function planOperation( operation: OperationDefinition ): GraphQLOperationPlan { const type = getOperationRootType(exeContext.schema, operation); - const strategy = (operation.operation === 'mutation') ? 'serial' : 'parallel'; + const concurrencyStrategy = + (operation.operation === 'mutation') ? 'serial' : 'parallel'; const fields = collectFields( exeContext, @@ -270,7 +271,7 @@ function planOperation( const plan: GraphQLOperationPlan = { kind: 'execute', type, - strategy, + concurrencyStrategy, fieldList, fieldPlans }; @@ -546,9 +547,12 @@ function executeOperation( rootValue: mixed ): Object { - invariant(plan.strategy === 'serial' || plan.strategy === 'parallel'); + invariant( + plan.concurrencyStrategy === 'serial' || + plan.concurrencyStrategy === 'parallel' + ); - if (plan.strategy === 'serial') { + if (plan.concurrencyStrategy === 'serial') { return executeFieldsSerially(exeContext, rootValue, plan.fieldPlans); } return executeFields(exeContext, rootValue, plan.fieldPlans); From 9feb9f34cfc2df6e6b1a999ec7ef6c8efd8f1bfc Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 17:05:30 -0800 Subject: [PATCH 84/94] Add an invariant in case types are setup incorrectly --- src/execution/execute.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/execution/execute.js b/src/execution/execute.js index 27387852d3..e0e64ea7fd 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -507,6 +507,11 @@ function planCompleteValue( const possibleTypes = abstractType.getPossibleTypes(); const selectionPlansByType = Object.create(null); possibleTypes.forEach(possibleType => { + invariant( + !selectionPlansByType[possibleType.name], + 'Two types cannot have the same name "${possibleType.name}"' + + 'as possible types of abstract type ${abstractType.name}' + ); selectionPlansByType[possibleType.name] = planSelection( exeContext, possibleType, From dfd4259c86bd001ca567e12e147577bec4eef70d Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 19:02:48 -0800 Subject: [PATCH 85/94] Rename fieldPlans to fieldPlansByAlias and fieldList to fieldPlans --- src/execution/execute.js | 38 +++++++++++++++++++------------------- src/type/definition.js | 4 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index e0e64ea7fd..2c35bba9e2 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -129,8 +129,8 @@ type GraphQLOperationPlan = { kind: 'execute'; type: GraphQLObjectType; concurrencyStrategy: string; - fieldList: {[fieldName: string]: [ string ]}; - fieldPlans: {[alias: string]: GraphQLResolvingPlan}; + fieldPlans: {[fieldName: string]: [ string ]}; + fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; } /** @@ -266,14 +266,14 @@ function planOperation( Object.create(null) ); - const {fieldPlans, fieldList} = planFields(exeContext, type, fields); + const {fieldPlansByAlias, fieldPlans} = planFields(exeContext, type, fields); const plan: GraphQLOperationPlan = { kind: 'execute', type, concurrencyStrategy, - fieldList, - fieldPlans + fieldPlans, + fieldPlansByAlias }; return plan; @@ -305,7 +305,7 @@ function planSelection( } } - const {fieldPlans, fieldList} = planFields(exeContext, type, fields); + const {fieldPlansByAlias, fieldPlans} = planFields(exeContext, type, fields); const plan: GraphQLSelectionPlan = { kind: 'select', @@ -318,16 +318,16 @@ function planSelection( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - fieldList, - fieldPlans + fieldPlans, + fieldPlansByAlias }; return plan; } type planFieldsResult = { - fieldPlans: {[alias: string]: GraphQLResolvingPlan}; - fieldList: {[fieldName: string]: [ string ]}; + fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; + fieldPlans: {[fieldName: string]: [ string ]}; } /** @@ -338,8 +338,8 @@ function planFields( parentType: GraphQLObjectType, fields: {[alias: string]: Array} ): planFieldsResult { + const fieldPlansByAlias = Object.create(null); const fieldPlans = Object.create(null); - const fieldList = Object.create(null); Object.keys(fields).forEach( responseName => { const fieldASTs = fields[responseName]; @@ -349,16 +349,16 @@ function planFields( fieldASTs ); if (fieldPlan) { - fieldPlans[responseName] = fieldPlan; - if (fieldList[fieldPlan.fieldName]) { - fieldList[fieldPlan.fieldName].push(responseName); + fieldPlansByAlias[responseName] = fieldPlan; + if (fieldPlans[fieldPlan.fieldName]) { + fieldPlans[fieldPlan.fieldName].push(fieldPlan); } else { - fieldList[fieldPlan.fieldName] = [ responseName ]; + fieldPlans[fieldPlan.fieldName] = [ fieldPlan ]; } } } ); - return {fieldPlans, fieldList}; + return {fieldPlansByAlias, fieldPlans}; } /** @@ -558,9 +558,9 @@ function executeOperation( ); if (plan.concurrencyStrategy === 'serial') { - return executeFieldsSerially(exeContext, rootValue, plan.fieldPlans); + return executeFieldsSerially(exeContext, rootValue, plan.fieldPlansByAlias); } - return executeFields(exeContext, rootValue, plan.fieldPlans); + return executeFields(exeContext, rootValue, plan.fieldPlansByAlias); } /** @@ -1123,7 +1123,7 @@ function completeValue( return executeFields( exeContext, result, - selectionPlan.fieldPlans + selectionPlan.fieldPlansByAlias ); } diff --git a/src/type/definition.js b/src/type/definition.js index 036d5d47bb..19c7a85443 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -556,8 +556,8 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - fieldList: {[fieldName: string]: [ string ]}; - fieldPlans: {[alias: string]: GraphQLResolvingPlan}; + fieldPlans: {[fieldName: string]: [ string ]}; + fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; } /** From 675feecd0825d79d06066682477a93eb51dd7633 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Thu, 3 Mar 2016 19:53:20 -0800 Subject: [PATCH 86/94] forgot to change the type of the fieldPlans array --- src/execution/execute.js | 4 ++-- src/type/definition.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 2c35bba9e2..47acc97655 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -129,7 +129,7 @@ type GraphQLOperationPlan = { kind: 'execute'; type: GraphQLObjectType; concurrencyStrategy: string; - fieldPlans: {[fieldName: string]: [ string ]}; + fieldPlans: {[fieldName: string]: [ GraphQLResolvingPlan ]}; fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; } @@ -327,7 +327,7 @@ function planSelection( type planFieldsResult = { fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; - fieldPlans: {[fieldName: string]: [ string ]}; + fieldPlans: {[fieldName: string]: [ GraphQLResolvingPlan ]}; } /** diff --git a/src/type/definition.js b/src/type/definition.js index 19c7a85443..1e7edcb06c 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -556,7 +556,7 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - fieldPlans: {[fieldName: string]: [ string ]}; + fieldPlans: {[fieldName: string]: [ GraphQLResolvingPlan ]}; fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; } From f97def5fa50df16de9a4d08cb337abbe420854e3 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Fri, 4 Mar 2016 09:54:35 -0800 Subject: [PATCH 87/94] Rename plan traversal fields to form a more fluent interface: completionPlan to returned, elementPlan to listElement, fieldPlans to fields, and selectionPlansByType to typeChoices --- src/execution/execute.js | 30 +++++++++++++++--------------- src/type/definition.js | 8 ++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 47acc97655..49e66ea8e8 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -129,7 +129,7 @@ type GraphQLOperationPlan = { kind: 'execute'; type: GraphQLObjectType; concurrencyStrategy: string; - fieldPlans: {[fieldName: string]: [ GraphQLResolvingPlan ]}; + fields: {[fieldName: string]: [ GraphQLResolvingPlan ]}; fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; } @@ -272,7 +272,7 @@ function planOperation( kind: 'execute', type, concurrencyStrategy, - fieldPlans, + fields: fieldPlans, fieldPlansByAlias }; @@ -318,7 +318,7 @@ function planSelection( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - fieldPlans, + fields: fieldPlans, fieldPlansByAlias }; @@ -409,7 +409,7 @@ function planResolveField( variableValues: exeContext.variableValues, fieldDefinition: fieldDef, args, - completionPlan + returned: completionPlan }; return plan; @@ -465,7 +465,7 @@ function planCompleteValue( returnType, fieldName, parentType, - elementPlan + listElement: elementPlan }; return plan; @@ -505,14 +505,14 @@ function planCompleteValue( if (isAbstractType(returnType)) { const abstractType = ((returnType: any): GraphQLAbstractType); const possibleTypes = abstractType.getPossibleTypes(); - const selectionPlansByType = Object.create(null); + const typeChoices = Object.create(null); possibleTypes.forEach(possibleType => { invariant( - !selectionPlansByType[possibleType.name], + !typeChoices[possibleType.name], 'Two types cannot have the same name "${possibleType.name}"' + 'as possible types of abstract type ${abstractType.name}' ); - selectionPlansByType[possibleType.name] = planSelection( + typeChoices[possibleType.name] = planSelection( exeContext, possibleType, fieldASTs, @@ -532,7 +532,7 @@ function planCompleteValue( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - selectionPlansByType + typeChoices }; return plan; @@ -848,7 +848,7 @@ function resolveField( return completeValueCatchingError( exeContext, - plan.completionPlan, + plan.returned, plan.returnType, result ); @@ -1013,7 +1013,7 @@ function completeValue( `for field ${plan.parentType}.${plan.fieldName}.` ); - const elementPlan = plan.elementPlan; + const elementPlan = plan.listElement; // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another @@ -1087,10 +1087,10 @@ function completeValue( 'probably an error in a resolveType or isTypeOf function in the schema' ); - invariant(plan.selectionPlansByType !== null); + invariant(plan.typeChoices !== null); - const selectionPlansByType = plan.selectionPlansByType; - selectionPlan = selectionPlansByType[runtimeType.name]; + const typeChoices = plan.typeChoices; + selectionPlan = typeChoices[runtimeType.name]; if (!selectionPlan) { throw new GraphQLError( `Runtime Object type "${runtimeType}" ` + @@ -1169,7 +1169,7 @@ function findTypeWithIsTypeOf( plan: GraphQLCoercionPlan, result: mixed ): ?GraphQLObjectType { - const plansByType = plan.selectionPlansByType; + const plansByType = plan.typeChoices; // We constructed plansByType without a prototype /* eslint guard-for-in:0 */ for (const typeName in plansByType) { diff --git a/src/type/definition.js b/src/type/definition.js index 1e7edcb06c..63c93c5518 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -505,7 +505,7 @@ export type GraphQLResolvingPlan = { variableValues: { [variableName: string]: mixed }; fieldDefinition: GraphQLFieldDefinition; args: { [key: string]: mixed }; - completionPlan: GraphQLCompletionPlan; + returned: GraphQLCompletionPlan; } /** @@ -538,7 +538,7 @@ export type GraphQLMappingPlan = { fieldASTs: Array; returnType: GraphQLType; parentType: GraphQLCompositeType; - elementPlan: GraphQLCompletionPlan; + listElement: GraphQLCompletionPlan; } /** @@ -556,7 +556,7 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - fieldPlans: {[fieldName: string]: [ GraphQLResolvingPlan ]}; + fields: {[fieldName: string]: [ GraphQLResolvingPlan ]}; fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; } @@ -575,7 +575,7 @@ export type GraphQLCoercionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - selectionPlansByType: {[typeName: string]:GraphQLSelectionPlan}; + typeChoices: {[typeName: string]:GraphQLSelectionPlan}; } export type GraphQLFieldConfig = { From 368d718b7b0f18a68fe3eeb08fc3134f015f83ab Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 5 Mar 2016 00:01:12 -0800 Subject: [PATCH 88/94] Extract serialization to its own function --- src/execution/execute.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 49e66ea8e8..d9ee6e939b 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -917,6 +917,21 @@ function completeValueCatchingError( } } +/** + * Evaluate a serialization plan + */ +function evaluateSerializationPlan( + result: mixed, + plan: GraphQLSerializationPlan +): mixed { + invariant(plan.kind === 'serialize'); + invariant(plan.returnType.serialize, 'Missing serialize method on type'); + + // If result is null-like, return null. + const serializedResult = plan.returnType.serialize(result); + return isNullish(serializedResult) ? null : serializedResult; +} + /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -1038,19 +1053,8 @@ function completeValue( Promise.all(completedResults) : completedResults; // --- CASE F: Serialize (run GraphQLSerializationCompletionPlan) - // If result is null-like, return null. case 'serialize': - - // Tested in planCompleteValue - invariant( - returnType instanceof GraphQLScalarType || - returnType instanceof GraphQLEnumType, - 'Type detected at runtime does not match type expected in planning'); - - invariant(returnType.serialize, 'Missing serialize method on type'); - - const serializedResult = returnType.serialize(result); - return isNullish(serializedResult) ? null : serializedResult; + return evaluateSerializationPlan(result, plan); // --- CASE G: GraphQLObjectType (run GraphQLSelectionPlan) case 'select': From 72364488f8ac32e852df0d3bcc76c24a477a2897 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 5 Mar 2016 08:22:19 -0800 Subject: [PATCH 89/94] Introduce evaluateSelectionPlan method and add isTypeOfFn to SelectionPlan --- src/execution/execute.js | 73 +++++++++++++++++++++++----------------- src/type/definition.js | 2 ++ 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index d9ee6e939b..e748b30e2b 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -305,6 +305,16 @@ function planSelection( } } + let isTypeOfOptimisticFn; + let isTypeOfPessimisticFn; + if (type.isTypeOf) { + isTypeOfOptimisticFn = type.isTypeOf; + isTypeOfPessimisticFn = type.isTypeOf; + } else { + isTypeOfOptimisticFn = () => true; + isTypeOfPessimisticFn = () => false; + } + const {fieldPlansByAlias, fieldPlans} = planFields(exeContext, type, fields); const plan: GraphQLSelectionPlan = { @@ -318,6 +328,8 @@ function planSelection( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, + isTypeOfOptimisticFn, + isTypeOfPessimisticFn, fields: fieldPlans, fieldPlansByAlias }; @@ -932,6 +944,33 @@ function evaluateSerializationPlan( return isNullish(serializedResult) ? null : serializedResult; } +/** + * Evaluate a slection plan + */ +function evaluateSelectionPlan( + exeContext: ExecutionContext, + result: mixed, + plan: GraphQLSelectionPlan +): mixed { + invariant(plan.kind === 'select'); + + // 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 (!plan.isTypeOfOptimisticFn(result, plan)) { + throw new GraphQLError( + `Expected value of type "${plan.returnType}" but got: ${result}.`, + plan.fieldASTs + ); + } + + return executeFields( + exeContext, + result, + plan.fieldPlansByAlias + ); +} + /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -1058,16 +1097,7 @@ function completeValue( // --- CASE G: GraphQLObjectType (run GraphQLSelectionPlan) case 'select': - // Tested in planCompleteValue - invariant( - returnType instanceof GraphQLObjectType, - 'Type detected at runtime does not match type expected in planning' - ); - - selectionPlan = plan; - runtimeType = returnType; - - break; + return evaluateSelectionPlan(exeContext, result, plan); // --- CASE H: isAbstractType (run GraphQLCoercionPlan) case 'coerce': @@ -1103,7 +1133,7 @@ function completeValue( ); } - break; + return evaluateSelectionPlan(exeContext, result, selectionPlan); // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable @@ -1111,25 +1141,6 @@ function completeValue( invariant(false, 'No plan covers runtime conditions'); } - invariant(selectionPlan); - invariant(runtimeType); - - // 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 (runtimeType.isTypeOf && !runtimeType.isTypeOf(result, selectionPlan)) { - throw new GraphQLError( - `Expected value of type "${runtimeType}" but got: ${result}.`, - selectionPlan.fieldASTs - ); - } - - return executeFields( - exeContext, - result, - selectionPlan.fieldPlansByAlias - ); - } /** @@ -1179,7 +1190,7 @@ function findTypeWithIsTypeOf( for (const typeName in plansByType) { const candidatePlan = plansByType[typeName]; const type = candidatePlan.returnType; - if (type.isTypeOf && type.isTypeOf(result, candidatePlan)) { + if (candidatePlan.isTypeOfPessimisticFn(result, candidatePlan)) { return type; } } diff --git a/src/type/definition.js b/src/type/definition.js index 63c93c5518..0ab267ecf1 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -556,6 +556,8 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; + isTypeOfOptimisticFn: GraphQLIsTypeOfFn; + isTypeOfPessimisticFn: GraphQLIsTypeOfFn; fields: {[fieldName: string]: [ GraphQLResolvingPlan ]}; fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; } From 79973f6cd18e487dd6266c08087d9a797133fd6e Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 5 Mar 2016 08:29:38 -0800 Subject: [PATCH 90/94] extract evaluateCoercionPlan function --- src/execution/execute.js | 84 +++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index e748b30e2b..412c28c2fb 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -945,7 +945,7 @@ function evaluateSerializationPlan( } /** - * Evaluate a slection plan + * Evaluate a selection plan */ function evaluateSelectionPlan( exeContext: ExecutionContext, @@ -971,6 +971,49 @@ function evaluateSelectionPlan( ); } +/** + * Evaluate a coercion plan + */ +function evaluateCoercionPlan( + exeContext: ExecutionContext, + result: mixed, + plan: GraphQLCoercionPlan +): mixed { + invariant(plan.kind === 'coerce'); + + // Field type must be Object, Interface or Union and expect + // sub-selections. + let runtimeType: ?GraphQLObjectType; + + const abstractType = ((plan.returnType: any): GraphQLAbstractType); + + if (abstractType.resolveType) { + runtimeType = findTypeWithResolveType(abstractType, plan, result); + } else { + runtimeType = findTypeWithIsTypeOf(plan, result); + } + + invariant( + runtimeType, + 'Could not resolve type,' + + 'probably an error in a resolveType or isTypeOf function in the schema' + ); + + invariant(plan.typeChoices !== null); + + const typeChoices = plan.typeChoices; + const selectionPlan = typeChoices[runtimeType.name]; + if (!selectionPlan) { + throw new GraphQLError( + `Runtime Object type "${runtimeType}" ` + + `is not a possible type for "${abstractType}".`, + plan.fieldASTs + ); + } + + return evaluateSelectionPlan(exeContext, result, selectionPlan); +} + /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -1044,11 +1087,6 @@ function completeValue( return null; } - // Field type must be Object, Interface or Union and expect - // sub-selections. - let runtimeType: ?GraphQLObjectType; - let selectionPlan: ?GraphQLSelectionPlan; - // Execution Completion Plan switch (plan.kind) { @@ -1101,39 +1139,7 @@ function completeValue( // --- CASE H: isAbstractType (run GraphQLCoercionPlan) case 'coerce': - // Tested in planCompleteValue - invariant( - isAbstractType(returnType), - 'Type detected at runtime does not match type expected in planning' - ); - - const abstractType = ((returnType: any): GraphQLAbstractType); - - if (abstractType.resolveType) { - runtimeType = findTypeWithResolveType(abstractType, plan, result); - } else { - runtimeType = findTypeWithIsTypeOf(plan, result); - } - - invariant( - runtimeType, - 'Could not resolve type,' + - 'probably an error in a resolveType or isTypeOf function in the schema' - ); - - invariant(plan.typeChoices !== null); - - const typeChoices = plan.typeChoices; - selectionPlan = typeChoices[runtimeType.name]; - if (!selectionPlan) { - throw new GraphQLError( - `Runtime Object type "${runtimeType}" ` + - `is not a possible type for "${abstractType}".`, - plan.fieldASTs - ); - } - - return evaluateSelectionPlan(exeContext, result, selectionPlan); + return evaluateCoercionPlan(exeContext, result, plan); // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable From eb225cfa8d01ce1a43ed0737bb69a8ec60e58416 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 5 Mar 2016 08:41:34 -0800 Subject: [PATCH 91/94] Extract evaluateMappingPlan function --- src/execution/execute.js | 78 +++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 412c28c2fb..d11d207979 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -1014,6 +1014,47 @@ function evaluateCoercionPlan( return evaluateSelectionPlan(exeContext, result, selectionPlan); } +/** + * Evaluate a coercion plan + */ +function evaluateMappingPlan( + exeContext: ExecutionContext, + result: mixed, + plan: GraphQLMappingPlan +): mixed { + + invariant( + Array.isArray(result), + 'User Error: expected iterable, but did not find one ' + + `for field ${plan.parentType}.${plan.fieldName}.` + ); + invariant(plan.returnType.ofType); + + const elementPlan = plan.listElement; + + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another + // Promise. + const itemType = plan.returnType.ofType; + let containsPromise = false; + const completedResults = result.map(item => { + const completedItem = + completeValueCatchingError( + exeContext, + elementPlan, + itemType, + item + ); + if (!containsPromise && isThenable(completedItem)) { + containsPromise = true; + } + return completedItem; + }); + + return containsPromise ? + Promise.all(completedResults) : completedResults; +} + /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -1091,43 +1132,8 @@ function completeValue( switch (plan.kind) { // --- CASE E: GraphQLList (run GraphQLMappingPlan) - // If result is null-like, return null. case 'map': - // Tested in planCompleteValue - invariant( - returnType instanceof GraphQLList, - 'Type detected at runtime does not match type expected in planning' - ); - - invariant( - Array.isArray(result), - 'User Error: expected iterable, but did not find one ' + - `for field ${plan.parentType}.${plan.fieldName}.` - ); - - const elementPlan = plan.listElement; - - // This is specified as a simple map, however we're optimizing the path - // where the list contains no Promises by avoiding creating another - // Promise. - const itemType = returnType.ofType; - let containsPromise = false; - const completedResults = result.map(item => { - const completedItem = - completeValueCatchingError( - exeContext, - elementPlan, - itemType, - item - ); - if (!containsPromise && isThenable(completedItem)) { - containsPromise = true; - } - return completedItem; - }); - - return containsPromise ? - Promise.all(completedResults) : completedResults; + return evaluateMappingPlan(exeContext, result, plan); // --- CASE F: Serialize (run GraphQLSerializationCompletionPlan) case 'serialize': From f4ada13232a97b250db24a5ac8abc9230b7de02c Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 5 Mar 2016 17:48:24 -0800 Subject: [PATCH 92/94] Break up planCompleteValue, fix ordering of completion operations to be consistent, grouping functions in same location --- src/execution/execute.js | 628 ++++++++++++++++++++------------------- 1 file changed, 328 insertions(+), 300 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index d11d207979..5942950ca7 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -22,6 +22,8 @@ import { GraphQLList, GraphQLNonNull, GraphQLCompositeType, + GraphQLInterfaceType, + GraphQLUnionType, isAbstractType } from '../type/definition'; import type { @@ -33,6 +35,7 @@ import type { GraphQLCompletionPlan, GraphQLSerializationPlan, GraphQLMappingPlan, + GraphQLLeafType, GraphQLCoercionPlan } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; @@ -80,20 +83,7 @@ import type { * Queries are execcuted in two phases: planning and evaluation. * The goal of planning is to precompute data needed for evaluation * and to give resolvers insight into how the query will be - * executed - * - * The planning and evaluation phases are coupled in this way: - * +-------------------+---------------------------+------------------+ - * | Planning | Plan | Evaluation | - * +-------------------+---------------------------+------------------+ - * | planOperation | GraphQLOperationPlan | executeOperation | - * | planFields | GraphQLResolvingPlan | resolveField | - * | planCompleteValue | GraphQLCompletionPlan | completeValue | - * | planCompleteValue | GraphQLCoercionPlan | completeValue | - * | planCompleteValue | GraphQLMappingPlan | completeValue | - * | planCompleteValue | GraphQLSerializationPlan | completeValue | - * | planSelection | GraphQLSelectionPlan | completeValue | - * +-------------------+---------------------------+------------------+ + * executed. */ /** @@ -279,64 +269,6 @@ function planOperation( return plan; } -/** - * Create a plan based on the "Evaluating operations" section of the spec. - */ -function planSelection( - exeContext: ExecutionContext, - type: GraphQLObjectType, - fieldASTs: Array, - fieldName: string, - parentType: GraphQLCompositeType -): GraphQLSelectionPlan { - - let fields = Object.create(null); - const visitedFragmentNames = Object.create(null); - for (let i = 0; i < fieldASTs.length; i++) { - const selectionSet = fieldASTs[i].selectionSet; - if (selectionSet) { - fields = collectFields( - exeContext, - type, - selectionSet, - fields, - visitedFragmentNames - ); - } - } - - let isTypeOfOptimisticFn; - let isTypeOfPessimisticFn; - if (type.isTypeOf) { - isTypeOfOptimisticFn = type.isTypeOf; - isTypeOfPessimisticFn = type.isTypeOf; - } else { - isTypeOfOptimisticFn = () => true; - isTypeOfPessimisticFn = () => false; - } - - const {fieldPlansByAlias, fieldPlans} = planFields(exeContext, type, fields); - - const plan: GraphQLSelectionPlan = { - kind: 'select', - fieldName, - fieldASTs, - returnType: type, - parentType, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - isTypeOfOptimisticFn, - isTypeOfPessimisticFn, - fields: fieldPlans, - fieldPlansByAlias - }; - - return plan; -} - type planFieldsResult = { fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; fieldPlans: {[fieldName: string]: [ GraphQLResolvingPlan ]}; @@ -428,81 +360,213 @@ function planResolveField( } /** - * Plans the evaluation of completeValue as defined in the - * "Field entries" section of the spec. + * Plans the evaluation of a GraphQLScalarType or GraphQLEnumType + * value during value completion. */ -function planCompleteValue( +function planSerialization( exeContext: ExecutionContext, - returnType: GraphQLType, + returnType: GraphQLLeafType, fieldASTs: Array, fieldName: string, parentType: GraphQLCompositeType -): GraphQLCompletionPlan { +): GraphQLSerializationPlan { + invariant(returnType.serialize, 'Missing serialize method on type'); - // --- CASE A: Promise (Execution time only, see completeValue) + const plan: GraphQLSerializationPlan = { + kind: 'serialize', + fieldASTs, + returnType, + fieldName, + parentType + }; - // --- CASE B: Error (Execution time only, see completeValue) + return plan; +} - // --- CASE C: GraphQLNonNull (Structural, see completeValue) - // If field type is NonNull, complete for inner type - if (returnType instanceof GraphQLNonNull) { - return planCompleteValue( +/** + * Plans the evaluation of a GraphQLList value during value completion. + */ +function planMapping( + exeContext: ExecutionContext, + returnType: GraphQLList, + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType +): GraphQLMappingPlan { + const innerType = returnType.ofType; + + const elementPlan = + planCompleteValue( exeContext, - returnType.ofType, + innerType, fieldASTs, fieldName, parentType ); - } - // --- CASE D: Nullish (Execution time only, see completeValue) + const plan: GraphQLMappingPlan = { + kind: 'map', + fieldASTs, + returnType, + fieldName, + parentType, + listElement: elementPlan + }; - // --- CASE E: GraphQLList (See completeValue for plan Execution) - // If field type is List, complete each item in the list with the inner type - if (returnType instanceof GraphQLList) { - const innerType = returnType.ofType; + return plan; +} - const elementPlan = - planCompleteValue( +/** + * Plan the evaluation of a GraphQLObjectType value during value completion. + */ +function planSelection( + exeContext: ExecutionContext, + type: GraphQLObjectType, + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType +): GraphQLSelectionPlan { + + let fields = Object.create(null); + const visitedFragmentNames = Object.create(null); + for (let i = 0; i < fieldASTs.length; i++) { + const selectionSet = fieldASTs[i].selectionSet; + if (selectionSet) { + fields = collectFields( exeContext, - innerType, - fieldASTs, - fieldName, - parentType + type, + selectionSet, + fields, + visitedFragmentNames ); + } + } + + let isTypeOfOptimisticFn; + let isTypeOfPessimisticFn; + if (type.isTypeOf) { + isTypeOfOptimisticFn = type.isTypeOf; + isTypeOfPessimisticFn = type.isTypeOf; + } else { + isTypeOfOptimisticFn = () => true; + isTypeOfPessimisticFn = () => false; + } + + const {fieldPlansByAlias, fieldPlans} = planFields(exeContext, type, fields); - const plan: GraphQLMappingPlan = { - kind: 'map', + const plan: GraphQLSelectionPlan = { + kind: 'select', + fieldName, + fieldASTs, + returnType: type, + parentType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, + isTypeOfOptimisticFn, + isTypeOfPessimisticFn, + fields: fieldPlans, + fieldPlansByAlias + }; + + return plan; +} + +/** + * Plans the evaluation of an abstract type + * value during value completion. + */ +function planCoercion( + exeContext: ExecutionContext, + returnType: GraphQLAbstractType, + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType +): GraphQLCoercionPlan { + const abstractType = ((returnType: any): GraphQLAbstractType); + const possibleTypes = abstractType.getPossibleTypes(); + const typeChoices = Object.create(null); + possibleTypes.forEach(possibleType => { + invariant( + !typeChoices[possibleType.name], + 'Two types cannot have the same name "${possibleType.name}"' + + 'as possible types of abstract type ${abstractType.name}' + ); + typeChoices[possibleType.name] = planSelection( + exeContext, + possibleType, fieldASTs, - returnType, fieldName, - parentType, - listElement: elementPlan - }; + parentType + ); + }); - return plan; + const plan: GraphQLCoercionPlan = { + kind: 'coerce', + fieldName, + fieldASTs, + returnType: abstractType, + parentType, + schema: exeContext.schema, + fragments: exeContext.fragments, + rootValue: exeContext.rootValue, + operation: exeContext.operation, + variableValues: exeContext.variableValues, + typeChoices + }; + + return plan; +} + +/** + * Plans the evaluation of completeValue as defined in the + * "Field entries" section of the spec. + */ +function planCompleteValue( + exeContext: ExecutionContext, + returnType: GraphQLType, + fieldASTs: Array, + fieldName: string, + parentType: GraphQLCompositeType +): GraphQLCompletionPlan { + + // If field type is NonNull, complete for inner type + if (returnType instanceof GraphQLNonNull) { + return planCompleteValue( + exeContext, + returnType.ofType, + fieldASTs, + fieldName, + parentType + ); } - // --- CASE F: Serialize (See completeValue for plan Execution) // If field type is Scalar or Enum, serialize to a valid value, returning // null if serialization is not possible. if (returnType instanceof GraphQLScalarType || returnType instanceof GraphQLEnumType) { - invariant(returnType.serialize, 'Missing serialize method on type'); - - const plan: GraphQLSerializationPlan = { - kind: 'serialize', - fieldASTs, + return planSerialization( + exeContext, returnType, + fieldASTs, fieldName, parentType - }; - - return plan; + ); } + // If field type is List, complete each item in the list with the inner type + if (returnType instanceof GraphQLList) { + return planMapping( + exeContext, + returnType, + fieldASTs, + fieldName, + parentType + ); + } - // --- CASE G: GraphQLObjectType (See completeValue for plan Execution) if (returnType instanceof GraphQLObjectType) { return planSelection( exeContext, @@ -513,44 +577,19 @@ function planCompleteValue( ); } - // --- CASE H: isAbstractType (run GraphQLCoercionPlan) if (isAbstractType(returnType)) { - const abstractType = ((returnType: any): GraphQLAbstractType); - const possibleTypes = abstractType.getPossibleTypes(); - const typeChoices = Object.create(null); - possibleTypes.forEach(possibleType => { - invariant( - !typeChoices[possibleType.name], - 'Two types cannot have the same name "${possibleType.name}"' + - 'as possible types of abstract type ${abstractType.name}' - ); - typeChoices[possibleType.name] = planSelection( - exeContext, - possibleType, - fieldASTs, - fieldName, - parentType - ); - }); - - const plan: GraphQLCoercionPlan = { - kind: 'coerce', - fieldName, + invariant( + returnType instanceof GraphQLInterfaceType || + returnType instanceof GraphQLUnionType); + return planCoercion( + exeContext, + returnType, fieldASTs, - returnType: abstractType, - parentType, - schema: exeContext.schema, - fragments: exeContext.fragments, - rootValue: exeContext.rootValue, - operation: exeContext.operation, - variableValues: exeContext.variableValues, - typeChoices - }; - - return plan; + fieldName, + parentType + ); } - // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable invariant(false, `Cannot form plan for ${parentType}.${fieldName}`); } @@ -929,132 +968,6 @@ function completeValueCatchingError( } } -/** - * Evaluate a serialization plan - */ -function evaluateSerializationPlan( - result: mixed, - plan: GraphQLSerializationPlan -): mixed { - invariant(plan.kind === 'serialize'); - invariant(plan.returnType.serialize, 'Missing serialize method on type'); - - // If result is null-like, return null. - const serializedResult = plan.returnType.serialize(result); - return isNullish(serializedResult) ? null : serializedResult; -} - -/** - * Evaluate a selection plan - */ -function evaluateSelectionPlan( - exeContext: ExecutionContext, - result: mixed, - plan: GraphQLSelectionPlan -): mixed { - invariant(plan.kind === 'select'); - - // 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 (!plan.isTypeOfOptimisticFn(result, plan)) { - throw new GraphQLError( - `Expected value of type "${plan.returnType}" but got: ${result}.`, - plan.fieldASTs - ); - } - - return executeFields( - exeContext, - result, - plan.fieldPlansByAlias - ); -} - -/** - * Evaluate a coercion plan - */ -function evaluateCoercionPlan( - exeContext: ExecutionContext, - result: mixed, - plan: GraphQLCoercionPlan -): mixed { - invariant(plan.kind === 'coerce'); - - // Field type must be Object, Interface or Union and expect - // sub-selections. - let runtimeType: ?GraphQLObjectType; - - const abstractType = ((plan.returnType: any): GraphQLAbstractType); - - if (abstractType.resolveType) { - runtimeType = findTypeWithResolveType(abstractType, plan, result); - } else { - runtimeType = findTypeWithIsTypeOf(plan, result); - } - - invariant( - runtimeType, - 'Could not resolve type,' + - 'probably an error in a resolveType or isTypeOf function in the schema' - ); - - invariant(plan.typeChoices !== null); - - const typeChoices = plan.typeChoices; - const selectionPlan = typeChoices[runtimeType.name]; - if (!selectionPlan) { - throw new GraphQLError( - `Runtime Object type "${runtimeType}" ` + - `is not a possible type for "${abstractType}".`, - plan.fieldASTs - ); - } - - return evaluateSelectionPlan(exeContext, result, selectionPlan); -} - -/** - * Evaluate a coercion plan - */ -function evaluateMappingPlan( - exeContext: ExecutionContext, - result: mixed, - plan: GraphQLMappingPlan -): mixed { - - invariant( - Array.isArray(result), - 'User Error: expected iterable, but did not find one ' + - `for field ${plan.parentType}.${plan.fieldName}.` - ); - invariant(plan.returnType.ofType); - - const elementPlan = plan.listElement; - - // This is specified as a simple map, however we're optimizing the path - // where the list contains no Promises by avoiding creating another - // Promise. - const itemType = plan.returnType.ofType; - let containsPromise = false; - const completedResults = result.map(item => { - const completedItem = - completeValueCatchingError( - exeContext, - elementPlan, - itemType, - item - ); - if (!containsPromise && isThenable(completedItem)) { - containsPromise = true; - } - return completedItem; - }); - - return containsPromise ? - Promise.all(completedResults) : completedResults; -} - /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. @@ -1080,7 +993,6 @@ function completeValue( result: mixed ): mixed { - // --- CASE A: Promise (Execution time only, no plan) // If result is a Promise, apply-lift over completeValue. if (isThenable(result)) { return ((result: any): Promise).then( @@ -1096,13 +1008,11 @@ function completeValue( ); } - // --- CASE B: Error (Execution time only, no plan) // If result is an Error, throw a located error. if (result instanceof Error) { throw locatedError(result, plan.fieldASTs); } - // --- CASE C: GraphQLNonNull (No plan, structural. See planCompleteValue) // If field type is NonNull, complete for inner type, and throw field error // if result is null. if (returnType instanceof GraphQLNonNull) { @@ -1122,32 +1032,24 @@ function completeValue( return completed; } - // --- CASE D: Nullish (Execution time only, no plan) // If result is null-like, return null. if (isNullish(result)) { return null; } - // Execution Completion Plan switch (plan.kind) { + case 'serialize': + return evaluateSerializationPlan(result, plan); - // --- CASE E: GraphQLList (run GraphQLMappingPlan) case 'map': return evaluateMappingPlan(exeContext, result, plan); - // --- CASE F: Serialize (run GraphQLSerializationCompletionPlan) - case 'serialize': - return evaluateSerializationPlan(result, plan); - - // --- CASE G: GraphQLObjectType (run GraphQLSelectionPlan) case 'select': return evaluateSelectionPlan(exeContext, result, plan); - // --- CASE H: isAbstractType (run GraphQLCoercionPlan) case 'coerce': return evaluateCoercionPlan(exeContext, result, plan); - // --- CASE Z: Unreachable // We have handled all possibilities. Not reachable default: invariant(false, 'No plan covers runtime conditions'); @@ -1156,23 +1058,129 @@ function completeValue( } /** - * If a resolve function is not given, then a default resolve behavior is used - * which takes the property of the source object of the same name as the field - * and returns it as the result, or if it's a function, returns the result - * of calling that function. + * Evaluate a serialization plan */ -function defaultResolveFn( - source:mixed, - args:{ [key: string]: mixed }, - info: GraphQLResolvingPlan -) { - const fieldName = info.fieldName; +function evaluateSerializationPlan( + result: mixed, + plan: GraphQLSerializationPlan +): mixed { + invariant(plan.kind === 'serialize'); + invariant(plan.returnType.serialize, 'Missing serialize method on type'); - // ensure source is a value for which property access is acceptable. - if (typeof source !== 'number' && typeof source !== 'string' && source) { - const property = (source: any)[fieldName]; - return typeof property === 'function' ? property.call(source) : property; + // If result is null-like, return null. + const serializedResult = plan.returnType.serialize(result); + return isNullish(serializedResult) ? null : serializedResult; +} + +/** + * Evaluate a mapping plan + */ +function evaluateMappingPlan( + exeContext: ExecutionContext, + result: mixed, + plan: GraphQLMappingPlan +): mixed { + + invariant( + Array.isArray(result), + 'User Error: expected iterable, but did not find one ' + + `for field ${plan.parentType}.${plan.fieldName}.` + ); + invariant(plan.returnType.ofType); + + const elementPlan = plan.listElement; + + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another + // Promise. + const itemType = plan.returnType.ofType; + let containsPromise = false; + const completedResults = result.map(item => { + const completedItem = + completeValueCatchingError( + exeContext, + elementPlan, + itemType, + item + ); + if (!containsPromise && isThenable(completedItem)) { + containsPromise = true; + } + return completedItem; + }); + + return containsPromise ? + Promise.all(completedResults) : completedResults; +} + +/** + * Evaluate a selection plan + */ +function evaluateSelectionPlan( + exeContext: ExecutionContext, + result: mixed, + plan: GraphQLSelectionPlan +): mixed { + invariant(plan.kind === 'select'); + + // 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 (!plan.isTypeOfOptimisticFn(result, plan)) { + throw new GraphQLError( + `Expected value of type "${plan.returnType}" but got: ${result}.`, + plan.fieldASTs + ); } + + return executeFields( + exeContext, + result, + plan.fieldPlansByAlias + ); +} + +/** + * Evaluate a coercion plan + */ +function evaluateCoercionPlan( + exeContext: ExecutionContext, + result: mixed, + plan: GraphQLCoercionPlan +): mixed { + invariant(plan.kind === 'coerce'); + + // Field type must be Object, Interface or Union and expect + // sub-selections. + let runtimeType: ?GraphQLObjectType; + + const abstractType = ((plan.returnType: any): GraphQLAbstractType); + + if (abstractType.resolveType) { + runtimeType = findTypeWithResolveType(abstractType, plan, result); + } else { + runtimeType = findTypeWithIsTypeOf(plan, result); + } + + invariant( + runtimeType, + 'Could not resolve type,' + + 'probably an error in a resolveType or isTypeOf function in the schema' + ); + + invariant(plan.typeChoices !== null); + + const typeChoices = plan.typeChoices; + const selectionPlan = typeChoices[runtimeType.name]; + if (!selectionPlan) { + throw new GraphQLError( + `Runtime Object type "${runtimeType}" ` + + `is not a possible type for "${abstractType}".`, + plan.fieldASTs + ); + } + + return evaluateSelectionPlan(exeContext, result, selectionPlan); } /** @@ -1211,6 +1219,26 @@ function findTypeWithIsTypeOf( return null; } +/** + * If a resolve function is not given, then a default resolve behavior is used + * which takes the property of the source object of the same name as the field + * and returns it as the result, or if it's a function, returns the result + * of calling that function. + */ +function defaultResolveFn( + source:mixed, + args:{ [key: string]: mixed }, + info: GraphQLResolvingPlan +) { + const fieldName = info.fieldName; + + // ensure source is a value for which property access is acceptable. + if (typeof source !== 'number' && typeof source !== 'string' && source) { + const property = (source: any)[fieldName]; + return typeof property === 'function' ? property.call(source) : property; + } +} + /** * Checks to see if this object acts like a Promise, i.e. has a "then" * function. From de7116aa7ced1bf849a4493c114b21c4c2718bd3 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 5 Mar 2016 17:50:04 -0800 Subject: [PATCH 93/94] Remove unnecessary invariant tests --- src/execution/execute.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index 5942950ca7..eefab7b458 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -1064,7 +1064,6 @@ function evaluateSerializationPlan( result: mixed, plan: GraphQLSerializationPlan ): mixed { - invariant(plan.kind === 'serialize'); invariant(plan.returnType.serialize, 'Missing serialize method on type'); // If result is null-like, return null. @@ -1121,8 +1120,6 @@ function evaluateSelectionPlan( result: mixed, plan: GraphQLSelectionPlan ): mixed { - invariant(plan.kind === 'select'); - // 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. @@ -1148,8 +1145,6 @@ function evaluateCoercionPlan( result: mixed, plan: GraphQLCoercionPlan ): mixed { - invariant(plan.kind === 'coerce'); - // Field type must be Object, Interface or Union and expect // sub-selections. let runtimeType: ?GraphQLObjectType; From ea5bfae8d79edf94e19bf6502b54602a31a39ec9 Mon Sep 17 00:00:00 2001 From: "jeff@procata.com" Date: Sat, 5 Mar 2016 17:58:02 -0800 Subject: [PATCH 94/94] remove isTypeOfX functions; failed experiment --- src/execution/execute.js | 18 ++++-------------- src/type/definition.js | 2 -- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/execution/execute.js b/src/execution/execute.js index eefab7b458..78b58d60f5 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -442,16 +442,6 @@ function planSelection( } } - let isTypeOfOptimisticFn; - let isTypeOfPessimisticFn; - if (type.isTypeOf) { - isTypeOfOptimisticFn = type.isTypeOf; - isTypeOfPessimisticFn = type.isTypeOf; - } else { - isTypeOfOptimisticFn = () => true; - isTypeOfPessimisticFn = () => false; - } - const {fieldPlansByAlias, fieldPlans} = planFields(exeContext, type, fields); const plan: GraphQLSelectionPlan = { @@ -465,8 +455,6 @@ function planSelection( rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, - isTypeOfOptimisticFn, - isTypeOfPessimisticFn, fields: fieldPlans, fieldPlansByAlias }; @@ -1120,10 +1108,12 @@ function evaluateSelectionPlan( result: mixed, plan: GraphQLSelectionPlan ): mixed { + const returnType = plan.returnType; + // 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 (!plan.isTypeOfOptimisticFn(result, plan)) { + if (returnType.isTypeOf && !returnType.isTypeOf(result, plan)) { throw new GraphQLError( `Expected value of type "${plan.returnType}" but got: ${result}.`, plan.fieldASTs @@ -1205,7 +1195,7 @@ function findTypeWithIsTypeOf( for (const typeName in plansByType) { const candidatePlan = plansByType[typeName]; const type = candidatePlan.returnType; - if (candidatePlan.isTypeOfPessimisticFn(result, candidatePlan)) { + if (type.isTypeOf && type.isTypeOf(result, candidatePlan)) { return type; } } diff --git a/src/type/definition.js b/src/type/definition.js index 0ab267ecf1..63c93c5518 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -556,8 +556,6 @@ export type GraphQLSelectionPlan = { rootValue: mixed; operation: OperationDefinition; variableValues: { [variableName: string]: mixed }; - isTypeOfOptimisticFn: GraphQLIsTypeOfFn; - isTypeOfPessimisticFn: GraphQLIsTypeOfFn; fields: {[fieldName: string]: [ GraphQLResolvingPlan ]}; fieldPlansByAlias: {[alias: string]: GraphQLResolvingPlan}; }