@opra/mongodb 0.32.6 → 0.33.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.
@@ -1,6 +1,6 @@
1
1
  import omit from 'lodash.omit';
2
2
  import { ObjectId } from 'mongodb';
3
- import { ComplexType, NotAcceptableError, ResourceNotFoundError } from '@opra/common';
3
+ import { ComplexType, NotAcceptableError, ResourceNotAvailableError } from '@opra/common';
4
4
  import { MongoAdapter } from './mongo-adapter.js';
5
5
  import { MongoService } from './mongo-service.js';
6
6
  /**
@@ -34,11 +34,11 @@ export class MongoArrayService extends MongoService {
34
34
  * @param {AnyId} id - The ID of the resource.
35
35
  * @param {MongoArrayService.ExistsOptions} [options] - Optional parameters for checking resource existence.
36
36
  * @return {Promise<void>} - A promise that resolves with no value upon success.
37
- * @throws {ResourceNotFoundError} - If the resource does not exist.
37
+ * @throws {ResourceNotAvailableError} - If the resource does not exist.
38
38
  */
39
39
  async assert(documentId, id, options) {
40
40
  if (!(await this.exists(documentId, id, options)))
41
- throw new ResourceNotFoundError(this.getResourceName() + '.' + this.arrayKey, documentId + '/' + id);
41
+ throw new ResourceNotAvailableError(this.getResourceName() + '.' + this.arrayKey, documentId + '/' + id);
42
42
  }
43
43
  /**
44
44
  * Adds a single item into the array field.
@@ -47,18 +47,20 @@ export class MongoArrayService extends MongoService {
47
47
  * @param {T} input - The item to be added to the array field.
48
48
  * @param {MongoArrayService.CreateOptions} [options] - Optional options for the create operation.
49
49
  * @return {Promise<PartialDTO<T>>} - A promise that resolves with the partial output of the created item.
50
- * @throws {ResourceNotFoundError} - If the parent document is not found.
50
+ * @throws {ResourceNotAvailableError} - If the parent document is not found.
51
51
  */
52
52
  async create(documentId, input, options) {
53
- if (this.$interceptor && !options?.__interceptor__)
54
- return this.$interceptor(() => this.create(documentId, input, { ...options, __interceptor__: true }), {
55
- crud: 'create',
56
- method: 'create',
57
- documentId,
58
- itemId: input._id,
59
- input,
60
- options
61
- }, this);
53
+ const info = {
54
+ crud: 'create',
55
+ method: 'create',
56
+ documentId,
57
+ itemId: input._id,
58
+ input,
59
+ options
60
+ };
61
+ return this._intercept(() => this._create(documentId, input, options), info);
62
+ }
63
+ async _create(documentId, input, options) {
62
64
  const encode = this.getEncoder('create');
63
65
  const doc = encode(input, { coerce: true });
64
66
  doc._id = doc._id || this._generateId();
@@ -66,18 +68,19 @@ export class MongoArrayService extends MongoService {
66
68
  const r = await this.__updateOne(docFilter, {
67
69
  $push: { [this.fieldName]: doc }
68
70
  }, options);
69
- if (r.modifiedCount) {
71
+ if (r.matchedCount) {
70
72
  if (!options)
71
73
  return doc;
72
- try {
73
- return this.get(documentId, doc[this.arrayKey], { ...options, filter: undefined, skip: undefined });
74
- }
75
- catch (e) {
76
- Error.captureStackTrace(e);
77
- throw e;
78
- }
74
+ const id = doc[this.arrayKey];
75
+ const out = await this._findById(documentId, id, {
76
+ ...options,
77
+ filter: undefined,
78
+ skip: undefined
79
+ });
80
+ if (out)
81
+ return out;
79
82
  }
80
- throw new ResourceNotFoundError(this.getResourceName(), documentId);
83
+ throw new ResourceNotAvailableError(this.getResourceName(), documentId);
81
84
  }
82
85
  /**
83
86
  * Counts the number of documents in the collection that match the specified parentId and options.
@@ -88,26 +91,36 @@ export class MongoArrayService extends MongoService {
88
91
  * @returns {Promise<number>} - A promise that resolves to the count of documents.
89
92
  */
90
93
  async count(documentId, options) {
91
- if (this.$interceptor && !options?.__interceptor__)
92
- return this.$interceptor(() => this.count(documentId, { ...options, __interceptor__: true }), {
93
- crud: 'read',
94
- method: 'count',
95
- documentId,
96
- options
97
- }, this);
94
+ const info = {
95
+ crud: 'read',
96
+ method: 'count',
97
+ documentId,
98
+ options
99
+ };
100
+ return this._intercept(async () => {
101
+ const documentFilter = MongoAdapter.prepareFilter([
102
+ await this._getDocumentFilter(info)
103
+ ]);
104
+ const filter = MongoAdapter.prepareFilter([
105
+ await this._getArrayFilter(info),
106
+ options?.filter
107
+ ]);
108
+ return this._count(documentId, { ...options, filter, documentFilter });
109
+ }, info);
110
+ }
111
+ async _count(documentId, options) {
98
112
  const matchFilter = MongoAdapter.prepareFilter([
99
113
  MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
100
- await this._getDocumentFilter()
114
+ options?.documentFilter
101
115
  ]);
102
116
  const stages = [
103
117
  { $match: matchFilter },
104
118
  { $unwind: { path: "$" + this.fieldName } },
105
119
  { $replaceRoot: { newRoot: "$" + this.fieldName } }
106
120
  ];
107
- const contextArrayFilter = await this._getArrayFilter();
108
- if (options?.filter || contextArrayFilter) {
109
- const optionsFilter = MongoAdapter.prepareFilter([options?.filter, contextArrayFilter]);
110
- stages.push({ $match: optionsFilter });
121
+ if (options?.filter) {
122
+ const filter = MongoAdapter.prepareFilter(options?.filter);
123
+ stages.push({ $match: filter });
111
124
  }
112
125
  stages.push({ $count: '*' });
113
126
  const r = await this.__aggregate(stages, options);
@@ -128,22 +141,32 @@ export class MongoArrayService extends MongoService {
128
141
  * @return {Promise<number>} - A Promise that resolves to the number of elements deleted (1 if successful, 0 if not).
129
142
  */
130
143
  async delete(documentId, id, options) {
131
- if (this.$interceptor && !options?.__interceptor__)
132
- return this.$interceptor(() => this.delete(documentId, id, { ...options, __interceptor__: true }), {
133
- crud: 'delete',
134
- method: 'delete',
135
- documentId,
136
- itemId: id,
137
- options
138
- }, this);
144
+ const info = {
145
+ crud: 'delete',
146
+ method: 'delete',
147
+ documentId,
148
+ itemId: id,
149
+ options
150
+ };
151
+ return this._intercept(async () => {
152
+ const documentFilter = MongoAdapter.prepareFilter([
153
+ await this._getDocumentFilter(info)
154
+ ]);
155
+ const filter = MongoAdapter.prepareFilter([
156
+ await this._getArrayFilter(info),
157
+ options?.filter
158
+ ]);
159
+ return this._delete(documentId, id, { ...options, filter, documentFilter });
160
+ }, info);
161
+ }
162
+ async _delete(documentId, id, options) {
139
163
  const matchFilter = MongoAdapter.prepareFilter([
140
164
  MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
141
- await this._getDocumentFilter()
165
+ options?.documentFilter
142
166
  ]);
143
167
  const pullFilter = MongoAdapter.prepareFilter([
144
168
  MongoAdapter.prepareKeyValues(id, [this.arrayKey]),
145
- options?.filter,
146
- await this._getArrayFilter()
169
+ options?.filter
147
170
  ]) || {};
148
171
  const r = await this.__updateOne(matchFilter, {
149
172
  $pull: { [this.fieldName]: pullFilter }
@@ -158,23 +181,31 @@ export class MongoArrayService extends MongoService {
158
181
  * @returns {Promise<number>} - A Promise that resolves to the number of items deleted.
159
182
  */
160
183
  async deleteMany(documentId, options) {
161
- if (this.$interceptor && !options?.__interceptor__)
162
- return this.$interceptor(() => this.deleteMany(documentId, { ...options, __interceptor__: true }), {
163
- crud: 'delete',
164
- method: 'deleteMany',
165
- documentId,
166
- options
167
- }, this);
184
+ const info = {
185
+ crud: 'delete',
186
+ method: 'deleteMany',
187
+ documentId,
188
+ options
189
+ };
190
+ return this._intercept(async () => {
191
+ const documentFilter = MongoAdapter.prepareFilter([
192
+ await this._getDocumentFilter(info)
193
+ ]);
194
+ const filter = MongoAdapter.prepareFilter([
195
+ await this._getArrayFilter(info),
196
+ options?.filter
197
+ ]);
198
+ return this._deleteMany(documentId, { ...options, filter, documentFilter });
199
+ }, info);
200
+ }
201
+ async _deleteMany(documentId, options) {
168
202
  const matchFilter = MongoAdapter.prepareFilter([
169
203
  MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
170
- await this._getDocumentFilter()
204
+ options?.documentFilter
171
205
  ]);
172
206
  // Count matching items, we will use this as result
173
207
  const matchCount = await this.count(documentId, options);
174
- const pullFilter = MongoAdapter.prepareFilter([
175
- options?.filter,
176
- await this._getArrayFilter()
177
- ]) || {};
208
+ const pullFilter = MongoAdapter.prepareFilter(options?.filter) || {};
178
209
  const r = await this.__updateOne(matchFilter, {
179
210
  $pull: { [this.fieldName]: pullFilter }
180
211
  }, options);
@@ -191,15 +222,7 @@ export class MongoArrayService extends MongoService {
191
222
  * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating if the record exists or not.
192
223
  */
193
224
  async exists(documentId, id, options) {
194
- if (this.$interceptor && !options?.__interceptor__)
195
- return this.$interceptor(() => this.exists(documentId, id, { ...options, __interceptor__: true }), {
196
- crud: 'read',
197
- method: 'exists',
198
- documentId,
199
- itemId: id,
200
- options
201
- }, this);
202
- return !!(await this.findById(documentId, id, { ...options, pick: ['_id'] }));
225
+ return !!(await this.findById(documentId, id, { ...options, pick: ['_id'], omit: undefined, include: undefined }));
203
226
  }
204
227
  /**
205
228
  * Finds an element in array field by its parent ID and ID.
@@ -210,19 +233,37 @@ export class MongoArrayService extends MongoService {
210
233
  * @returns {Promise<PartialDTO<T> | undefined>} - A promise that resolves to the found document or undefined if not found.
211
234
  */
212
235
  async findById(documentId, id, options) {
213
- if (this.$interceptor && !options?.__interceptor__)
214
- return this.$interceptor(() => this.findOne(documentId, { ...options, __interceptor__: true }), {
215
- crud: 'read',
216
- method: 'findById',
217
- documentId,
218
- itemId: id,
219
- options
220
- }, this);
236
+ const info = {
237
+ crud: 'read',
238
+ method: 'findById',
239
+ documentId,
240
+ itemId: id,
241
+ options
242
+ };
243
+ return this._intercept(async () => {
244
+ const documentFilter = MongoAdapter.prepareFilter([
245
+ await this._getDocumentFilter(info)
246
+ ]);
247
+ const filter = MongoAdapter.prepareFilter([
248
+ await this._getArrayFilter(info),
249
+ options?.filter
250
+ ]);
251
+ return this._findById(documentId, id, { ...options, filter, documentFilter });
252
+ }, info);
253
+ }
254
+ async _findById(documentId, id, options) {
221
255
  const filter = MongoAdapter.prepareFilter([
222
256
  MongoAdapter.prepareKeyValues(id, [this.arrayKey]),
223
257
  options?.filter
224
258
  ]);
225
- return await this.findOne(documentId, { ...options, filter });
259
+ const rows = await this._findMany(documentId, {
260
+ ...options,
261
+ filter,
262
+ limit: 1,
263
+ skip: undefined,
264
+ sort: undefined
265
+ });
266
+ return rows?.[0];
226
267
  }
227
268
  /**
228
269
  * Finds the first array element that matches the given parentId.
@@ -232,14 +273,25 @@ export class MongoArrayService extends MongoService {
232
273
  * @returns {Promise<PartialDTO<T> | undefined>} A promise that resolves to the first matching document, or `undefined` if no match is found.
233
274
  */
234
275
  async findOne(documentId, options) {
235
- if (this.$interceptor && !options?.__interceptor__)
236
- return this.$interceptor(() => this.findOne(documentId, { ...options, __interceptor__: true }), {
237
- crud: 'read',
238
- method: 'findOne',
239
- documentId,
240
- options
241
- }, this);
242
- const rows = await this.findMany(documentId, {
276
+ const info = {
277
+ crud: 'read',
278
+ method: 'findOne',
279
+ documentId,
280
+ options
281
+ };
282
+ return this._intercept(async () => {
283
+ const documentFilter = MongoAdapter.prepareFilter([
284
+ await this._getDocumentFilter(info)
285
+ ]);
286
+ const filter = MongoAdapter.prepareFilter([
287
+ await this._getArrayFilter(info),
288
+ options?.filter
289
+ ]);
290
+ return this._findOne(documentId, { ...options, filter, documentFilter });
291
+ }, info);
292
+ }
293
+ async _findOne(documentId, options) {
294
+ const rows = await this._findMany(documentId, {
243
295
  ...options,
244
296
  limit: 1
245
297
  });
@@ -253,16 +305,22 @@ export class MongoArrayService extends MongoService {
253
305
  * @returns {Promise<PartialDTO<T>[]>} - The found documents.
254
306
  */
255
307
  async findMany(documentId, options) {
256
- if (this.$interceptor && !options?.__interceptor__)
257
- return this.$interceptor(() => this.findMany(documentId, { ...options, __interceptor__: true }), {
258
- crud: 'read',
259
- method: 'findMany',
260
- documentId,
261
- options
262
- }, this);
308
+ const args = {
309
+ crud: 'read',
310
+ method: 'findMany',
311
+ documentId,
312
+ options
313
+ };
314
+ return this._intercept(async () => {
315
+ const documentFilter = await this._getDocumentFilter(args);
316
+ const arrayFilter = await this._getArrayFilter(args);
317
+ return this._findMany(documentId, { ...options, documentFilter, arrayFilter });
318
+ }, args);
319
+ }
320
+ async _findMany(documentId, options) {
263
321
  const matchFilter = MongoAdapter.prepareFilter([
264
322
  MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
265
- await this._getDocumentFilter()
323
+ options.documentFilter
266
324
  ]);
267
325
  const mongoOptions = {
268
326
  ...omit(options, ['pick', 'include', 'omit', 'sort', 'skip', 'limit', 'filter', 'count'])
@@ -283,11 +341,10 @@ export class MongoArrayService extends MongoService {
283
341
  }
284
342
  });
285
343
  }
286
- const contextArrayFilter = await this._getArrayFilter();
287
- if (options?.filter || contextArrayFilter) {
344
+ if (options?.filter || options.arrayFilter) {
288
345
  const optionsFilter = MongoAdapter.prepareFilter([
289
346
  options?.filter,
290
- contextArrayFilter
347
+ options.arrayFilter
291
348
  ]);
292
349
  dataStages.push({ $match: optionsFilter });
293
350
  }
@@ -328,12 +385,12 @@ export class MongoArrayService extends MongoService {
328
385
  * @param {AnyId} id - The ID of the item.
329
386
  * @param {MongoArrayService.FindOneOptions<T>} [options] - The options for finding the item.
330
387
  * @returns {Promise<PartialDTO<T>>} - The item found.
331
- * @throws {ResourceNotFoundError} - If the item is not found.
388
+ * @throws {ResourceNotAvailableError} - If the item is not found.
332
389
  */
333
390
  async get(documentId, id, options) {
334
391
  const out = await this.findById(documentId, id, options);
335
392
  if (!out)
336
- throw new ResourceNotFoundError(this.getResourceName() + '.' + this.arrayKey, documentId + '/' + id);
393
+ throw new ResourceNotAvailableError(this.getResourceName() + '.' + this.arrayKey, documentId + '/' + id);
337
394
  return out;
338
395
  }
339
396
  /**
@@ -347,24 +404,13 @@ export class MongoArrayService extends MongoService {
347
404
  * @throws {Error} If an error occurs while updating the item.
348
405
  */
349
406
  async update(documentId, id, input, options) {
350
- if (this.$interceptor && !options?.__interceptor__)
351
- return this.$interceptor(() => this.update(documentId, id, input, { ...options, __interceptor__: true }), {
352
- crud: 'update',
353
- method: 'update',
354
- documentId,
355
- itemId: id,
356
- options
357
- }, this);
358
407
  const r = await this.updateOnly(documentId, id, input, options);
359
408
  if (!r)
360
409
  return;
361
- try {
362
- return await this.findById(documentId, id, options);
363
- }
364
- catch (e) {
365
- Error.captureStackTrace(e);
366
- throw e;
367
- }
410
+ const out = await this._findById(documentId, id, options);
411
+ if (out)
412
+ return out;
413
+ throw new ResourceNotAvailableError(this.getResourceName() + '.' + this.arrayKey, documentId + '/' + id);
368
414
  }
369
415
  /**
370
416
  * Update an array element with new data. Returns 1 if document updated 0 otherwise.
@@ -376,19 +422,29 @@ export class MongoArrayService extends MongoService {
376
422
  * @returns {Promise<number>} - A promise that resolves to the number of elements updated.
377
423
  */
378
424
  async updateOnly(documentId, id, input, options) {
379
- if (this.$interceptor && !options?.__interceptor__)
380
- return this.$interceptor(() => this.updateOnly(documentId, id, input, { ...options, __interceptor__: true }), {
381
- crud: 'update',
382
- method: 'updateOnly',
383
- documentId,
384
- itemId: id,
385
- input,
386
- options
387
- }, this);
425
+ const info = {
426
+ crud: 'update',
427
+ method: 'update',
428
+ documentId,
429
+ itemId: id,
430
+ options
431
+ };
432
+ return this._intercept(async () => {
433
+ const documentFilter = MongoAdapter.prepareFilter([
434
+ await this._getDocumentFilter(info)
435
+ ]);
436
+ const filter = MongoAdapter.prepareFilter([
437
+ await this._getArrayFilter(info),
438
+ options?.filter
439
+ ]);
440
+ return this._updateOnly(documentId, id, input, { ...options, filter, documentFilter });
441
+ }, info);
442
+ }
443
+ async _updateOnly(documentId, id, input, options) {
388
444
  let filter = MongoAdapter.prepareKeyValues(id, [this.arrayKey]);
389
445
  if (options?.filter)
390
446
  filter = MongoAdapter.prepareFilter([filter, options?.filter]);
391
- return await this.updateMany(documentId, input, { ...options, filter });
447
+ return await this._updateMany(documentId, input, { ...options, filter });
392
448
  }
393
449
  /**
394
450
  * Updates multiple array elements in document
@@ -399,56 +455,48 @@ export class MongoArrayService extends MongoService {
399
455
  * @returns {Promise<number>} - A promise that resolves to the number of documents updated.
400
456
  */
401
457
  async updateMany(documentId, input, options) {
402
- if (this.$interceptor && !options?.__interceptor__)
403
- return this.$interceptor(() => this.updateMany(documentId, input, { ...options, __interceptor__: true }), {
404
- crud: 'update',
405
- method: 'updateMany',
406
- documentId,
407
- input,
408
- options
409
- }, this);
458
+ const info = {
459
+ crud: 'update',
460
+ method: 'updateMany',
461
+ documentId,
462
+ input,
463
+ options
464
+ };
465
+ return this._intercept(async () => {
466
+ const documentFilter = MongoAdapter.prepareFilter([
467
+ await this._getDocumentFilter(info)
468
+ ]);
469
+ const filter = MongoAdapter.prepareFilter([
470
+ await this._getArrayFilter(info),
471
+ options?.filter
472
+ ]);
473
+ return this._updateMany(documentId, input, { ...options, filter, documentFilter });
474
+ }, info);
475
+ }
476
+ async _updateMany(documentId, input, options) {
410
477
  const encode = this.getEncoder('update');
411
478
  const doc = encode(input, { coerce: true });
412
479
  if (!Object.keys(doc).length)
413
480
  return 0;
414
481
  const matchFilter = MongoAdapter.prepareFilter([
415
482
  MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
416
- await this._getDocumentFilter(),
483
+ options?.documentFilter,
417
484
  { [this.fieldName]: { $exists: true } }
418
485
  ]);
419
- const contextArrayFilter = await this._getArrayFilter();
420
- if (options?.filter || contextArrayFilter) {
421
- const elemMatch = MongoAdapter.prepareFilter([options?.filter, contextArrayFilter], { fieldPrefix: 'elem.' });
486
+ if (options?.filter) {
487
+ const elemMatch = MongoAdapter.prepareFilter([options?.filter], { fieldPrefix: 'elem.' });
422
488
  options = options || {};
423
489
  options.arrayFilters = [elemMatch];
424
490
  }
425
491
  const update = MongoAdapter.preparePatch(doc, {
426
- fieldPrefix: this.fieldName + ((options?.filter || contextArrayFilter) ? '.$[elem].' : '.$[].')
492
+ fieldPrefix: this.fieldName + (options?.filter ? '.$[elem].' : '.$[].')
427
493
  });
428
494
  const r = await this.__updateOne(matchFilter, update, options);
429
- return r.modifiedCount;
430
- }
431
- /**
432
- * Updates multiple elements and returns the count of elements that were updated.
433
- *
434
- * @param {AnyId} documentId - The ID of the document to update.
435
- * @param {PatchDTO<T>} input - The partial document to update with.
436
- * @param {MongoArrayService.UpdateManyOptions<T>} [options] - The options for updating multiple documents.
437
- * @return {Promise<number>} A promise that resolves to the number of elements updated.
438
- */
439
- async updateManyReturnCount(documentId, input, options) {
440
- if (this.$interceptor && !options?.__interceptor__)
441
- return this.$interceptor(() => this.updateManyReturnCount(documentId, input, { ...options, __interceptor__: true }), {
442
- crud: 'update',
443
- method: 'updateManyReturnCount',
444
- documentId,
445
- input,
446
- options
447
- }, this);
448
- const r = await this.updateMany(documentId, input, options);
449
- return r
495
+ if (!options?.count)
496
+ return r.modifiedCount;
497
+ return r.modifiedCount
450
498
  // Count matching items that fits filter criteria
451
- ? await this.count(documentId, options)
499
+ ? await this._count(documentId, options)
452
500
  : 0;
453
501
  }
454
502
  /**
@@ -482,9 +530,9 @@ export class MongoArrayService extends MongoService {
482
530
  * @returns {FilterInput | Promise<FilterInput> | undefined} The common filter or a Promise
483
531
  * that resolves to the common filter, or undefined if not available.
484
532
  */
485
- _getDocumentFilter() {
533
+ _getDocumentFilter(args) {
486
534
  return typeof this.$documentFilter === 'function' ?
487
- this.$documentFilter(this) : this.$documentFilter;
535
+ this.$documentFilter(args, this) : this.$documentFilter;
488
536
  }
489
537
  /**
490
538
  * Retrieves the common filter used for querying array elements.
@@ -494,8 +542,13 @@ export class MongoArrayService extends MongoService {
494
542
  * @returns {FilterInput | Promise<FilterInput> | undefined} The common filter or a Promise
495
543
  * that resolves to the common filter, or undefined if not available.
496
544
  */
497
- _getArrayFilter() {
545
+ _getArrayFilter(args) {
498
546
  return typeof this.$arrayFilter === 'function' ?
499
- this.$arrayFilter(this) : this.$arrayFilter;
547
+ this.$arrayFilter(args, this) : this.$arrayFilter;
548
+ }
549
+ async _intercept(callback, args) {
550
+ if (this.$interceptor)
551
+ return this.$interceptor(callback, args, this);
552
+ return callback();
500
553
  }
501
554
  }