cry-synced-db-client 0.1.173 → 0.1.175

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
@@ -2,6 +2,78 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### `save(coll, id, {field: {}})` clears existing nested children
6
+
7
+ `computeDiffInto` for plain objects iterated only `Object.keys(update)`,
8
+ so an update like `{cepljenja: {}}` against an existing
9
+ `{cepljenja: {<id>: {…}}}` emitted **zero diff entries** — children were
10
+ preserved silently. Empty-object value is now treated as a full replace
11
+ of the field (symmetric with `{field: []}` array replace and
12
+ `{field: undefined}` delete).
13
+
14
+ ```typescript
15
+ // existing.cepljenja = { "<id>": { …data } }
16
+ await syncedDb.save("pacienti", id, { cepljenja: {} });
17
+ // after: pacient.cepljenja === {} — children physically removed in
18
+ // in-mem, Dexie, and the dirty payload
19
+ ```
20
+
21
+ Implementation in `src/utils/computeDiff.ts:computeDiffInto`: when both
22
+ sides are plain objects and `update` has zero keys while `existing` has
23
+ some, emit `diff[basePath] = {}` instead of falling through to the
24
+ "iterate update keys" loop. The earlier `deepEquals(existing, update)`
25
+ guard still short-circuits the `{} → {}` no-op case.
26
+
27
+ Regression test: `test/saveEmptyObjectClearsChildren.test.ts` (4 cases:
28
+ in-mem clear, Dexie clear, dirty payload emits wipe, sibling fields
29
+ preserved). 721 pass / 0 fail.
30
+
31
+ ### `findById` / `findByIds` auto-register unconfigured collections as temporary
32
+
33
+ Calling `findById(collection, id)` or `findByIds(collection, ids)` for a
34
+ collection NOT in the runtime sync config (e.g. boot-time `collections: [...]`)
35
+ no longer throws. Instead the collection is auto-registered as
36
+ **temporary** with `syncConfig.query: () => ({_id: {$in: [<ids>]}})`. The
37
+ call then proceeds through the normal flow — `referToServer` (default
38
+ `true`) loads the row from the server on cache miss and returns it.
39
+
40
+ Behavior on subsequent calls (inspected against the existing config's
41
+ `syncConfig.query` shape, no extra bookkeeping state):
42
+
43
+ | Existing config | Action |
44
+ |---|---|
45
+ | none | install `{_id: {$in: [<ids>]}}` (static object) |
46
+ | temporary, query matches `{_id: {$in: [...]}}` | append novel ids to the existing `$in` array |
47
+ | temporary, query is a function / different shape / absent | skip — leave alone |
48
+ | permanent | skip — never touch a permanent config |
49
+
50
+ `replaceSyncCollection` naturally resets accumulation by overwriting the
51
+ spec; the new config (whatever shape) drives future syncs alone.
52
+
53
+ Constraints:
54
+ - Dexie schema must already declare the table (Dexie does not support
55
+ adding tables to an open database). The auto-register handles only the
56
+ runtime SyncedDb-level config.
57
+ - An active `syncOnlyTheseCollections` filter (non-null — set via
58
+ `setSyncOnlyTheseCollections([…])` with at least one entry) is extended
59
+ to include the new temp collection so it participates in future sync
60
+ ticks. When no filter is set (sync-all mode), this step is a no-op.
61
+
62
+ ```typescript
63
+ // No runtime config for "zivali" — boot only registers "racuni".
64
+ new SyncedDb({ collections: [{ name: "racuni" }], dexieDb /* has zivali */, ... });
65
+
66
+ // Previously: throws "Collection 'zivali' not configured".
67
+ // Now: auto-registers as temporary, fetches via referToServer, returns.
68
+ const zival = await syncedDb.findById("zivali", id);
69
+
70
+ // Upgrade to permanent when the app wires up real sync:
71
+ await syncedDb.replaceSyncCollection({
72
+ name: "zivali",
73
+ syncConfig: { query: () => ({ vrsta: "pes" }) },
74
+ });
75
+ ```
76
+
5
77
  ### `preprocessDirtyItem` callback — per-item filter / transform before upload
6
78
 
7
79
  New optional config callback fired for **every** dirty item just before it
package/dist/index.js CHANGED
@@ -440,6 +440,10 @@ function computeDiffInto(existing, update, basePath, diff) {
440
440
  diff[basePath] = update;
441
441
  return;
442
442
  }
443
+ if (Object.keys(update).length === 0 && Object.keys(existing).length > 0) {
444
+ diff[basePath] = update;
445
+ return;
446
+ }
443
447
  for (const key of Object.keys(update)) {
444
448
  const childPath = basePath ? `${basePath}.${key}` : key;
445
449
  computeDiffInto(existing[key], update[key], childPath, diff);
@@ -4871,6 +4875,7 @@ var _SyncedDb = class _SyncedDb {
4871
4875
  // ==================== Read Operations ====================
4872
4876
  async findById(collection, id, opts) {
4873
4877
  var _a;
4878
+ this._autoRegisterTemporaryForFind(collection, [id]);
4874
4879
  this.assertCollection(collection);
4875
4880
  if (!id) {
4876
4881
  const err = new Error(`[SyncedDb] findById ${collection} no id ${id}`);
@@ -4928,6 +4933,7 @@ var _SyncedDb = class _SyncedDb {
4928
4933
  }
4929
4934
  async findByIds(collection, ids, opts) {
4930
4935
  var _a;
4936
+ this._autoRegisterTemporaryForFind(collection, ids);
4931
4937
  this.assertCollection(collection);
4932
4938
  ids = ids.map((id) => this.normalizeId(id, "findByIds", collection));
4933
4939
  opts = this.resolveOpts(opts);
@@ -6262,6 +6268,44 @@ var _SyncedDb = class _SyncedDb {
6262
6268
  }
6263
6269
  }
6264
6270
  });
6271
+ const dirty = await this.dexieDb.getDirty(name);
6272
+ if (dirty.length > 0) {
6273
+ const itemById = /* @__PURE__ */ new Map();
6274
+ for (let i = 0; i < allItems.length; i++) {
6275
+ itemById.set(String(allItems[i]._id), allItems[i]);
6276
+ }
6277
+ let orphanCount = 0;
6278
+ for (const dirtyItem of dirty) {
6279
+ const id = dirtyItem._id;
6280
+ if (id == null) continue;
6281
+ const idStr = String(id);
6282
+ const existing = itemById.get(idStr);
6283
+ const diff = {};
6284
+ for (const key of Object.keys(dirtyItem)) {
6285
+ if (key === "_id" || key === "_ts" || key === "_rev") continue;
6286
+ diff[key] = dirtyItem[key];
6287
+ }
6288
+ const merged = _SyncedDb.applyDiffLocally(
6289
+ existing != null ? existing : null,
6290
+ diff,
6291
+ id,
6292
+ name
6293
+ );
6294
+ if (merged._deleted || merged._archived) {
6295
+ itemById.delete(idStr);
6296
+ continue;
6297
+ }
6298
+ if (!existing) orphanCount++;
6299
+ itemById.set(idStr, merged);
6300
+ }
6301
+ if (orphanCount > 0) {
6302
+ console.warn(
6303
+ `[SyncedDb] init(${name}): ${orphanCount} dirty record(s) had no matching Dexie main row \u2014 included in in-mem pending next sync.`
6304
+ );
6305
+ }
6306
+ allItems.length = 0;
6307
+ for (const item of itemById.values()) allItems.push(item);
6308
+ }
6265
6309
  this.inMemManager.initCollection(name, allItems);
6266
6310
  const meta = await this.dexieDb.getSyncMeta(name);
6267
6311
  if (meta) {
@@ -6293,6 +6337,68 @@ var _SyncedDb = class _SyncedDb {
6293
6337
  throw new Error(`SyncedDb: Collection "${(name == null ? void 0 : name.toString()) || "?"}" not configured`);
6294
6338
  }
6295
6339
  }
6340
+ /**
6341
+ * Auto-register an unconfigured collection as TEMPORARY when `findById` /
6342
+ * `findByIds` is called for it. The collection becomes part of the sync
6343
+ * scope with a `{_id: {$in: <ids>}}` static query so future sync ticks
6344
+ * keep those rows fresh.
6345
+ *
6346
+ * Behavior matrix:
6347
+ *
6348
+ * | Existing config | Action |
6349
+ * |---|---|
6350
+ * | none | install temp with `query: {_id: {$in: [<ids>]}}` |
6351
+ * | temporary, query is `{_id: {$in: [...]}}` | append novel `<ids>` to the existing `$in` array |
6352
+ * | temporary, query is anything else (function, different shape, missing) | skip — leave config untouched |
6353
+ * | permanent | skip — never alter a permanent config |
6354
+ *
6355
+ * No extra state map — accumulation lives in the config's own `$in`
6356
+ * array. `replaceSyncCollection` (or any other path that installs a new
6357
+ * config) naturally resets accumulation by overwriting the config.
6358
+ *
6359
+ * Cheap fast paths: synchronous, no Dexie/server I/O. The regular
6360
+ * `findById` flow (in-mem cache → `referToServer` → `ensureItemsAreLoaded`)
6361
+ * handles the actual data load for THIS call.
6362
+ *
6363
+ * If an `syncOnlyTheseCollections` filter is active (non-null, i.e.
6364
+ * `setSyncOnlyTheseCollections([…])` was called with at least one
6365
+ * entry), a newly-installed temp collection is added to the filter so
6366
+ * it participates in future sync ticks. When no filter is set
6367
+ * (sync-all mode), this step is a no-op.
6368
+ */
6369
+ _autoRegisterTemporaryForFind(name, ids) {
6370
+ var _a;
6371
+ const idStrings = ids.map((id) => String(id));
6372
+ const existing = this.collections.get(name);
6373
+ if (!existing) {
6374
+ this.collections.set(name, {
6375
+ name,
6376
+ temporaryConfig: true,
6377
+ syncConfig: {
6378
+ query: { _id: { $in: idStrings } }
6379
+ }
6380
+ });
6381
+ if (this.syncOnlyCollections) {
6382
+ this.syncOnlyCollections.add(name);
6383
+ }
6384
+ return;
6385
+ }
6386
+ if (!existing.temporaryConfig) return;
6387
+ const query = (_a = existing.syncConfig) == null ? void 0 : _a.query;
6388
+ if (typeof query !== "object" || query === null) return;
6389
+ const idField = query._id;
6390
+ if (typeof idField !== "object" || idField === null) return;
6391
+ const inArr = idField.$in;
6392
+ if (!Array.isArray(inArr)) return;
6393
+ const seen = /* @__PURE__ */ new Set();
6394
+ for (const existingId of inArr) seen.add(String(existingId));
6395
+ for (const id of idStrings) {
6396
+ if (!seen.has(id)) {
6397
+ inArr.push(id);
6398
+ seen.add(id);
6399
+ }
6400
+ }
6401
+ }
6296
6402
  /** Stringify an Id parameter (ObjectId → hex string). */
6297
6403
  normalizeId(id, method, collection) {
6298
6404
  if (!id && id !== void 0) {
@@ -426,6 +426,36 @@ export declare class SyncedDb implements I_SyncedDb {
426
426
  */
427
427
  private preloadAllSyncMetas;
428
428
  private assertCollection;
429
+ /**
430
+ * Auto-register an unconfigured collection as TEMPORARY when `findById` /
431
+ * `findByIds` is called for it. The collection becomes part of the sync
432
+ * scope with a `{_id: {$in: <ids>}}` static query so future sync ticks
433
+ * keep those rows fresh.
434
+ *
435
+ * Behavior matrix:
436
+ *
437
+ * | Existing config | Action |
438
+ * |---|---|
439
+ * | none | install temp with `query: {_id: {$in: [<ids>]}}` |
440
+ * | temporary, query is `{_id: {$in: [...]}}` | append novel `<ids>` to the existing `$in` array |
441
+ * | temporary, query is anything else (function, different shape, missing) | skip — leave config untouched |
442
+ * | permanent | skip — never alter a permanent config |
443
+ *
444
+ * No extra state map — accumulation lives in the config's own `$in`
445
+ * array. `replaceSyncCollection` (or any other path that installs a new
446
+ * config) naturally resets accumulation by overwriting the config.
447
+ *
448
+ * Cheap fast paths: synchronous, no Dexie/server I/O. The regular
449
+ * `findById` flow (in-mem cache → `referToServer` → `ensureItemsAreLoaded`)
450
+ * handles the actual data load for THIS call.
451
+ *
452
+ * If an `syncOnlyTheseCollections` filter is active (non-null, i.e.
453
+ * `setSyncOnlyTheseCollections([…])` was called with at least one
454
+ * entry), a newly-installed temp collection is added to the filter so
455
+ * it participates in future sync ticks. When no filter is set
456
+ * (sync-all mode), this step is a no-op.
457
+ */
458
+ private _autoRegisterTemporaryForFind;
429
459
  private static readonly STRINGIFIED_FALSY;
430
460
  /** Stringify an Id parameter (ObjectId → hex string). */
431
461
  private normalizeId;
@@ -864,6 +864,13 @@ export interface I_SyncedDb {
864
864
  * jih v ozadju revalidira s serverja (Dexie + in-mem skozi konflikt
865
865
  * resolucijo). Ortogonalno do referToServer; ne povzroči duplikata
866
866
  * server klicev za missing ID-je.
867
+ *
868
+ * Auto-registracija: če `collection` ni registrirana v runtime sync
869
+ * configu, se avtomatsko doda kot **temporary** s queryjem
870
+ * `{_id: {$in: [<id>]}}`. Dexie schema mora že imeti tabelo
871
+ * (Dexie ne podpira dodajanja tabel ob runtime-u). Klic nato teče
872
+ * skozi normalen findById flow — `referToServer` (privzeto `true`)
873
+ * naloži zapis s serverja ob cache miss-u.
867
874
  */
868
875
  findById<T extends DbEntity>(collection: string, id: Id, opts?: QueryOpts): Promise<T | null>;
869
876
  /**
@@ -873,6 +880,11 @@ export interface I_SyncedDb {
873
880
  * jih v ozadju revalidira s serverja (Dexie + in-mem skozi konflikt
874
881
  * resolucijo). Ortogonalno do referToServer; ne povzroči duplikata
875
882
  * server klicev za missing ID-je.
883
+ *
884
+ * Auto-registracija: enako kot `findById` — če `collection` ni v
885
+ * runtime sync configu, se doda kot temporary s queryjem
886
+ * `{_id: {$in: [<ids>]}}` (vsi ID-ji iz klica). Dexie schema mora že
887
+ * imeti tabelo.
876
888
  */
877
889
  findByIds<T extends DbEntity>(collection: string, ids: Id[], opts?: QueryOpts): Promise<T[]>;
878
890
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.173",
3
+ "version": "0.1.175",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",