mongodb-ops 0.10.1 → 0.11.1
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/lib/mongodb-ops.js +406 -360
- package/mongodb-ops-0.11.1.tgz +0 -0
- package/package.json +29 -28
- package/readme.md +130 -65
- package/test/mongodb-ops.test.js +160 -0
package/lib/mongodb-ops.js
CHANGED
|
@@ -1,360 +1,406 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const MongoClient = require('mongodb').MongoClient;
|
|
4
|
-
const ObjectID = require('mongodb').ObjectId;
|
|
5
|
-
|
|
6
|
-
class MongoDBOps {
|
|
7
|
-
/**
|
|
8
|
-
* @class
|
|
9
|
-
* @classdesc MongoDB operations - Note: writeConcern is not supported in this class
|
|
10
|
-
*
|
|
11
|
-
* @param {string} connString Database connection string
|
|
12
|
-
*/
|
|
13
|
-
constructor(connString) {
|
|
14
|
-
if (!connString) { throw new Error("missing-connection-string"); }
|
|
15
|
-
this.connString = connString;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Static method - Get ObjectId instance from string
|
|
20
|
-
*
|
|
21
|
-
* @param {string} id Object ID string
|
|
22
|
-
* @returns {object} ObjectId instance
|
|
23
|
-
*/
|
|
24
|
-
static getObjectId(id) { return new ObjectID(id); }
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Instance method - Get ObjectId instance from string
|
|
28
|
-
*
|
|
29
|
-
* @param {string} id Object ID string
|
|
30
|
-
* @returns {object} ObjectId instance
|
|
31
|
-
*/
|
|
32
|
-
getObjectId(id) { return new ObjectID(id); }
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Static method - Get DB client - It supports multiple db client with different connection string. Active db client will be reused for better performance
|
|
36
|
-
*
|
|
37
|
-
* @param {string} connString Database connection string
|
|
38
|
-
* @returns {promise} Promise with db client
|
|
39
|
-
*/
|
|
40
|
-
static async getDbClient(connString) {
|
|
41
|
-
if (!connString) { throw new Error("missing-connection-string"); }
|
|
42
|
-
|
|
43
|
-
if (!
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*
|
|
130
|
-
* @
|
|
131
|
-
* @
|
|
132
|
-
*
|
|
133
|
-
* @param {
|
|
134
|
-
* @
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*
|
|
142
|
-
* {@link https://www.mongodb.com/docs/
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* @param {string} collectionName Collection name
|
|
192
|
-
* @param {object} search $search object - {@link https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/}
|
|
193
|
-
* @param {object} [obj]
|
|
194
|
-
* @param {object} [obj.projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
|
|
195
|
-
* @param {object} [obj.sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
|
|
196
|
-
* @param {object} [obj.pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
|
|
197
|
-
* @param {boolean} [isGetCount=true] Set true to get the number of total matching docs and current page number
|
|
198
|
-
* @returns {promise} Promise with object or object array
|
|
199
|
-
*/
|
|
200
|
-
async search(collectionName, search, { projection, sort, pagination }={}, isGetCount = true) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* @
|
|
255
|
-
*
|
|
256
|
-
* @
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MongoClient = require('mongodb').MongoClient;
|
|
4
|
+
const ObjectID = require('mongodb').ObjectId;
|
|
5
|
+
|
|
6
|
+
class MongoDBOps {
|
|
7
|
+
/**
|
|
8
|
+
* @class
|
|
9
|
+
* @classdesc MongoDB operations - Note: writeConcern is not supported in this class
|
|
10
|
+
*
|
|
11
|
+
* @param {string} connString Database connection string
|
|
12
|
+
*/
|
|
13
|
+
constructor(connString) {
|
|
14
|
+
if (!connString) { throw new Error("missing-connection-string"); }
|
|
15
|
+
this.connString = connString;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Static method - Get ObjectId instance from string
|
|
20
|
+
*
|
|
21
|
+
* @param {string} id Object ID string
|
|
22
|
+
* @returns {object} ObjectId instance
|
|
23
|
+
*/
|
|
24
|
+
static getObjectId(id) { return new ObjectID(id); }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Instance method - Get ObjectId instance from string
|
|
28
|
+
*
|
|
29
|
+
* @param {string} id Object ID string
|
|
30
|
+
* @returns {object} ObjectId instance
|
|
31
|
+
*/
|
|
32
|
+
getObjectId(id) { return new ObjectID(id); }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Static method - Get DB client - It supports multiple db client with different connection string. Active db client will be reused for better performance
|
|
36
|
+
*
|
|
37
|
+
* @param {string} connString Database connection string
|
|
38
|
+
* @returns {promise} Promise with db client
|
|
39
|
+
*/
|
|
40
|
+
static async getDbClient(connString) {
|
|
41
|
+
if (!connString) { throw new Error("missing-connection-string"); }
|
|
42
|
+
|
|
43
|
+
if (!(MongoDBOps.dbClients instanceof Map)) { MongoDBOps.dbClients = new Map(); }
|
|
44
|
+
|
|
45
|
+
let clientPromise = MongoDBOps.dbClients.get(connString);
|
|
46
|
+
if (!clientPromise) {
|
|
47
|
+
const options = Object.assign(
|
|
48
|
+
{ serverSelectionTimeoutMS: 8000, retryWrites: true, retryReads: true },
|
|
49
|
+
MongoDBOps.clientOptions || {}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
clientPromise = MongoClient.connect(connString, options).catch((err) => {
|
|
53
|
+
if (MongoDBOps.dbClients.get(connString) === clientPromise) {
|
|
54
|
+
MongoDBOps.dbClients.delete(connString);
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
});
|
|
58
|
+
MongoDBOps.dbClients.set(connString, clientPromise);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return clientPromise;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Static method - Close database connection
|
|
66
|
+
* @returns {promise}
|
|
67
|
+
*/
|
|
68
|
+
static async closeDBConn() {
|
|
69
|
+
if (MongoDBOps.dbClients instanceof Map) {
|
|
70
|
+
const clientPromises = Array.from(MongoDBOps.dbClients.values());
|
|
71
|
+
MongoDBOps.dbClients.clear();
|
|
72
|
+
|
|
73
|
+
for (const clientPromise of clientPromises) {
|
|
74
|
+
try {
|
|
75
|
+
const client = await clientPromise;
|
|
76
|
+
await client.close();
|
|
77
|
+
} catch {
|
|
78
|
+
// Failed or already-evicted connection attempts do not need separate cleanup.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(MongoDBOps.dbClientList)) {
|
|
84
|
+
const legacyClients = MongoDBOps.dbClientList;
|
|
85
|
+
MongoDBOps.dbClientList = [];
|
|
86
|
+
|
|
87
|
+
for (const item of legacyClients) {
|
|
88
|
+
try {
|
|
89
|
+
await item.close();
|
|
90
|
+
} catch {
|
|
91
|
+
// Ignore stale legacy clients from older mixed-version processes.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Instance method - Close database connection
|
|
101
|
+
* @returns {promise}
|
|
102
|
+
*/
|
|
103
|
+
async closeDBConn() {
|
|
104
|
+
return Promise.resolve(await MongoDBOps.closeDBConn());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Static method - Get estimated document count of a collection
|
|
109
|
+
*
|
|
110
|
+
* @param {string} collectionName Collection Name
|
|
111
|
+
* @param {string} connString Database connection string
|
|
112
|
+
*/
|
|
113
|
+
static async getCollectionCount(collectionName, connString) {
|
|
114
|
+
const db = (await MongoDBOps.getDbClient(connString)).db();
|
|
115
|
+
return Promise.resolve(await db.collection(collectionName).estimatedDocumentCount());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Instance method - Get estimated document count of a collection
|
|
120
|
+
*
|
|
121
|
+
* @param {string} collectionName Collection Name
|
|
122
|
+
* @param {string} connString Database connection string
|
|
123
|
+
*/
|
|
124
|
+
async getCollectionCount(collectionName) {
|
|
125
|
+
return Promise.resolve(await MongoDBOps.getData(collectionName, this.connString));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Static method - Get documents from MongoDB
|
|
130
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/}
|
|
131
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/}
|
|
132
|
+
*
|
|
133
|
+
* @param {string} collectionName Collection name
|
|
134
|
+
* @param {object} [queryExp] Query Specifies selection filter using query operators - {@link https://docs.mongodb.com/manual/reference/operator/}
|
|
135
|
+
* @param {boolean} [isAggregate=false] Set true to use the aggregation
|
|
136
|
+
* @param {object} [projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
|
|
137
|
+
* @param {object} [sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
|
|
138
|
+
* @param {object} [pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
|
|
139
|
+
* @param {boolean} [isGetCount=false] Set true to get the number of doc count based on the queryExp
|
|
140
|
+
* @param {string} connString Database connection string
|
|
141
|
+
* @param {object} [collation] Collation {@link https://www.mongodb.com/docs/manual/reference/collation/#std-label-collation-document-fields}
|
|
142
|
+
* @param {object} [options] Aggregate options {@link https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/}
|
|
143
|
+
* @returns {promise} Promise with object array
|
|
144
|
+
*/
|
|
145
|
+
static async getData(collectionName, queryExp, isAggregate = false, projection, sort, pagination, isGetCount = false, connString, collation, options) {
|
|
146
|
+
const db = (await MongoDBOps.getDbClient(connString)).db();
|
|
147
|
+
|
|
148
|
+
if (isAggregate) { return Promise.resolve(await db.collection(collectionName).aggregate(queryExp, options).toArray()); }
|
|
149
|
+
|
|
150
|
+
queryExp = queryExp || {};
|
|
151
|
+
if (isGetCount) { return Promise.resolve(await db.collection(collectionName).countDocuments(queryExp)); }
|
|
152
|
+
|
|
153
|
+
const { skip, limit } = parsePagination(pagination);
|
|
154
|
+
if (limit < 1) { return Promise.resolve([]); }
|
|
155
|
+
|
|
156
|
+
projection = { projection: projection || {} };
|
|
157
|
+
|
|
158
|
+
let data = db.collection(collectionName).find(queryExp, projection);
|
|
159
|
+
if (sort) { data = data.sort(sort); }
|
|
160
|
+
if (pagination) { data = data.skip(skip).limit(limit); }
|
|
161
|
+
if (collation) { data = data.collation(collation); }
|
|
162
|
+
|
|
163
|
+
return Promise.resolve(await data.toArray());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Instance method - Get documents from MongoDB
|
|
168
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/}
|
|
169
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/}
|
|
170
|
+
*
|
|
171
|
+
* @param {string} collectionName Collection name
|
|
172
|
+
* @param {object} [queryExp] Query Specifies selection filter using query operators - {@link https://docs.mongodb.com/manual/reference/operator/}
|
|
173
|
+
* @param {boolean} [isAggregate=false] Set true to use the aggregation
|
|
174
|
+
* @param {object} [projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
|
|
175
|
+
* @param {object} [sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
|
|
176
|
+
* @param {object} [pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
|
|
177
|
+
* @param {boolean} [isGetCount=false] Set true to get the number of doc count based on the queryExp
|
|
178
|
+
* @param {object} [collation] Collation {@link https://www.mongodb.com/docs/manual/reference/collation/#std-label-collation-document-fields}
|
|
179
|
+
* @param {object} [options] Aggregate options {@link https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/}
|
|
180
|
+
* @returns {promise} Promise with object array
|
|
181
|
+
*/
|
|
182
|
+
async getData(collectionName, queryExp, isAggregate, projection, sort, pagination, isGetCount, collation, options) {
|
|
183
|
+
return Promise.resolve(await MongoDBOps.getData(collectionName, queryExp, isAggregate, projection, sort, pagination, isGetCount, this.connString, collation, options));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Static method - Use altas search at MongoDB
|
|
188
|
+
* {@link https://www.mongodb.com/docs/atlas/atlas-search/}
|
|
189
|
+
*
|
|
190
|
+
* @param {string} connString Database connection string
|
|
191
|
+
* @param {string} collectionName Collection name
|
|
192
|
+
* @param {object} search $search object - {@link https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/}
|
|
193
|
+
* @param {object} [obj]
|
|
194
|
+
* @param {object} [obj.projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
|
|
195
|
+
* @param {object} [obj.sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
|
|
196
|
+
* @param {object} [obj.pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
|
|
197
|
+
* @param {boolean} [isGetCount=true] Set true to get the number of total matching docs and current page number
|
|
198
|
+
* @returns {promise} Promise with object or object array
|
|
199
|
+
*/
|
|
200
|
+
static async search(connString, collectionName, search, { projection, sort, pagination }={}, isGetCount = true) {
|
|
201
|
+
if ([connString, collectionName, search].includes(undefined)) { return Promise.reject("Connection string, collection name and search cannot be undefined"); }
|
|
202
|
+
|
|
203
|
+
const { skip, limit } = parsePagination(pagination);
|
|
204
|
+
if (limit < 1) { return Promise.resolve([]); }
|
|
205
|
+
|
|
206
|
+
const payload = [
|
|
207
|
+
{ $search: search },
|
|
208
|
+
{ $addFields: { score: { $meta: "searchScore" }}},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
if (projection) { payload.push({ $project: projection }); }
|
|
212
|
+
if (sort) { payload.push({ $sort: sort }); }
|
|
213
|
+
|
|
214
|
+
const skipLimit = [];
|
|
215
|
+
if (skip) { skipLimit.push({ $skip: skip }); }
|
|
216
|
+
if (limit) { skipLimit.push({ $limit: limit }); }
|
|
217
|
+
|
|
218
|
+
if (isGetCount) {
|
|
219
|
+
payload.push({
|
|
220
|
+
$facet: {
|
|
221
|
+
metadata: [{ $count: "total" }, { $addFields: { page: Math.ceil((skip + 1) / limit) }}],
|
|
222
|
+
data: skipLimit,
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else { payload.push(...skipLimit); }
|
|
227
|
+
|
|
228
|
+
const result = await MongoDBOps.getData(collectionName, payload, true, undefined, undefined, undefined, undefined, connString);
|
|
229
|
+
|
|
230
|
+
return Promise.resolve(isGetCount ? result[0] : result);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Instance method - Use altas search at MongoDB
|
|
235
|
+
* {@link https://www.mongodb.com/docs/atlas/atlas-search/}
|
|
236
|
+
*
|
|
237
|
+
* @param {string} collectionName Collection name
|
|
238
|
+
* @param {object} search $search object - {@link https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/}
|
|
239
|
+
* @param {object} [obj]
|
|
240
|
+
* @param {object} [obj.projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
|
|
241
|
+
* @param {object} [obj.sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
|
|
242
|
+
* @param {object} [obj.pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
|
|
243
|
+
* @param {boolean} [isGetCount=true] Set true to get the number of total matching docs and current page number
|
|
244
|
+
* @returns {promise} Promise with object or object array
|
|
245
|
+
*/
|
|
246
|
+
async search(collectionName, search, { projection, sort, pagination }={}, isGetCount = true) {
|
|
247
|
+
return Promise.resolve(await MongoDBOps.search(this.connString, collectionName, search, { projection, sort, pagination }, isGetCount));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Static method - Write document to MongoDB
|
|
252
|
+
*
|
|
253
|
+
* References:
|
|
254
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.insertOne/}
|
|
255
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/}
|
|
256
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/}
|
|
257
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/}
|
|
258
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteOne/}
|
|
259
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteMany/}
|
|
260
|
+
*
|
|
261
|
+
* @param {string} type Write type - insertOne, replaceOne, updateOne, updateMany, deleteOne, deleteMany
|
|
262
|
+
* @param {string} collectionName Collection name
|
|
263
|
+
* @param {object} doc Data document
|
|
264
|
+
* @param {object} filter Query filter {@link https://docs.mongodb.com/manual/core/document/#document-query-filter}
|
|
265
|
+
* @param {string} connString Database connection string
|
|
266
|
+
* @returns {promise}
|
|
267
|
+
*/
|
|
268
|
+
static async writeData(type, collectionName, doc, filter, connString) {
|
|
269
|
+
try {
|
|
270
|
+
const db = (await MongoDBOps.getDbClient(connString)).db();
|
|
271
|
+
|
|
272
|
+
let result;
|
|
273
|
+
switch(type) {
|
|
274
|
+
case "insertOne": result = await db.collection(collectionName).insertOne(doc); break;
|
|
275
|
+
case "replaceOne": result = await db.collection(collectionName).replaceOne(filter, doc); break;
|
|
276
|
+
case "updateOne": result = await db.collection(collectionName).updateOne(filter, doc); break;
|
|
277
|
+
case "updateMany": result = await db.collection(collectionName).updateMany(filter, doc); break;
|
|
278
|
+
case "deleteOne": result = await db.collection(collectionName).deleteOne(filter); break;
|
|
279
|
+
case "deleteMany": result = await db.collection(collectionName).deleteMany(filter); break;
|
|
280
|
+
default: throw new Error("invalid-writeData-type");
|
|
281
|
+
}
|
|
282
|
+
return Promise.resolve(result);
|
|
283
|
+
}
|
|
284
|
+
catch (err) { return Promise.reject(err.errmsg || err.message || err); }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Instance method - Write document to MongoDB
|
|
289
|
+
*
|
|
290
|
+
* References:
|
|
291
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.insertOne/}
|
|
292
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/}
|
|
293
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/}
|
|
294
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/}
|
|
295
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteOne/}
|
|
296
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteMany/}
|
|
297
|
+
*
|
|
298
|
+
* @param {string} type Write type - insertOne, replaceOne, updateOne, updateMany, deleteOne, deleteMany
|
|
299
|
+
* @param {string} collectionName Collection name
|
|
300
|
+
* @param {object} doc Data document
|
|
301
|
+
* @param {object} filter Query filter {@link https://docs.mongodb.com/manual/core/document/#document-query-filter}
|
|
302
|
+
* @returns {promise}
|
|
303
|
+
*/
|
|
304
|
+
async writeData(type, collectionName, doc, filter) {
|
|
305
|
+
return Promise.resolve(await MongoDBOps.writeData(type, collectionName, doc, filter, this.connString));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Static method - Write document to MongoDB via BulkOps / BulkWrite
|
|
310
|
+
*
|
|
311
|
+
* References:
|
|
312
|
+
* {@link https://mongodb.github.io/node-mongodb-native/3.6/api/BulkOperationBase.html}
|
|
313
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/}
|
|
314
|
+
*
|
|
315
|
+
* @param {string} type Write type - insertBulk, replaceBulk, updateBulk, allBulk
|
|
316
|
+
* @param {string} collectionName Collection name
|
|
317
|
+
* @param {Array} docs Data documents array
|
|
318
|
+
* @param {boolean} [ordered=false] Set true to use ordered bulkWrite
|
|
319
|
+
* @param {string} connString Database connection string
|
|
320
|
+
* @returns {promise}
|
|
321
|
+
*/
|
|
322
|
+
static async writeBulkData(type, collectionName, docs, ordered = false, connString) {
|
|
323
|
+
try {
|
|
324
|
+
const db = (await MongoDBOps.getDbClient(connString)).db();
|
|
325
|
+
|
|
326
|
+
let result;
|
|
327
|
+
switch(type) {
|
|
328
|
+
case "insertBulk":
|
|
329
|
+
for (let i = 0; i < docs.length; ++i) { docs[i] = { insertOne: { "document": docs[i] }}; }
|
|
330
|
+
break;
|
|
331
|
+
case "replaceBulk":
|
|
332
|
+
// doc = {
|
|
333
|
+
// "filter": <document>,
|
|
334
|
+
// "replacement": <document>,
|
|
335
|
+
// "upsert": <boolean>,
|
|
336
|
+
// "collation": <document>,
|
|
337
|
+
// "hint": <document|string>
|
|
338
|
+
// }
|
|
339
|
+
for (let i = 0; i < docs.length; ++i) { docs[i] = { replaceOne: docs[i] }; }
|
|
340
|
+
break;
|
|
341
|
+
case "updateBulk":
|
|
342
|
+
// doc = {
|
|
343
|
+
// "filter": <document>,
|
|
344
|
+
// "update": <document or pipeline>,
|
|
345
|
+
// "upsert": <boolean>,
|
|
346
|
+
// "collation": <document>,
|
|
347
|
+
// "arrayFilters": [ <filterdocument1>, ... ],
|
|
348
|
+
// "hint": <document|string>
|
|
349
|
+
// }
|
|
350
|
+
for (let i = 0; i < docs.length; ++i) { docs[i] = { updateOne: docs[i] }; }
|
|
351
|
+
break;
|
|
352
|
+
case "deleteBulk":
|
|
353
|
+
// doc = {
|
|
354
|
+
// "filter": <document>,
|
|
355
|
+
// "collation": <document>
|
|
356
|
+
// }
|
|
357
|
+
for (let i = 0; i < docs.length; ++i) { docs[i] = { deleteOne: docs[i] }; }
|
|
358
|
+
break;
|
|
359
|
+
case "allBulk":
|
|
360
|
+
// allowed bulkWrite operations include insertOne, replaceOne, updateOne, updateMany, deleteOne, deleteMany
|
|
361
|
+
break;
|
|
362
|
+
default: throw new Error("invalid-writeBulkData-type");
|
|
363
|
+
}
|
|
364
|
+
result = await db.collection(collectionName).bulkWrite(docs, { ordered: ordered });
|
|
365
|
+
return Promise.resolve(result);
|
|
366
|
+
}
|
|
367
|
+
catch (err) { return Promise.reject(err.result || err.errmsg || err.message); }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Instance method - Write document to MongoDB via BulkOps / BulkWrite
|
|
372
|
+
*
|
|
373
|
+
* References:
|
|
374
|
+
* {@link https://mongodb.github.io/node-mongodb-native/3.6/api/BulkOperationBase.html}
|
|
375
|
+
* {@link https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/}
|
|
376
|
+
*
|
|
377
|
+
* @param {string} type Write type - insertBulk, replaceBulk, updateBulk, allBulk
|
|
378
|
+
* @param {string} collectionName Collection name
|
|
379
|
+
* @param {Array} docs Data documents array
|
|
380
|
+
* @param {boolean} [ordered=false] Set true to use ordered bulkWrite
|
|
381
|
+
* @returns {promise}
|
|
382
|
+
*/
|
|
383
|
+
async writeBulkData(type, collectionName, docs, ordered = false) {
|
|
384
|
+
return Promise.resolve(await MongoDBOps.writeBulkData(type, collectionName, docs, ordered, this.connString));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = MongoDBOps;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Parse the pagaintion and return skip and limit values
|
|
392
|
+
*
|
|
393
|
+
* @param {object} obj
|
|
394
|
+
* @param {number} [obj.startIndex] Start index
|
|
395
|
+
* @param {number} [obj.endIndex] Start index
|
|
396
|
+
* @returns {object} Object with skip and limit values
|
|
397
|
+
*/
|
|
398
|
+
const parsePagination = ({ startIndex, endIndex }={})=> {
|
|
399
|
+
startIndex = +startIndex; endIndex = +endIndex;
|
|
400
|
+
startIndex = startIndex < 1 ? 1 : startIndex;
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
skip: Number.isInteger(startIndex) ? startIndex - 1 : undefined,
|
|
404
|
+
limit: Number.isInteger(startIndex) && Number.isInteger(endIndex) ? ((endIndex - startIndex + 1) < 0 ? 0 : endIndex - startIndex + 1) : undefined
|
|
405
|
+
};
|
|
406
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mongodb-ops",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Read and write ops for MongoDB",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mongodb-ops",
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"description": "Read and write ops for MongoDB",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test",
|
|
8
|
+
"lint": "node --check index.js && node --check lib/mongodb-ops.js && node --check test/mongodb-ops.test.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"mongodb": "^6.8.0"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/CompAndSave/mongodb-ops.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mongodb"
|
|
19
|
+
],
|
|
20
|
+
"author": "Andrew Y",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/CompAndSave/mongodb-ops/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/CompAndSave/mongodb-ops#readme",
|
|
26
|
+
"directories": {
|
|
27
|
+
"lib": "lib"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/readme.md
CHANGED
|
@@ -1,65 +1,130 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
static
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
# mongodb-ops
|
|
2
|
+
|
|
3
|
+
Lightweight read/write helpers for MongoDB with a **shared, self-managing client**. The MongoDB client is cached at the class (process) level per connection string and reused across calls — you connect once and let the driver handle pooling, topology monitoring, and failover recovery.
|
|
4
|
+
|
|
5
|
+
> `writeConcern` is not configurable through this library.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install mongodb-ops
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js 16+ and bundles the official `mongodb` driver (v6).
|
|
14
|
+
|
|
15
|
+
## Exports
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const { MongoDBOps, MongoDBToolSet } = require('mongodb-ops');
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **`MongoDBOps`** — low-level static/instance operations (`getData`, `search`, `writeData`, `writeBulkData`, `getCollectionCount`, `getDbClient`, `closeDBConn`, `getObjectId`).
|
|
22
|
+
- **`MongoDBToolSet`** — a higher-level, collection-scoped convenience class that extends `MongoDBOps` (get / insert / update / replace / delete + bulk variants).
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
### Collection-scoped (`MongoDBToolSet`)
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const { MongoDBToolSet } = require('mongodb-ops');
|
|
30
|
+
|
|
31
|
+
const connString = "mongodb+srv://USER:PASS@your-cluster.mongodb.net/your-db?retryWrites=true&w=majority";
|
|
32
|
+
|
|
33
|
+
// Instance style — bind a collection + connection once
|
|
34
|
+
const orders = new MongoDBToolSet('orders', connString);
|
|
35
|
+
|
|
36
|
+
await orders.insertOne({ number: 'PO-1001', status: 'open' });
|
|
37
|
+
const open = await orders.getDataByFilter({ status: 'open' }, { number: 1 }, { number: -1 });
|
|
38
|
+
await orders.updateOne({ $set: { status: 'closed' } }, { number: 'PO-1001' });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every method also has a **static** form that takes the collection name and connection string explicitly — handy when wrapping it in your own model class:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
class Order extends MongoDBToolSet {
|
|
45
|
+
static collectionName = 'orders';
|
|
46
|
+
static connString = connString;
|
|
47
|
+
|
|
48
|
+
static getById(id) {
|
|
49
|
+
return MongoDBToolSet.getDataByID(this.collectionName, id, undefined, this.connString);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Low-level (`MongoDBOps`)
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const { MongoDBOps } = require('mongodb-ops');
|
|
58
|
+
|
|
59
|
+
const rows = await MongoDBOps.getData(
|
|
60
|
+
'orders',
|
|
61
|
+
{ status: 'open' }, // query
|
|
62
|
+
false, // isAggregate
|
|
63
|
+
{ number: 1 }, // projection
|
|
64
|
+
{ number: -1 }, // sort
|
|
65
|
+
{ startIndex: 1, endIndex: 20 }, // pagination (1-based, inclusive)
|
|
66
|
+
false, // isGetCount
|
|
67
|
+
connString
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Connection management
|
|
72
|
+
|
|
73
|
+
`getDbClient(connString)` maintains **one cached `MongoClient` per connection string** for the life of the process (kept in an internal `Map`; concurrent first-connects are de-duplicated). All operations reuse it, so there is no per-call connect overhead — and the driver's topology monitoring transparently rediscovers a new primary after a replica-set failover.
|
|
74
|
+
|
|
75
|
+
Clients are created with safe, failover-friendly defaults:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
{ serverSelectionTimeoutMS: 8000, retryWrites: true, retryReads: true }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Override or extend** the driver options process-wide, before the first connection is made — your values are merged over the defaults:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
const { MongoDBOps } = require('mongodb-ops');
|
|
85
|
+
MongoDBOps.clientOptions = { maxPoolSize: 20 };
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Close** every cached connection (e.g. in a CLI or test teardown; long-running servers/Lambdas normally leave them open for reuse):
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
await MongoDBOps.closeDBConn();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API
|
|
95
|
+
|
|
96
|
+
### `MongoDBToolSet`
|
|
97
|
+
|
|
98
|
+
Constructor: `new MongoDBToolSet(collectionName, connString)`. Instance methods use the bound collection/connection; each has a matching static method that takes them as arguments.
|
|
99
|
+
|
|
100
|
+
| Read | Write | Bulk |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `getDataByID` | `insertOne` | `insertBulkOrdered` / `insertBulkUnOrdered` |
|
|
103
|
+
| `getDataByFilter` | `replaceOne` | `replaceBulkOrdered` / `replaceBulkUnOrdered` |
|
|
104
|
+
| `getDataByAggregate` | `updateOne` / `updateMany` | `updateBulkOrdered` / `updateBulkUnOrdered` |
|
|
105
|
+
| `getDataCount` | `deleteOne` / `deleteMany` | `deleteBulkOrdered` / `deleteBulkUnOrdered` |
|
|
106
|
+
| `list` (rows + optional total) | | `allBulkOrdered` / `allBulkUnOrdered` |
|
|
107
|
+
| `getAllData` | | |
|
|
108
|
+
|
|
109
|
+
### `MongoDBOps`
|
|
110
|
+
|
|
111
|
+
`getData`, `search`, `writeData(type, …)`, `writeBulkData(type, …, ordered)`, `getCollectionCount`, `getObjectId`, `getDbClient`, `closeDBConn`.
|
|
112
|
+
|
|
113
|
+
## Changelog
|
|
114
|
+
|
|
115
|
+
### 0.11.1
|
|
116
|
+
- **Hardened connection caching.** The client cache is now keyed by connection string in a `Map` (previously matched against MongoDB driver internals), making client reuse reliable across driver versions.
|
|
117
|
+
- **De-duplicated concurrent cold-start connects** by caching the connect promise; a failed connect is evicted so the next call retries instead of caching a rejected promise.
|
|
118
|
+
- **Safer driver defaults:** `serverSelectionTimeoutMS: 8000`, `retryWrites: true`, `retryReads: true` — reads now recover automatically across a replica-set failover/election.
|
|
119
|
+
- **New `MongoDBOps.clientOptions`** hook to override/extend the driver options process-wide.
|
|
120
|
+
- `closeDBConn()` now drains the client `Map` (and any legacy cache).
|
|
121
|
+
|
|
122
|
+
### 0.11.0
|
|
123
|
+
- Added estimated collection count (`getCollectionCount`).
|
|
124
|
+
|
|
125
|
+
### Earlier
|
|
126
|
+
- `search`, collation, and aggregate options. See the git history for details.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
ISC
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const mongodb = require('mongodb');
|
|
6
|
+
|
|
7
|
+
const originalConnect = mongodb.MongoClient.connect;
|
|
8
|
+
|
|
9
|
+
function freshMongoDBOps(connectImpl) {
|
|
10
|
+
delete require.cache[require.resolve('../lib/mongodb-ops')];
|
|
11
|
+
mongodb.MongoClient.connect = connectImpl;
|
|
12
|
+
|
|
13
|
+
const MongoDBOps = require('../lib/mongodb-ops');
|
|
14
|
+
MongoDBOps.dbClients = undefined;
|
|
15
|
+
MongoDBOps.dbClientList = undefined;
|
|
16
|
+
MongoDBOps.clientOptions = undefined;
|
|
17
|
+
|
|
18
|
+
return MongoDBOps;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createClient(name) {
|
|
22
|
+
const closeCalls = [];
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
closeCalls,
|
|
26
|
+
db() { return {}; },
|
|
27
|
+
async close() { closeCalls.push(name); }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test.afterEach(() => {
|
|
32
|
+
mongodb.MongoClient.connect = originalConnect;
|
|
33
|
+
delete require.cache[require.resolve('../lib/mongodb-ops')];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('reuses one client for repeated calls with the same connection string', async () => {
|
|
37
|
+
const calls = [];
|
|
38
|
+
const client = createClient('same');
|
|
39
|
+
const MongoDBOps = freshMongoDBOps(async (connString, options) => {
|
|
40
|
+
calls.push({ connString, options });
|
|
41
|
+
return client;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const first = await MongoDBOps.getDbClient('mongodb://example-a');
|
|
45
|
+
const second = await MongoDBOps.getDbClient('mongodb://example-a');
|
|
46
|
+
|
|
47
|
+
assert.equal(first, client);
|
|
48
|
+
assert.equal(second, client);
|
|
49
|
+
assert.equal(first, second);
|
|
50
|
+
assert.equal(calls.length, 1);
|
|
51
|
+
assert.equal(calls[0].options.serverSelectionTimeoutMS, 8000);
|
|
52
|
+
assert.equal(calls[0].options.retryWrites, true);
|
|
53
|
+
assert.equal(calls[0].options.retryReads, true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('uses separate clients for different connection strings', async () => {
|
|
57
|
+
const calls = [];
|
|
58
|
+
const MongoDBOps = freshMongoDBOps(async (connString, options) => {
|
|
59
|
+
calls.push({ connString, options });
|
|
60
|
+
return createClient(connString);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const first = await MongoDBOps.getDbClient('mongodb://example-a');
|
|
64
|
+
const second = await MongoDBOps.getDbClient('mongodb://example-b');
|
|
65
|
+
|
|
66
|
+
assert.notEqual(first, second);
|
|
67
|
+
assert.deepEqual(calls.map((call) => call.connString), ['mongodb://example-a', 'mongodb://example-b']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('dedupes concurrent cold-start connection attempts', async () => {
|
|
71
|
+
const calls = [];
|
|
72
|
+
const client = createClient('concurrent');
|
|
73
|
+
let releaseConnect;
|
|
74
|
+
const connectStarted = new Promise((resolve) => {
|
|
75
|
+
releaseConnect = resolve;
|
|
76
|
+
});
|
|
77
|
+
const MongoDBOps = freshMongoDBOps(async (connString, options) => {
|
|
78
|
+
calls.push({ connString, options });
|
|
79
|
+
await connectStarted;
|
|
80
|
+
return client;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const pending = Array.from({ length: 5 }, () => MongoDBOps.getDbClient('mongodb://example-a'));
|
|
84
|
+
assert.equal(calls.length, 1);
|
|
85
|
+
|
|
86
|
+
releaseConnect();
|
|
87
|
+
const clients = await Promise.all(pending);
|
|
88
|
+
|
|
89
|
+
assert.equal(calls.length, 1);
|
|
90
|
+
assert.equal(new Set(clients).size, 1);
|
|
91
|
+
assert.equal(clients[0], client);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('evicts failed connection promises so a later call reconnects', async () => {
|
|
95
|
+
let attempts = 0;
|
|
96
|
+
const client = createClient('retry');
|
|
97
|
+
const MongoDBOps = freshMongoDBOps(async () => {
|
|
98
|
+
attempts += 1;
|
|
99
|
+
if (attempts === 1) { throw new Error('connect failed'); }
|
|
100
|
+
return client;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await assert.rejects(MongoDBOps.getDbClient('mongodb://example-a'), /connect failed/);
|
|
104
|
+
const recovered = await MongoDBOps.getDbClient('mongodb://example-a');
|
|
105
|
+
|
|
106
|
+
assert.equal(recovered, client);
|
|
107
|
+
assert.equal(attempts, 2);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('closeDBConn closes cached clients, clears the map, and reconnects next time', async () => {
|
|
111
|
+
const calls = [];
|
|
112
|
+
const clients = [];
|
|
113
|
+
const MongoDBOps = freshMongoDBOps(async (connString, options) => {
|
|
114
|
+
calls.push({ connString, options });
|
|
115
|
+
const client = createClient(connString);
|
|
116
|
+
clients.push(client);
|
|
117
|
+
return client;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await MongoDBOps.getDbClient('mongodb://example-a');
|
|
121
|
+
await MongoDBOps.getDbClient('mongodb://example-b');
|
|
122
|
+
await MongoDBOps.closeDBConn();
|
|
123
|
+
|
|
124
|
+
assert.equal(clients[0].closeCalls.length, 1);
|
|
125
|
+
assert.equal(clients[1].closeCalls.length, 1);
|
|
126
|
+
assert.equal(MongoDBOps.dbClients.size, 0);
|
|
127
|
+
|
|
128
|
+
await MongoDBOps.getDbClient('mongodb://example-a');
|
|
129
|
+
assert.equal(calls.length, 3);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('closeDBConn also drains legacy dbClientList clients', async () => {
|
|
133
|
+
const MongoDBOps = freshMongoDBOps(async () => createClient('unused'));
|
|
134
|
+
const legacyClient = createClient('legacy');
|
|
135
|
+
MongoDBOps.dbClientList = [legacyClient];
|
|
136
|
+
|
|
137
|
+
await MongoDBOps.closeDBConn();
|
|
138
|
+
|
|
139
|
+
assert.equal(legacyClient.closeCalls.length, 1);
|
|
140
|
+
assert.deepEqual(MongoDBOps.dbClientList, []);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('passes safe MongoDB driver options and permits consumer overrides', async () => {
|
|
144
|
+
const calls = [];
|
|
145
|
+
const client = createClient('options');
|
|
146
|
+
const MongoDBOps = freshMongoDBOps(async (connString, options) => {
|
|
147
|
+
calls.push({ connString, options });
|
|
148
|
+
return client;
|
|
149
|
+
});
|
|
150
|
+
MongoDBOps.clientOptions = { serverSelectionTimeoutMS: 5000, maxPoolSize: 10 };
|
|
151
|
+
|
|
152
|
+
await MongoDBOps.getDbClient('mongodb://example-a');
|
|
153
|
+
|
|
154
|
+
assert.equal(calls.length, 1);
|
|
155
|
+
assert.equal(calls[0].options.serverSelectionTimeoutMS, 5000);
|
|
156
|
+
assert.equal(calls[0].options.retryWrites, true);
|
|
157
|
+
assert.equal(calls[0].options.retryReads, true);
|
|
158
|
+
assert.equal(calls[0].options.maxPoolSize, 10);
|
|
159
|
+
});
|
|
160
|
+
|