@korajs/store 1.0.0-beta.12 → 1.0.0-beta.14
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/README.md +29 -37
- package/dist/adapters/better-sqlite3.cjs +90 -4
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +21 -12
- package/dist/adapters/better-sqlite3.d.ts +21 -12
- package/dist/adapters/better-sqlite3.js +69 -5
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +1316 -610
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +29 -3
- package/dist/adapters/indexeddb.d.ts +29 -3
- package/dist/adapters/indexeddb.js +247 -221
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.cjs +856 -63
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +529 -63
- package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +1032 -344
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +239 -18
- package/dist/adapters/sqlite-wasm.d.ts +239 -18
- package/dist/adapters/sqlite-wasm.js +111 -5
- package/dist/adapters/sqlite-wasm.js.map +1 -1
- package/dist/backup-IHKVMUAM.js +25 -0
- package/dist/chunk-5AQSGMJV.js +5471 -0
- package/dist/chunk-5AQSGMJV.js.map +1 -0
- package/dist/chunk-5JMGMPUE.js +591 -0
- package/dist/chunk-5JMGMPUE.js.map +1 -0
- package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
- package/dist/chunk-A6GFH5IG.js +36 -0
- package/dist/chunk-A6GFH5IG.js.map +1 -0
- package/dist/chunk-BILSA6XZ.js +608 -0
- package/dist/chunk-BILSA6XZ.js.map +1 -0
- package/dist/chunk-BQSIL6TT.js +1095 -0
- package/dist/chunk-BQSIL6TT.js.map +1 -0
- package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
- package/dist/chunk-J5AGXF3D.js.map +1 -0
- package/dist/chunk-KK3GBONL.js +112 -0
- package/dist/chunk-KK3GBONL.js.map +1 -0
- package/dist/chunk-KTVNANUH.js +876 -0
- package/dist/chunk-KTVNANUH.js.map +1 -0
- package/dist/chunk-QW4UU7NG.js +1003 -0
- package/dist/chunk-QW4UU7NG.js.map +1 -0
- package/dist/chunk-SRHDJF5B.js +186 -0
- package/dist/chunk-SRHDJF5B.js.map +1 -0
- package/dist/chunk-UC2LGFPV.js +810 -0
- package/dist/chunk-UC2LGFPV.js.map +1 -0
- package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
- package/dist/index.cjs +9533 -4246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1143 -110
- package/dist/index.d.ts +1143 -110
- package/dist/index.js +298 -3191
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +581 -264
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +15 -85
- package/dist/internal.d.ts +15 -85
- package/dist/internal.js +10 -8
- package/dist/restore-MNDPR557.js +299 -0
- package/dist/restore-MNDPR557.js.map +1 -0
- package/dist/sql-builder-D0tC3sv9.d.cts +615 -0
- package/dist/sql-builder-D5UMzHEo.d.ts +615 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
- package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
- package/dist/sqlite-wasm-channel-QCCNBYCU.js +12 -0
- package/dist/store-HNGX4KTT.js +16 -0
- package/dist/store-HNGX4KTT.js.map +1 -0
- package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
- package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
- package/package.json +3 -2
- package/dist/backup-5S7K2Z35.js +0 -14
- package/dist/chunk-GSJ7EEUC.js.map +0 -1
- package/dist/chunk-HS6DBMST.js +0 -383
- package/dist/chunk-HS6DBMST.js.map +0 -1
- package/dist/chunk-RJIEKEN4.js +0 -751
- package/dist/chunk-RJIEKEN4.js.map +0 -1
- package/dist/chunk-TUR7RGEP.js +0 -84
- package/dist/chunk-TUR7RGEP.js.map +0 -1
- package/dist/chunk-VDBJQ75K.js +0 -78
- package/dist/chunk-VDBJQ75K.js.map +0 -1
- package/dist/chunk-YT7MRFCP.js +0 -687
- package/dist/chunk-YT7MRFCP.js.map +0 -1
- package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
- package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
- package/dist/types-BOO9EPt1.d.ts +0 -170
- package/dist/types-C_GDavG-.d.cts +0 -170
- /package/dist/{backup-5S7K2Z35.js.map → backup-IHKVMUAM.js.map} +0 -0
- /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
- /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-QCCNBYCU.js.map} +0 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-MYSJN3DF.js";
|
|
6
6
|
import {
|
|
7
7
|
deserializeOperationWithCollection
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-J5AGXF3D.js";
|
|
9
9
|
|
|
10
10
|
// src/audit/export-audit.ts
|
|
11
11
|
import { quoteIdent } from "@korajs/core";
|
|
@@ -186,4 +186,4 @@ export {
|
|
|
186
186
|
readAuditExportManifest,
|
|
187
187
|
verifyAuditExportChecksum
|
|
188
188
|
};
|
|
189
|
-
//# sourceMappingURL=chunk-
|
|
189
|
+
//# sourceMappingURL=chunk-5ZRRF6R2.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/migrations/schema-ceiling.ts
|
|
2
|
+
var DIRECTIVE = /^--kora:schema-ceiling (\d+)$/;
|
|
3
|
+
var AHEAD = /KORA_SCHEMA_VERSION_AHEAD stored=(\d+) code=(\d+)/;
|
|
4
|
+
function parseSchemaCeiling(sql) {
|
|
5
|
+
const match = DIRECTIVE.exec(sql.trim());
|
|
6
|
+
return match ? Number(match[1]) : null;
|
|
7
|
+
}
|
|
8
|
+
function storedSchemaVersion(rows) {
|
|
9
|
+
const value = Number(rows[0]?.value ?? 0);
|
|
10
|
+
return Number.isFinite(value) ? value : 0;
|
|
11
|
+
}
|
|
12
|
+
function schemaAheadMessage(stored, code) {
|
|
13
|
+
return `KORA_SCHEMA_VERSION_AHEAD stored=${stored} code=${code}: the database was migrated by a newer build (schema version ${stored}); this build's schema version is ${code}.`;
|
|
14
|
+
}
|
|
15
|
+
function parseSchemaAhead(error) {
|
|
16
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17
|
+
let current = error;
|
|
18
|
+
while (current !== null && current !== void 0 && !seen.has(current)) {
|
|
19
|
+
seen.add(current);
|
|
20
|
+
const text = current instanceof Error ? current.message : typeof current === "string" ? current : "";
|
|
21
|
+
const match = AHEAD.exec(text);
|
|
22
|
+
if (match) return { stored: Number(match[1]), code: Number(match[2]) };
|
|
23
|
+
current = current instanceof Error ? current.cause : null;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
var STORED_SCHEMA_VERSION_SQL = "SELECT value FROM _kora_meta WHERE key = 'schema_version'";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
parseSchemaCeiling,
|
|
31
|
+
storedSchemaVersion,
|
|
32
|
+
schemaAheadMessage,
|
|
33
|
+
parseSchemaAhead,
|
|
34
|
+
STORED_SCHEMA_VERSION_SQL
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=chunk-A6GFH5IG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/migrations/schema-ceiling.ts"],"sourcesContent":["/**\n * The schema-version ceiling (RT-109): a build never runs against a database a newer\n * build already migrated.\n *\n * `generateFullDDL` emits `--kora:schema-ceiling <version>` right after `_kora_meta`, before\n * any other DDL. It is a SQL comment; Kora's DDL executors (the native adapter, the SQLite\n * WASM worker, the test bridge) stop there with {@link schemaAheadMessage} when the\n * database's stored schema version is newer, so not even this build's additive DDL touches\n * it. The store then maps the failure to `SchemaVersionAheadError` and checks again after\n * every open (adapters that restore data after their DDL, such as the IndexedDB fallback).\n *\n * No imports: the SQLite WASM worker bundles this file.\n */\n\nconst DIRECTIVE = /^--kora:schema-ceiling (\\d+)$/\nconst AHEAD = /KORA_SCHEMA_VERSION_AHEAD stored=(\\d+) code=(\\d+)/\n\n/** The version a `--kora:schema-ceiling <version>` DDL statement names, or null. */\nexport function parseSchemaCeiling(sql: string): number | null {\n\tconst match = DIRECTIVE.exec(sql.trim())\n\treturn match ? Number(match[1]) : null\n}\n\n/** The stored version from a `_kora_meta` row read, or 0 when none is stored. */\nexport function storedSchemaVersion(rows: ReadonlyArray<{ value: unknown }>): number {\n\tconst value = Number(rows[0]?.value ?? 0)\n\treturn Number.isFinite(value) ? value : 0\n}\n\n/** The message a DDL executor throws when the database is ahead (parsed back by the store). */\nexport function schemaAheadMessage(stored: number, code: number): string {\n\treturn `KORA_SCHEMA_VERSION_AHEAD stored=${stored} code=${code}: the database was migrated by a newer build (schema version ${stored}); this build's schema version is ${code}.`\n}\n\n/** The versions in an error raised by {@link schemaAheadMessage}, wherever it was wrapped. */\nexport function parseSchemaAhead(error: unknown): { stored: number; code: number } | null {\n\tconst seen = new Set<unknown>()\n\tlet current: unknown = error\n\twhile (current !== null && current !== undefined && !seen.has(current)) {\n\t\tseen.add(current)\n\t\tconst text =\n\t\t\tcurrent instanceof Error ? current.message : typeof current === 'string' ? current : ''\n\t\tconst match = AHEAD.exec(text)\n\t\tif (match) return { stored: Number(match[1]), code: Number(match[2]) }\n\t\tcurrent = current instanceof Error ? (current as { cause?: unknown }).cause : null\n\t}\n\treturn null\n}\n\n/** Query the stored schema version through a synchronous runner (DDL executors). */\nexport const STORED_SCHEMA_VERSION_SQL = \"SELECT value FROM _kora_meta WHERE key = 'schema_version'\"\n"],"mappings":";AAcA,IAAM,YAAY;AAClB,IAAM,QAAQ;AAGP,SAAS,mBAAmB,KAA4B;AAC9D,QAAM,QAAQ,UAAU,KAAK,IAAI,KAAK,CAAC;AACvC,SAAO,QAAQ,OAAO,MAAM,CAAC,CAAC,IAAI;AACnC;AAGO,SAAS,oBAAoB,MAAiD;AACpF,QAAM,QAAQ,OAAO,KAAK,CAAC,GAAG,SAAS,CAAC;AACxC,SAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AACzC;AAGO,SAAS,mBAAmB,QAAgB,MAAsB;AACxE,SAAO,oCAAoC,MAAM,SAAS,IAAI,gEAAgE,MAAM,qCAAqC,IAAI;AAC9K;AAGO,SAAS,iBAAiB,OAAyD;AACzF,QAAM,OAAO,oBAAI,IAAa;AAC9B,MAAI,UAAmB;AACvB,SAAO,YAAY,QAAQ,YAAY,UAAa,CAAC,KAAK,IAAI,OAAO,GAAG;AACvE,SAAK,IAAI,OAAO;AAChB,UAAM,OACL,mBAAmB,QAAQ,QAAQ,UAAU,OAAO,YAAY,WAAW,UAAU;AACtF,UAAM,QAAQ,MAAM,KAAK,IAAI;AAC7B,QAAI,MAAO,QAAO,EAAE,QAAQ,OAAO,MAAM,CAAC,CAAC,GAAG,MAAM,OAAO,MAAM,CAAC,CAAC,EAAE;AACrE,cAAU,mBAAmB,QAAS,QAAgC,QAAQ;AAAA,EAC/E;AACA,SAAO;AACR;AAGO,IAAM,4BAA4B;","names":[]}
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_ROW_VERSION,
|
|
3
|
+
buildInsertQuery,
|
|
4
|
+
buildSoftDeleteQuery,
|
|
5
|
+
buildUpdateQuery,
|
|
6
|
+
serializeRowVersion
|
|
7
|
+
} from "./chunk-QW4UU7NG.js";
|
|
8
|
+
import {
|
|
9
|
+
deserializeRecord,
|
|
10
|
+
encodeRichtextFieldsForOpData,
|
|
11
|
+
serializeOperation,
|
|
12
|
+
serializeRecord,
|
|
13
|
+
toRichtextReadShape
|
|
14
|
+
} from "./chunk-J5AGXF3D.js";
|
|
15
|
+
import {
|
|
16
|
+
RecordNotFoundError
|
|
17
|
+
} from "./chunk-SRHDJF5B.js";
|
|
18
|
+
|
|
19
|
+
// src/mutations/write-context.ts
|
|
20
|
+
import { KoraError } from "@korajs/core";
|
|
21
|
+
var CausalScope = class {
|
|
22
|
+
constructor(tracker, chainOperations) {
|
|
23
|
+
this.tracker = tracker;
|
|
24
|
+
this.chainOperations = chainOperations;
|
|
25
|
+
}
|
|
26
|
+
tracker;
|
|
27
|
+
chainOperations;
|
|
28
|
+
lastByCollection = /* @__PURE__ */ new Map();
|
|
29
|
+
lastInScope = null;
|
|
30
|
+
recorded = [];
|
|
31
|
+
/**
|
|
32
|
+
* Direct causal parents for the next operation in `collection`.
|
|
33
|
+
*
|
|
34
|
+
* @param collection - Collection of the operation being built
|
|
35
|
+
* @param extra - Explicit parents (e.g. the delete that caused a cascade)
|
|
36
|
+
*/
|
|
37
|
+
depsFor(collection, extra = []) {
|
|
38
|
+
const deps = [];
|
|
39
|
+
const add = (id) => {
|
|
40
|
+
if (id && !deps.includes(id)) deps.push(id);
|
|
41
|
+
};
|
|
42
|
+
for (const id of extra) add(id);
|
|
43
|
+
const local = this.lastByCollection.get(collection);
|
|
44
|
+
if (local !== void 0) {
|
|
45
|
+
add(local);
|
|
46
|
+
} else {
|
|
47
|
+
for (const id of this.tracker?.nextCausalDeps(collection, false) ?? []) add(id);
|
|
48
|
+
}
|
|
49
|
+
if (this.chainOperations) add(this.lastInScope);
|
|
50
|
+
return deps;
|
|
51
|
+
}
|
|
52
|
+
/** Record an operation built in this scope. */
|
|
53
|
+
record(collection, operationId) {
|
|
54
|
+
this.lastByCollection.set(collection, operationId);
|
|
55
|
+
this.lastInScope = operationId;
|
|
56
|
+
this.recorded.push({ collection, id: operationId });
|
|
57
|
+
}
|
|
58
|
+
/** Publish this scope's heads to the shared tracker. Call only after commit. */
|
|
59
|
+
publish() {
|
|
60
|
+
for (const { collection, id } of this.recorded) {
|
|
61
|
+
this.tracker?.afterOperation(collection, id, false);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var SequenceBlock = class {
|
|
66
|
+
constructor(tx, nodeId) {
|
|
67
|
+
this.tx = tx;
|
|
68
|
+
this.nodeId = nodeId;
|
|
69
|
+
}
|
|
70
|
+
tx;
|
|
71
|
+
nodeId;
|
|
72
|
+
last = null;
|
|
73
|
+
persisted = 0;
|
|
74
|
+
/** The next number of the block. */
|
|
75
|
+
async take() {
|
|
76
|
+
if (this.last === null) {
|
|
77
|
+
const rows = await this.tx.query(
|
|
78
|
+
`INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
|
|
79
|
+
ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
|
|
80
|
+
RETURNING sequence_number`,
|
|
81
|
+
[this.nodeId]
|
|
82
|
+
);
|
|
83
|
+
const first = rows[0]?.sequence_number;
|
|
84
|
+
if (first === void 0) {
|
|
85
|
+
throw new KoraError(
|
|
86
|
+
`Failed to reserve a sequence number for node "${this.nodeId}"`,
|
|
87
|
+
"SEQUENCE_RESERVATION_FAILED",
|
|
88
|
+
{ nodeId: this.nodeId }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
this.last = first;
|
|
92
|
+
this.persisted = first;
|
|
93
|
+
return first;
|
|
94
|
+
}
|
|
95
|
+
this.last += 1;
|
|
96
|
+
return this.last;
|
|
97
|
+
}
|
|
98
|
+
/** Persist the end of the block. Must run inside the transaction, before commit. */
|
|
99
|
+
async close() {
|
|
100
|
+
if (this.last === null || this.last === this.persisted) return;
|
|
101
|
+
await this.tx.execute(
|
|
102
|
+
"UPDATE _kora_version_vector SET sequence_number = MAX(sequence_number, ?) WHERE node_id = ?",
|
|
103
|
+
[this.last, this.nodeId]
|
|
104
|
+
);
|
|
105
|
+
this.persisted = this.last;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
async function withWriteScope(tx, nodeId, causal, write) {
|
|
109
|
+
const scope = { tx, sequence: new SequenceBlock(tx, nodeId), causal };
|
|
110
|
+
const result = await write(scope);
|
|
111
|
+
await scope.sequence.close();
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function isStorageFullError(error) {
|
|
115
|
+
if (error === null || typeof error !== "object") return false;
|
|
116
|
+
const candidate = error;
|
|
117
|
+
if (candidate.name === "QuotaExceededError") return true;
|
|
118
|
+
if (candidate.code === "SQLITE_FULL" || candidate.code === "QUOTA_EXCEEDED") return true;
|
|
119
|
+
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
120
|
+
if (/SQLITE_FULL|database or disk is full|quota ?exceeded/i.test(message)) return true;
|
|
121
|
+
return candidate.cause !== void 0 && candidate.cause !== error ? isStorageFullError(candidate.cause) : false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/lww/field-versions.ts
|
|
125
|
+
function parseFieldVersions(raw) {
|
|
126
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
const result = {};
|
|
135
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
136
|
+
if (typeof value === "string") {
|
|
137
|
+
result[key] = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
} catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function serializeFieldVersions(versions) {
|
|
146
|
+
return JSON.stringify(versions);
|
|
147
|
+
}
|
|
148
|
+
function fieldVersionsForFields(fieldNames, version) {
|
|
149
|
+
const versions = {};
|
|
150
|
+
for (const field of fieldNames) {
|
|
151
|
+
versions[field] = version;
|
|
152
|
+
}
|
|
153
|
+
return versions;
|
|
154
|
+
}
|
|
155
|
+
function effectiveFieldVersion(current, field, rowVersion) {
|
|
156
|
+
const own = current[field];
|
|
157
|
+
if (own !== void 0) {
|
|
158
|
+
return own;
|
|
159
|
+
}
|
|
160
|
+
return rowVersion ?? EMPTY_ROW_VERSION;
|
|
161
|
+
}
|
|
162
|
+
function resolvePerFieldLww(current, incomingFields, incomingVersion, rowVersion) {
|
|
163
|
+
const winners = [];
|
|
164
|
+
const merged = { ...current };
|
|
165
|
+
for (const field of incomingFields) {
|
|
166
|
+
const stored = effectiveFieldVersion(current, field, rowVersion);
|
|
167
|
+
if (incomingVersion > stored) {
|
|
168
|
+
winners.push(field);
|
|
169
|
+
merged[field] = incomingVersion;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { winners, merged };
|
|
173
|
+
}
|
|
174
|
+
function stampFieldVersions(currentRaw, fields, version) {
|
|
175
|
+
const versions = parseFieldVersions(currentRaw);
|
|
176
|
+
for (const field of fields) {
|
|
177
|
+
const existing = versions[field];
|
|
178
|
+
if (existing === void 0 || existing < version) {
|
|
179
|
+
versions[field] = version;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return serializeFieldVersions(versions);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/state-machine/state-validator.ts
|
|
186
|
+
import { KoraError as KoraError2, validateTransition } from "@korajs/core";
|
|
187
|
+
var InvalidStateTransitionError = class extends KoraError2 {
|
|
188
|
+
constructor(collection, recordId, field, fromState, toState, allowedStates) {
|
|
189
|
+
super(
|
|
190
|
+
`Invalid state transition in collection "${collection}": cannot transition field "${field}" from "${fromState}" to "${toState}". Allowed transitions from "${fromState}": ${allowedStates.length > 0 ? allowedStates.join(", ") : "(none -- terminal state)"}`,
|
|
191
|
+
"INVALID_STATE_TRANSITION",
|
|
192
|
+
{ collection, recordId, field, fromState, toState, allowedStates }
|
|
193
|
+
);
|
|
194
|
+
this.collection = collection;
|
|
195
|
+
this.recordId = recordId;
|
|
196
|
+
this.field = field;
|
|
197
|
+
this.fromState = fromState;
|
|
198
|
+
this.toState = toState;
|
|
199
|
+
this.allowedStates = allowedStates;
|
|
200
|
+
this.name = "InvalidStateTransitionError";
|
|
201
|
+
}
|
|
202
|
+
collection;
|
|
203
|
+
recordId;
|
|
204
|
+
field;
|
|
205
|
+
fromState;
|
|
206
|
+
toState;
|
|
207
|
+
allowedStates;
|
|
208
|
+
};
|
|
209
|
+
function validateStateTransition(collectionName, recordId, stateMachine, currentState, newState) {
|
|
210
|
+
if (currentState === null) {
|
|
211
|
+
return { valid: true, allowedStates: [] };
|
|
212
|
+
}
|
|
213
|
+
if (currentState === newState) {
|
|
214
|
+
return { valid: true, allowedStates: stateMachine.transitions[currentState] ?? [] };
|
|
215
|
+
}
|
|
216
|
+
const constraint = {
|
|
217
|
+
field: stateMachine.field,
|
|
218
|
+
collection: collectionName,
|
|
219
|
+
transitions: stateMachine.transitions
|
|
220
|
+
};
|
|
221
|
+
const transitionResult = validateTransition(constraint, currentState, newState);
|
|
222
|
+
if (transitionResult.valid) {
|
|
223
|
+
return { valid: true, allowedStates: transitionResult.allowedTargets };
|
|
224
|
+
}
|
|
225
|
+
const allowedStates = transitionResult.allowedTargets;
|
|
226
|
+
if (stateMachine.onInvalidTransition === "reject") {
|
|
227
|
+
throw new InvalidStateTransitionError(
|
|
228
|
+
collectionName,
|
|
229
|
+
recordId,
|
|
230
|
+
stateMachine.field,
|
|
231
|
+
currentState,
|
|
232
|
+
newState,
|
|
233
|
+
allowedStates
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return { valid: false, allowedStates };
|
|
237
|
+
}
|
|
238
|
+
function validateUpdateStateMachine(collectionName, recordId, collectionDef, currentRecord, updateData) {
|
|
239
|
+
let result = updateData;
|
|
240
|
+
const stateMachine = collectionDef.stateMachine;
|
|
241
|
+
if (stateMachine !== void 0) {
|
|
242
|
+
result = applyStateMachine(collectionName, recordId, stateMachine, currentRecord, result);
|
|
243
|
+
}
|
|
244
|
+
for (const [field, descriptor] of Object.entries(collectionDef.fields)) {
|
|
245
|
+
if (descriptor.kind !== "enum" || !descriptor.transitions) continue;
|
|
246
|
+
if (stateMachine?.field === field) continue;
|
|
247
|
+
result = applyStateMachine(
|
|
248
|
+
collectionName,
|
|
249
|
+
recordId,
|
|
250
|
+
{ field, transitions: descriptor.transitions, onInvalidTransition: "reject" },
|
|
251
|
+
currentRecord,
|
|
252
|
+
result
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
function applyStateMachine(collectionName, recordId, stateMachine, currentRecord, updateData) {
|
|
258
|
+
const stateField = stateMachine.field;
|
|
259
|
+
if (!(stateField in updateData)) {
|
|
260
|
+
return updateData;
|
|
261
|
+
}
|
|
262
|
+
const currentState = currentRecord[stateField];
|
|
263
|
+
const newState = updateData[stateField];
|
|
264
|
+
if (typeof currentState !== "string" || typeof newState !== "string") {
|
|
265
|
+
return updateData;
|
|
266
|
+
}
|
|
267
|
+
const result = validateStateTransition(
|
|
268
|
+
collectionName,
|
|
269
|
+
recordId,
|
|
270
|
+
stateMachine,
|
|
271
|
+
currentState,
|
|
272
|
+
newState
|
|
273
|
+
);
|
|
274
|
+
if (result.valid) {
|
|
275
|
+
return updateData;
|
|
276
|
+
}
|
|
277
|
+
const filtered = { ...updateData };
|
|
278
|
+
delete filtered[stateField];
|
|
279
|
+
return filtered;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/mutations/run-local-write.ts
|
|
283
|
+
import { KoraError as KoraError3 } from "@korajs/core";
|
|
284
|
+
async function runLocalWrite(ctx, write) {
|
|
285
|
+
const causal = new CausalScope(ctx.causalTracker, false);
|
|
286
|
+
let result;
|
|
287
|
+
try {
|
|
288
|
+
await ctx.adapter.transaction(async (tx) => {
|
|
289
|
+
result = await withWriteScope(tx, ctx.nodeId, causal, write);
|
|
290
|
+
});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
ctx.onStorageError?.(error);
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
if (!result) {
|
|
296
|
+
throw new KoraError3("Local write transaction completed without a result", "WRITE_INCOMPLETE", {
|
|
297
|
+
collection: ctx.collection
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
causal.publish();
|
|
301
|
+
publishOperations(ctx, result);
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
function publishOperations(ctx, result) {
|
|
305
|
+
const ops = [];
|
|
306
|
+
if (result.operation) ops.push(result.operation);
|
|
307
|
+
ops.push(...result.sideEffects);
|
|
308
|
+
for (const op of ops) {
|
|
309
|
+
ctx.onMutation(op.collection, op);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/mutations/write-ops.ts
|
|
314
|
+
import {
|
|
315
|
+
DEFAULT_MAX_OPERATION_BYTES,
|
|
316
|
+
KoraError as KoraError4,
|
|
317
|
+
OperationTooLargeError,
|
|
318
|
+
assertResolvedFieldValue,
|
|
319
|
+
createOperation,
|
|
320
|
+
generateUUIDv7,
|
|
321
|
+
isAtomicOp,
|
|
322
|
+
measureOperationBytes,
|
|
323
|
+
quoteIdent,
|
|
324
|
+
resolveAtomicOp,
|
|
325
|
+
toAtomicOp,
|
|
326
|
+
validateRecord
|
|
327
|
+
} from "@korajs/core";
|
|
328
|
+
|
|
329
|
+
// src/mutations/secret-write.ts
|
|
330
|
+
import { transformSecretFieldsForWrite } from "@korajs/core";
|
|
331
|
+
function toAtRestWriteData(data, definition, secretKeyProvider) {
|
|
332
|
+
return transformSecretFieldsForWrite(data, definition, secretKeyProvider);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/mutations/write-ops.ts
|
|
336
|
+
async function prepareInsert(env, collection, data) {
|
|
337
|
+
const definition = requireDefinition(env, collection);
|
|
338
|
+
const validated = validateRecord(collection, definition, data, "insert");
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
|
|
341
|
+
if (descriptor.auto && descriptor.kind === "timestamp") {
|
|
342
|
+
validated[fieldName] = now;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const writeData = await toAtRestWriteData(validated, definition, env.secretKeyProvider);
|
|
346
|
+
return { collection, recordId: generateUUIDv7(), data: writeData };
|
|
347
|
+
}
|
|
348
|
+
async function writeInsertInTx(env, scope, insert, extraCausalDeps = []) {
|
|
349
|
+
const definition = requireDefinition(env, insert.collection);
|
|
350
|
+
const operation = await buildLocalOperation(env, scope, {
|
|
351
|
+
type: "insert",
|
|
352
|
+
collection: insert.collection,
|
|
353
|
+
recordId: insert.recordId,
|
|
354
|
+
// Binary richtext values are tagged as canonical JSON BEFORE hashing, so the
|
|
355
|
+
// hash input, persisted JSON and wire payload are the identical value.
|
|
356
|
+
data: encodeRichtextFieldsForOpData(insert.data, definition.fields),
|
|
357
|
+
previousData: null,
|
|
358
|
+
extraCausalDeps
|
|
359
|
+
});
|
|
360
|
+
if (env.fold) {
|
|
361
|
+
await appendOperationRow(scope, operation);
|
|
362
|
+
await env.fold.applyInTx(scope.tx, operation, "local");
|
|
363
|
+
} else {
|
|
364
|
+
const serialized = serializeRecord(insert.data, definition.fields);
|
|
365
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
366
|
+
const row = {
|
|
367
|
+
id: insert.recordId,
|
|
368
|
+
...serialized,
|
|
369
|
+
_created_at: operation.timestamp.wallTime,
|
|
370
|
+
_updated_at: operation.timestamp.wallTime,
|
|
371
|
+
_version: version,
|
|
372
|
+
// Every inserted field is stamped with this operation's version, so later
|
|
373
|
+
// per-field LWW compares against a real writer, not the row fallback.
|
|
374
|
+
_field_versions: stampFieldVersions(null, Object.keys(serialized), version)
|
|
375
|
+
};
|
|
376
|
+
const rowInsert = buildInsertQuery(insert.collection, row);
|
|
377
|
+
await scope.tx.execute(rowInsert.sql, rowInsert.params);
|
|
378
|
+
await appendOperationRow(scope, operation);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
operation,
|
|
382
|
+
record: {
|
|
383
|
+
id: insert.recordId,
|
|
384
|
+
// A richtext value written as a string returns as the bytes reads return.
|
|
385
|
+
...toRichtextReadShape(insert.data, definition.fields),
|
|
386
|
+
createdAt: operation.timestamp.wallTime,
|
|
387
|
+
updatedAt: operation.timestamp.wallTime
|
|
388
|
+
},
|
|
389
|
+
sideEffects: []
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function writeUpdateInTx(env, scope, collection, id, data, extraCausalDeps = []) {
|
|
393
|
+
const definition = requireDefinition(env, collection);
|
|
394
|
+
const currentRow = await readLiveRow(scope, collection, id);
|
|
395
|
+
if (!currentRow) {
|
|
396
|
+
throw new RecordNotFoundError(collection, id);
|
|
397
|
+
}
|
|
398
|
+
const currentRecord = deserializeRecord(currentRow, definition.fields);
|
|
399
|
+
const allowed = validateUpdateStateMachine(collection, id, definition, currentRecord, data);
|
|
400
|
+
if (Object.keys(allowed).length === 0) {
|
|
401
|
+
return { operation: null, record: currentRecord, sideEffects: [] };
|
|
402
|
+
}
|
|
403
|
+
const previousData = {};
|
|
404
|
+
const resolved = {};
|
|
405
|
+
const atomicOps = {};
|
|
406
|
+
for (const [key, value] of Object.entries(allowed)) {
|
|
407
|
+
previousData[key] = currentRecord[key];
|
|
408
|
+
if (isAtomicOp(value)) {
|
|
409
|
+
resolved[key] = resolveAtomicOp(currentRecord[key], value);
|
|
410
|
+
const descriptor = definition.fields[key];
|
|
411
|
+
if (descriptor) assertResolvedFieldValue(collection, key, descriptor, resolved[key]);
|
|
412
|
+
atomicOps[key] = toAtomicOp(value);
|
|
413
|
+
} else {
|
|
414
|
+
resolved[key] = value;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const writeData = await toAtRestWriteData(resolved, definition, env.secretKeyProvider);
|
|
418
|
+
const operation = await buildLocalOperation(env, scope, {
|
|
419
|
+
type: "update",
|
|
420
|
+
collection,
|
|
421
|
+
recordId: id,
|
|
422
|
+
data: encodeRichtextFieldsForOpData(writeData, definition.fields),
|
|
423
|
+
previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
|
|
424
|
+
extraCausalDeps,
|
|
425
|
+
...Object.keys(atomicOps).length > 0 ? { atomicOps } : {}
|
|
426
|
+
});
|
|
427
|
+
if (env.fold) {
|
|
428
|
+
await appendOperationRow(scope, operation);
|
|
429
|
+
await env.fold.applyInTx(scope.tx, operation, "local");
|
|
430
|
+
} else {
|
|
431
|
+
const serializedChanges = serializeRecord(writeData, definition.fields);
|
|
432
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
433
|
+
const rowUpdate = buildUpdateQuery(collection, id, {
|
|
434
|
+
...serializedChanges,
|
|
435
|
+
_updated_at: operation.timestamp.wallTime,
|
|
436
|
+
_version: version,
|
|
437
|
+
_field_versions: stampFieldVersions(
|
|
438
|
+
currentRow._field_versions,
|
|
439
|
+
Object.keys(serializedChanges),
|
|
440
|
+
version
|
|
441
|
+
)
|
|
442
|
+
});
|
|
443
|
+
await scope.tx.execute(rowUpdate.sql, rowUpdate.params);
|
|
444
|
+
await appendOperationRow(scope, operation);
|
|
445
|
+
}
|
|
446
|
+
const updatedRow = await readLiveRow(scope, collection, id);
|
|
447
|
+
if (!updatedRow) {
|
|
448
|
+
throw new RecordNotFoundError(collection, id);
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
operation,
|
|
452
|
+
record: deserializeRecord(updatedRow, definition.fields),
|
|
453
|
+
sideEffects: []
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function writeDeleteInTx(env, scope, collection, id, options = {}) {
|
|
457
|
+
const definition = requireDefinition(env, collection);
|
|
458
|
+
const currentRow = await readLiveRow(scope, collection, id);
|
|
459
|
+
if (!currentRow) {
|
|
460
|
+
throw new RecordNotFoundError(collection, id);
|
|
461
|
+
}
|
|
462
|
+
const operation = await buildLocalOperation(env, scope, {
|
|
463
|
+
type: "delete",
|
|
464
|
+
collection,
|
|
465
|
+
recordId: id,
|
|
466
|
+
data: null,
|
|
467
|
+
previousData: null,
|
|
468
|
+
extraCausalDeps: options.extraCausalDeps ?? []
|
|
469
|
+
});
|
|
470
|
+
if (!options.skipReferentialEnforcement && env.beforeLocalDelete) {
|
|
471
|
+
await env.beforeLocalDelete(operation, scope.tx);
|
|
472
|
+
}
|
|
473
|
+
if (env.fold) {
|
|
474
|
+
await appendOperationRow(scope, operation);
|
|
475
|
+
await env.fold.applyInTx(scope.tx, operation, "local");
|
|
476
|
+
} else {
|
|
477
|
+
const version = serializeRowVersion(operation.timestamp);
|
|
478
|
+
const softDelete = buildSoftDeleteQuery(collection, id, operation.timestamp.wallTime, version);
|
|
479
|
+
await scope.tx.execute(softDelete.sql, softDelete.params);
|
|
480
|
+
await appendOperationRow(scope, operation);
|
|
481
|
+
}
|
|
482
|
+
const sideEffects = [];
|
|
483
|
+
if (!options.skipReferentialEnforcement && env.relationEnforcer) {
|
|
484
|
+
const result = await env.relationEnforcer.enforceDelete(
|
|
485
|
+
collection,
|
|
486
|
+
id,
|
|
487
|
+
scope,
|
|
488
|
+
env,
|
|
489
|
+
operation.id
|
|
490
|
+
);
|
|
491
|
+
sideEffects.push(...result.operations);
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
operation,
|
|
495
|
+
record: deserializeRecord(currentRow, definition.fields),
|
|
496
|
+
sideEffects
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
async function buildLocalOperation(env, scope, input) {
|
|
500
|
+
env.assertLocalWriteAllowed?.();
|
|
501
|
+
const sequenceNumber = await scope.sequence.take();
|
|
502
|
+
const causalDeps = scope.causal.depsFor(input.collection, input.extraCausalDeps);
|
|
503
|
+
const operation = await createOperation(
|
|
504
|
+
{
|
|
505
|
+
nodeId: env.nodeId,
|
|
506
|
+
type: input.type,
|
|
507
|
+
collection: input.collection,
|
|
508
|
+
recordId: input.recordId,
|
|
509
|
+
data: input.data,
|
|
510
|
+
previousData: input.previousData,
|
|
511
|
+
sequenceNumber,
|
|
512
|
+
causalDeps,
|
|
513
|
+
schemaVersion: env.schema.version,
|
|
514
|
+
...input.atomicOps !== void 0 ? { atomicOps: input.atomicOps } : {},
|
|
515
|
+
...env.transactionId !== void 0 ? { transactionId: env.transactionId } : {},
|
|
516
|
+
...env.mutationName !== void 0 ? { mutationName: env.mutationName } : {}
|
|
517
|
+
},
|
|
518
|
+
env.clock
|
|
519
|
+
);
|
|
520
|
+
const maxBytes = env.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
|
|
521
|
+
const bytes = measureOperationBytes(operation);
|
|
522
|
+
if (bytes > maxBytes) {
|
|
523
|
+
throw new OperationTooLargeError(input.collection, input.recordId, bytes, maxBytes);
|
|
524
|
+
}
|
|
525
|
+
scope.causal.record(input.collection, operation.id);
|
|
526
|
+
return operation;
|
|
527
|
+
}
|
|
528
|
+
async function appendOperationRow(scope, operation) {
|
|
529
|
+
const opInsert = buildInsertQuery(
|
|
530
|
+
`_kora_ops_${operation.collection}`,
|
|
531
|
+
serializeOperation(operation)
|
|
532
|
+
);
|
|
533
|
+
await scope.tx.execute(opInsert.sql, opInsert.params);
|
|
534
|
+
}
|
|
535
|
+
async function readLiveRow(scope, collection, id) {
|
|
536
|
+
const rows = await scope.tx.query(
|
|
537
|
+
`SELECT * FROM ${quoteIdent(collection)} WHERE id = ? AND _deleted = 0`,
|
|
538
|
+
[id]
|
|
539
|
+
);
|
|
540
|
+
return rows[0];
|
|
541
|
+
}
|
|
542
|
+
function requireDefinition(env, collection) {
|
|
543
|
+
const definition = env.schema.collections[collection];
|
|
544
|
+
if (!definition) {
|
|
545
|
+
throw new KoraError4(
|
|
546
|
+
`Unknown collection "${collection}". Available: ${Object.keys(env.schema.collections).join(", ")}`,
|
|
547
|
+
"UNKNOWN_COLLECTION",
|
|
548
|
+
{ collection }
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return definition;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/mutations/execute-delete.ts
|
|
555
|
+
async function executeDelete(ctx, id, options) {
|
|
556
|
+
const result = await runLocalWrite(
|
|
557
|
+
ctx,
|
|
558
|
+
(scope) => writeDeleteInTx(ctx, scope, ctx.collection, id, {
|
|
559
|
+
skipReferentialEnforcement: options?.skipReferentialEnforcement ?? false,
|
|
560
|
+
extraCausalDeps: ctx.extraCausalDeps ?? []
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
return result.operation ? [result.operation, ...result.sideEffects] : result.sideEffects;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/mutations/execute-insert.ts
|
|
567
|
+
async function executeInsert(ctx, data) {
|
|
568
|
+
const prepared = await prepareInsert(ctx, ctx.collection, data);
|
|
569
|
+
const result = await runLocalWrite(
|
|
570
|
+
ctx,
|
|
571
|
+
(scope) => writeInsertInTx(ctx, scope, prepared, ctx.extraCausalDeps ?? [])
|
|
572
|
+
);
|
|
573
|
+
return result.record;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/mutations/execute-update.ts
|
|
577
|
+
import { validateRecord as validateRecord2 } from "@korajs/core";
|
|
578
|
+
async function executeUpdate(ctx, id, data) {
|
|
579
|
+
const validated = validateRecord2(ctx.collection, ctx.definition, data, "update");
|
|
580
|
+
const result = await runLocalWrite(
|
|
581
|
+
ctx,
|
|
582
|
+
(scope) => writeUpdateInTx(ctx, scope, ctx.collection, id, validated, ctx.extraCausalDeps ?? [])
|
|
583
|
+
);
|
|
584
|
+
return result.record;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export {
|
|
588
|
+
CausalScope,
|
|
589
|
+
withWriteScope,
|
|
590
|
+
isStorageFullError,
|
|
591
|
+
parseFieldVersions,
|
|
592
|
+
serializeFieldVersions,
|
|
593
|
+
fieldVersionsForFields,
|
|
594
|
+
effectiveFieldVersion,
|
|
595
|
+
resolvePerFieldLww,
|
|
596
|
+
InvalidStateTransitionError,
|
|
597
|
+
validateStateTransition,
|
|
598
|
+
validateUpdateStateMachine,
|
|
599
|
+
toAtRestWriteData,
|
|
600
|
+
prepareInsert,
|
|
601
|
+
writeInsertInTx,
|
|
602
|
+
writeUpdateInTx,
|
|
603
|
+
writeDeleteInTx,
|
|
604
|
+
executeDelete,
|
|
605
|
+
executeInsert,
|
|
606
|
+
executeUpdate
|
|
607
|
+
};
|
|
608
|
+
//# sourceMappingURL=chunk-BILSA6XZ.js.map
|