UNPKG

graphql

Version:

A Query Language and Runtime which can target any service.

574 lines 27.9 kB
import { inspect } from "../jsutils/inspect.mjs"; import { invariant } from "../jsutils/invariant.mjs"; import { isAsyncIterable } from "../jsutils/isAsyncIterable.mjs"; import { isIterableObject } from "../jsutils/isIterableObject.mjs"; import { isPromise, isPromiseLike } from "../jsutils/isPromise.mjs"; import { memoize2 } from "../jsutils/memoize2.mjs"; import { memoize3 } from "../jsutils/memoize3.mjs"; import { addPath, pathToArray } from "../jsutils/Path.mjs"; import { promiseForObject } from "../jsutils/promiseForObject.mjs"; import { promiseReduce } from "../jsutils/promiseReduce.mjs"; import { ensureGraphQLError } from "../error/ensureGraphQLError.mjs"; import { GraphQLError } from "../error/GraphQLError.mjs"; import { locatedError } from "../error/locatedError.mjs"; import { OperationTypeNode } from "../language/ast.mjs"; import { isAbstractType, isLeafType, isListType, isNonNullType, isObjectType, } from "../type/definition.mjs"; import { executeRootSelectionSetChannel, resolveChannel, shouldTrace, traceMixed, } from "../diagnostics.mjs"; import { AbortedGraphQLExecutionError } from "./AbortedGraphQLExecutionError.mjs"; import { buildResolveInfo } from "./buildResolveInfo.mjs"; import { withCancellation } from "./cancellablePromise.mjs"; import { collectFields, collectSubfields as _collectSubfields, } from "./collectFields.mjs"; import { collectIteratorPromises } from "./collectIteratorPromises.mjs"; import { createSharedExecutionContext } from "./createSharedExecutionContext.mjs"; import { getStreamUsage as _getStreamUsage } from "./getStreamUsage.mjs"; import { runAsyncWorkFinishedHook } from "./hooks.mjs"; import { returnIteratorCatchingErrors } from "./returnIteratorCatchingErrors.mjs"; import { getArgumentValues } from "./values.mjs"; export const collectSubfields = memoize3((validatedExecutionArgs, returnType, fieldDetailsList) => { const { schema, fragments, variableValues, hideSuggestions } = validatedExecutionArgs; return _collectSubfields(schema, fragments, variableValues, returnType, fieldDetailsList, hideSuggestions); }); export const getStreamUsage = memoize2((validatedExecutionArgs, fieldDetailsList) => _getStreamUsage(validatedExecutionArgs, fieldDetailsList)); class CollectedErrors { constructor() { this._errorPositions = new Set(); this._errors = []; } get errors() { return this._errors; } add(error, path) { if (this.hasNulledPosition(path)) { return; } this._errorPositions.add(path); this._errors.push(error); } hasNulledPosition(startPath) { let path = startPath; while (path !== undefined) { if (this._errorPositions.has(path)) { return true; } path = path.prev; } return this._errorPositions.has(undefined); } } const defaultAbortReason = new Error('This operation was aborted'); export class Executor { constructor(validatedExecutionArgs, sharedExecutionContext) { this.validatedExecutionArgs = validatedExecutionArgs; this.aborted = false; this.abortReason = defaultAbortReason; this.collectedErrors = new CollectedErrors(); if (sharedExecutionContext === undefined) { this.resolverAbortController = new AbortController(); this.sharedExecutionContext = createSharedExecutionContext(this.resolverAbortController.signal); } else { this.sharedExecutionContext = sharedExecutionContext; } const { getAbortSignal, getAsyncHelpers, promiseAll } = this.sharedExecutionContext; this.getAbortSignal = getAbortSignal; this.getAsyncHelpers = getAsyncHelpers; this.promiseAll = promiseAll; } executeRootSelectionSet(serially) { if (!shouldTrace(executeRootSelectionSetChannel)) { return this.executeRootSelectionSetImpl(serially); } return traceMixed(executeRootSelectionSetChannel, this.buildExecuteContextFromValidatedArgs(this.validatedExecutionArgs), () => this.executeRootSelectionSetImpl(serially)); } buildExecuteContextFromValidatedArgs(args) { return { schema: args.schema, document: args.document, operation: args.operation, rawVariableValues: args.rawVariableValues, operationName: args.operation.name?.value, operationType: args.operation.operation, }; } executeRootSelectionSetImpl(serially) { const externalAbortSignal = this.validatedExecutionArgs.externalAbortSignal; let removeExternalAbortListener; if (externalAbortSignal) { externalAbortSignal.throwIfAborted(); const onExternalAbort = () => { this.abort(externalAbortSignal.reason); }; removeExternalAbortListener = () => externalAbortSignal.removeEventListener('abort', onExternalAbort); externalAbortSignal.addEventListener('abort', onExternalAbort); } const maybeRemoveExternalAbortListener = () => { removeExternalAbortListener?.(); }; let result; try { const { schema, fragments, rootValue, operation, variableValues, hideSuggestions, } = this.validatedExecutionArgs; const { operation: operationType, selectionSet } = operation; const rootType = schema.getRootType(operationType); if (rootType == null) { throw new GraphQLError(`Schema is not configured to execute ${operationType} operation.`, { nodes: operation }); } const { groupedFieldSet, newDeferUsages } = collectFields(schema, fragments, variableValues, rootType, selectionSet, hideSuggestions); result = this.executeCollectedRootFields(rootType, rootValue, groupedFieldSet, serially ?? operationType === OperationTypeNode.MUTATION, newDeferUsages); if (isPromise(result)) { const promise = result.then((data) => { maybeRemoveExternalAbortListener(); return this.buildResponse(data); }, (error) => { maybeRemoveExternalAbortListener(); this.collectedErrors.add(ensureGraphQLError(error), undefined); return this.buildResponse(null); }); this.sharedExecutionContext.asyncWorkTracker.add(promise); const { promise: cancellablePromise, abort: abortResultPromise } = withCancellation(promise.then((resolved) => this.finish(resolved))); this.abortResultPromise = () => { abortResultPromise(this.createAbortedExecutionError(promise)); }; if (this.aborted) { this.abortResultPromise(); } return cancellablePromise; } maybeRemoveExternalAbortListener(); } catch (error) { maybeRemoveExternalAbortListener(); this.collectedErrors.add(ensureGraphQLError(error), undefined); return this.finish(this.buildResponse(null)); } return this.finish(this.buildResponse(result)); } abort(reason) { if (this.aborted) { return; } this.aborted = true; if (reason !== undefined) { this.abortReason = reason; } this.abortResultPromise?.(); this.resolverAbortController?.abort(this.abortReason); } finish(result) { if (this.aborted) { throw this.createAbortedExecutionError(result); } this.aborted = true; return result; } createAbortedExecutionError(result) { return new AbortedGraphQLExecutionError(this.abortReason, result); } getFinishSharedExecution() { const resolverAbortController = this.resolverAbortController; const asyncWorkFinishedHook = this.validatedExecutionArgs.hooks?.asyncWorkFinished; if (asyncWorkFinishedHook === undefined) { return () => resolverAbortController?.abort(); } const validatedExecutionArgs = this.validatedExecutionArgs; const sharedExecutionContext = this.sharedExecutionContext; return () => { resolverAbortController?.abort(); runAsyncWorkFinishedHook(validatedExecutionArgs, sharedExecutionContext, asyncWorkFinishedHook); }; } buildResponse(data) { this.getFinishSharedExecution()(); const errors = this.collectedErrors.errors; return errors.length ? { errors, data } : { data }; } executeCollectedRootFields(rootType, rootValue, originalGroupedFieldSet, serially, _newDeferUsages) { return this.executeRootGroupedFieldSet(rootType, rootValue, originalGroupedFieldSet, serially, undefined); } executeRootGroupedFieldSet(rootType, rootValue, groupedFieldSet, serially, positionContext) { return serially ? this.executeFieldsSerially(rootType, rootValue, undefined, groupedFieldSet, positionContext) : this.executeFields(rootType, rootValue, undefined, groupedFieldSet, positionContext); } executeFieldsSerially(parentType, sourceValue, path, groupedFieldSet, positionContext) { let tracingChannel = shouldTrace(resolveChannel) ? resolveChannel : undefined; return promiseReduce(groupedFieldSet, (results, [responseName, fieldDetailsList]) => { if (this.aborted) { throw new Error('Aborted!'); } const fieldPath = addPath(path, responseName, parentType.name); const result = this.executeField(parentType, sourceValue, fieldDetailsList, fieldPath, positionContext, tracingChannel); if (result === undefined) { return results; } if (isPromise(result)) { return result.then((resolved) => { results[responseName] = resolved; tracingChannel = shouldTrace(resolveChannel) ? resolveChannel : undefined; return results; }); } results[responseName] = result; return results; }, Object.create(null)); } executeFields(parentType, sourceValue, path, groupedFieldSet, positionContext) { const results = Object.create(null); let containsPromise = false; const tracingChannel = shouldTrace(resolveChannel) ? resolveChannel : undefined; try { for (const [responseName, fieldDetailsList] of groupedFieldSet) { const fieldPath = addPath(path, responseName, parentType.name); const result = this.executeField(parentType, sourceValue, fieldDetailsList, fieldPath, positionContext, tracingChannel); if (result !== undefined) { results[responseName] = result; if (isPromise(result)) { containsPromise = true; } } } } catch (error) { if (containsPromise) { this.sharedExecutionContext.asyncWorkTracker.addValues(Object.values(results)); } throw error; } if (!containsPromise) { return results; } return promiseForObject(results, this.promiseAll); } executeField(parentType, source, fieldDetailsList, path, positionContext, tracingChannel) { const validatedExecutionArgs = this.validatedExecutionArgs; const { schema, contextValue, variableValues, hideSuggestions } = validatedExecutionArgs; const firstFieldDetails = fieldDetailsList[0]; const firstNode = firstFieldDetails.node; const fieldName = firstNode.name.value; const fieldDef = schema.getField(parentType, fieldName); if (!fieldDef) { return; } const returnType = fieldDef.type; let resolveFn = fieldDef.resolve ?? validatedExecutionArgs.fieldResolver; if (tracingChannel !== undefined) { const originalResolveFn = resolveFn; resolveFn = (s, args, c, info) => traceMixed(tracingChannel, this.buildResolveContext(args, info, fieldDef.resolve === undefined), () => originalResolveFn(s, args, c, info)); } const info = buildResolveInfo(validatedExecutionArgs, fieldDef, toNodes(fieldDetailsList), parentType, path, this.getAbortSignal, this.getAsyncHelpers); try { const args = getArgumentValues(fieldDef, firstNode, variableValues, firstFieldDetails.fragmentVariableValues, hideSuggestions); const result = resolveFn(source, args, contextValue, info); if (isPromiseLike(result)) { return this.completePromisedValue(returnType, fieldDetailsList, info, path, result, positionContext); } const completed = this.completeValue(returnType, fieldDetailsList, info, path, result, positionContext); if (isPromise(completed)) { return completed.then(undefined, (rawError) => { this.handleFieldError(rawError, returnType, fieldDetailsList, path); return null; }); } return completed; } catch (rawError) { this.handleFieldError(rawError, returnType, fieldDetailsList, path); return null; } } buildResolveContext(args, info, isDefaultResolver) { let cachedFieldPath; return { fieldName: info.fieldName, alias: String(info.path.key), parentType: info.parentType.name, fieldType: String(info.returnType), args, isDefaultResolver, get fieldPath() { cachedFieldPath ??= pathToArray(info.path).join('.'); return cachedFieldPath; }, }; } handleFieldError(rawError, returnType, fieldDetailsList, path) { const error = locatedError(rawError, toNodes(fieldDetailsList), pathToArray(path)); if (this.validatedExecutionArgs.errorPropagation && isNonNullType(returnType)) { throw error; } this.collectedErrors.add(error, path); } completeValue(returnType, fieldDetailsList, info, path, result, positionContext) { if (result instanceof Error) { throw result; } if (isNonNullType(returnType)) { const completed = this.completeValue(returnType.ofType, fieldDetailsList, info, path, result, positionContext); if (completed === null) { throw new Error(`Cannot return null for non-nullable field ${info.parentType}.${info.fieldName}.`); } return completed; } if (result == null) { return null; } if (isListType(returnType)) { return this.completeListValue(returnType, fieldDetailsList, info, path, result, positionContext); } if (isLeafType(returnType)) { return this.completeLeafValue(returnType, result); } if (isAbstractType(returnType)) { return this.completeAbstractValue(returnType, fieldDetailsList, info, path, result, positionContext); } if (isObjectType(returnType)) { return this.completeObjectValue(returnType, fieldDetailsList, info, path, result, positionContext); } invariant(false, 'Cannot complete value of unexpected output type: ' + inspect(returnType)); } async completePromisedValue(returnType, fieldDetailsList, info, path, result, positionContext) { try { const resolved = await result; if (this.aborted) { throw new Error('Aborted!'); } let completed = this.completeValue(returnType, fieldDetailsList, info, path, resolved, positionContext); if (isPromise(completed)) { completed = await completed; } return completed; } catch (rawError) { this.handleFieldError(rawError, returnType, fieldDetailsList, path); return null; } } async completeAsyncIterableValue(itemType, fieldDetailsList, info, path, items, positionContext) { const streamUsage = typeof path.key === 'number' ? undefined : getStreamUsage(this.validatedExecutionArgs, fieldDetailsList); let containsPromise = false; const completedResults = []; const asyncIterator = items[Symbol.asyncIterator](); let index = 0; let iteration; try { while (true) { if (streamUsage?.initialCount === index && this.handleStream(index, path, { handle: asyncIterator, isAsync: true }, streamUsage, info, itemType)) { break; } const itemPath = addPath(path, index, undefined); try { iteration = await asyncIterator.next(); } catch (rawError) { throw locatedError(rawError, toNodes(fieldDetailsList), pathToArray(path)); } if (this.aborted || iteration.done) { break; } const item = iteration.value; if (this.completeMaybePromisedListItemValue(item, completedResults, itemType, fieldDetailsList, info, itemPath, positionContext)) { containsPromise = true; } index++; } } catch (error) { this.sharedExecutionContext.asyncWorkTracker.add(returnIteratorCatchingErrors(asyncIterator)); if (containsPromise) { this.sharedExecutionContext.asyncWorkTracker.addValues(completedResults); } throw error; } if (this.aborted) { if (!iteration?.done) { this.sharedExecutionContext.asyncWorkTracker.add(returnIteratorCatchingErrors(asyncIterator)); } throw new Error('Aborted!'); } return containsPromise ? this.promiseAll(completedResults) : completedResults; } handleStream(_index, _path, _iterator, _streamUsage, _info, _itemType) { return false; } completeListValue(returnType, fieldDetailsList, info, path, result, positionContext) { const itemType = returnType.ofType; if (isAsyncIterable(result)) { return this.completeAsyncIterableValue(itemType, fieldDetailsList, info, path, result, positionContext); } if (!isIterableObject(result)) { throw new GraphQLError(`Expected Iterable, but did not find one for field "${info.parentType}.${info.fieldName}".`); } return this.completeIterableValue(itemType, fieldDetailsList, info, path, result, positionContext); } completeIterableValue(itemType, fieldDetailsList, info, path, items, positionContext) { const streamUsage = typeof path.key === 'number' ? undefined : getStreamUsage(this.validatedExecutionArgs, fieldDetailsList); let containsPromise = false; const completedResults = []; let index = 0; const iterator = items[Symbol.iterator](); try { while (true) { if (streamUsage?.initialCount === index && this.handleStream(index, path, { handle: iterator }, streamUsage, info, itemType)) { break; } const iteration = iterator.next(); if (iteration.done) { break; } const item = iteration.value; const itemPath = addPath(path, index, undefined); if (this.completeMaybePromisedListItemValue(item, completedResults, itemType, fieldDetailsList, info, itemPath, positionContext)) { containsPromise = true; } index++; } } catch (error) { const asyncWorkTracker = this.sharedExecutionContext.asyncWorkTracker; if (containsPromise) { asyncWorkTracker.addValues(completedResults); } asyncWorkTracker.addValues(collectIteratorPromises(iterator)); throw error; } return containsPromise ? this.promiseAll(completedResults) : completedResults; } completeMaybePromisedListItemValue(item, completedResults, itemType, fieldDetailsList, info, itemPath, positionContext) { if (isPromiseLike(item)) { completedResults.push(this.completePromisedListItemValue(item, itemType, fieldDetailsList, info, itemPath, positionContext)); return true; } else if (this.completeListItemValue(item, completedResults, itemType, fieldDetailsList, info, itemPath, positionContext)) { return true; } return false; } completeListItemValue(item, completedResults, itemType, fieldDetailsList, info, itemPath, positionContext) { try { const completedItem = this.completeValue(itemType, fieldDetailsList, info, itemPath, item, positionContext); if (isPromise(completedItem)) { completedResults.push(completedItem.then(undefined, (rawError) => { this.handleFieldError(rawError, itemType, fieldDetailsList, itemPath); return null; })); return true; } completedResults.push(completedItem); } catch (rawError) { this.handleFieldError(rawError, itemType, fieldDetailsList, itemPath); completedResults.push(null); } return false; } async completePromisedListItemValue(item, itemType, fieldDetailsList, info, itemPath, positionContext) { try { const resolved = await item; if (this.aborted) { throw new Error('Aborted!'); } let completed = this.completeValue(itemType, fieldDetailsList, info, itemPath, resolved, positionContext); if (isPromise(completed)) { completed = await completed; } return completed; } catch (rawError) { this.handleFieldError(rawError, itemType, fieldDetailsList, itemPath); return null; } } completeLeafValue(returnType, result) { const coerced = returnType.coerceOutputValue(result); if (coerced == null) { throw new Error(`Expected \`${inspect(returnType)}.coerceOutputValue(${inspect(result)})\` to ` + `return non-nullable value, returned: ${inspect(coerced)}`); } return coerced; } completeAbstractValue(returnType, fieldDetailsList, info, path, result, positionContext) { const validatedExecutionArgs = this.validatedExecutionArgs; const { schema, contextValue } = validatedExecutionArgs; const resolveTypeFn = returnType.resolveType ?? validatedExecutionArgs.typeResolver; const runtimeType = resolveTypeFn(result, contextValue, info, returnType); if (isPromiseLike(runtimeType)) { return runtimeType.then((resolvedRuntimeType) => { if (this.aborted) { throw new Error('Aborted!'); } return this.completeObjectValue(this.ensureValidRuntimeType(resolvedRuntimeType, schema, returnType, fieldDetailsList, info, result), fieldDetailsList, info, path, result, positionContext); }); } return this.completeObjectValue(this.ensureValidRuntimeType(runtimeType, schema, returnType, fieldDetailsList, info, result), fieldDetailsList, info, path, result, positionContext); } ensureValidRuntimeType(runtimeTypeName, schema, returnType, fieldDetailsList, info, result) { if (runtimeTypeName == null) { throw new GraphQLError(`Abstract type "${returnType}" must resolve to an Object type at runtime for field "${info.parentType}.${info.fieldName}". Either the "${returnType}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`, { nodes: toNodes(fieldDetailsList) }); } if (typeof runtimeTypeName !== 'string') { throw new GraphQLError(`Abstract type "${returnType}" must resolve to an Object type at runtime for field "${info.parentType}.${info.fieldName}" with ` + `value ${inspect(result)}, received "${inspect(runtimeTypeName)}", which is not a valid Object type name.`); } const runtimeType = schema.getType(runtimeTypeName); if (runtimeType == null) { throw new GraphQLError(`Abstract type "${returnType}" was resolved to a type "${runtimeTypeName}" that does not exist inside the schema.`, { nodes: toNodes(fieldDetailsList) }); } if (!isObjectType(runtimeType)) { throw new GraphQLError(`Abstract type "${returnType}" was resolved to a non-object type "${runtimeTypeName}".`, { nodes: toNodes(fieldDetailsList) }); } if (!schema.isSubType(returnType, runtimeType)) { throw new GraphQLError(`Runtime Object type "${runtimeType}" is not a possible type for "${returnType}".`, { nodes: toNodes(fieldDetailsList) }); } return runtimeType; } completeObjectValue(returnType, fieldDetailsList, info, path, result, positionContext) { if (returnType.isTypeOf) { const isTypeOf = returnType.isTypeOf(result, this.validatedExecutionArgs.contextValue, info); if (isPromiseLike(isTypeOf)) { return isTypeOf.then((resolvedIsTypeOf) => { if (this.aborted) { throw new Error('Aborted!'); } if (!resolvedIsTypeOf) { throw this.invalidReturnTypeError(returnType, result, fieldDetailsList); } return this.collectAndExecuteSubfields(returnType, fieldDetailsList, path, result, positionContext); }); } if (!isTypeOf) { throw this.invalidReturnTypeError(returnType, result, fieldDetailsList); } } return this.collectAndExecuteSubfields(returnType, fieldDetailsList, path, result, positionContext); } invalidReturnTypeError(returnType, result, fieldDetailsList) { return new GraphQLError(`Expected value of type "${returnType}" but got: ${inspect(result)}.`, { nodes: toNodes(fieldDetailsList) }); } collectAndExecuteSubfields(returnType, fieldDetailsList, path, result, positionContext) { const { groupedFieldSet, newDeferUsages } = collectSubfields(this.validatedExecutionArgs, returnType, fieldDetailsList); return this.executeCollectedSubfields(returnType, result, path, groupedFieldSet, newDeferUsages, positionContext); } executeCollectedSubfields(parentType, sourceValue, path, originalGroupedFieldSet, _newDeferUsages, _positionContext) { return this.executeFields(parentType, sourceValue, path, originalGroupedFieldSet, undefined); } } function toNodes(fieldDetailsList) { return fieldDetailsList.map((fieldDetails) => fieldDetails.node); } //# sourceMappingURL=Executor.js.map