UNPKG

18.4 kBJavaScriptView Raw
1"use strict";
2
3const _ = require(`lodash`);
4
5const {
6 isAbstractType,
7 GraphQLOutputType,
8 GraphQLUnionType,
9 GraphQLList,
10 getNamedType,
11 getNullableType,
12 isCompositeType
13} = require(`graphql`);
14
15const invariant = require(`invariant`);
16
17const reporter = require(`gatsby-cli/lib/reporter`);
18
19class LocalNodeModel {
20 constructor({
21 schema,
22 schemaComposer,
23 nodeStore,
24 createPageDependency
25 }) {
26 this.schema = schema;
27 this.schemaComposer = schemaComposer;
28 this.nodeStore = nodeStore;
29 this.createPageDependency = createPageDependency;
30 this._rootNodeMap = new WeakMap();
31 this._trackedRootNodes = new Set();
32 this._prepareNodesQueues = {};
33 this._prepareNodesPromises = {};
34 this._preparedNodesCache = new Map();
35 this.replaceTypeKeyValueCache();
36 }
37 /**
38 * Replace the cache either with the value passed on (mainly for tests) or
39 * an empty new Map.
40 *
41 * @param {undefined | Map<string, Map<string, Set<Node>> | Map<string, Node>>} map
42 * (This cached is used in redux/nodes.js and caches a set of buckets (Sets)
43 * of Nodes based on filter and tracks this for each set of types which are
44 * actually queried. If the filter targets `id` directly, only one Node is
45 * cached instead of a Set of Nodes.
46 */
47
48
49 replaceTypeKeyValueCache(map = new Map()) {
50 this._typedKeyValueIndexes = new Map(); // See redux/nodes.js for usage
51 }
52
53 withContext(context) {
54 return new ContextualNodeModel(this, context);
55 }
56 /**
57 * Get a node from the store by ID and optional type.
58 *
59 * @param {Object} args
60 * @param {string} args.id ID of the requested node
61 * @param {(string|GraphQLOutputType)} [args.type] Optional type of the node
62 * @param {PageDependencies} [pageDependencies]
63 * @returns {(Node|null)}
64 */
65
66
67 getNodeById(args, pageDependencies) {
68 const {
69 id,
70 type
71 } = args || {};
72 const node = getNodeById(this.nodeStore, id);
73 let result;
74
75 if (!node) {
76 result = null;
77 } else if (!type) {
78 result = node;
79 } else {
80 const nodeTypeNames = toNodeTypeNames(this.schema, type);
81 result = nodeTypeNames.includes(node.internal.type) ? node : null;
82 }
83
84 if (result) {
85 this.trackInlineObjectsInRootNode(node);
86 }
87
88 return this.trackPageDependencies(result, pageDependencies);
89 }
90 /**
91 * Get nodes from the store by IDs and optional type.
92 *
93 * @param {Object} args
94 * @param {string[]} args.ids IDs of the requested nodes
95 * @param {(string|GraphQLOutputType)} [args.type] Optional type of the nodes
96 * @param {PageDependencies} [pageDependencies]
97 * @returns {Node[]}
98 */
99
100
101 getNodesByIds(args, pageDependencies) {
102 const {
103 ids,
104 type
105 } = args || {};
106 const nodes = Array.isArray(ids) ? ids.map(id => getNodeById(this.nodeStore, id)).filter(Boolean) : [];
107 let result;
108
109 if (!nodes.length || !type) {
110 result = nodes;
111 } else {
112 const nodeTypeNames = toNodeTypeNames(this.schema, type);
113 result = nodes.filter(node => nodeTypeNames.includes(node.internal.type));
114 }
115
116 if (result) {
117 result.forEach(node => this.trackInlineObjectsInRootNode(node));
118 }
119
120 return this.trackPageDependencies(result, pageDependencies);
121 }
122 /**
123 * Get all nodes in the store, or all nodes of a specified type. Note that
124 * this doesn't add tracking to all the nodes, unless pageDependencies are
125 * passed.
126 *
127 * @param {Object} args
128 * @param {(string|GraphQLOutputType)} [args.type] Optional type of the nodes
129 * @param {PageDependencies} [pageDependencies]
130 * @returns {Node[]}
131 */
132
133
134 getAllNodes(args, pageDependencies) {
135 const {
136 type
137 } = args || {};
138 let result;
139
140 if (!type) {
141 result = this.nodeStore.getNodes();
142 } else {
143 const nodeTypeNames = toNodeTypeNames(this.schema, type);
144 const nodes = nodeTypeNames.reduce((acc, typeName) => {
145 acc.push(...this.nodeStore.getNodesByType(typeName));
146 return acc;
147 }, []);
148 result = nodes.filter(Boolean);
149 }
150
151 if (result) {
152 result.forEach(node => this.trackInlineObjectsInRootNode(node));
153 }
154
155 if (pageDependencies) {
156 return this.trackPageDependencies(result, pageDependencies);
157 } else {
158 return result;
159 }
160 }
161 /**
162 * Get nodes of a type matching the specified query.
163 *
164 * @param {Object} args
165 * @param {Object} args.query Query arguments (`filter` and `sort`)
166 * @param {(string|GraphQLOutputType)} args.type Type
167 * @param {boolean} [args.firstOnly] If true, return only first match
168 * @param {PageDependencies} [pageDependencies]
169 * @returns {Promise<Node[]>}
170 */
171
172
173 async runQuery(args, pageDependencies) {
174 const {
175 query,
176 firstOnly,
177 type
178 } = args || {}; // We don't support querying union types (yet?), because the combined types
179 // need not have any fields in common.
180
181 const gqlType = typeof type === `string` ? this.schema.getType(type) : type;
182 invariant(!(gqlType instanceof GraphQLUnionType), `Querying GraphQLUnion types is not supported.`);
183 const nodeTypeNames = toNodeTypeNames(this.schema, gqlType);
184 const fields = getQueryFields({
185 filter: query.filter,
186 sort: query.sort,
187 group: query.group,
188 distinct: query.distinct
189 });
190 const fieldsToResolve = determineResolvableFields(this.schemaComposer, this.schema, gqlType, fields, nodeTypeNames);
191 await this.prepareNodes(gqlType, fields, fieldsToResolve, nodeTypeNames);
192 const queryResult = await this.nodeStore.runQuery({
193 queryArgs: query,
194 firstOnly,
195 gqlSchema: this.schema,
196 gqlComposer: this.schemaComposer,
197 gqlType,
198 resolvedFields: fieldsToResolve,
199 nodeTypeNames,
200 typedKeyValueIndexes: this._typedKeyValueIndexes
201 });
202 let result = queryResult;
203
204 if (firstOnly) {
205 var _result;
206
207 if (((_result = result) === null || _result === void 0 ? void 0 : _result.length) > 0) {
208 result = result[0];
209 this.trackInlineObjectsInRootNode(result);
210 } else {
211 result = null;
212 }
213 } else if (result) {
214 result.forEach(node => this.trackInlineObjectsInRootNode(node));
215 }
216
217 return this.trackPageDependencies(result, pageDependencies);
218 }
219
220 prepareNodes(type, queryFields, fieldsToResolve, nodeTypeNames) {
221 const typeName = type.name;
222
223 if (!this._prepareNodesQueues[typeName]) {
224 this._prepareNodesQueues[typeName] = [];
225 }
226
227 this._prepareNodesQueues[typeName].push({
228 queryFields,
229 fieldsToResolve
230 });
231
232 if (!this._prepareNodesPromises[typeName]) {
233 this._prepareNodesPromises[typeName] = new Promise(resolve => {
234 process.nextTick(async () => {
235 await this._doResolvePrepareNodesQueue(type, nodeTypeNames);
236 resolve();
237 });
238 });
239 }
240
241 return this._prepareNodesPromises[typeName];
242 }
243
244 async _doResolvePrepareNodesQueue(type, nodeTypeNames) {
245 const typeName = type.name;
246 const queue = this._prepareNodesQueues[typeName];
247 this._prepareNodesQueues[typeName] = [];
248 this._prepareNodesPromises[typeName] = null;
249 const {
250 queryFields,
251 fieldsToResolve
252 } = queue.reduce(({
253 queryFields,
254 fieldsToResolve
255 }, {
256 queryFields: nextQueryFields,
257 fieldsToResolve: nextFieldsToResolve
258 }) => {
259 return {
260 queryFields: _.merge(queryFields, nextQueryFields),
261 fieldsToResolve: _.merge(fieldsToResolve, nextFieldsToResolve)
262 };
263 }, {
264 queryFields: {},
265 fieldsToResolve: {}
266 });
267 const actualFieldsToResolve = deepObjectDifference(fieldsToResolve, this._preparedNodesCache.get(typeName) || {});
268
269 if (!_.isEmpty(actualFieldsToResolve)) {
270 await this.nodeStore.saveResolvedNodes(nodeTypeNames, async node => {
271 this.trackInlineObjectsInRootNode(node);
272 const resolvedFields = await resolveRecursive(this, this.schemaComposer, this.schema, node, type, queryFields, actualFieldsToResolve);
273
274 const mergedResolved = _.merge(node.__gatsby_resolved || {}, resolvedFields);
275
276 return mergedResolved;
277 });
278
279 this._preparedNodesCache.set(typeName, _.merge({}, this._preparedNodesCache.get(typeName) || {}, actualFieldsToResolve));
280 }
281 }
282 /**
283 * Get the names of all node types in the store.
284 *
285 * @returns {string[]}
286 */
287
288
289 getTypes() {
290 return this.nodeStore.getTypes();
291 }
292 /**
293 * Adds link between inline objects/arrays contained in Node object
294 * and that Node object.
295 * @param {Node} node Root Node
296 */
297
298
299 trackInlineObjectsInRootNode(node) {
300 if (!this._trackedRootNodes.has(node.id)) {
301 addRootNodeToInlineObject(this._rootNodeMap, node, node.id, true, new Set());
302
303 this._trackedRootNodes.add(node.id);
304 }
305 }
306 /**
307 * Finds top most ancestor of node that contains passed Object or Array
308 * @param {(Object|Array)} obj Object/Array belonging to Node object or Node object
309 * @param {nodePredicate} [predicate] Optional callback to check if ancestor meets defined conditions
310 * @returns {Node} Top most ancestor if predicate is not specified
311 * or first node that meet predicate conditions if predicate is specified
312 */
313
314
315 findRootNodeAncestor(obj, predicate = null) {
316 let iterations = 0;
317 let node = obj;
318
319 while (iterations++ < 100) {
320 if (predicate && predicate(node)) return node;
321 const parent = node.parent && getNodeById(this.nodeStore, node.parent);
322
323 const id = this._rootNodeMap.get(node);
324
325 const trackedParent = id && getNodeById(this.nodeStore, id);
326 if (!parent && !trackedParent) return node;
327 node = parent || trackedParent;
328 }
329
330 reporter.error(`It looks like you have a node that's set its parent as itself:\n\n` + node);
331 return null;
332 }
333 /**
334 * Given a result, that's either a single node or an array of them, track them
335 * using pageDependencies. Defaults to tracking according to current resolver
336 * path. Returns the result back.
337 *
338 * @param {Node | Node[]} result
339 * @param {PageDependencies} [pageDependencies]
340 * @returns {Node | Node[]}
341 */
342
343
344 trackPageDependencies(result, pageDependencies = {}) {
345 const {
346 path,
347 connectionType
348 } = pageDependencies;
349
350 if (path) {
351 if (connectionType) {
352 this.createPageDependency({
353 path,
354 connection: connectionType
355 });
356 } else {
357 const nodes = Array.isArray(result) ? result : [result];
358
359 for (const node of nodes) {
360 if (node) {
361 this.createPageDependency({
362 path,
363 nodeId: node.id
364 });
365 }
366 }
367 }
368 }
369
370 return result;
371 }
372
373}
374
375class ContextualNodeModel {
376 constructor(rootNodeModel, context) {
377 this.nodeModel = rootNodeModel;
378 this.context = context;
379 }
380
381 withContext(context) {
382 return new ContextualNodeModel(this.nodeModel, Object.assign({}, this.context, {}, context));
383 }
384
385 _getFullDependencies(pageDependencies) {
386 return Object.assign({
387 path: this.context.path
388 }, pageDependencies || {});
389 }
390
391 getNodeById(args, pageDependencies) {
392 return this.nodeModel.getNodeById(args, this._getFullDependencies(pageDependencies));
393 }
394
395 getNodesByIds(args, pageDependencies) {
396 return this.nodeModel.getNodesByIds(args, this._getFullDependencies(pageDependencies));
397 }
398
399 getAllNodes(args, pageDependencies) {
400 const fullDependencies = pageDependencies ? this._getFullDependencies(pageDependencies) : null;
401 return this.nodeModel.getAllNodes(args, fullDependencies);
402 }
403
404 runQuery(args, pageDependencies) {
405 return this.nodeModel.runQuery(args, this._getFullDependencies(pageDependencies));
406 }
407
408 prepareNodes(...args) {
409 return this.nodeModel.prepareNodes(...args);
410 }
411
412 getTypes(...args) {
413 return this.nodeModel.getTypes(...args);
414 }
415
416 trackInlineObjectsInRootNode(...args) {
417 return this.nodeModel.trackInlineObjectsInRootNode(...args);
418 }
419
420 findRootNodeAncestor(...args) {
421 return this.nodeModel.findRootNodeAncestor(...args);
422 }
423
424 createPageDependency(...args) {
425 return this.nodeModel.createPageDependency(...args);
426 }
427
428 trackPageDependencies(result, pageDependencies) {
429 return this.nodeModel.trackPageDependencies(result, this._getFullDependencies(pageDependencies));
430 }
431
432}
433
434const getNodeById = (nodeStore, id) => id != null ? nodeStore.getNode(id) : null;
435
436const toNodeTypeNames = (schema, gqlTypeName) => {
437 const gqlType = typeof gqlTypeName === `string` ? schema.getType(gqlTypeName) : gqlTypeName;
438 if (!gqlType) return [];
439 const possibleTypes = isAbstractType(gqlType) ? schema.getPossibleTypes(gqlType) : [gqlType];
440 return possibleTypes.filter(type => type.getInterfaces().some(iface => iface.name === `Node`)).map(type => type.name);
441};
442
443const getQueryFields = ({
444 filter,
445 sort,
446 group,
447 distinct
448}) => {
449 const filterFields = filter ? dropQueryOperators(filter) : {};
450 const sortFields = sort && sort.fields || [];
451
452 if (group && !Array.isArray(group)) {
453 group = [group];
454 } else if (group == null) {
455 group = [];
456 }
457
458 if (distinct && !Array.isArray(distinct)) {
459 distinct = [distinct];
460 } else if (distinct == null) {
461 distinct = [];
462 }
463
464 return _.merge(filterFields, ...sortFields.map(pathToObject), ...group.map(pathToObject), ...distinct.map(pathToObject));
465};
466
467const pathToObject = path => {
468 if (path && typeof path === `string`) {
469 return path.split(`.`).reduceRight((acc, key) => {
470 return {
471 [key]: acc
472 };
473 }, true);
474 }
475
476 return {};
477};
478
479const dropQueryOperators = filter => Object.keys(filter).reduce((acc, key) => {
480 const value = filter[key];
481 const k = Object.keys(value)[0];
482 const v = value[k];
483
484 if (_.isPlainObject(value) && _.isPlainObject(v)) {
485 acc[key] = k === `elemMatch` ? dropQueryOperators(v) : dropQueryOperators(value);
486 } else {
487 acc[key] = true;
488 }
489
490 return acc;
491}, {});
492
493const getFields = (schema, type, node) => {
494 if (!isAbstractType(type)) {
495 return type.getFields();
496 }
497
498 const concreteType = type.resolveType(node);
499 return schema.getType(concreteType).getFields();
500};
501
502async function resolveRecursive(nodeModel, schemaComposer, schema, node, type, queryFields, fieldsToResolve) {
503 const gqlFields = getFields(schema, type, node);
504 const resolvedFields = {};
505
506 for (const fieldName of Object.keys(fieldsToResolve)) {
507 const fieldToResolve = fieldsToResolve[fieldName];
508 const queryField = queryFields[fieldName];
509 const gqlField = gqlFields[fieldName];
510 const gqlNonNullType = getNullableType(gqlField.type);
511 const gqlFieldType = getNamedType(gqlField.type);
512 let innerValue;
513
514 if (gqlField.resolve) {
515 innerValue = await resolveField(nodeModel, schemaComposer, schema, node, gqlField, fieldName);
516 } else {
517 innerValue = node[fieldName];
518 }
519
520 if (gqlField && innerValue != null) {
521 if (isCompositeType(gqlFieldType) && !(gqlNonNullType instanceof GraphQLList)) {
522 innerValue = await resolveRecursive(nodeModel, schemaComposer, schema, innerValue, gqlFieldType, queryField, _.isObject(fieldToResolve) ? fieldToResolve : queryField);
523 } else if (isCompositeType(gqlFieldType) && _.isArray(innerValue) && gqlNonNullType instanceof GraphQLList) {
524 innerValue = await Promise.all(innerValue.map(item => resolveRecursive(nodeModel, schemaComposer, schema, item, gqlFieldType, queryField, _.isObject(fieldToResolve) ? fieldToResolve : queryField)));
525 }
526 }
527
528 if (innerValue != null) {
529 resolvedFields[fieldName] = innerValue;
530 }
531 }
532
533 Object.keys(queryFields).forEach(key => {
534 if (!fieldsToResolve[key] && node[key]) {
535 resolvedFields[key] = node[key];
536 }
537 });
538 return _.pickBy(resolvedFields, (value, key) => queryFields[key]);
539}
540
541function resolveField(nodeModel, schemaComposer, schema, node, gqlField, fieldName) {
542 const withResolverContext = require(`./context`);
543
544 return gqlField.resolve(node, gqlField.args.reduce((acc, arg) => {
545 acc[arg.name] = arg.defaultValue;
546 return acc;
547 }, {}), withResolverContext({
548 schema,
549 schemaComposer,
550 nodeModel
551 }), {
552 fieldName,
553 schema,
554 returnType: gqlField.type
555 });
556}
557
558const determineResolvableFields = (schemaComposer, schema, type, fields, nodeTypeNames) => {
559 const fieldsToResolve = {};
560 const gqlFields = type.getFields();
561 Object.keys(fields).forEach(fieldName => {
562 const field = fields[fieldName];
563 const gqlField = gqlFields[fieldName];
564 const gqlFieldType = getNamedType(gqlField.type);
565 const typeComposer = schemaComposer.getAnyTC(type.name);
566 const possibleTCs = [typeComposer, ...nodeTypeNames.map(name => schemaComposer.getAnyTC(name))];
567 let needsResolve = false;
568
569 for (const tc of possibleTCs) {
570 needsResolve = tc.getFieldExtension(fieldName, `needsResolve`) || false;
571
572 if (needsResolve) {
573 break;
574 }
575 }
576
577 if (_.isObject(field) && gqlField) {
578 const innerResolved = determineResolvableFields(schemaComposer, schema, gqlFieldType, field, toNodeTypeNames(schema, gqlFieldType));
579
580 if (!_.isEmpty(innerResolved)) {
581 fieldsToResolve[fieldName] = innerResolved;
582 }
583 }
584
585 if (!fieldsToResolve[fieldName] && needsResolve) {
586 fieldsToResolve[fieldName] = true;
587 }
588 });
589 return fieldsToResolve;
590};
591
592const addRootNodeToInlineObject = (rootNodeMap, data, nodeId, isNode
593/*: boolean */
594, path
595/*: Set<mixed> */
596) =>
597/*: void */
598{
599 const isPlainObject = _.isPlainObject(data);
600
601 if (isPlainObject || _.isArray(data)) {
602 if (path.has(data)) return;
603 path.add(data);
604
605 _.each(data, (o, key) => {
606 if (!isNode || key !== `internal`) {
607 addRootNodeToInlineObject(rootNodeMap, o, nodeId, false, path);
608 }
609 }); // don't need to track node itself
610
611
612 if (!isNode) {
613 rootNodeMap.set(data, nodeId);
614 }
615 }
616};
617
618const deepObjectDifference = (from, to) => {
619 const result = {};
620 Object.keys(from).forEach(key => {
621 const toValue = to[key];
622
623 if (toValue) {
624 if (_.isPlainObject(toValue)) {
625 const deepResult = deepObjectDifference(from[key], toValue);
626
627 if (!_.isEmpty(deepResult)) {
628 result[key] = deepResult;
629 }
630 }
631 } else {
632 result[key] = from[key];
633 }
634 });
635 return result;
636};
637
638module.exports = {
639 LocalNodeModel
640};
641//# sourceMappingURL=node-model.js.map
\No newline at end of file