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.
@@ -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 (!Array.isArray(MongoDBOps.dbClientList)) { MongoDBOps.dbClientList = []; }
44
-
45
- for (let item of MongoDBOps.dbClientList) {
46
- if (item.s.url === connString) {
47
- if (item.topology.s.state !== "connected") {
48
- item = await MongoClient.connect(connString);
49
- }
50
-
51
- return Promise.resolve(item);
52
- }
53
- }
54
-
55
- const newDbClient = await MongoClient.connect(connString);
56
- MongoDBOps.dbClientList.push(newDbClient);
57
-
58
- return Promise.resolve(newDbClient);
59
- }
60
-
61
- /**
62
- * Static method - Close database connection
63
- * @returns {promise}
64
- */
65
- static async closeDBConn() {
66
- if (Array.isArray(MongoDBOps.dbClientList)) {
67
- for (const item of MongoDBOps.dbClientList) {
68
- await item.close();
69
- }
70
- }
71
- return Promise.resolve();
72
- }
73
-
74
- /**
75
- * Instance method - Close database connection
76
- * @returns {promise}
77
- */
78
- async closeDBConn() {
79
- return Promise.resolve(await MongoDBOps.closeDBConn());
80
- }
81
-
82
- /**
83
- * Static method - Get documents from MongoDB
84
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/}
85
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/}
86
- *
87
- * @param {string} collectionName Collection name
88
- * @param {object} [queryExp] Query Specifies selection filter using query operators - {@link https://docs.mongodb.com/manual/reference/operator/}
89
- * @param {boolean} [isAggregate=false] Set true to use the aggregation
90
- * @param {object} [projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
91
- * @param {object} [sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
92
- * @param {object} [pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
93
- * @param {boolean} [isGetCount=false] Set true to get the number of doc count based on the queryExp
94
- * @param {string} connString Database connection string
95
- * @param {object} [collation] Collation {@link https://www.mongodb.com/docs/manual/reference/collation/#std-label-collation-document-fields}
96
- * @param {object} [options] Aggregate options {@link https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/}
97
- * @returns {promise} Promise with object array
98
- */
99
- static async getData(collectionName, queryExp, isAggregate = false, projection, sort, pagination, isGetCount = false, connString, collation, options) {
100
- const db = (await MongoDBOps.getDbClient(connString)).db();
101
-
102
- if (isAggregate) { return Promise.resolve(await db.collection(collectionName).aggregate(queryExp, options).toArray()); }
103
-
104
- queryExp = queryExp || {};
105
- if (isGetCount) { return Promise.resolve(await db.collection(collectionName).countDocuments(queryExp)); }
106
-
107
- const { skip, limit } = parsePagination(pagination);
108
- if (limit < 1) { return Promise.resolve([]); }
109
-
110
- projection = { projection: projection || {} };
111
-
112
- let data = db.collection(collectionName).find(queryExp, projection);
113
- if (sort) { data = data.sort(sort); }
114
- if (pagination) { data = data.skip(skip).limit(limit); }
115
- if (collation) { data = data.collation(collation); }
116
-
117
- return Promise.resolve(await data.toArray());
118
- }
119
-
120
- /**
121
- * Instance method - Get documents from MongoDB
122
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/}
123
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/}
124
- *
125
- * @param {string} collectionName Collection name
126
- * @param {object} [queryExp] Query Specifies selection filter using query operators - {@link https://docs.mongodb.com/manual/reference/operator/}
127
- * @param {boolean} [isAggregate=false] Set true to use the aggregation
128
- * @param {object} [projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
129
- * @param {object} [sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
130
- * @param {object} [pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
131
- * @param {boolean} [isGetCount=false] Set true to get the number of doc count based on the queryExp
132
- * @param {object} [collation] Collation {@link https://www.mongodb.com/docs/manual/reference/collation/#std-label-collation-document-fields}
133
- * @param {object} [options] Aggregate options {@link https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/}
134
- * @returns {promise} Promise with object array
135
- */
136
- async getData(collectionName, queryExp, isAggregate, projection, sort, pagination, isGetCount, collation, options) {
137
- return Promise.resolve(await MongoDBOps.getData(collectionName, queryExp, isAggregate, projection, sort, pagination, isGetCount, this.connString, collation, options));
138
- }
139
-
140
- /**
141
- * Static method - Use altas search at MongoDB
142
- * {@link https://www.mongodb.com/docs/atlas/atlas-search/}
143
- *
144
- * @param {string} connString Database connection string
145
- * @param {string} collectionName Collection name
146
- * @param {object} search $search object - {@link https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/}
147
- * @param {object} [obj]
148
- * @param {object} [obj.projection] Projection {@link https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection}
149
- * @param {object} [obj.sort] Sort {@link https://docs.mongodb.com/manual/reference/method/cursor.sort/#cursor.sort}
150
- * @param {object} [obj.pagination] Pagination `E.g., { startIndex: 11, endIndex: 20 }`
151
- * @param {boolean} [isGetCount=true] Set true to get the number of total matching docs and current page number
152
- * @returns {promise} Promise with object or object array
153
- */
154
- static async search(connString, collectionName, search, { projection, sort, pagination }={}, isGetCount = true) {
155
- if ([connString, collectionName, search].includes(undefined)) { return Promise.reject("Connection string, collection name and search cannot be undefined"); }
156
-
157
- const { skip, limit } = parsePagination(pagination);
158
- if (limit < 1) { return Promise.resolve([]); }
159
-
160
- const payload = [
161
- { $search: search },
162
- { $addFields: { score: { $meta: "searchScore" }}},
163
- ];
164
-
165
- if (projection) { payload.push({ $project: projection }); }
166
- if (sort) { payload.push({ $sort: sort }); }
167
-
168
- const skipLimit = [];
169
- if (skip) { skipLimit.push({ $skip: skip }); }
170
- if (limit) { skipLimit.push({ $limit: limit }); }
171
-
172
- if (isGetCount) {
173
- payload.push({
174
- $facet: {
175
- metadata: [{ $count: "total" }, { $addFields: { page: Math.ceil((skip + 1) / limit) }}],
176
- data: skipLimit,
177
- }
178
- });
179
- }
180
- else { payload.push(...skipLimit); }
181
-
182
- const result = await MongoDBOps.getData(collectionName, payload, true, undefined, undefined, undefined, undefined, connString);
183
-
184
- return Promise.resolve(isGetCount ? result[0] : result);
185
- }
186
-
187
- /**
188
- * Instance method - Use altas search at MongoDB
189
- * {@link https://www.mongodb.com/docs/atlas/atlas-search/}
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
- return Promise.resolve(await MongoDBOps.search(this.connString, collectionName, search, { projection, sort, pagination }, isGetCount));
202
- }
203
-
204
- /**
205
- * Static method - Write document to MongoDB
206
- *
207
- * References:
208
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.insertOne/}
209
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/}
210
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/}
211
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/}
212
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteOne/}
213
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteMany/}
214
- *
215
- * @param {string} type Write type - insertOne, replaceOne, updateOne, updateMany, deleteOne, deleteMany
216
- * @param {string} collectionName Collection name
217
- * @param {object} doc Data document
218
- * @param {object} filter Query filter {@link https://docs.mongodb.com/manual/core/document/#document-query-filter}
219
- * @param {string} connString Database connection string
220
- * @returns {promise}
221
- */
222
- static async writeData(type, collectionName, doc, filter, connString) {
223
- try {
224
- const db = (await MongoDBOps.getDbClient(connString)).db();
225
-
226
- let result;
227
- switch(type) {
228
- case "insertOne": result = await db.collection(collectionName).insertOne(doc); break;
229
- case "replaceOne": result = await db.collection(collectionName).replaceOne(filter, doc); break;
230
- case "updateOne": result = await db.collection(collectionName).updateOne(filter, doc); break;
231
- case "updateMany": result = await db.collection(collectionName).updateMany(filter, doc); break;
232
- case "deleteOne": result = await db.collection(collectionName).deleteOne(filter); break;
233
- case "deleteMany": result = await db.collection(collectionName).deleteMany(filter); break;
234
- default: throw new Error("invalid-writeData-type");
235
- }
236
- return Promise.resolve(result);
237
- }
238
- catch (err) { return Promise.reject(err.errmsg || err.message || err); }
239
- }
240
-
241
- /**
242
- * Instance method - Write document to MongoDB
243
- *
244
- * References:
245
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.insertOne/}
246
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/}
247
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/}
248
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/}
249
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteOne/}
250
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.deleteMany/}
251
- *
252
- * @param {string} type Write type - insertOne, replaceOne, updateOne, updateMany, deleteOne, deleteMany
253
- * @param {string} collectionName Collection name
254
- * @param {object} doc Data document
255
- * @param {object} filter Query filter {@link https://docs.mongodb.com/manual/core/document/#document-query-filter}
256
- * @returns {promise}
257
- */
258
- async writeData(type, collectionName, doc, filter) {
259
- return Promise.resolve(await MongoDBOps.writeData(type, collectionName, doc, filter, this.connString));
260
- }
261
-
262
- /**
263
- * Static method - Write document to MongoDB via BulkOps / BulkWrite
264
- *
265
- * References:
266
- * {@link https://mongodb.github.io/node-mongodb-native/3.6/api/BulkOperationBase.html}
267
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/}
268
- *
269
- * @param {string} type Write type - insertBulk, replaceBulk, updateBulk, allBulk
270
- * @param {string} collectionName Collection name
271
- * @param {Array} docs Data documents array
272
- * @param {boolean} [ordered=false] Set true to use ordered bulkWrite
273
- * @param {string} connString Database connection string
274
- * @returns {promise}
275
- */
276
- static async writeBulkData(type, collectionName, docs, ordered = false, connString) {
277
- try {
278
- const db = (await MongoDBOps.getDbClient(connString)).db();
279
-
280
- let result;
281
- switch(type) {
282
- case "insertBulk":
283
- for (let i = 0; i < docs.length; ++i) { docs[i] = { insertOne: { "document": docs[i] }}; }
284
- break;
285
- case "replaceBulk":
286
- // doc = {
287
- // "filter": <document>,
288
- // "replacement": <document>,
289
- // "upsert": <boolean>,
290
- // "collation": <document>,
291
- // "hint": <document|string>
292
- // }
293
- for (let i = 0; i < docs.length; ++i) { docs[i] = { replaceOne: docs[i] }; }
294
- break;
295
- case "updateBulk":
296
- // doc = {
297
- // "filter": <document>,
298
- // "update": <document or pipeline>,
299
- // "upsert": <boolean>,
300
- // "collation": <document>,
301
- // "arrayFilters": [ <filterdocument1>, ... ],
302
- // "hint": <document|string>
303
- // }
304
- for (let i = 0; i < docs.length; ++i) { docs[i] = { updateOne: docs[i] }; }
305
- break;
306
- case "deleteBulk":
307
- // doc = {
308
- // "filter": <document>,
309
- // "collation": <document>
310
- // }
311
- for (let i = 0; i < docs.length; ++i) { docs[i] = { deleteOne: docs[i] }; }
312
- break;
313
- case "allBulk":
314
- // allowed bulkWrite operations include insertOne, replaceOne, updateOne, updateMany, deleteOne, deleteMany
315
- break;
316
- default: throw new Error("invalid-writeBulkData-type");
317
- }
318
- result = await db.collection(collectionName).bulkWrite(docs, { ordered: ordered });
319
- return Promise.resolve(result);
320
- }
321
- catch (err) { return Promise.reject(err.result || err.errmsg || err.message); }
322
- }
323
-
324
- /**
325
- * Instance method - Write document to MongoDB via BulkOps / BulkWrite
326
- *
327
- * References:
328
- * {@link https://mongodb.github.io/node-mongodb-native/3.6/api/BulkOperationBase.html}
329
- * {@link https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/}
330
- *
331
- * @param {string} type Write type - insertBulk, replaceBulk, updateBulk, allBulk
332
- * @param {string} collectionName Collection name
333
- * @param {Array} docs Data documents array
334
- * @param {boolean} [ordered=false] Set true to use ordered bulkWrite
335
- * @returns {promise}
336
- */
337
- async writeBulkData(type, collectionName, docs, ordered = false) {
338
- return Promise.resolve(await MongoDBOps.writeBulkData(type, collectionName, docs, ordered, this.connString));
339
- }
340
- }
341
-
342
- module.exports = MongoDBOps;
343
-
344
- /**
345
- * Parse the pagaintion and return skip and limit values
346
- *
347
- * @param {object} obj
348
- * @param {number} [obj.startIndex] Start index
349
- * @param {number} [obj.endIndex] Start index
350
- * @returns {object} Object with skip and limit values
351
- */
352
- const parsePagination = ({ startIndex, endIndex }={})=> {
353
- startIndex = +startIndex; endIndex = +endIndex;
354
- startIndex = startIndex < 1 ? 1 : startIndex;
355
-
356
- return {
357
- skip: Number.isInteger(startIndex) ? startIndex - 1 : undefined,
358
- limit: Number.isInteger(startIndex) && Number.isInteger(endIndex) ? ((endIndex - startIndex + 1) < 0 ? 0 : endIndex - startIndex + 1) : undefined
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.10.1",
4
- "description": "Read and write ops for MongoDB",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "dependencies": {
10
- "mongodb": "^6.8.0"
11
- },
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/CompAndSave/mongodb-ops.git"
15
- },
16
- "keywords": [
17
- "mongodb"
18
- ],
19
- "author": "Andrew Y",
20
- "license": "ISC",
21
- "bugs": {
22
- "url": "https://github.com/CompAndSave/mongodb-ops/issues"
23
- },
24
- "homepage": "https://github.com/CompAndSave/mongodb-ops#readme",
25
- "directories": {
26
- "lib": "lib"
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
- # Read and write ops for MongoDB
2
- **DB client is at static level - It will keep alive and can be reused**
3
-
4
- **Updated - MongoDBToolSet class is added**
5
- ```
6
- const { MongoDBToolSet } = require('mongodb-ops');
7
- ```
8
-
9
- **Sample code to use the class**
10
-
11
- ```
12
- const { MongoDBOps } = require('mongodb-ops');
13
- const connString = "mongodb+srv://YOUR_DB_USERNAME:YOUR_DB_PASSWORD@YOUR_MONGO_ATLAS_URL/YOUR_DBNAME?retryWrites=true&w=majority";
14
-
15
- class MongoDBToolSet extends MongoDBOps {
16
- constructor(collectionName) {
17
- super(connString);
18
- this.collectionName = collectionName;
19
- }
20
-
21
- static async getDataByID(collectionName, id, projection) {
22
- let queryFilter = { _id:id };
23
- return Promise.resolve(await MongoDBOps.getData(collectionName, queryFilter, false, projection, undefined, undefined, undefined, connString));
24
- }
25
-
26
- async getDataByID(id, projection) { return Promise.resolve(await MongoDBToolSet.getDataByID(this.collectionName, id, projection)); }
27
-
28
- static async getAllData(collectionName, projection, sort, pagination) {
29
- let queryFilter = {};
30
- return Promise.resolve(await MongoDBOps.getData(collectionName, queryFilter, false, projection, sort, pagination, false, connString));
31
- }
32
-
33
- async getAllData(site, projection, sort, pagination) { return Promise.resolve(await MongoDBToolSet.getAllData(this.collectionName, projection, sort, pagination)); }
34
-
35
- static async insertOne(collectionName, doc) { return Promise.resolve(await MongoDBOps.writeData("insertOne", collectionName, doc, undefined, connString)); }
36
- async insertOne(doc) { return Promise.resolve(await super.writeData("insertOne", this.collectionName, doc)); }
37
- static async insertBulkOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("insertBulk", collectionName, docs, true, connString)); }
38
- async insertBulkOrdered(docs) { return Promise.resolve(await super.writeBulkData("insertBulk", this.collectionName, docs, true)); }
39
- static async insertBulkUnOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("insertBulk", collectionName, docs, false, connString)); }
40
- async insertBulkUnOrdered(docs) { return Promise.resolve(await super.writeBulkData("insertBulk", this.collectionName, docs, false)); }
41
-
42
- static async replaceOne(collectionName, doc, filter) { return Promise.resolve(await MongoDBOps.writeData("replaceOne", collectionName, doc, filter, connString)); }
43
- async replaceOne(doc, filter) { return Promise.resolve(await super.writeData("replaceOne", this.collectionName, doc, filter)); }
44
- static async replaceBulkOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("replaceBulk", collectionName, docs, true, connString)); }
45
- async replaceBulkOrdered(docs) { return Promise.resolve(await super.writeBulkData("replaceBulk", this.collectionName, docs, true)); }
46
- static async replaceBulkUnOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("replaceBulk", collectionName, docs, false, connString)); }
47
- async replaceBulkUnOrdered(docs) { return Promise.resolve(await super.writeBulkData("replaceBulk", this.collectionName, docs, false)); }
48
-
49
- static async updateOne(collectionName, doc, filter) { return Promise.resolve(await MongoDBOps.writeData("updateOne", collectionName, doc, filter, connString)); }
50
- async updateOne(doc, filter) { return Promise.resolve(await super.writeData("updateOne", this.collectionName, doc, filter)); }
51
- static async updateMany(collectionName, doc, filter) { return Promise.resolve(await MongoDBOps.writeData("updateMany", collectionName, doc, filter, connString)); }
52
- async updateMany(doc, filter) { return Promise.resolve(await super.writeData("updateMany", this.collectionName, doc, filter)); }
53
- static async updateBulkOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("updateBulk", collectionName, docs, true, connString)); }
54
- async updateBulkOrdered(docs) { return Promise.resolve(await super.writeBulkData("updateBulk", this.collectionName, docs, true)); }
55
- static async updateBulkUnOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("updateBulk", collectionName, docs, false, connString)); }
56
- async updateBulkUnOrdered(docs) { return Promise.resolve(await super.writeBulkData("updateBulk", this.collectionName, docs, false)); }
57
-
58
- static async allBulkOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("allBulk", collectionName, docs, true, connString)); }
59
- async allBulkOrdered(docs) { return Promise.resolve(await super.writeBulkData("allBulk", this.collectionName, docs, true)); }
60
- static async allBulkUnOrdered(collectionName, docs) { return Promise.resolve(await MongoDBOps.writeBulkData("allBulk", collectionName, docs, false, connString)); }
61
- async allBulkUnOrdered(docs) { return Promise.resolve(await super.writeBulkData("allBulk", this.collectionName, docs, false)); }
62
- }
63
-
64
- module.exports = MongoDB;
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
+