UNPKG

12.8 kBJavaScriptView Raw
1import DataLoader from 'dataloader';
2import { Kind, visit } from 'graphql';
3import { relocatedError } from '@graphql-tools/utils';
4
5// adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
6function createPrefix(index) {
7 return `graphqlTools${index}_`;
8}
9function parseKey(prefixedKey) {
10 const match = /^graphqlTools([\d]+)_(.*)$/.exec(prefixedKey);
11 if (match && match.length === 3 && !isNaN(Number(match[1])) && match[2]) {
12 return { index: Number(match[1]), originalKey: match[2] };
13 }
14 throw new Error(`Key ${prefixedKey} is not correctly prefixed`);
15}
16
17// adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
18/**
19 * Merge multiple queries into a single query in such a way that query results
20 * can be split and transformed as if they were obtained by running original queries.
21 *
22 * Merging algorithm involves several transformations:
23 * 1. Replace top-level fragment spreads with inline fragments (... on Query {})
24 * 2. Add unique aliases to all top-level query fields (including those on inline fragments)
25 * 3. Prefix all variable definitions and variable usages
26 * 4. Prefix names (and spreads) of fragments
27 *
28 * i.e transform:
29 * [
30 * `query Foo($id: ID!) { foo, bar(id: $id), ...FooQuery }
31 * fragment FooQuery on Query { baz }`,
32 *
33 * `query Bar($id: ID!) { foo: baz, bar(id: $id), ... on Query { baz } }`
34 * ]
35 * to:
36 * query (
37 * $graphqlTools1_id: ID!
38 * $graphqlTools2_id: ID!
39 * ) {
40 * graphqlTools1_foo: foo,
41 * graphqlTools1_bar: bar(id: $graphqlTools1_id)
42 * ... on Query {
43 * graphqlTools1__baz: baz
44 * }
45 * graphqlTools1__foo: baz
46 * graphqlTools1__bar: bar(id: $graphqlTools1__id)
47 * ... on Query {
48 * graphqlTools1__baz: baz
49 * }
50 * }
51 */
52function mergeRequests(requests, extensionsReducer) {
53 const mergedVariables = Object.create(null);
54 const mergedVariableDefinitions = [];
55 const mergedSelections = [];
56 const mergedFragmentDefinitions = [];
57 let mergedExtensions = Object.create(null);
58 for (const index in requests) {
59 const request = requests[index];
60 const prefixedRequests = prefixRequest(createPrefix(index), request);
61 for (const def of prefixedRequests.document.definitions) {
62 if (isOperationDefinition(def)) {
63 mergedSelections.push(...def.selectionSet.selections);
64 if (def.variableDefinitions) {
65 mergedVariableDefinitions.push(...def.variableDefinitions);
66 }
67 }
68 if (isFragmentDefinition(def)) {
69 mergedFragmentDefinitions.push(def);
70 }
71 }
72 Object.assign(mergedVariables, prefixedRequests.variables);
73 mergedExtensions = extensionsReducer(mergedExtensions, request);
74 }
75 const mergedOperationDefinition = {
76 kind: Kind.OPERATION_DEFINITION,
77 operation: requests[0].operationType,
78 variableDefinitions: mergedVariableDefinitions,
79 selectionSet: {
80 kind: Kind.SELECTION_SET,
81 selections: mergedSelections,
82 },
83 };
84 return {
85 document: {
86 kind: Kind.DOCUMENT,
87 definitions: [mergedOperationDefinition, ...mergedFragmentDefinitions],
88 },
89 variables: mergedVariables,
90 extensions: mergedExtensions,
91 context: requests[0].context,
92 info: requests[0].info,
93 operationType: requests[0].operationType,
94 };
95}
96function prefixRequest(prefix, request) {
97 var _a;
98 const executionVariables = (_a = request.variables) !== null && _a !== void 0 ? _a : {};
99 function prefixNode(node) {
100 return prefixNodeName(node, prefix);
101 }
102 let prefixedDocument = aliasTopLevelFields(prefix, request.document);
103 const executionVariableNames = Object.keys(executionVariables);
104 if (executionVariableNames.length > 0) {
105 prefixedDocument = visit(prefixedDocument, {
106 [Kind.VARIABLE]: prefixNode,
107 [Kind.FRAGMENT_DEFINITION]: prefixNode,
108 [Kind.FRAGMENT_SPREAD]: prefixNode,
109 });
110 }
111 const prefixedVariables = {};
112 for (const variableName of executionVariableNames) {
113 prefixedVariables[prefix + variableName] = executionVariables[variableName];
114 }
115 return {
116 document: prefixedDocument,
117 variables: prefixedVariables,
118 operationType: request.operationType,
119 };
120}
121/**
122 * Adds prefixed aliases to top-level fields of the query.
123 *
124 * @see aliasFieldsInSelection for implementation details
125 */
126function aliasTopLevelFields(prefix, document) {
127 const transformer = {
128 [Kind.OPERATION_DEFINITION]: (def) => {
129 const { selections } = def.selectionSet;
130 return {
131 ...def,
132 selectionSet: {
133 ...def.selectionSet,
134 selections: aliasFieldsInSelection(prefix, selections, document),
135 },
136 };
137 },
138 };
139 return visit(document, transformer, {
140 [Kind.DOCUMENT]: [`definitions`],
141 });
142}
143/**
144 * Add aliases to fields of the selection, including top-level fields of inline fragments.
145 * Fragment spreads are converted to inline fragments and their top-level fields are also aliased.
146 *
147 * Note that this method is shallow. It adds aliases only to the top-level fields and doesn't
148 * descend to field sub-selections.
149 *
150 * For example, transforms:
151 * {
152 * foo
153 * ... on Query { foo }
154 * ...FragmentWithBarField
155 * }
156 * To:
157 * {
158 * graphqlTools1_foo: foo
159 * ... on Query { graphqlTools1_foo: foo }
160 * ... on Query { graphqlTools1_bar: bar }
161 * }
162 */
163function aliasFieldsInSelection(prefix, selections, document) {
164 return selections.map(selection => {
165 switch (selection.kind) {
166 case Kind.INLINE_FRAGMENT:
167 return aliasFieldsInInlineFragment(prefix, selection, document);
168 case Kind.FRAGMENT_SPREAD: {
169 const inlineFragment = inlineFragmentSpread(selection, document);
170 return aliasFieldsInInlineFragment(prefix, inlineFragment, document);
171 }
172 case Kind.FIELD:
173 default:
174 return aliasField(selection, prefix);
175 }
176 });
177}
178/**
179 * Add aliases to top-level fields of the inline fragment.
180 * Returns new inline fragment node.
181 *
182 * For Example, transforms:
183 * ... on Query { foo, ... on Query { bar: foo } }
184 * To
185 * ... on Query { graphqlTools1_foo: foo, ... on Query { graphqlTools1_bar: foo } }
186 */
187function aliasFieldsInInlineFragment(prefix, fragment, document) {
188 const { selections } = fragment.selectionSet;
189 return {
190 ...fragment,
191 selectionSet: {
192 ...fragment.selectionSet,
193 selections: aliasFieldsInSelection(prefix, selections, document),
194 },
195 };
196}
197/**
198 * Replaces fragment spread with inline fragment
199 *
200 * Example:
201 * query { ...Spread }
202 * fragment Spread on Query { bar }
203 *
204 * Transforms to:
205 * query { ... on Query { bar } }
206 */
207function inlineFragmentSpread(spread, document) {
208 const fragment = document.definitions.find(def => isFragmentDefinition(def) && def.name.value === spread.name.value);
209 if (!fragment) {
210 throw new Error(`Fragment ${spread.name.value} does not exist`);
211 }
212 const { typeCondition, selectionSet } = fragment;
213 return {
214 kind: Kind.INLINE_FRAGMENT,
215 typeCondition,
216 selectionSet,
217 directives: spread.directives,
218 };
219}
220function prefixNodeName(namedNode, prefix) {
221 return {
222 ...namedNode,
223 name: {
224 ...namedNode.name,
225 value: prefix + namedNode.name.value,
226 },
227 };
228}
229/**
230 * Returns a new FieldNode with prefixed alias
231 *
232 * Example. Given prefix === "graphqlTools1_" transforms:
233 * { foo } -> { graphqlTools1_foo: foo }
234 * { foo: bar } -> { graphqlTools1_foo: bar }
235 */
236function aliasField(field, aliasPrefix) {
237 const aliasNode = field.alias ? field.alias : field.name;
238 return {
239 ...field,
240 alias: {
241 ...aliasNode,
242 value: aliasPrefix + aliasNode.value,
243 },
244 };
245}
246function isOperationDefinition(def) {
247 return def.kind === Kind.OPERATION_DEFINITION;
248}
249function isFragmentDefinition(def) {
250 return def.kind === Kind.FRAGMENT_DEFINITION;
251}
252
253// adapted from https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/batching/merge-queries.js
254/**
255 * Split and transform result of the query produced by the `merge` function
256 */
257function splitResult({ data, errors }, numResults) {
258 const splitResults = [];
259 for (let i = 0; i < numResults; i++) {
260 splitResults.push({});
261 }
262 if (data) {
263 for (const prefixedKey in data) {
264 const { index, originalKey } = parseKey(prefixedKey);
265 const result = splitResults[index];
266 if (result == null) {
267 continue;
268 }
269 if (result.data == null) {
270 result.data = { [originalKey]: data[prefixedKey] };
271 }
272 else {
273 result.data[originalKey] = data[prefixedKey];
274 }
275 }
276 }
277 if (errors) {
278 for (const error of errors) {
279 if (error.path) {
280 const parsedKey = parseKey(error.path[0]);
281 const { index, originalKey } = parsedKey;
282 const newError = relocatedError(error, [originalKey, ...error.path.slice(1)]);
283 const errors = (splitResults[index].errors = (splitResults[index].errors || []));
284 errors.push(newError);
285 }
286 }
287 }
288 return splitResults;
289}
290
291function createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer = defaultExtensionsReducer) {
292 const loader = new DataLoader(createLoadFn(executor, extensionsReducer), dataLoaderOptions);
293 return (request) => {
294 return request.operationType === 'subscription' ? executor(request) : loader.load(request);
295 };
296}
297function createLoadFn(executor, extensionsReducer) {
298 return async function batchExecuteLoadFn(requests) {
299 const execBatches = [];
300 let index = 0;
301 const request = requests[index];
302 let currentBatch = [request];
303 execBatches.push(currentBatch);
304 const operationType = request.operationType;
305 while (++index < requests.length) {
306 const currentOperationType = requests[index].operationType;
307 if (operationType == null) {
308 throw new Error('Could not identify operation type of document.');
309 }
310 if (operationType === currentOperationType) {
311 currentBatch.push(requests[index]);
312 }
313 else {
314 currentBatch = [requests[index]];
315 execBatches.push(currentBatch);
316 }
317 }
318 const results = await Promise.all(execBatches.map(async (execBatch) => {
319 const mergedRequests = mergeRequests(execBatch, extensionsReducer);
320 const resultBatches = (await executor(mergedRequests));
321 return splitResult(resultBatches, execBatch.length);
322 }));
323 return results.flat();
324 };
325}
326function defaultExtensionsReducer(mergedExtensions, request) {
327 const newExtensions = request.extensions;
328 if (newExtensions != null) {
329 Object.assign(mergedExtensions, newExtensions);
330 }
331 return mergedExtensions;
332}
333
334function memoize2of4(fn) {
335 let cache1;
336 function memoized(a1, a2, a3, a4) {
337 if (!cache1) {
338 cache1 = new WeakMap();
339 const cache2 = new WeakMap();
340 cache1.set(a1, cache2);
341 const newValue = fn(a1, a2, a3, a4);
342 cache2.set(a2, newValue);
343 return newValue;
344 }
345 let cache2 = cache1.get(a1);
346 if (!cache2) {
347 cache2 = new WeakMap();
348 cache1.set(a1, cache2);
349 const newValue = fn(a1, a2, a3, a4);
350 cache2.set(a2, newValue);
351 return newValue;
352 }
353 const cachedValue = cache2.get(a2);
354 if (cachedValue === undefined) {
355 const newValue = fn(a1, a2, a3, a4);
356 cache2.set(a2, newValue);
357 return newValue;
358 }
359 return cachedValue;
360 }
361 return memoized;
362}
363
364const getBatchingExecutor = memoize2of4(function getBatchingExecutor(_context, executor, dataLoaderOptions, extensionsReducer) {
365 return createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer);
366});
367
368export { createBatchingExecutor, getBatchingExecutor };