cry-db 2.6.1 → 2.6.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/CHANGELOG.md ADDED
@@ -0,0 +1,567 @@
1
+ # Changelog
2
+
3
+ ## 2.6.0 — 2026-07-09
4
+
5
+ ### Added
6
+
7
+ - `emitPublishBracketedEvents()` — third publish-event flavor alongside
8
+ `emitPublishEvents()` (full doc) and `emitPublishRevEvents()` (revision-only).
9
+ Emits `publishBracketed` events on `dbbracketed/{db}/{collection}` carrying
10
+ only the **client diff** (forwarded `rawUpdate`) instead of full parent
11
+ arrays/objects. Insert operations still carry the full document (there is
12
+ no diff). Useful for consumers that apply deltas locally rather than
13
+ replacing whole arrays.
14
+ - `MongoEmittedEvents` extended with `"publishBracketed"`.
15
+ - New types: `DbBracketedChannel`, `PublishBracketedPayloadBatch`,
16
+ `PublishBracketedPayloadBatchItem`, `PublishBracketedSpec`.
17
+
18
+ ### Fixed
19
+
20
+ - `_buildPublishDelta` dot-path handling: dot-path updates like
21
+ `cepljenja.xxx = { ... }` now correctly publish the **whole parent object**
22
+ (`cepljenja`) instead of a flat `cepljenja.xxx` key with `undefined` value.
23
+ Previously `_buildPublishDelta` treated dot-paths as literal top-level keys,
24
+ which do not exist in the post-update document → published `undefined`.
25
+
26
+ ### Internal
27
+
28
+ - Testi: `updateCollections.mjs` — 7 novih testov za publish behavior:
29
+ - dot-path object-map publish (prej failal)
30
+ - bracketed array publish (insert/update/remove)
31
+ - bracketed publish events (3 testi za `publishBracketed` event)
32
+
33
+ ## 2.5.6 — 2026-06-25
34
+
35
+ ### Added
36
+
37
+ - `findNewerManyStream` server-side scope-exit: po pozitivni delti pošlje še
38
+ iteme, ki so v tem sync oknu padli izven query-ja. Uporabi `_ts` filter + JS
39
+ evalvacijo query-ja (`matchesQuery` iz `cry-local-query`). Rezultat pride z
40
+ `_evictIds: true`, da klient ve, da mora te iteme odstraniti.
41
+ - `_extractQueryFields` — rekurzivno ekstrahira top-level field imena iz
42
+ MongoDB query-ja (vključno z `$and`/`$or`/`$nor`) za dinamično generiranje
43
+ projekcije v scope-exit passu.
44
+
45
+ ### Internal
46
+
47
+ - Dodana dependency `cry-local-query` (za `matchesQuery` v scope-exit).
48
+ - `tsconfig.json` exclude: `dist`, `test`, `node_modules`.
49
+ - Testi: `bracketUnsetPullConflict.mjs` cleanup — odstranjen `describe.skip`
50
+ bug-repro test, ohranjen samo fixed behaviour.
51
+ - AI test coverage: `deepseek-findNewerManyStream-scopeExit`,
52
+ `kimi-extractQueryFields`, `kimi-syncedCollections-scopeExit`.
53
+
54
+ ## 2.5.2 — 2026-06-18
55
+
56
+ ### Fixed
57
+
58
+ - `fix:` detektiran konflikt bare-field array replacement (`postavke: [...]`) +
59
+ bracket-by-_id sub-field path (`postavke[<id>].field: x`) na istem arrayu.
60
+ Prej je MongoDB tiho uporabil samo polno zamenjavo, sub-field se je izgubil.
61
+ Zdaj vrže error v `results.errors` z navodilom naj uporabi terminal-bracket
62
+ obliko. Zajeti tudi primeri, ko bracket sub-field vsebuje `undefined` (gre v
63
+ `$unset`) ali ko doda novo polje.
64
+
65
+ ### Internal
66
+
67
+ - Testi: `bracketFullArrayPlusSubField.mjs` — 8 testov za oba patterna
68
+ (error in pravilni) z različnimi tipi vrednosti in `undefined`.
69
+
70
+ ## 2.5.1 — 2026-06-18
71
+
72
+ ### Fixed
73
+
74
+ - `fix:` `$set + $pull` conflikt na istem parent array-u — `setTargetsField` check
75
+ detektira, ko bracket sub-field update (`arr[A].field: x`) in terminal-bracket
76
+ remove (`arr[B]: undefined`) targetirata isti array, in pade v pipeline form.
77
+ - `re:` dodani testi za produkcijsko reprodukcijo (odprtiRacuni — remove ene
78
+ postavke + sub-field update druge + top-level polja).
79
+ - `vp:` dodan TDD test `bracketPullSetConflict.mjs` — 6 testov, vključno z
80
+ raw MongoDB kontrolo, da `$pull + $set` na istem fieldu res vrže conflikt.
81
+
82
+ ### Internal
83
+
84
+ - Testi: `computeObjDiff.mjs` dodani testi za `odprtiRacuni` production
85
+ scenario (remove + modify na različnih arrayih).
86
+ - Testi: `bracketSubFieldViaUpdateCollections.mjs` dodani testi za sub-field
87
+ update + remove na istem arrayu + top-level fields.
88
+ - Doc: `CHANGES.md` preimenovan v `CHANGELOG.md`.
89
+
90
+ ## 2.5.2 — 2026-06-18
91
+
92
+ ### Added
93
+
94
+ - `fix:` detekcija konflikta bare-field array replacement (`postavke: [...]`) +
95
+ bracket-by-_id sub-field (`postavke[<id>].field`) v `_applyBracketProcessing`.
96
+ MongoDB tiho ignorira bracket sub-field ob polni zamenjavi (napačni podatki).
97
+ Zdaj vrže error v `results.errors`.
98
+ - `te:` `bracketFullArrayPlusSubField.mjs` — 7 testov za konflikt in pravilni
99
+ pattern (terminal-bracket insert + sub-field overlay) z različnimi tipi.
100
+
101
+ ## Unreleased
102
+
103
+ _Nothing yet._
104
+
105
+ ## 2.5.0
106
+
107
+ ### `computeObjDiff` / `applyObjDiff` — document diff utilities (NEW)
108
+
109
+ Two pure, mongo-independent helpers — the server-side counterpart of the
110
+ `cry-synced-db-client` diff machinery — exported from the package root:
111
+
112
+ - `computeObjDiff(existing, update)` — the minimal set of MongoDB-style
113
+ dot/bracket `$set` paths that transform `existing` into `update` (only for
114
+ keys present in `update`). Arrays of `_id`-keyed objects diff element-wise
115
+ via `arr[<id>].field`, with composition changes as `arr[<id>] = [el]` /
116
+ `arr[<id>] = undefined`. `_ts`/`_rev`/`_csq` are never emitted.
117
+ - `applyObjDiff(base, diff, fallbackId, collection)` — replays such a path-set
118
+ onto a base document the way a server-side `$set` + `$unset` would; returns a
119
+ fresh object (`base` is cloned, never mutated). Missing bracket-parent arrays
120
+ are materialized; unrepresentable multi-bracket paths are dropped (logged
121
+ unless the root field starts with `_`).
122
+
123
+ Also exported from the same module: `deepEquals`, `setByPath`, `deleteByPath`,
124
+ `tokenizePath`.
125
+
126
+ ```ts
127
+ import { computeObjDiff, applyObjDiff } from 'cry-db';
128
+
129
+ const diff = computeObjDiff(existing, update);
130
+ // → { "koraki[k1].diag": "new", "koraki[k2]": [{ _id: "k2", ... }] }
131
+ const merged = applyObjDiff(base, diff, fallbackId, collection);
132
+ ```
133
+
134
+ ### `updateCollections` — `mustRefresh` + required `_rev` (BREAKING)
135
+
136
+ Each `updates`/`deletes` entry now carries `_rev` — the revision the client's
137
+ change was computed against — and the result gains
138
+ `results.mustRefresh: (DbEntity & <full record>)[]`: full, sanitized records
139
+ the client MUST replace in its local store. A record lands in `mustRefresh`
140
+ when:
141
+
142
+ - cry-db **resolved a placeholder field** the client sent — `SEQ_*`
143
+ (auto-increment) or `__hashed__*` (hashing); or
144
+ - the stored `_rev` **advanced beyond the client's base** by more than this
145
+ write applied — another actor modified the record first (conflict); or
146
+ - the request **omitted `_rev`** (treated as `0` — the old-client fallback).
147
+
148
+ Contract:
149
+
150
+ - Every `mustRefresh._id` **also** appears in `inserted`/`updated`/`deleted`,
151
+ so dirty-clearing logic keyed off those arrays is unaffected — treat
152
+ `mustRefresh` as supplementary.
153
+ - Records are **full and sanitized**: `__hashed__*` values never leave the
154
+ server (the client adopting the record drops any local plaintext);
155
+ `removeFieldsAlways`/`findNewerRemoveFields` are applied as on reads.
156
+ - The client should **replace** its local record from the `mustRefresh` entry,
157
+ then run its dirty-merge — apply `mustRefresh` **before** the merge.
158
+ - Conflict detection requires `useRevisions()`. Field-resolution refresh
159
+ (SEQ/hash) works regardless.
160
+
161
+ **Why breaking**: the request shape now declares `_rev` required on
162
+ `updates`/`deletes` entries (omission is tolerated at runtime as the fallback,
163
+ but consumers should send it). Do **not** put `_rev` inside `update` — it is
164
+ stripped.
165
+
166
+ ## 2.4.49
167
+
168
+ ### Nested-bracket (multi-level) sub-field and terminal-bracket operations
169
+
170
+ Bracket-by-`_id` paths now support arbitrary nesting depth in BOTH the
171
+ arrayFilters fast path and the pipeline form. Paths like
172
+ `arr[A].sub[B].field`, `arr[A].sub[B]: [...]` (nested terminal insert),
173
+ and `arr[A].sub[B] = undefined` (nested terminal remove) all work, and
174
+ can be freely combined with top-level bracket operations in the same
175
+ update.
176
+
177
+ Evolution across releases:
178
+
179
+ - early bracket releases: combining a nested-bracket path with a
180
+ terminal-bracket insert threw a descriptive error.
181
+ - 2.4.45: the throw was softened to "drop + log warning" so production sync
182
+ payloads (e.g. `cry-synced-db-client`'s `_redundanca`) wouldn't fail the
183
+ whole batch.
184
+ - 2.4.49 (this entry): full support implemented. Drops + warnings of the
185
+ "nested-bracket dropped" kind no longer occur.
186
+
187
+ Implementation pieces (`src/mongo.mts`):
188
+
189
+ - New mutually-recursive types `NestedFieldOps` / `NestedElementOps`.
190
+ `ElementOps.nestedArrayOps?: Map<arrayField, FieldOps>` carries the
191
+ per-id sub-array transforms for the inner level.
192
+ - New `_buildArrayTransformExpr(basePath, FieldOps)` factors out the
193
+ filter+concat+map+switch composition. `_buildElementMergeExpr`
194
+ recursively calls it inside its overlay for each `nestedArrayOps`
195
+ entry. The top-level pipeline loop in `_applyBracketProcessing`
196
+ reuses the same helper.
197
+ - Removes-only nested transforms on a missing source field are wrapped
198
+ in `$cond: { $isArray: <path> }`, so deleting from a non-existent
199
+ inner array does NOT create a spurious empty array. Matches mongo's
200
+ native "$pull on missing field = no-op" semantic.
201
+
202
+ **Same-id overlap warning**: when a nested "new base" operation AND a
203
+ nested sub-field update target the same element (same `_id` at the
204
+ inner level), both apply (new-base value is set first, sub-field
205
+ overlay applies on top). The unusual overlap is surfaced as a
206
+ `{ kind: 'overlap' }` warning in `result.results.warnings`. Two flavors
207
+ trigger:
208
+
209
+ - terminal-array insert (`arr[id]: [{...}]`) + sub-field on same id;
210
+ - whole-element replace (`arr[id]: {...}`, object value) + sub-field on
211
+ same id.
212
+
213
+ ```ts
214
+ await items.updateOne({ _id: 'r1' }, {
215
+ 'terapije[t1].postavke[sp1].kolicina': 99,
216
+ "terapije[t1].postavke[sp1]": [{ _id: 'sp1', q: 5 }], // Flavor 1
217
+ // or, equivalently:
218
+ // "terapije[t1].postavke[sp1]": { _id: 'sp1', q: 5 }, // Flavor 2
219
+ });
220
+ // final sp1: { _id: 'sp1', q: 5, kolicina: 99 }
221
+ // warning: 'same-id overlap at "terapije[t1].postavke[sp1]" — …'
222
+ ```
223
+
224
+ Top-level same-id case (single-bracket insert/replace + sub-field on
225
+ the same outer `_id`) is the same semantic but stays silent — it's a
226
+ documented merge pattern.
227
+
228
+ 40+ regression tests in `test/bracketNestedSpec.mjs` and
229
+ `test/bracketNestedPermutations.mjs` cover the full matrix of
230
+ add/delete/modify operations at outer and inner levels.
231
+
232
+ ## 2.4.47
233
+
234
+ ### Fix: `$set` + `$pull` conflict for bracket sub-field + same-array remove
235
+
236
+ Combining a bracket sub-field update (`arr[id].field`, which compiles to
237
+ `$set` with `arrayFilters`) with a terminal-bracket remove on the **same**
238
+ parent array (`arr[otherId] = undefined`, which compiles to `$pull`) tripped
239
+ mongo's "conflict at <field>" guard. Conflict detection now recognizes this
240
+ case and routes the two operations so they no longer collide; `undefined`
241
+ values are logged for visibility. Regression test in
242
+ `test/bracketArrayPaths.mjs` / `updateCollections.mjs`.
243
+
244
+ ## 2.4.46
245
+
246
+ ### `MONGO_DEBUG_UPDATE_COLLECTIONS` — verbose `updateCollections` logging
247
+
248
+ New env var. When set, `updateCollections` logs each `findOneAndUpdate`
249
+ (query, translated update, options) and the final per-collection results at
250
+ `info` level instead of `debug`, for diagnosing sync-batch processing.
251
+
252
+ The `updateCollections` partial-failure semantics were also documented: per-record
253
+ failures land in `result.results.errors` and do not stop the rest of the
254
+ batch; only infrastructure failures (connection, sequence pre-processing,
255
+ audit/publish) propagate as a thrown exception.
256
+
257
+ ## 2.4.45
258
+
259
+ ### `CollectionUpdateResult.errors` is now an array (BREAKING)
260
+
261
+ `result.results.errors` was previously typed as a single `{ _id, error }`
262
+ object, but `updateCollections` internally collected an array and assigned
263
+ only `errors[0]` — silently dropping all but the first failure in a batch.
264
+ The type is now correct: `errors?: { _id: string, error: string }[]`. All
265
+ per-update / per-delete errors in the batch are surfaced.
266
+
267
+ Consumers that read `result.results.errors._id` / `.error` must switch to
268
+ `result.results.errors[0]._id` / `[0].error` (or iterate).
269
+
270
+ ### `CollectionUpdateResult.warnings` — new field
271
+
272
+ Companion to `errors`: `warnings?: { _id: string, error: string }[]` carries
273
+ non-fatal issues encountered while processing the batch. The `error` field
274
+ holds a human-readable message; the array entry's `_id` identifies the
275
+ affected document.
276
+
277
+ Initially emitted when a nested bracket-by-`_id` path had to be **dropped**
278
+ (rather than throwing and failing the whole batch). The **same-id overlap**
279
+ emitter — when a nested terminal-bracket insert and a nested sub-field update
280
+ target the same element — was added in 2.4.49 (see _Nested-bracket
281
+ (multi-level) sub-field and terminal-bracket operations_ above).
282
+
283
+ ### Pipeline `$switch case $eq` normalizes hex bracket-id to ObjectId (FIX)
284
+
285
+ When an update mixed a bracket-by-`_id` sub-field path
286
+ (`arr[id].field: val`) with a terminal-bracket insert/remove on a sibling
287
+ array — forcing the pipeline form — the per-id `$switch` branches
288
+ compared `$$el._id` against the raw bracket-id token (a hex string).
289
+ Stored `_id`s are usually `ObjectId`s, so the `$eq` branch never matched
290
+ and the sub-field merge silently no-op'd.
291
+
292
+ Now applies `_normalizeBracketId` to the bracket token (the same
293
+ conversion already used in `arrayFilters`, `$pull`, and `$filter` paths),
294
+ so 24-char hex strings become `ObjectId` for the comparison.
295
+
296
+ Production trigger (mozirje, 2026-05-10): updateCollections payloads
297
+ combining `postavke[<hex>].kolicina` (sub-field) with `davki[<hex>]:
298
+ [...]` (terminal insert on sibling) silently failed to apply the
299
+ `kolicina` change while the `davki` insert went through. Affected
300
+ documents stayed dirty and the dirty change couldn't drain.
301
+
302
+ Regression test in `test/bracketSubFieldViaUpdateCollections.mjs`.
303
+
304
+ ## 2.4.40
305
+
306
+ ### `fj` exported — recursive ObjectId formatter
307
+
308
+ `Mongo`'s internal `ObjectId` log-formatter is now exported from
309
+ `cry-db`. Given any value, it returns:
310
+
311
+ - `ObjectId("<hex>")` string for an `ObjectId` instance.
312
+ - An array / plain-object structure with all nested `ObjectId`s
313
+ recursively replaced by their formatted string.
314
+ - Anything else (primitives, `Date`, `Buffer`, BSON-types, …) returned
315
+ unchanged.
316
+
317
+ Useful when building log messages or `JSON.stringify` payloads that should
318
+ make `ObjectId` values visible as such rather than as bare hex strings.
319
+
320
+ ```ts
321
+ import { fj } from 'cry-db';
322
+ fj(new ObjectId('507f1f77bcf86cd799439011'));
323
+ // → 'ObjectId("507f1f77bcf86cd799439011")'
324
+ fj({ users: [{ _id: someId, name: 'x' }] });
325
+ // → { users: [{ _id: 'ObjectId("…")', name: 'x' }] }
326
+ ```
327
+
328
+ ## 2.4.36
329
+
330
+ ### `$literal` wrap for user values in pipeline form (FIX: dot-in-field-name keys)
331
+
332
+ User-supplied object/array values that flow into the pipeline form of
333
+ bracket-by-`_id` updates are now wrapped in `$literal` so mongo's
334
+ expression parser treats them as opaque data. Without the wrap, nested
335
+ field names containing `.` or `$` (legitimate as stored mongo doc keys)
336
+ crash the aggregation stage with:
337
+
338
+ > `FieldPath field names may not contain '.', given '<name>'.`
339
+
340
+ Production trigger (mozirje): pmz `karence` data carries `not. organi` /
341
+ `not. organi` field names (with a literal dot). Inserting a postavka
342
+ referencing such pmz data via terminal-bracket insert
343
+ (`postavke[<id>]: [<obj>]`), or updating an existing element's
344
+ sub-object value via `postavke[<id>].karence: <obj>` while another
345
+ insert was present in the same operation, threw the FieldPath error and
346
+ left the dirty change stuck forever.
347
+
348
+ **Two affected paths** in `_applyBracketProcessing`:
349
+
350
+ 1. **Insert path** — `$concatArrays: [filteredExisting, ops.insertElements]`
351
+ now wraps `ops.insertElements` in `$literal`.
352
+ 2. **Sub-field overlay path** — values stored in `node.directs[leaf]`
353
+ (which feed into `$mergeObjects`) are wrapped in `$literal` when they
354
+ are plain objects or arrays. New `_needsLiteralWrap` helper
355
+ distinguishes plain objects/arrays (need wrap) from primitives, `Date`,
356
+ and BSON types (`ObjectId`, `Timestamp`, `Decimal128`, `Binary`, …;
357
+ pass through).
358
+
359
+ Backward compatible — payloads without dot-in-field-name keys produce
360
+ identical mongo updates. `$literal` is a no-op for opaque BSON-encoded
361
+ data; no measurable performance cost.
362
+
363
+ ```ts
364
+ // Now works (previously threw):
365
+ await items.updateOne({ _id: 'racun' }, {
366
+ 'postavke[p2]': [{
367
+ _id: 'p2',
368
+ karence: {
369
+ karence: {
370
+ 'meso': 7,
371
+ 'not. organi': 7, // ← legit stored key, contains dot
372
+ 'mleko': 3,
373
+ },
374
+ },
375
+ }],
376
+ 'postavke[p1].kolicina': 99, // forces pipeline form
377
+ });
378
+ ```
379
+
380
+ Three new regression tests in `test/bracketArrayPaths.mjs` cover the
381
+ insert path (alone and combined with sub-field updates) and the
382
+ sub-field-with-object-value path.
383
+
384
+ ## 2.4.35
385
+
386
+ ### Publish payload: de-bracketed `data`, raw `rawUpdate` (BREAKING for bracket-using subscribers)
387
+
388
+ `update` publish events (and per-item updates inside `batch` events) now
389
+ carry two fields:
390
+
391
+ - `data` — the de-bracketed result. Sub-field updates (`arr[id].field`) and
392
+ terminal-bracket inserts/removes (`arr[id]: [els]` / `arr[id]: undefined`)
393
+ produce **the full post-update parent array** in `data` (no diff, no
394
+ positional placeholders). Values are sliced straight from the post-update
395
+ document — nothing is synthesized. `data` never contains mongo operators
396
+ (`$set`, `$unset`, `$pull`, `$[…]`) or flat dotted/bracket keys.
397
+ - `rawUpdate` — the user's update spec exactly as it came in (top-level
398
+ shorthand or `$set`/`$unset` form, bracket paths preserved). Mongo
399
+ operators are allowed here. Always present on `update` events; safe to
400
+ ignore for consumers that only need to apply the result.
401
+
402
+ `updateMany` events deliberately **do not** carry `rawUpdate` — mongo's
403
+ `updateMany` doesn't return the list of affected `_id`-s, so the spec on
404
+ its own is not actionable. The `data: { n, ok }` shape is unchanged from
405
+ prior versions; consumers refetch via `findNewer*`.
406
+
407
+ ```ts
408
+ await items.updateOne({ _id: 'r1' }, { 'postavke[p2].kolicina': 99 });
409
+ // publish event:
410
+ {
411
+ operation: 'update',
412
+ db: 'mydb',
413
+ collection: 'items',
414
+ data: {
415
+ _id: 'r1',
416
+ postavke: [
417
+ { _id: 'p1', kolicina: 1 },
418
+ { _id: 'p2', kolicina: 99 },
419
+ { _id: 'p3', kolicina: 3 },
420
+ ],
421
+ },
422
+ rawUpdate: { 'postavke[p2].kolicina': 99 },
423
+ }
424
+ ```
425
+
426
+ **Why breaking**: prior to this change, sub-field bracket updates leaked
427
+ mongo positional placeholders (`postavke.$[f0].kolicina`) into `data` with
428
+ `undefined` values, because `_removeUnchanged` looked up the post-translation
429
+ keys literally on the result document. Subscribers that worked around this
430
+ by reading the broken keys must switch to reading the parent array from
431
+ `data` (e.g. `data.postavke`). Subscribers that only used non-bracket
432
+ updates are unaffected.
433
+
434
+ ## 2.4.34
435
+
436
+ ### Bracketed arrays: no auto-`_id`, mixed full-object + bracketed writes
437
+
438
+ Bracket-by-`_id` processing no longer injects an `_id` onto objects written
439
+ into an array — the caller's value is stored as-is. A single update may now
440
+ mix a whole-object array replacement with bracketed per-id paths against
441
+ other arrays in the same call.
442
+
443
+ ## 2.4.33
444
+
445
+ ### Bracket-by-`_id` array paths — terminal-bracket insert and remove
446
+
447
+ All write methods — `updateOne`, `save`, `update`, `upsert`, `upsertBatch`,
448
+ and `updateCollections` — now recognize three shapes of `arr[<id>]` paths
449
+ and route each to the appropriate mongo construct:
450
+
451
+ | Path shape | Value | Operation |
452
+ |---|---|---|
453
+ | `arr[<id>].field` | any | `$set` / `$unset` with `arrayFilters` (existing behavior) |
454
+ | `arr[<id>]` | array of elements (each with `_id`) | **idempotent insert/replace** — aggregation pipeline `$filter + $concatArrays` |
455
+ | `arr[<id>]` | `undefined` (→ `$unset`) | **remove** — `$pull` on the parent array, or merged into the same pipeline stage when inserts are also present |
456
+
457
+ Quoted and unquoted ids are equivalent: `arr[p0]`, `arr['p0']`, `arr["p0"]`
458
+ all denote the same element.
459
+
460
+ Inserts are idempotent: re-sending the same `_id` replaces the element in
461
+ place rather than appending a duplicate. Inserts and removes against the
462
+ same parent array are merged into one atomic `$filter + $concatArrays`
463
+ stage, so remove+insert for the same id has no race window.
464
+
465
+ Sub-field `arrayFilters` updates may not coexist with terminal-bracket
466
+ inserts in the same update (mongo does not support `arrayFilters` on
467
+ pipeline updates). The library throws a descriptive error in that case;
468
+ split the update into two calls. Terminal-bracket removes alone remain
469
+ compatible with `arrayFilters`. (Softened to drop+warn in 2.4.45, then
470
+ fully supported in 2.4.49 — see above.)
471
+
472
+ ```ts
473
+ // insert / replace by _id
474
+ await items.updateOne({ _id: 'r1' }, {
475
+ "postavke['p0']": [{ _id: 'p0', value: 0 }],
476
+ });
477
+
478
+ // remove by _id
479
+ await items.updateOne({ _id: 'r1' }, {
480
+ "postavke['p1']": undefined,
481
+ });
482
+
483
+ // combined insert + remove on the same array — single atomic stage
484
+ await items.updateOne({ _id: 'r1' }, {
485
+ "postavke['p0']": [{ _id: 'p0', value: 0 }],
486
+ "postavke['p1']": undefined,
487
+ });
488
+ ```
489
+
490
+ See `README.md` → _Bracket-by-`_id` array paths_ for full reference.
491
+
492
+ ## 2.4.32
493
+
494
+ ### `GetNewerSpec.specId` — disambiguate duplicate-collection specs in `findNewerMany` / `findNewerManyStream`
495
+
496
+ New optional field on `GetNewerSpec`. When set, results for that spec
497
+ are keyed by `specId` in the `findNewerMany` response, and emitted as
498
+ the third arg to `onChunk` in the streaming variant. Lets a single
499
+ batched request carry multiple specs against the same `collection`
500
+ without `Record<key, items[]>` collisions.
501
+
502
+ ```ts
503
+ // Two specs against the same collection — disambiguated via specId
504
+ const result = await mongo.findNewerMany([
505
+ { collection: 'users', timestamp: 0, query: { active: true }, specId: 'active' },
506
+ { collection: 'users', timestamp: 0, query: { active: false }, specId: 'inactive' },
507
+ ]);
508
+ result.active // active users
509
+ result.inactive // inactive users
510
+
511
+ // Streaming — specId is forwarded to onChunk
512
+ await mongo.findNewerManyStream(specs, async (collection, items, specId) => {
513
+ // specId === undefined when the originating spec did not set one
514
+ });
515
+ ```
516
+
517
+ - Backwards compatible — specs without `specId` are keyed by
518
+ `collection` exactly as before.
519
+ - The streaming `onChunk` callback gains an optional third parameter;
520
+ existing two-arg callbacks continue to work unchanged.
521
+
522
+ (Docs and the `test/findNewerManySpecId.mjs` suite landed in 2.4.33.)
523
+
524
+ ## 2.4.27
525
+
526
+ ### `findNewerRemoveFields` now also strips publish events
527
+
528
+ Fields configured via `findNewerRemoveFields()` or `REMOVE_FIELDS_FOR_FINDNEWER` are now stripped from all publish event payloads (single-doc via `_publishAndAudit` and batch via `updateCollections`/`upsertBatch`), not just `findNewer*` query results.
529
+
530
+ ### `removeFieldsAlways()` — fields stripped from all find* and publish events
531
+
532
+ New instance-level setting for fields that should never reach the client. Seeded from `REMOVE_FIELDS_ALWAYS` env var, configurable at runtime via `removeFieldsAlways()` on both `Mongo` and `Repo<T>`.
533
+
534
+ ```ts
535
+ // getter — returns current field list
536
+ users.removeFieldsAlways(); // string[]
537
+
538
+ // setter — accepts comma-separated string or array
539
+ users.removeFieldsAlways('secret,internal');
540
+ users.removeFieldsAlways(['secret', 'internal']);
541
+ ```
542
+
543
+ These fields are removed from:
544
+
545
+ - All `find*` results (find, findOne, findById, findByIds, findAll, findNewer, etc.)
546
+ - All publish event payloads (single-doc and batch)
547
+
548
+ ## 2.4.25
549
+
550
+ ### `findNewerRemoveFields()` — runtime configuration of stripped fields
551
+
552
+ `REMOVE_FIELDS_FOR_FINDNEWER` was previously only configurable via environment variable (read once at module load). It is now an instance-level setting configurable at runtime via `findNewerRemoveFields()` on both `Mongo` and `Repo<T>`.
553
+
554
+ ```ts
555
+ // getter — returns current field list
556
+ users.findNewerRemoveFields(); // string[]
557
+
558
+ // setter — accepts comma-separated string or array
559
+ users.findNewerRemoveFields('_ts,_rev');
560
+ users.findNewerRemoveFields(['_ts', '_rev']);
561
+ ```
562
+
563
+ The env var `REMOVE_FIELDS_FOR_FINDNEWER` still seeds the initial value. All `findNewer`, `findNewerMany`, and `findNewerManyStream` calls use the instance field.
564
+
565
+ ### `auditCollections()` parameter made optional
566
+
567
+ `auditCollections(arr?)` now accepts no argument as a getter (was previously required to pass a value).