@aeriajs/core 0.0.139 → 0.0.141

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.
@@ -2,76 +2,23 @@
2
2
  import { throwIfError, getReferenceProperty } from "@aeriajs/common";
3
3
  import { getCollectionAsset } from "../assets.mjs";
4
4
  import { prepareCollectionName } from "../database.mjs";
5
+ const getTempName = (path) => {
6
+ return `_${path.join("_")}`;
7
+ };
5
8
  const referenceMemo = {};
6
9
  const lookupMemo = {};
7
- const narrowLookupPipelineProjection = (pipeline, projection) => {
8
- const hasAny = (propName) => {
9
- return propName.includes(".") || projection.includes(propName);
10
- };
11
- return pipeline.filter((stage) => {
12
- if (stage.$lookup) {
13
- return hasAny(stage.$lookup.as);
14
- }
15
- if (stage.$unwind) {
16
- return hasAny(stage.$unwind.path.slice(1));
17
- }
18
- return true;
19
- });
20
- };
21
- const buildGroupStages = (referenceMap, properties) => {
22
- const $group = Object.keys(properties).reduce((a, propName) => {
23
- const refMap = referenceMap[propName] || {};
24
- const groupType = !refMap.referencedCollection && refMap.isArray ? "push" : "first";
25
- return {
26
- ...a,
27
- [propName]: {
28
- [`$${groupType}`]: `$${propName}`
29
- }
30
- };
31
- }, {
32
- _id: "$_id"
33
- });
34
- return {
35
- $group
36
- };
37
- };
38
- const buildArrayCleanupStages = (referenceMap) => {
39
- const $set = Object.entries(referenceMap).reduce((a, [refName, refMap]) => {
40
- if (!refMap.isArray || refMap.referencedCollection) {
41
- return a;
42
- }
43
- return {
44
- ...a,
45
- [refName]: {
46
- $filter: {
47
- input: `$${refName}`,
48
- as: `${refName}_elem`,
49
- cond: {
50
- $ne: [
51
- `$$${refName}_elem`,
52
- {}
53
- ]
54
- }
55
- }
56
- }
57
- };
58
- }, {});
59
- return Object.keys($set).length > 0 ? {
60
- $set
61
- } : null;
62
- };
63
- export const getReferences = async (properties, options) => {
10
+ export const getReferences = async (properties, options = {}) => {
64
11
  const {
65
12
  depth = 0,
66
13
  maxDepth = 3,
67
14
  memoize
68
- } = options || {};
15
+ } = options;
69
16
  if (memoize) {
70
17
  if (referenceMemo[memoize]) {
71
18
  return referenceMemo[memoize];
72
19
  }
73
20
  }
74
- const references = {};
21
+ const refMap = {};
75
22
  for (const [propName, property] of Object.entries(properties)) {
76
23
  const refProperty = getReferenceProperty(property);
77
24
  const reference = {};
@@ -108,6 +55,9 @@ export const getReferences = async (properties, options) => {
108
55
  if ("items" in property) {
109
56
  reference.isArray = true;
110
57
  }
58
+ if (depth > 0) {
59
+ reference.isRecursive = true;
60
+ }
111
61
  if (refProperty) {
112
62
  if (refProperty.$ref) {
113
63
  reference.referencedCollection = refProperty.$ref;
@@ -116,180 +66,255 @@ export const getReferences = async (properties, options) => {
116
66
  reference.isInline = true;
117
67
  }
118
68
  }
119
- references[propName] = reference;
69
+ refMap[propName] = reference;
120
70
  }
121
71
  if (memoize) {
122
- referenceMemo[memoize] = references;
72
+ referenceMemo[memoize] = refMap;
123
73
  }
124
- return references;
74
+ return refMap;
125
75
  };
126
- const buildLookupStages = async (reference, propName, options) => {
127
- const {
128
- parent,
129
- properties,
130
- depth = 0,
131
- maxDepth = 3
132
- } = options;
133
- const stages = [];
134
- let refHasDeepReferences = false;
135
- const withParent = (propName2) => {
136
- return parent ? `${parent}.${propName2}` : propName2;
137
- };
138
- if (reference.referencedCollection) {
139
- if (!reference.populatedProperties) {
140
- stages.push({
141
- $lookup: {
142
- from: prepareCollectionName(reference.referencedCollection),
143
- foreignField: "_id",
144
- localField: withParent(propName),
145
- as: withParent(propName)
146
- }
147
- });
148
- } else {
149
- const subPipeline = [];
150
- if (reference.deepReferences) {
151
- const subProperties = throwIfError(await getCollectionAsset(reference.referencedCollection, "description")).properties;
152
- subPipeline.push(...await buildLookupPipeline(reference.deepReferences, {
153
- project: reference.populatedProperties,
154
- properties: subProperties
155
- }));
156
- }
157
- if (reference.populatedProperties.length > 0) {
158
- subPipeline.push({
159
- $project: Object.fromEntries(reference.populatedProperties.map((index) => [
160
- index,
161
- 1
162
- ]))
163
- });
164
- }
165
- stages.push({
166
- $lookup: {
167
- from: prepareCollectionName(reference.referencedCollection),
168
- let: {
169
- "ids": !reference.isArray ? `$${withParent(propName)}` : {
170
- $ifNull: [
171
- `$${withParent(propName)}`,
172
- []
76
+ export const recurseSetStage = (reference, path, parentElem, options = {
77
+ noCond: false
78
+ }) => {
79
+ const refName = path.at(-1);
80
+ let indexOfArray;
81
+ if (reference.isRecursive && !reference.isArrayElement) {
82
+ indexOfArray = {
83
+ $indexOfArray: [
84
+ `$${getTempName(path)}._id`,
85
+ {
86
+ $arrayElemAt: [
87
+ `$${getTempName(path.slice(0, -1))}.${refName}`,
88
+ {
89
+ $indexOfArray: [
90
+ `$${getTempName(path.slice(0, -1))}._id`,
91
+ parentElem
173
92
  ]
174
93
  }
175
- },
176
- as: withParent(propName),
177
- pipeline: [
178
- {
179
- $match: {
180
- $expr: {
181
- [reference.isArray ? "$in" : "$eq"]: [
182
- "$_id",
183
- "$$ids"
184
- ]
185
- }
186
- }
187
- },
188
- ...subPipeline
189
94
  ]
190
95
  }
191
- });
96
+ ]
97
+ };
98
+ } else {
99
+ indexOfArray = {
100
+ $indexOfArray: [
101
+ `$${getTempName(path)}._id`,
102
+ parentElem
103
+ ]
104
+ };
105
+ }
106
+ if (reference.isArray) {
107
+ const newElemName = `${refName}__elem`;
108
+ let mapIn;
109
+ if (reference.referencedCollection) {
110
+ mapIn = recurseSetStage({
111
+ ...reference,
112
+ isArray: false,
113
+ isArrayElement: true
114
+ }, path, `$$${newElemName}`);
115
+ } else {
116
+ mapIn = {
117
+ $mergeObjects: [
118
+ `$$${newElemName}`,
119
+ recurseSetStage({
120
+ ...reference,
121
+ isArray: false,
122
+ isArrayElement: true
123
+ }, path, `$$${newElemName}`)
124
+ ]
125
+ };
192
126
  }
193
- if (!reference.isArray) {
194
- stages.push({
195
- $unwind: {
196
- path: `$${withParent(propName)}`,
197
- preserveNullAndEmptyArrays: true
198
- }
199
- });
127
+ let mapInput = parentElem;
128
+ if (reference.isRecursive) {
129
+ mapInput = {
130
+ $arrayElemAt: [
131
+ `$${getTempName(path.slice(0, -1))}.${refName}`,
132
+ indexOfArray
133
+ ]
134
+ };
200
135
  }
201
- } else if (reference.deepReferences && depth <= maxDepth) {
202
- refHasDeepReferences = true;
203
- if (reference.isArray) {
204
- stages.push({
205
- $unwind: {
206
- path: `$${withParent(propName)}`,
207
- preserveNullAndEmptyArrays: true
136
+ return {
137
+ $filter: {
138
+ input: {
139
+ $map: {
140
+ input: mapInput,
141
+ as: newElemName,
142
+ in: mapIn
143
+ }
144
+ },
145
+ as: "elem",
146
+ cond: {
147
+ $ne: [
148
+ "$$elem",
149
+ null
150
+ ]
208
151
  }
209
- });
210
- }
211
- for (const [refName, refMap] of Object.entries(reference.deepReferences)) {
212
- if (!refMap) {
213
- continue;
214
152
  }
215
- if (refMap.referencedCollection) {
216
- const description = throwIfError(await getCollectionAsset(refMap.referencedCollection, "description"));
217
- const { stages: result } = await buildLookupStages(refMap, refName, {
218
- depth: depth + 1,
219
- parent: withParent(propName),
220
- properties: description.properties
221
- });
222
- stages.push(...result);
153
+ };
154
+ }
155
+ if (reference.deepReferences) {
156
+ const stages = [];
157
+ for (const [subRefName, subReference] of Object.entries(reference.deepReferences)) {
158
+ if (!subReference) {
223
159
  continue;
224
160
  }
225
- const refProperties = properties[propName];
226
- if ("properties" in refProperties) {
227
- const { stages: refStages } = await buildLookupStages(refMap, refName, {
228
- depth: depth + 1,
229
- parent: withParent(propName),
230
- properties: refProperties.properties
231
- });
232
- stages.push(...refStages);
233
- } else if ("items" in refProperties) {
234
- if (!("properties" in refProperties.items)) {
235
- throw new Error();
236
- }
237
- const { stages: refStages } = await buildLookupStages(refMap, refName, {
238
- depth: depth + 1,
239
- parent: withParent(propName),
240
- properties: refProperties.items.properties
241
- });
242
- stages.push(...refStages);
161
+ let newElem;
162
+ if (reference.isRecursive) {
163
+ newElem = {
164
+ $arrayElemAt: [
165
+ `$${getTempName(path.slice(0, -1))}.${refName}`,
166
+ indexOfArray
167
+ ]
168
+ };
243
169
  } else {
244
- throw new Error();
170
+ newElem = reference.referencedCollection ? parentElem : `${parentElem}.${subRefName}`;
245
171
  }
172
+ const result = recurseSetStage(subReference, path.concat(subRefName), newElem);
173
+ stages.push([
174
+ subRefName,
175
+ result
176
+ ]);
177
+ }
178
+ if (reference.referencedCollection) {
179
+ return {
180
+ $cond: [
181
+ {
182
+ $ne: [
183
+ indexOfArray,
184
+ -1
185
+ ]
186
+ },
187
+ {
188
+ $mergeObjects: [
189
+ recurseSetStage({
190
+ ...reference,
191
+ deepReferences: void 0
192
+ }, path, parentElem, {
193
+ noCond: true
194
+ }),
195
+ Object.fromEntries(stages)
196
+ ]
197
+ },
198
+ null
199
+ ]
200
+ };
246
201
  }
202
+ return Object.fromEntries(stages);
203
+ }
204
+ const arrayElemAt = {
205
+ $arrayElemAt: [
206
+ `$${getTempName(path)}`,
207
+ indexOfArray
208
+ ]
209
+ };
210
+ if (options.noCond) {
211
+ return arrayElemAt;
247
212
  }
248
213
  return {
249
- stages,
250
- refHasDeepReferences
214
+ $cond: [
215
+ {
216
+ $ne: [
217
+ indexOfArray,
218
+ -1
219
+ ]
220
+ },
221
+ arrayElemAt,
222
+ null
223
+ ]
251
224
  };
252
225
  };
253
- export const buildLookupPipeline = async (referenceMap, options) => {
226
+ export const buildLookupPipeline = (refMap, options = {}) => {
254
227
  const {
255
- properties,
256
- memoize: memoizeId,
257
- project = []
228
+ rootPipeline = [],
229
+ path = [],
230
+ tempNames = [],
231
+ project,
232
+ memoize: memoizeId
258
233
  } = options;
259
- const memoize = `${memoizeId}-${project.sort().join("-")}`;
260
- if (memoizeId && lookupMemo[memoize]) {
261
- const result = lookupMemo[memoize];
262
- return project.length > 0 ? narrowLookupPipelineProjection(result, project) : result;
234
+ const memoize = project ? `${memoizeId}-${project.sort().join("-")}` : memoizeId;
235
+ if (memoize) {
236
+ if (lookupMemo[memoize]) {
237
+ return lookupMemo[memoize];
238
+ }
263
239
  }
264
- let hasDeepReferences = false;
265
240
  const pipeline = [];
266
- for (const [propName, reference] of Object.entries(referenceMap)) {
241
+ const setProperties = [];
242
+ for (const [refName, reference] of Object.entries(refMap)) {
267
243
  if (!reference) {
268
244
  continue;
269
245
  }
270
- const {
271
- stages,
272
- refHasDeepReferences
273
- } = await buildLookupStages(reference, propName, options);
274
- hasDeepReferences = hasDeepReferences || refHasDeepReferences;
275
- pipeline.push(...stages);
276
- }
277
- if (hasDeepReferences) {
278
- pipeline.push(buildGroupStages(referenceMap, properties));
279
- const arrayCleanupStages = buildArrayCleanupStages(referenceMap);
280
- if (arrayCleanupStages) {
281
- pipeline.push(arrayCleanupStages);
246
+ if (project) {
247
+ if (!project.includes(refName)) {
248
+ continue;
249
+ }
250
+ }
251
+ if (reference.deepReferences) {
252
+ buildLookupPipeline(reference.deepReferences, {
253
+ rootPipeline,
254
+ tempNames,
255
+ path: path.concat(refName)
256
+ });
257
+ const result = recurseSetStage(reference, path.concat(refName), `$${refName}`);
258
+ setProperties.push([
259
+ refName,
260
+ result
261
+ ]);
262
+ }
263
+ if (reference.referencedCollection) {
264
+ const tempName = getTempName(path.concat(refName));
265
+ const lookupPipeline = [];
266
+ tempNames.unshift(tempName);
267
+ if (reference.populatedProperties && reference.populatedProperties.length > 0) {
268
+ const lookupPopulate = reference.populatedProperties;
269
+ if (reference.deepReferences) {
270
+ lookupPopulate.push(...Object.keys(reference.deepReferences));
271
+ }
272
+ lookupPipeline.push({
273
+ $project: Object.fromEntries(reference.populatedProperties.map((index) => [
274
+ index,
275
+ 1
276
+ ]))
277
+ });
278
+ }
279
+ const localField = reference.isRecursive ? `${getTempName(path)}.${refName}` : path.concat(refName).join(".");
280
+ rootPipeline.unshift({
281
+ $lookup: {
282
+ from: prepareCollectionName(reference.referencedCollection),
283
+ foreignField: "_id",
284
+ localField,
285
+ as: tempName,
286
+ pipeline: lookupPipeline
287
+ }
288
+ });
289
+ if (!reference.deepReferences) {
290
+ const result = recurseSetStage(reference, path.concat(refName), `$${refName}`);
291
+ setProperties.push([
292
+ refName,
293
+ result
294
+ ]);
295
+ }
282
296
  }
283
297
  }
284
- if (memoizeId) {
285
- lookupMemo[memoize] = pipeline;
298
+ if (path.length === 0) {
299
+ if (setProperties.length > 0) {
300
+ pipeline.push({
301
+ $set: Object.fromEntries(setProperties)
302
+ });
303
+ }
304
+ if (tempNames.length > 0) {
305
+ pipeline.push({
306
+ $unset: tempNames
307
+ });
308
+ }
309
+ const finalPipeline = rootPipeline.concat(pipeline);
310
+ if (memoize) {
311
+ lookupMemo[memoize] = finalPipeline;
312
+ }
313
+ return finalPipeline;
286
314
  }
287
- return project.length > 0 ? narrowLookupPipelineProjection(pipeline, project) : pipeline;
315
+ return pipeline;
288
316
  };
289
- export const getLookupPipeline = (description, _options) => {
290
- const options = Object.assign(_options || {}, {
291
- properties: description.properties
292
- });
293
- const references = getReferences(description.properties);
294
- return buildLookupPipeline(references, options);
317
+ export const getLookupPipeline = async (description, options) => {
318
+ const refMap = await getReferences(description.properties);
319
+ return buildLookupPipeline(refMap, options);
295
320
  };
@@ -29,7 +29,7 @@ type PhaseContext = {
29
29
  options: TraverseOptions & TraverseNormalized;
30
30
  isArray?: boolean;
31
31
  };
32
- export declare const traverseDocument: <const TWhat extends Record<string, unknown>>(what: TWhat, description: Description, _options: TraverseOptions) => Promise<{
32
+ export declare const traverseDocument: <const TWhat extends Record<string, unknown> | null>(what: TWhat, description: Description, _options: TraverseOptions) => Promise<{
33
33
  readonly _tag: "Result";
34
34
  readonly error: undefined;
35
35
  readonly result: any;
@@ -368,6 +368,9 @@ const recurse = async (target, ctx) => {
368
368
  const traverseDocument = async (what, description, _options) => {
369
369
  const options = Object.assign({}, _options);
370
370
  const functions = [];
371
+ if (!what) {
372
+ return types_1.Result.result(what);
373
+ }
371
374
  if (!options.validate && Object.keys(what).length === 0) {
372
375
  return types_1.Result.result({});
373
376
  }
@@ -326,6 +326,9 @@ const recurse = async (target, ctx) => {
326
326
  export const traverseDocument = async (what, description, _options) => {
327
327
  const options = Object.assign({}, _options);
328
328
  const functions = [];
329
+ if (!what) {
330
+ return Result.result(what);
331
+ }
329
332
  if (!options.validate && Object.keys(what).length === 0) {
330
333
  return Result.result({});
331
334
  }
package/dist/database.js CHANGED
@@ -70,9 +70,16 @@ const getDatabaseSync = () => {
70
70
  };
71
71
  exports.getDatabaseSync = getDatabaseSync;
72
72
  const prepareCollectionName = (collectionName) => {
73
- const pluralized = collectionName.endsWith('s')
74
- ? `${collectionName}es`
75
- : `${collectionName}s`;
73
+ let pluralized;
74
+ if (collectionName.endsWith('y')) {
75
+ pluralized = collectionName.replace(/y$/, 'ies');
76
+ }
77
+ else if (collectionName.endsWith('s')) {
78
+ pluralized = `${collectionName}es`;
79
+ }
80
+ else {
81
+ pluralized = `${collectionName}s`;
82
+ }
76
83
  return pluralized.toLowerCase();
77
84
  };
78
85
  exports.prepareCollectionName = prepareCollectionName;
package/dist/database.mjs CHANGED
@@ -43,7 +43,14 @@ export const getDatabaseSync = () => {
43
43
  return dbMemo.db;
44
44
  };
45
45
  export const prepareCollectionName = (collectionName) => {
46
- const pluralized = collectionName.endsWith("s") ? `${collectionName}es` : `${collectionName}s`;
46
+ let pluralized;
47
+ if (collectionName.endsWith("y")) {
48
+ pluralized = collectionName.replace(/y$/, "ies");
49
+ } else if (collectionName.endsWith("s")) {
50
+ pluralized = `${collectionName}es`;
51
+ } else {
52
+ pluralized = `${collectionName}s`;
53
+ }
47
54
  return pluralized.toLowerCase();
48
55
  };
49
56
  export const getDatabaseCollection = (collectionName) => {
@@ -6,14 +6,14 @@ const types_1 = require("@aeriajs/types");
6
6
  const common_1 = require("@aeriajs/common");
7
7
  const index_js_1 = require("../collection/index.js");
8
8
  const internalGet = async (payload, context) => {
9
- const { filters = {}, project = [], } = payload;
9
+ const { filters = {}, project, } = payload;
10
10
  if (Object.keys(filters).length === 0) {
11
11
  return context.error(types_1.HTTPStatus.BadRequest, {
12
12
  code: types_1.ACError.MalformedInput,
13
13
  });
14
14
  }
15
15
  const pipeline = [];
16
- const references = await (0, index_js_1.getReferences)(context.description.properties, {
16
+ const refMap = await (0, index_js_1.getReferences)(context.description.properties, {
17
17
  memoize: context.description.$id,
18
18
  });
19
19
  pipeline.push({
@@ -22,18 +22,19 @@ const internalGet = async (payload, context) => {
22
22
  allowOperators: true,
23
23
  })),
24
24
  });
25
- const projection = (0, index_js_1.normalizeProjection)(project, context.description);
26
- if (projection) {
27
- pipeline.push({
28
- $project: projection,
29
- });
25
+ if (project) {
26
+ const projection = (0, index_js_1.normalizeProjection)(project, context.description);
27
+ if (projection) {
28
+ pipeline.push({
29
+ $project: projection,
30
+ });
31
+ }
30
32
  }
31
- pipeline.push(...await (0, index_js_1.buildLookupPipeline)(references, {
33
+ pipeline.push(...(0, index_js_1.buildLookupPipeline)(refMap, {
32
34
  memoize: context.description.$id,
33
35
  project: payload.populate
34
36
  ? payload.populate
35
37
  : project,
36
- properties: context.description.properties,
37
38
  }));
38
39
  const doc = await context.collection.model.aggregate(pipeline).next();
39
40
  if (!doc) {
@@ -41,12 +42,12 @@ const internalGet = async (payload, context) => {
41
42
  code: types_1.ACError.ResourceNotFound,
42
43
  });
43
44
  }
44
- const result = (0, index_js_1.fill)((0, common_1.throwIfError)(await (0, index_js_1.traverseDocument)(doc, context.description, {
45
+ const result = (0, common_1.throwIfError)(await (0, index_js_1.traverseDocument)(doc, context.description, {
45
46
  getters: true,
46
47
  fromProperties: true,
47
48
  recurseReferences: true,
48
49
  recurseDeep: true,
49
- })), context.description);
50
+ }));
50
51
  return types_1.Result.result(result);
51
52
  };
52
53
  const get = async (payload, context, options = {}) => {
@@ -6,13 +6,12 @@ import {
6
6
  traverseDocument,
7
7
  normalizeProjection,
8
8
  getReferences,
9
- buildLookupPipeline,
10
- fill
9
+ buildLookupPipeline
11
10
  } from "../collection/index.mjs";
12
11
  const internalGet = async (payload, context) => {
13
12
  const {
14
13
  filters = {},
15
- project = []
14
+ project
16
15
  } = payload;
17
16
  if (Object.keys(filters).length === 0) {
18
17
  return context.error(HTTPStatus.BadRequest, {
@@ -20,7 +19,7 @@ const internalGet = async (payload, context) => {
20
19
  });
21
20
  }
22
21
  const pipeline = [];
23
- const references = await getReferences(context.description.properties, {
22
+ const refMap = await getReferences(context.description.properties, {
24
23
  memoize: context.description.$id
25
24
  });
26
25
  pipeline.push({
@@ -29,16 +28,17 @@ const internalGet = async (payload, context) => {
29
28
  allowOperators: true
30
29
  }))
31
30
  });
32
- const projection = normalizeProjection(project, context.description);
33
- if (projection) {
34
- pipeline.push({
35
- $project: projection
36
- });
31
+ if (project) {
32
+ const projection = normalizeProjection(project, context.description);
33
+ if (projection) {
34
+ pipeline.push({
35
+ $project: projection
36
+ });
37
+ }
37
38
  }
38
- pipeline.push(...await buildLookupPipeline(references, {
39
+ pipeline.push(...buildLookupPipeline(refMap, {
39
40
  memoize: context.description.$id,
40
- project: payload.populate ? payload.populate : project,
41
- properties: context.description.properties
41
+ project: payload.populate ? payload.populate : project
42
42
  }));
43
43
  const doc = await context.collection.model.aggregate(pipeline).next();
44
44
  if (!doc) {
@@ -46,12 +46,12 @@ const internalGet = async (payload, context) => {
46
46
  code: ACError.ResourceNotFound
47
47
  });
48
48
  }
49
- const result = fill(throwIfError(await traverseDocument(doc, context.description, {
49
+ const result = throwIfError(await traverseDocument(doc, context.description, {
50
50
  getters: true,
51
51
  fromProperties: true,
52
52
  recurseReferences: true,
53
53
  recurseDeep: true
54
- })), context.description);
54
+ }));
55
55
  return Result.result(result);
56
56
  };
57
57
  export const get = async (payload, context, options = {}) => {