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/lib/mongodb-ops.js +406 -381
- package/package.json +32 -28
- package/readme.md +130 -65
- package/test/fixtures/README.md +7 -0
- package/test/fixtures/vendor-orders-validator.json +224 -0
- package/test/mongodb-ops.test.js +160 -0
- package/test/mongodb-write-errors.test.js +89 -0
package/package.json
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mongodb-ops",
|
|
3
|
-
"version": "0.11.
|
|
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.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
|
-
#
|
|
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,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
|
+
|