UNPKG

@graphql-tools/executor

Version:

Fork of GraphQL.js' execute function

1,074 lines • 63.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isIncrementalResult = exports.getFieldDef = exports.flattenIncrementalResults = exports.subscribe = exports.defaultFieldResolver = exports.defaultTypeResolver = exports.CRITICAL_ERROR = exports.buildResolveInfo = exports.buildExecutionContext = exports.getFragmentsFromDocument = exports.assertValidExecutionArguments = exports.executeSync = exports.execute = void 0; const graphql_1 = require("graphql"); const utils_1 = require("@graphql-tools/utils"); const AccumulatorMap_js_1 = require("./AccumulatorMap.js"); const BoxedPromiseOrValue_js_1 = require("./BoxedPromiseOrValue.js"); const buildExecutionPlan_js_1 = require("./buildExecutionPlan.js"); const coerceError_js_1 = require("./coerceError.js"); const collectFields_js_1 = require("./collectFields.js"); const DeferredFragments_js_1 = require("./DeferredFragments.js"); const flattenAsyncIterable_js_1 = require("./flattenAsyncIterable.js"); const IncrementalPublisher_js_1 = require("./IncrementalPublisher.js"); const invariant_js_1 = require("./invariant.js"); const promiseForObject_js_1 = require("./promiseForObject.js"); const values_js_1 = require("./values.js"); /** * A memoized collection of relevant subfields with regard to the return * type. Memoizing ensures the subfields are not repeatedly calculated, which * saves overhead when resolving lists of values. */ const collectSubfields = (0, utils_1.memoize3of4)((exeContext, returnType, fieldGroup, path) => (0, collectFields_js_1.collectSubfields)(exeContext.schema, exeContext.fragments, exeContext.variableValues, exeContext.errorOnSubscriptionWithIncrementalDelivery, returnType, fieldGroup, path)); /** * Implements the "Executing requests" section of the GraphQL specification, * including `@defer` and `@stream` as proposed in * https://github.com/graphql/graphql-spec/pull/742 * * This function returns a Promise of an IncrementalExecutionResults * object. This object either consists of a single ExecutionResult, or an * object containing an `initialResult` and a stream of `subsequentResults`. * * If the arguments to this function do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. */ function execute(args) { // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. const exeContext = buildExecutionContext(args); // Return early errors if execution context failed. if (!('schema' in exeContext)) { return { errors: exeContext.map(e => { Object.defineProperty(e, 'extensions', { value: { ...e.extensions, http: { ...e.extensions?.['http'], status: 400, }, }, }); return e; }), }; } return executeOperation(exeContext); } exports.execute = execute; /** * Also implements the "Executing requests" section of the GraphQL specification. * However, it guarantees to complete synchronously (or throw an error) assuming * that all field resolvers are also synchronous. */ function executeSync(args) { const result = execute(args); // Assert that the execution was synchronous. if ((0, utils_1.isPromise)(result) || 'initialResult' in result) { throw new Error('GraphQL execution failed to complete synchronously.'); } return result; } exports.executeSync = executeSync; /** * Given a completed execution context and data, build the `{ errors, data }` * response defined by the "Response" section of the GraphQL specification. */ function buildDataResponse(exeContext, data) { const { errors, incrementalDataRecords } = exeContext; if (incrementalDataRecords === undefined) { return buildSingleResult(data, errors); } if (errors === undefined) { return (0, IncrementalPublisher_js_1.buildIncrementalResponse)(exeContext, data, undefined, incrementalDataRecords); } const filteredIncrementalDataRecords = filterIncrementalDataRecords(undefined, errors, incrementalDataRecords); if (filteredIncrementalDataRecords.length === 0) { return buildSingleResult(data, errors); } return (0, IncrementalPublisher_js_1.buildIncrementalResponse)(exeContext, data, flattenErrors(errors), filteredIncrementalDataRecords); } function buildSingleResult(data, errors) { return errors !== undefined ? { errors: Array.from(errors.values()).flat(), data } : { data }; } function filterIncrementalDataRecords(initialPath, errors, incrementalDataRecords) { const filteredIncrementalDataRecords = []; for (const incrementalDataRecord of incrementalDataRecords) { let currentPath = incrementalDataRecord.path; if (errors.has(currentPath)) { continue; } const paths = [currentPath]; let filtered = false; while (currentPath !== initialPath) { // Because currentPath leads to initialPath or is undefined, and the // loop will exit if initialPath is undefined, currentPath must be // defined. // TODO: Consider, however, adding an invariant. currentPath = currentPath.prev; if (errors.has(currentPath)) { filtered = true; break; } paths.push(currentPath); } if (!filtered) { filteredIncrementalDataRecords.push(incrementalDataRecord); } } return filteredIncrementalDataRecords; } function flattenErrors(errors) { const errorsByPath = [...errors.values()]; return errorsByPath.flat(); } /** * Essential assertions before executing to provide developer feedback for * improper use of the GraphQL library. * * @internal */ function assertValidExecutionArguments(schema, document, rawVariableValues) { console.assert(!!document, 'Must provide document.'); // If the schema used for execution is invalid, throw an error. (0, graphql_1.assertValidSchema)(schema); // Variables, if provided, must be an object. console.assert(rawVariableValues == null || (0, utils_1.isObjectLike)(rawVariableValues), '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.'); } exports.assertValidExecutionArguments = assertValidExecutionArguments; exports.getFragmentsFromDocument = (0, utils_1.memoize1)(function getFragmentsFromDocument(document) { const fragments = Object.create(null); for (const definition of document.definitions) { if (definition.kind === graphql_1.Kind.FRAGMENT_DEFINITION) { fragments[definition.name.value] = definition; } } return fragments; }); /** * 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. * * TODO: consider no longer exporting this function * @internal */ function buildExecutionContext(args) { const { schema, document, rootValue, contextValue, variableValues: rawVariableValues, operationName, fieldResolver, typeResolver, subscribeFieldResolver, enableEarlyExecution, incrementalPreset, deferWithoutDuplication, useIncrementalNotifications, errorOnSubscriptionWithIncrementalDelivery, signal, } = args; // If the schema used for execution is invalid, throw an error. (0, graphql_1.assertValidSchema)(schema); const fragments = (0, exports.getFragmentsFromDocument)(document); let operation; for (const definition of document.definitions) { switch (definition.kind) { case graphql_1.Kind.OPERATION_DEFINITION: if (operationName == null) { if (operation !== undefined) { return [ (0, utils_1.createGraphQLError)('Must provide operation name if query contains multiple operations.'), ]; } operation = definition; } else if (definition.name?.value === operationName) { operation = definition; } break; default: // ignore non-executable definitions } } if (operation == null) { if (operationName != null) { return [(0, utils_1.createGraphQLError)(`Unknown operation named "${operationName}".`)]; } return [(0, utils_1.createGraphQLError)('Must provide an operation.')]; } // FIXME: https://github.com/graphql/graphql-js/issues/2203 /* c8 ignore next */ const variableDefinitions = operation.variableDefinitions ?? []; const coercedVariableValues = (0, values_js_1.getVariableValues)(schema, variableDefinitions, rawVariableValues ?? {}, { maxErrors: 50, }); if (coercedVariableValues.errors) { return coercedVariableValues.errors; } const latestPreset = incrementalPreset !== 'v17.0.0-alpha.2'; return { schema, fragments, rootValue, contextValue, operation, variableValues: coercedVariableValues.coerced, fieldResolver: fieldResolver ?? exports.defaultFieldResolver, typeResolver: typeResolver ?? exports.defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? exports.defaultFieldResolver, enableEarlyExecution: enableEarlyExecution !== false, deferWithoutDuplication: deferWithoutDuplication != null ? deferWithoutDuplication : latestPreset, useIncrementalNotifications: useIncrementalNotifications != null ? useIncrementalNotifications : latestPreset, errorOnSubscriptionWithIncrementalDelivery: operation.operation === 'subscription' && (errorOnSubscriptionWithIncrementalDelivery != null ? errorOnSubscriptionWithIncrementalDelivery : latestPreset), signal, errors: undefined, encounteredDefer: false, deferredFragmentFactory: undefined, cancellableStreams: undefined, incrementalDataRecords: undefined, }; } exports.buildExecutionContext = buildExecutionContext; function buildPerEventExecutionContext(exeContext, payload) { return { ...exeContext, rootValue: payload, errors: undefined, encounteredDefer: false, deferredFragmentFactory: undefined, cancellableStreams: undefined, incrementalDataRecords: undefined, }; } /** * Implements the "Executing operations" section of the spec. */ function executeOperation(exeContext) { if (exeContext.signal?.aborted) { throw exeContext.signal.reason; } try { const { operation, schema, fragments, variableValues, rootValue, deferWithoutDuplication, errorOnSubscriptionWithIncrementalDelivery, } = exeContext; const rootType = (0, utils_1.getDefinedRootType)(schema, operation.operation, [operation]); if (rootType == null) { (0, utils_1.createGraphQLError)(`Schema is not configured to execute ${operation.operation} operation.`, { nodes: operation, }); } const originalGroupedFieldSet = (0, collectFields_js_1.collectFields)(schema, fragments, variableValues, rootType, operation.selectionSet, errorOnSubscriptionWithIncrementalDelivery); let data; if (!originalGroupedFieldSet.encounteredDefer) { data = executeRootGroupedFieldSet(exeContext, operation.operation, rootType, rootValue, originalGroupedFieldSet); } else { exeContext.encounteredDefer = true; const { groupedFieldSet, newGroupedFieldSets } = deferWithoutDuplication ? (0, buildExecutionPlan_js_1.buildExecutionPlan)(originalGroupedFieldSet) : (0, buildExecutionPlan_js_1.buildBranchingExecutionPlan)(originalGroupedFieldSet); data = executeRootGroupedFieldSet(exeContext, operation.operation, rootType, rootValue, groupedFieldSet); if (newGroupedFieldSets.size > 0) { const newPendingExecutionGroups = collectExecutionGroups(exeContext, rootType, rootValue, undefined, undefined, newGroupedFieldSets); addIncrementalDataRecords(exeContext, newPendingExecutionGroups); } } if ((0, utils_1.isPromise)(data)) { return data.then(resolved => buildDataResponse(exeContext, resolved), error => { if (exeContext.signal?.aborted) { throw exeContext.signal.reason; } return { data: null, errors: withError(exeContext.errors, error), }; }); } return buildDataResponse(exeContext, data); } catch (error) { if (exeContext.signal?.aborted) { throw exeContext.signal.reason; } return { data: null, errors: withError(exeContext.errors, error) }; } } function executeRootGroupedFieldSet(exeContext, operation, rootType, rootValue, groupedFieldSet) { let result; if (operation === 'mutation') { result = executeFieldsSerially(exeContext, rootType, rootValue, undefined, groupedFieldSet, undefined); } else { result = executeFields(exeContext, rootType, rootValue, undefined, groupedFieldSet, undefined); } return result; } function addIncrementalDataRecords(context, newIncrementalDataRecords) { const incrementalDataRecords = context.incrementalDataRecords; if (incrementalDataRecords === undefined) { context.incrementalDataRecords = [...newIncrementalDataRecords]; return; } incrementalDataRecords.push(...newIncrementalDataRecords); } function withError(errors, error) { const newErrors = 'errors' in error ? error.errors : [error]; return errors === undefined ? newErrors : [...flattenErrors(errors), ...newErrors]; } /** * Implements the "Executing selection sets" section of the spec * for fields that must be executed serially. */ function executeFieldsSerially(exeContext, parentType, sourceValue, path, groupedFieldSet, incrementalContext) { return (0, utils_1.promiseReduce)(groupedFieldSet, (results, [responseName, fieldGroup]) => { const fieldPath = (0, utils_1.addPath)(path, responseName, parentType.name); if (exeContext.signal?.aborted) { throw exeContext.signal.reason; } const result = executeField(exeContext, parentType, sourceValue, fieldGroup, fieldPath, incrementalContext); if (result === undefined) { return results; } if ((0, utils_1.isPromise)(result)) { return result.then(resolved => { results[responseName] = resolved; return results; }); } results[responseName] = result; return results; }, Object.create(null)); } /** * Implements the "Executing selection sets" section of the spec * for fields that may be executed in parallel. */ function executeFields(exeContext, parentType, sourceValue, path, groupedFieldSet, incrementalContext) { const results = Object.create(null); let containsPromise = false; try { for (const [responseName, fieldGroup] of groupedFieldSet) { if (exeContext.signal?.aborted) { throw exeContext.signal.reason; } const fieldPath = (0, utils_1.addPath)(path, responseName, parentType.name); const result = executeField(exeContext, parentType, sourceValue, fieldGroup, fieldPath, incrementalContext); if (result !== undefined) { results[responseName] = result; if ((0, utils_1.isPromise)(result)) { containsPromise = true; } } } } catch (error) { if (containsPromise) { // Ensure that any promises returned by other fields are handled, as they may also reject. return (0, promiseForObject_js_1.promiseForObject)(results, exeContext.signal).finally(() => { throw error; }); } throw error; } // If there are no promises, we can just return the object and any incrementalDataRecords if (!containsPromise) { return results; } // Otherwise, results is a map from field name to the result of resolving that // field, which is possibly a promise. Return a promise that will return this // same map, but with any promises replaced with the values they resolved to. return (0, promiseForObject_js_1.promiseForObject)(results, exeContext.signal); } function toNodes(fieldGroup) { return fieldGroup.map(fieldDetails => fieldDetails.node); } /** * Implements the "Executing fields" section of the spec * In particular, this function 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 executeField(exeContext, parentType, source, fieldGroup, path, incrementalContext) { const fieldDef = getFieldDef(exeContext.schema, parentType, fieldGroup[0].node); if (!fieldDef) { return; } const returnType = fieldDef.type; const resolveFn = fieldDef.resolve ?? exeContext.fieldResolver; const info = buildResolveInfo(exeContext, fieldDef, toNodes(fieldGroup), parentType, path); // Get the resolve function, regardless of if its result is normal or abrupt (error). 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 = (0, utils_1.getArgumentValues)(fieldDef, fieldGroup[0].node, 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 contextValue = exeContext.contextValue; const result = resolveFn(source, args, contextValue, info); if ((0, utils_1.isPromise)(result)) { return completePromisedValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext); } const completed = completeValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext); if ((0, utils_1.isPromise)(completed)) { // Note: we don't rely on a `catch` method, but we do expect "thenable" // to take a second callback for the error case. return completed.then(undefined, rawError => { handleFieldError(rawError, exeContext, returnType, fieldGroup, path, incrementalContext); return null; }); } return completed; } catch (rawError) { handleFieldError(rawError, exeContext, returnType, fieldGroup, path, incrementalContext); return null; } } /** * TODO: consider no longer exporting this function * @internal */ function buildResolveInfo(exeContext, fieldDef, fieldNodes, parentType, path) { // The resolve function's optional fourth argument is a collection of // information about the current execution state. return { fieldName: fieldDef.name, fieldNodes, returnType: fieldDef.type, parentType, path, schema: exeContext.schema, fragments: exeContext.fragments, rootValue: exeContext.rootValue, operation: exeContext.operation, variableValues: exeContext.variableValues, }; } exports.buildResolveInfo = buildResolveInfo; exports.CRITICAL_ERROR = 'CRITICAL_ERROR'; function handleFieldError(rawError, exeContext, returnType, fieldGroup, path, incrementalContext) { if (rawError instanceof AggregateError) { for (const rawErrorItem of rawError.errors) { handleFieldError(rawErrorItem, exeContext, returnType, fieldGroup, path, incrementalContext); } return; } const error = (0, graphql_1.locatedError)((0, coerceError_js_1.coerceError)(rawError), toNodes(fieldGroup), (0, utils_1.pathToArray)(path)); // If the field type is non-nullable, then it is resolved without any // protection from errors, however it still properly locates the error. if ((0, graphql_1.isNonNullType)(returnType)) { throw error; } if (error.extensions?.[exports.CRITICAL_ERROR]) { throw error; } // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. const context = incrementalContext ?? exeContext; let errors = context.errors; if (errors === undefined) { errors = new AccumulatorMap_js_1.AccumulatorMap(); context.errors = errors; } errors.add(path, error); } /** * Implements the instructions for completeValue as defined in the * "Value Completion" 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 executing all sub-selections. */ function completeValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext) { // 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 ((0, graphql_1.isNonNullType)(returnType)) { const completed = completeValue(exeContext, returnType.ofType, fieldGroup, info, path, result, incrementalContext); 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 or undefined then return null. if (result == null) { return null; } // If field type is List, complete each item in the list with the inner type if ((0, graphql_1.isListType)(returnType)) { return completeListValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext); } // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning null if serialization is not possible. if ((0, graphql_1.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 ((0, graphql_1.isAbstractType)(returnType)) { return completeAbstractValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext); } // If field type is Object, execute and complete all sub-selections. if ((0, graphql_1.isObjectType)(returnType)) { return completeObjectValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext); } /* c8 ignore next 6 */ // Not reachable, all possible output types have been considered. (0, invariant_js_1.invariant)(false, 'Cannot complete value of unexpected output type: ' + (0, utils_1.inspect)(returnType)); } async function completePromisedValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext) { try { const resolved = await result; let completed = completeValue(exeContext, returnType, fieldGroup, info, path, resolved, incrementalContext); if ((0, utils_1.isPromise)(completed)) { completed = await completed; } return completed; } catch (rawError) { handleFieldError(rawError, exeContext, returnType, fieldGroup, path, incrementalContext); return null; } } /** * Returns an object containing info for streaming if a field should be * streamed based on the experimental flag, stream directive present and * not disabled by the "if" argument. */ function getStreamUsage(exeContext, fieldGroup, path) { // do not stream inner lists of multi-dimensional lists if (typeof path.key === 'number') { return; } // TODO: add test for this case (a streamed list nested under a list). /* c8 ignore next 7 */ if (fieldGroup._streamUsage !== undefined) { return fieldGroup._streamUsage; } // validation only allows equivalent streams on multiple fields, so it is // safe to only check the first fieldNode for the stream directive const stream = (0, graphql_1.getDirectiveValues)(utils_1.GraphQLStreamDirective, fieldGroup[0].node, exeContext.variableValues); if (!stream) { return; } if (stream.if === false) { return; } (0, invariant_js_1.invariant)(typeof stream['initialCount'] === 'number', 'initialCount must be a number'); (0, invariant_js_1.invariant)(stream['initialCount'] >= 0, 'initialCount must be a positive integer'); (0, invariant_js_1.invariant)(!exeContext.errorOnSubscriptionWithIncrementalDelivery, '`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.'); const streamedFieldGroup = fieldGroup.map(fieldDetails => ({ node: fieldDetails.node, deferUsage: undefined, })); const streamUsage = { initialCount: stream['initialCount'], label: typeof stream['label'] === 'string' ? stream['label'] : undefined, fieldGroup: streamedFieldGroup, }; fieldGroup._streamUsage = streamUsage; return streamUsage; } /** * Complete a async iterator value by completing the result and calling * recursively until all the results are completed. */ async function completeAsyncIteratorValue(exeContext, itemType, fieldGroup, info, path, asyncIterator, incrementalContext) { exeContext.signal?.addEventListener('abort', () => { asyncIterator.return?.(); }); let containsPromise = false; const completedResults = []; let index = 0; const streamUsage = getStreamUsage(exeContext, fieldGroup, path); const earlyReturn = asyncIterator.return === undefined ? undefined : asyncIterator.return.bind(asyncIterator); try { while (true) { if (streamUsage && index >= streamUsage.initialCount) { const streamItemQueue = buildAsyncStreamItemQueue(index, path, asyncIterator, exeContext, streamUsage.fieldGroup, info, itemType); let streamRecord; if (earlyReturn === undefined) { streamRecord = { label: streamUsage.label, path, index, streamItemQueue, }; } else { streamRecord = { label: streamUsage.label, path, index, streamItemQueue, earlyReturn, }; if (exeContext.cancellableStreams === undefined) { exeContext.cancellableStreams = new Set(); } exeContext.cancellableStreams.add(streamRecord); } const context = incrementalContext ?? exeContext; addIncrementalDataRecords(context, [streamRecord]); break; } const itemPath = (0, utils_1.addPath)(path, index, undefined); let iteration; try { iteration = await asyncIterator.next(); } catch (rawError) { throw (0, graphql_1.locatedError)((0, coerceError_js_1.coerceError)(rawError), toNodes(fieldGroup), (0, utils_1.pathToArray)(path)); } // TODO: add test case for stream returning done before initialCount /* c8 ignore next 3 */ if (iteration.done) { break; } const item = iteration.value; // TODO: add tests for stream backed by asyncIterator that returns a promise /* c8 ignore start */ if ((0, utils_1.isPromise)(item)) { completedResults.push(completePromisedListItemValue(item, exeContext, itemType, fieldGroup, info, itemPath, incrementalContext)); containsPromise = true; } else if ( /* c8 ignore stop */ completeListItemValue(item, completedResults, exeContext, itemType, fieldGroup, info, itemPath, incrementalContext) // TODO: add tests for stream backed by asyncIterator that completes to a promise /* c8 ignore start */ ) { containsPromise = true; } /* c8 ignore stop */ index++; } } catch (error) { if (earlyReturn !== undefined) { earlyReturn().catch(() => { /* c8 ignore next 1 */ // ignore error }); } throw error; } return containsPromise ? /* c8 ignore start */ Promise.all(completedResults) : /* c8 ignore stop */ completedResults; } /** * Complete a list value by completing each item in the list with the * inner type */ function completeListValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext) { const itemType = returnType.ofType; if ((0, utils_1.isAsyncIterable)(result)) { const asyncIterator = result[Symbol.asyncIterator](); return completeAsyncIteratorValue(exeContext, itemType, fieldGroup, info, path, asyncIterator, incrementalContext); } if (!(0, utils_1.isIterableObject)(result)) { throw (0, utils_1.createGraphQLError)(`Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`); } return completeIterableValue(exeContext, itemType, fieldGroup, info, path, result, incrementalContext); } function completeIterableValue(exeContext, itemType, fieldGroup, info, path, items, incrementalContext) { // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. let containsPromise = false; const completedResults = []; let index = 0; const streamUsage = getStreamUsage(exeContext, fieldGroup, path); const iterator = items[Symbol.iterator](); let iteration = iterator.next(); while (!iteration.done) { const item = iteration.value; if (streamUsage && index >= streamUsage.initialCount) { const streamRecord = { label: streamUsage.label, path, index, streamItemQueue: buildSyncStreamItemQueue(item, index, path, iterator, exeContext, streamUsage.fieldGroup, info, itemType), }; const context = incrementalContext ?? exeContext; addIncrementalDataRecords(context, [streamRecord]); break; } // No need to modify the info object containing the path, // since from here on it is not ever accessed by resolver functions. const itemPath = (0, utils_1.addPath)(path, index, undefined); if ((0, utils_1.isPromise)(item)) { completedResults.push(completePromisedListItemValue(item, exeContext, itemType, fieldGroup, info, itemPath, incrementalContext)); containsPromise = true; } else if (completeListItemValue(item, completedResults, exeContext, itemType, fieldGroup, info, itemPath, incrementalContext)) { containsPromise = true; } index++; iteration = iterator.next(); } return containsPromise ? Promise.all(completedResults) : completedResults; } /** * Complete a list item value by adding it to the completed results. * * Returns true if the value is a Promise. */ function completeListItemValue(item, completedResults, exeContext, itemType, fieldGroup, info, itemPath, incrementalContext) { try { const completedItem = completeValue(exeContext, itemType, fieldGroup, info, itemPath, item, incrementalContext); if ((0, utils_1.isPromise)(completedItem)) { // Note: we don't rely on a `catch` method, but we do expect "thenable" // to take a second callback for the error case. completedResults.push(completedItem.then(undefined, rawError => { handleFieldError(rawError, exeContext, itemType, fieldGroup, itemPath, incrementalContext); return null; })); return true; } completedResults.push(completedItem); } catch (rawError) { handleFieldError(rawError, exeContext, itemType, fieldGroup, itemPath, incrementalContext); completedResults.push(null); } return false; } async function completePromisedListItemValue(item, exeContext, itemType, fieldGroup, info, itemPath, incrementalContext) { try { const resolved = await item; let completed = completeValue(exeContext, itemType, fieldGroup, info, itemPath, resolved, incrementalContext); if ((0, utils_1.isPromise)(completed)) { completed = await completed; } return completed; } catch (rawError) { handleFieldError(rawError, exeContext, itemType, fieldGroup, itemPath, incrementalContext); return null; } } /** * Complete a Scalar or Enum by serializing to a valid value, returning * null if serialization is not possible. */ function completeLeafValue(returnType, result) { let serializedResult; // Note: We transform GraphQLError to Error in order to be consistent with // how non-null checks work later on. // See https://github.com/kamilkisiela/graphql-hive/pull/2299 // See https://github.com/n1ru4l/envelop/issues/1808 try { serializedResult = returnType.serialize(result); } catch (err) { if (err instanceof graphql_1.GraphQLError) { throw new Error(err.message); } throw err; } if (serializedResult == null) { throw new Error(`Expected \`${(0, utils_1.inspect)(returnType)}.serialize(${(0, utils_1.inspect)(result)})\` to ` + `return non-nullable value, returned: ${(0, utils_1.inspect)(serializedResult)}`); } 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, returnType, fieldGroup, info, path, result, incrementalContext) { const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; const contextValue = exeContext.contextValue; const runtimeType = resolveTypeFn(result, contextValue, info, returnType); if ((0, utils_1.isPromise)(runtimeType)) { return runtimeType.then(resolvedRuntimeType => completeObjectValue(exeContext, ensureValidRuntimeType(resolvedRuntimeType, exeContext, returnType, fieldGroup, info, result), fieldGroup, info, path, result, incrementalContext)); } return completeObjectValue(exeContext, ensureValidRuntimeType(runtimeType, exeContext, returnType, fieldGroup, info, result), fieldGroup, info, path, result, incrementalContext); } function ensureValidRuntimeType(runtimeTypeName, exeContext, returnType, fieldGroup, info, result) { if (runtimeTypeName == null) { throw (0, utils_1.createGraphQLError)(`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}". Either the "${returnType.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, { nodes: toNodes(fieldGroup) }); } // releases before 16.0.0 supported returning `GraphQLObjectType` from `resolveType` // TODO: remove in 17.0.0 release if ((0, graphql_1.isObjectType)(runtimeTypeName)) { if (graphql_1.versionInfo.major >= 16) { throw (0, utils_1.createGraphQLError)('Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.'); } runtimeTypeName = runtimeTypeName.name; } if (typeof runtimeTypeName !== 'string') { throw (0, utils_1.createGraphQLError)(`Abstract type "${returnType.name}" must resolve to an Object type at runtime for field "${info.parentType.name}.${info.fieldName}" with ` + `value ${(0, utils_1.inspect)(result)}, received "${(0, utils_1.inspect)(runtimeTypeName)}".`); } const runtimeType = exeContext.schema.getType(runtimeTypeName); if (runtimeType == null) { throw (0, utils_1.createGraphQLError)(`Abstract type "${returnType.name}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, { nodes: toNodes(fieldGroup) }); } if (!(0, graphql_1.isObjectType)(runtimeType)) { throw (0, utils_1.createGraphQLError)(`Abstract type "${returnType.name}" was resolved to a non-object type "${runtimeTypeName}".`, { nodes: toNodes(fieldGroup) }); } if (!exeContext.schema.isSubType(returnType, runtimeType)) { throw (0, utils_1.createGraphQLError)(`Runtime Object type "${runtimeType.name}" is not a possible type for "${returnType.name}".`, { nodes: toNodes(fieldGroup) }); } return runtimeType; } /** * Complete an Object value by executing all sub-selections. */ function completeObjectValue(exeContext, returnType, fieldGroup, info, path, result, incrementalContext) { // 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); if ((0, utils_1.isPromise)(isTypeOf)) { return isTypeOf.then(resolvedIsTypeOf => { if (!resolvedIsTypeOf) { throw invalidReturnTypeError(returnType, result, fieldGroup); } return collectAndExecuteSubfields(exeContext, returnType, fieldGroup, path, result, incrementalContext); }); } if (!isTypeOf) { throw invalidReturnTypeError(returnType, result, fieldGroup); } } return collectAndExecuteSubfields(exeContext, returnType, fieldGroup, path, result, incrementalContext); } function invalidReturnTypeError(returnType, result, fieldGroup) { return (0, utils_1.createGraphQLError)(`Expected value of type "${returnType.name}" but got: ${(0, utils_1.inspect)(result)}.`, { nodes: toNodes(fieldGroup) }); } function collectAndExecuteSubfields(exeContext, returnType, fieldGroup, path, result, incrementalContext) { // Collect sub-fields to execute to complete this value. const originalGroupedFieldSet = collectSubfields(exeContext, returnType, fieldGroup, path); if (!exeContext.encounteredDefer && !originalGroupedFieldSet.encounteredDefer) { return executeFields(exeContext, returnType, result, path, originalGroupedFieldSet, incrementalContext); } exeContext.encounteredDefer = true; const { groupedFieldSet, newGroupedFieldSets } = buildSubExecutionPlan(originalGroupedFieldSet, incrementalContext?.deferUsageSet, exeContext.deferWithoutDuplication); const subFields = executeFields(exeContext, returnType, result, path, groupedFieldSet, incrementalContext); if (newGroupedFieldSets.size > 0) { const newPendingExecutionGroups = collectExecutionGroups(exeContext, returnType, result, path, incrementalContext?.deferUsageSet, newGroupedFieldSets); const context = incrementalContext ?? exeContext; addIncrementalDataRecords(context, newPendingExecutionGroups); } return subFields; } function buildSubExecutionPlan(originalGroupedFieldSet, deferUsageSet, deferWithoutDuplication) { let executionPlan = originalGroupedFieldSet ._executionPlan; if (executionPlan !== undefined) { return executionPlan; } executionPlan = deferWithoutDuplication ? (0, buildExecutionPlan_js_1.buildExecutionPlan)(originalGroupedFieldSet, deferUsageSet) : (0, buildExecutionPlan_js_1.buildBranchingExecutionPlan)(originalGroupedFieldSet, deferUsageSet); originalGroupedFieldSet._executionPlan = executionPlan; return executionPlan; } /** * If a resolveType function is not given, then a default resolve behavior is * used which attempts two strategies: * * First, See if the provided value has a `__typename` field defined, if so, use * that value as name of the resolved type. * * Otherwise, test each possible type for the abstract type by calling * isTypeOf for the object being coerced, returning the first type that matches. */ const defaultTypeResolver = function (value, contextValue, info, abstractType) { // First, look for `__typename`. if ((0, utils_1.isObjectLike)(value) && typeof value['__typename'] === 'string') { return value['__typename']; } // Otherwise, test each possible type. 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, contextValue, info); if ((0, utils_1.isPromise)(isTypeOfResult)) { promisedIsTypeOfResults[i] = isTypeOfResult; } else if (isTypeOfResult) { return type.name; } } } if (promisedIsTypeOfResults.length) { return Promise.all(promisedIsTypeOfResults).then(isTypeOfResults => { for (let i = 0; i < isTypeOfResults.length; i++) { if (isTypeOfResults[i]) { return possibleTypes[i].name; } } }); } }; exports.defaultTypeResolver = defaultTypeResolver; /** * 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 value. */ const defaultFieldResolver = function (source, args, contextValue, info) { // ensure source is a value for which property access is acceptable. if ((0, utils_1.isObjectLike)(source) || typeof source === 'function') { const property = source[info.fieldName]; if (typeof property === 'function') { return source[info.fieldName](args, contextValue, info); } return property; } }; exports.defaultFieldResolver = defaultFieldResolver; function subscribe(args) { // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. const exeContext = buildExecutionContext(args); // Return early errors if execution context failed. if (!('schema' in exeContext)) { return { errors: exeContext.map(e => { Object.defineProperty(e, 'extensions', { value: { ...e.extensions, http: { ...e.extensions?.['http'], status: 400, }, }, }); return e; }), }; } const resultOrStream = createSourceEventStreamImpl(exeContext); if ((0, utils_1.isPromise)(resultOrStream)) { return resultOrStream.then(resolvedResultOrStream => mapSourceToResponse(exeContext, resolvedResultOrStream)); } return mapSourceToResponse(exeContext, resultOrStream); } exports.subscribe = subscribe; function flattenIncrementalResults(incrementalResults) { const subsequentIterator = incrementalResults.subsequentResults; let initialResultSent = false; let done = false; return { [Symbol.asyncIterator]() { return this; }, next() { if (done) { return Promise.resolve({ value: undefined, done, }); } if (initialResultSent) { return subsequentIterator.next(); } initialResultSent = true; return Promise.resolve({ value: incrementalResults.initialResult, done, }); }, return() { done = true; return subsequentIterator.return(); }, throw(error) { done = true; return subsequentIterator.throw(error); }, }; } exports.flattenIncrementalResults = flattenIncrementalResults; async function* ensureAsyncIterable(someExecutionResult) { if ('initialResult' in someExecutionResult) { yield* flattenIncrementalResults(someExecutionResult); } else { yield someExecutionResult; } } function mapSourceToResponse(exeContext, resultOrStream) { if (!(0, utils_1.isAsyncIterable)(resultOrStream)) { return resultOrStream; } // For each payload yielded from a subscription, map it over the normal // GraphQL `execute` function, with `payload` as the rootValue. // This implements the "MapSourceToResponseEvent" algorithm described in // the GraphQL specification. The `execute` function provides the // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. return (0, flattenAsyncIterable_js_1.flattenAsyncIterable)((0, utils_1.mapAsyncIterator)(resultOrStream, async (payload) => ensureAsyncIterable(await executeOperation(buildPerEventExecutionContext(exeContext, payload))), (error) => { if (error instanceof AggregateError) { throw new AggregateError(error.errors.map(e => wrapError(e, exeContext.operation)), error.message); } throw wrapError(error, exeContext.operation); })); } function wrapError(error, operation) { return (0, utils_1.createGraphQLError)(error.message, { originalError: error, nodes: [operation], }); } function createSourceEventStreamImpl(exeContext) { try { const eventStream = executeSubscription(exeContext); if ((0, utils_1.isPromise)(eventStream)) { return eventStream.then(undefined, error => ({ errors: [error] })); } return eventStream; } catch (error) { return { errors: [error] }; } } function executeSubscription(exeContext) { const { schema, fragments, operation, variableValues, rootValue, errorOnSubscriptionWithIncrementalDelivery, } = exeContext; const rootType = schema.getSubscriptionType(); if (rootType == null) { throw (0, utils_1.createGraphQLError)('Schema is not configured to execute subscription operation.', { nodes: operation, }); } const groupedFieldSet = (0, collectFields_js_1.collectFields)(schema, fragments, variableValues, rootType, operation.selectionSet, errorOnSubscriptionWithIncrementalDelivery); const firstRootField = [...groupedFieldSet.entries()][0]; const [responseName, fieldGroup] = firstRootField; const fieldName = fieldGroup[0].node.name.value; const fieldDef = getFieldDef(schema, rootType, fieldGroup[0].node); if (!fieldDef) { throw (0, utils_1.createGraphQLError)(`The subscription field "${fieldName}" is not defined.`, { nodes: toNodes(fieldGroup), }); } const path = (0, utils_1.addPath)(undefined, responseName, rootType.name); const info = buildResolveInfo(exeContext, fieldDef, toNodes(fieldGroup), rootType, path); try { // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. const args = (0, utils_1.getArgumentValues)(fieldDef, fieldGroup[0].node, 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 con