@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.
- package/.c8rc +9 -0
- package/.commitlintrc +5 -0
- package/.editorconfig +13 -0
- package/.eslintrc.cjs +20 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +9 -0
- package/.mocharc.cjs +7 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/mocha.setup.js +13 -0
- package/package.json +59 -0
- package/setup.sh +39 -0
- package/src/index.js +1 -0
- package/src/mongodb-adapter.js +825 -0
- package/src/mongodb-adapter.spec.js +2391 -0
- package/src/utils/create-mongodb-url.js +87 -0
- package/src/utils/create-mongodb-url.spec.js +29 -0
- package/src/utils/index.js +3 -0
- package/src/utils/is-iso-date.js +13 -0
- package/src/utils/is-object-id.js +14 -0
- package/src/utils/is-object-id.spec.js +24 -0
- package/src/utils/transform-values-deep.js +38 -0
- package/src/utils/transform-values-deep.spec.js +47 -0
- package/test.env +4 -0
|
@@ -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
|
+
}
|