@toa.io/storages.mongodb 1.0.0-alpha.326 → 1.0.0-alpha.328
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 +15 -0
- package/package.json +6 -6
- package/src/match.js +10 -5
- package/src/record.js +2 -1
- package/src/storage.js +5 -3
- package/src/streams.js +47 -13
- package/test/match.test.js +4 -1
- package/test/record.test.js +21 -5
- package/test/storage.test.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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.328](https://github.com/toa-io/toa/compare/v1.0.0-alpha.327...v1.0.0-alpha.328) (2026-10-10)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.0.0-alpha.327](https://github.com/toa-io/toa/compare/v1.0.0-alpha.326...v1.0.0-alpha.327) (2026-10-10)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **storages.mongodb:** pass a deleted record by when an effect gets or creates ([0cea640](https://github.com/toa-io/toa/commit/0cea6405021c8230e9b68a0f3086ff610332b34d))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# [1.0.0-alpha.326](https://github.com/toa-io/toa/compare/v1.0.0-alpha.325...v1.0.0-alpha.326) (2026-10-06)
|
|
7
22
|
|
|
8
23
|
### Features
|
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.328",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Toa MongoDB Storage Connector",
|
|
6
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@toa.io/conveyor": "1.0.0-alpha.311",
|
|
24
|
-
"@toa.io/core": "1.0.0-alpha.
|
|
25
|
-
"@toa.io/definitions": "1.0.0-alpha.
|
|
24
|
+
"@toa.io/core": "1.0.0-alpha.328",
|
|
25
|
+
"@toa.io/definitions": "1.0.0-alpha.328",
|
|
26
26
|
"@toa.io/generic": "1.0.0-alpha.311",
|
|
27
|
-
"@toa.io/pointer": "1.0.0-alpha.
|
|
27
|
+
"@toa.io/pointer": "1.0.0-alpha.327",
|
|
28
28
|
"mongodb": "7.7.0",
|
|
29
|
-
"openspan": "1.0.0-alpha.
|
|
29
|
+
"openspan": "1.0.0-alpha.327"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "c4fcb94548759b069014187ef4fa238179d2eca3"
|
|
32
32
|
}
|
package/src/match.js
CHANGED
|
@@ -27,7 +27,8 @@ function matches(record, key, condition) {
|
|
|
27
27
|
case '$nor':
|
|
28
28
|
return !condition.some((filter) => match(record, filter))
|
|
29
29
|
default:
|
|
30
|
-
if (key.startsWith('$'))
|
|
30
|
+
if (key.startsWith('$'))
|
|
31
|
+
throw new Error(`A change cannot be matched against '${key}'`)
|
|
31
32
|
|
|
32
33
|
return field(read(record, key), condition)
|
|
33
34
|
}
|
|
@@ -39,7 +40,8 @@ function field(value, condition) {
|
|
|
39
40
|
for (const [operator, operand] of Object.entries(condition)) {
|
|
40
41
|
const test = OPERATORS[operator]
|
|
41
42
|
|
|
42
|
-
if (test === undefined)
|
|
43
|
+
if (test === undefined)
|
|
44
|
+
throw new Error(`A change cannot be matched against '${operator}'`)
|
|
43
45
|
if (!test(value, operand)) return false
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -65,14 +67,16 @@ function some(value, test) {
|
|
|
65
67
|
/** `null` is equal to a field that is not there, and a date to a date of the same instant. */
|
|
66
68
|
function equal(value, operand) {
|
|
67
69
|
if (operand === null) return value === null || value === undefined
|
|
68
|
-
if (operand instanceof Date)
|
|
70
|
+
if (operand instanceof Date)
|
|
71
|
+
return value instanceof Date && value.getTime() === operand.getTime()
|
|
69
72
|
|
|
70
73
|
return value === operand
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
/** Values of one type compare, and values of two types are neither greater nor less. */
|
|
74
77
|
function compare(value, operand) {
|
|
75
|
-
if (value instanceof Date && operand instanceof Date)
|
|
78
|
+
if (value instanceof Date && operand instanceof Date)
|
|
79
|
+
return value.getTime() - operand.getTime()
|
|
76
80
|
if (typeof value === 'number' && typeof operand === 'number') return value - operand
|
|
77
81
|
|
|
78
82
|
if (typeof value === 'string' && typeof operand === 'string')
|
|
@@ -82,7 +86,8 @@ function compare(value, operand) {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
function operators(condition) {
|
|
85
|
-
if (condition === null || typeof condition !== 'object' || condition instanceof Date)
|
|
89
|
+
if (condition === null || typeof condition !== 'object' || condition instanceof Date)
|
|
90
|
+
return false
|
|
86
91
|
if (Array.isArray(condition)) return false
|
|
87
92
|
|
|
88
93
|
const keys = Object.keys(condition)
|
package/src/record.js
CHANGED
|
@@ -44,7 +44,8 @@ export function codec(properties) {
|
|
|
44
44
|
for (const [name, schema] of Object.entries(properties ?? {})) {
|
|
45
45
|
const cast = READ[schema?.format]
|
|
46
46
|
|
|
47
|
-
if (cast !== undefined && schema.type === TYPES[schema.format])
|
|
47
|
+
if (cast !== undefined && schema.type === TYPES[schema.format])
|
|
48
|
+
read.push([name, cast])
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const dates = read.map(([name]) => name)
|
package/src/storage.js
CHANGED
|
@@ -492,7 +492,10 @@ export class Storage extends Connector {
|
|
|
492
492
|
async ensure(query, properties, state, row = undefined) {
|
|
493
493
|
let { criteria, options } = translate(query, this.#dates)
|
|
494
494
|
|
|
495
|
-
if (query === undefined) criteria = properties
|
|
495
|
+
if (query === undefined) criteria = { ...properties }
|
|
496
|
+
|
|
497
|
+
// a deleted record is not one to get: the upsert passes it by and creates
|
|
498
|
+
if (query?.options?.deleted !== true) criteria.DELETED = null
|
|
496
499
|
|
|
497
500
|
const update = { $setOnInsert: this.#to(state) }
|
|
498
501
|
|
|
@@ -523,8 +526,7 @@ export class Storage extends Connector {
|
|
|
523
526
|
return found
|
|
524
527
|
})
|
|
525
528
|
|
|
526
|
-
|
|
527
|
-
else return this.#from(result)
|
|
529
|
+
return this.#from(result)
|
|
528
530
|
} catch (error) {
|
|
529
531
|
if (error.code === ERR_DUPLICATE_KEY)
|
|
530
532
|
throw new exceptions.DuplicateException(this.#client.name)
|
package/src/streams.js
CHANGED
|
@@ -52,7 +52,9 @@ export class Streams {
|
|
|
52
52
|
const token = decode(given)
|
|
53
53
|
|
|
54
54
|
if (token.h !== hash)
|
|
55
|
-
throw new exceptions.QuerySyntaxException(
|
|
55
|
+
throw new exceptions.QuerySyntaxException(
|
|
56
|
+
'The token was issued for other criteria or order'
|
|
57
|
+
)
|
|
56
58
|
|
|
57
59
|
if (token.id !== undefined) return await this.#window(criteria, options, hash, token)
|
|
58
60
|
if (token.p === null || !this.#history) throw lost()
|
|
@@ -84,7 +86,10 @@ export class Streams {
|
|
|
84
86
|
const position = token.p === null ? null : { p: token.p, t: token.t }
|
|
85
87
|
|
|
86
88
|
try {
|
|
87
|
-
return await this.#read(criteria, options, hash, session, position, {
|
|
89
|
+
return await this.#read(criteria, options, hash, session, position, {
|
|
90
|
+
id: token.id,
|
|
91
|
+
c: token.c
|
|
92
|
+
})
|
|
88
93
|
} catch (exception) {
|
|
89
94
|
await session.endSession()
|
|
90
95
|
|
|
@@ -124,8 +129,10 @@ export class Streams {
|
|
|
124
129
|
*/
|
|
125
130
|
async #read(criteria, options, hash, session, position, after) {
|
|
126
131
|
const limit = options.limit
|
|
127
|
-
const order =
|
|
128
|
-
|
|
132
|
+
const order =
|
|
133
|
+
limit === undefined && after === undefined ? null : ordered(options.sort)
|
|
134
|
+
const filter =
|
|
135
|
+
after === undefined ? criteria : { $and: [criteria, beyond(order, after)] }
|
|
129
136
|
|
|
130
137
|
const read = { ...options, session, readConcern: { level: 'majority' } }
|
|
131
138
|
|
|
@@ -147,18 +154,30 @@ export class Streams {
|
|
|
147
154
|
try {
|
|
148
155
|
for await (const record of cursor) {
|
|
149
156
|
count++
|
|
150
|
-
last = {
|
|
157
|
+
last = {
|
|
158
|
+
id: record._id,
|
|
159
|
+
c: record.CREATED instanceof Date ? record.CREATED.getTime() : undefined
|
|
160
|
+
}
|
|
151
161
|
|
|
152
162
|
yield parts.entry(from(record))
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
if (limit !== undefined && count === limit && options.stop !== true) {
|
|
156
|
-
const window = {
|
|
166
|
+
const window = {
|
|
167
|
+
v: VERSION,
|
|
168
|
+
p: position?.p ?? null,
|
|
169
|
+
t: position?.t ?? null,
|
|
170
|
+
id: last.id,
|
|
171
|
+
h: hash
|
|
172
|
+
}
|
|
157
173
|
|
|
158
174
|
if (order?.[0]?.[0] === 'CREATED') window.c = last.c
|
|
159
175
|
|
|
160
176
|
yield parts.token(encode(window))
|
|
161
|
-
} else
|
|
177
|
+
} else
|
|
178
|
+
yield parts.token(
|
|
179
|
+
position === null ? null : encode({ v: VERSION, p: position.p, h: hash })
|
|
180
|
+
)
|
|
162
181
|
} finally {
|
|
163
182
|
await cursor.close()
|
|
164
183
|
await session.endSession()
|
|
@@ -243,7 +262,8 @@ function classify(event, criteria, from) {
|
|
|
243
262
|
const after = event.fullDocument
|
|
244
263
|
const before = event.fullDocumentBeforeChange
|
|
245
264
|
|
|
246
|
-
if (after !== undefined && after !== null && match(after, criteria))
|
|
265
|
+
if (after !== undefined && after !== null && match(after, criteria))
|
|
266
|
+
return parts.entry(from(after))
|
|
247
267
|
|
|
248
268
|
if (before !== undefined && before !== null && match(before, criteria))
|
|
249
269
|
return parts.removed(String(event.documentKey._id))
|
|
@@ -261,21 +281,28 @@ function prefix(filter, path) {
|
|
|
261
281
|
for (const [key, value] of Object.entries(filter))
|
|
262
282
|
if (LOGICAL.includes(key)) result[key] = value.map((filter) => prefix(filter, path))
|
|
263
283
|
else if (key.startsWith('$'))
|
|
264
|
-
throw new exceptions.QuerySyntaxException(
|
|
284
|
+
throw new exceptions.QuerySyntaxException(
|
|
285
|
+
`What changed in a collection cannot be read by '${key}'`
|
|
286
|
+
)
|
|
265
287
|
else result[path + key] = value
|
|
266
288
|
|
|
267
289
|
return result
|
|
268
290
|
}
|
|
269
291
|
|
|
270
292
|
function refusal(exception) {
|
|
271
|
-
if (
|
|
293
|
+
if (
|
|
294
|
+
LOST.includes(exception?.code) ||
|
|
295
|
+
exception?.hasErrorLabel?.('NonResumableChangeStreamError')
|
|
296
|
+
)
|
|
272
297
|
return lost()
|
|
273
298
|
|
|
274
299
|
return exception
|
|
275
300
|
}
|
|
276
301
|
|
|
277
302
|
function lost() {
|
|
278
|
-
return new exceptions.StateHistoryException(
|
|
303
|
+
return new exceptions.StateHistoryException(
|
|
304
|
+
'The token names a point the storage no longer holds'
|
|
305
|
+
)
|
|
279
306
|
}
|
|
280
307
|
|
|
281
308
|
/**
|
|
@@ -300,11 +327,18 @@ function ordered(sort) {
|
|
|
300
327
|
|
|
301
328
|
for (const [property] of sort)
|
|
302
329
|
if (!UNMOVING.includes(property))
|
|
303
|
-
throw new exceptions.QuerySyntaxException(
|
|
330
|
+
throw new exceptions.QuerySyntaxException(
|
|
331
|
+
`A stream read in windows is not ordered by '${property}', which changes`
|
|
332
|
+
)
|
|
304
333
|
|
|
305
334
|
const [[first, direction]] = sort
|
|
306
335
|
|
|
307
|
-
return first === '_id'
|
|
336
|
+
return first === '_id'
|
|
337
|
+
? [['_id', direction]]
|
|
338
|
+
: [
|
|
339
|
+
['CREATED', direction],
|
|
340
|
+
['_id', direction]
|
|
341
|
+
]
|
|
308
342
|
}
|
|
309
343
|
|
|
310
344
|
/** What comes after the last entry a window read, in its order. */
|
package/test/match.test.js
CHANGED
|
@@ -55,7 +55,10 @@ describe('match', () => {
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
it('should combine filters', () => {
|
|
58
|
-
assert.equal(
|
|
58
|
+
assert.equal(
|
|
59
|
+
match(record, { $and: [{ owner: 'alice' }, { rank: { $gt: 1 } }] }),
|
|
60
|
+
true
|
|
61
|
+
)
|
|
59
62
|
assert.equal(match(record, { $or: [{ owner: 'bob' }, { rank: { $gt: 1 } }] }), true)
|
|
60
63
|
assert.equal(match(record, { $or: [{ owner: 'bob' }, { rank: { $gt: 9 } }] }), false)
|
|
61
64
|
assert.equal(match(record, { owner: 'alice', DELETED: null, _id: 'a1' }), true)
|
package/test/record.test.js
CHANGED
|
@@ -63,7 +63,11 @@ describe('codec', () => {
|
|
|
63
63
|
|
|
64
64
|
it('should write a date-time property as a date', () => {
|
|
65
65
|
const { to: write } = codec(properties)
|
|
66
|
-
const record = write({
|
|
66
|
+
const record = write({
|
|
67
|
+
id: '1',
|
|
68
|
+
settled: '2026-09-05T10:00:00.000Z',
|
|
69
|
+
endpoint: 'a.b.c'
|
|
70
|
+
})
|
|
67
71
|
|
|
68
72
|
assert.ok(record.settled instanceof Date)
|
|
69
73
|
assert.strictEqual(record.settled.toISOString(), '2026-09-05T10:00:00.000Z')
|
|
@@ -100,8 +104,14 @@ describe('codec', () => {
|
|
|
100
104
|
})
|
|
101
105
|
|
|
102
106
|
it('should ignore a format said of the wrong type', () => {
|
|
103
|
-
assert.deepStrictEqual(
|
|
104
|
-
|
|
107
|
+
assert.deepStrictEqual(
|
|
108
|
+
codec({ at: { type: 'string', format: 'epoch-millis' } }).dates,
|
|
109
|
+
[]
|
|
110
|
+
)
|
|
111
|
+
assert.deepStrictEqual(
|
|
112
|
+
codec({ at: { type: 'integer', format: 'date-time' } }).dates,
|
|
113
|
+
[]
|
|
114
|
+
)
|
|
105
115
|
})
|
|
106
116
|
})
|
|
107
117
|
|
|
@@ -133,7 +143,10 @@ describe('codec, epoch-millis', () => {
|
|
|
133
143
|
})
|
|
134
144
|
|
|
135
145
|
it('should read a record written before the property was a date', () => {
|
|
136
|
-
assert.strictEqual(
|
|
146
|
+
assert.strictEqual(
|
|
147
|
+
codec(properties).from({ _id: '1', DELETED: millis }).DELETED,
|
|
148
|
+
millis
|
|
149
|
+
)
|
|
137
150
|
})
|
|
138
151
|
|
|
139
152
|
it('should write the moments of a changeset as dates', () => {
|
|
@@ -156,6 +169,9 @@ describe('codec, epoch-millis', () => {
|
|
|
156
169
|
it('should leave a changeset of an entity that declares no moment as it is', () => {
|
|
157
170
|
const changeset = { VERSION: 2 }
|
|
158
171
|
|
|
159
|
-
assert.strictEqual(
|
|
172
|
+
assert.strictEqual(
|
|
173
|
+
codec({ VERSION: { type: 'integer' } }).changes(changeset),
|
|
174
|
+
changeset
|
|
175
|
+
)
|
|
160
176
|
})
|
|
161
177
|
})
|
package/test/storage.test.js
CHANGED
|
@@ -151,7 +151,10 @@ describe('stream', () => {
|
|
|
151
151
|
it('should read a window newest first, and by id where entries share a time', async () => {
|
|
152
152
|
await storage.stream({ options: { limit: 2, sort: [['CREATED', 'desc']] } })
|
|
153
153
|
|
|
154
|
-
assert.deepStrictEqual(read()[1].sort, [
|
|
154
|
+
assert.deepStrictEqual(read()[1].sort, [
|
|
155
|
+
['CREATED', -1],
|
|
156
|
+
['_id', -1]
|
|
157
|
+
])
|
|
155
158
|
})
|
|
156
159
|
|
|
157
160
|
it('should refuse a window ordered by what an entry changes', async () => {
|