@e22m4u/js-repository-mongodb-adapter 0.0.14

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.
@@ -0,0 +1,825 @@
1
+ /* eslint no-unused-vars: 0 */
2
+ import {ObjectId} from 'mongodb';
3
+ import {MongoClient} from 'mongodb';
4
+ import {isObjectId} from './utils/index.js';
5
+ import {Adapter} from '@e22m4u/js-repository';
6
+ import {DataType} from '@e22m4u/js-repository';
7
+ import {isIsoDate} from './utils/is-iso-date.js';
8
+ import {capitalize} from '@e22m4u/js-repository';
9
+ import {createMongodbUrl} from './utils/index.js';
10
+ import {ServiceContainer} from '@e22m4u/js-service';
11
+ import {transformValuesDeep} from './utils/index.js';
12
+ import {stringToRegexp} from '@e22m4u/js-repository';
13
+ import {selectObjectKeys} from '@e22m4u/js-repository';
14
+ import {ModelDefinitionUtils} from '@e22m4u/js-repository';
15
+ import {InvalidArgumentError} from '@e22m4u/js-repository';
16
+ import {InvalidOperatorValueError} from '@e22m4u/js-repository';
17
+
18
+ /**
19
+ * Mongodb option names.
20
+ * 5.8.1
21
+ *
22
+ * @type {string[]}
23
+ */
24
+ const MONGODB_OPTION_NAMES = [
25
+ 'appname',
26
+ 'authMechanism',
27
+ 'authMechanismProperties',
28
+ 'authSource',
29
+ 'compressors',
30
+ 'connectTimeoutMS',
31
+ 'directConnection',
32
+ 'heartbeatFrequencyMS',
33
+ 'journal',
34
+ 'loadBalanced',
35
+ 'localThresholdMS',
36
+ 'maxIdleTimeMS',
37
+ 'maxPoolSize',
38
+ 'maxConnecting',
39
+ 'maxStalenessSeconds',
40
+ 'minPoolSize',
41
+ 'proxyHost',
42
+ 'proxyPort',
43
+ 'proxyUsername',
44
+ 'proxyPassword',
45
+ 'readConcernLevel',
46
+ 'readPreference',
47
+ 'readPreferenceTags',
48
+ 'replicaSet',
49
+ 'retryReads',
50
+ 'retryWrites',
51
+ 'serverSelectionTimeoutMS',
52
+ 'serverSelectionTryOnce',
53
+ 'socketTimeoutMS',
54
+ 'srvMaxHosts',
55
+ 'srvServiceName',
56
+ 'ssl',
57
+ 'timeoutMS',
58
+ 'tls',
59
+ 'tlsAllowInvalidCertificates',
60
+ 'tlsAllowInvalidHostnames',
61
+ 'tlsCAFile',
62
+ 'tlsCertificateKeyFile',
63
+ 'tlsCertificateKeyFilePassword',
64
+ 'tlsInsecure',
65
+ 'w',
66
+ 'waitQueueTimeoutMS',
67
+ 'wTimeoutMS',
68
+ 'zlibCompressionLevel',
69
+ ];
70
+
71
+ /**
72
+ * Default settings.
73
+ *
74
+ * @type {{connectTimeoutMS: number}}
75
+ */
76
+ const DEFAULT_SETTINGS = {
77
+ reconnectInterval: 2000, // adapter specific option
78
+ connectTimeoutMS: 2000,
79
+ serverSelectionTimeoutMS: 2000,
80
+ };
81
+
82
+ /**
83
+ * Mongodb adapter.
84
+ */
85
+ export class MongodbAdapter extends Adapter {
86
+ /**
87
+ * Mongodb instance.
88
+ *
89
+ * @private
90
+ */
91
+ _client;
92
+
93
+ /**
94
+ * Collections.
95
+ *
96
+ * @type {Map<any, any>}
97
+ * @private
98
+ */
99
+ _collections = new Map();
100
+
101
+ /**
102
+ * Connected.
103
+ *
104
+ * @type {boolean}
105
+ * @private
106
+ */
107
+ _connected = false;
108
+
109
+ /**
110
+ * Connected.
111
+ *
112
+ * @return {boolean}
113
+ */
114
+ get connected() {
115
+ return this._connected;
116
+ }
117
+
118
+ /**
119
+ * Connecting.
120
+ *
121
+ * @type {boolean}
122
+ * @private
123
+ */
124
+ _connecting = false;
125
+
126
+ /**
127
+ * Connecting.
128
+ *
129
+ * @return {boolean}
130
+ */
131
+ get connecting() {
132
+ return this._connecting;
133
+ }
134
+
135
+ /**
136
+ * Constructor.
137
+ *
138
+ * @param {ServiceContainer} container
139
+ * @param settings
140
+ */
141
+ constructor(container, settings) {
142
+ settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
143
+ super(container, settings);
144
+ settings.protocol = settings.protocol || 'mongodb';
145
+ settings.hostname = settings.hostname || settings.host || '127.0.0.1';
146
+ settings.port = settings.port || 27017;
147
+ settings.database = settings.database || settings.db || 'test';
148
+ }
149
+
150
+ /**
151
+ * Connect.
152
+ *
153
+ * @return {Promise<*|undefined>}
154
+ * @private
155
+ */
156
+ async connect() {
157
+ if (this._connecting) {
158
+ const tryAgainAfter = 500;
159
+ await new Promise(r => setTimeout(() => r(), tryAgainAfter));
160
+ return this.connect();
161
+ }
162
+
163
+ if (this._connected) return;
164
+ this._connecting = true;
165
+
166
+ const options = selectObjectKeys(this.settings, MONGODB_OPTION_NAMES);
167
+ const url = createMongodbUrl(this.settings);
168
+
169
+ // console.log(`Connecting to ${url}`);
170
+ this._client = new MongoClient(url, options);
171
+
172
+ const {reconnectInterval} = this.settings;
173
+ const connectFn = async () => {
174
+ try {
175
+ await this._client.connect();
176
+ } catch (e) {
177
+ console.error(e);
178
+ // console.log('MongoDB connection failed!');
179
+ // console.log(`Reconnecting after ${reconnectInterval} ms.`);
180
+ await new Promise(r => setTimeout(() => r(), reconnectInterval));
181
+ return connectFn();
182
+ }
183
+ // console.log('MongoDB is connected.');
184
+ this._connected = true;
185
+ this._connecting = false;
186
+ };
187
+
188
+ await connectFn();
189
+
190
+ this._client.once('serverClosed', event => {
191
+ if (this._connected) {
192
+ this._connected = false;
193
+ // console.log('MongoDB lost connection!');
194
+ console.log(event);
195
+ // console.log(`Reconnecting after ${reconnectInterval} ms.`);
196
+ setTimeout(() => connectFn(), reconnectInterval);
197
+ } else {
198
+ // console.log('MongoDB connection closed.');
199
+ }
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Disconnect.
205
+ *
206
+ * @return {Promise<*|undefined>}
207
+ */
208
+ async disconnect() {
209
+ if (this._connecting) {
210
+ const tryAgainAfter = 500;
211
+ await new Promise(r => setTimeout(() => r(), tryAgainAfter));
212
+ return this.disconnect();
213
+ }
214
+ if (!this._connected) return;
215
+ this._connected = false;
216
+ if (this._client) await this._client.close();
217
+ }
218
+
219
+ /**
220
+ * Get id prop name.
221
+ *
222
+ * @param modelName
223
+ */
224
+ _getIdPropName(modelName) {
225
+ return this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
226
+ modelName,
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Get id col name.
232
+ *
233
+ * @param modelName
234
+ */
235
+ _getIdColName(modelName) {
236
+ return this.getService(ModelDefinitionUtils).getPrimaryKeyAsColumnName(
237
+ modelName,
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Coerce id.
243
+ *
244
+ * @param value
245
+ * @return {ObjectId|*}
246
+ * @private
247
+ */
248
+ _coerceId(value) {
249
+ if (value == null) return value;
250
+ if (isObjectId(value)) return new ObjectId(value);
251
+ return value;
252
+ }
253
+
254
+ /**
255
+ * Coerce iso date.
256
+ *
257
+ * @param value
258
+ * @return {*|Date}
259
+ * @private
260
+ */
261
+ _coerceIsoDate(value) {
262
+ if (value === null) return value;
263
+ if (isIsoDate(value)) return new Date(value);
264
+ return value;
265
+ }
266
+
267
+ /**
268
+ * To database.
269
+ *
270
+ * @param {string} modelName
271
+ * @param {object} modelData
272
+ * @return {object}
273
+ * @private
274
+ */
275
+ _toDatabase(modelName, modelData) {
276
+ const tableData = this.getService(
277
+ ModelDefinitionUtils,
278
+ ).convertPropertyNamesToColumnNames(modelName, modelData);
279
+
280
+ const idColName = this._getIdColName(modelName);
281
+ if (idColName !== 'id' && idColName !== '_id')
282
+ throw new InvalidArgumentError(
283
+ 'MongoDB is not supporting custom names of the primary key. ' +
284
+ 'Do use "id" as a primary key instead of %v.',
285
+ idColName,
286
+ );
287
+ if (idColName in tableData && idColName !== '_id') {
288
+ tableData._id = tableData[idColName];
289
+ delete tableData[idColName];
290
+ }
291
+
292
+ return transformValuesDeep(tableData, value => {
293
+ if (value instanceof ObjectId) return value;
294
+ if (value instanceof Date) return value;
295
+ if (isObjectId(value)) return new ObjectId(value);
296
+ if (isIsoDate(value)) return new Date(value);
297
+ return value;
298
+ });
299
+ }
300
+
301
+ /**
302
+ * From database.
303
+ *
304
+ * @param {string} modelName
305
+ * @param {object} tableData
306
+ * @return {object}
307
+ * @private
308
+ */
309
+ _fromDatabase(modelName, tableData) {
310
+ if ('_id' in tableData) {
311
+ const idColName = this._getIdColName(modelName);
312
+ if (idColName !== 'id' && idColName !== '_id')
313
+ throw new InvalidArgumentError(
314
+ 'MongoDB is not supporting custom names of the primary key. ' +
315
+ 'Do use "id" as a primary key instead of %v.',
316
+ idColName,
317
+ );
318
+ if (idColName !== '_id') {
319
+ tableData[idColName] = tableData._id;
320
+ delete tableData._id;
321
+ }
322
+ }
323
+
324
+ const modelData = this.getService(
325
+ ModelDefinitionUtils,
326
+ ).convertColumnNamesToPropertyNames(modelName, tableData);
327
+
328
+ return transformValuesDeep(modelData, value => {
329
+ if (value instanceof ObjectId) return String(value);
330
+ if (value instanceof Date) return value.toISOString();
331
+ return value;
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Get collection.
337
+ *
338
+ * @param {string} modelName
339
+ * @return {*}
340
+ * @private
341
+ */
342
+ _getCollection(modelName) {
343
+ let collection = this._collections.get(modelName);
344
+ if (collection) return collection;
345
+ const tableName =
346
+ this.getService(ModelDefinitionUtils).getTableNameByModelName(modelName);
347
+ collection = this._client.db(this.settings.database).collection(tableName);
348
+ this._collections.set(modelName, collection);
349
+ return collection;
350
+ }
351
+
352
+ /**
353
+ * Get id type.
354
+ *
355
+ * @param modelName
356
+ * @return {string|*}
357
+ * @private
358
+ */
359
+ _getIdType(modelName) {
360
+ const utils = this.getService(ModelDefinitionUtils);
361
+ const pkPropName = utils.getPrimaryKeyAsPropertyName(modelName);
362
+ return utils.getDataTypeByPropertyName(modelName, pkPropName);
363
+ }
364
+
365
+ /**
366
+ * Build projection.
367
+ *
368
+ * @param {string} modelName
369
+ * @param {string|string[]} fields
370
+ * @return {Record<string, number>|undefined}
371
+ * @private
372
+ */
373
+ _buildProjection(modelName, fields) {
374
+ if (!fields) return;
375
+ fields = Array.isArray(fields) ? fields : [fields];
376
+ if (!fields.length) return;
377
+ if (fields.indexOf('_id') === -1) fields.push('_id');
378
+ return fields.reduce((acc, field) => {
379
+ if (!field || typeof field !== 'string')
380
+ throw new InvalidArgumentError(
381
+ 'A field name must be a non-empty String, but %v given.',
382
+ field,
383
+ );
384
+ let colName = this._getColName(modelName, field);
385
+ acc[colName] = 1;
386
+ return acc;
387
+ }, {});
388
+ }
389
+
390
+ /**
391
+ * Get col name.
392
+ *
393
+ * @param {string} modelName
394
+ * @param {string} propName
395
+ * @return {string}
396
+ * @private
397
+ */
398
+ _getColName(modelName, propName) {
399
+ if (!propName || typeof propName !== 'string')
400
+ throw new InvalidArgumentError(
401
+ 'A property name must be a non-empty String, but %v given.',
402
+ propName,
403
+ );
404
+ const utils = this.getService(ModelDefinitionUtils);
405
+ let colName = propName;
406
+ try {
407
+ colName = utils.getColumnNameByPropertyName(modelName, propName);
408
+ } catch (error) {
409
+ if (
410
+ !(error instanceof InvalidArgumentError) ||
411
+ error.message.indexOf('does not have the property') === -1
412
+ ) {
413
+ throw error;
414
+ }
415
+ }
416
+ return colName;
417
+ }
418
+
419
+ /**
420
+ * Build sort.
421
+ *
422
+ * @param {string} modelName
423
+ * @param {string|string[]} clause
424
+ * @return {object|undefined}
425
+ * @private
426
+ */
427
+ _buildSort(modelName, clause) {
428
+ if (!clause) return;
429
+ clause = Array.isArray(clause) ? clause : [clause];
430
+ if (!clause.length) return;
431
+ const utils = this.getService(ModelDefinitionUtils);
432
+ const idPropName = this._getIdPropName(modelName);
433
+ return clause.reduce((acc, order) => {
434
+ if (!order || typeof order !== 'string')
435
+ throw new InvalidArgumentError(
436
+ 'A field order must be a non-empty String, but %v given.',
437
+ order,
438
+ );
439
+ const direction = order.match(/\s+(A|DE)SC$/);
440
+ let key = order.replace(/\s+(A|DE)SC$/, '').trim();
441
+ if (key === idPropName) {
442
+ key = '_id';
443
+ } else {
444
+ try {
445
+ key = utils.getColumnNameByPropertyName(modelName, key);
446
+ } catch (error) {
447
+ if (
448
+ !(error instanceof InvalidArgumentError) ||
449
+ error.message.indexOf('does not have the property') === -1
450
+ ) {
451
+ throw error;
452
+ }
453
+ }
454
+ }
455
+ acc[key] = direction && direction[1] === 'DE' ? -1 : 1;
456
+ return acc;
457
+ }, {});
458
+ }
459
+
460
+ /**
461
+ * Build query.
462
+ *
463
+ * @param {string} modelName
464
+ * @param {object} clause
465
+ * @return {object}
466
+ * @private
467
+ */
468
+ _buildQuery(modelName, clause) {
469
+ const query = {};
470
+ if (!clause || typeof clause !== 'object') return query;
471
+ const idPropName = this._getIdPropName(modelName);
472
+ Object.keys(clause).forEach(key => {
473
+ let cond = clause[key];
474
+ // and/or/nor clause
475
+ if (key === 'and' || key === 'or' || key === 'nor') {
476
+ if (Array.isArray(cond))
477
+ cond = cond.map(c => this._buildQuery(modelName, c));
478
+ query['$' + key] = cond;
479
+ return;
480
+ }
481
+ // id
482
+ if (key === idPropName) {
483
+ key = '_id';
484
+ } else {
485
+ key = this._getColName(modelName, key);
486
+ }
487
+ // string
488
+ if (typeof cond === 'string') {
489
+ query[key] = this._coerceId(cond);
490
+ return;
491
+ }
492
+ // ObjectId
493
+ if (cond instanceof ObjectId) {
494
+ query[key] = cond;
495
+ return;
496
+ }
497
+ // operator
498
+ if (cond && cond.constructor && cond.constructor.name === 'Object') {
499
+ // eq
500
+ if ('eq' in cond) {
501
+ query[key] = this._coerceId(cond.eq);
502
+ }
503
+ // neq
504
+ if ('neq' in cond) {
505
+ query[key] = {$ne: this._coerceId(cond.neq)};
506
+ }
507
+ // gt
508
+ if ('gt' in cond) {
509
+ query[key] = {$gt: cond.gt};
510
+ }
511
+ // lt
512
+ if ('lt' in cond) {
513
+ query[key] = {$lt: cond.lt};
514
+ }
515
+ // gte
516
+ if ('gte' in cond) {
517
+ query[key] = {$gte: cond.gte};
518
+ }
519
+ // lte
520
+ if ('lte' in cond) {
521
+ query[key] = {$lte: cond.lte};
522
+ }
523
+ // inq
524
+ if ('inq' in cond) {
525
+ if (!cond.inq || !Array.isArray(cond.inq))
526
+ throw new InvalidOperatorValueError(
527
+ 'inq',
528
+ 'an Array of possible values',
529
+ cond.inq,
530
+ );
531
+ query[key] = {$in: cond.inq.map(v => this._coerceId(v))};
532
+ }
533
+ // nin
534
+ if ('nin' in cond) {
535
+ if (!cond.nin || !Array.isArray(cond.nin))
536
+ throw new InvalidOperatorValueError(
537
+ 'nin',
538
+ 'an Array of possible values',
539
+ cond,
540
+ );
541
+ query[key] = {$nin: cond.nin.map(v => this._coerceId(v))};
542
+ }
543
+ // between
544
+ if ('between' in cond) {
545
+ if (!Array.isArray(cond.between) || cond.between.length !== 2)
546
+ throw new InvalidOperatorValueError(
547
+ 'between',
548
+ 'an Array of 2 elements',
549
+ cond.between,
550
+ );
551
+ query[key] = {$gte: cond.between[0], $lte: cond.between[1]};
552
+ }
553
+ // exists
554
+ if ('exists' in cond) {
555
+ if (typeof cond.exists !== 'boolean')
556
+ throw new InvalidOperatorValueError(
557
+ 'exists',
558
+ 'a Boolean',
559
+ cond.exists,
560
+ );
561
+ query[key] = {$exists: cond.exists};
562
+ }
563
+ // like
564
+ if ('like' in cond) {
565
+ if (typeof cond.like !== 'string' && !(cond.like instanceof RegExp))
566
+ throw new InvalidOperatorValueError(
567
+ 'like',
568
+ 'a String or RegExp',
569
+ cond.like,
570
+ );
571
+ query[key] = {$regex: stringToRegexp(cond.like)};
572
+ }
573
+ // nlike
574
+ if ('nlike' in cond) {
575
+ if (typeof cond.nlike !== 'string' && !(cond.nlike instanceof RegExp))
576
+ throw new InvalidOperatorValueError(
577
+ 'nlike',
578
+ 'a String or RegExp',
579
+ cond.nlike,
580
+ );
581
+ query[key] = {$not: stringToRegexp(cond.nlike)};
582
+ }
583
+ // ilike
584
+ if ('ilike' in cond) {
585
+ if (typeof cond.ilike !== 'string' && !(cond.ilike instanceof RegExp))
586
+ throw new InvalidOperatorValueError(
587
+ 'ilike',
588
+ 'a String or RegExp',
589
+ cond.ilike,
590
+ );
591
+ query[key] = {$regex: stringToRegexp(cond.ilike, 'i')};
592
+ }
593
+ // nilike
594
+ if ('nilike' in cond) {
595
+ if (
596
+ typeof cond.nilike !== 'string' &&
597
+ !(cond.nilike instanceof RegExp)
598
+ ) {
599
+ throw new InvalidOperatorValueError(
600
+ 'nilike',
601
+ 'a String or RegExp',
602
+ cond.nilike,
603
+ );
604
+ }
605
+ query[key] = {$not: stringToRegexp(cond.nilike, 'i')};
606
+ }
607
+ // regexp and flags (optional)
608
+ if ('regexp' in cond) {
609
+ if (
610
+ typeof cond.regexp !== 'string' &&
611
+ !(cond.regexp instanceof RegExp)
612
+ ) {
613
+ throw new InvalidOperatorValueError(
614
+ 'regexp',
615
+ 'a String or RegExp',
616
+ cond.regexp,
617
+ );
618
+ }
619
+ const flags = cond.flags || undefined;
620
+ if (flags && typeof flags !== 'string')
621
+ throw new InvalidArgumentError(
622
+ 'RegExp flags must be a String, but %v given.',
623
+ cond.flags,
624
+ );
625
+ query[key] = {$regex: stringToRegexp(cond.regexp, flags)};
626
+ }
627
+ return;
628
+ }
629
+ // unknown
630
+ query[key] = cond;
631
+ });
632
+ return query;
633
+ }
634
+
635
+ /**
636
+ * Create.
637
+ *
638
+ * @param {string} modelName
639
+ * @param {object} modelData
640
+ * @param {object|undefined} filter
641
+ * @return {Promise<object>}
642
+ */
643
+ async create(modelName, modelData, filter = undefined) {
644
+ await this.connect();
645
+ const idPropName = this._getIdPropName(modelName);
646
+ const idValue = modelData[idPropName];
647
+ if (idValue == null) {
648
+ const pkType = this._getIdType(modelName);
649
+ if (pkType !== DataType.STRING && pkType !== DataType.ANY)
650
+ throw new InvalidArgumentError(
651
+ 'MongoDB unable to generate primary keys of %s. ' +
652
+ 'Do provide your own value for the %v property ' +
653
+ 'or set property type to String.',
654
+ capitalize(pkType),
655
+ idPropName,
656
+ );
657
+ delete modelData[idPropName];
658
+ }
659
+ const tableData = this._toDatabase(modelName, modelData);
660
+ const table = this._getCollection(modelName);
661
+ const {insertedId} = await table.insertOne(tableData);
662
+ const projection = this._buildProjection(
663
+ modelName,
664
+ filter && filter.fields,
665
+ );
666
+ const insertedData = await table.findOne({_id: insertedId}, {projection});
667
+ return this._fromDatabase(modelName, insertedData);
668
+ }
669
+
670
+ /**
671
+ * Replace by id.
672
+ *
673
+ * @param {string} modelName
674
+ * @param {string|number} id
675
+ * @param {object} modelData
676
+ * @param {object|undefined} filter
677
+ * @return {Promise<object>}
678
+ */
679
+ async replaceById(modelName, id, modelData, filter = undefined) {
680
+ await this.connect();
681
+ id = this._coerceId(id);
682
+ const idPropName = this._getIdPropName(modelName);
683
+ modelData[idPropName] = id;
684
+ const tableData = this._toDatabase(modelName, modelData);
685
+ const table = this._getCollection(modelName);
686
+ const {modifiedCount} = await table.replaceOne({_id: id}, tableData);
687
+ if (modifiedCount < 1)
688
+ throw new InvalidArgumentError('Identifier %v is not found.', String(id));
689
+ const projection = this._buildProjection(
690
+ modelName,
691
+ filter && filter.fields,
692
+ );
693
+ const replacedData = await table.findOne({_id: id}, {projection});
694
+ return this._fromDatabase(modelName, replacedData);
695
+ }
696
+
697
+ /**
698
+ * Patch by id.
699
+ *
700
+ * @param {string} modelName
701
+ * @param {string|number} id
702
+ * @param {object} modelData
703
+ * @param {object|undefined} filter
704
+ * @return {Promise<object>}
705
+ */
706
+ async patchById(modelName, id, modelData, filter = undefined) {
707
+ await this.connect();
708
+ id = this._coerceId(id);
709
+ const idPropName = this._getIdPropName(modelName);
710
+ delete modelData[idPropName];
711
+ const tableData = this._toDatabase(modelName, modelData);
712
+ const table = this._getCollection(modelName);
713
+ const {modifiedCount} = await table.updateOne({_id: id}, {$set: tableData});
714
+ if (modifiedCount < 1)
715
+ throw new InvalidArgumentError('Identifier %v is not found.', String(id));
716
+ const projection = this._buildProjection(
717
+ modelName,
718
+ filter && filter.fields,
719
+ );
720
+ const patchedData = await table.findOne({_id: id}, {projection});
721
+ return this._fromDatabase(modelName, patchedData);
722
+ }
723
+
724
+ /**
725
+ * Find.
726
+ *
727
+ * @param {string} modelName
728
+ * @param {object|undefined} filter
729
+ * @return {Promise<object[]>}
730
+ */
731
+ async find(modelName, filter = undefined) {
732
+ await this.connect();
733
+ filter = filter || {};
734
+ const query = this._buildQuery(modelName, filter.where);
735
+ const sort = this._buildSort(modelName, filter.order);
736
+ const limit = filter.limit || undefined;
737
+ const skip = filter.skip || undefined;
738
+ const projection = this._buildProjection(modelName, filter.fields);
739
+ const collection = this._getCollection(modelName);
740
+ const options = {sort, limit, skip, projection};
741
+ const tableItems = await collection.find(query, options).toArray();
742
+ return tableItems.map(v => this._fromDatabase(modelName, v));
743
+ }
744
+
745
+ /**
746
+ * Find by id.
747
+ *
748
+ * @param {string} modelName
749
+ * @param {string|number} id
750
+ * @param {object|undefined} filter
751
+ * @return {Promise<object>}
752
+ */
753
+ async findById(modelName, id, filter = undefined) {
754
+ await this.connect();
755
+ id = this._coerceId(id);
756
+ const table = this._getCollection(modelName);
757
+ const projection = this._buildProjection(
758
+ modelName,
759
+ filter && filter.fields,
760
+ );
761
+ const patchedData = await table.findOne({_id: id}, {projection});
762
+ if (!patchedData)
763
+ throw new InvalidArgumentError('Identifier %v is not found.', String(id));
764
+ return this._fromDatabase(modelName, patchedData);
765
+ }
766
+
767
+ /**
768
+ * Delete.
769
+ *
770
+ * @param {string} modelName
771
+ * @param {object|undefined} where
772
+ * @return {Promise<number>}
773
+ */
774
+ async delete(modelName, where = undefined) {
775
+ await this.connect();
776
+ const table = this._getCollection(modelName);
777
+ const query = this._buildQuery(modelName, where);
778
+ const {deletedCount} = await table.deleteMany(query);
779
+ return deletedCount;
780
+ }
781
+
782
+ /**
783
+ * Delete by id.
784
+ *
785
+ * @param {string} modelName
786
+ * @param {string|number} id
787
+ * @return {Promise<boolean>}
788
+ */
789
+ async deleteById(modelName, id) {
790
+ await this.connect();
791
+ id = this._coerceId(id);
792
+ const table = this._getCollection(modelName);
793
+ const {deletedCount} = await table.deleteOne({_id: id});
794
+ return deletedCount > 0;
795
+ }
796
+
797
+ /**
798
+ * Exists.
799
+ *
800
+ * @param {string} modelName
801
+ * @param {string|number} id
802
+ * @return {Promise<boolean>}
803
+ */
804
+ async exists(modelName, id) {
805
+ await this.connect();
806
+ id = this._coerceId(id);
807
+ const table = this._getCollection(modelName);
808
+ const result = await table.findOne({_id: id}, {});
809
+ return result != null;
810
+ }
811
+
812
+ /**
813
+ * Count.
814
+ *
815
+ * @param {string} modelName
816
+ * @param {object|undefined} where
817
+ * @return {Promise<number>}
818
+ */
819
+ async count(modelName, where = undefined) {
820
+ await this.connect();
821
+ const query = this._buildQuery(modelName, where);
822
+ const table = this._getCollection(modelName);
823
+ return await table.count(query);
824
+ }
825
+ }