1 | ;
|
2 |
|
3 | const {
|
4 | default: sift
|
5 | } = require(`sift`);
|
6 |
|
7 | const _ = require(`lodash`);
|
8 |
|
9 | const prepareRegex = require(`../utils/prepare-regex`);
|
10 |
|
11 | const {
|
12 | makeRe
|
13 | } = require(`micromatch`);
|
14 |
|
15 | const {
|
16 | getValueAt
|
17 | } = require(`../utils/get-value-at`);
|
18 |
|
19 | const {
|
20 | toDottedFields,
|
21 | objectToDottedField,
|
22 | liftResolvedFields
|
23 | } = require(`../db/common/query`);
|
24 |
|
25 | const {
|
26 | ensureIndexByTypedChain,
|
27 | getNodesByTypedChain,
|
28 | addResolvedNodes,
|
29 | getNode: siftGetNode
|
30 | } = require(`./nodes`); /////////////////////////////////////////////////////////////////////
|
31 | // Parse filter
|
32 | /////////////////////////////////////////////////////////////////////
|
33 |
|
34 |
|
35 | const 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 |
|
58 | const getFilters = filters => Object.keys(filters).reduce((acc, key) => acc.push({
|
59 | [key]: filters[key]
|
60 | }) && acc, []); /////////////////////////////////////////////////////////////////////
|
61 | // Run Sift
|
62 | /////////////////////////////////////////////////////////////////////
|
63 |
|
64 |
|
65 | function 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 |
|
70 | function 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 |
|
86 | function 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 |
|
109 | const 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 |
|
161 | const 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 |
|
199 | const 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 |
|
217 | exports.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 |
|
232 | const 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 |
|
261 | const 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 |
|
300 | const 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 |
|
317 | const 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 |
|
331 | exports.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 |
|
345 | const _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 |
|
379 | const 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 |