@korajs/store 1.0.0-beta.11 → 1.0.0-beta.13
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 +803 -62
- package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm-worker.js +480 -62
- 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-4NUPDEMB.js +25 -0
- package/dist/chunk-32EDKWMI.js +876 -0
- package/dist/chunk-32EDKWMI.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-DUKTWVZW.js +175 -0
- package/dist/chunk-DUKTWVZW.js.map +1 -0
- package/dist/chunk-HZTDBLDB.js +607 -0
- package/dist/chunk-HZTDBLDB.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-MCQKEHOD.js +112 -0
- package/dist/chunk-MCQKEHOD.js.map +1 -0
- package/dist/chunk-P4V375FY.js +810 -0
- package/dist/chunk-P4V375FY.js.map +1 -0
- package/dist/chunk-QDUSJMFV.js +1095 -0
- package/dist/chunk-QDUSJMFV.js.map +1 -0
- package/dist/chunk-SJP2HTYZ.js +1003 -0
- package/dist/chunk-SJP2HTYZ.js.map +1 -0
- package/dist/chunk-W5F2LWG6.js +591 -0
- package/dist/chunk-W5F2LWG6.js.map +1 -0
- package/dist/chunk-Y5LRH7KI.js +5341 -0
- package/dist/chunk-Y5LRH7KI.js.map +1 -0
- package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
- package/dist/index.cjs +9354 -4157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1064 -108
- package/dist/index.d.ts +1064 -108
- package/dist/index.js +249 -3092
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +580 -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-FR7XIKT2.js +299 -0
- package/dist/restore-FR7XIKT2.js.map +1 -0
- package/dist/sql-builder-C4u55jKW.d.cts +608 -0
- package/dist/sql-builder-CGckdBDL.d.ts +608 -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-MLHLKEED.js +12 -0
- package/dist/store-IMWKZLWB.js +16 -0
- package/dist/store-IMWKZLWB.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-4NUPDEMB.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-MLHLKEED.js.map} +0 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_ROW_VERSION,
|
|
3
|
+
buildInsertQuery,
|
|
4
|
+
buildSoftDeleteQuery,
|
|
5
|
+
buildUpdateQuery,
|
|
6
|
+
serializeRowVersion
|
|
7
|
+
} from "./chunk-SJP2HTYZ.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-DUKTWVZW.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
|
+
const sequenceNumber = await scope.sequence.take();
|
|
501
|
+
const causalDeps = scope.causal.depsFor(input.collection, input.extraCausalDeps);
|
|
502
|
+
const operation = await createOperation(
|
|
503
|
+
{
|
|
504
|
+
nodeId: env.nodeId,
|
|
505
|
+
type: input.type,
|
|
506
|
+
collection: input.collection,
|
|
507
|
+
recordId: input.recordId,
|
|
508
|
+
data: input.data,
|
|
509
|
+
previousData: input.previousData,
|
|
510
|
+
sequenceNumber,
|
|
511
|
+
causalDeps,
|
|
512
|
+
schemaVersion: env.schema.version,
|
|
513
|
+
...input.atomicOps !== void 0 ? { atomicOps: input.atomicOps } : {},
|
|
514
|
+
...env.transactionId !== void 0 ? { transactionId: env.transactionId } : {},
|
|
515
|
+
...env.mutationName !== void 0 ? { mutationName: env.mutationName } : {}
|
|
516
|
+
},
|
|
517
|
+
env.clock
|
|
518
|
+
);
|
|
519
|
+
const maxBytes = env.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
|
|
520
|
+
const bytes = measureOperationBytes(operation);
|
|
521
|
+
if (bytes > maxBytes) {
|
|
522
|
+
throw new OperationTooLargeError(input.collection, input.recordId, bytes, maxBytes);
|
|
523
|
+
}
|
|
524
|
+
scope.causal.record(input.collection, operation.id);
|
|
525
|
+
return operation;
|
|
526
|
+
}
|
|
527
|
+
async function appendOperationRow(scope, operation) {
|
|
528
|
+
const opInsert = buildInsertQuery(
|
|
529
|
+
`_kora_ops_${operation.collection}`,
|
|
530
|
+
serializeOperation(operation)
|
|
531
|
+
);
|
|
532
|
+
await scope.tx.execute(opInsert.sql, opInsert.params);
|
|
533
|
+
}
|
|
534
|
+
async function readLiveRow(scope, collection, id) {
|
|
535
|
+
const rows = await scope.tx.query(
|
|
536
|
+
`SELECT * FROM ${quoteIdent(collection)} WHERE id = ? AND _deleted = 0`,
|
|
537
|
+
[id]
|
|
538
|
+
);
|
|
539
|
+
return rows[0];
|
|
540
|
+
}
|
|
541
|
+
function requireDefinition(env, collection) {
|
|
542
|
+
const definition = env.schema.collections[collection];
|
|
543
|
+
if (!definition) {
|
|
544
|
+
throw new KoraError4(
|
|
545
|
+
`Unknown collection "${collection}". Available: ${Object.keys(env.schema.collections).join(", ")}`,
|
|
546
|
+
"UNKNOWN_COLLECTION",
|
|
547
|
+
{ collection }
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
return definition;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/mutations/execute-delete.ts
|
|
554
|
+
async function executeDelete(ctx, id, options) {
|
|
555
|
+
const result = await runLocalWrite(
|
|
556
|
+
ctx,
|
|
557
|
+
(scope) => writeDeleteInTx(ctx, scope, ctx.collection, id, {
|
|
558
|
+
skipReferentialEnforcement: options?.skipReferentialEnforcement ?? false,
|
|
559
|
+
extraCausalDeps: ctx.extraCausalDeps ?? []
|
|
560
|
+
})
|
|
561
|
+
);
|
|
562
|
+
return result.operation ? [result.operation, ...result.sideEffects] : result.sideEffects;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/mutations/execute-insert.ts
|
|
566
|
+
async function executeInsert(ctx, data) {
|
|
567
|
+
const prepared = await prepareInsert(ctx, ctx.collection, data);
|
|
568
|
+
const result = await runLocalWrite(
|
|
569
|
+
ctx,
|
|
570
|
+
(scope) => writeInsertInTx(ctx, scope, prepared, ctx.extraCausalDeps ?? [])
|
|
571
|
+
);
|
|
572
|
+
return result.record;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/mutations/execute-update.ts
|
|
576
|
+
import { validateRecord as validateRecord2 } from "@korajs/core";
|
|
577
|
+
async function executeUpdate(ctx, id, data) {
|
|
578
|
+
const validated = validateRecord2(ctx.collection, ctx.definition, data, "update");
|
|
579
|
+
const result = await runLocalWrite(
|
|
580
|
+
ctx,
|
|
581
|
+
(scope) => writeUpdateInTx(ctx, scope, ctx.collection, id, validated, ctx.extraCausalDeps ?? [])
|
|
582
|
+
);
|
|
583
|
+
return result.record;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export {
|
|
587
|
+
CausalScope,
|
|
588
|
+
withWriteScope,
|
|
589
|
+
isStorageFullError,
|
|
590
|
+
parseFieldVersions,
|
|
591
|
+
serializeFieldVersions,
|
|
592
|
+
fieldVersionsForFields,
|
|
593
|
+
effectiveFieldVersion,
|
|
594
|
+
resolvePerFieldLww,
|
|
595
|
+
InvalidStateTransitionError,
|
|
596
|
+
validateStateTransition,
|
|
597
|
+
validateUpdateStateMachine,
|
|
598
|
+
toAtRestWriteData,
|
|
599
|
+
prepareInsert,
|
|
600
|
+
writeInsertInTx,
|
|
601
|
+
writeUpdateInTx,
|
|
602
|
+
writeDeleteInTx,
|
|
603
|
+
executeDelete,
|
|
604
|
+
executeInsert,
|
|
605
|
+
executeUpdate
|
|
606
|
+
};
|
|
607
|
+
//# sourceMappingURL=chunk-HZTDBLDB.js.map
|