UNPKG

@graphql-tools/batch-execute

Version:

A set of utils for faster development of GraphQL tools

343 lines (333 loc) • 10.8 kB
'use strict'; var utils = require('@graphql-tools/utils'); var promiseHelpers = require('@whatwg-node/promise-helpers'); var DataLoader = require('dataloader'); var graphql = require('graphql'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var DataLoader__default = /*#__PURE__*/_interopDefault(DataLoader); function createPrefix(index) { return `_v${index}_`; } function matchKey(prefixedKey) { const match = /^_v(\d+)_(.*)$/.exec(prefixedKey); if (match && match.length === 3 && !isNaN(Number(match[1])) && match[2]) { return { index: Number(match[1]), originalKey: match[2] }; } return null; } function parseKey(prefixedKey) { const match = matchKey(prefixedKey); if (!match) { throw new Error(`Key ${prefixedKey} is not correctly prefixed`); } return match; } function parseKeyFromPath(path) { let keyOffset = 0; let match = null; for (; !match && keyOffset < path.length; keyOffset++) { const pathKey = path[keyOffset]; if (typeof pathKey === "string") { match = matchKey(pathKey); } } if (!match) { throw new Error( `Path ${path.join(".")} does not contain correctly prefixed key` ); } return { ...match, keyOffset }; } function mergeRequests(requests, extensionsReducer) { const mergedVariables = /* @__PURE__ */ Object.create(null); const mergedVariableDefinitions = []; const mergedSelections = []; const mergedFragmentDefinitions = []; let mergedExtensions = /* @__PURE__ */ Object.create(null); for (let index = 0; index < requests.length; index++) { const request = requests[index]; if (request) { const prefixedRequests = prefixRequest(createPrefix(index), request); for (const def of prefixedRequests.document.definitions) { if (isOperationDefinition(def)) { mergedSelections.push(...def.selectionSet.selections); if (def.variableDefinitions) { mergedVariableDefinitions.push(...def.variableDefinitions); } } if (isFragmentDefinition(def)) { mergedFragmentDefinitions.push(def); } } Object.assign(mergedVariables, prefixedRequests.variables); mergedExtensions = extensionsReducer(mergedExtensions, request); } } const firstRequest = requests[0]; if (!firstRequest) { throw new Error("At least one request is required"); } const operationType = firstRequest.operationType ?? utils.getOperationASTFromRequest(firstRequest).operation; const mergedOperationDefinition = { kind: graphql.Kind.OPERATION_DEFINITION, operation: operationType, variableDefinitions: mergedVariableDefinitions, selectionSet: { kind: graphql.Kind.SELECTION_SET, selections: mergedSelections } }; const operationName = firstRequest.operationName ?? firstRequest.info?.operation?.name?.value; if (operationName) { mergedOperationDefinition.name = { kind: graphql.Kind.NAME, value: operationName }; } return { document: { kind: graphql.Kind.DOCUMENT, definitions: [mergedOperationDefinition, ...mergedFragmentDefinitions] }, variables: mergedVariables, extensions: mergedExtensions, context: firstRequest.context, info: firstRequest.info, operationType, rootValue: firstRequest.rootValue }; } function prefixRequest(prefix, request) { function prefixNode(node) { return prefixNodeName(node, prefix); } let prefixedDocument = aliasTopLevelFields(prefix, request.document); let hasFragmentDefinitionsOrVariables = false; for (const def of prefixedDocument.definitions) { if (isFragmentDefinition(def) || isOperationDefinition(def) && !!def.variableDefinitions?.length) { hasFragmentDefinitionsOrVariables = true; break; } } const fragmentSpreadImpl = {}; let hasFragments = false; if (hasFragmentDefinitionsOrVariables) { prefixedDocument = graphql.visit(prefixedDocument, { [graphql.Kind.VARIABLE]: prefixNode, [graphql.Kind.FRAGMENT_DEFINITION](node) { hasFragments = true; return prefixNode(node); }, [graphql.Kind.FRAGMENT_SPREAD]: (node) => { node = prefixNodeName(node, prefix); fragmentSpreadImpl[node.name.value] = true; return node; } }); } let prefixedVariables; const executionVariables = request.variables; if (executionVariables) { prefixedVariables = /* @__PURE__ */ Object.create(null); for (const variableName in executionVariables) { prefixedVariables[prefix + variableName] = executionVariables[variableName]; } } if (hasFragments) { prefixedDocument = { ...prefixedDocument, definitions: prefixedDocument.definitions.filter( (def) => !isFragmentDefinition(def) || fragmentSpreadImpl[def.name.value] ) }; } return { document: prefixedDocument, variables: prefixedVariables }; } function aliasTopLevelFields(prefix, document) { const transformer = { [graphql.Kind.OPERATION_DEFINITION]: (def) => { const { selections } = def.selectionSet; return { ...def, selectionSet: { ...def.selectionSet, selections: aliasFieldsInSelection(prefix, selections, document) } }; } }; return graphql.visit(document, transformer, { [graphql.Kind.DOCUMENT]: [`definitions`] }); } function aliasFieldsInSelection(prefix, selections, document) { return selections.map((selection) => { switch (selection.kind) { case graphql.Kind.INLINE_FRAGMENT: return aliasFieldsInInlineFragment(prefix, selection, document); case graphql.Kind.FRAGMENT_SPREAD: { const inlineFragment = inlineFragmentSpread(selection, document); return aliasFieldsInInlineFragment(prefix, inlineFragment, document); } case graphql.Kind.FIELD: default: return aliasField(selection, prefix); } }); } function aliasFieldsInInlineFragment(prefix, fragment, document) { const { selections } = fragment.selectionSet; return { ...fragment, selectionSet: { ...fragment.selectionSet, selections: aliasFieldsInSelection(prefix, selections, document) } }; } function inlineFragmentSpread(spread, document) { const fragment = document.definitions.find( (def) => isFragmentDefinition(def) && def.name.value === spread.name.value ); if (!fragment) { throw new Error(`Fragment ${spread.name.value} does not exist`); } const { typeCondition, selectionSet } = fragment; return { kind: graphql.Kind.INLINE_FRAGMENT, typeCondition, selectionSet, directives: spread.directives }; } function prefixNodeName(namedNode, prefix) { return { ...namedNode, name: { ...namedNode.name, value: prefix + namedNode.name.value } }; } function aliasField(field, aliasPrefix) { const aliasNode = field.alias ? field.alias : field.name; return { ...field, alias: { ...aliasNode, value: aliasPrefix + aliasNode.value } }; } function isOperationDefinition(def) { return def.kind === graphql.Kind.OPERATION_DEFINITION; } function isFragmentDefinition(def) { return def.kind === graphql.Kind.FRAGMENT_DEFINITION; } function splitResult({ data, errors }, numResults) { const splitResults = []; for (let i = 0; i < numResults; i++) { splitResults.push({}); } if (data) { for (const prefixedKey in data) { const { index, originalKey } = parseKey(prefixedKey); const result = splitResults[index]; if (result == null) { continue; } if (result.data == null) { result.data = { [originalKey]: data[prefixedKey] }; } else { result.data[originalKey] = data[prefixedKey]; } } } if (errors) { for (const error of errors) { if (error.path) { const { index, originalKey, keyOffset } = parseKeyFromPath(error.path); const newError = utils.relocatedError(error, [ originalKey, ...error.path.slice(keyOffset) ]); const splittedResult = splitResults[index]; if (splittedResult) { const resultErrors = splittedResult.errors ||= []; resultErrors.push(newError); } } else { splitResults.forEach((result) => { const resultErrors = result.errors ||= []; resultErrors.push(error); }); } } } return splitResults; } function createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer = defaultExtensionsReducer) { const loadFn = createLoadFn(executor, extensionsReducer); const queryLoader = new DataLoader__default.default(loadFn, dataLoaderOptions); const mutationLoader = new DataLoader__default.default(loadFn, dataLoaderOptions); return function batchingExecutor(request) { const operationType = request.operationType ?? utils.getOperationASTFromRequest(request)?.operation; switch (operationType) { case "query": return queryLoader.load(request); case "mutation": return mutationLoader.load(request); case "subscription": return executor(request); default: throw new Error(`Invalid operation type "${operationType}"`); } }; } function createLoadFn(executor, extensionsReducer) { return function batchExecuteLoadFn(requests) { if (requests.length === 1 && requests[0]) { const request = requests[0]; return promiseHelpers.fakePromise( promiseHelpers.handleMaybePromise( () => executor(request), (result) => [result], (err) => [err] ) ); } const mergedRequests = mergeRequests(requests, extensionsReducer); return promiseHelpers.fakePromise( promiseHelpers.handleMaybePromise( () => executor(mergedRequests), (resultBatches) => { if (utils.isAsyncIterable(resultBatches)) { throw new Error( "Executor must not return incremental results for batching" ); } return splitResult(resultBatches, requests.length); } ) ); }; } function defaultExtensionsReducer(mergedExtensions, request) { const newExtensions = request.extensions; if (newExtensions != null) { Object.assign(mergedExtensions, newExtensions); } return mergedExtensions; } const getBatchingExecutor = utils.memoize2of4(function getBatchingExecutor2(_context, executor, dataLoaderOptions, extensionsReducer) { return createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer); }); exports.createBatchingExecutor = createBatchingExecutor; exports.getBatchingExecutor = getBatchingExecutor;