@opra/mongodb 0.32.3 → 0.32.4

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.
@@ -8,25 +8,23 @@ import { MongoService } from './mongo-service.js';
8
8
  * @template T The type of the array item.
9
9
  */
10
10
  export class MongoArrayService extends MongoService {
11
+ /**
12
+ * Constructs a new instance
13
+ *
14
+ * @param {Type | string} dataType - The data type of the array elements.
15
+ * @param {string} fieldName - The name of the field in the document representing the array.
16
+ * @param {MongoArrayService.Options} [options] - The options for the array service.
17
+ * @constructor
18
+ */
11
19
  constructor(dataType, fieldName, options) {
12
20
  super(dataType, options);
13
21
  this.fieldName = fieldName;
14
22
  this.defaultLimit = options?.defaultLimit || 10;
15
23
  this.collectionKey = options?.collectionKey || '_id';
16
24
  this.arrayKey = options?.arrayKey || '_id';
17
- }
18
- /**
19
- * Retrieves the data type of the array field
20
- *
21
- * @returns {ComplexType} The complex data type of the field.
22
- * @throws {NotAcceptableError} If the data type is not a ComplexType.
23
- */
24
- getDataType() {
25
- const t = super.getDataType()
26
- .getField(this.fieldName).type;
27
- if (!(t instanceof ComplexType))
28
- throw new NotAcceptableError(`Data type "${t.name}" is not a ComplexType`);
29
- return t;
25
+ this.$documentFilter = options?.documentFilter;
26
+ this.$arrayFilter = options?.arrayFilter;
27
+ this.$interceptor = this.$interceptor || options?.interceptor;
30
28
  }
31
29
  /**
32
30
  * Asserts whether a resource with the specified parentId and id exists.
@@ -34,12 +32,13 @@ export class MongoArrayService extends MongoService {
34
32
  *
35
33
  * @param {AnyId} documentId - The ID of the parent document.
36
34
  * @param {AnyId} id - The ID of the resource.
35
+ * @param {MongoArrayService.ExistsOptions} [options] - Optional parameters for checking resource existence.
37
36
  * @return {Promise<void>} - A promise that resolves with no value upon success.
38
37
  * @throws {ResourceNotFoundError} - If the resource does not exist.
39
38
  */
40
- async assert(documentId, id) {
41
- if (!(await this.exists(documentId, id)))
42
- throw new ResourceNotFoundError((this.resourceName || this.getCollectionName()) + '.' + this.arrayKey, documentId + '/' + id);
39
+ async assert(documentId, id, options) {
40
+ if (!(await this.exists(documentId, id, options)))
41
+ throw new ResourceNotFoundError(this.getResourceName() + '.' + this.arrayKey, documentId + '/' + id);
43
42
  }
44
43
  /**
45
44
  * Adds a single item into the array field.
@@ -47,13 +46,22 @@ export class MongoArrayService extends MongoService {
47
46
  * @param {AnyId} documentId - The ID of the parent document.
48
47
  * @param {T} input - The item to be added to the array field.
49
48
  * @param {MongoArrayService.CreateOptions} [options] - Optional options for the create operation.
50
- * @return {Promise<PartialOutput<T>>} - A promise that resolves with the partial output of the created item.
49
+ * @return {Promise<PartialDTO<T>>} - A promise that resolves with the partial output of the created item.
51
50
  * @throws {ResourceNotFoundError} - If the parent document is not found.
52
51
  */
53
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);
54
62
  const encode = this.getEncoder('create');
55
63
  const doc = encode(input);
56
- doc[this.arrayKey] = doc[this.arrayKey] || this._generateId();
64
+ doc._id = doc._id || this._generateId();
57
65
  const docFilter = MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]);
58
66
  const r = await this.__updateOne(docFilter, {
59
67
  $push: { [this.fieldName]: doc }
@@ -69,7 +77,7 @@ export class MongoArrayService extends MongoService {
69
77
  throw e;
70
78
  }
71
79
  }
72
- throw new ResourceNotFoundError(this.resourceName || this.getCollectionName(), documentId);
80
+ throw new ResourceNotFoundError(this.getResourceName(), documentId);
73
81
  }
74
82
  /**
75
83
  * Counts the number of documents in the collection that match the specified parentId and options.
@@ -80,14 +88,25 @@ export class MongoArrayService extends MongoService {
80
88
  * @returns {Promise<number>} - A promise that resolves to the count of documents.
81
89
  */
82
90
  async count(documentId, options) {
83
- const matchFilter = MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]);
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);
98
+ const matchFilter = MongoAdapter.prepareFilter([
99
+ MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
100
+ await this._getDocumentFilter()
101
+ ]);
84
102
  const stages = [
85
103
  { $match: matchFilter },
86
104
  { $unwind: { path: "$" + this.fieldName } },
87
105
  { $replaceRoot: { newRoot: "$" + this.fieldName } }
88
106
  ];
89
- if (options?.filter) {
90
- const optionsFilter = MongoAdapter.prepareFilter(options.filter);
107
+ const contextArrayFilter = await this._getArrayFilter();
108
+ if (options?.filter || contextArrayFilter) {
109
+ const optionsFilter = MongoAdapter.prepareFilter([options?.filter, contextArrayFilter]);
91
110
  stages.push({ $match: optionsFilter });
92
111
  }
93
112
  stages.push({ $count: '*' });
@@ -109,11 +128,23 @@ export class MongoArrayService extends MongoService {
109
128
  * @return {Promise<number>} - A Promise that resolves to the number of elements deleted (1 if successful, 0 if not).
110
129
  */
111
130
  async delete(documentId, id, options) {
112
- const matchFilter = MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]);
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);
139
+ const matchFilter = MongoAdapter.prepareFilter([
140
+ MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
141
+ await this._getDocumentFilter()
142
+ ]);
113
143
  const pullFilter = MongoAdapter.prepareFilter([
114
144
  MongoAdapter.prepareKeyValues(id, [this.arrayKey]),
115
- options?.filter
116
- ]);
145
+ options?.filter,
146
+ await this._getArrayFilter()
147
+ ]) || {};
117
148
  const r = await this.__updateOne(matchFilter, {
118
149
  $pull: { [this.fieldName]: pullFilter }
119
150
  }, options);
@@ -127,11 +158,24 @@ export class MongoArrayService extends MongoService {
127
158
  * @returns {Promise<number>} - A Promise that resolves to the number of items deleted.
128
159
  */
129
160
  async deleteMany(documentId, options) {
130
- const docFilter = MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]);
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);
168
+ const matchFilter = MongoAdapter.prepareFilter([
169
+ MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
170
+ await this._getDocumentFilter()
171
+ ]);
131
172
  // Count matching items, we will use this as result
132
173
  const matchCount = await this.count(documentId, options);
133
- const pullFilter = MongoAdapter.prepareFilter(options?.filter) || {};
134
- const r = await this.__updateOne(docFilter, {
174
+ const pullFilter = MongoAdapter.prepareFilter([
175
+ options?.filter,
176
+ await this._getArrayFilter()
177
+ ]) || {};
178
+ const r = await this.__updateOne(matchFilter, {
135
179
  $pull: { [this.fieldName]: pullFilter }
136
180
  }, options);
137
181
  if (r.modifiedCount)
@@ -143,10 +187,19 @@ export class MongoArrayService extends MongoService {
143
187
  *
144
188
  * @param {AnyId} documentId - The ID of the parent document.
145
189
  * @param {AnyId} id - The id of the record.
190
+ * @param {MongoArrayService.ExistsOptions} [options] - The options for the exists method.
146
191
  * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating if the record exists or not.
147
192
  */
148
- async exists(documentId, id) {
149
- return !!(await this.findById(documentId, id, { pick: ['_id'] }));
193
+ 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'] }));
150
203
  }
151
204
  /**
152
205
  * Finds an element in array field by its parent ID and ID.
@@ -154,12 +207,21 @@ export class MongoArrayService extends MongoService {
154
207
  * @param {AnyId} documentId - The ID of the document.
155
208
  * @param {AnyId} id - The ID of the document.
156
209
  * @param {MongoArrayService.FindOneOptions} [options] - The optional options for the operation.
157
- * @returns {Promise<PartialOutput<T> | undefined>} - A promise that resolves to the found document or undefined if not found.
210
+ * @returns {Promise<PartialDTO<T> | undefined>} - A promise that resolves to the found document or undefined if not found.
158
211
  */
159
212
  async findById(documentId, id, options) {
160
- let filter = MongoAdapter.prepareKeyValues(id, [this.arrayKey]);
161
- if (options?.filter)
162
- filter = MongoAdapter.prepareFilter([filter, options?.filter]);
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);
221
+ const filter = MongoAdapter.prepareFilter([
222
+ MongoAdapter.prepareKeyValues(id, [this.arrayKey]),
223
+ options?.filter
224
+ ]);
163
225
  return await this.findOne(documentId, { ...options, filter });
164
226
  }
165
227
  /**
@@ -167,9 +229,16 @@ export class MongoArrayService extends MongoService {
167
229
  *
168
230
  * @param {AnyId} documentId - The ID of the document.
169
231
  * @param {MongoArrayService.FindOneOptions} [options] - Optional options to customize the query.
170
- * @returns {Promise<PartialOutput<T> | undefined>} A promise that resolves to the first matching document, or `undefined` if no match is found.
232
+ * @returns {Promise<PartialDTO<T> | undefined>} A promise that resolves to the first matching document, or `undefined` if no match is found.
171
233
  */
172
234
  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);
173
242
  const rows = await this.findMany(documentId, {
174
243
  ...options,
175
244
  limit: 1
@@ -181,10 +250,20 @@ export class MongoArrayService extends MongoService {
181
250
  *
182
251
  * @param {AnyId} documentId - The ID of the parent document.
183
252
  * @param {MongoArrayService.FindManyOptions<T>} [options] - The options for finding the documents.
184
- * @returns {Promise<PartialOutput<T>[] | undefined>} - The found documents.
253
+ * @returns {Promise<PartialDTO<T>[]>} - The found documents.
185
254
  */
186
255
  async findMany(documentId, options) {
187
- const matchFilter = MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]);
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);
263
+ const matchFilter = MongoAdapter.prepareFilter([
264
+ MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
265
+ await this._getDocumentFilter()
266
+ ]);
188
267
  const mongoOptions = {
189
268
  ...omit(options, ['pick', 'include', 'omit', 'sort', 'skip', 'limit', 'filter', 'count'])
190
269
  };
@@ -204,8 +283,12 @@ export class MongoArrayService extends MongoService {
204
283
  }
205
284
  });
206
285
  }
207
- if (options?.filter) {
208
- const optionsFilter = MongoAdapter.prepareFilter(options?.filter);
286
+ const contextArrayFilter = await this._getArrayFilter();
287
+ if (options?.filter || contextArrayFilter) {
288
+ const optionsFilter = MongoAdapter.prepareFilter([
289
+ options?.filter,
290
+ contextArrayFilter
291
+ ]);
209
292
  dataStages.push({ $match: optionsFilter });
210
293
  }
211
294
  if (options?.skip)
@@ -244,13 +327,13 @@ export class MongoArrayService extends MongoService {
244
327
  * @param {AnyId} documentId - The ID of the document.
245
328
  * @param {AnyId} id - The ID of the item.
246
329
  * @param {MongoArrayService.FindOneOptions<T>} [options] - The options for finding the item.
247
- * @returns {Promise<PartialOutput<T>>} - The item found.
330
+ * @returns {Promise<PartialDTO<T>>} - The item found.
248
331
  * @throws {ResourceNotFoundError} - If the item is not found.
249
332
  */
250
333
  async get(documentId, id, options) {
251
334
  const out = await this.findById(documentId, id, options);
252
335
  if (!out)
253
- throw new ResourceNotFoundError((this.resourceName || this.getCollectionName()) + '.' + this.arrayKey, documentId + '/' + id);
336
+ throw new ResourceNotFoundError(this.getResourceName() + '.' + this.arrayKey, documentId + '/' + id);
254
337
  return out;
255
338
  }
256
339
  /**
@@ -258,13 +341,23 @@ export class MongoArrayService extends MongoService {
258
341
  *
259
342
  * @param {AnyId} documentId - The ID of the document to update.
260
343
  * @param {AnyId} id - The ID of the item to update within the document.
261
- * @param {PartialInput<T>} input - The new data to update the item with.
344
+ * @param {PatchDTO<T>} input - The new data to update the item with.
262
345
  * @param {MongoArrayService.UpdateOptions<T>} [options] - Additional update options.
263
- * @returns {Promise<PartialOutput<T> | undefined>} The updated item or undefined if it does not exist.
346
+ * @returns {Promise<PartialDTO<T> | undefined>} The updated item or undefined if it does not exist.
264
347
  * @throws {Error} If an error occurs while updating the item.
265
348
  */
266
349
  async update(documentId, id, input, options) {
267
- await this.updateOnly(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
+ const r = await this.updateOnly(documentId, id, input, options);
359
+ if (!r)
360
+ return;
268
361
  try {
269
362
  return await this.findById(documentId, id, options);
270
363
  }
@@ -278,40 +371,59 @@ export class MongoArrayService extends MongoService {
278
371
  *
279
372
  * @param {AnyId} documentId - The ID of the parent document.
280
373
  * @param {AnyId} id - The ID of the document to update.
281
- * @param {PartialInput<T>} doc - The partial input object containing the fields to update.
374
+ * @param {PatchDTO<T>} input - The partial input object containing the fields to update.
282
375
  * @param {MongoArrayService.UpdateOptions<T>} [options] - Optional update options.
283
376
  * @returns {Promise<number>} - A promise that resolves to the number of elements updated.
284
377
  */
285
- async updateOnly(documentId, id, doc, options) {
378
+ 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);
286
388
  let filter = MongoAdapter.prepareKeyValues(id, [this.arrayKey]);
287
389
  if (options?.filter)
288
390
  filter = MongoAdapter.prepareFilter([filter, options?.filter]);
289
- return await this.updateMany(documentId, doc, { ...options, filter });
391
+ return await this.updateMany(documentId, input, { ...options, filter });
290
392
  }
291
393
  /**
292
394
  * Updates multiple array elements in document
293
395
  *
294
396
  * @param {AnyId} documentId - The ID of the document to update.
295
- * @param {PartialInput<T>} input - The updated data for the document(s).
397
+ * @param {PatchDTO<T>} input - The updated data for the document(s).
296
398
  * @param {MongoArrayService.UpdateManyOptions<T>} [options] - Additional options for the update operation.
297
399
  * @returns {Promise<number>} - A promise that resolves to the number of documents updated.
298
400
  */
299
401
  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);
300
410
  const encode = this.getEncoder('update');
301
411
  const doc = encode(input);
302
412
  if (!Object.keys(doc).length)
303
413
  return 0;
304
414
  const matchFilter = MongoAdapter.prepareFilter([
305
415
  MongoAdapter.prepareKeyValues(documentId, [this.collectionKey]),
416
+ await this._getDocumentFilter(),
306
417
  { [this.fieldName]: { $exists: true } }
307
418
  ]);
308
- if (options?.filter) {
309
- const elemMatch = MongoAdapter.prepareFilter(options?.filter, { fieldPrefix: 'elem.' });
419
+ const contextArrayFilter = await this._getArrayFilter();
420
+ if (options?.filter || contextArrayFilter) {
421
+ const elemMatch = MongoAdapter.prepareFilter([options?.filter, contextArrayFilter], { fieldPrefix: 'elem.' });
310
422
  options = options || {};
311
423
  options.arrayFilters = [elemMatch];
312
424
  }
313
425
  const update = MongoAdapter.preparePatch(doc, {
314
- fieldPrefix: this.fieldName + (options?.filter ? '.$[elem].' : '.$[].')
426
+ fieldPrefix: this.fieldName + ((options?.filter || contextArrayFilter) ? '.$[elem].' : '.$[].')
315
427
  });
316
428
  const r = await this.__updateOne(matchFilter, update, options);
317
429
  return r.modifiedCount;
@@ -320,24 +432,70 @@ export class MongoArrayService extends MongoService {
320
432
  * Updates multiple elements and returns the count of elements that were updated.
321
433
  *
322
434
  * @param {AnyId} documentId - The ID of the document to update.
323
- * @param {PartialInput<T>} doc - The partial document to update with.
435
+ * @param {PatchDTO<T>} input - The partial document to update with.
324
436
  * @param {MongoArrayService.UpdateManyOptions<T>} [options] - The options for updating multiple documents.
325
437
  * @return {Promise<number>} A promise that resolves to the number of elements updated.
326
438
  */
327
- async updateManyReturnCount(documentId, doc, options) {
328
- const r = await this.updateMany(documentId, doc, options);
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);
329
449
  return r
330
450
  // Count matching items that fits filter criteria
331
451
  ? await this.count(documentId, options)
332
452
  : 0;
333
453
  }
334
454
  /**
335
- * Generates a new id for new inserting Document.
455
+ * Retrieves the data type of the array field
456
+ *
457
+ * @returns {ComplexType} The complex data type of the field.
458
+ * @throws {NotAcceptableError} If the data type is not a ComplexType.
459
+ */
460
+ getDataType() {
461
+ const t = super.getDataType()
462
+ .getField(this.fieldName).type;
463
+ if (!(t instanceof ComplexType))
464
+ throw new NotAcceptableError(`Data type "${t.name}" is not a ComplexType`);
465
+ return t;
466
+ }
467
+ /**
468
+ * Generates an ID.
336
469
  *
337
470
  * @protected
338
- * @returns {AnyId} A new instance of AnyId.
471
+ * @returns {AnyId} The generated ID.
339
472
  */
340
473
  _generateId() {
341
- return new ObjectId();
474
+ return typeof this.$idGenerator === 'function' ?
475
+ this.$idGenerator(this) : new ObjectId();
476
+ }
477
+ /**
478
+ * Retrieves the common filter used for querying documents.
479
+ * This method is mostly used for security issues like securing multi-tenant applications.
480
+ *
481
+ * @protected
482
+ * @returns {FilterInput | Promise<FilterInput> | undefined} The common filter or a Promise
483
+ * that resolves to the common filter, or undefined if not available.
484
+ */
485
+ _getDocumentFilter() {
486
+ return typeof this.$documentFilter === 'function' ?
487
+ this.$documentFilter(this) : this.$documentFilter;
488
+ }
489
+ /**
490
+ * Retrieves the common filter used for querying array elements.
491
+ * This method is mostly used for security issues like securing multi-tenant applications.
492
+ *
493
+ * @protected
494
+ * @returns {FilterInput | Promise<FilterInput> | undefined} The common filter or a Promise
495
+ * that resolves to the common filter, or undefined if not available.
496
+ */
497
+ _getArrayFilter() {
498
+ return typeof this.$arrayFilter === 'function' ?
499
+ this.$arrayFilter(this) : this.$arrayFilter;
342
500
  }
343
501
  }