@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,11 +1,11 @@
1
- import * as assert from 'node:assert';
2
1
  import { InternalServerError } from '@opra/common';
3
2
  import omit from 'lodash.omit';
3
+ import { isNotNullish } from 'valgen';
4
4
  import { MongoAdapter } from './mongo-adapter.js';
5
5
  import { MongoService } from './mongo-service.js';
6
6
  /**
7
7
  * @class MongoEntityService
8
- * @template T - The type of the documents in the collection.
8
+ * @template T - The type of the documents in the collection
9
9
  */
10
10
  export class MongoEntityService extends MongoService {
11
11
  /**
@@ -19,121 +19,155 @@ export class MongoEntityService extends MongoService {
19
19
  super(dataType, options);
20
20
  }
21
21
  /**
22
- * Creates a new document in the MongoDB collection.
22
+ * Creates a new document in the MongoDB collection
23
23
  *
24
- * @param {PartialDTO<T>} input
25
- * @param {MongoEntityService.CreateOptions} options
24
+ * @param {MongoEntityService.CreateCommand} command
26
25
  * @protected
27
26
  */
28
- async _create(input, options) {
29
- const inputCodec = this.getInputCodec('create');
30
- const doc = inputCodec(input);
31
- assert.ok(doc._id, 'You must provide the "_id" field');
32
- const r = await this._dbInsertOne(doc, options);
33
- if (r.insertedId) {
34
- if (!options)
35
- return doc;
36
- const out = await this._findById(doc._id, omit(options, 'filter'));
37
- if (out)
38
- return out;
39
- }
27
+ async _create(command) {
28
+ const input = command.input;
29
+ isNotNullish(input, { label: 'input' });
30
+ isNotNullish(input._id, { label: 'input._id' });
31
+ const inputCodec = this._getInputCodec('create');
32
+ const document = inputCodec(input);
33
+ const { options } = command;
34
+ const db = this.getDatabase();
35
+ const collection = await this.getCollection(db);
36
+ const r = await collection.insertOne(document, {
37
+ ...options,
38
+ session: options?.session || this.getSession(),
39
+ });
40
40
  /* istanbul ignore next */
41
- throw new InternalServerError(`Unknown error while creating document for "${this.getResourceName()}"`);
41
+ if (!r.insertedId) {
42
+ throw new InternalServerError(`Unknown error while creating document for "${this.getResourceName()}"`);
43
+ }
44
+ return document;
42
45
  }
43
46
  /**
44
47
  * Returns the count of documents in the collection based on the provided options.
45
48
  *
46
- * @param {MongoEntityService.CountOptions<T>} options - The options for the count operation.
47
- * @return {Promise<number>} - A promise that resolves to the count of documents in the collection.
49
+ * @param {MongoEntityService.CountCommand<T>} command
50
+ * @protected
48
51
  */
49
- async _count(options) {
52
+ async _count(command) {
53
+ const { options } = command;
50
54
  const filter = MongoAdapter.prepareFilter(options?.filter);
51
- return this._dbCountDocuments(filter, omit(options, 'filter'));
55
+ const db = this.getDatabase();
56
+ const collection = await this.getCollection(db);
57
+ return ((await collection.countDocuments(filter || {}, {
58
+ ...options,
59
+ limit: undefined,
60
+ session: options?.session || this.getSession(),
61
+ })) || 0);
52
62
  }
53
63
  /**
54
- * Deletes a document from the collection.
64
+ * Deletes a document from the collection
55
65
  *
56
- * @param {MongoAdapter.AnyId} id - The ID of the document to delete.
57
- * @param {MongoEntityService.DeleteOptions<T>} [options] - Optional delete options.
58
- * @return {Promise<number>} - A Promise that resolves to the number of documents deleted.
66
+ * @param {MongoEntityService.DeleteCommand<T>} command
67
+ * @protected
59
68
  */
60
- async _delete(id, options) {
61
- assert.ok(id, 'You must provide an id');
62
- const filter = MongoAdapter.prepareFilter([MongoAdapter.prepareKeyValues(id, ['_id']), options?.filter]);
63
- const r = await this._dbDeleteOne(filter, options);
64
- return r.deletedCount;
69
+ async _delete(command) {
70
+ isNotNullish(command.documentId, { label: 'documentId' });
71
+ const { options } = command;
72
+ const filter = MongoAdapter.prepareFilter([
73
+ MongoAdapter.prepareKeyValues(command.documentId, ['_id']),
74
+ options?.filter,
75
+ ]);
76
+ const db = this.getDatabase();
77
+ const collection = await this.getCollection(db);
78
+ return (await collection.deleteOne(filter || {}, {
79
+ ...options,
80
+ session: options?.session || this.getSession(),
81
+ })).deletedCount;
65
82
  }
66
83
  /**
67
84
  * Deletes multiple documents from the collection that meet the specified filter criteria.
68
85
  *
69
- * @param {MongoEntityService.DeleteManyOptions<T>} options - The options for the delete operation.
70
- * @return {Promise<number>} - A promise that resolves to the number of documents deleted.
86
+ * @param {MongoEntityService.DeleteCommand<T>} command
87
+ * @protected
71
88
  */
72
- async _deleteMany(options) {
89
+ async _deleteMany(command) {
90
+ const { options } = command;
73
91
  const filter = MongoAdapter.prepareFilter(options?.filter);
74
- const r = await this._dbDeleteMany(filter, omit(options, 'filter'));
75
- return r.deletedCount;
92
+ const db = this.getDatabase();
93
+ const collection = await this.getCollection(db);
94
+ return (await collection.deleteMany(filter || {}, {
95
+ ...options,
96
+ session: options?.session || this.getSession(),
97
+ })).deletedCount;
76
98
  }
77
99
  /**
78
- * The distinct command returns a list of distinct values for the given key across a collection.
79
- * @param {string} field
80
- * @param {MongoEntityService.DistinctOptions<T>} options
100
+ * The distinct command returns a list of distinct values for the given key across a collection
101
+ *
102
+ * @param {MongoEntityService.DistinctCommand<T>} command
81
103
  * @protected
82
104
  */
83
- async _distinct(field, options) {
105
+ async _distinct(command) {
106
+ const { options, field } = command;
84
107
  const filter = MongoAdapter.prepareFilter(options?.filter);
85
- return await this._dbDistinct(field, filter, omit(options, 'filter'));
108
+ const db = this.getDatabase();
109
+ const collection = await this.getCollection(db);
110
+ return await collection.distinct(field, filter || {}, {
111
+ ...options,
112
+ session: options?.session || this.getSession(),
113
+ });
86
114
  }
87
115
  /**
88
116
  * Finds a document by its ID.
89
117
  *
90
- * @param {MongoAdapter.AnyId} id - The ID of the document.
91
- * @param {MongoEntityService.FindOneOptions<T>} [options] - The options for the find query.
92
- * @return {Promise<PartialDTO<T | undefined>>} - A promise resolving to the found document, or undefined if not found.
118
+ * @param { MongoEntityService.FindOneCommand<T>} command
93
119
  */
94
- async _findById(id, options) {
95
- const filter = MongoAdapter.prepareFilter([MongoAdapter.prepareKeyValues(id, ['_id']), options?.filter]);
96
- const mongoOptions = {
97
- ...options,
120
+ async _findById(command) {
121
+ isNotNullish(command.documentId, { label: 'documentId' });
122
+ const filter = MongoAdapter.prepareFilter([
123
+ MongoAdapter.prepareKeyValues(command.documentId, ['_id']),
124
+ command.options?.filter,
125
+ ]);
126
+ const { options } = command;
127
+ const db = this.getDatabase();
128
+ const collection = await this.getCollection(db);
129
+ const out = await collection.findOne(filter || {}, {
130
+ ...omit(options, 'filter'),
131
+ session: options?.session || this.getSession(),
98
132
  projection: MongoAdapter.prepareProjection(this.dataType, options?.projection),
99
133
  limit: undefined,
100
134
  skip: undefined,
101
135
  sort: undefined,
102
- };
103
- const out = await this._dbFindOne(filter, mongoOptions);
104
- const outputCodec = this.getOutputCodec('find');
105
- if (out)
136
+ });
137
+ if (out) {
138
+ const outputCodec = this._getOutputCodec('find');
106
139
  return outputCodec(out);
140
+ }
107
141
  }
108
142
  /**
109
143
  * Finds a document in the collection that matches the specified options.
110
144
  *
111
- * @param {MongoEntityService.FindOneOptions} [options] - The options for the query.
112
- * @return {Promise<PartialDTO<T> | undefined>} A promise that resolves with the found document or undefined if no document is found.
145
+ * @param {MongoEntityService.FindOneCommand<T>} command
113
146
  */
114
- async _findOne(options) {
147
+ async _findOne(command) {
148
+ const { options } = command;
115
149
  const filter = MongoAdapter.prepareFilter(options?.filter);
116
- const mongoOptions = {
150
+ const db = this.getDatabase();
151
+ const collection = await this.getCollection(db);
152
+ const out = await collection.findOne(filter || {}, {
117
153
  ...omit(options, 'filter'),
154
+ session: options?.session || this.getSession(),
118
155
  sort: options?.sort ? MongoAdapter.prepareSort(options.sort) : undefined,
119
156
  projection: MongoAdapter.prepareProjection(this.dataType, options?.projection),
120
157
  limit: undefined,
121
- };
122
- const out = await this._dbFindOne(filter, mongoOptions);
123
- const outputCodec = this.getOutputCodec('find');
124
- if (out)
158
+ });
159
+ if (out) {
160
+ const outputCodec = this._getOutputCodec('find');
125
161
  return outputCodec(out);
162
+ }
126
163
  }
127
164
  /**
128
- * Finds multiple documents in the MongoDB collection.
165
+ * Finds multiple documents in the MongoDB collection
129
166
  *
130
- * @param {MongoEntityService.FindManyOptions<T>} [options] - The options for the find operation.
131
- * @return A Promise that resolves to an array of partial outputs of type T.
167
+ * @param {MongoEntityService.FindManyCommand<T>} command
132
168
  */
133
- async _findMany(options) {
134
- const mongoOptions = {
135
- ...omit(options, ['projection', 'sort', 'skip', 'limit', 'filter']),
136
- };
169
+ async _findMany(command) {
170
+ const { options } = command;
137
171
  const limit = options?.limit || 10;
138
172
  const stages = [];
139
173
  let filter;
@@ -153,11 +187,16 @@ export class MongoEntityService extends MongoService {
153
187
  const projection = MongoAdapter.prepareProjection(dataType, options?.projection);
154
188
  if (projection)
155
189
  stages.push({ $project: projection });
156
- const cursor = await this._dbAggregate(stages, mongoOptions);
190
+ const db = this.getDatabase();
191
+ const collection = await this.getCollection(db);
192
+ const cursor = collection.aggregate(stages, {
193
+ ...omit(options, ['projection', 'sort', 'skip', 'limit', 'filter']),
194
+ session: options?.session || this.getSession(),
195
+ });
157
196
  /** Execute db command */
158
197
  try {
159
198
  /** Fetch the cursor and decode the result objects */
160
- const outputCodec = this.getOutputCodec('find');
199
+ const outputCodec = this._getOutputCodec('find');
161
200
  return (await cursor.toArray()).map((r) => outputCodec(r));
162
201
  }
163
202
  finally {
@@ -169,13 +208,10 @@ export class MongoEntityService extends MongoService {
169
208
  * Finds multiple documents in the collection and returns both records (max limit)
170
209
  * and total count that matched the given criteria
171
210
  *
172
- * @param {MongoEntityService.FindManyOptions<T>} [options] - The options for the find operation.
173
- * @return A Promise that resolves to an array of partial outputs of type T.
211
+ * @param {MongoEntityService.FindManyCommand<T>} command
174
212
  */
175
- async _findManyWithCount(options) {
176
- const mongoOptions = {
177
- ...omit(options, ['projection', 'sort', 'skip', 'limit', 'filter']),
178
- };
213
+ async _findManyWithCount(command) {
214
+ const { options } = command;
179
215
  const limit = options?.limit || 10;
180
216
  let filter;
181
217
  if (options?.filter)
@@ -207,11 +243,16 @@ export class MongoEntityService extends MongoService {
207
243
  const projection = MongoAdapter.prepareProjection(dataType, options?.projection);
208
244
  if (projection)
209
245
  dataStages.push({ $project: projection });
210
- const outputCodec = this.getOutputCodec('find');
246
+ const outputCodec = this._getOutputCodec('find');
211
247
  /** Execute db command */
212
- const cursor = await this._dbAggregate(stages, mongoOptions);
248
+ const db = this.getDatabase();
249
+ const collection = await this.getCollection(db);
250
+ const cursor = collection.aggregate(stages, {
251
+ ...omit(options, ['projection', 'sort', 'skip', 'limit', 'filter']),
252
+ session: options?.session || this.getSession(),
253
+ });
254
+ /** Fetch the cursor and decode the result objects */
213
255
  try {
214
- /** Fetch the cursor and decode the result objects */
215
256
  const facetResult = await cursor.toArray();
216
257
  return {
217
258
  count: facetResult[0].count[0]?.totalMatches || 0,
@@ -224,59 +265,60 @@ export class MongoEntityService extends MongoService {
224
265
  }
225
266
  }
226
267
  /**
227
- * Updates a document with the given id in the collection.
268
+ * Updates a document with the given id in the collection
228
269
  *
229
- * @param {AnyId} id - The id of the document to update.
230
- * @param {PatchDTO<T>|UpdateFilter<T>} input - The partial input object containing the fields to update.
231
- * @param {MongoEntityService.UpdateOptions<T>} [options] - The options for the update operation.
232
- * @returns {Promise<PartialDTO<T> | undefined>} A promise that resolves to the updated document or
233
- * undefined if the document was not found.
270
+ * @param {MongoEntityService.UpdateOneCommand<T>} command
234
271
  */
235
- async _update(id, input, options) {
236
- const isUpdateFilter = Array.isArray(input) || !!Object.keys(input).find(x => x.startsWith('$'));
237
- const isDocument = !Array.isArray(input) && !!Object.keys(input).find(x => !x.startsWith('$'));
238
- if (isUpdateFilter && isDocument) {
272
+ async _update(command) {
273
+ isNotNullish(command.documentId, { label: 'documentId' });
274
+ const { input, inputRaw, options } = command;
275
+ isNotNullish(input || inputRaw, { label: 'input' });
276
+ if (input && inputRaw) {
239
277
  throw new TypeError('You must pass one of MongoDB UpdateFilter or a partial document, not both');
240
278
  }
241
279
  let update;
242
- if (isDocument) {
243
- const inputCodec = this.getInputCodec('update');
280
+ if (input) {
281
+ const inputCodec = this._getInputCodec('update');
244
282
  const doc = inputCodec(input);
245
283
  delete doc._id;
246
284
  update = MongoAdapter.preparePatch(doc);
247
285
  update.$set = update.$set || {};
248
286
  }
249
287
  else
250
- update = input;
251
- const filter = MongoAdapter.prepareFilter([MongoAdapter.prepareKeyValues(id, ['_id']), options?.filter]);
252
- const mongoOptions = {
288
+ update = inputRaw;
289
+ const filter = MongoAdapter.prepareFilter([
290
+ MongoAdapter.prepareKeyValues(command.documentId, ['_id']),
291
+ options?.filter,
292
+ ]);
293
+ const db = this.getDatabase();
294
+ const collection = await this.getCollection(db);
295
+ const out = await collection.findOneAndUpdate(filter || {}, update, {
296
+ upsert: undefined,
253
297
  ...options,
298
+ returnDocument: 'after',
254
299
  includeResultMetadata: false,
255
- upsert: undefined,
300
+ session: options?.session || this.getSession(),
256
301
  projection: MongoAdapter.prepareProjection(this.dataType, options?.projection),
257
- };
258
- const out = await this._dbFindOneAndUpdate(filter, update, mongoOptions);
259
- const outputCodec = this.getOutputCodec('update');
302
+ });
303
+ const outputCodec = this._getOutputCodec('update');
260
304
  if (out)
261
305
  return outputCodec(out);
262
306
  }
263
307
  /**
264
308
  * Updates a document in the collection with the specified ID.
265
309
  *
266
- * @param {MongoAdapter.AnyId} id - The ID of the document to update.
267
- * @param {PatchDTO<T>|UpdateFilter<T>} input - The partial input data to update the document with.
268
- * @param {MongoEntityService.UpdateOptions<T>} [options] - The options for updating the document.
269
- * @returns {Promise<number>} - A promise that resolves to the number of documents modified.
310
+ * @param {MongoEntityService.UpdateOneCommand<T>} command
270
311
  */
271
- async _updateOnly(id, input, options) {
272
- const isUpdateFilter = Array.isArray(input) || !!Object.keys(input).find(x => x.startsWith('$'));
273
- const isDocument = !Array.isArray(input) && !!Object.keys(input).find(x => !x.startsWith('$'));
274
- if (isUpdateFilter && isDocument) {
312
+ async _updateOnly(command) {
313
+ isNotNullish(command.documentId, { label: 'documentId' });
314
+ const { input, inputRaw, options } = command;
315
+ isNotNullish(input || inputRaw, { label: 'input' });
316
+ if (input && inputRaw) {
275
317
  throw new TypeError('You must pass one of MongoDB UpdateFilter or a partial document, not both');
276
318
  }
277
319
  let update;
278
- if (isDocument) {
279
- const inputCodec = this.getInputCodec('update');
320
+ if (input) {
321
+ const inputCodec = this._getInputCodec('update');
280
322
  const doc = inputCodec(input);
281
323
  delete doc._id;
282
324
  update = MongoAdapter.preparePatch(doc);
@@ -284,33 +326,34 @@ export class MongoEntityService extends MongoService {
284
326
  return 0;
285
327
  }
286
328
  else
287
- update = input;
288
- const filter = MongoAdapter.prepareFilter([MongoAdapter.prepareKeyValues(id, ['_id']), options?.filter]);
289
- const mongoOptions = {
329
+ update = inputRaw;
330
+ const filter = MongoAdapter.prepareFilter([
331
+ MongoAdapter.prepareKeyValues(command.documentId, ['_id']),
332
+ options?.filter,
333
+ ]);
334
+ const db = this.getDatabase();
335
+ const collection = await this.getCollection(db);
336
+ return (await collection.updateOne(filter || {}, update, {
290
337
  ...options,
291
- includeResultMetadata: false,
338
+ session: options?.session || this.getSession(),
292
339
  upsert: undefined,
293
- projection: MongoAdapter.prepareProjection(this.dataType, options?.projection),
294
- };
295
- const out = await this._dbUpdateOne(filter, update, mongoOptions);
296
- return out.matchedCount;
340
+ })).matchedCount;
297
341
  }
298
342
  /**
299
343
  * Updates multiple documents in the collection based on the specified input and options.
300
344
  *
301
- * @param {PatchDTO<T>|UpdateFilter<T>} input - The partial input to update the documents with.
302
- * @param {MongoEntityService.UpdateManyOptions<T>} [options] - The options for updating the documents.
303
- * @return {Promise<number>} - A promise that resolves to the number of documents matched and modified.
345
+ * @param {MongoEntityService.UpdateManyCommand<T>} command
304
346
  */
305
- async _updateMany(input, options) {
306
- const isUpdateFilter = Array.isArray(input) || !!Object.keys(input).find(x => x.startsWith('$'));
307
- const isDocument = !Array.isArray(input) && !!Object.keys(input).find(x => !x.startsWith('$'));
308
- if (isUpdateFilter && isDocument) {
347
+ async _updateMany(command) {
348
+ isNotNullish(command.input, { label: 'input' });
349
+ const { input, inputRaw, options } = command;
350
+ isNotNullish(input || inputRaw, { label: 'input' });
351
+ if (input && inputRaw) {
309
352
  throw new TypeError('You must pass one of MongoDB UpdateFilter or a partial document, not both');
310
353
  }
311
354
  let update;
312
- if (isDocument) {
313
- const inputCodec = this.getInputCodec('update');
355
+ if (input) {
356
+ const inputCodec = this._getInputCodec('update');
314
357
  const doc = inputCodec(input);
315
358
  delete doc._id;
316
359
  update = MongoAdapter.preparePatch(doc);
@@ -318,13 +361,98 @@ export class MongoEntityService extends MongoService {
318
361
  return 0;
319
362
  }
320
363
  else
321
- update = input;
322
- const mongoOptions = {
323
- ...omit(options, 'filter'),
324
- upsert: undefined,
325
- };
364
+ update = inputRaw;
326
365
  const filter = MongoAdapter.prepareFilter(options?.filter);
327
- const r = await this._dbUpdateMany(filter, update, mongoOptions);
328
- return r.matchedCount;
366
+ const db = this.getDatabase();
367
+ const collection = await this.getCollection(db);
368
+ return (await collection.updateMany(filter || {}, update, {
369
+ ...omit(options, 'filter'),
370
+ session: options?.session || this.getSession(),
371
+ upsert: false,
372
+ })).matchedCount;
373
+ }
374
+ async _executeCommand(command, commandFn) {
375
+ try {
376
+ const result = await super._executeCommand(command, async () => {
377
+ /** Call before[X] hooks */
378
+ if (command.crud === 'create')
379
+ await this._beforeCreate(command);
380
+ else if (command.crud === 'update' && command.byId) {
381
+ await this._beforeUpdate(command);
382
+ }
383
+ else if (command.crud === 'update' && !command.byId) {
384
+ await this._beforeUpdateMany(command);
385
+ }
386
+ else if (command.crud === 'delete' && command.byId) {
387
+ await this._beforeDelete(command);
388
+ }
389
+ else if (command.crud === 'delete' && !command.byId) {
390
+ await this._beforeDeleteMany(command);
391
+ }
392
+ /** Call command function */
393
+ return commandFn();
394
+ });
395
+ /** Call after[X] hooks */
396
+ if (command.crud === 'create')
397
+ await this._afterCreate(command, result);
398
+ else if (command.crud === 'update' && command.byId) {
399
+ await this._afterUpdate(command, result);
400
+ }
401
+ else if (command.crud === 'update' && !command.byId) {
402
+ await this._afterUpdateMany(command, result);
403
+ }
404
+ else if (command.crud === 'delete' && command.byId) {
405
+ await this._afterDelete(command, result);
406
+ }
407
+ else if (command.crud === 'delete' && !command.byId) {
408
+ await this._afterDeleteMany(command, result);
409
+ }
410
+ return result;
411
+ }
412
+ catch (e) {
413
+ Error.captureStackTrace(e, this._executeCommand);
414
+ await this.onError?.(e, this);
415
+ throw e;
416
+ }
417
+ }
418
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
419
+ async _beforeCreate(command) {
420
+ // Do nothing
421
+ }
422
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
423
+ async _beforeUpdate(command) {
424
+ // Do nothing
425
+ }
426
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
427
+ async _beforeUpdateMany(command) {
428
+ // Do nothing
429
+ }
430
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
431
+ async _beforeDelete(command) {
432
+ // Do nothing
433
+ }
434
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
435
+ async _beforeDeleteMany(command) {
436
+ // Do nothing
437
+ }
438
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
439
+ async _afterCreate(command, result) {
440
+ // Do nothing
441
+ }
442
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
443
+ async _afterUpdate(command, result) {
444
+ // Do nothing
445
+ }
446
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
447
+ async _afterUpdateMany(command, affected) {
448
+ // Do nothing
449
+ }
450
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
451
+ async _afterDelete(command, affected) {
452
+ // Do nothing
453
+ }
454
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
455
+ async _afterDeleteMany(command, affected) {
456
+ // Do nothing
329
457
  }
330
458
  }