cry-synced-db-client 0.1.157 → 0.1.158

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/dist/index.js CHANGED
@@ -2723,19 +2723,32 @@ function isPlainObject4(value) {
2723
2723
  function hasArrayIndexPath(key) {
2724
2724
  return /\.\d+(\.|$)/.test(key);
2725
2725
  }
2726
+ function pathTargetsArrayElement(key, arrayPathTokens) {
2727
+ const keyTokens = tokenizePath(key);
2728
+ if (keyTokens.length <= arrayPathTokens.length) return false;
2729
+ for (let i = 0; i < arrayPathTokens.length; i++) {
2730
+ if (keyTokens[i] !== arrayPathTokens[i]) return false;
2731
+ }
2732
+ const next = keyTokens[arrayPathTokens.length];
2733
+ return next.startsWith("[") || /^\d+$/.test(next);
2734
+ }
2726
2735
  function fixDotnetArrays(changes, serverRev, baseRev) {
2727
- const topLevelArrays = /* @__PURE__ */ new Set();
2736
+ const arrayPathTokens = [];
2728
2737
  for (const [k, v] of Object.entries(changes)) {
2729
- if (Array.isArray(v)) topLevelArrays.add(k);
2738
+ if (Array.isArray(v)) arrayPathTokens.push(tokenizePath(k));
2730
2739
  }
2731
2740
  const isStale = typeof serverRev === "number" && typeof baseRev === "number" && serverRev > baseRev;
2732
2741
  const cleaned = {};
2733
2742
  for (const [key, value] of Object.entries(changes)) {
2734
- if (hasArrayIndexPath(key)) {
2735
- const arrayField = key.split(".")[0];
2736
- if (arrayField && topLevelArrays.has(arrayField)) continue;
2737
- if (isStale) continue;
2743
+ let conflictsWithFullArray = false;
2744
+ for (const tokens of arrayPathTokens) {
2745
+ if (pathTargetsArrayElement(key, tokens)) {
2746
+ conflictsWithFullArray = true;
2747
+ break;
2748
+ }
2738
2749
  }
2750
+ if (conflictsWithFullArray) continue;
2751
+ if (isStale && hasArrayIndexPath(key)) continue;
2739
2752
  cleaned[key] = value;
2740
2753
  }
2741
2754
  return cleaned;
@@ -2789,6 +2802,42 @@ function translateKey(key, entity) {
2789
2802
  return out.join(".");
2790
2803
  }
2791
2804
 
2805
+ // src/utils/stripServerManaged.ts
2806
+ var SERVER_MANAGED_KEYS = /* @__PURE__ */ new Set(["_ts", "_rev", "_csq"]);
2807
+ function isObjectIdLike2(v) {
2808
+ return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || v._bsontype === "ObjectID"));
2809
+ }
2810
+ function isServerManagedPath(key) {
2811
+ for (const part of tokenizePath(key)) {
2812
+ if (part.startsWith("[")) continue;
2813
+ if (SERVER_MANAGED_KEYS.has(part)) return true;
2814
+ }
2815
+ return false;
2816
+ }
2817
+ function scrubServerManagedDeep(value) {
2818
+ if (value === null || value === void 0) return value;
2819
+ if (Array.isArray(value)) {
2820
+ return value.map((item) => scrubServerManagedDeep(item));
2821
+ }
2822
+ if (typeof value !== "object") return value;
2823
+ if (value instanceof Date) return value;
2824
+ if (isObjectIdLike2(value)) return value;
2825
+ const out = {};
2826
+ for (const k of Object.keys(value)) {
2827
+ if (SERVER_MANAGED_KEYS.has(k)) continue;
2828
+ out[k] = scrubServerManagedDeep(value[k]);
2829
+ }
2830
+ return out;
2831
+ }
2832
+ function stripServerManagedFromChanges(changes) {
2833
+ const out = {};
2834
+ for (const [key, value] of Object.entries(changes)) {
2835
+ if (isServerManagedPath(key)) continue;
2836
+ out[key] = scrubServerManagedDeep(value);
2837
+ }
2838
+ return out;
2839
+ }
2840
+
2792
2841
  // src/db/sync/SyncEngine.ts
2793
2842
  var _SyncEngine = class _SyncEngine {
2794
2843
  constructor(config) {
@@ -3060,16 +3109,14 @@ var _SyncEngine = class _SyncEngine {
3060
3109
  collection: collectionName,
3061
3110
  batch: {
3062
3111
  updates: updates.map((item) => {
3063
- const _a2 = item.delta, { _ts, _rev: dirtyBaseRev } = _a2, changes = __objRest(_a2, ["_ts", "_rev"]);
3064
- const stripped = {};
3065
- for (const [k, v] of Object.entries(changes)) {
3066
- if (k.startsWith("_ts.") || k.startsWith("_rev.") || k.startsWith("_csq.")) continue;
3067
- stripped[k] = v;
3068
- }
3112
+ const dirtyBaseRev = typeof item.delta._rev === "number" ? item.delta._rev : void 0;
3113
+ const stripped = stripServerManagedFromChanges(
3114
+ item.delta
3115
+ );
3069
3116
  const fixed = fixDotnetArrays(
3070
3117
  stripped,
3071
3118
  item.currentServerRev,
3072
- typeof dirtyBaseRev === "number" ? dirtyBaseRev : void 0
3119
+ dirtyBaseRev
3073
3120
  );
3074
3121
  const cleanedChanges = translateBracketPathsToIndex(fixed, item.fullItem);
3075
3122
  return {
@@ -4745,6 +4792,7 @@ var _SyncedDb = class _SyncedDb {
4745
4792
  });
4746
4793
  delete update._id;
4747
4794
  }
4795
+ _SyncedDb.ensureNestedIds(update);
4748
4796
  update = _SyncedDb.stringifyObjectIds(update);
4749
4797
  const existing = await this.dexieDb.getById(collection, id);
4750
4798
  if (!existing && !((_a = this.collections.get(collection)) == null ? void 0 : _a.writeOnly)) {
@@ -4775,6 +4823,7 @@ var _SyncedDb = class _SyncedDb {
4775
4823
  async upsert(collection, query, update) {
4776
4824
  this.assertCollection(collection);
4777
4825
  this.ensureId(update, "upsert", collection);
4826
+ _SyncedDb.ensureNestedIds(update);
4778
4827
  query = _SyncedDb.stringifyObjectIds(query);
4779
4828
  update = _SyncedDb.stringifyObjectIds(update);
4780
4829
  const existing = await this.findOne(collection, query);
@@ -4788,6 +4837,7 @@ var _SyncedDb = class _SyncedDb {
4788
4837
  var _a;
4789
4838
  this.assertCollection(collection);
4790
4839
  this.ensureId(data, "insert", collection);
4840
+ _SyncedDb.ensureNestedIds(data);
4791
4841
  data = _SyncedDb.stringifyObjectIds(data);
4792
4842
  const id = String(data._id);
4793
4843
  const existing = await this.dexieDb.getById(collection, id);
@@ -5889,6 +5939,48 @@ var _SyncedDb = class _SyncedDb {
5889
5939
  static isObjectIdLike(v) {
5890
5940
  return !!(v && typeof v === "object" && (v._bsontype === "ObjectId" || typeof v.toHexString === "function"));
5891
5941
  }
5942
+ /**
5943
+ * Recursively walk `value` and ensure every plain object that appears
5944
+ * as an element of an array carries an `_id`. Missing `_id`s are
5945
+ * generated as `new ObjectId().toHexString()` (string, not BSON instance).
5946
+ *
5947
+ * Mutates the input tree in place — caller sees the freshly-assigned ids
5948
+ * (matches the pattern of `ensureId`).
5949
+ *
5950
+ * Why: `computeDiff` falls back to a full-array replace whenever any
5951
+ * element of an array of objects lacks `_id` (see `allElementsHaveId`).
5952
+ * That defeats element-wise `arr[<_id>].field` paths and re-introduces
5953
+ * the stale-array-overwrite bug. Stamping ids upfront keeps every save
5954
+ * on the per-element bracket path.
5955
+ *
5956
+ * Skipped:
5957
+ * - primitives (numbers, strings, booleans, null, undefined)
5958
+ * - `Date`, `ObjectId`-like values
5959
+ * - top-level objects (only ARRAY ELEMENTS get an auto-id; the entity
5960
+ * itself is handled by `ensureId`)
5961
+ */
5962
+ static ensureNestedIds(value) {
5963
+ if (value === null || value === void 0) return;
5964
+ if (typeof value !== "object") return;
5965
+ if (value instanceof Date) return;
5966
+ if (_SyncedDb.isObjectIdLike(value)) return;
5967
+ if (Array.isArray(value)) {
5968
+ for (const element of value) {
5969
+ if (element !== null && typeof element === "object" && !Array.isArray(element) && !(element instanceof Date) && !_SyncedDb.isObjectIdLike(element)) {
5970
+ if (element._id == null || element._id === "") {
5971
+ element._id = new ObjectId2().toHexString();
5972
+ } else if (_SyncedDb.isObjectIdLike(element._id)) {
5973
+ element._id = String(element._id);
5974
+ }
5975
+ }
5976
+ _SyncedDb.ensureNestedIds(element);
5977
+ }
5978
+ return;
5979
+ }
5980
+ for (const key of Object.keys(value)) {
5981
+ _SyncedDb.ensureNestedIds(value[key]);
5982
+ }
5983
+ }
5892
5984
  /**
5893
5985
  * Asserts write-only collection has online connectivity for reads.
5894
5986
  * @throws Error if offline
@@ -403,6 +403,27 @@ export declare class SyncedDb implements I_SyncedDb {
403
403
  */
404
404
  private static stringifyObjectIds;
405
405
  private static isObjectIdLike;
406
+ /**
407
+ * Recursively walk `value` and ensure every plain object that appears
408
+ * as an element of an array carries an `_id`. Missing `_id`s are
409
+ * generated as `new ObjectId().toHexString()` (string, not BSON instance).
410
+ *
411
+ * Mutates the input tree in place — caller sees the freshly-assigned ids
412
+ * (matches the pattern of `ensureId`).
413
+ *
414
+ * Why: `computeDiff` falls back to a full-array replace whenever any
415
+ * element of an array of objects lacks `_id` (see `allElementsHaveId`).
416
+ * That defeats element-wise `arr[<_id>].field` paths and re-introduces
417
+ * the stale-array-overwrite bug. Stamping ids upfront keeps every save
418
+ * on the per-element bracket path.
419
+ *
420
+ * Skipped:
421
+ * - primitives (numbers, strings, booleans, null, undefined)
422
+ * - `Date`, `ObjectId`-like values
423
+ * - top-level objects (only ARRAY ELEMENTS get an auto-id; the entity
424
+ * itself is handled by `ensureId`)
425
+ */
426
+ private static ensureNestedIds;
406
427
  /**
407
428
  * Asserts write-only collection has online connectivity for reads.
408
429
  * @throws Error if offline
@@ -13,6 +13,28 @@
13
13
  * "arr.10" → true (multi-digit index)
14
14
  */
15
15
  export declare function hasArrayIndexPath(key: string): boolean;
16
+ /**
17
+ * `true` if `key` targets an element of the array at `arrayPath`.
18
+ *
19
+ * Both inputs are tokenized via `tokenizePath` (so dot AND bracket-by-_id
20
+ * segments are normalized). The key targets an element iff:
21
+ * 1. its first N tokens equal `arrayPath`'s tokens (prefix match), AND
22
+ * 2. the very next token is a numeric index ("0", "1", …) OR a bracket
23
+ * `[<_id>]` segment.
24
+ *
25
+ * Examples (with arrayPath = "zaracunaj"):
26
+ * "zaracunaj" → false (same path, not a child)
27
+ * "zaracunaj.0.kolicina" → true (child via numeric index)
28
+ * "zaracunaj[<_id>].kolicina" → true (child via bracket _id)
29
+ * "zaracunajX" → false (different field)
30
+ * "zaracunaj.kolicina" → false (object field, not element)
31
+ *
32
+ * Examples (with arrayPath = "_redundanca.terapije"):
33
+ * "_redundanca.terapije[A].postavke[B].x" → true
34
+ * "_redundanca.terapije.0.x" → true
35
+ * "_redundanca.terapija" → false (different field)
36
+ */
37
+ export declare function pathTargetsArrayElement(key: string, arrayPathTokens: readonly string[]): boolean;
16
38
  /**
17
39
  * TEMPORARY mitigation: when server has advanced past client's base _rev
18
40
  * (i.e., the dirty was emitted against a stale snapshot of the record),
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `true` if `key` (a dot/bracket-notation path) targets a server-managed
3
+ * metadata field anywhere along its tokenized path.
4
+ *
5
+ * Examples:
6
+ * "_ts" → true
7
+ * "_ts.t" → true (token `_ts`)
8
+ * "_rev" → true
9
+ * "field._ts" → true
10
+ * "arr[<_id>]._rev" → true
11
+ * "_redundanca.terapije[X]._ts.i" → true
12
+ * "stranka.gsm" → false
13
+ * "zaracunaj[<_id>].kolicina" → false
14
+ */
15
+ export declare function isServerManagedPath(key: string): boolean;
16
+ /**
17
+ * Recursively scrub `_ts` / `_rev` / `_csq` from a value tree without
18
+ * mutating the input. Returns a new tree with the offending keys removed
19
+ * at every depth, including inside arrays.
20
+ *
21
+ * Primitives, Date, and ObjectId-like instances pass through by reference.
22
+ */
23
+ export declare function scrubServerManagedDeep<T>(value: T): T;
24
+ /**
25
+ * Strip server-managed metadata from a dirty-changes payload at upload time.
26
+ *
27
+ * 1. Drop any path key whose tokenized path includes `_ts`, `_rev`, or `_csq`.
28
+ * 2. For surviving entries, recursively scrub the value (in case a value is
29
+ * a full object/array replace that contains nested `_ts` / `_rev`).
30
+ *
31
+ * Returns a new object — input is not mutated.
32
+ */
33
+ export declare function stripServerManagedFromChanges(changes: Record<string, unknown>): Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-synced-db-client",
3
- "version": "0.1.157",
3
+ "version": "0.1.158",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",