@toa.io/storages.mongodb 1.0.0-alpha.325 → 1.0.0-alpha.327

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 CHANGED
@@ -3,6 +3,20 @@
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.327](https://github.com/toa-io/toa/compare/v1.0.0-alpha.326...v1.0.0-alpha.327) (2026-10-10)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **storages.mongodb:** pass a deleted record by when an effect gets or creates ([0cea640](https://github.com/toa-io/toa/commit/0cea6405021c8230e9b68a0f3086ff610332b34d))
11
+
12
+
13
+ # [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)
14
+
15
+ ### Features
16
+
17
+ * **core:** parse criteria with [@toa](https://github.com/toa).io/rsql ([c4dc9f4](https://github.com/toa-io/toa/commit/c4dc9f41ca2719ac66607c5b14e9ead678baa740))
18
+
19
+
6
20
  # [1.0.0-alpha.325](https://github.com/toa-io/toa/compare/v1.0.0-alpha.324...v1.0.0-alpha.325) (2026-10-02)
7
21
 
8
22
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/storages.mongodb",
3
- "version": "1.0.0-alpha.325",
3
+ "version": "1.0.0-alpha.327",
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.323",
25
- "@toa.io/definitions": "1.0.0-alpha.325",
24
+ "@toa.io/core": "1.0.0-alpha.327",
25
+ "@toa.io/definitions": "1.0.0-alpha.327",
26
26
  "@toa.io/generic": "1.0.0-alpha.311",
27
- "@toa.io/pointer": "1.0.0-alpha.311",
28
- "mongodb": "7.6.0",
29
- "openspan": "1.0.0-alpha.323"
27
+ "@toa.io/pointer": "1.0.0-alpha.327",
28
+ "mongodb": "7.7.0",
29
+ "openspan": "1.0.0-alpha.327"
30
30
  },
31
- "gitHead": "95bc03e65c65f02d536da6ed027cf4f78f5cec74"
31
+ "gitHead": "cd6d6d77f19fbae5d7ff85d18005ca8deb9c77f3"
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('$')) throw new Error(`A change cannot be matched against '${key}'`)
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) throw new Error(`A change cannot be matched against '${operator}'`)
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) return value instanceof Date && value.getTime() === operand.getTime()
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) return value.getTime() - operand.getTime()
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) return false
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]) read.push([name, cast])
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
- if (result.DELETED !== undefined && result.DELETED !== null) return null
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('The token was issued for other criteria or order')
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, { id: token.id, c: token.c })
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 = limit === undefined && after === undefined ? null : ordered(options.sort)
128
- const filter = after === undefined ? criteria : { $and: [criteria, beyond(order, after)] }
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 = { id: record._id, c: record.CREATED instanceof Date ? record.CREATED.getTime() : undefined }
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 = { v: VERSION, p: position?.p ?? null, t: position?.t ?? null, id: last.id, h: hash }
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 yield parts.token(position === null ? null : encode({ v: VERSION, p: position.p, h: hash }))
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)) return parts.entry(from(after))
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(`What changed in a collection cannot be read by '${key}'`)
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 (LOST.includes(exception?.code) || exception?.hasErrorLabel?.('NonResumableChangeStreamError'))
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('The token names a point the storage no longer holds')
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(`A stream read in windows is not ordered by '${property}', which changes`)
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' ? [['_id', direction]] : [['CREATED', direction], ['_id', direction]]
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. */
@@ -14,9 +14,7 @@ export const criteria = (node, dates = NONE) => {
14
14
 
15
15
  const OPERATORS = {
16
16
  LOGIC: {
17
- and: '$and',
18
17
  ';': '$and',
19
- or: '$or',
20
18
  ',': '$or'
21
19
  },
22
20
  COMPARISON: {
@@ -55,7 +55,10 @@ describe('match', () => {
55
55
  })
56
56
 
57
57
  it('should combine filters', () => {
58
- assert.equal(match(record, { $and: [{ owner: 'alice' }, { rank: { $gt: 1 } }] }), true)
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)
@@ -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({ id: '1', settled: '2026-09-05T10:00:00.000Z', endpoint: 'a.b.c' })
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(codec({ at: { type: 'string', format: 'epoch-millis' } }).dates, [])
104
- assert.deepStrictEqual(codec({ at: { type: 'integer', format: 'date-time' } }).dates, [])
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(codec(properties).from({ _id: '1', DELETED: millis }).DELETED, millis)
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(codec({ VERSION: { type: 'integer' } }).changes(changeset), changeset)
172
+ assert.strictEqual(
173
+ codec({ VERSION: { type: 'integer' } }).changes(changeset),
174
+ changeset
175
+ )
160
176
  })
161
177
  })
@@ -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, [['CREATED', -1], ['_id', -1]])
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 () => {