@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.
- package/LICENSE +25 -0
- package/README.md +182 -0
- package/dist/mongodb.cjs +1523 -0
- package/dist/mongodb.d.cts +1 -0
- package/dist/test-utils.cjs +15 -0
- package/dist/test-utils.d.cts +11 -0
- package/index.js +3 -0
- package/package.json +63 -0
package/dist/mongodb.cjs
ADDED
|
@@ -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
|