@vsaas/loopback-connector-mongodb 10.0.0

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,1523 @@
1
+ //#region src/lib/mongodb.ts
2
+ const mongodb = require("mongodb");
3
+ const mongoObjectID = mongodb.ObjectId;
4
+ const util = require("util");
5
+ const Connector = require("@vsaas/loopback-connector").Connector;
6
+ const debug = require("debug")("loopback:connector:mongodb");
7
+ const Decimal128 = mongodb.Decimal128;
8
+ const Decimal128TypeRegex = /decimal128/i;
9
+ const ObjectIdValueRegex = /^[0-9a-fA-F]{24}$/;
10
+ const ObjectIdTypeRegex = /objectid/i;
11
+ exports.ObjectID = ObjectID;
12
+ /*!
13
+ * Convert the id to be a BSON ObjectID if it is compatible
14
+ * @param {*} id The id value
15
+ * @returns {ObjectID}
16
+ */
17
+ function ObjectID(id) {
18
+ if (id instanceof mongoObjectID) return id;
19
+ if (typeof id !== "string") return id;
20
+ try {
21
+ if (ObjectIdValueRegex.test(id)) return new mongoObjectID(id);
22
+ else return id;
23
+ } catch (e) {
24
+ return id;
25
+ }
26
+ }
27
+ exports.generateMongoDBURL = generateMongoDBURL;
28
+ /*!
29
+ * Generate the mongodb URL from the options
30
+ */
31
+ function generateMongoDBURL(options) {
32
+ options.protocol = options.protocol || "mongodb";
33
+ options.hostname = options.hostname || options.host || "127.0.0.1";
34
+ options.port = options.port || 27017;
35
+ options.database = options.database || options.db || "test";
36
+ const username = options.username || options.user;
37
+ let portUrl = "";
38
+ if (options.protocol !== "mongodb+srv") portUrl = ":" + options.port;
39
+ if (username && options.password) return options.protocol + "://" + username + ":" + options.password + "@" + options.hostname + portUrl + "/" + options.database;
40
+ else return options.protocol + "://" + options.hostname + portUrl + "/" + options.database;
41
+ }
42
+ function extractDatabaseNameFromUrl(url) {
43
+ if (!url || typeof url !== "string") return void 0;
44
+ const protocolSeparatorIndex = url.indexOf("://");
45
+ if (protocolSeparatorIndex === -1) return void 0;
46
+ const pathStartIndex = url.indexOf("/", protocolSeparatorIndex + 3);
47
+ if (pathStartIndex === -1) return void 0;
48
+ const path = url.slice(pathStartIndex + 1);
49
+ if (!path) return void 0;
50
+ const queryStartIndex = path.indexOf("?");
51
+ const databaseName = queryStartIndex === -1 ? path : path.slice(0, queryStartIndex);
52
+ return databaseName ? decodeURIComponent(databaseName) : void 0;
53
+ }
54
+ function pickSupportedOptions(source, names) {
55
+ const validOptions = {};
56
+ for (const option of Object.keys(source)) if (names.includes(option)) validOptions[option] = source[option];
57
+ return validOptions;
58
+ }
59
+ function isNamespaceMissingError(error) {
60
+ return !!(error && error.name === "MongoServerError" && (error.codeName === "NamespaceNotFound" || error.errmsg === "ns not found" || error.message.includes("ns not found")));
61
+ }
62
+ function normalizeMongoError(error) {
63
+ if (!error || typeof error !== "object") return error;
64
+ if (error.name === "MongoServerError") {
65
+ const normalizedError = new mongodb.MongoError(error.message);
66
+ Object.assign(normalizedError, error);
67
+ normalizedError.errmsg = error.errmsg ?? error.message;
68
+ normalizedError.stack = error.stack;
69
+ return normalizedError;
70
+ }
71
+ if (error.errmsg === void 0 && error.message) error.errmsg = error.message;
72
+ return error;
73
+ }
74
+ function applyProjectionToDocument(document, projection) {
75
+ if (!projection || !document || typeof document !== "object") return document;
76
+ const projectedDocument = {};
77
+ const projectionKeys = Object.keys(projection);
78
+ if (projectionKeys.length === 0) return document;
79
+ let hasInclusiveField = false;
80
+ for (const key of projectionKeys) if (key !== "_id" && projection[key]) {
81
+ hasInclusiveField = true;
82
+ break;
83
+ }
84
+ if (hasInclusiveField) {
85
+ for (const key of projectionKeys) if (projection[key] && document[key] !== void 0) projectedDocument[key] = document[key];
86
+ if (projection._id !== 0 && document._id !== void 0) projectedDocument._id = document._id;
87
+ return projectedDocument;
88
+ }
89
+ Object.assign(projectedDocument, document);
90
+ for (const key of projectionKeys) if (!projection[key]) delete projectedDocument[key];
91
+ return projectedDocument;
92
+ }
93
+ /**
94
+ * Initialize the MongoDB connector for the given data source
95
+ * @param {DataSource} dataSource The data source instance
96
+ * @param {Function} [callback] The callback function
97
+ */
98
+ exports.initialize = function initializeDataSource(dataSource, callback) {
99
+ if (!mongodb) return;
100
+ const s = dataSource.settings;
101
+ s.safe = s.safe !== false;
102
+ s.w = s.w || 1;
103
+ s.url = s.url || generateMongoDBURL(s);
104
+ s.useNewUrlParser = s.useNewUrlParser !== false;
105
+ s.useUnifiedTopology = s.useUnifiedTopology !== false;
106
+ dataSource.connector = new MongoDB(s, dataSource);
107
+ dataSource.ObjectID = mongoObjectID;
108
+ if (callback) if (s.lazyConnect) process.nextTick(function() {
109
+ callback();
110
+ });
111
+ else dataSource.connector.connect(callback);
112
+ };
113
+ const COMMAND_MAPPINGS = {
114
+ insertOne: "insert",
115
+ updateOne: "save",
116
+ findOneAndUpdate: "findAndModify",
117
+ deleteOne: "delete",
118
+ deleteMany: "delete",
119
+ replaceOne: "update",
120
+ updateMany: "update",
121
+ countDocuments: "count",
122
+ estimatedDocumentCount: "count"
123
+ };
124
+ const ACCEPTED_UPDATE_OPERATORS = [
125
+ "$currentDate",
126
+ "$inc",
127
+ "$max",
128
+ "$min",
129
+ "$mul",
130
+ "$rename",
131
+ "$setOnInsert",
132
+ "$set",
133
+ "$unset",
134
+ "$addToSet",
135
+ "$pop",
136
+ "$pullAll",
137
+ "$pull",
138
+ "$push",
139
+ "$bit"
140
+ ];
141
+ /**
142
+ * Helper function to be used in {@ fieldsArrayToObj} in order for V8 to avoid re-creating a new
143
+ * function every time {@ fieldsArrayToObj} is called
144
+ *
145
+ * @see fieldsArrayToObj
146
+ * @param {object} result
147
+ * @param {string} field
148
+ * @returns {object}
149
+ */
150
+ function arrayToObjectReducer(result, field) {
151
+ result[field] = 1;
152
+ return result;
153
+ }
154
+ exports.fieldsArrayToObj = fieldsArrayToObj;
155
+ /**
156
+ * Helper function to accept an array representation of fields projection and return the mongo
157
+ * required object notation
158
+ *
159
+ * @param {string[]} fieldsArray
160
+ * @returns {Object}
161
+ */
162
+ function fieldsArrayToObj(fieldsArray) {
163
+ if (!Array.isArray(fieldsArray)) return fieldsArray;
164
+ return fieldsArray.length ? fieldsArray.reduce(arrayToObjectReducer, {}) : { _id: 1 };
165
+ }
166
+ exports.MongoDB = MongoDB;
167
+ /**
168
+ * The constructor for MongoDB connector
169
+ * @param {Object} settings The settings object
170
+ * @param {DataSource} dataSource The data source instance
171
+ * @constructor
172
+ */
173
+ function MongoDB(settings, dataSource) {
174
+ Connector.call(this, "mongodb", settings);
175
+ this.debug = settings.debug || debug.enabled;
176
+ if (this.debug) debug("Settings: %j", settings);
177
+ this.dataSource = dataSource;
178
+ MongoDB.prototype.findOrCreate = optimizedFindOrCreate;
179
+ if (this.settings.enableGeoIndexing === true) MongoDB.prototype.buildNearFilter = buildNearFilter;
180
+ else MongoDB.prototype.buildNearFilter = void 0;
181
+ }
182
+ util.inherits(MongoDB, Connector);
183
+ /**
184
+ * Connect to MongoDB
185
+ * @param {Function} [callback] The callback function
186
+ *
187
+ * @callback callback
188
+ * @param {Error} err The error object
189
+ * @param {Db} db The mongo DB object
190
+ */
191
+ MongoDB.prototype.connect = function(callback) {
192
+ const self = this;
193
+ if (self.db) process.nextTick(function() {
194
+ if (callback) callback(null, self.db);
195
+ });
196
+ else if (self.dataSource.connecting) self.dataSource.once("connected", function() {
197
+ process.nextTick(function() {
198
+ if (callback) callback(null, self.db);
199
+ });
200
+ });
201
+ else {
202
+ const validOptions = pickSupportedOptions(self.settings, [
203
+ "poolSize",
204
+ "ssl",
205
+ "sslValidate",
206
+ "sslCA",
207
+ "sslCert",
208
+ "sslKey",
209
+ "sslPass",
210
+ "sslCRL",
211
+ "autoReconnect",
212
+ "noDelay",
213
+ "keepAlive",
214
+ "keepAliveInitialDelay",
215
+ "connectTimeoutMS",
216
+ "serverSelectionTimeoutMS",
217
+ "family",
218
+ "socketTimeoutMS",
219
+ "reconnectTries",
220
+ "reconnectInterval",
221
+ "ha",
222
+ "haInterval",
223
+ "replicaSet",
224
+ "secondaryAcceptableLatencyMS",
225
+ "acceptableLatencyMS",
226
+ "connectWithNoPrimary",
227
+ "authSource",
228
+ "w",
229
+ "wtimeout",
230
+ "j",
231
+ "forceServerObjectId",
232
+ "serializeFunctions",
233
+ "ignoreUndefined",
234
+ "raw",
235
+ "bufferMaxEntries",
236
+ "readPreference",
237
+ "pkFactory",
238
+ "promiseLibrary",
239
+ "readConcern",
240
+ "maxStalenessSeconds",
241
+ "loggerLevel",
242
+ "logger",
243
+ "promoteValues",
244
+ "promoteBuffers",
245
+ "promoteLongs",
246
+ "domainsEnabled",
247
+ "checkServerIdentity",
248
+ "validateOptions",
249
+ "appname",
250
+ "auth",
251
+ "user",
252
+ "password",
253
+ "authMechanism",
254
+ "compression",
255
+ "fsync",
256
+ "readPreferenceTags",
257
+ "numberOfRetries",
258
+ "auto_reconnect",
259
+ "minSize",
260
+ "useNewUrlParser",
261
+ "useUnifiedTopology",
262
+ "native_parser",
263
+ "server",
264
+ "replset",
265
+ "replSet",
266
+ "mongos",
267
+ "db"
268
+ ]);
269
+ debug("Valid options: %j", validOptions);
270
+ function onError(err) {
271
+ err = normalizeMongoError(err);
272
+ /* istanbul ignore if */
273
+ if (self.debug) console.error("MongoDB connection failed: %s %s", self.settings.url, err);
274
+ if (callback) callback(err);
275
+ }
276
+ const client = new mongodb.MongoClient(self.settings.url, validOptions);
277
+ client.connect().then(function() {
278
+ if (self.debug) debug("MongoDB connection is established: " + self.settings.url);
279
+ self.client = client;
280
+ const validDbOptions = pickSupportedOptions(self.settings, [
281
+ "authSource",
282
+ "forceServerObjectId",
283
+ "readPreference",
284
+ "pkFactory",
285
+ "readConcern",
286
+ "retryWrites",
287
+ "checkKeys",
288
+ "serializeFunctions",
289
+ "ignoreUndefined",
290
+ "promoteLongs",
291
+ "promoteBuffers",
292
+ "promoteValues",
293
+ "fieldsAsRaw",
294
+ "bsonRegExp",
295
+ "raw",
296
+ "writeConcern",
297
+ "logger",
298
+ "loggerLevel"
299
+ ]);
300
+ const databaseName = extractDatabaseNameFromUrl(self.settings.url) || self.settings.database || self.settings.db;
301
+ self.db = client.db(databaseName, validDbOptions);
302
+ self.db.topology = client.topology;
303
+ if (callback) callback(null, self.db);
304
+ }).catch(onError);
305
+ }
306
+ };
307
+ MongoDB.prototype.getTypes = function() {
308
+ return [
309
+ "db",
310
+ "nosql",
311
+ "mongodb"
312
+ ];
313
+ };
314
+ MongoDB.prototype.getDefaultIdType = function() {
315
+ return ObjectID;
316
+ };
317
+ /**
318
+ * Get collection name for a given model
319
+ * @param {String} modelName The model name
320
+ * @returns {String} collection name
321
+ */
322
+ MongoDB.prototype.collectionName = function(modelName) {
323
+ const modelClass = this._models[modelName];
324
+ if (modelClass?.settings?.mongodb) modelName = modelClass.settings.mongodb.collection || modelName;
325
+ return modelName;
326
+ };
327
+ /**
328
+ * Access a MongoDB collection by model name
329
+ * @param {String} modelName The model name
330
+ * @returns {*}
331
+ */
332
+ MongoDB.prototype.collection = function(modelName) {
333
+ if (!this.db) throw new Error("MongoDB connection is not established");
334
+ const collectionName = this.collectionName(modelName);
335
+ return this.db.collection(collectionName);
336
+ };
337
+ /*!
338
+ * Convert the data from database to JSON
339
+ *
340
+ * @param {String} modelName The model name
341
+ * @param {Object} data The data from DB
342
+ */
343
+ MongoDB.prototype.fromDatabase = function(modelName, data) {
344
+ if (!data) return null;
345
+ const props = (this._models[modelName] || this.dataSource.modelBuilder.definitions[modelName]).properties;
346
+ for (const p in props) {
347
+ const prop = props[p];
348
+ if (prop && prop.type === Buffer) {
349
+ if (data[p] instanceof mongodb.Binary) data[p] = data[p].read(0, data[p].length());
350
+ } else if (prop && prop.type === String) {
351
+ if (data[p] instanceof mongodb.Binary) data[p] = data[p].toString();
352
+ } else if (data[p] && prop && prop.type && prop.type.name === "GeoPoint" && this.settings.enableGeoIndexing === true) data[p] = {
353
+ lat: data[p].coordinates[1],
354
+ lng: data[p].coordinates[0]
355
+ };
356
+ else if (data[p] && prop && prop.type.definition) data[p] = this.fromDatabase(prop.type.definition.name, data[p]);
357
+ }
358
+ data = this.fromDatabaseToPropertyNames(modelName, data);
359
+ return data;
360
+ };
361
+ /*!
362
+ * Convert JSON to database-appropriate format
363
+ *
364
+ * @param {String} modelName The model name
365
+ * @param {Object} data The JSON data to convert
366
+ */
367
+ MongoDB.prototype.toDatabase = function(modelName, data) {
368
+ const modelCtor = this._models[modelName];
369
+ const props = modelCtor.properties;
370
+ if (this.settings.enableGeoIndexing !== true) {
371
+ visitAllProperties(data, modelCtor, coercePropertyValue);
372
+ data = this.fromPropertyToDatabaseNames(modelName, data);
373
+ return data;
374
+ }
375
+ for (const p in props) {
376
+ const prop = props[p];
377
+ if (data[p] && prop && prop.type && prop.type.name === "GeoPoint") data[p] = {
378
+ coordinates: [data[p].lng, data[p].lat],
379
+ type: "Point"
380
+ };
381
+ }
382
+ visitAllProperties(data, modelCtor, coercePropertyValue);
383
+ data = this.fromPropertyToDatabaseNames(modelName, data);
384
+ if (debug.enabled) debug("toDatabase data: ", util.inspect(data));
385
+ return data;
386
+ };
387
+ function doExecute(self, modelName, command, args, callback) {
388
+ let collection;
389
+ const context = Object.assign({}, {
390
+ model: modelName,
391
+ collection,
392
+ req: {
393
+ command,
394
+ params: args
395
+ }
396
+ });
397
+ try {
398
+ collection = self.collection(modelName);
399
+ } catch (err) {
400
+ err = normalizeMongoError(err);
401
+ debug("Error: ", err);
402
+ callback(err);
403
+ return;
404
+ }
405
+ if (command in COMMAND_MAPPINGS) context.req.command = COMMAND_MAPPINGS[command];
406
+ self.notifyObserversAround("execute", context, function(context, done) {
407
+ debug("MongoDB: model=%s command=%s", modelName, command, args);
408
+ try {
409
+ const result = collection[command].apply(collection, args);
410
+ if (result && typeof result.then === "function") result.then(function(res) {
411
+ context.res = res;
412
+ done(null, res);
413
+ }, function(err) {
414
+ err = normalizeMongoError(err);
415
+ debug("Error: ", err);
416
+ done(err);
417
+ });
418
+ else {
419
+ context.res = result;
420
+ done(null, result);
421
+ }
422
+ } catch (err) {
423
+ err = normalizeMongoError(err);
424
+ debug("Error: ", err);
425
+ done(err);
426
+ return;
427
+ }
428
+ }, callback);
429
+ }
430
+ /**
431
+ * Execute a mongodb command
432
+ * @param {String} modelName The model name
433
+ * @param {String} command The command name
434
+ * @param [...] params Parameters for the given command
435
+ */
436
+ MongoDB.prototype.execute = function(modelName, command) {
437
+ const self = this;
438
+ const args = [].slice.call(arguments, 2, arguments.length - 1);
439
+ const callback = arguments[arguments.length - 1];
440
+ if (self.db && self.db.topology && !self.db.topology.isDestroyed()) doExecute(self, modelName, command, args, callback);
441
+ else if (self.db && !self.db.topology) doExecute(self, modelName, command, args, callback);
442
+ else {
443
+ if (self.db) {
444
+ self.disconnect();
445
+ self.db = null;
446
+ }
447
+ self.connect(function(err, db) {
448
+ if (err) debug("Connection not established - MongoDB: model=%s command=%s -- error=%s", modelName, command, err);
449
+ doExecute(self, modelName, command, args, callback);
450
+ });
451
+ }
452
+ };
453
+ MongoDB.prototype.coerceId = function(modelName, id, options) {
454
+ if (id == null) return id;
455
+ const self = this;
456
+ let idValue = id;
457
+ const idName = self.idName(modelName);
458
+ const idProp = self.getPropertyDefinition(modelName, idName);
459
+ if (idProp && typeof idProp.type === "function") {
460
+ if (!(idValue instanceof idProp.type)) {
461
+ idValue = idProp.type(id);
462
+ if (idProp.type === Number && isNaN(id)) idValue = id;
463
+ }
464
+ const modelCtor = this._models[modelName];
465
+ idValue = coerceToObjectId(modelCtor, idProp, idValue);
466
+ }
467
+ return idValue;
468
+ };
469
+ /**
470
+ * Create a new model instance for the given data
471
+ * @param {String} modelName The model name
472
+ * @param {Object} data The model data
473
+ * @param {Function} [callback] The callback function
474
+ */
475
+ MongoDB.prototype.create = function(modelName, data, options, callback) {
476
+ const self = this;
477
+ if (self.debug) debug("create", modelName, data);
478
+ let idValue = self.getIdValue(modelName, data);
479
+ const idName = self.idName(modelName);
480
+ if (idValue === null) delete data[idName];
481
+ else {
482
+ const oid = self.coerceId(modelName, idValue, options);
483
+ data._id = oid;
484
+ if (idName !== "_id") delete data[idName];
485
+ }
486
+ data = self.toDatabase(modelName, data);
487
+ this.execute(modelName, "insertOne", data, buildOptions({ safe: true }, options), function(err, result) {
488
+ if (self.debug) debug("create.callback", modelName, err, result);
489
+ if (err) return callback(err);
490
+ idValue = result.insertedId;
491
+ try {
492
+ idValue = self.coerceId(modelName, idValue, options);
493
+ } catch (err) {
494
+ return callback(err);
495
+ }
496
+ process.nextTick(function() {
497
+ delete data._id;
498
+ data[idName] = idValue;
499
+ callback(err, err ? null : idValue);
500
+ });
501
+ });
502
+ };
503
+ /**
504
+ * Save the model instance for the given data
505
+ * @param {String} modelName The model name
506
+ * @param {Object} data The model data
507
+ * @param {Function} [callback] The callback function
508
+ */
509
+ MongoDB.prototype.save = function(modelName, data, options, callback) {
510
+ const self = this;
511
+ if (self.debug) debug("save", modelName, data);
512
+ const idValue = self.getIdValue(modelName, data);
513
+ const idName = self.idName(modelName);
514
+ const oid = self.coerceId(modelName, idValue, options);
515
+ data._id = oid;
516
+ if (idName !== "_id") delete data[idName];
517
+ data = self.toDatabase(modelName, data);
518
+ this.execute(modelName, "updateOne", { _id: oid }, { $set: data }, buildOptions({ upsert: true }, options), function(err, result) {
519
+ if (!err) {
520
+ self.setIdValue(modelName, data, idValue);
521
+ if (idName !== "_id") delete data._id;
522
+ }
523
+ if (self.debug) debug("save.callback", modelName, err, result);
524
+ const info = {};
525
+ if (result) if (result.acknowledged === true && (result.matchedCount === 1 || result.upsertedCount === 1)) info.isNewInstance = result.upsertedCount === 1;
526
+ else debug("save result format not recognized: %j", result);
527
+ if (callback) callback(err, result && result.ops, info);
528
+ });
529
+ };
530
+ /**
531
+ * Check if a model instance exists by id
532
+ * @param {String} modelName The model name
533
+ * @param {*} id The id value
534
+ * @param {Function} [callback] The callback function
535
+ *
536
+ */
537
+ MongoDB.prototype.exists = function(modelName, id, options, callback) {
538
+ const self = this;
539
+ if (self.debug) debug("exists", modelName, id);
540
+ id = self.coerceId(modelName, id, options);
541
+ this.execute(modelName, "findOne", { _id: id }, function(err, data) {
542
+ if (self.debug) debug("exists.callback", modelName, id, err, data);
543
+ callback(err, !!(!err && data));
544
+ });
545
+ };
546
+ /**
547
+ * Find a model instance by id
548
+ * @param {String} modelName The model name
549
+ * @param {*} id The id value
550
+ * @param {Function} [callback] The callback function
551
+ */
552
+ MongoDB.prototype.find = function find(modelName, id, options, callback) {
553
+ const self = this;
554
+ if (self.debug) debug("find", modelName, id);
555
+ const idName = self.idName(modelName);
556
+ const oid = self.coerceId(modelName, id, options);
557
+ this.execute(modelName, "findOne", { _id: oid }, function(err, data) {
558
+ if (self.debug) debug("find.callback", modelName, id, err, data);
559
+ data = self.fromDatabase(modelName, data);
560
+ if (data && idName !== "_id") delete data._id;
561
+ if (callback) callback(err, data);
562
+ });
563
+ };
564
+ Connector.defineAliases(MongoDB.prototype, "find", "findById");
565
+ /**
566
+ * Parses the data input for update operations and returns the
567
+ * sanitised version of the object.
568
+ *
569
+ * @param data
570
+ * @returns {*}
571
+ */
572
+ MongoDB.prototype.parseUpdateData = function(modelName, data, options) {
573
+ options = options || {};
574
+ const parsedData = {};
575
+ const modelClass = this._models[modelName];
576
+ let allowExtendedOperators = this.settings.allowExtendedOperators;
577
+ if (options.hasOwnProperty("allowExtendedOperators")) allowExtendedOperators = options.allowExtendedOperators === true;
578
+ else if (allowExtendedOperators !== false && modelClass.settings.mongodb && modelClass.settings.mongodb.hasOwnProperty("allowExtendedOperators")) allowExtendedOperators = modelClass.settings.mongodb.allowExtendedOperators === true;
579
+ else if (allowExtendedOperators === true) allowExtendedOperators = true;
580
+ if (allowExtendedOperators) {
581
+ let usedOperators = 0;
582
+ for (let i = 0; i < ACCEPTED_UPDATE_OPERATORS.length; i++) if (data[ACCEPTED_UPDATE_OPERATORS[i]]) {
583
+ parsedData[ACCEPTED_UPDATE_OPERATORS[i]] = data[ACCEPTED_UPDATE_OPERATORS[i]];
584
+ usedOperators++;
585
+ }
586
+ if (usedOperators === 0 && Object.keys(data).length > 0) parsedData.$set = data;
587
+ } else if (Object.keys(data).length > 0) parsedData.$set = data;
588
+ return parsedData;
589
+ };
590
+ /**
591
+ * Update if the model instance exists with the same id or create a new instance
592
+ *
593
+ * @param {String} modelName The model name
594
+ * @param {Object} data The model instance data
595
+ * @param {Function} [callback] The callback function
596
+ */
597
+ MongoDB.prototype.updateOrCreate = function updateOrCreate(modelName, data, options, callback) {
598
+ const self = this;
599
+ if (self.debug) debug("updateOrCreate", modelName, data);
600
+ const id = self.getIdValue(modelName, data);
601
+ const idName = self.idName(modelName);
602
+ const oid = self.coerceId(modelName, id, options);
603
+ delete data[idName];
604
+ data = self.toDatabase(modelName, data);
605
+ data = self.parseUpdateData(modelName, data, options);
606
+ this.execute(modelName, "findOneAndUpdate", { _id: oid }, data, buildOptions({
607
+ upsert: true,
608
+ returnNewDocument: true,
609
+ returnDocument: "after",
610
+ sort: [["_id", "asc"]]
611
+ }, options), function(err, result) {
612
+ if (self.debug) debug("updateOrCreate.callback", modelName, id, err, result);
613
+ const object = result && result.value;
614
+ if (!err && !object) err = "No " + modelName + " found for id " + id;
615
+ if (!err) {
616
+ self.setIdValue(modelName, object, oid);
617
+ if (object && idName !== "_id") delete object._id;
618
+ }
619
+ let info;
620
+ if (result && result.lastErrorObject) info = { isNewInstance: !result.lastErrorObject.updatedExisting };
621
+ else debug("updateOrCreate result format not recognized: %j", result);
622
+ if (callback) callback(err, self.fromDatabase(modelName, object), info);
623
+ });
624
+ };
625
+ /**
626
+ * Replace model instance if it exists or create a new one if it doesn't
627
+ *
628
+ * @param {String} modelName The model name
629
+ * @param {Object} data The model instance data
630
+ * @param {Object} options The options object
631
+ * @param {Function} [cb] The callback function
632
+ */
633
+ MongoDB.prototype.replaceOrCreate = function(modelName, data, options, cb) {
634
+ if (this.debug) debug("replaceOrCreate", modelName, data);
635
+ const id = this.getIdValue(modelName, data);
636
+ const oid = this.coerceId(modelName, id, options);
637
+ const idName = this.idName(modelName);
638
+ data._id = data[idName];
639
+ delete data[idName];
640
+ this.replaceWithOptions(modelName, oid, data, { upsert: true }, cb);
641
+ };
642
+ /**
643
+ * Delete a model instance by id
644
+ * @param {String} modelName The model name
645
+ * @param {*} id The id value
646
+ * @param [callback] The callback function
647
+ */
648
+ MongoDB.prototype.destroy = function destroy(modelName, id, options, callback) {
649
+ const self = this;
650
+ if (!callback && typeof options === "function") {
651
+ callback = options;
652
+ options = void 0;
653
+ }
654
+ if (self.debug) debug("delete", modelName, id);
655
+ id = self.coerceId(modelName, id, options);
656
+ this.execute(modelName, "deleteOne", { _id: id }, function(err, result) {
657
+ if (self.debug) debug("delete.callback", modelName, id, err, result);
658
+ let res;
659
+ if (result && typeof result.deletedCount === "number") res = { count: result.deletedCount };
660
+ else if (result && result.result) res = { count: result.result.n };
661
+ if (callback) callback(err, res);
662
+ });
663
+ };
664
+ /*!
665
+ * Decide if id should be included
666
+ * @param {Object} fields
667
+ * @returns {Boolean}
668
+ * @private
669
+ */
670
+ function idIncluded(fields, idName) {
671
+ if (!fields) return true;
672
+ if (Array.isArray(fields)) return fields.indexOf(idName) >= 0;
673
+ if (fields[idName]) return true;
674
+ if (idName in fields && !fields[idName]) return false;
675
+ for (const f in fields) return !fields[f];
676
+ return true;
677
+ }
678
+ MongoDB.prototype.buildWhere = function(modelName, where, options) {
679
+ const self = this;
680
+ const query = {};
681
+ if (where === null || typeof where !== "object") return query;
682
+ where = sanitizeFilter(where, options);
683
+ let implicitNullType = false;
684
+ if (this.settings.hasOwnProperty("implicitNullType")) implicitNullType = !!this.settings.implicitNullType;
685
+ const idName = self.idName(modelName);
686
+ Object.keys(where).forEach(function(k) {
687
+ let cond = where[k];
688
+ if (k === "and" || k === "or" || k === "nor") {
689
+ if (Array.isArray(cond)) cond = cond.map(function(c) {
690
+ return self.buildWhere(modelName, c, options);
691
+ });
692
+ query["$" + k] = cond;
693
+ delete query[k];
694
+ return;
695
+ }
696
+ if (k === idName) k = "_id";
697
+ let propName = k;
698
+ if (k === "_id") propName = idName;
699
+ const propDef = self.getPropertyDefinition(modelName, propName);
700
+ if (propDef && propDef.mongodb && typeof propDef.mongodb.dataType === "string") {
701
+ if (Decimal128TypeRegex.test(propDef.mongodb.dataType)) {
702
+ cond = Decimal128.fromString(cond);
703
+ debug("buildWhere decimal value: %s, constructor name: %s", cond, cond.constructor.name);
704
+ } else if (isStoredAsObjectID(propDef)) cond = ObjectID(cond);
705
+ }
706
+ k = self.getDatabaseColumnName(modelName, k);
707
+ let spec = false;
708
+ let regexOptions = null;
709
+ if (cond && cond.constructor.name === "Object") {
710
+ regexOptions = cond.options;
711
+ spec = Object.keys(cond)[0];
712
+ cond = cond[spec];
713
+ }
714
+ const modelCtor = self._models[modelName];
715
+ if (spec) {
716
+ spec = trimLeadingDollarSigns(spec);
717
+ if (spec === "between") query[k] = {
718
+ $gte: cond[0],
719
+ $lte: cond[1]
720
+ };
721
+ else if (spec === "inq") {
722
+ cond = [].concat(cond || []);
723
+ query[k] = { $in: cond.map(function(x) {
724
+ if (isObjectIDProperty(modelCtor, propDef, x, options)) return ObjectID(x);
725
+ return x;
726
+ }) };
727
+ } else if (spec === "nin") {
728
+ cond = [].concat(cond || []);
729
+ query[k] = { $nin: cond.map(function(x) {
730
+ if (isObjectIDProperty(modelCtor, propDef, x, options)) return ObjectID(x);
731
+ return x;
732
+ }) };
733
+ } else if (spec === "like") if (cond instanceof RegExp) query[k] = { $regex: cond };
734
+ else query[k] = { $regex: new RegExp(cond, regexOptions) };
735
+ else if (spec === "nlike") if (cond instanceof RegExp) query[k] = { $not: cond };
736
+ else query[k] = { $not: new RegExp(cond, regexOptions) };
737
+ else if (spec === "neq") query[k] = { $ne: cond };
738
+ else if (spec === "regexp") {
739
+ if (cond.global) console.warn("MongoDB regex syntax does not respect the `g` flag");
740
+ query[k] = { $regex: cond };
741
+ } else {
742
+ query[k] = {};
743
+ query[k]["$" + spec] = cond;
744
+ }
745
+ } else if (cond === null && !implicitNullType) query[k] = { $type: 10 };
746
+ else {
747
+ if (isObjectIDProperty(modelCtor, propDef, cond, options)) cond = ObjectID(cond);
748
+ query[k] = cond;
749
+ }
750
+ });
751
+ return query;
752
+ };
753
+ MongoDB.prototype.buildSort = function(modelName, order, options) {
754
+ let sort = {};
755
+ const idName = this.idName(modelName);
756
+ const modelClass = this._models[modelName];
757
+ let disableDefaultSort = false;
758
+ if (this.settings.hasOwnProperty("disableDefaultSort")) disableDefaultSort = this.settings.disableDefaultSort;
759
+ if (modelClass.settings.hasOwnProperty("disableDefaultSort")) disableDefaultSort = modelClass.settings.disableDefaultSort;
760
+ if (options && options.hasOwnProperty("disableDefaultSort")) disableDefaultSort = options.disableDefaultSort;
761
+ if (!order && !disableDefaultSort) {
762
+ const idNames = this.idNames(modelName);
763
+ if (idNames && idNames.length) order = idNames;
764
+ }
765
+ if (order) {
766
+ order = sanitizeFilter(order, options);
767
+ let keys = order;
768
+ if (typeof keys === "string") keys = keys.split(",");
769
+ for (let index = 0, len = keys.length; index < len; index++) {
770
+ const m = keys[index].match(/\s+(A|DE)SC$/);
771
+ let key = keys[index];
772
+ key = key.replace(/\s+(A|DE)SC$/, "").trim();
773
+ if (key === idName) key = "_id";
774
+ else key = this.getDatabaseColumnName(modelName, key);
775
+ if (m && m[1] === "DE") sort[key] = -1;
776
+ else sort[key] = 1;
777
+ }
778
+ } else if (!disableDefaultSort) sort = { _id: 1 };
779
+ return sort;
780
+ };
781
+ function convertToMeters(distance, unit) {
782
+ switch (unit) {
783
+ case "meters": return distance;
784
+ case "kilometers": return distance * 1e3;
785
+ case "miles": return distance * 1600;
786
+ case "feet": return distance * .3048;
787
+ default:
788
+ console.warn("unsupported unit " + unit + ", fallback to mongodb default unit 'meters'");
789
+ return distance;
790
+ }
791
+ }
792
+ function buildNearFilter(query, params) {
793
+ if (!Array.isArray(params)) params = [params];
794
+ params.forEach(function(near) {
795
+ let coordinates = {};
796
+ if (typeof near.near === "string") {
797
+ const s = near.near.split(",");
798
+ coordinates.lng = parseFloat(s[0]);
799
+ coordinates.lat = parseFloat(s[1]);
800
+ } else if (Array.isArray(near.near)) {
801
+ coordinates.lng = near.near[0];
802
+ coordinates.lat = near.near[1];
803
+ } else coordinates = near.near;
804
+ const props = ["maxDistance", "minDistance"];
805
+ const unit = near.unit || "meters";
806
+ const queryValue = { near: { $geometry: {
807
+ coordinates: [coordinates.lng, coordinates.lat],
808
+ type: "Point"
809
+ } } };
810
+ props.forEach(function(p) {
811
+ if (near[p]) queryValue.near["$" + p] = convertToMeters(near[p], unit);
812
+ });
813
+ let property;
814
+ if (near.mongoKey) if (Array.isArray(near.mongoKey)) {
815
+ property = query.where;
816
+ let i;
817
+ for (i = 0; i < near.mongoKey.length; i++) {
818
+ const subKey = near.mongoKey[i];
819
+ if (near.mongoKey.hasOwnProperty(i + 1)) {
820
+ if (!property.hasOwnProperty(subKey)) property[subKey] = Number.isInteger(near.mongoKey[i + 1]) ? [] : {};
821
+ property = property[subKey];
822
+ }
823
+ }
824
+ property[near.mongoKey[i - 1]] = queryValue;
825
+ } else property = query.where[near.mongoKey] = queryValue;
826
+ });
827
+ }
828
+ function hasNearFilter(where) {
829
+ if (!where) return false;
830
+ let isFound = false;
831
+ searchForNear(where);
832
+ function found(prop) {
833
+ return prop && prop.near;
834
+ }
835
+ function searchForNear(node) {
836
+ if (!node) return;
837
+ if (Array.isArray(node)) node.forEach(function(prop) {
838
+ isFound = found(prop);
839
+ if (!isFound) searchForNear(prop);
840
+ });
841
+ else if (typeof node === "object") Object.keys(node).forEach(function(key) {
842
+ const prop = node[key];
843
+ isFound = found(prop);
844
+ if (!isFound) searchForNear(prop);
845
+ });
846
+ }
847
+ return isFound;
848
+ }
849
+ MongoDB.prototype.getDatabaseColumnName = function(model, propName) {
850
+ if (typeof model === "string") model = this._models[model];
851
+ if (typeof model !== "object") return propName;
852
+ if (typeof model.properties !== "object") return propName;
853
+ const prop = model.properties[propName] || {};
854
+ if (prop.id) {
855
+ const customFieldName = prop.mongodb && (prop.mongodb.fieldName || prop.mongodb.field || prop.mongodb.columnName || prop.mongodb.column) || prop.columnName || prop.column;
856
+ if (customFieldName) throw new Error(`custom id field name '${customFieldName}' is not allowed in model ${model.model.definition.name}`);
857
+ return propName;
858
+ } else if (prop.mongodb) propName = prop.mongodb.fieldName || prop.mongodb.field || prop.mongodb.columnName || prop.mongodb.column || prop.columnName || prop.column || propName;
859
+ else propName = prop.columnName || prop.column || propName;
860
+ return propName;
861
+ };
862
+ MongoDB.prototype.convertColumnNames = function(model, data, direction) {
863
+ if (typeof data !== "object") return data;
864
+ if (typeof model === "string") model = this._models[model];
865
+ if (typeof model !== "object") return data;
866
+ if (typeof model.properties !== "object") return data;
867
+ for (const propName in model.properties) {
868
+ const columnName = this.getDatabaseColumnName(model, propName);
869
+ if (propName === columnName) continue;
870
+ if (direction === "database") {
871
+ data[columnName] = data[propName];
872
+ delete data[propName];
873
+ }
874
+ if (direction === "property") {
875
+ data[propName] = data[columnName];
876
+ delete data[columnName];
877
+ }
878
+ }
879
+ return data;
880
+ };
881
+ MongoDB.prototype.fromPropertyToDatabaseNames = function(model, data) {
882
+ return this.convertColumnNames(model, data, "database");
883
+ };
884
+ MongoDB.prototype.fromDatabaseToPropertyNames = function(model, data) {
885
+ return this.convertColumnNames(model, data, "property");
886
+ };
887
+ /**
888
+ * Find matching model instances by the filter
889
+ *
890
+ * @param {String} modelName The model name
891
+ * @param {Object} filter The filter
892
+ * @param {Function} [callback] The callback function
893
+ */
894
+ MongoDB.prototype.all = function all(modelName, filter, options, callback) {
895
+ const self = this;
896
+ if (!callback && typeof options === "function") {
897
+ callback = options;
898
+ options = void 0;
899
+ }
900
+ if (self.debug) debug("all", modelName, filter);
901
+ filter = filter || {};
902
+ let query = {};
903
+ if (filter.where) query = self.buildWhere(modelName, filter.where, options);
904
+ let fields = filter.fields;
905
+ fields = self.fromPropertyToDatabaseNames(modelName, fields);
906
+ if (fields) {
907
+ const findOpts = { projection: fieldsArrayToObj(fields) };
908
+ this.execute(modelName, "find", query, findOpts, processResponse);
909
+ } else this.execute(modelName, "find", query, processResponse);
910
+ function processResponse(err, cursor) {
911
+ if (err) return callback(err);
912
+ const collation = options && options.collation;
913
+ if (collation) cursor.collation(collation);
914
+ if (!hasNearFilter(filter.where)) {
915
+ const order = self.buildSort(modelName, filter.order, options);
916
+ cursor.sort(order);
917
+ }
918
+ if (filter.limit) cursor.limit(filter.limit);
919
+ if (filter.skip) cursor.skip(filter.skip);
920
+ else if (filter.offset) cursor.skip(filter.offset);
921
+ cursor.toArray().then(function(data) {
922
+ if (self.debug) debug("all", modelName, filter, data);
923
+ const objs = self.toModelEntity(modelName, data, fields);
924
+ if (filter && filter.include) self._models[modelName].model.include(objs, filter.include, options, callback);
925
+ else callback(null, objs);
926
+ }).catch(function(err) {
927
+ if (self.debug) debug("all Error", modelName, filter, err);
928
+ callback(err);
929
+ });
930
+ }
931
+ };
932
+ /**
933
+ * Transform db data to model entity
934
+ *
935
+ * @param {String} modelName The model name
936
+ * @param {[Object]} data The data
937
+ * @param {Object} fields The fields
938
+ */
939
+ MongoDB.prototype.toModelEntity = function(modelName, data, fields) {
940
+ const self = this;
941
+ const idName = self.idName(modelName);
942
+ const shouldSetIdValue = idIncluded(fields, idName);
943
+ const deleteMongoId = !shouldSetIdValue || idName !== "_id";
944
+ return data.map(function(o) {
945
+ if (shouldSetIdValue) self.setIdValue(modelName, o, o._id);
946
+ if (deleteMongoId) delete o._id;
947
+ o = self.fromDatabase(modelName, o);
948
+ return o;
949
+ });
950
+ };
951
+ /**
952
+ * Delete all instances for the given model
953
+ * @param {String} modelName The model name
954
+ * @param {Object} [where] The filter for where
955
+ * @param {Function} [callback] The callback function
956
+ */
957
+ MongoDB.prototype.destroyAll = function destroyAll(modelName, where, options, callback) {
958
+ const self = this;
959
+ if (self.debug) debug("destroyAll", modelName, where);
960
+ if (!callback && "function" === typeof where) {
961
+ callback = where;
962
+ where = void 0;
963
+ }
964
+ where = self.buildWhere(modelName, where, options);
965
+ if (debug.enabled) debug("destroyAll where %s", util.inspect(where));
966
+ this.execute(modelName, "deleteMany", where || {}, function(err, info) {
967
+ if (err) return callback && callback(err);
968
+ if (self.debug) debug("destroyAll.callback", modelName, where, err, info);
969
+ let affectedCount;
970
+ if (info && typeof info.deletedCount === "number") affectedCount = info.deletedCount;
971
+ else if (info && info.result) affectedCount = info.result.n;
972
+ if (callback) callback(err, { count: affectedCount });
973
+ });
974
+ };
975
+ /**
976
+ * Count the number of instances for the given model
977
+ *
978
+ * @param {String} modelName The model name
979
+ * @param {Function} [callback] The callback function
980
+ * @param {Object} filter The filter for where
981
+ *
982
+ */
983
+ MongoDB.prototype.count = function count(modelName, where, options, callback) {
984
+ const self = this;
985
+ if (self.debug) debug("count", modelName, where);
986
+ where = self.buildWhere(modelName, where, options) || {};
987
+ options = buildOptions({}, options);
988
+ if (Object.keys(where).length === 0 && !options.session) this.execute(modelName, "estimatedDocumentCount", function(err, count) {
989
+ if (self.debug) debug("count.callback", modelName, err, count);
990
+ if (callback) callback(err, count);
991
+ });
992
+ else this.execute(modelName, "countDocuments", where, options, function(err, count) {
993
+ if (self.debug) debug("count.callback", modelName, err, count);
994
+ if (callback) callback(err, count);
995
+ });
996
+ };
997
+ /**
998
+ * Replace properties for the model instance data
999
+ * @param {String} modelName The model name
1000
+ * @param {*} id The instance id
1001
+ * @param {Object} data The model data
1002
+ * @param {Object} options The options object
1003
+ * @param {Function} [cb] The callback function
1004
+ */
1005
+ MongoDB.prototype.replaceById = function replace(modelName, id, data, options, cb) {
1006
+ if (!cb && typeof options === "function") {
1007
+ cb = options;
1008
+ options = void 0;
1009
+ }
1010
+ if (this.debug) debug("replace", modelName, id, data);
1011
+ const oid = this.coerceId(modelName, id, options);
1012
+ this.replaceWithOptions(modelName, oid, data, buildOptions({ upsert: false }, options), function(err, data) {
1013
+ cb(err, data);
1014
+ });
1015
+ };
1016
+ function errorIdNotFoundForReplace(idValue) {
1017
+ const msg = "Could not replace. Object with id " + idValue + " does not exist!";
1018
+ const error = new Error(msg);
1019
+ error.statusCode = error.status = 404;
1020
+ return error;
1021
+ }
1022
+ /**
1023
+ * Update a model instance with id
1024
+ * @param {String} modelName The model name
1025
+ * @param {Object} id The id of the model instance
1026
+ * @param {Object} data The property/value pairs to be updated or inserted if {upsert: true} is passed as options
1027
+ * @param {Object} options The options you want to pass for update, e.g, {upsert: true}
1028
+ * @callback {Function} [cb] Callback function
1029
+ */
1030
+ MongoDB.prototype.replaceWithOptions = function(modelName, id, data, options, cb) {
1031
+ const self = this;
1032
+ if (!cb && typeof options === "function") {
1033
+ cb = options;
1034
+ options = void 0;
1035
+ }
1036
+ const idName = self.idName(modelName);
1037
+ delete data[idName];
1038
+ data = self.toDatabase(modelName, data);
1039
+ this.execute(modelName, "replaceOne", { _id: id }, data, options, function(err, info) {
1040
+ debug("updateWithOptions.callback", modelName, { _id: id }, data, err, info);
1041
+ if (err) return cb && cb(err);
1042
+ let result;
1043
+ const cbInfo = {};
1044
+ if (info && (info.matchedCount === 1 || info.upsertedCount === 1)) {
1045
+ result = self.fromDatabase(modelName, data);
1046
+ delete result._id;
1047
+ result[idName] = id;
1048
+ cbInfo.isNewInstance = info.upsertedCount === 1;
1049
+ } else if (info && info.result && info.result.n == 1) {
1050
+ result = self.fromDatabase(modelName, data);
1051
+ delete result._id;
1052
+ result[idName] = id;
1053
+ if (info.result.nModified !== void 0) cbInfo.isNewInstance = info.result.nModified === 0;
1054
+ } else {
1055
+ result = void 0;
1056
+ err = errorIdNotFoundForReplace(id);
1057
+ }
1058
+ if (cb) cb(err, result, cbInfo);
1059
+ });
1060
+ };
1061
+ /**
1062
+ * Update properties for the model instance data
1063
+ * @param {String} modelName The model name
1064
+ * @param {Object} data The model data
1065
+ * @param {Function} [callback] The callback function
1066
+ */
1067
+ MongoDB.prototype.updateAttributes = function updateAttrs(modelName, id, data, options, cb) {
1068
+ const self = this;
1069
+ data = self.toDatabase(modelName, data || {});
1070
+ data = self.parseUpdateData(modelName, data, options);
1071
+ if (self.debug) debug("updateAttributes", modelName, id, data);
1072
+ if (Object.keys(data).length === 0) {
1073
+ if (cb) process.nextTick(function() {
1074
+ cb(null, {});
1075
+ });
1076
+ return;
1077
+ }
1078
+ const oid = self.coerceId(modelName, id, options);
1079
+ const idName = this.idName(modelName);
1080
+ this.execute(modelName, "findOneAndUpdate", { _id: oid }, data, buildOptions({ sort: [["_id", "asc"]] }, options), function(err, result) {
1081
+ if (self.debug) debug("updateAttributes.callback", modelName, id, err, result);
1082
+ const object = result && result.value;
1083
+ if (!err && !object) err = errorIdNotFoundForUpdate(modelName, id);
1084
+ self.setIdValue(modelName, object, id);
1085
+ if (object && idName !== "_id") delete object._id;
1086
+ if (cb) cb(err, object);
1087
+ });
1088
+ };
1089
+ function errorIdNotFoundForUpdate(modelvalue, idValue) {
1090
+ const msg = "No " + modelvalue + " found for id " + idValue;
1091
+ const error = new Error(msg);
1092
+ error.statusCode = error.status = 404;
1093
+ return error;
1094
+ }
1095
+ /**
1096
+ * Update all matching instances
1097
+ * @param {String} modelName The model name
1098
+ * @param {Object} where The search criteria
1099
+ * @param {Object} data The property/value pairs to be updated
1100
+ * @callback {Function} cb Callback function
1101
+ */
1102
+ MongoDB.prototype.update = MongoDB.prototype.updateAll = function updateAll(modelName, where, data, options, cb) {
1103
+ const self = this;
1104
+ if (self.debug) debug("updateAll", modelName, where, data);
1105
+ const idName = this.idName(modelName);
1106
+ where = self.buildWhere(modelName, where, options);
1107
+ let updateData = Object.assign({}, data);
1108
+ delete updateData[idName];
1109
+ updateData = self.toDatabase(modelName, updateData);
1110
+ updateData = self.parseUpdateData(modelName, updateData, options);
1111
+ this.execute(modelName, "updateMany", where, updateData, buildOptions({ upsert: false }, options), function(err, info) {
1112
+ if (err) return cb && cb(err);
1113
+ if (self.debug) debug("updateAll.callback", modelName, where, updateData, err, info);
1114
+ const affectedCount = info ? info.matchedCount : void 0;
1115
+ if (cb) cb(err, { count: affectedCount });
1116
+ });
1117
+ };
1118
+ MongoDB.prototype.upsertWithWhere = function upsertWithWhere(modelName, where, data, options, cb) {
1119
+ const self = this;
1120
+ if (self.debug) debug("upsertWithWhere", modelName, where, data);
1121
+ let updateData = Object.assign({}, data);
1122
+ const idValue = self.getIdValue(modelName, updateData);
1123
+ const idName = self.idName(modelName);
1124
+ where = self.buildWhere(modelName, where, options);
1125
+ if (idValue === null || idValue === void 0) delete updateData[idName];
1126
+ else {
1127
+ const oid = self.coerceId(modelName, idValue, options);
1128
+ updateData._id = oid;
1129
+ if (idName !== "_id") delete updateData[idName];
1130
+ }
1131
+ updateData = self.toDatabase(modelName, updateData);
1132
+ updateData = self.parseUpdateData(modelName, updateData, options);
1133
+ this.execute(modelName, "findOneAndUpdate", where, updateData, buildOptions({
1134
+ upsert: true,
1135
+ returnNewDocument: true,
1136
+ returnDocument: "after",
1137
+ sort: [["_id", "asc"]]
1138
+ }, options), function(err, result) {
1139
+ if (err) return cb && cb(err);
1140
+ if (self.debug) debug("upsertWithWhere.callback", modelName, where, updateData, err, result);
1141
+ const object = result && result.value;
1142
+ self.setIdValue(modelName, object, object._id);
1143
+ if (object && idName !== "_id") delete object._id;
1144
+ let info;
1145
+ if (result && result.lastErrorObject) info = { isNewInstance: !result.lastErrorObject.updatedExisting };
1146
+ else debug("upsertWithWhere result format not recognized: %j", result);
1147
+ if (cb) cb(err, self.fromDatabase(modelName, object), info);
1148
+ });
1149
+ };
1150
+ /**
1151
+ * Disconnect from MongoDB
1152
+ */
1153
+ MongoDB.prototype.disconnect = function(cb) {
1154
+ if (this.debug) debug("disconnect");
1155
+ if (this.client) this.client.close();
1156
+ this.db = null;
1157
+ if (cb) process.nextTick(cb);
1158
+ };
1159
+ /**
1160
+ * Perform autoupdate for the given models. It basically calls createIndex
1161
+ * @param {String[]} [models] A model name or an array of model names. If not
1162
+ * present, apply to all models
1163
+ * @param {Function} [cb] The callback function
1164
+ */
1165
+ MongoDB.prototype.autoupdate = function(models, cb) {
1166
+ const self = this;
1167
+ if (self.db) {
1168
+ if (self.debug) debug("autoupdate");
1169
+ if (!cb && "function" === typeof models) {
1170
+ cb = models;
1171
+ models = void 0;
1172
+ }
1173
+ if ("string" === typeof models) models = [models];
1174
+ models = models || Object.keys(self._models);
1175
+ const enableGeoIndexing = this.settings.enableGeoIndexing === true;
1176
+ (async function() {
1177
+ for (const modelName of models) {
1178
+ const indexes = self._models[modelName].settings.indexes || [];
1179
+ let indexList = [];
1180
+ let index = {};
1181
+ let options = {};
1182
+ if (typeof indexes === "object") for (const indexName in indexes) {
1183
+ index = indexes[indexName];
1184
+ if (index.keys) {
1185
+ options = index.options || {};
1186
+ options.name = options.name || indexName;
1187
+ index.options = options;
1188
+ } else {
1189
+ options = { name: indexName };
1190
+ index = {
1191
+ keys: index,
1192
+ options
1193
+ };
1194
+ }
1195
+ indexList.push(index);
1196
+ }
1197
+ else if (Array.isArray(indexes)) indexList = indexList.concat(indexes);
1198
+ const properties = self._models[modelName].properties;
1199
+ for (const p in properties) {
1200
+ if (!properties[p].index) continue;
1201
+ index = {};
1202
+ index[p] = 1;
1203
+ if (typeof properties[p].index === "object") if (typeof properties[p].index.mongodb === "object") {
1204
+ options = properties[p].index.mongodb;
1205
+ index[p] = options.kind || 1;
1206
+ if (options.kind) delete options.kind;
1207
+ if (properties[p].index.unique === true) options.unique = true;
1208
+ } else options = properties[p].index;
1209
+ else if (enableGeoIndexing && properties[p].type && properties[p].type.name === "GeoPoint") {
1210
+ const indexType = typeof properties[p].index === "string" ? properties[p].index : "2dsphere";
1211
+ options = { name: "index" + indexType + p };
1212
+ index[p] = indexType;
1213
+ } else {
1214
+ options = {};
1215
+ if (properties[p].unique) options.unique = true;
1216
+ }
1217
+ indexList.push({
1218
+ keys: index,
1219
+ options
1220
+ });
1221
+ }
1222
+ if (self.debug) debug("create indexes: ", indexList);
1223
+ for (const currentIndex of indexList) {
1224
+ if (self.debug) debug("createIndex: ", currentIndex);
1225
+ await self.collection(modelName).createIndex(currentIndex.fields || currentIndex.keys, currentIndex.options);
1226
+ }
1227
+ }
1228
+ })().then(function() {
1229
+ if (cb) cb();
1230
+ }).catch(function(err) {
1231
+ if (cb) cb(err);
1232
+ });
1233
+ } else self.dataSource.once("connected", function() {
1234
+ self.autoupdate(models, cb);
1235
+ });
1236
+ };
1237
+ /**
1238
+ * Perform automigrate for the given models. It drops the corresponding collections
1239
+ * and calls createIndex
1240
+ * @param {String[]} [models] A model name or an array of model names. If not present, apply to all models
1241
+ * @param {Function} [cb] The callback function
1242
+ */
1243
+ MongoDB.prototype.automigrate = function(models, cb) {
1244
+ const self = this;
1245
+ if (self.db) {
1246
+ if (self.debug) debug("automigrate");
1247
+ if (!cb && "function" === typeof models) {
1248
+ cb = models;
1249
+ models = void 0;
1250
+ }
1251
+ if ("string" === typeof models) models = [models];
1252
+ models = models || Object.keys(self._models);
1253
+ (async function() {
1254
+ for (const modelName of models) {
1255
+ const collectionName = self.collectionName(modelName);
1256
+ if (self.debug) debug("drop collection %s for model %s", collectionName, modelName);
1257
+ try {
1258
+ await self.db.dropCollection(collectionName);
1259
+ } catch (err) {
1260
+ debug("Error dropping collection %s for model %s: ", collectionName, modelName, err);
1261
+ if (!isNamespaceMissingError(err)) throw err;
1262
+ }
1263
+ if (self.debug) debug("create collection %s for model %s", collectionName, modelName);
1264
+ await self.db.createCollection(collectionName);
1265
+ }
1266
+ await new Promise(function(resolve, reject) {
1267
+ self.autoupdate(models, function(err) {
1268
+ if (err) reject(err);
1269
+ else resolve();
1270
+ });
1271
+ });
1272
+ })().then(function() {
1273
+ if (cb) cb();
1274
+ }).catch(function(err) {
1275
+ if (cb) cb(err);
1276
+ });
1277
+ } else self.dataSource.once("connected", function() {
1278
+ self.automigrate(models, cb);
1279
+ });
1280
+ };
1281
+ MongoDB.prototype.ping = function(cb) {
1282
+ const self = this;
1283
+ if (self.db) this.db.collection("dummy").findOne({ _id: 1 }).then(function(result) {
1284
+ cb(null, result);
1285
+ }).catch(cb);
1286
+ else {
1287
+ self.dataSource.once("connected", function() {
1288
+ self.ping(cb);
1289
+ });
1290
+ self.dataSource.once("error", function(err) {
1291
+ cb(err);
1292
+ });
1293
+ self.connect(function() {});
1294
+ }
1295
+ };
1296
+ MongoDB.prototype.commit = function(tx, cb) {
1297
+ tx.commitTransaction().then(function() {
1298
+ return tx.endSession();
1299
+ }).then(function() {
1300
+ cb();
1301
+ }).catch(cb);
1302
+ };
1303
+ MongoDB.prototype.rollback = function(tx, cb) {
1304
+ tx.abortTransaction().then(function() {
1305
+ return tx.endSession();
1306
+ }).then(function() {
1307
+ cb();
1308
+ }).catch(cb);
1309
+ };
1310
+ MongoDB.prototype.beginTransaction = function(isolationLevel, cb) {
1311
+ const transactionOptions = {
1312
+ readPreference: "primary",
1313
+ readConcern: { level: "local" },
1314
+ writeConcern: { w: "majority" }
1315
+ };
1316
+ if (isolationLevel instanceof Object) Object.assign(transactionOptions, isolationLevel || {});
1317
+ const session = this.client.startSession();
1318
+ session.startTransaction(transactionOptions);
1319
+ cb(null, session);
1320
+ };
1321
+ function typeIsObjectId(input) {
1322
+ if (!input) return false;
1323
+ return typeof input === "string" && input.match(ObjectIdTypeRegex);
1324
+ }
1325
+ function isStoredAsObjectID(propDef) {
1326
+ if (!propDef) return false;
1327
+ if (propDef.mongodb) {
1328
+ if (ObjectIdTypeRegex.test(propDef.mongodb.dataType)) return true;
1329
+ } else if (propDef.type) {
1330
+ if (typeof propDef.type === "string" && typeIsObjectId(propDef.type)) return true;
1331
+ else if (Array.isArray(propDef.type)) {
1332
+ if (propDef.type[0] === ObjectID || typeIsObjectId(propDef.type[0])) return true;
1333
+ }
1334
+ }
1335
+ return false;
1336
+ }
1337
+ function isStrictObjectIDCoercionEnabled(modelCtor, options) {
1338
+ const settings = modelCtor.settings;
1339
+ return settings && settings.strictObjectIDCoercion || modelCtor.model && modelCtor.model.getConnector().settings.strictObjectIDCoercion || options && options.strictObjectIDCoercion;
1340
+ }
1341
+ function coerceToObjectId(modelCtor, propDef, propValue) {
1342
+ if (isStoredAsObjectID(propDef)) if (isObjectIDProperty(modelCtor, propDef, propValue)) return ObjectID(propValue);
1343
+ else throw new Error(`${propValue} is not an ObjectID string`);
1344
+ else if (isStrictObjectIDCoercionEnabled(modelCtor)) {
1345
+ if (isObjectIDProperty(modelCtor, propDef, propValue)) return ObjectID(propValue);
1346
+ } else if (ObjectIdValueRegex.test(propValue)) return ObjectID(propValue);
1347
+ return propValue;
1348
+ }
1349
+ /**
1350
+ * Check whether the property is an ObjectID (or Array thereof)
1351
+ *
1352
+ */
1353
+ function isObjectIDProperty(modelCtor, propDef, value, options) {
1354
+ if (!propDef) return false;
1355
+ if (typeof value === "string" && value.match(ObjectIdValueRegex)) if (isStoredAsObjectID(propDef)) return true;
1356
+ else return !isStrictObjectIDCoercionEnabled(modelCtor, options);
1357
+ else if (value instanceof mongoObjectID) return true;
1358
+ else return false;
1359
+ }
1360
+ /**
1361
+ * Removes extra dollar sign '$' for operators
1362
+ *
1363
+ * @param {*} spec the operator for Where filter
1364
+ */
1365
+ function trimLeadingDollarSigns(spec) {
1366
+ return spec.replace(/^(\$)+/, "");
1367
+ }
1368
+ exports.trimLeadingDollarSigns = trimLeadingDollarSigns;
1369
+ function sanitizeFilter(filter, options) {
1370
+ options = Object.assign({}, options);
1371
+ if (options && options.disableSanitization) return filter;
1372
+ if (!filter || typeof filter !== "object") return filter;
1373
+ for (const key in filter) if (key === "$where" || key === "mapReduce") {
1374
+ debug(`sanitizeFilter: deleting ${key}`);
1375
+ delete filter[key];
1376
+ }
1377
+ return filter;
1378
+ }
1379
+ exports.sanitizeFilter = sanitizeFilter;
1380
+ /**
1381
+ * Find a matching model instances by the filter or create a new instance
1382
+ *
1383
+ * Only supported on mongodb 2.6+
1384
+ *
1385
+ * @param {String} modelName The model name
1386
+ * @param {Object} data The model instance data
1387
+ * @param {Object} filter The filter
1388
+ * @param {Function} [callback] The callback function
1389
+ */
1390
+ function optimizedFindOrCreate(modelName, filter, data, options, callback) {
1391
+ const self = this;
1392
+ if (self.debug) debug("findOrCreate", modelName, filter, data);
1393
+ if (!callback) callback = options;
1394
+ const idValue = self.getIdValue(modelName, data);
1395
+ const idName = self.idName(modelName);
1396
+ if (idValue == null) delete data[idName];
1397
+ else {
1398
+ data._id = self.coerceId(modelName, idValue, options);
1399
+ if (idName !== "_id") delete data[idName];
1400
+ }
1401
+ filter = filter || {};
1402
+ let query = {};
1403
+ if (filter.where) {
1404
+ if (filter.where[idName]) {
1405
+ let id = filter.where[idName];
1406
+ delete filter.where[idName];
1407
+ id = self.coerceId(modelName, id, options);
1408
+ filter.where._id = id;
1409
+ }
1410
+ query = self.buildWhere(modelName, filter.where, options);
1411
+ }
1412
+ const sort = self.buildSort(modelName, filter.order, options);
1413
+ const projection = fieldsArrayToObj(filter.fields);
1414
+ this.collection(modelName).findOneAndUpdate(query, { $setOnInsert: data }, {
1415
+ projection,
1416
+ sort,
1417
+ upsert: true
1418
+ }).then((result) => {
1419
+ if (self.debug) debug("findOrCreate.callback", modelName, filter, null, result);
1420
+ let value = result.value;
1421
+ const created = !!result.lastErrorObject.upserted;
1422
+ if (created && (value == null || Object.keys(value).length == 0)) value = Object.assign({}, data);
1423
+ if (created && value && value._id === void 0) value._id = result.lastErrorObject.upserted;
1424
+ if (value) {
1425
+ value = applyProjectionToDocument(value, projection);
1426
+ value = self.fromDatabase(modelName, value);
1427
+ self.setIdValue(modelName, value, value._id ?? result.lastErrorObject.upserted);
1428
+ }
1429
+ if (value && idName !== "_id") delete value._id;
1430
+ if (filter && filter.include) self._models[modelName].model.include([value], filter.include, function(err, data) {
1431
+ callback(err, data[0], created);
1432
+ });
1433
+ else callback(null, value, created);
1434
+ }).catch((err) => {
1435
+ callback(err);
1436
+ });
1437
+ }
1438
+ /**
1439
+ * @param {*} data Plain Data Object for the matching property definition(s)
1440
+ * @param {*} modelCtor Model constructor
1441
+ * @param {*} visitor A callback function which takes a property value and
1442
+ * definition to apply custom property coercion
1443
+ */
1444
+ function visitAllProperties(data, modelCtor, visitor) {
1445
+ if (data === null || data === void 0) return;
1446
+ const modelProps = modelCtor.properties ? modelCtor.properties : modelCtor.definition.properties;
1447
+ const allProps = new Set(Object.keys(data).concat(Object.keys(modelProps)));
1448
+ for (const p of allProps) {
1449
+ const value = data[p];
1450
+ const def = modelProps[p];
1451
+ if (!value) continue;
1452
+ if (def && def.type && isNestedModel(def.type)) if (Array.isArray(def.type) && Array.isArray(value)) for (const it of value) visitAllProperties(it, def.type[0].definition, visitor);
1453
+ else visitAllProperties(value, def.type.definition, visitor);
1454
+ else visitor(modelCtor, value, def, (newValue) => {
1455
+ data[p] = newValue;
1456
+ });
1457
+ }
1458
+ }
1459
+ /**
1460
+ * @param {*} modelCtor Model constructor
1461
+ * @param {*} propValue Property value to coerce into special types supported by the connector
1462
+ * @param {*} propDef Property definition to check if property is for MongoDB
1463
+ */
1464
+ function coercePropertyValue(modelCtor, propValue, propDef, setValue) {
1465
+ let coercedValue;
1466
+ if (propDef && propDef.mongodb && propDef.mongodb.dataType) {
1467
+ const dataType = propDef.mongodb.dataType;
1468
+ if (typeof dataType === "string") {
1469
+ if (hasDataType("decimal128", propDef)) if (Array.isArray(propValue)) {
1470
+ coercedValue = propValue.map((val) => Decimal128.fromString(val));
1471
+ return setValue(coercedValue);
1472
+ } else {
1473
+ coercedValue = Decimal128.fromString(propValue);
1474
+ return setValue(coercedValue);
1475
+ }
1476
+ else if (typeIsObjectId(dataType)) if (Array.isArray(propValue)) {
1477
+ propValue = propValue.map((val) => {
1478
+ if (isObjectIDProperty(modelCtor, propDef, val)) return coercedValue = ObjectID(val);
1479
+ else throw new Error(`${val} is not an ObjectID string`);
1480
+ });
1481
+ return setValue(propValue);
1482
+ } else if (isObjectIDProperty(modelCtor, propDef, propValue)) {
1483
+ coercedValue = ObjectID(propValue);
1484
+ return setValue(coercedValue);
1485
+ } else throw new Error(`${propValue} is not an ObjectID string`);
1486
+ }
1487
+ } else {
1488
+ if (Array.isArray(propValue)) propValue = propValue.map((val) => coerceToObjectId(modelCtor, propDef, val));
1489
+ else propValue = coerceToObjectId(modelCtor, propDef, propValue);
1490
+ setValue(propValue);
1491
+ }
1492
+ }
1493
+ /**
1494
+ * A utility function which checks for nested property definitions
1495
+ *
1496
+ * @param {*} propType Property type metadata
1497
+ *
1498
+ */
1499
+ function isNestedModel(propType) {
1500
+ if (!propType) return false;
1501
+ if (Array.isArray(propType)) return isNestedModel(propType[0]);
1502
+ return propType.definition && propType.definition.properties;
1503
+ }
1504
+ /**
1505
+ * A utility function which checks if a certain property definition matches
1506
+ * the given data type
1507
+ * @param {*} dataType The data type to check the property definition against
1508
+ * @param {*} propertyDef A property definition containing metadata about property type
1509
+ */
1510
+ function hasDataType(dataType, propertyDef) {
1511
+ return propertyDef && propertyDef.mongodb && propertyDef.mongodb.dataType && propertyDef.mongodb.dataType.toLowerCase() === dataType.toLowerCase();
1512
+ }
1513
+ /**
1514
+ * A utility function which adds the connector options to mongodb options
1515
+ * the given data type
1516
+ * @param {*} requiredOptions Required options by the connector
1517
+ * @param {*} connectorOptions User specified Options
1518
+ */
1519
+ function buildOptions(requiredOptions, connectorOptions) {
1520
+ if (connectorOptions && connectorOptions.transaction && connectorOptions.transaction.isActive()) return Object.assign({ session: connectorOptions.transaction.connection }, connectorOptions, requiredOptions);
1521
+ else return Object.assign({}, connectorOptions, requiredOptions);
1522
+ }
1523
+ //#endregion