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