@toa.io/storages.mongodb 1.0.0-alpha.331 → 1.0.0-alpha.332
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/CHANGELOG.md +7 -0
- package/package.json +7 -7
- package/readme.md +5 -0
- package/src/outbox.js +7 -1
- package/src/storage.js +31 -19
- package/test/storage.test.js +43 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [1.0.0-alpha.332](https://github.com/toa-io/toa/compare/v1.0.0-alpha.331...v1.0.0-alpha.332) (2026-10-23)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **storages.mongodb:** commit a set where the URL sets timeoutMS ([0b51f02](https://github.com/toa-io/toa/commit/0b51f0281de218dbcb677b75d6dd58685c4e2958))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# [1.0.0-alpha.331](https://github.com/toa-io/toa/compare/v1.0.0-alpha.330...v1.0.0-alpha.331) (2026-10-16)
|
|
7
14
|
|
|
8
15
|
**Note:** Version bump only for package @toa.io/storages.mongodb
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/storages.mongodb",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.332",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa MongoDB Storage Connector",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@toa.io/conveyor": "1.0.0-alpha.
|
|
24
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
25
|
-
"@toa.io/definitions": "1.0.0-alpha.
|
|
26
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
27
|
-
"@toa.io/pointer": "1.0.0-alpha.
|
|
23
|
+
"@toa.io/conveyor": "1.0.0-alpha.332",
|
|
24
|
+
"@toa.io/core": "1.0.0-alpha.332",
|
|
25
|
+
"@toa.io/definitions": "1.0.0-alpha.332",
|
|
26
|
+
"@toa.io/generic": "1.0.0-alpha.332",
|
|
27
|
+
"@toa.io/pointer": "1.0.0-alpha.332",
|
|
28
28
|
"mongodb": "7.7.0",
|
|
29
29
|
"openspan": "1.0.0-alpha.330"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "37dbdefd96c01df050ea0d46a647da7f2f9f93f2"
|
|
32
32
|
}
|
package/readme.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# MongoDB Storage
|
|
2
2
|
|
|
3
|
+
## Server
|
|
4
|
+
|
|
5
|
+
MongoDB 8.0 or later. An earlier server refuses a `transition` of scope `entries`
|
|
6
|
+
([Operations](/documentation/component/declaration.md#operations)).
|
|
7
|
+
|
|
3
8
|
## Tracing
|
|
4
9
|
|
|
5
10
|
Commands are recorded as `client` spans within the trace of the current invocation.
|
package/src/outbox.js
CHANGED
|
@@ -27,7 +27,13 @@ export class Outbox {
|
|
|
27
27
|
async insertMany(rows, session) {
|
|
28
28
|
if (rows.length === 0) return
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const namespace = this.#collection.namespace
|
|
31
|
+
|
|
32
|
+
// the client's bulk write, not `insertMany`: see `massStore` in ./storage.js
|
|
33
|
+
await this.#collection.client.bulkWrite(
|
|
34
|
+
rows.map((row) => ({ name: 'insertOne', namespace, document: to(row) })),
|
|
35
|
+
{ session }
|
|
36
|
+
)
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
/**
|
package/src/storage.js
CHANGED
|
@@ -287,33 +287,35 @@ export class Storage extends Connector {
|
|
|
287
287
|
async massStore(entities, rows = undefined, attempt = 0) {
|
|
288
288
|
if (entities.length === 0) return true
|
|
289
289
|
|
|
290
|
+
const namespace = this.#collection.namespace
|
|
291
|
+
|
|
290
292
|
const operations = entities.map((entity) => {
|
|
291
293
|
const record = this.#to(entity)
|
|
292
294
|
|
|
293
295
|
if (entity.VERSION === 1) {
|
|
294
296
|
const { VERSION, ...rest } = record
|
|
295
297
|
|
|
298
|
+
// one the read did not find: absent, and inserted, or deleted, and revived — a live
|
|
299
|
+
// one makes the upsert collide on `_id`, which is the swap lost
|
|
296
300
|
return {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
DELETED: null
|
|
305
|
-
},
|
|
306
|
-
$inc: { VERSION: 1 }
|
|
301
|
+
name: 'updateOne',
|
|
302
|
+
namespace,
|
|
303
|
+
filter: { _id: entity.id, DELETED: { $ne: null } },
|
|
304
|
+
update: {
|
|
305
|
+
$set: {
|
|
306
|
+
...rest,
|
|
307
|
+
DELETED: null
|
|
307
308
|
},
|
|
308
|
-
|
|
309
|
-
}
|
|
309
|
+
$inc: { VERSION: 1 }
|
|
310
|
+
},
|
|
311
|
+
upsert: true
|
|
310
312
|
}
|
|
311
313
|
} else
|
|
312
314
|
return {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
name: 'replaceOne',
|
|
316
|
+
namespace,
|
|
317
|
+
filter: { _id: entity.id, VERSION: entity.VERSION - 1 },
|
|
318
|
+
replacement: record
|
|
317
319
|
}
|
|
318
320
|
})
|
|
319
321
|
|
|
@@ -327,10 +329,13 @@ export class Storage extends Connector {
|
|
|
327
329
|
result = await this.command(
|
|
328
330
|
'bulkWrite',
|
|
329
331
|
{ operations: operations.length },
|
|
330
|
-
|
|
332
|
+
// the client's bulk write, not the collection's: the latter takes a `timeoutMS` the
|
|
333
|
+
// URL sets for one the call passed, and refuses it inside a transaction that has
|
|
334
|
+
// its own
|
|
335
|
+
async () => await this.#collection.client.bulkWrite(operations, { session })
|
|
331
336
|
)
|
|
332
337
|
} catch (error) {
|
|
333
|
-
if (!collidedOnId(error)) throw error
|
|
338
|
+
if (!collidedOnId(firstWriteError(error))) throw error
|
|
334
339
|
|
|
335
340
|
await session.abortTransaction()
|
|
336
341
|
|
|
@@ -353,7 +358,7 @@ export class Storage extends Connector {
|
|
|
353
358
|
} catch (error) {
|
|
354
359
|
console.error('MongoDB error', error)
|
|
355
360
|
|
|
356
|
-
const retry = await retriable(error, attempt)
|
|
361
|
+
const retry = await retriable(firstWriteError(error), attempt)
|
|
357
362
|
|
|
358
363
|
if (retry) return await this.massStore(entities, rows, attempt + 1)
|
|
359
364
|
else return false
|
|
@@ -608,6 +613,13 @@ const ERR_DUPLICATE_KEY = 11000
|
|
|
608
613
|
/** what the transaction answers where the call it carries has already been made */
|
|
609
614
|
const MADE = Symbol('made')
|
|
610
615
|
|
|
616
|
+
/** What an ordered bulk write stopped at, or the error itself where no write failed. */
|
|
617
|
+
function firstWriteError(error) {
|
|
618
|
+
return error?.writeErrors instanceof Map && error.writeErrors.size > 0
|
|
619
|
+
? error.writeErrors.values().next().value
|
|
620
|
+
: error
|
|
621
|
+
}
|
|
622
|
+
|
|
611
623
|
/** whether the key a write collided on is `_id` */
|
|
612
624
|
function collidedOnId(error) {
|
|
613
625
|
if (error?.code !== ERR_DUPLICATE_KEY) return false
|
package/test/storage.test.js
CHANGED
|
@@ -12,6 +12,8 @@ let storage
|
|
|
12
12
|
beforeEach(async () => {
|
|
13
13
|
collection = {
|
|
14
14
|
collectionName: 'test',
|
|
15
|
+
namespace: 'db.test',
|
|
16
|
+
client: {},
|
|
15
17
|
findOne: mock.fn(async () => null),
|
|
16
18
|
find: mock.fn(() => cursor()),
|
|
17
19
|
updateMany: mock.fn(async () => ({ modifiedCount: 0 })),
|
|
@@ -270,10 +272,10 @@ describe('massStore', () => {
|
|
|
270
272
|
const stored = { id: 'a1', VERSION: 3, foo: 1 }
|
|
271
273
|
const created = { id: 'b2', VERSION: 1, foo: 1 }
|
|
272
274
|
|
|
273
|
-
const operations = () => collection.bulkWrite.mock.calls[0].arguments[0]
|
|
275
|
+
const operations = () => collection.client.bulkWrite.mock.calls[0].arguments[0]
|
|
274
276
|
|
|
275
277
|
beforeEach(() => {
|
|
276
|
-
collection.bulkWrite = mock.fn(async (operations) => ({
|
|
278
|
+
collection.client.bulkWrite = mock.fn(async (operations) => ({
|
|
277
279
|
matchedCount: operations.length,
|
|
278
280
|
upsertedCount: 0
|
|
279
281
|
}))
|
|
@@ -282,14 +284,19 @@ describe('massStore', () => {
|
|
|
282
284
|
it('should replace a record only at the version it was read at', async () => {
|
|
283
285
|
await storage.massStore([stored])
|
|
284
286
|
|
|
285
|
-
|
|
287
|
+
const { name, namespace, filter } = operations()[0]
|
|
288
|
+
|
|
289
|
+
assert.equal(name, 'replaceOne')
|
|
290
|
+
assert.equal(namespace, 'db.test')
|
|
291
|
+
assert.deepEqual(filter, { _id: 'a1', VERSION: 2 })
|
|
286
292
|
})
|
|
287
293
|
|
|
288
294
|
it('should create a record only where none but a deleted one holds its id', async () => {
|
|
289
295
|
await storage.massStore([created])
|
|
290
296
|
|
|
291
|
-
const { filter, upsert } = operations()[0]
|
|
297
|
+
const { name, filter, upsert } = operations()[0]
|
|
292
298
|
|
|
299
|
+
assert.equal(name, 'updateOne')
|
|
293
300
|
assert.deepEqual(filter, { _id: 'b2', DELETED: { $ne: null } })
|
|
294
301
|
assert.equal(upsert, true)
|
|
295
302
|
})
|
|
@@ -300,20 +307,48 @@ describe('massStore', () => {
|
|
|
300
307
|
})
|
|
301
308
|
|
|
302
309
|
it('should abort and answer false where a record was not matched', async () => {
|
|
303
|
-
collection.bulkWrite = mock.fn(async () => ({
|
|
310
|
+
collection.client.bulkWrite = mock.fn(async () => ({
|
|
311
|
+
matchedCount: 1,
|
|
312
|
+
upsertedCount: 0
|
|
313
|
+
}))
|
|
304
314
|
|
|
305
315
|
assert.equal(await storage.massStore([stored, { ...stored, id: 'c3' }]), false)
|
|
306
316
|
assert.equal(session.abortTransaction.mock.callCount(), 1)
|
|
307
317
|
})
|
|
308
318
|
|
|
309
319
|
it('should abort and answer false where a record being created exists', async () => {
|
|
310
|
-
collection.bulkWrite = mock.fn(async () => {
|
|
311
|
-
|
|
312
|
-
|
|
320
|
+
collection.client.bulkWrite = mock.fn(async () => {
|
|
321
|
+
const collision = Object.assign(
|
|
322
|
+
new Error('E11000 duplicate key error index: _id_ dup key'),
|
|
323
|
+
{
|
|
324
|
+
code: 11000
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
throw Object.assign(new Error('ordered bulk write encountered a write error'), {
|
|
329
|
+
writeErrors: new Map([[0, collision]])
|
|
313
330
|
})
|
|
314
331
|
})
|
|
315
332
|
|
|
316
333
|
assert.equal(await storage.massStore([created]), false)
|
|
317
334
|
assert.equal(session.abortTransaction.mock.callCount(), 1)
|
|
318
335
|
})
|
|
336
|
+
|
|
337
|
+
it('should throw a duplicate where a record collides on a key other than its id', async () => {
|
|
338
|
+
collection.client.bulkWrite = mock.fn(async () => {
|
|
339
|
+
const collision = Object.assign(
|
|
340
|
+
new Error('E11000 duplicate key error index: foo_1'),
|
|
341
|
+
{
|
|
342
|
+
code: 11000,
|
|
343
|
+
keyPattern: { foo: 1 }
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
throw Object.assign(new Error('ordered bulk write encountered a write error'), {
|
|
348
|
+
writeErrors: new Map([[0, collision]])
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
await assert.rejects(storage.massStore([created]), { code: 306 })
|
|
353
|
+
})
|
|
319
354
|
})
|