@aeriajs/core 0.0.138 → 0.0.140

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,96 +2,34 @@
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,
13
+ maxDepth = 3,
66
14
  memoize
67
- } = options || {};
15
+ } = options;
68
16
  if (memoize) {
69
17
  if (referenceMemo[memoize]) {
70
18
  return referenceMemo[memoize];
71
19
  }
72
20
  }
73
- const references = {};
21
+ const refMap = {};
74
22
  for (const [propName, property] of Object.entries(properties)) {
75
23
  const refProperty = getReferenceProperty(property);
76
24
  const reference = {};
77
- if (depth === 2 || refProperty && refProperty.populate && refProperty.populate.length === 0) {
25
+ if (depth === maxDepth || refProperty && refProperty.populate && refProperty.populate.length === 0) {
78
26
  continue;
79
27
  }
80
- if (!refProperty) {
81
- const entrypoint = "items" in property ? property.items : property;
82
- if ("properties" in entrypoint) {
83
- const deepReferences = await getReferences(entrypoint.properties, {
84
- memoize: `${memoize}.${propName}`
85
- });
86
- if (Object.keys(deepReferences).length > 0) {
87
- reference.deepReferences ??= {};
88
- reference.deepReferences = deepReferences;
89
- }
90
- }
91
- } else {
28
+ if (refProperty) {
92
29
  const description = throwIfError(await getCollectionAsset(refProperty.$ref, "description"));
93
30
  const deepReferences = await getReferences(description.properties, {
94
31
  depth: depth + 1,
32
+ maxDepth: refProperty.populateDepth || maxDepth,
95
33
  memoize: `${memoize}.${propName}`
96
34
  });
97
35
  if (Object.keys(deepReferences).length > 0) {
@@ -99,6 +37,17 @@ export const getReferences = async (properties, options) => {
99
37
  }
100
38
  const indexes = refProperty.indexes ? refProperty.indexes : description.indexes || [];
101
39
  reference.populatedProperties = (refProperty.populate || []).concat(indexes.filter((index) => typeof index === "string"));
40
+ } else {
41
+ const entrypoint = "items" in property ? property.items : property;
42
+ if ("properties" in entrypoint) {
43
+ const deepReferences = await getReferences(entrypoint.properties, {
44
+ memoize: `${memoize}.${propName}`
45
+ });
46
+ if (Object.keys(deepReferences).length > 0) {
47
+ reference.deepReferences ??= {};
48
+ reference.deepReferences = deepReferences;
49
+ }
50
+ }
102
51
  }
103
52
  if (!refProperty?.$ref && !reference.deepReferences) {
104
53
  continue;
@@ -106,6 +55,9 @@ export const getReferences = async (properties, options) => {
106
55
  if ("items" in property) {
107
56
  reference.isArray = true;
108
57
  }
58
+ if (depth > 0) {
59
+ reference.isRecursive = true;
60
+ }
109
61
  if (refProperty) {
110
62
  if (refProperty.$ref) {
111
63
  reference.referencedCollection = refProperty.$ref;
@@ -114,178 +66,255 @@ export const getReferences = async (properties, options) => {
114
66
  reference.isInline = true;
115
67
  }
116
68
  }
117
- references[propName] = reference;
69
+ refMap[propName] = reference;
118
70
  }
119
71
  if (memoize) {
120
- referenceMemo[memoize] = references;
72
+ referenceMemo[memoize] = refMap;
121
73
  }
122
- return references;
74
+ return refMap;
123
75
  };
124
- const buildLookupStages = async (reference, propName, options) => {
125
- const {
126
- parent,
127
- properties,
128
- depth = 0,
129
- maxDepth = 3
130
- } = options;
131
- const stages = [];
132
- let refHasDeepReferences = false;
133
- const withParent = (propName2) => {
134
- return parent ? `${parent}.${propName2}` : propName2;
135
- };
136
- if (reference.referencedCollection) {
137
- if (!reference.populatedProperties) {
138
- stages.push({
139
- $lookup: {
140
- from: prepareCollectionName(reference.referencedCollection),
141
- foreignField: "_id",
142
- localField: withParent(propName),
143
- as: withParent(propName)
144
- }
145
- });
146
- } else {
147
- const subPipeline = [];
148
- if (reference.deepReferences) {
149
- const subProperties = throwIfError(await getCollectionAsset(reference.referencedCollection, "description")).properties;
150
- subPipeline.push(...await buildLookupPipeline(reference.deepReferences, {
151
- project: reference.populatedProperties,
152
- properties: subProperties
153
- }));
154
- }
155
- if (reference.populatedProperties.length > 0) {
156
- subPipeline.push({
157
- $project: Object.fromEntries(reference.populatedProperties.map((index) => [
158
- index,
159
- 1
160
- ]))
161
- });
162
- }
163
- stages.push({
164
- $lookup: {
165
- from: prepareCollectionName(reference.referencedCollection),
166
- let: {
167
- "ids": !reference.isArray ? `$${withParent(propName)}` : {
168
- $ifNull: [
169
- `$${withParent(propName)}`,
170
- []
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
171
92
  ]
172
93
  }
173
- },
174
- as: withParent(propName),
175
- pipeline: [
176
- {
177
- $match: {
178
- $expr: {
179
- [reference.isArray ? "$in" : "$eq"]: [
180
- "$_id",
181
- "$$ids"
182
- ]
183
- }
184
- }
185
- },
186
- ...subPipeline
187
94
  ]
188
95
  }
189
- });
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
+ };
190
126
  }
191
- if (!reference.isArray) {
192
- stages.push({
193
- $unwind: {
194
- path: `$${withParent(propName)}`,
195
- preserveNullAndEmptyArrays: true
196
- }
197
- });
127
+ let mapInput = parentElem;
128
+ if (reference.isRecursive) {
129
+ mapInput = {
130
+ $arrayElemAt: [
131
+ `$${getTempName(path.slice(0, -1))}.${refName}`,
132
+ indexOfArray
133
+ ]
134
+ };
198
135
  }
199
- } else if (reference.deepReferences && depth <= maxDepth) {
200
- refHasDeepReferences = true;
201
- stages.push({
202
- $unwind: {
203
- path: `$${withParent(propName)}`,
204
- preserveNullAndEmptyArrays: true
205
- }
206
- });
207
- for (const [refName, refMap] of Object.entries(reference.deepReferences)) {
208
- if (!refMap) {
209
- continue;
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
+ ]
151
+ }
210
152
  }
211
- if (refMap.referencedCollection) {
212
- const description = throwIfError(await getCollectionAsset(refMap.referencedCollection, "description"));
213
- const { stages: result } = await buildLookupStages(refMap, refName, {
214
- depth: depth + 1,
215
- parent: withParent(propName),
216
- properties: description.properties
217
- });
218
- 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) {
219
159
  continue;
220
160
  }
221
- const refProperties = properties[propName];
222
- if ("properties" in refProperties) {
223
- const { stages: refStages } = await buildLookupStages(refMap, refName, {
224
- depth: depth + 1,
225
- parent: withParent(propName),
226
- properties: refProperties.properties
227
- });
228
- stages.push(...refStages);
229
- } else if ("items" in refProperties) {
230
- if (!("properties" in refProperties.items)) {
231
- throw new Error();
232
- }
233
- const { stages: refStages } = await buildLookupStages(refMap, refName, {
234
- depth: depth + 1,
235
- parent: withParent(propName),
236
- properties: refProperties.items.properties
237
- });
238
- 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
+ };
239
169
  } else {
240
- throw new Error();
170
+ newElem = reference.referencedCollection ? parentElem : `${parentElem}.${subRefName}`;
241
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
+ };
242
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;
243
212
  }
244
213
  return {
245
- stages,
246
- refHasDeepReferences
214
+ $cond: [
215
+ {
216
+ $ne: [
217
+ indexOfArray,
218
+ -1
219
+ ]
220
+ },
221
+ arrayElemAt,
222
+ null
223
+ ]
247
224
  };
248
225
  };
249
- export const buildLookupPipeline = async (referenceMap, options) => {
226
+ export const buildLookupPipeline = (refMap, options = {}) => {
250
227
  const {
251
- properties,
252
- memoize: memoizeId,
253
- project = []
228
+ rootPipeline = [],
229
+ path = [],
230
+ tempNames = [],
231
+ project,
232
+ memoize: memoizeId
254
233
  } = options;
255
- const memoize = `${memoizeId}-${project.sort().join("-")}`;
256
- if (memoizeId && lookupMemo[memoize]) {
257
- const result = lookupMemo[memoize];
258
- 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
+ }
259
239
  }
260
- let hasDeepReferences = false;
261
240
  const pipeline = [];
262
- for (const [propName, reference] of Object.entries(referenceMap)) {
241
+ const setProperties = [];
242
+ for (const [refName, reference] of Object.entries(refMap)) {
263
243
  if (!reference) {
264
244
  continue;
265
245
  }
266
- const {
267
- stages,
268
- refHasDeepReferences
269
- } = await buildLookupStages(reference, propName, options);
270
- hasDeepReferences = hasDeepReferences || refHasDeepReferences;
271
- pipeline.push(...stages);
272
- }
273
- if (hasDeepReferences) {
274
- pipeline.push(buildGroupStages(referenceMap, properties));
275
- const arrayCleanupStages = buildArrayCleanupStages(referenceMap);
276
- if (arrayCleanupStages) {
277
- 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
+ }
278
296
  }
279
297
  }
280
- if (memoizeId) {
281
- 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;
282
314
  }
283
- return project.length > 0 ? narrowLookupPipelineProjection(pipeline, project) : pipeline;
315
+ return pipeline;
284
316
  };
285
- export const getLookupPipeline = (description, _options) => {
286
- const options = Object.assign(_options || {}, {
287
- properties: description.properties
288
- });
289
- const references = getReferences(description.properties);
290
- return buildLookupPipeline(references, options);
317
+ export const getLookupPipeline = async (description, options) => {
318
+ const refMap = await getReferences(description.properties);
319
+ return buildLookupPipeline(refMap, options);
291
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
  }
@@ -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 = {}) => {