mongodb-ops 0.11.0 → 0.11.2

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/package.json CHANGED
@@ -1,28 +1,32 @@
1
- {
2
- "name": "mongodb-ops",
3
- "version": "0.11.0",
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.2",
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 && node --check test/mongodb-write-errors.test.js"
9
+ },
10
+ "dependencies": {
11
+ "mongodb": "^6.8.0"
12
+ },
13
+ "devDependencies": {
14
+ "mongodb-memory-server": "^11.2.0"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/CompAndSave/mongodb-ops.git"
19
+ },
20
+ "keywords": [
21
+ "mongodb"
22
+ ],
23
+ "author": "Andrew Y",
24
+ "license": "ISC",
25
+ "bugs": {
26
+ "url": "https://github.com/CompAndSave/mongodb-ops/issues"
27
+ },
28
+ "homepage": "https://github.com/CompAndSave/mongodb-ops#readme",
29
+ "directories": {
30
+ "lib": "lib"
31
+ }
32
+ }
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,7 @@
1
+ # Orders Validator
2
+
3
+ `vendor-orders-validator.json` is an unmodified copy of vendor-api's
4
+ `db/validators/orders.json` at commit `303a01b425246b588f027145db04abe53049882e`.
5
+ The real schema is checked in so the integration tests do not depend on a
6
+ separate vendor-api checkout. In particular, `invoice_date: null` must be
7
+ rejected by the same strict validator used by that service.
@@ -0,0 +1,224 @@
1
+ {
2
+ "$jsonSchema": {
3
+ "bsonType": "object",
4
+ "required": [
5
+ "_id",
6
+ "ts",
7
+ "order_id",
8
+ "vendor",
9
+ "submitted_by",
10
+ "status",
11
+ "address"
12
+ ],
13
+ "additionalProperties": false,
14
+ "properties": {
15
+ "_id": {
16
+ "bsonType": "string",
17
+ "pattern": "^EDI-[A-Za-z0-9-]+$",
18
+ "description": "site EDI-<digits> or marketplace EDI-<SELLERCODE>-<rawOrderId>"
19
+ },
20
+ "ts": {
21
+ "bsonType": "date"
22
+ },
23
+ "order_id": {
24
+ "bsonType": "string",
25
+ "pattern": "^[A-Za-z0-9-]+$",
26
+ "description": "site numeric id or marketplace raw order id"
27
+ },
28
+ "vendor": {
29
+ "bsonType": "string",
30
+ "pattern": "^[a-z]+$"
31
+ },
32
+ "vendor_po_num": {
33
+ "bsonType": "string",
34
+ "pattern": "^EDI-[A-Za-z0-9-]+$"
35
+ },
36
+ "submitted_by": {
37
+ "bsonType": "string"
38
+ },
39
+ "status": {
40
+ "bsonType": "string",
41
+ "pattern": "^(?:new|shipped|completed|send_error)$"
42
+ },
43
+ "items": {
44
+ "bsonType": "array",
45
+ "minimum": 1,
46
+ "items": {
47
+ "bsonType": "object",
48
+ "required": [
49
+ "sku",
50
+ "vendor_sku",
51
+ "unit_price",
52
+ "qty"
53
+ ],
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "sku": {
57
+ "bsonType": "string"
58
+ },
59
+ "vendor_sku": {
60
+ "bsonType": "string"
61
+ },
62
+ "unit_price": {
63
+ "bsonType": [
64
+ "int",
65
+ "double"
66
+ ]
67
+ },
68
+ "qty": {
69
+ "bsonType": "int"
70
+ }
71
+ }
72
+ }
73
+ },
74
+ "total_before_shipping": {
75
+ "bsonType": [
76
+ "int",
77
+ "double"
78
+ ]
79
+ },
80
+ "address": {
81
+ "bsonType": "object",
82
+ "required": [
83
+ "company_name",
84
+ "name",
85
+ "address_line1",
86
+ "address_line2",
87
+ "city",
88
+ "state_province",
89
+ "postal_code",
90
+ "country_code"
91
+ ],
92
+ "additionalProperties": false,
93
+ "properties": {
94
+ "company_name": {
95
+ "bsonType": "string"
96
+ },
97
+ "name": {
98
+ "bsonType": "string"
99
+ },
100
+ "address_line1": {
101
+ "bsonType": "string"
102
+ },
103
+ "address_line2": {
104
+ "bsonType": "string"
105
+ },
106
+ "city": {
107
+ "bsonType": "string"
108
+ },
109
+ "state_province": {
110
+ "bsonType": "string"
111
+ },
112
+ "postal_code": {
113
+ "bsonType": "string"
114
+ },
115
+ "country_code": {
116
+ "bsonType": "string"
117
+ }
118
+ }
119
+ },
120
+ "invoice_date": {
121
+ "bsonType": "string",
122
+ "pattern": "^[\\d]{4}-[\\d]{2}-[\\d]{2}$"
123
+ },
124
+ "invoice_id": {
125
+ "bsonType": "string"
126
+ },
127
+ "invoice_total": {
128
+ "bsonType": [
129
+ "int",
130
+ "double"
131
+ ]
132
+ },
133
+ "tracking_num": {
134
+ "bsonType": "string"
135
+ },
136
+ "tracking_updated_at": {
137
+ "bsonType": "date",
138
+ "description": "when tracking was last populated on this PO"
139
+ },
140
+ "shipping_cost": {
141
+ "bsonType": [
142
+ "int",
143
+ "double"
144
+ ]
145
+ },
146
+ "carrier": {
147
+ "bsonType": "string"
148
+ },
149
+ "signature_requested": {
150
+ "bsonType": "bool"
151
+ },
152
+ "channel": {
153
+ "bsonType": "string"
154
+ },
155
+ "seller_account": {
156
+ "bsonType": [
157
+ "string",
158
+ "null"
159
+ ]
160
+ },
161
+ "seller_code": {
162
+ "bsonType": [
163
+ "string",
164
+ "null"
165
+ ]
166
+ },
167
+ "marketplace_id": {
168
+ "bsonType": [
169
+ "string",
170
+ "null"
171
+ ]
172
+ },
173
+ "raw_order_id": {
174
+ "bsonType": [
175
+ "string",
176
+ "null"
177
+ ]
178
+ },
179
+ "source_order_id": {
180
+ "bsonType": "string"
181
+ },
182
+ "send_error": {
183
+ "bsonType": "string"
184
+ },
185
+ "marketplace_writeback_status": {
186
+ "bsonType": "string"
187
+ },
188
+ "marketplace_writeback_attempt_at": {
189
+ "bsonType": "date"
190
+ },
191
+ "marketplace_writeback_channel": {
192
+ "bsonType": "string"
193
+ },
194
+ "marketplace_writeback_error": {
195
+ "bsonType": [
196
+ "string",
197
+ "null"
198
+ ]
199
+ },
200
+ "marketplace_writeback_confirmed_at": {
201
+ "bsonType": "date"
202
+ },
203
+ "marketplace_writeback_carrier": {
204
+ "bsonType": "string"
205
+ },
206
+ "marketplace_writeback_tracking_number": {
207
+ "bsonType": "string"
208
+ },
209
+ "marketplace_writeback_failed_at": {
210
+ "bsonType": "date"
211
+ },
212
+ "marketplace_writeback_http_status": {
213
+ "bsonType": [
214
+ "int",
215
+ "double",
216
+ "null"
217
+ ]
218
+ },
219
+ "marketplace_writeback_last_error_at": {
220
+ "bsonType": "date"
221
+ }
222
+ }
223
+ }
224
+ }
@@ -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
+