UNPKG

12.8 kBJavaScriptView Raw
1"use strict";
2
3const {
4 default: sift
5} = require(`sift`);
6
7const _ = require(`lodash`);
8
9const prepareRegex = require(`../utils/prepare-regex`);
10
11const {
12 makeRe
13} = require(`micromatch`);
14
15const {
16 getValueAt
17} = require(`../utils/get-value-at`);
18
19const {
20 toDottedFields,
21 objectToDottedField,
22 liftResolvedFields
23} = require(`../db/common/query`);
24
25const {
26 ensureIndexByTypedChain,
27 getNodesByTypedChain,
28 addResolvedNodes,
29 getNode: siftGetNode
30} = require(`./nodes`); /////////////////////////////////////////////////////////////////////
31// Parse filter
32/////////////////////////////////////////////////////////////////////
33
34
35const prepareQueryArgs = (filterFields = {}) => Object.keys(filterFields).reduce((acc, key) => {
36 const value = filterFields[key];
37
38 if (_.isPlainObject(value)) {
39 acc[key === `elemMatch` ? `$elemMatch` : key] = prepareQueryArgs(value);
40 } else {
41 switch (key) {
42 case `regex`:
43 acc[`$regex`] = prepareRegex(value);
44 break;
45
46 case `glob`:
47 acc[`$regex`] = makeRe(value);
48 break;
49
50 default:
51 acc[`$${key}`] = value;
52 }
53 }
54
55 return acc;
56}, {});
57
58const getFilters = filters => Object.keys(filters).reduce((acc, key) => acc.push({
59 [key]: filters[key]
60}) && acc, []); /////////////////////////////////////////////////////////////////////
61// Run Sift
62/////////////////////////////////////////////////////////////////////
63
64
65function isEqId(siftArgs) {
66 // The `id` of each node is invariably unique. So if a query is doing id $eq(string) it can find only one node tops
67 return siftArgs.length > 0 && siftArgs[0].id && Object.keys(siftArgs[0].id).length === 1 && Object.keys(siftArgs[0].id)[0] === `$eq`;
68}
69
70function handleFirst(siftArgs, nodes) {
71 if (nodes.length === 0) {
72 return [];
73 }
74
75 const index = _.isEmpty(siftArgs) ? 0 : nodes.findIndex(sift({
76 $and: siftArgs
77 }));
78
79 if (index !== -1) {
80 return [nodes[index]];
81 } else {
82 return [];
83 }
84}
85
86function handleMany(siftArgs, nodes) {
87 let result = _.isEmpty(siftArgs) ? nodes : nodes.filter(sift({
88 $and: siftArgs
89 }));
90 return (result === null || result === void 0 ? void 0 : result.length) ? result : null;
91}
92/**
93 * Given an object, assert that it has exactly one leaf property and that this
94 * leaf is a number, string, or boolean. Additionally confirms that the path
95 * does not contain the special cased `elemMatch` name.
96 * Returns undefined if not a flat path, if it contains `elemMatch`, or if the
97 * leaf value was not a bool, number, or string.
98 * If array, it contains the property path followed by the leaf value.
99 * Returns `undefined` if any condition is not met
100 *
101 * Example: `{a: {b: {c: "x"}}}` is flat with a chain of `['a', 'b', 'c', 'x']`
102 * Example: `{a: {b: "x", c: "y"}}` is not flat because x and y are 2 leafs
103 *
104 * @param {Object} obj
105 * @returns {Array<string | number | boolean>|undefined}
106 */
107
108
109const getFlatPropertyChain = obj => {
110 if (!obj) {
111 return undefined;
112 }
113
114 let chain = [];
115 let props = Object.getOwnPropertyNames(obj);
116 let next = obj;
117
118 while (props.length === 1) {
119 const prop = props[0];
120
121 if (prop === `elemMatch`) {
122 // TODO: Support handling this special case without sift as well
123 return undefined;
124 }
125
126 chain.push(prop);
127 next = next[prop];
128
129 if (typeof next === `string` || typeof next === `number` || typeof next === `boolean`) {
130 chain.push(next);
131 return chain;
132 }
133
134 if (!next) {
135 return undefined;
136 }
137
138 props = Object.getOwnPropertyNames(next);
139 } // This means at least one object in the chain had more than one property
140
141
142 return undefined;
143};
144/**
145 * Given the chain of a simple filter, return the set of nodes that pass the
146 * filter. The chain should be a property chain leading to the property to
147 * check, followed by the value to check against. Common example:
148 * `allThings(filter: { fields: { slug: { eq: $slug } } })`
149 * Only nodes of given node types will be considered
150 * A fast index is created if one doesn't exist yet so cold call is slower.
151 * The empty result value is null if firstOnly is false, or else an empty array.
152 *
153 * @param {Array<string>} chain Note: `eq` is assumed to be the leaf prop here
154 * @param {boolean | number | string} targetValue chain.chain.eq === targetValue
155 * @param {Array<string>} nodeTypeNames
156 * @param {undefined | Map<string, Map<string | number | boolean, Node>>} typedKeyValueIndexes
157 * @returns {Array<Node> | undefined}
158 */
159
160
161const runFlatFilterWithoutSift = (chain, targetValue, nodeTypeNames, typedKeyValueIndexes) => {
162 ensureIndexByTypedChain(chain, nodeTypeNames, typedKeyValueIndexes);
163 const nodesByKeyValue = getNodesByTypedChain(chain, targetValue, nodeTypeNames, typedKeyValueIndexes); // If we couldn't find the needle then maybe sift can, for example if the
164 // schema contained a proxy; `slug: String @proxy(from: "slugInternal")`
165 // There are also cases (and tests) where id exists with a different type
166
167 if (!nodesByKeyValue) {
168 return undefined;
169 }
170
171 if (chain.join(`,`) === `id`) {
172 // The `id` key is not indexed in Sets (because why) so don't spread it
173 return [nodesByKeyValue];
174 } // In all other cases this must be a non-empty Set because the indexing
175 // mechanism does not create a Set unless there's a Node for it
176
177
178 return [...nodesByKeyValue];
179};
180/**
181 * Filters and sorts a list of nodes using mongodb-like syntax.
182 *
183 * @param args raw graphql query filter/sort as an object
184 * @property {boolean | number | string} args.type gqlType. See build-node-types
185 * @property {boolean} args.firstOnly true if you want to return only the first
186 * result found. This will return a collection of size 1. Not a single element
187 * @property {{filter?: Object, sort?: Object} | undefined} args.queryArgs
188 * @property {undefined | Map<string, Map<string | number | boolean, Node>>} args.typedKeyValueIndexes
189 * May be undefined. A cache of indexes where you can look up Nodes grouped
190 * by a key: `types.join(',')+'/'+filterPath.join('+')`, which yields a Map
191 * which holds a Set of Nodes for the value that the filter is trying to eq
192 * against. If the property is `id` then there is no Set, it's just the Node.
193 * This object lives in query/query-runner.js and is passed down runQuery
194 * @returns Collection of results. Collection will be limited to 1
195 * if `firstOnly` is true
196 */
197
198
199const runFilterAndSort = args => {
200 const {
201 queryArgs: {
202 filter,
203 sort
204 } = {
205 filter: {},
206 sort: {}
207 },
208 resolvedFields = {},
209 firstOnly = false,
210 nodeTypeNames,
211 typedKeyValueIndexes
212 } = args;
213 let result = applyFilters(filter, firstOnly, nodeTypeNames, typedKeyValueIndexes, resolvedFields);
214 return sortNodes(result, sort, resolvedFields);
215};
216
217exports.runSift = runFilterAndSort;
218/**
219 * Applies filter. First through a simple approach, which is much faster than
220 * running sift, but not as versatile and correct. If no nodes were found then
221 * it falls back to filtering through sift.
222 *
223 * @param {Object | undefined} filter
224 * @param {boolean} firstOnly
225 * @param {Array<string>} nodeTypeNames
226 * @param {undefined | Map<string, Map<string | number | boolean, Node>>} typedKeyValueIndexes
227 * @param resolvedFields
228 * @returns {Array<Node> | undefined} Collection of results. Collection will be
229 * limited to 1 if `firstOnly` is true
230 */
231
232const applyFilters = (filter, firstOnly, nodeTypeNames, typedKeyValueIndexes, resolvedFields) => {
233 let result;
234
235 if (typedKeyValueIndexes) {
236 result = filterWithoutSift(filter, nodeTypeNames, typedKeyValueIndexes);
237
238 if (result) {
239 if (firstOnly) {
240 return result.slice(0, 1);
241 }
242
243 return result;
244 }
245 }
246
247 return filterWithSift(filter, firstOnly, nodeTypeNames, resolvedFields);
248};
249/**
250 * Check if the filter is "flat" (single leaf) and an "eq". If so, uses custom
251 * indexes based on filter and types and returns any result it finds.
252 * If conditions are not met or no nodes are found, returns undefined.
253 *
254 * @param {Object | undefined} filter
255 * @param {Array<string>} nodeTypeNames
256 * @param {undefined | Map<string, Map<string | number | boolean, Node>>} typedKeyValueIndexes
257 * @returns {Array|undefined} Collection of results
258 */
259
260
261const filterWithoutSift = (filter, nodeTypeNames, typedKeyValueIndexes) => {
262 if (!filter) {
263 return undefined;
264 } // Filter can be any struct of {a: {b: {c: {eq: "x"}}}} and we want to confirm
265 // there is exactly one leaf in this structure and that this leaf is `eq`. The
266 // actual names are irrelevant, they are a chain of props on a Node.
267
268
269 let chainWithNeedle = getFlatPropertyChain(filter);
270
271 if (!chainWithNeedle) {
272 return undefined;
273 } // `chainWithNeedle` should now be like:
274 // `filter = {this: {is: {the: {chain: {eq: needle}}}}}`
275 // ->
276 // `['this', 'is', 'the', 'chain', 'eq', needle]`
277
278
279 let targetValue = chainWithNeedle.pop();
280 let lastPath = chainWithNeedle.pop(); // This can also be `ne`, `in` or any other grapqhl comparison op
281
282 if (lastPath !== `eq`) {
283 return undefined;
284 }
285
286 return runFlatFilterWithoutSift(chainWithNeedle, targetValue, nodeTypeNames, typedKeyValueIndexes);
287};
288/**
289 * Use sift to apply filters
290 *
291 * @param {Object | undefined} filter
292 * @param {boolean} firstOnly
293 * @param {Array<string>} nodeTypeNames
294 * @param resolvedFields
295 * @returns {Array<Node> | undefined | null} Collection of results. Collection
296 * will be limited to 1 if `firstOnly` is true
297 */
298
299
300const filterWithSift = (filter, firstOnly, nodeTypeNames, resolvedFields) => {
301 let nodes = [];
302 nodeTypeNames.forEach(typeName => addResolvedNodes(typeName, nodes));
303 return _runSiftOnNodes(nodes, filter, firstOnly, nodeTypeNames, resolvedFields, siftGetNode);
304};
305/**
306 * Given a list of filtered nodes and sorting parameters, sort the nodes
307 * Note: this entry point is used by GATSBY_DB_NODES=loki
308 *
309 * @param {Array<Node>} nodes Should be all nodes of given type(s)
310 * @param args Legacy api arg, see _runSiftOnNodes
311 * @param {?function(id: string): Node} getNode
312 * @returns {Array<Node> | undefined | null} Collection of results. Collection
313 * will be limited to 1 if `firstOnly` is true
314 */
315
316
317const runSiftOnNodes = (nodes, args, getNode = siftGetNode) => {
318 const {
319 queryArgs: {
320 filter
321 } = {
322 filter: {}
323 },
324 firstOnly = false,
325 resolvedFields = {},
326 nodeTypeNames
327 } = args;
328 return _runSiftOnNodes(nodes, filter, firstOnly, nodeTypeNames, resolvedFields, getNode);
329};
330
331exports.runSiftOnNodes = runSiftOnNodes;
332/**
333 * Given a list of filtered nodes and sorting parameters, sort the nodes
334 *
335 * @param {Array<Node>} nodes Should be all nodes of given type(s)
336 * @param {Object | undefined} filter
337 * @param {boolean} firstOnly
338 * @param {Array<string>} nodeTypeNames
339 * @param resolvedFields
340 * @param {function(id: string): Node} getNode Note: this is different for loki
341 * @returns {Array<Node> | undefined | null} Collection of results. Collection
342 * will be limited to 1 if `firstOnly` is true
343 */
344
345const _runSiftOnNodes = (nodes, filter, firstOnly, nodeTypeNames, resolvedFields, getNode) => {
346 let siftFilter = getFilters(liftResolvedFields(toDottedFields(prepareQueryArgs(filter)), resolvedFields)); // If the the query for single node only has a filter for an "id"
347 // using "eq" operator, then we'll just grab that ID and return it.
348
349 if (isEqId(siftFilter)) {
350 const node = getNode(siftFilter[0].id.$eq);
351
352 if (!node || node.internal && !nodeTypeNames.includes(node.internal.type)) {
353 if (firstOnly) {
354 return [];
355 }
356
357 return null;
358 }
359
360 return [node];
361 }
362
363 if (firstOnly) {
364 return handleFirst(siftFilter, nodes);
365 } else {
366 return handleMany(siftFilter, nodes);
367 }
368};
369/**
370 * Given a list of filtered nodes and sorting parameters, sort the nodes
371 *
372 * @param {Array<Node> | undefined | null} nodes Pre-filtered list of nodes
373 * @param {Object | undefined} sort Sorting arguments
374 * @param resolvedFields
375 * @returns {Array<Node> | undefined | null} Same as input, except sorted
376 */
377
378
379const sortNodes = (nodes, sort, resolvedFields) => {
380 if (!sort || (nodes === null || nodes === void 0 ? void 0 : nodes.length) <= 1) {
381 return nodes;
382 } // create functions that return the item to compare on
383
384
385 const dottedFields = objectToDottedField(resolvedFields);
386 const dottedFieldKeys = Object.keys(dottedFields);
387 const sortFields = sort.fields.map(field => {
388 if (dottedFields[field] || dottedFieldKeys.some(key => field.startsWith(key))) {
389 return `__gatsby_resolved.${field}`;
390 } else {
391 return field;
392 }
393 }).map(field => v => getValueAt(v, field));
394 const sortOrder = sort.order.map(order => order.toLowerCase());
395 return _.orderBy(nodes, sortFields, sortOrder);
396};
397//# sourceMappingURL=run-sift.js.map
\No newline at end of file