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 +567 -0
- package/CLAUDE.md +202 -0
- package/dist/mongo.d.mts.map +1 -1
- package/dist/mongo.mjs +27 -20
- package/dist/mongo.mjs.map +1 -1
- package/dist/types.d.mts +3 -0
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +5 -2
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).
|