UNPKG

@graphql-tools/batch-execute

Version:

A set of utils for faster development of GraphQL tools

361 lines (354 loc) • 12.9 kB
import DataLoader from 'dataloader'; import { Kind, visit, GraphQLError } from 'graphql'; import { relocatedError, memoize2of4 } from '@graphql-tools/utils'; // adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js function createPrefix(index) { return `_${index}_`; } function parseKey(prefixedKey) { const match = /^_([\d]+)_(.*)$/.exec(prefixedKey); if (match && match.length === 3 && !isNaN(Number(match[1])) && match[2]) { return { index: Number(match[1]), originalKey: match[2] }; } throw new Error(`Key ${prefixedKey} is not correctly prefixed`); } // adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js /** * Merge multiple queries into a single query in such a way that query results * can be split and transformed as if they were obtained by running original queries. * * Merging algorithm involves several transformations: * 1. Replace top-level fragment spreads with inline fragments (... on Query {}) * 2. Add unique aliases to all top-level query fields (including those on inline fragments) * 3. Prefix all variable definitions and variable usages * 4. Prefix names (and spreads) of fragments * * i.e transform: * [ * `query Foo($id: ID!) { foo, bar(id: $id), ...FooQuery } * fragment FooQuery on Query { baz }`, * * `query Bar($id: ID!) { foo: baz, bar(id: $id), ... on Query { baz } }` * ] * to: * query ( * $graphqlTools1_id: ID! * $graphqlTools2_id: ID! * ) { * graphqlTools1_foo: foo, * graphqlTools1_bar: bar(id: $graphqlTools1_id) * ... on Query { * graphqlTools1__baz: baz * } * graphqlTools1__foo: baz * graphqlTools1__bar: bar(id: $graphqlTools1__id) * ... on Query { * graphqlTools1__baz: baz * } * } */ function mergeRequests(operationType, requests, extensionsReducer) { const mergedVariables = Object.create(null); const mergedVariableDefinitions = []; const mergedSelections = []; const mergedFragmentDefinitions = []; let mergedExtensions = Object.create(null); for (const index in requests) { const request = requests[index]; const prefixedRequests = prefixRequest(createPrefix(index), request, operationType); 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 mergedOperationDefinition = { kind: Kind.OPERATION_DEFINITION, operation: operationType, variableDefinitions: mergedVariableDefinitions, selectionSet: { kind: Kind.SELECTION_SET, selections: mergedSelections, }, }; return { document: { kind: Kind.DOCUMENT, definitions: [mergedOperationDefinition, ...mergedFragmentDefinitions], }, variables: mergedVariables, extensions: mergedExtensions, context: requests[0].context, info: requests[0].info, operationType, }; } function prefixRequest(prefix, request, operationType) { var _a; const executionVariables = (_a = request.variables) !== null && _a !== void 0 ? _a : {}; function prefixNode(node) { return prefixNodeName(node, prefix); } let prefixedDocument = aliasTopLevelFields(prefix, request.document); const executionVariableNames = Object.keys(executionVariables); const hasFragmentDefinitions = request.document.definitions.some(def => isFragmentDefinition(def)); const fragmentSpreadImpl = {}; if (executionVariableNames.length > 0 || hasFragmentDefinitions) { prefixedDocument = visit(prefixedDocument, { [Kind.VARIABLE]: prefixNode, [Kind.FRAGMENT_DEFINITION]: prefixNode, [Kind.FRAGMENT_SPREAD]: node => { node = prefixNodeName(node, prefix); fragmentSpreadImpl[node.name.value] = true; return node; }, }); } const prefixedVariables = {}; for (const variableName of executionVariableNames) { prefixedVariables[prefix + variableName] = executionVariables[variableName]; } if (hasFragmentDefinitions) { prefixedDocument = { ...prefixedDocument, definitions: prefixedDocument.definitions.filter(def => { return !isFragmentDefinition(def) || fragmentSpreadImpl[def.name.value]; }), }; } return { document: prefixedDocument, variables: prefixedVariables, operationType, }; } /** * Adds prefixed aliases to top-level fields of the query. * * @see aliasFieldsInSelection for implementation details */ function aliasTopLevelFields(prefix, document) { const transformer = { [Kind.OPERATION_DEFINITION]: (def) => { const { selections } = def.selectionSet; return { ...def, selectionSet: { ...def.selectionSet, selections: aliasFieldsInSelection(prefix, selections, document), }, }; }, }; return visit(document, transformer, { [Kind.DOCUMENT]: [`definitions`], }); } /** * Add aliases to fields of the selection, including top-level fields of inline fragments. * Fragment spreads are converted to inline fragments and their top-level fields are also aliased. * * Note that this method is shallow. It adds aliases only to the top-level fields and doesn't * descend to field sub-selections. * * For example, transforms: * { * foo * ... on Query { foo } * ...FragmentWithBarField * } * To: * { * graphqlTools1_foo: foo * ... on Query { graphqlTools1_foo: foo } * ... on Query { graphqlTools1_bar: bar } * } */ function aliasFieldsInSelection(prefix, selections, document) { return selections.map(selection => { switch (selection.kind) { case Kind.INLINE_FRAGMENT: return aliasFieldsInInlineFragment(prefix, selection, document); case Kind.FRAGMENT_SPREAD: { const inlineFragment = inlineFragmentSpread(selection, document); return aliasFieldsInInlineFragment(prefix, inlineFragment, document); } case Kind.FIELD: default: return aliasField(selection, prefix); } }); } /** * Add aliases to top-level fields of the inline fragment. * Returns new inline fragment node. * * For Example, transforms: * ... on Query { foo, ... on Query { bar: foo } } * To * ... on Query { graphqlTools1_foo: foo, ... on Query { graphqlTools1_bar: foo } } */ function aliasFieldsInInlineFragment(prefix, fragment, document) { const { selections } = fragment.selectionSet; return { ...fragment, selectionSet: { ...fragment.selectionSet, selections: aliasFieldsInSelection(prefix, selections, document), }, }; } /** * Replaces fragment spread with inline fragment * * Example: * query { ...Spread } * fragment Spread on Query { bar } * * Transforms to: * query { ... on Query { bar } } */ 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: Kind.INLINE_FRAGMENT, typeCondition, selectionSet, directives: spread.directives, }; } function prefixNodeName(namedNode, prefix) { return { ...namedNode, name: { ...namedNode.name, value: prefix + namedNode.name.value, }, }; } /** * Returns a new FieldNode with prefixed alias * * Example. Given prefix === "graphqlTools1_" transforms: * { foo } -> { graphqlTools1_foo: foo } * { foo: bar } -> { graphqlTools1_foo: bar } */ 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 === Kind.OPERATION_DEFINITION; } function isFragmentDefinition(def) { return def.kind === Kind.FRAGMENT_DEFINITION; } // adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js /** * Split and transform result of the query produced by the `merge` function */ 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 parsedKey = parseKey(error.path[0]); const { index, originalKey } = parsedKey; const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]); const resultErrors = (splitResults[index].errors = (splitResults[index].errors || [])); resultErrors.push(newError); } else { splitResults.forEach(result => { const resultErrors = (result.errors = (result.errors || [])); resultErrors.push(new GraphQLError(error.message)); }); } } } return splitResults; } function createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer = defaultExtensionsReducer) { const loadFn = createLoadFn(executor, extensionsReducer); const loader = new DataLoader(loadFn, dataLoaderOptions); return function batchingExecutor(request) { return request.operationType === 'subscription' ? executor(request) : loader.load(request); }; } function createLoadFn(executor, extensionsReducer) { return async function batchExecuteLoadFn(requests) { const execBatches = []; let index = 0; const request = requests[index]; let currentBatch = [request]; execBatches.push(currentBatch); const operationType = request.operationType; if (operationType == null) { throw new Error('could not identify operation type of document'); } while (++index < requests.length) { const currentRequest = requests[index]; const currentOperationType = currentRequest.operationType; if (operationType === currentOperationType) { currentBatch.push(currentRequest); } else { currentBatch = [currentRequest]; execBatches.push(currentBatch); } } const results = await Promise.all(execBatches.map(async (execBatch) => { const mergedRequests = mergeRequests(execBatch[0].operationType, execBatch, extensionsReducer); const resultBatches = (await executor(mergedRequests)); return splitResult(resultBatches, execBatch.length); })); return results.flat(); }; } function defaultExtensionsReducer(mergedExtensions, request) { const newExtensions = request.extensions; if (newExtensions != null) { Object.assign(mergedExtensions, newExtensions); } return mergedExtensions; } const getBatchingExecutor = memoize2of4(function getBatchingExecutor(_context, executor, dataLoaderOptions, extensionsReducer) { return createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer); }); export { createBatchingExecutor, getBatchingExecutor };