@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.
- package/LICENSE +22 -0
- package/README.md +37 -0
- package/lib/buildQuery.js +748 -0
- package/lib/get-query-params.js +74 -0
- package/lib/index.js +231 -0
- package/lib/migrations/draft-publish.js +79 -0
- package/lib/migrations/index.js +7 -0
- package/lib/mount-models.js +566 -0
- package/lib/queries.js +649 -0
- package/lib/relations.js +510 -0
- package/lib/utils/connectivity.js +42 -0
- package/lib/utils/errors.js +17 -0
- package/lib/utils/helpers.js +11 -0
- package/lib/utils/index.js +128 -0
- package/lib/utils/populate-queries.js +14 -0
- package/lib/utils/store-definition.js +67 -0
- package/package.json +58 -0
|
@@ -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;
|