@opra/mongodb 1.0.0-alpha.9 → 1.0.0-beta.2

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.
@@ -1,5 +1,6 @@
1
1
  import { ComplexType, NotAcceptableError, ResourceNotAvailableError } from '@opra/common';
2
2
  import omit from 'lodash.omit';
3
+ import { isNotNullish } from 'valgen';
3
4
  import { MongoAdapter } from './mongo-adapter.js';
4
5
  import { MongoService } from './mongo-service.js';
5
6
  /**
@@ -21,7 +22,7 @@ export class MongoNestedService extends MongoService {
21
22
  this.fieldName = fieldName;
22
23
  this.nestedKey = options?.nestedKey || '_id';
23
24
  this.defaultLimit = options?.defaultLimit || 10;
24
- this.$nestedFilter = options?.$nestedFilter;
25
+ this.nestedFilter = options?.nestedFilter;
25
26
  }
26
27
  /**
27
28
  * Retrieves the data type of the array field
@@ -50,51 +51,81 @@ export class MongoNestedService extends MongoService {
50
51
  throw new ResourceNotAvailableError(this.getResourceName() + '.' + this.nestedKey, documentId + '/' + id);
51
52
  }
52
53
  }
54
+ async create(documentId, input, options) {
55
+ const command = {
56
+ crud: 'create',
57
+ method: 'create',
58
+ byId: false,
59
+ documentId,
60
+ input,
61
+ options,
62
+ };
63
+ input[this.nestedKey] = input[this.nestedKey] ?? this._generateId(command);
64
+ return this._executeCommand(command, async () => {
65
+ const r = await this._create(command);
66
+ if (!options?.projection)
67
+ return r;
68
+ const findCommand = {
69
+ crud: 'read',
70
+ method: 'findById',
71
+ byId: true,
72
+ documentId,
73
+ nestedId: r[this.nestedKey],
74
+ options: {
75
+ ...options,
76
+ sort: undefined,
77
+ filter: undefined,
78
+ skip: undefined,
79
+ },
80
+ };
81
+ const out = await this._findById(findCommand);
82
+ if (out)
83
+ return out;
84
+ });
85
+ }
53
86
  /**
54
87
  * Adds a single item into the array field.
55
88
  *
56
89
  * @param {MongoAdapter.AnyId} documentId - The ID of the parent document.
57
- * @param {T} input - The item to be added to the array field.
90
+ * @param {DTO<T>} input - The item to be added to the array field.
58
91
  * @param {MongoNestedService.CreateOptions} [options] - Optional options for the create operation.
59
- * @return {Promise<PartialDTO<T>>} - A promise that resolves with the partial output of the created item.
92
+ * @return {Promise<PartialDTO<T>>} - A promise that resolves create operation result
60
93
  * @throws {ResourceNotAvailableError} - If the parent document is not found.
61
94
  */
62
- async create(documentId, input, options) {
63
- const id = input._id || this._generateId();
64
- if (id != null)
65
- input._id = id;
66
- const info = {
95
+ async createOnly(documentId, input, options) {
96
+ const command = {
67
97
  crud: 'create',
68
98
  method: 'create',
69
99
  byId: false,
70
100
  documentId,
71
- nestedId: id,
72
101
  input,
73
102
  options,
74
103
  };
75
- return this._intercept(() => this._create(documentId, input, options), info);
104
+ input[this.nestedKey] = input[this.nestedKey] ?? this._generateId(command);
105
+ return this._executeCommand(command, () => this._create(command));
76
106
  }
77
- async _create(documentId, input, options) {
78
- const inputCodec = this.getInputCodec('create');
79
- const doc = inputCodec(input);
80
- doc._id = doc._id || this._generateId();
107
+ async _create(command) {
108
+ const inputCodec = this._getInputCodec('create');
109
+ const { documentId, options } = command;
110
+ const input = command.input;
111
+ isNotNullish(input, { label: 'input' });
112
+ isNotNullish(input[this.nestedKey], { label: `input.${this.nestedKey}` });
113
+ const document = inputCodec(input);
81
114
  const docFilter = MongoAdapter.prepareKeyValues(documentId, ['_id']);
82
- const r = await this._dbUpdateOne(docFilter, {
83
- $push: { [this.fieldName]: doc },
84
- }, options);
85
- if (r.matchedCount) {
86
- if (!options)
87
- return doc;
88
- const id = doc[this.nestedKey];
89
- const out = await this._findById(documentId, id, {
90
- ...options,
91
- filter: undefined,
92
- skip: undefined,
93
- });
94
- if (out)
95
- return out;
115
+ const db = this.getDatabase();
116
+ const collection = await this.getCollection(db);
117
+ const update = {
118
+ $push: { [this.fieldName]: document },
119
+ };
120
+ const r = await collection.updateOne(docFilter, update, {
121
+ ...options,
122
+ session: options?.session || this.getSession(),
123
+ upsert: undefined,
124
+ });
125
+ if (!r.matchedCount) {
126
+ throw new ResourceNotAvailableError(this.getResourceName(), documentId);
96
127
  }
97
- throw new ResourceNotAvailableError(this.getResourceName(), documentId);
128
+ return document;
98
129
  }
99
130
  /**
100
131
  * Counts the number of documents in the collection that match the specified parentId and options.
@@ -104,20 +135,22 @@ export class MongoNestedService extends MongoService {
104
135
  * @returns {Promise<number>} - A promise that resolves to the count of documents.
105
136
  */
106
137
  async count(documentId, options) {
107
- const info = {
138
+ const command = {
108
139
  crud: 'read',
109
140
  method: 'count',
110
141
  byId: false,
111
142
  documentId,
112
143
  options,
113
144
  };
114
- return this._intercept(async () => {
115
- const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info)]);
116
- const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(info), options?.filter]);
117
- return this._count(documentId, { ...options, filter, documentFilter });
118
- }, info);
145
+ return this._executeCommand(command, async () => {
146
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
147
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
148
+ command.options = { ...command.options, filter, documentFilter };
149
+ return this._count(command);
150
+ });
119
151
  }
120
- async _count(documentId, options) {
152
+ async _count(command) {
153
+ const { documentId, options } = command;
121
154
  const matchFilter = MongoAdapter.prepareFilter([
122
155
  MongoAdapter.prepareKeyValues(documentId, ['_id']),
123
156
  options?.documentFilter,
@@ -132,13 +165,18 @@ export class MongoNestedService extends MongoService {
132
165
  stages.push({ $match: filter });
133
166
  }
134
167
  stages.push({ $count: '*' });
135
- const r = await this._dbAggregate(stages, options);
168
+ const db = this.getDatabase();
169
+ const collection = await this.getCollection(db);
170
+ const cursor = collection.aggregate(stages, {
171
+ ...omit(options, ['documentFilter', 'nestedFilter', 'projection', 'sort', 'skip', 'limit', 'filter', 'count']),
172
+ session: options?.session || this.getSession(),
173
+ });
136
174
  try {
137
- const n = await r.next();
175
+ const n = await cursor.next();
138
176
  return n?.['*'] || 0;
139
177
  }
140
178
  finally {
141
- await r.close();
179
+ await cursor.close();
142
180
  }
143
181
  }
144
182
  /**
@@ -150,7 +188,7 @@ export class MongoNestedService extends MongoService {
150
188
  * @return {Promise<number>} - A Promise that resolves to the number of elements deleted (1 if successful, 0 if not).
151
189
  */
152
190
  async delete(documentId, nestedId, options) {
153
- const info = {
191
+ const command = {
154
192
  crud: 'delete',
155
193
  method: 'delete',
156
194
  byId: true,
@@ -158,21 +196,32 @@ export class MongoNestedService extends MongoService {
158
196
  nestedId,
159
197
  options,
160
198
  };
161
- return this._intercept(async () => {
162
- const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info)]);
163
- const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(info), options?.filter]);
164
- return this._delete(documentId, nestedId, { ...options, filter, documentFilter });
165
- }, info);
199
+ return this._executeCommand(command, async () => {
200
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
201
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
202
+ command.options = { ...command.options, filter, documentFilter };
203
+ return this._delete(command);
204
+ });
166
205
  }
167
- async _delete(documentId, nestedId, options) {
206
+ async _delete(command) {
207
+ const { documentId, nestedId, options } = command;
208
+ isNotNullish(documentId, { label: 'documentId' });
209
+ isNotNullish(documentId, { label: 'nestedId' });
168
210
  const matchFilter = MongoAdapter.prepareFilter([
169
211
  MongoAdapter.prepareKeyValues(documentId, ['_id']),
170
212
  options?.documentFilter,
171
213
  ]);
172
214
  const pullFilter = MongoAdapter.prepareFilter([MongoAdapter.prepareKeyValues(nestedId, [this.nestedKey]), options?.filter]) || {};
173
- const r = await this._dbUpdateOne(matchFilter, {
215
+ const update = {
174
216
  $pull: { [this.fieldName]: pullFilter },
175
- }, options);
217
+ };
218
+ const db = this.getDatabase();
219
+ const collection = await this.getCollection(db);
220
+ const r = await collection.updateOne(matchFilter, update, {
221
+ ...options,
222
+ session: options?.session || this.getSession(),
223
+ upsert: undefined,
224
+ });
176
225
  return r.modifiedCount ? 1 : 0;
177
226
  }
178
227
  /**
@@ -183,33 +232,47 @@ export class MongoNestedService extends MongoService {
183
232
  * @returns {Promise<number>} - A Promise that resolves to the number of items deleted.
184
233
  */
185
234
  async deleteMany(documentId, options) {
186
- const info = {
235
+ const command = {
187
236
  crud: 'delete',
188
237
  method: 'deleteMany',
189
238
  byId: false,
190
239
  documentId,
191
240
  options,
192
241
  };
193
- return this._intercept(async () => {
194
- const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info)]);
195
- const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(info), options?.filter]);
196
- return this._deleteMany(documentId, { ...options, filter, documentFilter });
197
- }, info);
242
+ return this._executeCommand(command, async () => {
243
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
244
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
245
+ command.options = { ...command.options, filter, documentFilter };
246
+ return this._deleteMany(command);
247
+ });
198
248
  }
199
- async _deleteMany(documentId, options) {
249
+ async _deleteMany(command) {
250
+ const { documentId, options } = command;
200
251
  const matchFilter = MongoAdapter.prepareFilter([
201
252
  MongoAdapter.prepareKeyValues(documentId, ['_id']),
202
253
  options?.documentFilter,
203
254
  ]);
204
255
  // Count matching items, we will use this as result
205
- const matchCount = await this.count(documentId, options);
256
+ const countCommand = {
257
+ crud: 'read',
258
+ method: 'count',
259
+ byId: false,
260
+ documentId,
261
+ options,
262
+ };
263
+ const matchCount = await this._count(countCommand);
206
264
  const pullFilter = MongoAdapter.prepareFilter(options?.filter) || {};
207
- const r = await this._dbUpdateOne(matchFilter, {
265
+ const update = {
208
266
  $pull: { [this.fieldName]: pullFilter },
209
- }, options);
210
- if (r.matchedCount)
211
- return matchCount;
212
- return 0;
267
+ };
268
+ const db = this.getDatabase();
269
+ const collection = await this.getCollection(db);
270
+ await collection.updateOne(matchFilter, update, {
271
+ ...options,
272
+ session: options?.session || this.getSession(),
273
+ upsert: undefined,
274
+ });
275
+ return matchCount;
213
276
  }
214
277
  /**
215
278
  * Checks if an array element with the given parentId and id exists.
@@ -220,28 +283,50 @@ export class MongoNestedService extends MongoService {
220
283
  * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating if the record exists or not.
221
284
  */
222
285
  async exists(documentId, nestedId, options) {
223
- return !!(await this.findById(documentId, nestedId, { ...options, projection: ['_id'] }));
286
+ const command = {
287
+ crud: 'read',
288
+ method: 'exists',
289
+ byId: true,
290
+ documentId,
291
+ nestedId,
292
+ options,
293
+ };
294
+ return this._executeCommand(command, async () => {
295
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
296
+ const filter = MongoAdapter.prepareFilter([
297
+ await this._getNestedFilter(command),
298
+ documentFilter,
299
+ command.options?.filter,
300
+ ]);
301
+ command.options = { ...command.options, filter };
302
+ return !!(await this._findById(command));
303
+ });
224
304
  }
225
305
  /**
226
306
  * Checks if an object with the given arguments exists.
227
307
  *
228
308
  * @param {MongoAdapter.AnyId} documentId - The ID of the parent document.
229
- * @param {MongoNestedService.ExistsOneOptions} [options] - The options for the query (optional).
309
+ * @param {MongoNestedService.ExistsOptions} [options] - The options for the query (optional).
230
310
  * @return {Promise<boolean>} - A Promise that resolves to a boolean indicating whether the object exists or not.
231
311
  */
232
312
  async existsOne(documentId, options) {
233
- return !!(await this.findOne(documentId, { ...options, projection: ['_id'] }));
313
+ const command = {
314
+ crud: 'read',
315
+ method: 'exists',
316
+ byId: false,
317
+ documentId,
318
+ options,
319
+ };
320
+ return this._executeCommand(command, async () => {
321
+ const documentFilter = await this._getDocumentFilter(command);
322
+ const filter = MongoAdapter.prepareFilter([documentFilter, command.options?.filter]);
323
+ const findCommand = command;
324
+ findCommand.options = { ...command.options, filter, documentFilter, projection: ['_id'] };
325
+ return !!(await this._findOne(findCommand));
326
+ });
234
327
  }
235
- /**
236
- * Finds an element in array field by its parent ID and ID.
237
- *
238
- * @param {MongoAdapter.AnyId} documentId - The ID of the document.
239
- * @param {MongoAdapter.AnyId} nestedId - The ID of the document.
240
- * @param {MongoNestedService.FindOneOptions<T>} [options] - The optional options for the operation.
241
- * @returns {Promise<PartialDTO<T> | undefined>} - A promise that resolves to the found document or undefined if not found.
242
- */
243
328
  async findById(documentId, nestedId, options) {
244
- const info = {
329
+ const command = {
245
330
  crud: 'read',
246
331
  method: 'findById',
247
332
  byId: true,
@@ -249,95 +334,96 @@ export class MongoNestedService extends MongoService {
249
334
  nestedId,
250
335
  options,
251
336
  };
252
- return this._intercept(async () => {
253
- const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info)]);
254
- const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(info), options?.filter]);
255
- return this._findById(documentId, nestedId, { ...options, filter, documentFilter });
256
- }, info);
337
+ return this._executeCommand(command, async () => {
338
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
339
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
340
+ command.options = { ...command.options, filter, documentFilter };
341
+ return this._findById(command);
342
+ });
257
343
  }
258
- async _findById(documentId, nestedId, options) {
344
+ async _findById(command) {
345
+ const { documentId, nestedId, options } = command;
346
+ isNotNullish(documentId, { label: 'documentId' });
347
+ isNotNullish(nestedId, { label: 'nestedId' });
259
348
  const filter = MongoAdapter.prepareFilter([
260
349
  MongoAdapter.prepareKeyValues(nestedId, [this.nestedKey]),
261
350
  options?.filter,
262
351
  ]);
263
- const rows = await this._findMany(documentId, {
264
- ...options,
265
- filter,
266
- limit: 1,
267
- skip: undefined,
268
- sort: undefined,
269
- });
352
+ const findManyCommand = {
353
+ ...command,
354
+ options: {
355
+ ...options,
356
+ filter,
357
+ limit: 1,
358
+ skip: undefined,
359
+ sort: undefined,
360
+ },
361
+ };
362
+ const rows = await this._findMany(findManyCommand);
270
363
  return rows?.[0];
271
364
  }
272
- /**
273
- * Finds the first array element that matches the given parentId.
274
- *
275
- * @param {MongoAdapter.AnyId} documentId - The ID of the document.
276
- * @param {MongoNestedService.FindOneOptions<T>} [options] - Optional options to customize the query.
277
- * @returns {Promise<PartialDTO<T> | undefined>} A promise that resolves to the first matching document, or `undefined` if no match is found.
278
- */
279
365
  async findOne(documentId, options) {
280
- const info = {
366
+ const command = {
281
367
  crud: 'read',
282
368
  method: 'findOne',
283
369
  byId: false,
284
370
  documentId,
285
371
  options,
286
372
  };
287
- return this._intercept(async () => {
288
- const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info)]);
289
- const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(info), options?.filter]);
290
- return this._findOne(documentId, { ...options, filter, documentFilter });
291
- }, info);
292
- }
293
- async _findOne(documentId, options) {
294
- const rows = await this._findMany(documentId, {
295
- ...options,
296
- limit: 1,
373
+ return this._executeCommand(command, async () => {
374
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
375
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
376
+ command.options = { ...command.options, filter, documentFilter };
377
+ return this._findOne(command);
297
378
  });
379
+ }
380
+ async _findOne(command) {
381
+ const { documentId, options } = command;
382
+ isNotNullish(documentId, { label: 'documentId' });
383
+ const findManyCommand = {
384
+ ...command,
385
+ options: {
386
+ ...options,
387
+ limit: 1,
388
+ },
389
+ };
390
+ const rows = await this._findMany(findManyCommand);
298
391
  return rows?.[0];
299
392
  }
300
- /**
301
- * Finds multiple elements in an array field.
302
- *
303
- * @param {MongoAdapter.AnyId} documentId - The ID of the parent document.
304
- * @param {MongoNestedService.FindManyOptions<T>} [options] - The options for finding the documents.
305
- * @returns {Promise<PartialDTO<T>[]>} - The found documents.
306
- */
307
393
  async findMany(documentId, options) {
308
- const args = {
394
+ const command = {
309
395
  crud: 'read',
310
396
  method: 'findMany',
311
397
  byId: false,
312
398
  documentId,
313
399
  options,
314
400
  };
315
- return this._intercept(async () => {
316
- const documentFilter = await this._getDocumentFilter(args);
317
- const nestedFilter = await this._getNestedFilter(args);
318
- return this._findMany(documentId, {
319
- ...options,
320
- documentFilter,
401
+ return this._executeCommand(command, async () => {
402
+ const documentFilter = await this._getDocumentFilter(command);
403
+ const nestedFilter = await this._getNestedFilter(command);
404
+ command.options = {
405
+ ...command.options,
321
406
  nestedFilter,
322
- limit: options?.limit || this.defaultLimit,
323
- });
324
- }, args);
407
+ documentFilter,
408
+ limit: command.options?.limit || this.defaultLimit,
409
+ };
410
+ return this._findMany(command);
411
+ });
325
412
  }
326
- async _findMany(documentId, options) {
413
+ async _findMany(command) {
414
+ const { documentId, options } = command;
415
+ isNotNullish(documentId, { label: 'documentId' });
327
416
  const matchFilter = MongoAdapter.prepareFilter([
328
417
  MongoAdapter.prepareKeyValues(documentId, ['_id']),
329
- options.documentFilter,
418
+ options?.documentFilter,
330
419
  ]);
331
- const mongoOptions = {
332
- ...omit(options, ['documentFilter', 'nestedFilter', 'projection', 'sort', 'skip', 'limit', 'filter', 'count']),
333
- };
334
420
  const limit = options?.limit || this.defaultLimit;
335
421
  const stages = [
336
422
  { $match: matchFilter },
337
423
  { $unwind: { path: '$' + this.fieldName } },
338
424
  { $replaceRoot: { newRoot: '$' + this.fieldName } },
339
425
  ];
340
- if (options?.filter || options.nestedFilter) {
426
+ if (options?.filter || options?.nestedFilter) {
341
427
  const optionsFilter = MongoAdapter.prepareFilter([options?.filter, options.nestedFilter]);
342
428
  stages.push({ $match: optionsFilter });
343
429
  }
@@ -353,51 +439,48 @@ export class MongoNestedService extends MongoService {
353
439
  const projection = MongoAdapter.prepareProjection(dataType, options?.projection);
354
440
  if (projection)
355
441
  stages.push({ $project: projection });
356
- const cursor = await this._dbAggregate(stages, mongoOptions);
442
+ const db = this.getDatabase();
443
+ const collection = await this.getCollection(db);
444
+ const cursor = collection.aggregate(stages, {
445
+ ...omit(options, ['documentFilter', 'nestedFilter', 'projection', 'sort', 'skip', 'limit', 'filter', 'count']),
446
+ session: options?.session || this.getSession(),
447
+ });
357
448
  try {
358
- const outputCodec = this.getOutputCodec('find');
359
- const out = await (await cursor.toArray()).map((r) => outputCodec(r));
360
- return out;
449
+ const outputCodec = this._getOutputCodec('find');
450
+ return (await cursor.toArray()).map((r) => outputCodec(r));
361
451
  }
362
452
  finally {
363
453
  if (!cursor.closed)
364
454
  await cursor.close();
365
455
  }
366
456
  }
367
- /**
368
- * Finds multiple elements in an array field.
369
- *
370
- * @param {MongoAdapter.AnyId} documentId - The ID of the parent document.
371
- * @param {MongoNestedService.FindManyOptions<T>} [options] - The options for finding the documents.
372
- * @returns {Promise<PartialDTO<T>[]>} - The found documents.
373
- */
374
457
  async findManyWithCount(documentId, options) {
375
- const args = {
458
+ const command = {
376
459
  crud: 'read',
377
460
  method: 'findMany',
378
461
  byId: false,
379
462
  documentId,
380
463
  options,
381
464
  };
382
- return this._intercept(async () => {
383
- const documentFilter = await this._getDocumentFilter(args);
384
- const nestedFilter = await this._getNestedFilter(args);
385
- return this._findManyWithCount(documentId, {
386
- ...options,
387
- documentFilter,
465
+ return this._executeCommand(command, async () => {
466
+ const documentFilter = await this._getDocumentFilter(command);
467
+ const nestedFilter = await this._getNestedFilter(command);
468
+ command.options = {
469
+ ...command.options,
388
470
  nestedFilter,
389
- limit: options?.limit || this.defaultLimit,
390
- });
391
- }, args);
471
+ documentFilter,
472
+ limit: command.options?.limit || this.defaultLimit,
473
+ };
474
+ return this._findManyWithCount(command);
475
+ });
392
476
  }
393
- async _findManyWithCount(documentId, options) {
477
+ async _findManyWithCount(command) {
478
+ const { documentId, options } = command;
479
+ isNotNullish(documentId, { label: 'documentId' });
394
480
  const matchFilter = MongoAdapter.prepareFilter([
395
481
  MongoAdapter.prepareKeyValues(documentId, ['_id']),
396
- options.documentFilter,
482
+ options?.documentFilter,
397
483
  ]);
398
- const mongoOptions = {
399
- ...omit(options, ['pick', 'include', 'omit', 'sort', 'skip', 'limit', 'filter', 'count']),
400
- };
401
484
  const limit = options?.limit || this.defaultLimit;
402
485
  const dataStages = [];
403
486
  const stages = [
@@ -411,8 +494,8 @@ export class MongoNestedService extends MongoService {
411
494
  },
412
495
  },
413
496
  ];
414
- if (options?.filter || options.nestedFilter) {
415
- const optionsFilter = MongoAdapter.prepareFilter([options?.filter, options.nestedFilter]);
497
+ if (options?.filter || options?.nestedFilter) {
498
+ const optionsFilter = MongoAdapter.prepareFilter([options?.filter, options?.nestedFilter]);
416
499
  dataStages.push({ $match: optionsFilter });
417
500
  }
418
501
  if (options?.skip)
@@ -427,12 +510,15 @@ export class MongoNestedService extends MongoService {
427
510
  const projection = MongoAdapter.prepareProjection(dataType, options?.projection);
428
511
  if (projection)
429
512
  dataStages.push({ $project: projection });
430
- const cursor = await this._dbAggregate(stages, {
431
- ...mongoOptions,
513
+ const db = this.getDatabase();
514
+ const collection = await this.getCollection(db);
515
+ const cursor = collection.aggregate(stages, {
516
+ ...omit(options, ['documentFilter', 'nestedFilter', 'projection', 'sort', 'skip', 'limit', 'filter', 'count']),
517
+ session: options?.session || this.getSession(),
432
518
  });
433
519
  try {
434
520
  const facetResult = await cursor.toArray();
435
- const outputCodec = this.getOutputCodec('find');
521
+ const outputCodec = this._getOutputCodec('find');
436
522
  return {
437
523
  count: facetResult[0].count[0].totalMatches || 0,
438
524
  items: facetResult[0].data.map((r) => outputCodec(r)),
@@ -443,15 +529,6 @@ export class MongoNestedService extends MongoService {
443
529
  await cursor.close();
444
530
  }
445
531
  }
446
- /**
447
- * Retrieves a specific item from the array of a document.
448
- *
449
- * @param {MongoAdapter.AnyId} documentId - The ID of the document.
450
- * @param {MongoAdapter.AnyId} nestedId - The ID of the item.
451
- * @param {MongoNestedService.FindOneOptions<T>} [options] - The options for finding the item.
452
- * @returns {Promise<PartialDTO<T>>} - The item found.
453
- * @throws {ResourceNotAvailableError} - If the item is not found.
454
- */
455
532
  async get(documentId, nestedId, options) {
456
533
  const out = await this.findById(documentId, nestedId, options);
457
534
  if (!out) {
@@ -459,27 +536,40 @@ export class MongoNestedService extends MongoService {
459
536
  }
460
537
  return out;
461
538
  }
462
- /**
463
- * Updates an array element with new data and returns the updated element
464
- *
465
- * @param {AnyId} documentId - The ID of the document to update.
466
- * @param {AnyId} nestedId - The ID of the item to update within the document.
467
- * @param {PatchDTO<T>} input - The new data to update the item with.
468
- * @param {MongoNestedService.UpdateOptions<T>} [options] - Additional update options.
469
- * @returns {Promise<PartialDTO<T> | undefined>} The updated item or undefined if it does not exist.
470
- * @throws {Error} If an error occurs while updating the item.
471
- */
472
539
  async update(documentId, nestedId, input, options) {
473
- const r = await this.updateOnly(documentId, nestedId, input, options);
474
- if (!r)
475
- return;
476
- const out = await this._findById(documentId, nestedId, {
477
- ...options,
478
- sort: undefined,
540
+ const command = {
541
+ crud: 'update',
542
+ method: 'update',
543
+ byId: true,
544
+ documentId,
545
+ nestedId,
546
+ input,
547
+ options,
548
+ };
549
+ return this._executeCommand(command, async () => {
550
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
551
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
552
+ command.options = {
553
+ ...command.options,
554
+ filter,
555
+ documentFilter,
556
+ };
557
+ const matchCount = await this._updateOnly(command);
558
+ if (matchCount) {
559
+ const findCommand = {
560
+ crud: 'read',
561
+ method: 'findById',
562
+ byId: true,
563
+ documentId,
564
+ nestedId,
565
+ options: { ...options, sort: undefined },
566
+ };
567
+ const out = this._findById(findCommand);
568
+ if (out)
569
+ return out;
570
+ }
571
+ throw new ResourceNotAvailableError(this.getResourceName() + '.' + this.nestedKey, documentId + '/' + nestedId);
479
572
  });
480
- if (out)
481
- return out;
482
- throw new ResourceNotAvailableError(this.getResourceName() + '.' + this.nestedKey, documentId + '/' + nestedId);
483
573
  }
484
574
  /**
485
575
  * Update an array element with new data. Returns 1 if document updated 0 otherwise.
@@ -487,29 +577,45 @@ export class MongoNestedService extends MongoService {
487
577
  * @param {MongoAdapter.AnyId} documentId - The ID of the parent document.
488
578
  * @param {MongoAdapter.AnyId} nestedId - The ID of the document to update.
489
579
  * @param {PatchDTO<T>} input - The partial input object containing the fields to update.
490
- * @param {MongoNestedService.UpdateOptions<T>} [options] - Optional update options.
580
+ * @param {MongoNestedService.UpdateOneOptions<T>} [options] - Optional update options.
491
581
  * @returns {Promise<number>} - A promise that resolves to the number of elements updated.
492
582
  */
493
583
  async updateOnly(documentId, nestedId, input, options) {
494
- const info = {
584
+ const command = {
495
585
  crud: 'update',
496
586
  method: 'update',
497
587
  byId: true,
498
588
  documentId,
499
589
  nestedId,
590
+ input,
500
591
  options,
501
592
  };
502
- return this._intercept(async () => {
503
- const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info)]);
504
- const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(info), options?.filter]);
505
- return this._updateOnly(documentId, nestedId, input, { ...options, filter, documentFilter });
506
- }, info);
593
+ return this._executeCommand(command, async () => {
594
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
595
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
596
+ command.options = {
597
+ ...command.options,
598
+ filter,
599
+ documentFilter,
600
+ };
601
+ return await this._updateOnly(command);
602
+ });
507
603
  }
508
- async _updateOnly(documentId, nestedId, input, options) {
604
+ async _updateOnly(command) {
605
+ const { documentId, nestedId, options } = command;
606
+ isNotNullish(documentId, { label: 'documentId' });
607
+ isNotNullish(nestedId, { label: 'nestedId' });
509
608
  let filter = MongoAdapter.prepareKeyValues(nestedId, [this.nestedKey]);
510
609
  if (options?.filter)
511
610
  filter = MongoAdapter.prepareFilter([filter, options?.filter]);
512
- return await this._updateMany(documentId, input, { ...options, filter });
611
+ const updateManyCommand = {
612
+ ...command,
613
+ options: {
614
+ ...command.options,
615
+ filter,
616
+ },
617
+ };
618
+ return await this._updateMany(updateManyCommand);
513
619
  }
514
620
  /**
515
621
  * Updates multiple array elements in document
@@ -520,7 +626,7 @@ export class MongoNestedService extends MongoService {
520
626
  * @returns {Promise<number>} - A promise that resolves to the number of documents updated.
521
627
  */
522
628
  async updateMany(documentId, input, options) {
523
- const info = {
629
+ const command = {
524
630
  crud: 'update',
525
631
  method: 'updateMany',
526
632
  documentId,
@@ -528,14 +634,18 @@ export class MongoNestedService extends MongoService {
528
634
  input,
529
635
  options,
530
636
  };
531
- return this._intercept(async () => {
532
- const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info)]);
533
- const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(info), options?.filter]);
534
- return this._updateMany(documentId, input, { ...options, filter, documentFilter });
535
- }, info);
536
- }
537
- async _updateMany(documentId, input, options) {
538
- const inputCodec = this.getInputCodec('update');
637
+ return this._executeCommand(command, async () => {
638
+ const documentFilter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command)]);
639
+ const filter = MongoAdapter.prepareFilter([await this._getNestedFilter(command), command.options?.filter]);
640
+ command.options = { ...command.options, filter, documentFilter };
641
+ return this._updateMany(command);
642
+ });
643
+ }
644
+ async _updateMany(command) {
645
+ const { documentId, input } = command;
646
+ isNotNullish(documentId, { label: 'documentId' });
647
+ const options = { ...command.options };
648
+ const inputCodec = this._getInputCodec('update');
539
649
  const doc = inputCodec(input);
540
650
  if (!Object.keys(doc).length)
541
651
  return 0;
@@ -546,26 +656,121 @@ export class MongoNestedService extends MongoService {
546
656
  ]);
547
657
  if (options?.filter) {
548
658
  const elemMatch = MongoAdapter.prepareFilter([options?.filter], { fieldPrefix: 'elem.' });
549
- options = options || {};
550
659
  options.arrayFilters = [elemMatch];
551
660
  }
552
661
  const update = MongoAdapter.preparePatch(doc, {
553
662
  fieldPrefix: this.fieldName + (options?.filter ? '.$[elem].' : '.$[].'),
554
663
  });
555
- const r = await this._dbUpdateOne(matchFilter, update, options);
556
- if (options?.count)
557
- return await this._count(documentId, options);
558
- return r.modifiedCount || 0;
664
+ // Count matching items, we will use this as result
665
+ const count = await this._count({
666
+ crud: 'read',
667
+ method: 'count',
668
+ byId: false,
669
+ documentId,
670
+ options,
671
+ });
672
+ const db = this.getDatabase();
673
+ const collection = await this.getCollection(db);
674
+ await collection.updateOne(matchFilter, update, {
675
+ ...options,
676
+ session: options?.session || this.getSession(),
677
+ upsert: undefined,
678
+ });
679
+ return count;
559
680
  }
560
681
  /**
561
682
  * Retrieves the common filter used for querying array elements.
562
683
  * This method is mostly used for security issues like securing multi-tenant applications.
563
684
  *
564
685
  * @protected
565
- * @returns {FilterInput | Promise<FilterInput> | undefined} The common filter or a Promise
686
+ * @returns {MongoAdapter.FilterInput | Promise<MongoAdapter.FilterInput> | undefined} The common filter or a Promise
566
687
  * that resolves to the common filter, or undefined if not available.
567
688
  */
568
689
  _getNestedFilter(args) {
569
- return typeof this.$nestedFilter === 'function' ? this.$nestedFilter(args, this) : this.$nestedFilter;
690
+ return typeof this.nestedFilter === 'function' ? this.nestedFilter(args, this) : this.nestedFilter;
691
+ }
692
+ async _executeCommand(command, commandFn) {
693
+ try {
694
+ const result = await super._executeCommand(command, async () => {
695
+ /** Call before[X] hooks */
696
+ if (command.crud === 'create')
697
+ await this._beforeCreate(command);
698
+ else if (command.crud === 'update' && command.byId) {
699
+ await this._beforeUpdate(command);
700
+ }
701
+ else if (command.crud === 'update' && !command.byId) {
702
+ await this._beforeUpdateMany(command);
703
+ }
704
+ else if (command.crud === 'delete' && command.byId) {
705
+ await this._beforeDelete(command);
706
+ }
707
+ else if (command.crud === 'delete' && !command.byId) {
708
+ await this._beforeDeleteMany(command);
709
+ }
710
+ /** Call command function */
711
+ return commandFn();
712
+ });
713
+ /** Call after[X] hooks */
714
+ if (command.crud === 'create')
715
+ await this._afterCreate(command, result);
716
+ else if (command.crud === 'update' && command.byId) {
717
+ await this._afterUpdate(command, result);
718
+ }
719
+ else if (command.crud === 'update' && !command.byId) {
720
+ await this._afterUpdateMany(command, result);
721
+ }
722
+ else if (command.crud === 'delete' && command.byId) {
723
+ await this._afterDelete(command, result);
724
+ }
725
+ else if (command.crud === 'delete' && !command.byId) {
726
+ await this._afterDeleteMany(command, result);
727
+ }
728
+ return result;
729
+ }
730
+ catch (e) {
731
+ Error.captureStackTrace(e, this._executeCommand);
732
+ await this.onError?.(e, this);
733
+ throw e;
734
+ }
735
+ }
736
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
737
+ async _beforeCreate(command) {
738
+ // Do nothing
739
+ }
740
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
741
+ async _beforeUpdate(command) {
742
+ // Do nothing
743
+ }
744
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
745
+ async _beforeUpdateMany(command) {
746
+ // Do nothing
747
+ }
748
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
749
+ async _beforeDelete(command) {
750
+ // Do nothing
751
+ }
752
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
753
+ async _beforeDeleteMany(command) {
754
+ // Do nothing
755
+ }
756
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
757
+ async _afterCreate(command, result) {
758
+ // Do nothing
759
+ }
760
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
761
+ async _afterUpdate(command, result) {
762
+ // Do nothing
763
+ }
764
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
765
+ async _afterUpdateMany(command, affected) {
766
+ // Do nothing
767
+ }
768
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
769
+ async _afterDelete(command, affected) {
770
+ // Do nothing
771
+ }
772
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
773
+ async _afterDeleteMany(command, affected) {
774
+ // Do nothing
570
775
  }
571
776
  }