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 +72 -0
- package/dist/index.js +106 -0
- package/dist/src/db/SyncedDb.d.ts +30 -0
- package/dist/src/types/I_SyncedDb.d.ts +12 -0
- package/package.json +1 -1
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
|
/**
|