@akemona-org/strapi-connector-mongoose 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,748 @@
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const { isNil, isEmpty, set, omit, assoc } = require('lodash/fp');
5
+ const semver = require('semver');
6
+ const {
7
+ hasDeepFilters,
8
+ contentTypes: {
9
+ constants: { DP_PUB_STATES },
10
+ hasDraftAndPublish,
11
+ },
12
+ } = require('@akemona-org/strapi-utils');
13
+ const utils = require('./utils')();
14
+ const populateQueries = require('./utils/populate-queries');
15
+
16
+ const sortOrderMapper = {
17
+ asc: 1,
18
+ desc: -1,
19
+ };
20
+
21
+ const combineSearchAndWhere = (search = [], wheres = []) => {
22
+ const criterias = {};
23
+ if (search.length > 0 && wheres.length > 0) {
24
+ criterias.$and = [{ $and: wheres }, { $or: search }];
25
+ } else if (search.length > 0) {
26
+ criterias.$or = search;
27
+ } else if (wheres.length > 0) {
28
+ criterias.$and = wheres;
29
+ }
30
+ return criterias;
31
+ };
32
+
33
+ const buildSearchOr = (model, query) => {
34
+ if (typeof query !== 'string') {
35
+ return [];
36
+ }
37
+
38
+ const searchOr = Object.keys(model.attributes).reduce((acc, curr) => {
39
+ if (model.attributes[curr].searchable === false) {
40
+ return acc;
41
+ }
42
+ switch (model.attributes[curr].type) {
43
+ case 'biginteger':
44
+ case 'integer':
45
+ case 'float':
46
+ case 'decimal':
47
+ if (!_.isNaN(_.toNumber(query))) {
48
+ const mongoVersion = model.db.base.mongoDBVersion;
49
+ if (semver.valid(mongoVersion) && semver.gt(mongoVersion, '4.2.0')) {
50
+ return acc.concat({
51
+ $expr: {
52
+ $regexMatch: {
53
+ input: { $toString: `$${curr}` },
54
+ regex: _.escapeRegExp(query),
55
+ },
56
+ },
57
+ });
58
+ } else {
59
+ return acc.concat({ [curr]: _.toNumber(query) });
60
+ }
61
+ }
62
+ return acc;
63
+ case 'string':
64
+ case 'text':
65
+ case 'richtext':
66
+ case 'email':
67
+ case 'enumeration':
68
+ case 'uid':
69
+ return acc.concat({ [curr]: { $regex: _.escapeRegExp(query), $options: 'i' } });
70
+ default:
71
+ return acc;
72
+ }
73
+ }, []);
74
+
75
+ if (utils.isMongoId(query)) {
76
+ searchOr.push({ _id: query });
77
+ }
78
+
79
+ return searchOr;
80
+ };
81
+
82
+ const BOOLEAN_OPERATORS = ['or'];
83
+
84
+ /**
85
+ * Build a mongo query
86
+ * @param {Object} options - Query options
87
+ * @param {Object} options.model - The model you are querying
88
+ * @param {Object} options.filters - An object with the possible filters (start, limit, sort, where)
89
+ * @param {Object} options.populate - An array of paths to populate
90
+ * @param {boolean} options.aggregate - Force aggregate function to use group by feature
91
+ */
92
+ const buildQuery = ({
93
+ model,
94
+ filters = {},
95
+ searchParam,
96
+ populate = [],
97
+ aggregate = false,
98
+ session = null,
99
+ } = {}) => {
100
+ const search = buildSearchOr(model, searchParam);
101
+
102
+ if (!hasDeepFilters(filters) && aggregate === false) {
103
+ return buildSimpleQuery({ model, filters, search, populate }, { session });
104
+ }
105
+
106
+ return buildDeepQuery({ model, filters, populate, search }, { session });
107
+ };
108
+
109
+ /**
110
+ * Builds a simple find query when there are no deep filters
111
+ * @param {Object} options - Query options
112
+ * @param {Object} options.model - The model you are querying
113
+ * @param {Object} options.filters - An object with the possible filters (start, limit, sort, where)
114
+ * @param {Object} options.search - An object with the possible search params
115
+ * @param {Object} options.populate - An array of paths to populate
116
+ */
117
+ const buildSimpleQuery = ({ model, filters, search, populate }, { session }) => {
118
+ const { where = [] } = filters;
119
+
120
+ const wheres = where.map(buildWhereClause);
121
+
122
+ const findCriteria = combineSearchAndWhere(search, wheres);
123
+
124
+ let query = model
125
+ .find(findCriteria, null, { publicationState: filters.publicationState })
126
+ .session(session)
127
+ .populate(populate);
128
+
129
+ query = applyQueryParams({ model, query, filters });
130
+
131
+ return Object.assign(query, {
132
+ // Override count to use countDocuments on simple find query
133
+ count(...args) {
134
+ return query.countDocuments(...args);
135
+ },
136
+ });
137
+ };
138
+
139
+ /**
140
+ * Builds a deep aggregate query when there are deep filters
141
+ * @param {Object} options - Query options
142
+ * @param {Object} options.model - The model you are querying
143
+ * @param {Object} options.filters - An object with the possible filters (start, limit, sort, where)
144
+ * @param {Object} options.populate - An array of paths to populate
145
+ */
146
+ const buildDeepQuery = ({ model, filters, search, populate }, { session }) => {
147
+ // Build a tree of paths to populate based on the filtering and the populate option
148
+ const { populatePaths, wherePaths } = computePopulatedPaths({
149
+ model,
150
+ populate,
151
+ where: filters.where,
152
+ });
153
+
154
+ const aggregateOptions = {
155
+ paths: _.merge({}, populatePaths, wherePaths),
156
+ };
157
+
158
+ // Init the query
159
+ let query = model
160
+ .aggregate(buildQueryAggregate(model, filters, aggregateOptions))
161
+ .session(session)
162
+ .append(buildQueryMatches(model, filters, search))
163
+ .append(buildQuerySort(model, filters))
164
+ .append(buildQueryPagination(model, filters));
165
+
166
+ return {
167
+ /**
168
+ * Overrides the promise to rehydrate mongoose docs after the aggregation query
169
+ */
170
+ then(...args) {
171
+ return query
172
+ .append({ $project: { _id: true } })
173
+ .then((results) => results.map((el) => el._id))
174
+ .then((ids) => {
175
+ if (ids.length === 0) return [];
176
+
177
+ const idsMap = ids.reduce((acc, id, idx) => assoc(id, idx, acc), {});
178
+
179
+ const mongooseQuery = model
180
+ .find({ _id: { $in: ids } }, null)
181
+ .session(session)
182
+ .populate(populate);
183
+ const query = applyQueryParams({
184
+ model,
185
+ query: mongooseQuery,
186
+ filters: omit(['sort', 'start', 'limit'], filters),
187
+ });
188
+
189
+ return query.then(orderByIndexMap(idsMap));
190
+ })
191
+ .then(...args);
192
+ },
193
+ catch(...args) {
194
+ return this.then((r) => r).catch(...args);
195
+ },
196
+ /**
197
+ * Maps to query.count
198
+ */
199
+ count() {
200
+ return query.count('count').then((results) => _.get(results, ['0', 'count'], 0));
201
+ },
202
+
203
+ /**
204
+ * Maps to query group
205
+ */
206
+ group(...args) {
207
+ return query.group(...args);
208
+ },
209
+ /**
210
+ * Returns an array of plain JS object instead of mongoose documents
211
+ */
212
+ lean() {
213
+ // Returns plain js objects without the transformations we normally do on find
214
+ return this.then((results) => {
215
+ return results.map((r) => r.toObject({ transform: false }));
216
+ });
217
+ },
218
+ };
219
+ };
220
+
221
+ /**
222
+ * Apply sort limit and start params
223
+ * @param {Object} options - Options
224
+ * @param {Object} options.query - Mongoose query
225
+ * @param {Object} options.filters - Filters object
226
+ */
227
+ const applyQueryParams = ({ model, query, filters }) => {
228
+ if (_.has(filters, 'sort')) {
229
+ const sortFilter = filters.sort.reduce((acc, sort) => {
230
+ const { field, order } = sort;
231
+ acc[field] = sortOrderMapper[order];
232
+ return acc;
233
+ }, {});
234
+
235
+ query = query.sort(sortFilter);
236
+ }
237
+
238
+ // Apply start param
239
+ if (_.has(filters, 'start')) {
240
+ query = query.skip(filters.start);
241
+ }
242
+
243
+ // Apply limit param
244
+ if (_.has(filters, 'limit') && filters.limit >= 0) {
245
+ query = query.limit(filters.limit);
246
+ }
247
+
248
+ // Apply publication state param
249
+ if (_.has(filters, 'publicationState')) {
250
+ const populateQuery = populateQueries.publicationState[filters.publicationState];
251
+
252
+ if (hasDraftAndPublish(model) && populateQuery) {
253
+ query = query.where(populateQuery);
254
+ }
255
+ }
256
+
257
+ return query;
258
+ };
259
+
260
+ /**
261
+ * Returns a tree of the paths to populate both for population and deep filtering purposes
262
+ * @param {Object} options - Options
263
+ * @param {Object} options.model - Model from which to populate
264
+ * @param {Object} options.populate - Paths to populate
265
+ * @param {Object} options.where - Where clauses we need to populate to filters
266
+ */
267
+ const computePopulatedPaths = ({ model, populate = [], where = [] }) => {
268
+ const castedPopulatePaths = populate
269
+ .map((el) => (Array.isArray(el) ? el.join('.') : el))
270
+ .map((path) => findModelPath({ rootModel: model, path }))
271
+ .map((path) => {
272
+ const assocModel = findModelByPath({ rootModel: model, path });
273
+
274
+ // autoload morph relations
275
+ let extraPaths = [];
276
+ if (assocModel) {
277
+ extraPaths = assocModel.associations
278
+ .filter((assoc) => assoc.nature.toLowerCase().indexOf('morph') !== -1)
279
+ .map((assoc) => `${path}.${assoc.alias}`);
280
+ }
281
+
282
+ return [path, ...extraPaths];
283
+ })
284
+ .reduce((acc, paths) => acc.concat(paths), []);
285
+
286
+ const castedWherePaths = recursiveCastedWherePaths(where, { model });
287
+
288
+ return {
289
+ populatePaths: pathsToTree(castedPopulatePaths),
290
+ wherePaths: pathsToTree(castedWherePaths),
291
+ };
292
+ };
293
+
294
+ const recursiveCastedWherePaths = (whereClauses, { model }) => {
295
+ const paths = whereClauses.map(({ field, operator, value }) => {
296
+ if (BOOLEAN_OPERATORS.includes(operator)) {
297
+ return value.map((where) => recursiveCastedWherePaths(where, { model }));
298
+ }
299
+
300
+ return findModelPath({ rootModel: model, path: field });
301
+ });
302
+
303
+ return _.flattenDeep(paths).filter((path) => !!path);
304
+ };
305
+
306
+ /**
307
+ * Builds an object based on paths:
308
+ * [
309
+ * 'articles',
310
+ * 'articles.tags.category',
311
+ * 'articles.tags.label',
312
+ * ] => {
313
+ * articles: {
314
+ * tags: {
315
+ * category: {},
316
+ * label: {}
317
+ * }
318
+ * }
319
+ * }
320
+ * @param {Array<string>} paths - A list of paths to transform
321
+ */
322
+ const pathsToTree = (paths) => paths.reduce((acc, path) => _.merge(acc, _.set({}, path, {})), {});
323
+
324
+ /**
325
+ * Builds the aggregations pipeline of the query
326
+ * @param {Object} model - Queried model
327
+ * @param {Object} filters - The query filters
328
+ * @param {Object} options - Options
329
+ * @param {Object} options.paths - A tree of paths to aggregate e.g { article : { tags : { label: {}}}}
330
+ */
331
+ const buildQueryAggregate = (model, filters, { paths } = {}) => {
332
+ return Object.keys(paths).reduce((acc, key) => {
333
+ return acc.concat(buildLookup({ model, key, paths: paths[key], filters }));
334
+ }, []);
335
+ };
336
+
337
+ /**
338
+ * Builds a lookup aggregation for a specific key
339
+ * @param {Object} options - Options
340
+ * @param {Object} options.model - Queried model
341
+ * @param {string} options.key - The attribute name to lookup on the model
342
+ * @param {Object} options.paths - A tree of paths to aggregate inside the current lookup e.g { { tags : { label: {}}}
343
+ */
344
+ const buildLookup = ({ model, key, paths, filters }) => {
345
+ const assoc = model.associations.find((a) => a.alias === key);
346
+ const assocModel = strapi.db.getModelByAssoc(assoc);
347
+
348
+ if (!assocModel) return [];
349
+
350
+ return [
351
+ {
352
+ $lookup: {
353
+ from: assocModel.collectionName,
354
+ as: assoc.alias,
355
+ let: {
356
+ localId: '$_id',
357
+ localAlias: `$${assoc.alias}`,
358
+ },
359
+ pipeline: []
360
+ .concat(buildLookupMatch({ assoc, assocModel, filters }))
361
+ .concat(buildQueryAggregate(assocModel, filters, { paths })),
362
+ },
363
+ },
364
+ ];
365
+ };
366
+
367
+ /**
368
+ * Build a lookup match expression (equivalent to a SQL join condition)
369
+ * @param {Object} options - Options
370
+ * @param {Object} options.assoc - The association on which is based the matching expression
371
+ */
372
+ const buildLookupMatch = ({ assoc, assocModel, filters = {} }) => {
373
+ const defaultMatches = [];
374
+
375
+ if (hasDraftAndPublish(assocModel) && DP_PUB_STATES.includes(filters.publicationState)) {
376
+ const dpQuery = populateQueries.publicationState[filters.publicationState];
377
+
378
+ if (_.isObject(dpQuery)) {
379
+ defaultMatches.push(dpQuery);
380
+ }
381
+ }
382
+
383
+ switch (assoc.nature) {
384
+ case 'oneToOne': {
385
+ return [
386
+ {
387
+ $match: {
388
+ $and: defaultMatches.concat({
389
+ $expr: {
390
+ $eq: [`$${assoc.via}`, '$$localId'],
391
+ },
392
+ }),
393
+ },
394
+ },
395
+ ];
396
+ }
397
+ case 'oneToMany': {
398
+ return {
399
+ $match: {
400
+ $and: defaultMatches.concat({
401
+ $expr: {
402
+ $eq: [`$${assoc.via}`, '$$localId'],
403
+ },
404
+ }),
405
+ },
406
+ };
407
+ }
408
+ case 'oneWay':
409
+ case 'manyToOne': {
410
+ return {
411
+ $match: {
412
+ $and: defaultMatches.concat({
413
+ $expr: {
414
+ $eq: ['$$localAlias', '$_id'],
415
+ },
416
+ }),
417
+ },
418
+ };
419
+ }
420
+ case 'manyWay': {
421
+ return {
422
+ $match: {
423
+ $and: defaultMatches.concat({
424
+ $expr: {
425
+ $in: ['$_id', { $ifNull: ['$$localAlias', []] }],
426
+ },
427
+ }),
428
+ },
429
+ };
430
+ }
431
+ case 'manyToMany': {
432
+ if (assoc.dominant === true) {
433
+ return {
434
+ $match: {
435
+ $and: defaultMatches.concat({
436
+ $expr: {
437
+ $in: ['$_id', { $ifNull: ['$$localAlias', []] }],
438
+ },
439
+ }),
440
+ },
441
+ };
442
+ }
443
+
444
+ return {
445
+ $match: {
446
+ $and: defaultMatches.concat({
447
+ $expr: {
448
+ $in: ['$$localId', { $ifNull: [`$${assoc.via}`, []] }],
449
+ },
450
+ }),
451
+ },
452
+ };
453
+ }
454
+ case 'manyToManyMorph':
455
+ case 'oneToManyMorph': {
456
+ return [
457
+ {
458
+ $unwind: { path: `$${assoc.via}`, preserveNullAndEmptyArrays: true },
459
+ },
460
+ {
461
+ $match: {
462
+ $and: defaultMatches.concat({
463
+ $expr: {
464
+ $and: [
465
+ { $eq: [`$${assoc.via}.ref`, '$$localId'] },
466
+ { $eq: [`$${assoc.via}.${assoc.filter}`, assoc.alias] },
467
+ ],
468
+ },
469
+ }),
470
+ },
471
+ },
472
+ ];
473
+ }
474
+ default:
475
+ return [];
476
+ }
477
+ };
478
+
479
+ /**
480
+ * Match query for lookups
481
+ * @param {Object} model - Mongoose model
482
+ * @param {Object} filters - Filters object
483
+ * @param {Array} search
484
+ */
485
+ const buildQueryMatches = (model, filters, search = []) => {
486
+ if (_.has(filters, 'where') && Array.isArray(filters.where)) {
487
+ const wheres = filters.where.map((whereClause) => {
488
+ return buildWhereClause(formatWhereClause(model, whereClause));
489
+ });
490
+
491
+ const criterias = combineSearchAndWhere(search, wheres);
492
+
493
+ return [{ $match: criterias }];
494
+ }
495
+
496
+ return [];
497
+ };
498
+
499
+ /**
500
+ * Sort query for the aggregate
501
+ * @param {Object} model - Mongoose model
502
+ * @param {Object} filters - Filters object
503
+ */
504
+ const buildQuerySort = (model, filters) => {
505
+ const { sort } = filters;
506
+
507
+ if (Array.isArray(sort) && !isEmpty(sort)) {
508
+ return [
509
+ {
510
+ $sort: sort.reduce(
511
+ (acc, { field, order }) => set([field], sortOrderMapper[order], acc),
512
+ {}
513
+ ),
514
+ },
515
+ ];
516
+ }
517
+
518
+ return [];
519
+ };
520
+
521
+ /**
522
+ * Add pagination operators for the aggregate
523
+ * @param {Object} model - Mongoose model
524
+ * @param {Object} filters - Filters object
525
+ */
526
+ const buildQueryPagination = (model, filters) => {
527
+ const { limit, start } = filters;
528
+ const pagination = [];
529
+
530
+ if (start && start >= 0) {
531
+ pagination.push({ $skip: start });
532
+ }
533
+
534
+ if (limit && limit >= 0) {
535
+ pagination.push({ $limit: limit });
536
+ }
537
+
538
+ return pagination;
539
+ };
540
+
541
+ /**
542
+ * Cast values
543
+ * @param {*} value - Value to cast
544
+ */
545
+ const formatValue = (value) => utils.valueToId(value);
546
+
547
+ /**
548
+ * Builds a where clause
549
+ * @param {Object} options - Options
550
+ * @param {string} options.field - Where clause field
551
+ * @param {string} options.operator - Where clause operator
552
+ * @param {*} options.value - Where clause alue
553
+ */
554
+ const buildWhereClause = ({ field, operator, value }) => {
555
+ if (Array.isArray(value) && !['or', 'in', 'nin'].includes(operator)) {
556
+ return {
557
+ $or: value.map((val) => buildWhereClause({ field, operator, value: val })),
558
+ };
559
+ }
560
+
561
+ const val = formatValue(value);
562
+
563
+ switch (operator) {
564
+ case 'or': {
565
+ return {
566
+ $or: value.map((orClause) => {
567
+ if (Array.isArray(orClause)) {
568
+ return {
569
+ $and: orClause.map(buildWhereClause),
570
+ };
571
+ } else {
572
+ return buildWhereClause(orClause);
573
+ }
574
+ }),
575
+ };
576
+ }
577
+ case 'eq':
578
+ return { [field]: val };
579
+ case 'ne':
580
+ return { [field]: { $ne: val } };
581
+ case 'lt':
582
+ return { [field]: { $lt: val } };
583
+ case 'lte':
584
+ return { [field]: { $lte: val } };
585
+ case 'gt':
586
+ return { [field]: { $gt: val } };
587
+ case 'gte':
588
+ return { [field]: { $gte: val } };
589
+ case 'in':
590
+ return {
591
+ [field]: {
592
+ $in: Array.isArray(val) ? val : [val],
593
+ },
594
+ };
595
+ case 'nin':
596
+ return {
597
+ [field]: {
598
+ $nin: Array.isArray(val) ? val : [val],
599
+ },
600
+ };
601
+ case 'contains': {
602
+ return {
603
+ [field]: {
604
+ $regex: _.escapeRegExp(`${val}`),
605
+ $options: 'i',
606
+ },
607
+ };
608
+ }
609
+ case 'ncontains':
610
+ return {
611
+ [field]: {
612
+ $not: new RegExp(val, 'i'),
613
+ },
614
+ };
615
+ case 'containss':
616
+ return {
617
+ [field]: {
618
+ $regex: _.escapeRegExp(`${val}`),
619
+ },
620
+ };
621
+ case 'ncontainss':
622
+ return {
623
+ [field]: {
624
+ $not: new RegExp(val),
625
+ },
626
+ };
627
+ case 'null': {
628
+ return value ? { [field]: { $eq: null } } : { [field]: { $ne: null } };
629
+ }
630
+
631
+ default:
632
+ throw new Error(`Unhandled whereClause : ${field} ${operator} ${value}`);
633
+ }
634
+ };
635
+
636
+ /**
637
+ * Add primaryKey on relation where clause for lookups match
638
+ * @param {Object} model - Mongoose model
639
+ * @param {Object} whereClause - Where clause
640
+ * @param {string} whereClause.field - Where clause field
641
+ * @param {string} whereClause.operator - Where clause operator
642
+ * @param {*} whereClause.value - Where clause alue
643
+ */
644
+ const formatWhereClause = (model, { field, operator, value }) => {
645
+ if (BOOLEAN_OPERATORS.includes(operator)) {
646
+ return {
647
+ field,
648
+ operator,
649
+ value: value.map((v) => v.map((whereClause) => formatWhereClause(model, whereClause))),
650
+ };
651
+ }
652
+
653
+ const { assoc, model: assocModel } = getAssociationFromFieldKey(model, field);
654
+
655
+ const shouldFieldBeSuffixed =
656
+ assoc &&
657
+ !_.endsWith(field, assocModel.primaryKey) &&
658
+ (['in', 'nin'].includes(operator) || // When using in or nin operators we want to apply the filter on the relation's primary key and not the relation itself
659
+ (['eq', 'ne'].includes(operator) && utils.isMongoId(value))); // Only suffix the field if the operators are eq or ne and the value is a valid mongo id
660
+
661
+ return {
662
+ field: shouldFieldBeSuffixed ? `${field}.${assocModel.primaryKey}` : field,
663
+ operator,
664
+ value,
665
+ };
666
+ };
667
+
668
+ /**
669
+ * Returns an association from a path starting from model
670
+ * @param {Object} model - Mongoose model
671
+ * @param {string} fieldKey - Relation path
672
+ */
673
+ const getAssociationFromFieldKey = (model, fieldKey) => {
674
+ let tmpModel = model;
675
+ let assoc;
676
+
677
+ const parts = fieldKey.split('.');
678
+
679
+ for (let key of parts) {
680
+ assoc = tmpModel.associations.find((ast) => ast.alias === key);
681
+ if (assoc) {
682
+ tmpModel = strapi.db.getModelByAssoc(assoc);
683
+ }
684
+ }
685
+
686
+ return {
687
+ assoc,
688
+ model: tmpModel,
689
+ };
690
+ };
691
+
692
+ /**
693
+ * Returns a model from a relation path and a root model
694
+ * @param {Object} options - Options
695
+ * @param {Object} options.rootModel - Mongoose model
696
+ * @param {string} options.path - Relation path
697
+ */
698
+ const findModelByPath = ({ rootModel, path }) => {
699
+ const parts = path.split('.');
700
+
701
+ let tmpModel = rootModel;
702
+ for (let part of parts) {
703
+ const assoc = tmpModel.associations.find((ast) => ast.alias === part);
704
+ if (assoc) {
705
+ tmpModel = strapi.db.getModelByAssoc(assoc);
706
+ }
707
+ }
708
+
709
+ return tmpModel;
710
+ };
711
+
712
+ /**
713
+ * Returns a model path from an attribute path and a root model
714
+ * @param {Object} options - Options
715
+ * @param {Object} options.rootModel - Mongoose model
716
+ * @param {string|Object} options.path - Attribute path
717
+ */
718
+ const findModelPath = ({ rootModel, path }) => {
719
+ const parts = (_.isObject(path) ? path.path : path).split('.');
720
+
721
+ let tmpModel = rootModel;
722
+ let tmpPath = [];
723
+ for (let part of parts) {
724
+ const assoc = tmpModel.associations.find((ast) => ast.alias === part);
725
+
726
+ if (assoc) {
727
+ tmpModel = strapi.db.getModelByAssoc(assoc);
728
+ tmpPath.push(part);
729
+ }
730
+ }
731
+
732
+ return tmpPath.length > 0 ? tmpPath.join('.') : null;
733
+ };
734
+
735
+ /**
736
+ * Order a list of entities based on an indexMap
737
+ * @param {Object} indexMap - index map of the form { [id]: index }
738
+ */
739
+ const orderByIndexMap = (indexMap) => (entities) => {
740
+ return entities
741
+ .reduce((acc, entry) => {
742
+ acc[indexMap[entry._id]] = entry;
743
+ return acc;
744
+ }, [])
745
+ .filter((entity) => !isNil(entity));
746
+ };
747
+
748
+ module.exports = buildQuery;