UNPKG

@dillonkearns/elm-graphql

Version:

<img src="https://cdn.jsdelivr.net/gh/martimatix/logo-graphqelm/logo.svg" alt="dillonearns/elm-graphql logo" width="40%" align="right">

1,293 lines (1,211 loc) 35.8 kB
/** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import { forEach, isCollection } from 'iterall'; import { GraphQLError, locatedError } from '../error'; import invariant from '../jsutils/invariant'; import isNullish from '../jsutils/isNullish'; import type {ObjMap} from '../jsutils/ObjMap'; import { typeFromAST } from '../utilities/typeFromAST'; import * as Kind from '../language/kinds'; import { getVariableValues, getArgumentValues, getDirectiveValues, } from './values'; import { GraphQLObjectType, GraphQLList, GraphQLNonNull, isAbstractType, isLeafType, } from '../type/definition'; import type { GraphQLType, GraphQLLeafType, GraphQLAbstractType, GraphQLField, GraphQLFieldResolver, GraphQLResolveInfo, ResponsePath, } from '../type/definition'; import { GraphQLSchema } from '../type/schema'; import { SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, } from '../type/introspection'; import { GraphQLIncludeDirective, GraphQLSkipDirective, } from '../type/directives'; import type { DocumentNode, OperationDefinitionNode, SelectionSetNode, FieldNode, FragmentSpreadNode, InlineFragmentNode, FragmentDefinitionNode, } from '../language/ast'; /** * Terminology * * "Definitions" are the generic name for top-level statements in the document. * Examples of this include: * 1) Operations (such as a query) * 2) Fragments * * "Operations" are a generic name for requests in the document. * Examples of this include: * 1) query, * 2) mutation * * "Selections" are the definitions that can appear legally and at * single level of the query. These include: * 1) field references e.g "a" * 2) fragment "spreads" e.g. "...c" * 3) inline fragment "spreads" e.g. "...on Type { a }" */ /** * Data that must be available at all points during query execution. * * Namely, schema of the type system that is currently executing, * and the fragments defined in the query document */ export type ExecutionContext = { schema: GraphQLSchema; fragments: ObjMap<FragmentDefinitionNode>; rootValue: mixed; contextValue: mixed; operation: OperationDefinitionNode; variableValues: {[variable: string]: mixed}, fieldResolver: GraphQLFieldResolver<any, any>; errors: Array<GraphQLError>; }; /** * The result of GraphQL execution. * * - `errors` is included when any errors occurred as a non-empty array. * - `data` is the result of a successful execution of the query. */ export type ExecutionResult = { errors?: Array<GraphQLError>; data?: ObjMap<mixed>; }; export type ExecutionArgs = {| schema: GraphQLSchema, document: DocumentNode, rootValue?: mixed, contextValue?: mixed, variableValues?: ?{[variable: string]: mixed}, operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver<any, any> |}; /** * Implements the "Evaluating requests" section of the GraphQL specification. * * Returns a Promise that will eventually be resolved and never rejected. * * If the arguments to this function do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. * * Accepts either an object with named arguments, or individual arguments. */ declare function execute( ExecutionArgs, ..._: [] ): Promise<ExecutionResult>; /* eslint-disable no-redeclare */ declare function execute( schema: GraphQLSchema, document: DocumentNode, rootValue?: mixed, contextValue?: mixed, variableValues?: ?{[variable: string]: mixed}, operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver<any, any> ): Promise<ExecutionResult>; export function execute( argsOrSchema, document, rootValue, contextValue, variableValues, operationName, fieldResolver ) { // Extract arguments from object args if provided. return arguments.length === 1 ? executeImpl( argsOrSchema.schema, argsOrSchema.document, argsOrSchema.rootValue, argsOrSchema.contextValue, argsOrSchema.variableValues, argsOrSchema.operationName, argsOrSchema.fieldResolver ) : executeImpl( argsOrSchema, document, rootValue, contextValue, variableValues, operationName, fieldResolver ); } function executeImpl( schema, document, rootValue, contextValue, variableValues, operationName, fieldResolver ) { // If arguments are missing or incorrect, throw an error. assertValidExecutionArguments( schema, document, variableValues ); // If a valid context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. let context; try { context = buildExecutionContext( schema, document, rootValue, contextValue, variableValues, operationName, fieldResolver ); } catch (error) { return Promise.resolve({ errors: [ error ] }); } // Return a Promise that will eventually resolve to the data described by // The "Response" section of the GraphQL specification. // // If errors are encountered while executing a GraphQL field, only that // field and its descendants will be omitted, and sibling fields will still // be executed. An execution which encounters errors will still result in a // resolved Promise. return Promise.resolve( executeOperation(context, context.operation, rootValue) ).then(data => context.errors.length === 0 ? { data } : { errors: context.errors, data } ); } /** * Given a ResponsePath (found in the `path` entry in the information provided * as the last argument to a field resolver), return an Array of the path keys. */ export function responsePathAsArray( path: ResponsePath ): Array<string | number> { const flattened = []; let curr = path; while (curr) { flattened.push(curr.key); curr = curr.prev; } return flattened.reverse(); } /** * Given a ResponsePath and a key, return a new ResponsePath containing the * new key. */ export function addPath(prev: ResponsePath, key: string | number) { return { prev, key }; } /** * Essential assertions before executing to provide developer feedback for * improper use of the GraphQL library. */ export function assertValidExecutionArguments( schema: GraphQLSchema, document: DocumentNode, rawVariableValues: ?ObjMap<mixed> ): void { invariant(schema, 'Must provide schema'); invariant(document, 'Must provide document'); invariant( schema instanceof GraphQLSchema, 'Schema must be an instance of GraphQLSchema. Also ensure that there are ' + 'not multiple versions of GraphQL installed in your node_modules directory.' ); // Variables, if provided, must be an object. invariant( !rawVariableValues || typeof rawVariableValues === 'object', 'Variables must be provided as an Object where each property is a ' + 'variable value. Perhaps look to see if an unparsed JSON string ' + 'was provided.' ); } /** * Constructs a ExecutionContext object from the arguments passed to * execute, which we will pass throughout the other execution methods. * * Throws a GraphQLError if a valid execution context cannot be created. */ export function buildExecutionContext( schema: GraphQLSchema, document: DocumentNode, rootValue: mixed, contextValue: mixed, rawVariableValues: ?ObjMap<mixed>, operationName: ?string, fieldResolver: ?GraphQLFieldResolver<any, any> ): ExecutionContext { const errors: Array<GraphQLError> = []; let operation: ?OperationDefinitionNode; const fragments: ObjMap<FragmentDefinitionNode> = Object.create(null); document.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 || {} ); return { schema, fragments, rootValue, contextValue, operation, variableValues, fieldResolver: fieldResolver || defaultFieldResolver, errors, }; } /** * Implements the "Evaluating operations" section of the spec. */ function executeOperation( exeContext: ExecutionContext, operation: OperationDefinitionNode, rootValue: mixed ): ?(Promise<?ObjMap<mixed>> | ObjMap<mixed>) { const type = getOperationRootType(exeContext.schema, operation); const fields = collectFields( exeContext, type, operation.selectionSet, Object.create(null), Object.create(null) ); const path = undefined; // Errors from sub-fields of a NonNull type may propagate to the top level, // at which point we still log the error and null the parent field, which // in this case is the entire response. // // Similar to completeValueCatchingError. try { const result = operation.operation === 'mutation' ? executeFieldsSerially(exeContext, type, rootValue, path, fields) : executeFields(exeContext, type, rootValue, path, fields); const promise = getPromise(result); if (promise) { return promise.then(undefined, error => { exeContext.errors.push(error); return Promise.resolve(null); }); } return result; } catch (error) { exeContext.errors.push(error); return null; } } /** * Extracts the root type of the operation from the schema. */ export function getOperationRootType( schema: GraphQLSchema, operation: OperationDefinitionNode ): 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. */ function executeFieldsSerially( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, path: ResponsePath, fields: ObjMap<Array<FieldNode>>, ): Promise<ObjMap<mixed>> { return Object.keys(fields).reduce( (prevPromise, responseName) => prevPromise.then(results => { const fieldNodes = fields[responseName]; const fieldPath = addPath(path, responseName); const result = resolveField( exeContext, parentType, sourceValue, fieldNodes, fieldPath ); if (result === undefined) { return results; } const promise = getPromise(result); if (promise) { return promise.then(resolvedResult => { results[responseName] = resolvedResult; return results; }); } results[responseName] = result; return results; }), Promise.resolve({}) ); } /** * Implements the "Evaluating selection sets" section of the spec * for "read" mode. */ function executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: mixed, path: ResponsePath, fields: ObjMap<Array<FieldNode>>, ): Promise<ObjMap<mixed>> | ObjMap<mixed> { let containsPromise = false; const finalResults = Object.keys(fields).reduce( (results, responseName) => { const fieldNodes = fields[responseName]; const fieldPath = addPath(path, responseName); const result = resolveField( exeContext, parentType, sourceValue, fieldNodes, fieldPath ); if (result === undefined) { return results; } results[responseName] = result; if (getPromise(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); } /** * 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 an 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: SelectionSetNode, fields: ObjMap<Array<FieldNode>>, visitedFragmentNames: ObjMap<boolean>, ): ObjMap<Array<FieldNode>> { for (let i = 0; i < selectionSet.selections.length; i++) { const selection = selectionSet.selections[i]; switch (selection.kind) { case Kind.FIELD: if (!shouldIncludeNode(exeContext, selection)) { continue; } const name = getFieldEntryKey(selection); if (!fields[name]) { fields[name] = []; } fields[name].push(selection); break; case Kind.INLINE_FRAGMENT: if (!shouldIncludeNode(exeContext, selection) || !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)) { continue; } visitedFragmentNames[fragName] = true; const fragment = exeContext.fragments[fragName]; if (!fragment || !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, node: FragmentSpreadNode | FieldNode | InlineFragmentNode, ): boolean { const skip = getDirectiveValues( GraphQLSkipDirective, node, exeContext.variableValues ); if (skip && skip.if === true) { return false; } const include = getDirectiveValues( GraphQLIncludeDirective, node, exeContext.variableValues ); if (include && include.if === false) { return false; } return true; } /** * Determines if a fragment is applicable to the given type. */ function doesFragmentConditionMatch( exeContext: ExecutionContext, fragment: FragmentDefinitionNode | InlineFragmentNode, type: GraphQLObjectType ): boolean { const typeConditionNode = fragment.typeCondition; if (!typeConditionNode) { return true; } const conditionalType = typeFromAST(exeContext.schema, typeConditionNode); if (conditionalType === type) { return true; } if (isAbstractType(conditionalType)) { return exeContext.schema.isPossibleType(conditionalType, type); } return false; } /** * This function transforms a JS object `ObjMap<Promise<T>>` into * a `Promise<ObjMap<T>>` * * This is akin to bluebird's `Promise.props`, but implemented only using * `Promise.all` so it will work with any implementation of ES6 promises. */ function promiseForObject<T>(object: ObjMap<Promise<T>>): Promise<ObjMap<T>> { const keys = Object.keys(object); const valuesAndPromises = keys.map(name => object[name]); return Promise.all(valuesAndPromises).then( values => values.reduce((resolvedObject, value, i) => { resolvedObject[keys[i]] = value; return resolvedObject; }, Object.create(null)) ); } /** * Implements the logic to compute the key of a given field's entry */ function getFieldEntryKey(node: FieldNode): 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, * then calls completeValue to complete promises, serialize scalars, or execute * the sub-selection-set for objects. */ function resolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, source: mixed, fieldNodes: Array<FieldNode>, path: ResponsePath ): mixed { const fieldNode = fieldNodes[0]; const fieldName = fieldNode.name.value; const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName); if (!fieldDef) { return; } const resolveFn = fieldDef.resolve || exeContext.fieldResolver; const info = buildResolveInfo( exeContext, fieldDef, fieldNodes, parentType, path ); // Get the resolve function, regardless of if its result is normal // or abrupt (error). const result = resolveFieldValueOrError( exeContext, fieldDef, fieldNodes, resolveFn, source, info ); return completeValueCatchingError( exeContext, fieldDef.type, fieldNodes, info, path, result ); } export function buildResolveInfo( exeContext: ExecutionContext, fieldDef: GraphQLField<*, *>, fieldNodes: Array<FieldNode>, parentType: GraphQLObjectType, path: ResponsePath ): GraphQLResolveInfo { // The resolve function's optional fourth argument is a collection of // information about the current execution state. return { fieldName: fieldNodes[0].name.value, fieldNodes, returnType: fieldDef.type, parentType, path, schema: exeContext.schema, fragments: exeContext.fragments, rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, }; } // Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` // function. Returns the result of resolveFn or the abrupt-return Error object. export function resolveFieldValueOrError<TSource>( exeContext: ExecutionContext, fieldDef: GraphQLField<TSource, *>, fieldNodes: Array<FieldNode>, resolveFn: GraphQLFieldResolver<TSource, *>, source: TSource, info: GraphQLResolveInfo ): Error | mixed { try { // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. // TODO: find a way to memoize, in case this field is within a List type. const args = getArgumentValues( fieldDef, fieldNodes[0], exeContext.variableValues ); // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. const context = exeContext.contextValue; return resolveFn(source, args, context, info); } catch (error) { // Sometimes a non-error is thrown, wrap it as an Error for a // consistent interface. return error instanceof Error ? error : new Error(error); } } // This is a small wrapper around completeValue which detects and logs errors // in the execution context. function completeValueCatchingError( exeContext: ExecutionContext, returnType: GraphQLType, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, path: ResponsePath, result: mixed ): mixed { // If the field type is non-nullable, then it is resolved without any // protection from errors, however it still properly locates the error. if (returnType instanceof GraphQLNonNull) { return completeValueWithLocatedError( exeContext, returnType, fieldNodes, info, path, result ); } // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. try { const completed = completeValueWithLocatedError( exeContext, returnType, fieldNodes, info, path, result ); const promise = getPromise(completed); if (promise) { // If `completeValueWithLocatedError` returned a rejected promise, log // the rejection error and resolve to null. // Note: we don't rely on a `catch` method, but we do expect "thenable" // to take a second callback for the error case. return promise.then(undefined, error => { exeContext.errors.push(error); return Promise.resolve(null); }); } return completed; } catch (error) { // If `completeValueWithLocatedError` returned abruptly (threw an error), // log the error and return null. exeContext.errors.push(error); return null; } } // This is a small wrapper around completeValue which annotates errors with // location information. function completeValueWithLocatedError( exeContext: ExecutionContext, returnType: GraphQLType, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, path: ResponsePath, result: mixed ): mixed { try { const completed = completeValue( exeContext, returnType, fieldNodes, info, path, result ); const promise = getPromise(completed); if (promise) { return promise.then( undefined, error => Promise.reject( locatedError(error, fieldNodes, responsePathAsArray(path)) ) ); } return completed; } catch (error) { throw locatedError(error, fieldNodes, responsePathAsArray(path)); } } /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. * * If the field type is Non-Null, then this recursively completes the value * for the inner type. It throws a field error if that completion returns null, * as per the "Nullability" section of the spec. * * If the field type is a List, then this recursively completes the value * for the inner type on each item in the list. * * If the field type is a Scalar or Enum, ensures the completed value is a legal * value of the type by calling the `serialize` method of GraphQL type * definition. * * If the field is an abstract type, determine the runtime type of the value * and then complete based on that type * * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. */ function completeValue( exeContext: ExecutionContext, returnType: GraphQLType, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, path: ResponsePath, result: mixed ): mixed { // If result is a Promise, apply-lift over completeValue. const promise = getPromise(result); if (promise) { return promise.then( resolved => completeValue( exeContext, returnType, fieldNodes, info, path, resolved ) ); } // If result is an Error, throw a located error. if (result instanceof Error) { throw result; } // If field type is NonNull, complete for inner type, and throw field error // if result is null. if (returnType instanceof GraphQLNonNull) { const completed = completeValue( exeContext, returnType.ofType, fieldNodes, info, path, result ); if (completed === null) { throw new Error( `Cannot return null for non-nullable field ${ info.parentType.name}.${info.fieldName}.` ); } return completed; } // If result value is null-ish (null, undefined, or NaN) then return null. if (isNullish(result)) { return null; } // If field type is List, complete each item in the list with the inner type if (returnType instanceof GraphQLList) { return completeListValue( exeContext, returnType, fieldNodes, info, path, result ); } // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning null if serialization is not possible. if (isLeafType(returnType)) { return completeLeafValue(returnType, result); } // If field type is an abstract type, Interface or Union, determine the // runtime Object type and complete for that type. if (isAbstractType(returnType)) { return completeAbstractValue( exeContext, returnType, fieldNodes, info, path, result ); } // If field type is Object, execute and complete all sub-selections. if (returnType instanceof GraphQLObjectType) { return completeObjectValue( exeContext, returnType, fieldNodes, info, path, result ); } // Not reachable. All possible output types have been considered. throw new Error( `Cannot complete value of unexpected type "${String(returnType)}".` ); } /** * Complete a list value by completing each item in the list with the * inner type */ function completeListValue( exeContext: ExecutionContext, returnType: GraphQLList<*>, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, path: ResponsePath, result: mixed ): mixed { invariant( isCollection(result), `Expected Iterable, but did not find one for field ${ info.parentType.name}.${info.fieldName}.` ); // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. const itemType = returnType.ofType; let containsPromise = false; const completedResults = []; forEach((result: any), (item, index) => { // No need to modify the info object containing the path, // since from here on it is not ever accessed by resolver functions. const fieldPath = addPath(path, index); const completedItem = completeValueCatchingError( exeContext, itemType, fieldNodes, info, fieldPath, item ); if (!containsPromise && getPromise(completedItem)) { containsPromise = true; } completedResults.push(completedItem); }); return containsPromise ? Promise.all(completedResults) : completedResults; } /** * Complete a Scalar or Enum by serializing to a valid value, returning * null if serialization is not possible. */ function completeLeafValue( returnType: GraphQLLeafType, result: mixed ): mixed { invariant(returnType.serialize, 'Missing serialize method on type'); const serializedResult = returnType.serialize(result); if (isNullish(serializedResult)) { throw new Error( `Expected a value of type "${String(returnType)}" but ` + `received: ${String(result)}` ); } return serializedResult; } /** * Complete a value of an abstract type by determining the runtime object type * of that value, then complete the value for that type. */ function completeAbstractValue( exeContext: ExecutionContext, returnType: GraphQLAbstractType, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, path: ResponsePath, result: mixed ): mixed { const runtimeType = returnType.resolveType ? returnType.resolveType(result, exeContext.contextValue, info) : defaultResolveTypeFn(result, exeContext.contextValue, info, returnType); const promise = getPromise(runtimeType); if (promise) { return promise.then(resolvedRuntimeType => completeObjectValue( exeContext, ensureValidRuntimeType( resolvedRuntimeType, exeContext, returnType, fieldNodes, info, result ), fieldNodes, info, path, result ) ); } return completeObjectValue( exeContext, ensureValidRuntimeType( ((runtimeType: any): ?GraphQLObjectType | string), exeContext, returnType, fieldNodes, info, result ), fieldNodes, info, path, result ); } function ensureValidRuntimeType( runtimeTypeOrName: ?GraphQLObjectType | string, exeContext: ExecutionContext, returnType: GraphQLAbstractType, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, result: mixed ): GraphQLObjectType { const runtimeType = typeof runtimeTypeOrName === 'string' ? exeContext.schema.getType(runtimeTypeOrName) : runtimeTypeOrName; if (!(runtimeType instanceof GraphQLObjectType)) { throw new GraphQLError( `Abstract type ${returnType.name} must resolve to an Object type at ` + `runtime for field ${info.parentType.name}.${info.fieldName} with ` + `value "${String(result)}", received "${String(runtimeType)}".`, fieldNodes ); } if (!exeContext.schema.isPossibleType(returnType, runtimeType)) { throw new GraphQLError( `Runtime Object type "${runtimeType.name}" is not a possible type ` + `for "${returnType.name}".`, fieldNodes ); } return runtimeType; } /** * Complete an Object value by executing all sub-selections. */ function completeObjectValue( exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, path: ResponsePath, result: mixed ): mixed { // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. if (returnType.isTypeOf) { const isTypeOf = returnType.isTypeOf(result, exeContext.contextValue, info); const promise = getPromise(isTypeOf); if (promise) { return promise.then(isTypeOfResult => { if (!isTypeOfResult) { throw invalidReturnTypeError(returnType, result, fieldNodes); } return collectAndExecuteSubfields( exeContext, returnType, fieldNodes, info, path, result ); }); } if (!isTypeOf) { throw invalidReturnTypeError(returnType, result, fieldNodes); } } return collectAndExecuteSubfields( exeContext, returnType, fieldNodes, info, path, result ); } function invalidReturnTypeError( returnType: GraphQLObjectType, result: mixed, fieldNodes: Array<FieldNode> ): GraphQLError { return new GraphQLError( `Expected value of type "${returnType.name}" but got: ${String(result)}.`, fieldNodes ); } function collectAndExecuteSubfields( exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldNodes: Array<FieldNode>, info: GraphQLResolveInfo, path: ResponsePath, result: mixed ): mixed { // Collect sub-fields to execute to complete this value. let subFieldNodes = Object.create(null); const visitedFragmentNames = Object.create(null); for (let i = 0; i < fieldNodes.length; i++) { const selectionSet = fieldNodes[i].selectionSet; if (selectionSet) { subFieldNodes = collectFields( exeContext, returnType, selectionSet, subFieldNodes, visitedFragmentNames ); } } return executeFields(exeContext, returnType, result, path, subFieldNodes); } /** * If a resolveType function is not given, then a default resolve behavior is * used which tests each possible type for the abstract type by calling * isTypeOf for the object being coerced, returning the first type that matches. */ function defaultResolveTypeFn( value: mixed, context: mixed, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType ): ?GraphQLObjectType | Promise<?GraphQLObjectType> { const possibleTypes = info.schema.getPossibleTypes(abstractType); const promisedIsTypeOfResults = []; for (let i = 0; i < possibleTypes.length; i++) { const type = possibleTypes[i]; if (type.isTypeOf) { const isTypeOfResult = type.isTypeOf(value, context, info); const promise = getPromise(isTypeOfResult); if (promise) { promisedIsTypeOfResults[i] = promise; } else if (isTypeOfResult) { return type; } } } if (promisedIsTypeOfResults.length) { return Promise.all(promisedIsTypeOfResults).then(isTypeOfResults => { for (let i = 0; i < isTypeOfResults.length; i++) { if (isTypeOfResults[i]) { return possibleTypes[i]; } } }); } } /** * If a resolve function is not given, then a default resolve behavior is used * which takes the property of the source object of the same name as the field * and returns it as the result, or if it's a function, returns the result * of calling that function while passing along args and context. */ export const defaultFieldResolver: GraphQLFieldResolver<any, *> = function (source, args, context, info) { // ensure source is a value for which property access is acceptable. if (typeof source === 'object' || typeof source === 'function') { const property = source[info.fieldName]; if (typeof property === 'function') { return source[info.fieldName](args, context, info); } return property; } }; /** * Only returns the value if it acts like a Promise, i.e. has a "then" function, * otherwise returns void. */ function getPromise<T>(value: Promise<T> | mixed): Promise<T> | void { if (typeof value === 'object' && value !== null && typeof value.then === 'function') { return (value: any); } } /** * 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 ): ?GraphQLField<*, *> { 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]; }