@korajs/store 1.0.0-beta.0 → 1.0.0-beta.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.
Files changed (61) hide show
  1. package/dist/adapters/better-sqlite3.d.cts +1 -1
  2. package/dist/adapters/better-sqlite3.d.ts +1 -1
  3. package/dist/adapters/indexeddb.cjs +39 -5
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +1 -1
  6. package/dist/adapters/indexeddb.d.ts +1 -1
  7. package/dist/adapters/indexeddb.js +13 -6
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-worker.cjs +25 -3
  10. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  11. package/dist/adapters/sqlite-wasm-worker.js +25 -3
  12. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  13. package/dist/adapters/sqlite-wasm.cjs +29 -0
  14. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  15. package/dist/adapters/sqlite-wasm.d.cts +17 -2
  16. package/dist/adapters/sqlite-wasm.d.ts +17 -2
  17. package/dist/adapters/sqlite-wasm.js +1 -1
  18. package/dist/{backup-5XZ7THNJ.js → backup-IQE4QC7Q.js} +3 -3
  19. package/dist/blob-fs.cjs +166 -0
  20. package/dist/blob-fs.cjs.map +1 -0
  21. package/dist/blob-fs.d.cts +29 -0
  22. package/dist/blob-fs.d.ts +29 -0
  23. package/dist/blob-fs.js +128 -0
  24. package/dist/blob-fs.js.map +1 -0
  25. package/dist/{chunk-OUQDC5Z6.js → chunk-2VRPF25R.js} +58 -38
  26. package/dist/chunk-2VRPF25R.js.map +1 -0
  27. package/dist/{chunk-ETDJEH2I.js → chunk-6NKLDBTY.js} +11 -1
  28. package/dist/{chunk-ETDJEH2I.js.map → chunk-6NKLDBTY.js.map} +1 -1
  29. package/dist/{chunk-5R4QMY3Z.js → chunk-OCOGQRHW.js} +13 -10
  30. package/dist/chunk-OCOGQRHW.js.map +1 -0
  31. package/dist/{chunk-RBPVEAVM.js → chunk-QUJGXSZ6.js} +30 -1
  32. package/dist/chunk-QUJGXSZ6.js.map +1 -0
  33. package/dist/{chunk-EIVXWF4B.js → chunk-RCTG3IJG.js} +4 -3
  34. package/dist/chunk-RCTG3IJG.js.map +1 -0
  35. package/dist/chunk-W4MTTNEZ.js +55 -0
  36. package/dist/chunk-W4MTTNEZ.js.map +1 -0
  37. package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
  38. package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
  39. package/dist/{export-audit-VPGQWNR6.js → export-audit-MT2LTMIN.js} +3 -3
  40. package/dist/index.cjs +765 -128
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +397 -6
  43. package/dist/index.d.ts +397 -6
  44. package/dist/index.js +603 -60
  45. package/dist/index.js.map +1 -1
  46. package/dist/internal.cjs +68 -43
  47. package/dist/internal.cjs.map +1 -1
  48. package/dist/internal.d.cts +3 -3
  49. package/dist/internal.d.ts +3 -3
  50. package/dist/internal.js +3 -3
  51. package/dist/{types-CyjXwrjW.d.cts → types-5h4Hqcwc.d.ts} +10 -3
  52. package/dist/{types-DAxCladl.d.ts → types-DfPiPwKr.d.cts} +10 -3
  53. package/dist/{types-CJOVKxLw.d.cts → types-k6AZgiH4.d.cts} +6 -1
  54. package/dist/{types-CJOVKxLw.d.ts → types-k6AZgiH4.d.ts} +6 -1
  55. package/package.json +13 -2
  56. package/dist/chunk-5R4QMY3Z.js.map +0 -1
  57. package/dist/chunk-EIVXWF4B.js.map +0 -1
  58. package/dist/chunk-OUQDC5Z6.js.map +0 -1
  59. package/dist/chunk-RBPVEAVM.js.map +0 -1
  60. /package/dist/{backup-5XZ7THNJ.js.map → backup-IQE4QC7Q.js.map} +0 -0
  61. /package/dist/{export-audit-VPGQWNR6.js.map → export-audit-MT2LTMIN.js.map} +0 -0
@@ -0,0 +1,128 @@
1
+ import {
2
+ BlobIntegrityError
3
+ } from "./chunk-W4MTTNEZ.js";
4
+
5
+ // src/blob/filesystem-blob-store.ts
6
+ import { randomUUID } from "crypto";
7
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "fs/promises";
8
+ import { dirname, join } from "path";
9
+ import { createBlobRef, hashBlob } from "@korajs/core";
10
+ var FilesystemBlobStore = class {
11
+ constructor(dir) {
12
+ this.dir = dir;
13
+ }
14
+ dir;
15
+ pathFor(hash) {
16
+ return join(this.dir, hash.slice(0, 2), hash);
17
+ }
18
+ async put(bytes, metadata = {}) {
19
+ const ref = await createBlobRef(bytes, metadata);
20
+ const target = this.pathFor(ref.hash);
21
+ if (await this.exists(target)) {
22
+ return ref;
23
+ }
24
+ await mkdir(dirname(target), { recursive: true });
25
+ const out = new Uint8Array(bytes.byteLength);
26
+ out.set(bytes);
27
+ const tmp = `${target}.${randomUUID()}.tmp`;
28
+ await writeFile(tmp, out);
29
+ await rename(tmp, target);
30
+ return ref;
31
+ }
32
+ async get(hash) {
33
+ let contents;
34
+ try {
35
+ contents = await readFile(this.pathFor(hash));
36
+ } catch (error) {
37
+ if (isNotFound(error)) {
38
+ return null;
39
+ }
40
+ throw error;
41
+ }
42
+ const bytes = new Uint8Array(contents);
43
+ const actual = await hashBlob(bytes);
44
+ if (actual !== hash) {
45
+ throw new BlobIntegrityError(hash, actual);
46
+ }
47
+ return bytes;
48
+ }
49
+ async has(hash) {
50
+ return this.exists(this.pathFor(hash));
51
+ }
52
+ async delete(hash) {
53
+ const target = this.pathFor(hash);
54
+ if (!await this.exists(target)) {
55
+ return false;
56
+ }
57
+ await rm(target);
58
+ return true;
59
+ }
60
+ async size() {
61
+ let count = 0;
62
+ let shards;
63
+ try {
64
+ shards = await readdir(this.dir);
65
+ } catch (error) {
66
+ if (isNotFound(error)) {
67
+ return 0;
68
+ }
69
+ throw error;
70
+ }
71
+ for (const shard of shards) {
72
+ try {
73
+ const entries = await readdir(join(this.dir, shard));
74
+ count += entries.filter((name) => !name.endsWith(".tmp")).length;
75
+ } catch (error) {
76
+ if (!isNotFound(error)) {
77
+ throw error;
78
+ }
79
+ }
80
+ }
81
+ return count;
82
+ }
83
+ async list() {
84
+ let shards;
85
+ try {
86
+ shards = await readdir(this.dir);
87
+ } catch (error) {
88
+ if (isNotFound(error)) {
89
+ return [];
90
+ }
91
+ throw error;
92
+ }
93
+ const hashes = [];
94
+ for (const shard of shards) {
95
+ try {
96
+ const entries = await readdir(join(this.dir, shard));
97
+ for (const name of entries) {
98
+ if (!name.endsWith(".tmp")) {
99
+ hashes.push(name);
100
+ }
101
+ }
102
+ } catch (error) {
103
+ if (!isNotFound(error)) {
104
+ throw error;
105
+ }
106
+ }
107
+ }
108
+ return hashes;
109
+ }
110
+ async exists(path) {
111
+ try {
112
+ await stat(path);
113
+ return true;
114
+ } catch (error) {
115
+ if (isNotFound(error)) {
116
+ return false;
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+ };
122
+ function isNotFound(error) {
123
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
124
+ }
125
+ export {
126
+ FilesystemBlobStore
127
+ };
128
+ //# sourceMappingURL=blob-fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blob/filesystem-blob-store.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { type BlobRef, type BlobRefMetadata, createBlobRef, hashBlob } from '@korajs/core'\nimport { BlobIntegrityError, type ContentAddressedBlobStore } from './content-addressed-blob-store'\n\n/**\n * A persistent {@link ContentAddressedBlobStore} backed by the filesystem.\n *\n * Node-only (exported from `@korajs/store/blob-fs` so it never enters a browser\n * bundle). Blobs are stored at `<dir>/<hash[0:2]>/<hash>`, sharded by hash prefix\n * so a single directory never holds millions of entries. Writes are atomic (write\n * to a temp file, then rename) so a crash mid-write cannot leave a half-written\n * blob under a valid hash. Reads verify the bytes hash to the requested key.\n *\n * This is the same content-addressed contract as {@link MemoryBlobStore}, so it\n * drops into the blob transfer path (chunk staging, blob destination) unchanged.\n */\nexport class FilesystemBlobStore implements ContentAddressedBlobStore {\n\tconstructor(private readonly dir: string) {}\n\n\tprivate pathFor(hash: string): string {\n\t\treturn join(this.dir, hash.slice(0, 2), hash)\n\t}\n\n\tasync put(bytes: Uint8Array, metadata: BlobRefMetadata = {}): Promise<BlobRef> {\n\t\tconst ref = await createBlobRef(bytes, metadata)\n\t\tconst target = this.pathFor(ref.hash)\n\n\t\t// Dedup: identical content already stored is a no-op.\n\t\tif (await this.exists(target)) {\n\t\t\treturn ref\n\t\t}\n\n\t\tawait mkdir(dirname(target), { recursive: true })\n\t\t// Copy into an exactly-sized buffer so a subarray view writes its own bytes.\n\t\tconst out = new Uint8Array(bytes.byteLength)\n\t\tout.set(bytes)\n\t\t// Atomic write: temp file then rename, so a crash never leaves a partial\n\t\t// blob under a hash that readers would then trust.\n\t\tconst tmp = `${target}.${randomUUID()}.tmp`\n\t\tawait writeFile(tmp, out)\n\t\tawait rename(tmp, target)\n\t\treturn ref\n\t}\n\n\tasync get(hash: string): Promise<Uint8Array | null> {\n\t\tlet contents: Buffer\n\t\ttry {\n\t\t\tcontents = await readFile(this.pathFor(hash))\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tconst bytes = new Uint8Array(contents)\n\t\tconst actual = await hashBlob(bytes)\n\t\tif (actual !== hash) {\n\t\t\tthrow new BlobIntegrityError(hash, actual)\n\t\t}\n\t\treturn bytes\n\t}\n\n\tasync has(hash: string): Promise<boolean> {\n\t\treturn this.exists(this.pathFor(hash))\n\t}\n\n\tasync delete(hash: string): Promise<boolean> {\n\t\tconst target = this.pathFor(hash)\n\t\tif (!(await this.exists(target))) {\n\t\t\treturn false\n\t\t}\n\t\tawait rm(target)\n\t\treturn true\n\t}\n\n\tasync size(): Promise<number> {\n\t\tlet count = 0\n\t\tlet shards: string[]\n\t\ttry {\n\t\t\tshards = await readdir(this.dir)\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tfor (const shard of shards) {\n\t\t\ttry {\n\t\t\t\tconst entries = await readdir(join(this.dir, shard))\n\t\t\t\tcount += entries.filter((name) => !name.endsWith('.tmp')).length\n\t\t\t} catch (error) {\n\t\t\t\tif (!isNotFound(error)) {\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn count\n\t}\n\n\tasync list(): Promise<string[]> {\n\t\tlet shards: string[]\n\t\ttry {\n\t\t\tshards = await readdir(this.dir)\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t\tconst hashes: string[] = []\n\t\tfor (const shard of shards) {\n\t\t\ttry {\n\t\t\t\tconst entries = await readdir(join(this.dir, shard))\n\t\t\t\tfor (const name of entries) {\n\t\t\t\t\tif (!name.endsWith('.tmp')) {\n\t\t\t\t\t\thashes.push(name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!isNotFound(error)) {\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn hashes\n\t}\n\n\tprivate async exists(path: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait stat(path)\n\t\t\treturn true\n\t\t} catch (error) {\n\t\t\tif (isNotFound(error)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthrow error\n\t\t}\n\t}\n}\n\nfunction isNotFound(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' && error !== null && (error as { code?: string }).code === 'ENOENT'\n\t)\n}\n"],"mappings":";;;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,OAAO,UAAU,SAAS,QAAQ,IAAI,MAAM,iBAAiB;AACtE,SAAS,SAAS,YAAY;AAC9B,SAA6C,eAAe,gBAAgB;AAerE,IAAM,sBAAN,MAA+D;AAAA,EACrE,YAA6B,KAAa;AAAb;AAAA,EAAc;AAAA,EAAd;AAAA,EAErB,QAAQ,MAAsB;AACrC,WAAO,KAAK,KAAK,KAAK,KAAK,MAAM,GAAG,CAAC,GAAG,IAAI;AAAA,EAC7C;AAAA,EAEA,MAAM,IAAI,OAAmB,WAA4B,CAAC,GAAqB;AAC9E,UAAM,MAAM,MAAM,cAAc,OAAO,QAAQ;AAC/C,UAAM,SAAS,KAAK,QAAQ,IAAI,IAAI;AAGpC,QAAI,MAAM,KAAK,OAAO,MAAM,GAAG;AAC9B,aAAO;AAAA,IACR;AAEA,UAAM,MAAM,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AAEhD,UAAM,MAAM,IAAI,WAAW,MAAM,UAAU;AAC3C,QAAI,IAAI,KAAK;AAGb,UAAM,MAAM,GAAG,MAAM,IAAI,WAAW,CAAC;AACrC,UAAM,UAAU,KAAK,GAAG;AACxB,UAAM,OAAO,KAAK,MAAM;AACxB,WAAO;AAAA,EACR;AAAA,EAEA,MAAM,IAAI,MAA0C;AACnD,QAAI;AACJ,QAAI;AACH,iBAAW,MAAM,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC7C,SAAS,OAAO;AACf,UAAI,WAAW,KAAK,GAAG;AACtB,eAAO;AAAA,MACR;AACA,YAAM;AAAA,IACP;AACA,UAAM,QAAQ,IAAI,WAAW,QAAQ;AACrC,UAAM,SAAS,MAAM,SAAS,KAAK;AACnC,QAAI,WAAW,MAAM;AACpB,YAAM,IAAI,mBAAmB,MAAM,MAAM;AAAA,IAC1C;AACA,WAAO;AAAA,EACR;AAAA,EAEA,MAAM,IAAI,MAAgC;AACzC,WAAO,KAAK,OAAO,KAAK,QAAQ,IAAI,CAAC;AAAA,EACtC;AAAA,EAEA,MAAM,OAAO,MAAgC;AAC5C,UAAM,SAAS,KAAK,QAAQ,IAAI;AAChC,QAAI,CAAE,MAAM,KAAK,OAAO,MAAM,GAAI;AACjC,aAAO;AAAA,IACR;AACA,UAAM,GAAG,MAAM;AACf,WAAO;AAAA,EACR;AAAA,EAEA,MAAM,OAAwB;AAC7B,QAAI,QAAQ;AACZ,QAAI;AACJ,QAAI;AACH,eAAS,MAAM,QAAQ,KAAK,GAAG;AAAA,IAChC,SAAS,OAAO;AACf,UAAI,WAAW,KAAK,GAAG;AACtB,eAAO;AAAA,MACR;AACA,YAAM;AAAA,IACP;AACA,eAAW,SAAS,QAAQ;AAC3B,UAAI;AACH,cAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,CAAC;AACnD,iBAAS,QAAQ,OAAO,CAAC,SAAS,CAAC,KAAK,SAAS,MAAM,CAAC,EAAE;AAAA,MAC3D,SAAS,OAAO;AACf,YAAI,CAAC,WAAW,KAAK,GAAG;AACvB,gBAAM;AAAA,QACP;AAAA,MACD;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAAA,EAEA,MAAM,OAA0B;AAC/B,QAAI;AACJ,QAAI;AACH,eAAS,MAAM,QAAQ,KAAK,GAAG;AAAA,IAChC,SAAS,OAAO;AACf,UAAI,WAAW,KAAK,GAAG;AACtB,eAAO,CAAC;AAAA,MACT;AACA,YAAM;AAAA,IACP;AACA,UAAM,SAAmB,CAAC;AAC1B,eAAW,SAAS,QAAQ;AAC3B,UAAI;AACH,cAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,CAAC;AACnD,mBAAW,QAAQ,SAAS;AAC3B,cAAI,CAAC,KAAK,SAAS,MAAM,GAAG;AAC3B,mBAAO,KAAK,IAAI;AAAA,UACjB;AAAA,QACD;AAAA,MACD,SAAS,OAAO;AACf,YAAI,CAAC,WAAW,KAAK,GAAG;AACvB,gBAAM;AAAA,QACP;AAAA,MACD;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAAA,EAEA,MAAc,OAAO,MAAgC;AACpD,QAAI;AACH,YAAM,KAAK,IAAI;AACf,aAAO;AAAA,IACR,SAAS,OAAO;AACf,UAAI,WAAW,KAAK,GAAG;AACtB,eAAO;AAAA,MACR;AACA,YAAM;AAAA,IACP;AAAA,EACD;AACD;AAEA,SAAS,WAAW,OAAyB;AAC5C,SACC,OAAO,UAAU,YAAY,UAAU,QAAS,MAA4B,SAAS;AAEvF;","names":[]}
@@ -1,13 +1,13 @@
1
- import {
2
- QueryError,
3
- RecordNotFoundError
4
- } from "./chunk-QL2XVZRN.js";
5
1
  import {
6
2
  deserializeRecord,
7
3
  encodeRichtextFieldsForOpData,
8
4
  serializeOperation,
9
5
  serializeRecord
10
- } from "./chunk-ETDJEH2I.js";
6
+ } from "./chunk-6NKLDBTY.js";
7
+ import {
8
+ QueryError,
9
+ RecordNotFoundError
10
+ } from "./chunk-QL2XVZRN.js";
11
11
 
12
12
  // src/lww/row-version.ts
13
13
  import { HybridLogicalClock } from "@korajs/core";
@@ -33,9 +33,10 @@ function lwwVersionWhereClause(remoteVersion) {
33
33
  }
34
34
 
35
35
  // src/query/sql-builder.ts
36
+ import { quoteIdent } from "@korajs/core";
36
37
  function buildSelectQuery(descriptor, fields) {
37
38
  const params = [];
38
- const parts = [`SELECT * FROM ${descriptor.collection}`];
39
+ const parts = [`SELECT * FROM ${quoteIdent(descriptor.collection)}`];
39
40
  const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
40
41
  const deletedFilter = "_deleted = 0";
41
42
  if (whereClause) {
@@ -46,7 +47,7 @@ function buildSelectQuery(descriptor, fields) {
46
47
  if (descriptor.orderBy.length > 0) {
47
48
  const orderParts = descriptor.orderBy.map((o) => {
48
49
  validateFieldName(o.field, fields);
49
- return `${o.field} ${o.direction.toUpperCase()}`;
50
+ return `${quoteIdent(o.field)} ${o.direction.toUpperCase()}`;
50
51
  });
51
52
  parts.push(`ORDER BY ${orderParts.join(", ")}`);
52
53
  }
@@ -60,7 +61,7 @@ function buildSelectQuery(descriptor, fields) {
60
61
  }
61
62
  function buildCountQuery(descriptor, fields) {
62
63
  const params = [];
63
- const parts = [`SELECT COUNT(*) as count FROM ${descriptor.collection}`];
64
+ const parts = [`SELECT COUNT(*) as count FROM ${quoteIdent(descriptor.collection)}`];
64
65
  const whereClause = buildWhereClauseParts(descriptor.where, fields, params);
65
66
  const deletedFilter = "_deleted = 0";
66
67
  if (whereClause) {
@@ -74,17 +75,18 @@ function buildInsertQuery(collection, record) {
74
75
  const columns = Object.keys(record);
75
76
  const placeholders = columns.map(() => "?");
76
77
  const params = Object.values(record);
77
- const sql = `INSERT INTO ${collection} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
78
+ const quotedColumns = columns.map((col) => quoteIdent(col));
79
+ const sql = `INSERT INTO ${quoteIdent(collection)} (${quotedColumns.join(", ")}) VALUES (${placeholders.join(", ")})`;
78
80
  return { sql, params };
79
81
  }
80
82
  function buildUpdateQuery(collection, id, changes) {
81
- const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
83
+ const setClauses = Object.keys(changes).map((col) => `${quoteIdent(col)} = ?`);
82
84
  const params = [...Object.values(changes), id];
83
- const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
85
+ const sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(", ")} WHERE id = ?`;
84
86
  return { sql, params };
85
87
  }
86
88
  function buildFieldFastForwardUpdateQuery(collection, id, fieldChanges, remoteVersion, wallTime, options) {
87
- const fieldClauses = Object.keys(fieldChanges).map((col) => `${col} = ?`);
89
+ const fieldClauses = Object.keys(fieldChanges).map((col) => `${quoteIdent(col)} = ?`);
88
90
  const setClauses = [
89
91
  ...fieldClauses,
90
92
  "_version = CASE WHEN _version >= ? THEN _version ELSE ? END",
@@ -102,32 +104,32 @@ function buildFieldFastForwardUpdateQuery(collection, id, fieldChanges, remoteVe
102
104
  params.push(options.maxCreatedAt, options.maxCreatedAt);
103
105
  }
104
106
  params.push(id);
105
- const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
107
+ const sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(", ")} WHERE id = ?`;
106
108
  return { sql, params };
107
109
  }
108
110
  function buildSoftDeleteQuery(collection, id, updatedAt, version) {
109
111
  if (version !== void 0) {
110
112
  return {
111
- sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ?`,
113
+ sql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ?`,
112
114
  params: [updatedAt, version, id]
113
115
  };
114
116
  }
115
117
  return {
116
- sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
118
+ sql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
117
119
  params: [updatedAt, id]
118
120
  };
119
121
  }
120
122
  function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
121
- const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
123
+ const setClauses = Object.keys(changes).map((col) => `${quoteIdent(col)} = ?`);
122
124
  const lww = lwwVersionWhereClause(remoteVersion);
123
- const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
125
+ const sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
124
126
  const params = [...Object.values(changes), id, ...lww.params];
125
127
  return { sql, params };
126
128
  }
127
129
  function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
128
130
  const lww = lwwVersionWhereClause(version);
129
131
  return {
130
- sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ? AND ${lww.sql}`,
132
+ sql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ? AND ${lww.sql}`,
131
133
  params: [updatedAt, version, id, ...lww.params]
132
134
  };
133
135
  }
@@ -164,31 +166,32 @@ function buildWhereClauseParts(where, fields, params) {
164
166
  }
165
167
  function buildOperatorCondition(fieldName, operator, value, descriptor, params) {
166
168
  const sqlValue = descriptor?.kind === "boolean" && typeof value === "boolean" ? value ? 1 : 0 : value;
169
+ const column = quoteIdent(fieldName);
167
170
  switch (operator) {
168
171
  case "$eq":
169
172
  if (sqlValue === null) {
170
- return `${fieldName} IS NULL`;
173
+ return `${column} IS NULL`;
171
174
  }
172
175
  params.push(sqlValue);
173
- return `${fieldName} = ?`;
176
+ return `${column} = ?`;
174
177
  case "$ne":
175
178
  if (sqlValue === null) {
176
- return `${fieldName} IS NOT NULL`;
179
+ return `${column} IS NOT NULL`;
177
180
  }
178
181
  params.push(sqlValue);
179
- return `${fieldName} != ?`;
182
+ return `${column} != ?`;
180
183
  case "$gt":
181
184
  params.push(sqlValue);
182
- return `${fieldName} > ?`;
185
+ return `${column} > ?`;
183
186
  case "$gte":
184
187
  params.push(sqlValue);
185
- return `${fieldName} >= ?`;
188
+ return `${column} >= ?`;
186
189
  case "$lt":
187
190
  params.push(sqlValue);
188
- return `${fieldName} < ?`;
191
+ return `${column} < ?`;
189
192
  case "$lte":
190
193
  params.push(sqlValue);
191
- return `${fieldName} <= ?`;
194
+ return `${column} <= ?`;
192
195
  case "$in": {
193
196
  if (!Array.isArray(sqlValue)) {
194
197
  throw new QueryError(`$in operator requires an array value for field "${fieldName}"`, {
@@ -202,7 +205,7 @@ function buildOperatorCondition(fieldName, operator, value, descriptor, params)
202
205
  descriptor?.kind === "boolean" && typeof item === "boolean" ? item ? 1 : 0 : item
203
206
  );
204
207
  }
205
- return `${fieldName} IN (${placeholders.join(", ")})`;
208
+ return `${column} IN (${placeholders.join(", ")})`;
206
209
  }
207
210
  default:
208
211
  throw new QueryError(`Unknown operator "${operator}"`, { operator });
@@ -242,7 +245,7 @@ function resolveCausalDeps(ctx) {
242
245
  }
243
246
 
244
247
  // src/mutations/execute-delete.ts
245
- import { createOperation } from "@korajs/core";
248
+ import { createOperation, quoteIdent as quoteIdent2 } from "@korajs/core";
246
249
 
247
250
  // src/store/sequence-allocator.ts
248
251
  async function allocateNextSequenceNumber(adapter, nodeId) {
@@ -282,7 +285,7 @@ async function readSequenceNumber(adapter, nodeId) {
282
285
  // src/mutations/execute-delete.ts
283
286
  async function executeDelete(ctx, id, options) {
284
287
  const currentRows = await ctx.adapter.query(
285
- `SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
288
+ `SELECT * FROM ${quoteIdent2(ctx.collection)} WHERE id = ? AND _deleted = 0`,
286
289
  [id]
287
290
  );
288
291
  if (!currentRows[0]) {
@@ -341,7 +344,12 @@ async function executeDelete(ctx, id, options) {
341
344
  }
342
345
 
343
346
  // src/mutations/execute-insert.ts
344
- import { createOperation as createOperation2, generateUUIDv7, validateRecord } from "@korajs/core";
347
+ import {
348
+ createOperation as createOperation2,
349
+ generateUUIDv7,
350
+ transformSecretFieldsForWrite,
351
+ validateRecord
352
+ } from "@korajs/core";
345
353
 
346
354
  // src/lww/field-versions.ts
347
355
  function parseFieldVersions(raw) {
@@ -403,6 +411,11 @@ async function executeInsert(ctx, data) {
403
411
  validated[fieldName] = Date.now();
404
412
  }
405
413
  }
414
+ const writeData = await transformSecretFieldsForWrite(
415
+ validated,
416
+ ctx.definition,
417
+ ctx.secretKeyProvider
418
+ );
406
419
  const causalDeps = resolveCausalDeps(ctx);
407
420
  let operation;
408
421
  let record;
@@ -417,7 +430,7 @@ async function executeInsert(ctx, data) {
417
430
  // Binary richtext values are tagged as canonical JSON BEFORE the
418
431
  // operation is content-hashed, so the hash input, persisted JSON,
419
432
  // and wire payload are the identical value.
420
- data: encodeRichtextFieldsForOpData(validated, ctx.definition.fields),
433
+ data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
421
434
  previousData: null,
422
435
  sequenceNumber,
423
436
  causalDeps,
@@ -426,7 +439,7 @@ async function executeInsert(ctx, data) {
426
439
  ctx.clock
427
440
  );
428
441
  ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
429
- const serializedData = serializeRecord(validated, ctx.definition.fields);
442
+ const serializedData = serializeRecord(writeData, ctx.definition.fields);
430
443
  const version = serializeRowVersion(operation.timestamp);
431
444
  record = {
432
445
  id: recordId,
@@ -451,7 +464,7 @@ async function executeInsert(ctx, data) {
451
464
  ctx.onMutation(ctx.collection, operation);
452
465
  return {
453
466
  id: recordId,
454
- ...validated,
467
+ ...writeData,
455
468
  createdAt: operation.timestamp.wallTime,
456
469
  updatedAt: operation.timestamp.wallTime
457
470
  };
@@ -543,13 +556,15 @@ function validateUpdateStateMachine(collectionName, recordId, collectionDef, cur
543
556
  import {
544
557
  createOperation as createOperation3,
545
558
  isAtomicOp,
559
+ quoteIdent as quoteIdent3,
546
560
  resolveAtomicOp,
547
561
  toAtomicOp,
562
+ transformSecretFieldsForWrite as transformSecretFieldsForWrite2,
548
563
  validateRecord as validateRecord2
549
564
  } from "@korajs/core";
550
565
  async function executeUpdate(ctx, id, data) {
551
566
  const currentRows = await ctx.adapter.query(
552
- `SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
567
+ `SELECT * FROM ${quoteIdent3(ctx.collection)} WHERE id = ? AND _deleted = 0`,
553
568
  [id]
554
569
  );
555
570
  const currentRow = currentRows[0];
@@ -581,6 +596,11 @@ async function executeUpdate(ctx, id, data) {
581
596
  resolvedData[key] = value;
582
597
  }
583
598
  }
599
+ const writeData = await transformSecretFieldsForWrite2(
600
+ resolvedData,
601
+ ctx.definition,
602
+ ctx.secretKeyProvider
603
+ );
584
604
  const hasAtomicOps = Object.keys(atomicOps).length > 0;
585
605
  const causalDeps = resolveCausalDeps(ctx);
586
606
  let operation;
@@ -595,7 +615,7 @@ async function executeUpdate(ctx, id, data) {
595
615
  // Binary richtext values (new and previous) are tagged as canonical
596
616
  // JSON BEFORE the operation is content-hashed, so the hash input,
597
617
  // persisted JSON, and wire payload are the identical value.
598
- data: encodeRichtextFieldsForOpData(resolvedData, ctx.definition.fields),
618
+ data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
599
619
  previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
600
620
  sequenceNumber,
601
621
  causalDeps,
@@ -605,7 +625,7 @@ async function executeUpdate(ctx, id, data) {
605
625
  ctx.clock
606
626
  );
607
627
  ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
608
- const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
628
+ const serializedChanges = serializeRecord(writeData, ctx.definition.fields);
609
629
  const version = serializeRowVersion(operation.timestamp);
610
630
  const mergedFieldVersions = parseFieldVersions(currentRow._field_versions);
611
631
  for (const changedField of Object.keys(serializedChanges)) {
@@ -626,7 +646,7 @@ async function executeUpdate(ctx, id, data) {
626
646
  });
627
647
  ctx.onMutation(ctx.collection, operation);
628
648
  const rows = await ctx.adapter.query(
629
- `SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
649
+ `SELECT * FROM ${quoteIdent3(ctx.collection)} WHERE id = ? AND _deleted = 0`,
630
650
  [id]
631
651
  );
632
652
  const row = rows[0];
@@ -664,4 +684,4 @@ export {
664
684
  validateUpdateStateMachine,
665
685
  executeUpdate
666
686
  };
667
- //# sourceMappingURL=chunk-OUQDC5Z6.js.map
687
+ //# sourceMappingURL=chunk-2VRPF25R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lww/row-version.ts","../src/query/sql-builder.ts","../src/mutations/resolve-causal-deps.ts","../src/mutations/execute-delete.ts","../src/store/sequence-allocator.ts","../src/mutations/execute-insert.ts","../src/lww/field-versions.ts","../src/state-machine/state-validator.ts","../src/mutations/execute-update.ts"],"sourcesContent":["import { type HLCTimestamp, HybridLogicalClock } from '@korajs/core'\nimport type { RawCollectionRow } from '../types'\n\n/** Empty _version means legacy rows (compare by wallTime only via fallback). */\nexport const EMPTY_ROW_VERSION = ''\n\n/**\n * Serialize an HLC timestamp for storage in the `_version` column.\n * Lexicographic order matches {@link HybridLogicalClock.compare}.\n */\nexport function serializeRowVersion(timestamp: HLCTimestamp): string {\n\treturn HybridLogicalClock.serialize(timestamp)\n}\n\n/**\n * Read the version stamp stored on a materialized row.\n * Legacy rows without `_version` fall back to wallTime with logical 0.\n */\nexport function rowVersionFromRecord(\n\trow: Pick<RawCollectionRow, '_updated_at' | '_version'>,\n): HLCTimestamp {\n\tconst version = row._version\n\tif (typeof version === 'string' && version.length > 0) {\n\t\treturn HybridLogicalClock.deserialize(version)\n\t}\n\treturn { wallTime: row._updated_at, logical: 0, nodeId: '' }\n}\n\n/**\n * Returns true when `incoming` is strictly newer than the version on `row`.\n */\nexport function isIncomingNewerThanRow(\n\tincoming: HLCTimestamp,\n\trow: Pick<RawCollectionRow, '_updated_at' | '_version'>,\n): boolean {\n\treturn HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0\n}\n\n/**\n * SQL fragment and params for LWW guards: apply only when row is missing or older.\n * Uses serialized `_version` for total order (matches HLC compare).\n */\nexport function lwwVersionWhereClause(remoteVersion: string): { sql: string; params: string[] } {\n\treturn {\n\t\tsql: '(_version = ? OR _version < ?)',\n\t\tparams: [EMPTY_ROW_VERSION, remoteVersion],\n\t}\n}\n","import type { CollectionDefinition, FieldDescriptor } from '@korajs/core'\nimport { quoteIdent } from '@korajs/core'\nimport { QueryError } from '../errors'\nimport { lwwVersionWhereClause } from '../lww/row-version'\nimport type { QueryDescriptor, WhereOperators } from '../types'\n\n/**\n * Result of building a SQL query: the parameterized SQL string and its bound values.\n */\nexport interface SqlQuery {\n\tsql: string\n\tparams: unknown[]\n}\n\n/**\n * Build a SELECT query from a QueryDescriptor.\n * Automatically adds `WHERE _deleted = 0` to exclude soft-deleted records.\n *\n * @param descriptor - The query descriptor\n * @param fields - The field descriptors from the collection schema\n * @returns A parameterized SQL query\n */\nexport function buildSelectQuery(\n\tdescriptor: QueryDescriptor,\n\tfields: Record<string, FieldDescriptor>,\n): SqlQuery {\n\tconst params: unknown[] = []\n\tconst parts = [`SELECT * FROM ${quoteIdent(descriptor.collection)}`]\n\n\tconst whereClause = buildWhereClauseParts(descriptor.where, fields, params)\n\t// Always filter out soft-deleted records\n\tconst deletedFilter = '_deleted = 0'\n\tif (whereClause) {\n\t\tparts.push(`WHERE ${deletedFilter} AND ${whereClause}`)\n\t} else {\n\t\tparts.push(`WHERE ${deletedFilter}`)\n\t}\n\n\tif (descriptor.orderBy.length > 0) {\n\t\tconst orderParts = descriptor.orderBy.map((o) => {\n\t\t\tvalidateFieldName(o.field, fields)\n\t\t\treturn `${quoteIdent(o.field)} ${o.direction.toUpperCase()}`\n\t\t})\n\t\tparts.push(`ORDER BY ${orderParts.join(', ')}`)\n\t}\n\n\tif (descriptor.limit !== undefined) {\n\t\tparts.push(`LIMIT ${descriptor.limit}`)\n\t}\n\n\tif (descriptor.offset !== undefined) {\n\t\tparts.push(`OFFSET ${descriptor.offset}`)\n\t}\n\n\treturn { sql: parts.join(' '), params }\n}\n\n/**\n * Build a COUNT query from a QueryDescriptor.\n * Automatically adds `WHERE _deleted = 0`.\n *\n * @param descriptor - The query descriptor\n * @param fields - The field descriptors from the collection schema\n * @returns A parameterized SQL query that returns { count: number }\n */\nexport function buildCountQuery(\n\tdescriptor: QueryDescriptor,\n\tfields: Record<string, FieldDescriptor>,\n): SqlQuery {\n\tconst params: unknown[] = []\n\tconst parts = [`SELECT COUNT(*) as count FROM ${quoteIdent(descriptor.collection)}`]\n\n\tconst whereClause = buildWhereClauseParts(descriptor.where, fields, params)\n\tconst deletedFilter = '_deleted = 0'\n\tif (whereClause) {\n\t\tparts.push(`WHERE ${deletedFilter} AND ${whereClause}`)\n\t} else {\n\t\tparts.push(`WHERE ${deletedFilter}`)\n\t}\n\n\treturn { sql: parts.join(' '), params }\n}\n\n/**\n * Build an INSERT query for a collection record.\n *\n * @param collection - The collection name\n * @param record - The record data (already serialized with id, _created_at, _updated_at)\n * @returns A parameterized SQL query\n */\nexport function buildInsertQuery(collection: string, record: Record<string, unknown>): SqlQuery {\n\tconst columns = Object.keys(record)\n\tconst placeholders = columns.map(() => '?')\n\tconst params = Object.values(record)\n\n\tconst quotedColumns = columns.map((col) => quoteIdent(col))\n\tconst sql = `INSERT INTO ${quoteIdent(collection)} (${quotedColumns.join(', ')}) VALUES (${placeholders.join(', ')})`\n\treturn { sql, params }\n}\n\n/**\n * Build an UPDATE query for a collection record.\n *\n * @param collection - The collection name\n * @param id - The record ID\n * @param changes - The fields to update (already serialized)\n * @returns A parameterized SQL query\n */\nexport function buildUpdateQuery(\n\tcollection: string,\n\tid: string,\n\tchanges: Record<string, unknown>,\n): SqlQuery {\n\tconst setClauses = Object.keys(changes).map((col) => `${quoteIdent(col)} = ?`)\n\tconst params = [...Object.values(changes), id]\n\n\tconst sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(', ')} WHERE id = ?`\n\treturn { sql, params }\n}\n\n/**\n * Build a field-level fast-forward UPDATE: writes the changed field values\n * unconditionally (the caller has already proven per-field that the local value\n * still equals the base the remote wrote from, so the remote is the newest known\n * writer of those fields), while advancing the row's `_version` / `_updated_at`\n * watermark MONOTONICALLY — never regressing it below its current value.\n *\n * This is what makes per-field LWW correct on a per-row `_version` column: a\n * concurrent edit to a different field may have pushed `_version` past this\n * operation's timestamp, but that must not cause this operation's own field\n * change to be dropped, nor may materializing it move the watermark backward\n * (which would let a genuinely stale later operation win).\n *\n * @param collection - The collection name\n * @param id - The record ID\n * @param fieldChanges - Serialized field values to write unconditionally (must NOT include _version / _updated_at)\n * @param remoteVersion - The operation's serialized row version (lexicographically comparable)\n * @param wallTime - The operation's wall-clock time in ms\n * @param options - Optional extras. `maxCreatedAt` advances `_created_at` to the\n * greater of its current value and the given wall time — used for insert\n * collisions so every node converges on the max insert wall time regardless\n * of arrival order.\n */\nexport function buildFieldFastForwardUpdateQuery(\n\tcollection: string,\n\tid: string,\n\tfieldChanges: Record<string, unknown>,\n\tremoteVersion: string,\n\twallTime: number,\n\toptions?: { maxCreatedAt?: number },\n): SqlQuery {\n\tconst fieldClauses = Object.keys(fieldChanges).map((col) => `${quoteIdent(col)} = ?`)\n\t// _version is a lexicographically-sortable string; _updated_at is a number.\n\t// Keep the greater of the current and incoming value for each (monotonic).\n\tconst setClauses = [\n\t\t...fieldClauses,\n\t\t'_version = CASE WHEN _version >= ? THEN _version ELSE ? END',\n\t\t'_updated_at = CASE WHEN _updated_at >= ? THEN _updated_at ELSE ? END',\n\t]\n\tconst params: unknown[] = [\n\t\t...Object.values(fieldChanges),\n\t\tremoteVersion,\n\t\tremoteVersion,\n\t\twallTime,\n\t\twallTime,\n\t]\n\tif (options?.maxCreatedAt !== undefined) {\n\t\tsetClauses.push('_created_at = CASE WHEN _created_at >= ? THEN _created_at ELSE ? END')\n\t\tparams.push(options.maxCreatedAt, options.maxCreatedAt)\n\t}\n\tparams.push(id)\n\tconst sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(', ')} WHERE id = ?`\n\treturn { sql, params }\n}\n\n/**\n * Build a soft-delete query (SET _deleted = 1).\n *\n * @param collection - The collection name\n * @param id - The record ID\n * @param updatedAt - The timestamp to set on _updated_at\n * @returns A parameterized SQL query\n */\nexport function buildSoftDeleteQuery(\n\tcollection: string,\n\tid: string,\n\tupdatedAt: number,\n\tversion?: string,\n): SqlQuery {\n\tif (version !== undefined) {\n\t\treturn {\n\t\t\tsql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ?`,\n\t\t\tparams: [updatedAt, version, id],\n\t\t}\n\t}\n\treturn {\n\t\tsql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ? WHERE id = ?`,\n\t\tparams: [updatedAt, id],\n\t}\n}\n\n/**\n * Build an UPDATE that applies only when the row is missing or older than `remoteVersion`\n * (serialized HLC). Prevents stale remote sync from overwriting newer local materialized state.\n */\nexport function buildLwwUpdateQuery(\n\tcollection: string,\n\tid: string,\n\tchanges: Record<string, unknown>,\n\tremoteVersion: string,\n): SqlQuery {\n\tconst setClauses = Object.keys(changes).map((col) => `${quoteIdent(col)} = ?`)\n\tconst lww = lwwVersionWhereClause(remoteVersion)\n\tconst sql = `UPDATE ${quoteIdent(collection)} SET ${setClauses.join(', ')} WHERE id = ? AND ${lww.sql}`\n\tconst params = [...Object.values(changes), id, ...lww.params]\n\treturn { sql, params }\n}\n\n/**\n * Build a soft-delete that applies only when the row is older than `remoteVersion`.\n */\nexport function buildLwwSoftDeleteQuery(\n\tcollection: string,\n\tid: string,\n\tupdatedAt: number,\n\tversion: string,\n): SqlQuery {\n\tconst lww = lwwVersionWhereClause(version)\n\treturn {\n\t\tsql: `UPDATE ${quoteIdent(collection)} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ? AND ${lww.sql}`,\n\t\tparams: [updatedAt, version, id, ...lww.params],\n\t}\n}\n\n/**\n * Build a WHERE clause from conditions, validating field names against the schema.\n *\n * @param where - The where conditions\n * @param fields - The field descriptors from the collection schema\n * @returns The SQL WHERE clause string and params, or null if no conditions\n */\nexport function buildWhereClause(\n\twhere: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n): SqlQuery | null {\n\tconst params: unknown[] = []\n\tconst result = buildWhereClauseParts(where, fields, params)\n\tif (!result) return null\n\treturn { sql: result, params }\n}\n\n// --- Internal helpers ---\n\nconst VALID_OPERATORS = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in'])\n\nfunction buildWhereClauseParts(\n\twhere: Record<string, unknown>,\n\tfields: Record<string, FieldDescriptor>,\n\tparams: unknown[],\n): string | null {\n\tconst conditions: string[] = []\n\n\tfor (const [fieldName, value] of Object.entries(where)) {\n\t\tvalidateFieldName(fieldName, fields)\n\t\tconst descriptor = fields[fieldName]\n\n\t\tif (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\t// Operator object: { $gt: 5, $lt: 10 }\n\t\t\tconst ops = value as WhereOperators\n\t\t\tfor (const [op, opValue] of Object.entries(ops)) {\n\t\t\t\tif (!VALID_OPERATORS.has(op)) {\n\t\t\t\t\tthrow new QueryError(`Unknown operator \"${op}\" on field \"${fieldName}\"`, {\n\t\t\t\t\t\tfield: fieldName,\n\t\t\t\t\t\toperator: op,\n\t\t\t\t\t\tvalidOperators: [...VALID_OPERATORS],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tconditions.push(buildOperatorCondition(fieldName, op, opValue, descriptor, params))\n\t\t\t}\n\t\t} else {\n\t\t\t// Shorthand: { completed: false } means { completed: { $eq: false } }\n\t\t\tconditions.push(buildOperatorCondition(fieldName, '$eq', value, descriptor, params))\n\t\t}\n\t}\n\n\tif (conditions.length === 0) return null\n\treturn conditions.join(' AND ')\n}\n\nfunction buildOperatorCondition(\n\tfieldName: string,\n\toperator: string,\n\tvalue: unknown,\n\tdescriptor: FieldDescriptor | undefined,\n\tparams: unknown[],\n): string {\n\t// Serialize boolean values to 0/1 for SQL comparison\n\tconst sqlValue =\n\t\tdescriptor?.kind === 'boolean' && typeof value === 'boolean' ? (value ? 1 : 0) : value\n\n\tconst column = quoteIdent(fieldName)\n\n\tswitch (operator) {\n\t\tcase '$eq':\n\t\t\tif (sqlValue === null) {\n\t\t\t\treturn `${column} IS NULL`\n\t\t\t}\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${column} = ?`\n\t\tcase '$ne':\n\t\t\tif (sqlValue === null) {\n\t\t\t\treturn `${column} IS NOT NULL`\n\t\t\t}\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${column} != ?`\n\t\tcase '$gt':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${column} > ?`\n\t\tcase '$gte':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${column} >= ?`\n\t\tcase '$lt':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${column} < ?`\n\t\tcase '$lte':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${column} <= ?`\n\t\tcase '$in': {\n\t\t\tif (!Array.isArray(sqlValue)) {\n\t\t\t\tthrow new QueryError(`$in operator requires an array value for field \"${fieldName}\"`, {\n\t\t\t\t\tfield: fieldName,\n\t\t\t\t\treceived: typeof sqlValue,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst placeholders = sqlValue.map(() => '?')\n\t\t\tfor (const item of sqlValue) {\n\t\t\t\tparams.push(\n\t\t\t\t\tdescriptor?.kind === 'boolean' && typeof item === 'boolean' ? (item ? 1 : 0) : item,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn `${column} IN (${placeholders.join(', ')})`\n\t\t}\n\t\tdefault:\n\t\t\tthrow new QueryError(`Unknown operator \"${operator}\"`, { operator })\n\t}\n}\n\nfunction validateFieldName(fieldName: string, fields: Record<string, FieldDescriptor>): void {\n\t// Allow schema fields plus metadata fields that map to query-able columns\n\tconst allowedFields = new Set([\n\t\t...Object.keys(fields),\n\t\t'id',\n\t\t'createdAt',\n\t\t'updatedAt',\n\t\t'_created_at',\n\t\t'_updated_at',\n\t])\n\tif (!allowedFields.has(fieldName)) {\n\t\tthrow new QueryError(\n\t\t\t`Unknown field \"${fieldName}\" in query. Available fields: ${[...allowedFields].join(', ')}`,\n\t\t\t{ field: fieldName },\n\t\t)\n\t}\n}\n","import type { LocalMutationContext } from './types'\n\n/**\n * Merge explicit parent deps (e.g. referential cascade) with tracker-derived deps.\n */\nexport function resolveCausalDeps(ctx: LocalMutationContext): string[] {\n\tconst merged: string[] = []\n\tfor (const id of ctx.extraCausalDeps ?? []) {\n\t\tif (!merged.includes(id)) {\n\t\t\tmerged.push(id)\n\t\t}\n\t}\n\tfor (const id of ctx.causalTracker?.nextCausalDeps(ctx.collection, ctx.inTransaction) ?? []) {\n\t\tif (!merged.includes(id)) {\n\t\t\tmerged.push(id)\n\t\t}\n\t}\n\treturn merged\n}\n","import { createOperation, quoteIdent } from '@korajs/core'\nimport type { Operation } from '@korajs/core'\nimport { RecordNotFoundError } from '../errors'\nimport { serializeRowVersion } from '../lww/row-version'\nimport { buildInsertQuery, buildSoftDeleteQuery } from '../query/sql-builder'\nimport { serializeOperation } from '../serialization/serializer'\nimport { allocateNextSequenceInTransaction } from '../store/sequence-allocator'\nimport type { RawCollectionRow } from '../types'\nimport { resolveCausalDeps } from './resolve-causal-deps'\nimport type { LocalMutationContext } from './types'\n\nexport interface ExecuteDeleteOptions {\n\t/** When true, skip RelationEnforcer (caller already enforced referential integrity). */\n\tskipReferentialEnforcement?: boolean\n\t/** Pre-built delete operation (avoids duplicate sequence allocation). */\n\toperation?: Operation\n}\n\n/**\n * Soft-delete a record and persist the operation log entry atomically.\n * Returns the primary delete operation and any cascaded side-effect operations.\n */\nexport async function executeDelete(\n\tctx: LocalMutationContext,\n\tid: string,\n\toptions?: ExecuteDeleteOptions,\n): Promise<Operation[]> {\n\tconst currentRows = await ctx.adapter.query<RawCollectionRow>(\n\t\t`SELECT * FROM ${quoteIdent(ctx.collection)} WHERE id = ? AND _deleted = 0`,\n\t\t[id],\n\t)\n\tif (!currentRows[0]) {\n\t\tthrow new RecordNotFoundError(ctx.collection, id)\n\t}\n\n\tconst causalDeps = resolveCausalDeps(ctx)\n\tlet operation = options?.operation\n\tconst cascadedOps: Operation[] = []\n\n\tawait ctx.adapter.transaction(async (tx) => {\n\t\tif (!operation) {\n\t\t\tconst sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId)\n\t\t\toperation = await createOperation(\n\t\t\t\t{\n\t\t\t\t\tnodeId: ctx.nodeId,\n\t\t\t\t\ttype: 'delete',\n\t\t\t\t\tcollection: ctx.collection,\n\t\t\t\t\trecordId: id,\n\t\t\t\t\tdata: null,\n\t\t\t\t\tpreviousData: null,\n\t\t\t\t\tsequenceNumber,\n\t\t\t\t\tcausalDeps,\n\t\t\t\t\tschemaVersion: ctx.schema.version,\n\t\t\t\t},\n\t\t\t\tctx.clock,\n\t\t\t)\n\t\t\tctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction)\n\t\t}\n\n\t\tif (ctx.relationEnforcer && !options?.skipReferentialEnforcement) {\n\t\t\tconst enforcementResult = await ctx.relationEnforcer.enforceDelete(ctx.collection, id, tx, [\n\t\t\t\toperation.id,\n\t\t\t])\n\t\t\tcascadedOps.push(...enforcementResult.operations)\n\t\t}\n\n\t\tconst version = serializeRowVersion(operation.timestamp)\n\t\tconst deleteQuery = buildSoftDeleteQuery(\n\t\t\tctx.collection,\n\t\t\tid,\n\t\t\toperation.timestamp.wallTime,\n\t\t\tversion,\n\t\t)\n\t\tconst opInsert = buildInsertQuery(\n\t\t\t`_kora_ops_${ctx.collection}`,\n\t\t\tserializeOperation(operation) as unknown as Record<string, unknown>,\n\t\t)\n\n\t\tawait tx.execute(deleteQuery.sql, deleteQuery.params)\n\t\tawait tx.execute(opInsert.sql, opInsert.params)\n\t})\n\n\tif (!operation) {\n\t\tthrow new Error('Delete operation was not created')\n\t}\n\n\tctx.onMutation(ctx.collection, operation)\n\tfor (const cascadedOp of cascadedOps) {\n\t\tctx.onMutation(cascadedOp.collection, cascadedOp)\n\t}\n\n\treturn [operation, ...cascadedOps]\n}\n","import type { StorageAdapter, Transaction } from '../types'\n\ninterface SequenceRow {\n\tsequence_number: number\n}\n\n/**\n * Atomically increment and return the next sequence number for a node.\n * Uses SQLite UPSERT + RETURNING so concurrent tabs do not reuse sequence slots.\n */\nexport async function allocateNextSequenceNumber(\n\tadapter: StorageAdapter,\n\tnodeId: string,\n): Promise<number> {\n\tconst rows = await adapter.query<SequenceRow>(\n\t\t`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)\n ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1\n RETURNING sequence_number`,\n\t\t[nodeId],\n\t)\n\tconst seq = rows[0]?.sequence_number\n\tif (seq === undefined) {\n\t\tthrow new Error(`Failed to allocate sequence number for node \"${nodeId}\"`)\n\t}\n\treturn seq\n}\n\n/**\n * Allocate the next sequence inside an existing storage transaction.\n */\nexport async function allocateNextSequenceInTransaction(\n\ttx: Transaction,\n\tnodeId: string,\n): Promise<number> {\n\tconst rows = await tx.query<SequenceRow>(\n\t\t`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)\n ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1\n RETURNING sequence_number`,\n\t\t[nodeId],\n\t)\n\tconst seq = rows[0]?.sequence_number\n\tif (seq === undefined) {\n\t\tthrow new Error(`Failed to allocate sequence number for node \"${nodeId}\" in transaction`)\n\t}\n\treturn seq\n}\n\n/**\n * Read the current sequence for a node without incrementing.\n */\nexport async function readSequenceNumber(adapter: StorageAdapter, nodeId: string): Promise<number> {\n\tconst rows = await adapter.query<SequenceRow>(\n\t\t'SELECT sequence_number FROM _kora_version_vector WHERE node_id = ?',\n\t\t[nodeId],\n\t)\n\treturn rows[0]?.sequence_number ?? 0\n}\n","import {\n\tcreateOperation,\n\tgenerateUUIDv7,\n\ttransformSecretFieldsForWrite,\n\tvalidateRecord,\n} from '@korajs/core'\nimport { fieldVersionsForFields, serializeFieldVersions } from '../lww/field-versions'\nimport { serializeRowVersion } from '../lww/row-version'\nimport { buildInsertQuery } from '../query/sql-builder'\nimport { encodeRichtextFieldsForOpData } from '../serialization/op-data-encoding'\nimport { serializeOperation, serializeRecord } from '../serialization/serializer'\nimport { allocateNextSequenceInTransaction } from '../store/sequence-allocator'\nimport type { CollectionRecord } from '../types'\nimport { resolveCausalDeps } from './resolve-causal-deps'\nimport type { LocalMutationContext } from './types'\n\n/**\n * Insert a record and persist the operation log entry atomically.\n */\nexport async function executeInsert(\n\tctx: LocalMutationContext,\n\tdata: Record<string, unknown>,\n): Promise<CollectionRecord> {\n\tconst validated = validateRecord(ctx.collection, ctx.definition, data, 'insert')\n\tconst recordId = generateUUIDv7()\n\n\tfor (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {\n\t\tif (descriptor.auto && descriptor.kind === 'timestamp') {\n\t\t\tvalidated[fieldName] = Date.now()\n\t\t}\n\t}\n\n\t// Transform secret fields to their at-rest form (hash/ciphertext) BEFORE the\n\t// operation is built, so plaintext never enters the op log, store, or sync.\n\tconst writeData = await transformSecretFieldsForWrite(\n\t\tvalidated,\n\t\tctx.definition,\n\t\tctx.secretKeyProvider,\n\t)\n\n\tconst causalDeps = resolveCausalDeps(ctx)\n\n\tlet operation!: Awaited<ReturnType<typeof createOperation>>\n\tlet record!: Record<string, unknown>\n\n\tawait ctx.adapter.transaction(async (tx) => {\n\t\tconst sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId)\n\t\toperation = await createOperation(\n\t\t\t{\n\t\t\t\tnodeId: ctx.nodeId,\n\t\t\t\ttype: 'insert',\n\t\t\t\tcollection: ctx.collection,\n\t\t\t\trecordId,\n\t\t\t\t// Binary richtext values are tagged as canonical JSON BEFORE the\n\t\t\t\t// operation is content-hashed, so the hash input, persisted JSON,\n\t\t\t\t// and wire payload are the identical value.\n\t\t\t\tdata: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),\n\t\t\t\tpreviousData: null,\n\t\t\t\tsequenceNumber,\n\t\t\t\tcausalDeps,\n\t\t\t\tschemaVersion: ctx.schema.version,\n\t\t\t},\n\t\t\tctx.clock,\n\t\t)\n\t\tctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction)\n\n\t\tconst serializedData = serializeRecord(writeData, ctx.definition.fields)\n\t\tconst version = serializeRowVersion(operation.timestamp)\n\t\trecord = {\n\t\t\tid: recordId,\n\t\t\t...serializedData,\n\t\t\t_created_at: operation.timestamp.wallTime,\n\t\t\t_updated_at: operation.timestamp.wallTime,\n\t\t\t_version: version,\n\t\t\t// Stamp every inserted field with this operation's version so later\n\t\t\t// per-field LWW compares against a real writer, not the fallback.\n\t\t\t_field_versions: serializeFieldVersions(\n\t\t\t\tfieldVersionsForFields(Object.keys(serializedData), version),\n\t\t\t),\n\t\t}\n\n\t\tconst builtInsert = buildInsertQuery(ctx.collection, record)\n\t\tconst opInsert = buildInsertQuery(\n\t\t\t`_kora_ops_${ctx.collection}`,\n\t\t\tserializeOperation(operation) as unknown as Record<string, unknown>,\n\t\t)\n\n\t\tawait tx.execute(builtInsert.sql, builtInsert.params)\n\t\tawait tx.execute(opInsert.sql, opInsert.params)\n\t})\n\n\tctx.onMutation(ctx.collection, operation)\n\n\treturn {\n\t\tid: recordId,\n\t\t...writeData,\n\t\tcreatedAt: operation.timestamp.wallTime,\n\t\tupdatedAt: operation.timestamp.wallTime,\n\t}\n}\n","import { EMPTY_ROW_VERSION } from './row-version'\n\n/**\n * Per-field last-writer versions for a materialized row.\n *\n * Each entry maps a field name to a serialized HLC timestamp (the same\n * lexicographically-sortable form used by the row `_version` column). Because\n * the serialized form sorts identically to {@link HybridLogicalClock.compare},\n * per-field last-write-wins is a plain string comparison: the greater string is\n * the newer writer. This makes field-level convergence deterministic and\n * independent of the order operations happen to arrive in.\n */\nexport type FieldVersions = Record<string, string>\n\n/**\n * Parse the `_field_versions` JSON column into a map. Tolerates null, empty,\n * and malformed values by returning an empty map — a row that predates\n * per-field tracking simply has no field versions and falls back to `_version`.\n */\nexport function parseFieldVersions(raw: unknown): FieldVersions {\n\tif (typeof raw !== 'string' || raw.length === 0) {\n\t\treturn {}\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as unknown\n\t\tif (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n\t\t\treturn {}\n\t\t}\n\t\tconst result: FieldVersions = {}\n\t\tfor (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {\n\t\t\tif (typeof value === 'string') {\n\t\t\t\tresult[key] = value\n\t\t\t}\n\t\t}\n\t\treturn result\n\t} catch {\n\t\treturn {}\n\t}\n}\n\n/**\n * Serialize a field-version map for storage in the `_field_versions` column.\n */\nexport function serializeFieldVersions(versions: FieldVersions): string {\n\treturn JSON.stringify(versions)\n}\n\n/**\n * Build the initial field-version map for an insert: every supplied field is\n * stamped with the operation's version.\n */\nexport function fieldVersionsForFields(\n\tfieldNames: Iterable<string>,\n\tversion: string,\n): FieldVersions {\n\tconst versions: FieldVersions = {}\n\tfor (const field of fieldNames) {\n\t\tversions[field] = version\n\t}\n\treturn versions\n}\n\n/**\n * The effective stored version for a field: its own tracked version if present,\n * otherwise the row-level `_version` (so rows written before per-field tracking\n * still compare correctly), otherwise the empty sentinel (older than anything).\n */\nexport function effectiveFieldVersion(\n\tcurrent: FieldVersions,\n\tfield: string,\n\trowVersion: string | undefined,\n): string {\n\tconst own = current[field]\n\tif (own !== undefined) {\n\t\treturn own\n\t}\n\treturn rowVersion ?? EMPTY_ROW_VERSION\n}\n\n/**\n * Result of resolving an incoming operation against the current per-field\n * versions of a row.\n */\nexport interface PerFieldLwwResult {\n\t/** Fields the incoming operation wins (strictly newer than the stored version). */\n\twinners: string[]\n\t/** The merged field-version map to persist (winners advanced to `incomingVersion`). */\n\tmerged: FieldVersions\n}\n\n/**\n * Resolve per-field last-write-wins for an incoming operation.\n *\n * For each incoming field, the operation wins only when its version is strictly\n * greater than the field's effective stored version. Ties (identical version,\n * i.e. the same operation re-applied) are losses, keeping the resolution\n * idempotent. The comparison is a total order, so every node resolves the same\n * winner regardless of the order operations arrive.\n *\n * @param current - Parsed `_field_versions` of the target row\n * @param incomingFields - Field names carried by the incoming operation\n * @param incomingVersion - Serialized HLC version of the incoming operation\n * @param rowVersion - The row's `_version` (fallback for untracked fields)\n */\nexport function resolvePerFieldLww(\n\tcurrent: FieldVersions,\n\tincomingFields: Iterable<string>,\n\tincomingVersion: string,\n\trowVersion: string | undefined,\n): PerFieldLwwResult {\n\tconst winners: string[] = []\n\tconst merged: FieldVersions = { ...current }\n\tfor (const field of incomingFields) {\n\t\tconst stored = effectiveFieldVersion(current, field, rowVersion)\n\t\tif (incomingVersion > stored) {\n\t\t\twinners.push(field)\n\t\t\tmerged[field] = incomingVersion\n\t\t}\n\t\t// Losing fields are left untouched: their existing tracked version (or the\n\t\t// row-level `_version` fallback) already reflects the newer writer.\n\t}\n\treturn { winners, merged }\n}\n","import { KoraError, validateTransition } from '@korajs/core'\nimport type {\n\tCollectionDefinition,\n\tStateMachineConstraint,\n\tStateMachineDefinition,\n} from '@korajs/core'\n\n/**\n * Error thrown when a local mutation attempts an invalid state transition.\n *\n * Contains enough context to debug without reproduction:\n * the collection, record, field, current state, attempted state, and allowed transitions.\n */\nexport class InvalidStateTransitionError extends KoraError {\n\tconstructor(\n\t\tpublic readonly collection: string,\n\t\tpublic readonly recordId: string,\n\t\tpublic readonly field: string,\n\t\tpublic readonly fromState: string,\n\t\tpublic readonly toState: string,\n\t\tpublic readonly allowedStates: string[],\n\t) {\n\t\tsuper(\n\t\t\t`Invalid state transition in collection \"${collection}\": ` +\n\t\t\t\t`cannot transition field \"${field}\" from \"${fromState}\" to \"${toState}\". ` +\n\t\t\t\t`Allowed transitions from \"${fromState}\": ${allowedStates.length > 0 ? allowedStates.join(', ') : '(none -- terminal state)'}`,\n\t\t\t'INVALID_STATE_TRANSITION',\n\t\t\t{ collection, recordId, field, fromState, toState, allowedStates },\n\t\t)\n\t\tthis.name = 'InvalidStateTransitionError'\n\t}\n}\n\n/**\n * Validates a state machine transition for a local mutation (insert or update).\n *\n * For inserts: validates that the initial state (from the data or the default value)\n * is a known state in the state machine.\n *\n * For updates: looks up the current state field value and checks whether\n * the transition to the new value is allowed.\n *\n * @param collectionName - Name of the collection\n * @param recordId - The record being mutated\n * @param stateMachine - The state machine definition\n * @param currentState - The current value of the state field (null for inserts)\n * @param newState - The new value being set for the state field\n * @returns An object indicating whether the transition is valid, and if not, the allowed states.\n * When `onInvalidTransition` is 'last-valid-state', callers should suppress the field update\n * rather than throwing.\n */\nexport function validateStateTransition(\n\tcollectionName: string,\n\trecordId: string,\n\tstateMachine: StateMachineDefinition,\n\tcurrentState: string | null,\n\tnewState: string,\n): { valid: boolean; allowedStates: string[] } {\n\t// For inserts, any valid enum value is acceptable as the initial state\n\t// (schema validation already ensures the value is a valid enum value)\n\tif (currentState === null) {\n\t\treturn { valid: true, allowedStates: [] }\n\t}\n\n\t// Same-state transitions are always valid (idempotent updates)\n\tif (currentState === newState) {\n\t\treturn { valid: true, allowedStates: stateMachine.transitions[currentState] ?? [] }\n\t}\n\n\tconst constraint: StateMachineConstraint = {\n\t\tfield: stateMachine.field,\n\t\tcollection: collectionName,\n\t\ttransitions: stateMachine.transitions,\n\t}\n\tconst transitionResult = validateTransition(constraint, currentState, newState)\n\tif (transitionResult.valid) {\n\t\treturn { valid: true, allowedStates: transitionResult.allowedTargets }\n\t}\n\n\tconst allowedStates = transitionResult.allowedTargets\n\n\tif (stateMachine.onInvalidTransition === 'reject') {\n\t\tthrow new InvalidStateTransitionError(\n\t\t\tcollectionName,\n\t\t\trecordId,\n\t\t\tstateMachine.field,\n\t\t\tcurrentState,\n\t\t\tnewState,\n\t\t\tallowedStates,\n\t\t)\n\t}\n\n\t// 'last-valid-state': return invalid so the caller can suppress the field update\n\treturn { valid: false, allowedStates }\n}\n\n/**\n * Checks whether a collection update data object contains a change to the state machine field,\n * and if so, validates the transition.\n *\n * If the state field is not in the update data, returns the data unchanged.\n * If the transition is invalid and mode is 'last-valid-state', removes the field from the data.\n * If the transition is invalid and mode is 'reject', throws InvalidStateTransitionError.\n *\n * @param collectionName - Name of the collection\n * @param recordId - The record being updated\n * @param collectionDef - The collection definition from the schema\n * @param currentRecord - The current record data (must include the state field)\n * @param updateData - The partial update data\n * @returns The update data, potentially with the state field removed if invalid and mode is 'last-valid-state'\n */\nexport function validateUpdateStateMachine(\n\tcollectionName: string,\n\trecordId: string,\n\tcollectionDef: CollectionDefinition,\n\tcurrentRecord: Record<string, unknown>,\n\tupdateData: Record<string, unknown>,\n): Record<string, unknown> {\n\tconst stateMachine = collectionDef.stateMachine\n\tif (stateMachine === undefined) {\n\t\treturn updateData\n\t}\n\n\tconst stateField = stateMachine.field\n\tif (!(stateField in updateData)) {\n\t\t// State field not being changed -- no validation needed\n\t\treturn updateData\n\t}\n\n\tconst currentState = currentRecord[stateField]\n\tconst newState = updateData[stateField]\n\n\t// Both values must be strings for state machine validation\n\tif (typeof currentState !== 'string' || typeof newState !== 'string') {\n\t\treturn updateData\n\t}\n\n\tconst result = validateStateTransition(\n\t\tcollectionName,\n\t\trecordId,\n\t\tstateMachine,\n\t\tcurrentState,\n\t\tnewState,\n\t)\n\n\tif (result.valid) {\n\t\treturn updateData\n\t}\n\n\t// Mode is 'last-valid-state': silently remove the state field from the update\n\tconst filtered = { ...updateData }\n\tdelete filtered[stateField]\n\n\t// If no fields remain after removing the state field, the update becomes a no-op.\n\t// Return the empty object -- the caller can decide whether to proceed.\n\treturn filtered\n}\n","import type { AtomicOp } from '@korajs/core'\nimport {\n\tcreateOperation,\n\tisAtomicOp,\n\tquoteIdent,\n\tresolveAtomicOp,\n\ttoAtomicOp,\n\ttransformSecretFieldsForWrite,\n\tvalidateRecord,\n} from '@korajs/core'\nimport { RecordNotFoundError } from '../errors'\nimport { parseFieldVersions, serializeFieldVersions } from '../lww/field-versions'\nimport { serializeRowVersion } from '../lww/row-version'\nimport { buildInsertQuery, buildUpdateQuery } from '../query/sql-builder'\nimport { encodeRichtextFieldsForOpData } from '../serialization/op-data-encoding'\nimport { deserializeRecord, serializeOperation, serializeRecord } from '../serialization/serializer'\nimport { validateUpdateStateMachine } from '../state-machine/state-validator'\nimport { allocateNextSequenceInTransaction } from '../store/sequence-allocator'\nimport type { CollectionRecord, RawCollectionRow } from '../types'\nimport { resolveCausalDeps } from './resolve-causal-deps'\nimport type { LocalMutationContext } from './types'\n\n/**\n * Update a record and persist the operation log entry atomically.\n */\nexport async function executeUpdate(\n\tctx: LocalMutationContext,\n\tid: string,\n\tdata: Record<string, unknown>,\n): Promise<CollectionRecord> {\n\tconst currentRows = await ctx.adapter.query<RawCollectionRow>(\n\t\t`SELECT * FROM ${quoteIdent(ctx.collection)} WHERE id = ? AND _deleted = 0`,\n\t\t[id],\n\t)\n\tconst currentRow = currentRows[0]\n\tif (!currentRow) {\n\t\tthrow new RecordNotFoundError(ctx.collection, id)\n\t}\n\n\tlet validated = validateRecord(ctx.collection, ctx.definition, data, 'update')\n\tconst currentRecord = deserializeRecord(currentRow, ctx.definition.fields)\n\tvalidated = validateUpdateStateMachine(\n\t\tctx.collection,\n\t\tid,\n\t\tctx.definition,\n\t\tcurrentRecord,\n\t\tvalidated,\n\t)\n\n\tif (Object.keys(validated).length === 0) {\n\t\treturn { ...currentRecord }\n\t}\n\n\tconst previousData: Record<string, unknown> = {}\n\tconst resolvedData: Record<string, unknown> = {}\n\tconst atomicOps: Record<string, AtomicOp> = {}\n\n\tfor (const key of Object.keys(validated)) {\n\t\tconst value = validated[key]\n\t\tpreviousData[key] = currentRecord[key]\n\n\t\tif (isAtomicOp(value)) {\n\t\t\tresolvedData[key] = resolveAtomicOp(currentRecord[key], value)\n\t\t\tatomicOps[key] = toAtomicOp(value)\n\t\t} else {\n\t\t\tresolvedData[key] = value\n\t\t}\n\t}\n\n\t// Transform secret fields in the changed set to their at-rest form before the\n\t// operation is built. previousData already holds the stored (ciphertext) value\n\t// from the current record, so it needs no transform.\n\tconst writeData = await transformSecretFieldsForWrite(\n\t\tresolvedData,\n\t\tctx.definition,\n\t\tctx.secretKeyProvider,\n\t)\n\n\tconst hasAtomicOps = Object.keys(atomicOps).length > 0\n\tconst causalDeps = resolveCausalDeps(ctx)\n\tlet operation!: Awaited<ReturnType<typeof createOperation>>\n\n\tawait ctx.adapter.transaction(async (tx) => {\n\t\tconst sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId)\n\t\toperation = await createOperation(\n\t\t\t{\n\t\t\t\tnodeId: ctx.nodeId,\n\t\t\t\ttype: 'update',\n\t\t\t\tcollection: ctx.collection,\n\t\t\t\trecordId: id,\n\t\t\t\t// Binary richtext values (new and previous) are tagged as canonical\n\t\t\t\t// JSON BEFORE the operation is content-hashed, so the hash input,\n\t\t\t\t// persisted JSON, and wire payload are the identical value.\n\t\t\t\tdata: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),\n\t\t\t\tpreviousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),\n\t\t\t\tsequenceNumber,\n\t\t\t\tcausalDeps,\n\t\t\t\tschemaVersion: ctx.schema.version,\n\t\t\t\t...(hasAtomicOps ? { atomicOps } : {}),\n\t\t\t},\n\t\t\tctx.clock,\n\t\t)\n\t\tctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction)\n\n\t\tconst serializedChanges = serializeRecord(writeData, ctx.definition.fields)\n\t\tconst version = serializeRowVersion(operation.timestamp)\n\t\t// A local edit is always the newest writer of the fields it touches (the\n\t\t// HLC advances past every timestamp this device has seen), so stamp each\n\t\t// changed field with this operation's version.\n\t\tconst mergedFieldVersions = parseFieldVersions(currentRow._field_versions)\n\t\tfor (const changedField of Object.keys(serializedChanges)) {\n\t\t\tmergedFieldVersions[changedField] = version\n\t\t}\n\t\tconst updateQuery = buildUpdateQuery(ctx.collection, id, {\n\t\t\t...serializedChanges,\n\t\t\t_updated_at: operation.timestamp.wallTime,\n\t\t\t_version: version,\n\t\t\t_field_versions: serializeFieldVersions(mergedFieldVersions),\n\t\t})\n\t\tconst opInsert = buildInsertQuery(\n\t\t\t`_kora_ops_${ctx.collection}`,\n\t\t\tserializeOperation(operation) as unknown as Record<string, unknown>,\n\t\t)\n\n\t\tawait tx.execute(updateQuery.sql, updateQuery.params)\n\t\tawait tx.execute(opInsert.sql, opInsert.params)\n\t})\n\n\tctx.onMutation(ctx.collection, operation)\n\n\tconst rows = await ctx.adapter.query<RawCollectionRow>(\n\t\t`SELECT * FROM ${quoteIdent(ctx.collection)} WHERE id = ? AND _deleted = 0`,\n\t\t[id],\n\t)\n\tconst row = rows[0]\n\tif (!row) {\n\t\tthrow new RecordNotFoundError(ctx.collection, id)\n\t}\n\treturn deserializeRecord(row, ctx.definition.fields)\n}\n"],"mappings":"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","names":["quoteIdent","quoteIdent","createOperation","createOperation","createOperation","quoteIdent","transformSecretFieldsForWrite","validateRecord","quoteIdent","validateRecord","transformSecretFieldsForWrite","createOperation"]}
@@ -207,6 +207,9 @@ function serializeValue(value, descriptor) {
207
207
  case "boolean":
208
208
  return value ? 1 : 0;
209
209
  case "array":
210
+ case "object":
211
+ case "json":
212
+ case "blob":
210
213
  return JSON.stringify(value);
211
214
  case "richtext":
212
215
  return encodeRichtext(value);
@@ -223,6 +226,13 @@ function deserializeValue(value, descriptor) {
223
226
  return JSON.parse(value);
224
227
  }
225
228
  return value;
229
+ case "object":
230
+ case "json":
231
+ case "blob":
232
+ if (typeof value === "string") {
233
+ return JSON.parse(value);
234
+ }
235
+ return value;
226
236
  case "richtext":
227
237
  return decodeRichtext(value);
228
238
  default:
@@ -243,4 +253,4 @@ export {
243
253
  deserializeOperation,
244
254
  deserializeOperationWithCollection
245
255
  };
246
- //# sourceMappingURL=chunk-ETDJEH2I.js.map
256
+ //# sourceMappingURL=chunk-6NKLDBTY.js.map