@korajs/store 0.6.0 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/adapters/better-sqlite3.cjs +94 -40
- package/dist/adapters/better-sqlite3.cjs.map +1 -1
- package/dist/adapters/better-sqlite3.d.cts +9 -1
- package/dist/adapters/better-sqlite3.d.ts +9 -1
- package/dist/adapters/better-sqlite3.js +62 -41
- package/dist/adapters/better-sqlite3.js.map +1 -1
- package/dist/adapters/indexeddb.cjs +24 -13
- package/dist/adapters/indexeddb.cjs.map +1 -1
- package/dist/adapters/indexeddb.d.cts +2 -2
- package/dist/adapters/indexeddb.d.ts +2 -2
- package/dist/adapters/indexeddb.js +4 -3
- package/dist/adapters/indexeddb.js.map +1 -1
- package/dist/adapters/sqlite-wasm.cjs +24 -13
- package/dist/adapters/sqlite-wasm.cjs.map +1 -1
- package/dist/adapters/sqlite-wasm.d.cts +27 -4
- package/dist/adapters/sqlite-wasm.d.ts +27 -4
- package/dist/adapters/sqlite-wasm.js +6 -4
- package/dist/{backup-JN3LB4YT.js → backup-ZV4Y424B.js} +3 -3
- package/dist/blob-fs.cjs +166 -0
- package/dist/blob-fs.cjs.map +1 -0
- package/dist/blob-fs.d.cts +29 -0
- package/dist/blob-fs.d.ts +29 -0
- package/dist/blob-fs.js +128 -0
- package/dist/blob-fs.js.map +1 -0
- package/dist/{chunk-56LL4XP6.js → chunk-6NKLDBTY.js} +52 -1
- package/dist/chunk-6NKLDBTY.js.map +1 -0
- package/dist/{chunk-AGN3CIPB.js → chunk-FFYA7J5H.js} +2 -2
- package/dist/{chunk-XO4LMPAH.js → chunk-IOEFNHHP.js} +2 -35
- package/dist/chunk-IOEFNHHP.js.map +1 -0
- package/dist/{chunk-LAWV6CFH.js → chunk-QL2XVZRN.js} +12 -1
- package/dist/chunk-QL2XVZRN.js.map +1 -0
- package/dist/{chunk-6AHYGQQT.js → chunk-RBPVEAVM.js} +4 -4
- package/dist/chunk-TVJEC5S2.js +40 -0
- package/dist/chunk-TVJEC5S2.js.map +1 -0
- package/dist/chunk-W4MTTNEZ.js +55 -0
- package/dist/chunk-W4MTTNEZ.js.map +1 -0
- package/dist/{chunk-EZSTD2XC.js → chunk-X57MWW35.js} +2 -2
- package/dist/{chunk-7BPETY5Y.js → chunk-ZLWAYQEQ.js} +128 -15
- package/dist/chunk-ZLWAYQEQ.js.map +1 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.cts +60 -0
- package/dist/content-addressed-blob-store-Dcj3SB9r.d.ts +60 -0
- package/dist/{export-audit-5GV6VRDZ.js → export-audit-AJ7RORNK.js} +3 -3
- package/dist/index.cjs +1245 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +468 -7
- package/dist/index.d.ts +468 -7
- package/dist/index.js +975 -61
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +123 -33
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +3 -3
- package/dist/{sqlite-wasm-channel-I9y_jObg.d.cts → sqlite-wasm-channel-DUDPclTE.d.cts} +2 -15
- package/dist/{sqlite-wasm-channel-I9y_jObg.d.ts → sqlite-wasm-channel-DUDPclTE.d.ts} +2 -15
- package/dist/sqlite-wasm-channel-H43KEZLM.js +12 -0
- package/dist/{types-CTr00g_d.d.cts → types-5h4Hqcwc.d.ts} +10 -3
- package/dist/{types-BMyHmwvn.d.ts → types-DfPiPwKr.d.cts} +10 -3
- package/dist/{types-DepnRDk_.d.cts → types-k6AZgiH4.d.cts} +48 -2
- package/dist/{types-DepnRDk_.d.ts → types-k6AZgiH4.d.ts} +48 -2
- package/package.json +15 -2
- package/dist/chunk-56LL4XP6.js.map +0 -1
- package/dist/chunk-7BPETY5Y.js.map +0 -1
- package/dist/chunk-LAWV6CFH.js.map +0 -1
- package/dist/chunk-XO4LMPAH.js.map +0 -1
- package/dist/sqlite-wasm-channel-HDDW6UBH.js +0 -10
- /package/dist/{backup-JN3LB4YT.js.map → backup-ZV4Y424B.js.map} +0 -0
- /package/dist/{chunk-AGN3CIPB.js.map → chunk-FFYA7J5H.js.map} +0 -0
- /package/dist/{chunk-6AHYGQQT.js.map → chunk-RBPVEAVM.js.map} +0 -0
- /package/dist/{chunk-EZSTD2XC.js.map → chunk-X57MWW35.js.map} +0 -0
- /package/dist/{export-audit-5GV6VRDZ.js.map → export-audit-AJ7RORNK.js.map} +0 -0
- /package/dist/{sqlite-wasm-channel-HDDW6UBH.js.map → sqlite-wasm-channel-H43KEZLM.js.map} +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
QueryError,
|
|
3
|
-
RecordNotFoundError
|
|
4
|
-
} from "./chunk-LAWV6CFH.js";
|
|
5
1
|
import {
|
|
6
2
|
deserializeRecord,
|
|
3
|
+
encodeRichtextFieldsForOpData,
|
|
7
4
|
serializeOperation,
|
|
8
5
|
serializeRecord
|
|
9
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-6NKLDBTY.js";
|
|
7
|
+
import {
|
|
8
|
+
QueryError,
|
|
9
|
+
RecordNotFoundError
|
|
10
|
+
} from "./chunk-QL2XVZRN.js";
|
|
10
11
|
|
|
11
12
|
// src/lww/row-version.ts
|
|
12
13
|
import { HybridLogicalClock } from "@korajs/core";
|
|
@@ -82,6 +83,28 @@ function buildUpdateQuery(collection, id, changes) {
|
|
|
82
83
|
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
83
84
|
return { sql, params };
|
|
84
85
|
}
|
|
86
|
+
function buildFieldFastForwardUpdateQuery(collection, id, fieldChanges, remoteVersion, wallTime, options) {
|
|
87
|
+
const fieldClauses = Object.keys(fieldChanges).map((col) => `${col} = ?`);
|
|
88
|
+
const setClauses = [
|
|
89
|
+
...fieldClauses,
|
|
90
|
+
"_version = CASE WHEN _version >= ? THEN _version ELSE ? END",
|
|
91
|
+
"_updated_at = CASE WHEN _updated_at >= ? THEN _updated_at ELSE ? END"
|
|
92
|
+
];
|
|
93
|
+
const params = [
|
|
94
|
+
...Object.values(fieldChanges),
|
|
95
|
+
remoteVersion,
|
|
96
|
+
remoteVersion,
|
|
97
|
+
wallTime,
|
|
98
|
+
wallTime
|
|
99
|
+
];
|
|
100
|
+
if (options?.maxCreatedAt !== void 0) {
|
|
101
|
+
setClauses.push("_created_at = CASE WHEN _created_at >= ? THEN _created_at ELSE ? END");
|
|
102
|
+
params.push(options.maxCreatedAt, options.maxCreatedAt);
|
|
103
|
+
}
|
|
104
|
+
params.push(id);
|
|
105
|
+
const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
|
|
106
|
+
return { sql, params };
|
|
107
|
+
}
|
|
85
108
|
function buildSoftDeleteQuery(collection, id, updatedAt, version) {
|
|
86
109
|
if (version !== void 0) {
|
|
87
110
|
return {
|
|
@@ -318,7 +341,65 @@ async function executeDelete(ctx, id, options) {
|
|
|
318
341
|
}
|
|
319
342
|
|
|
320
343
|
// src/mutations/execute-insert.ts
|
|
321
|
-
import {
|
|
344
|
+
import {
|
|
345
|
+
createOperation as createOperation2,
|
|
346
|
+
generateUUIDv7,
|
|
347
|
+
transformSecretFieldsForWrite,
|
|
348
|
+
validateRecord
|
|
349
|
+
} from "@korajs/core";
|
|
350
|
+
|
|
351
|
+
// src/lww/field-versions.ts
|
|
352
|
+
function parseFieldVersions(raw) {
|
|
353
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const parsed = JSON.parse(raw);
|
|
358
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
359
|
+
return {};
|
|
360
|
+
}
|
|
361
|
+
const result = {};
|
|
362
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
363
|
+
if (typeof value === "string") {
|
|
364
|
+
result[key] = value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
} catch {
|
|
369
|
+
return {};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function serializeFieldVersions(versions) {
|
|
373
|
+
return JSON.stringify(versions);
|
|
374
|
+
}
|
|
375
|
+
function fieldVersionsForFields(fieldNames, version) {
|
|
376
|
+
const versions = {};
|
|
377
|
+
for (const field of fieldNames) {
|
|
378
|
+
versions[field] = version;
|
|
379
|
+
}
|
|
380
|
+
return versions;
|
|
381
|
+
}
|
|
382
|
+
function effectiveFieldVersion(current, field, rowVersion) {
|
|
383
|
+
const own = current[field];
|
|
384
|
+
if (own !== void 0) {
|
|
385
|
+
return own;
|
|
386
|
+
}
|
|
387
|
+
return rowVersion ?? EMPTY_ROW_VERSION;
|
|
388
|
+
}
|
|
389
|
+
function resolvePerFieldLww(current, incomingFields, incomingVersion, rowVersion) {
|
|
390
|
+
const winners = [];
|
|
391
|
+
const merged = { ...current };
|
|
392
|
+
for (const field of incomingFields) {
|
|
393
|
+
const stored = effectiveFieldVersion(current, field, rowVersion);
|
|
394
|
+
if (incomingVersion > stored) {
|
|
395
|
+
winners.push(field);
|
|
396
|
+
merged[field] = incomingVersion;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return { winners, merged };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/mutations/execute-insert.ts
|
|
322
403
|
async function executeInsert(ctx, data) {
|
|
323
404
|
const validated = validateRecord(ctx.collection, ctx.definition, data, "insert");
|
|
324
405
|
const recordId = generateUUIDv7();
|
|
@@ -327,6 +408,11 @@ async function executeInsert(ctx, data) {
|
|
|
327
408
|
validated[fieldName] = Date.now();
|
|
328
409
|
}
|
|
329
410
|
}
|
|
411
|
+
const writeData = await transformSecretFieldsForWrite(
|
|
412
|
+
validated,
|
|
413
|
+
ctx.definition,
|
|
414
|
+
ctx.secretKeyProvider
|
|
415
|
+
);
|
|
330
416
|
const causalDeps = resolveCausalDeps(ctx);
|
|
331
417
|
let operation;
|
|
332
418
|
let record;
|
|
@@ -338,7 +424,10 @@ async function executeInsert(ctx, data) {
|
|
|
338
424
|
type: "insert",
|
|
339
425
|
collection: ctx.collection,
|
|
340
426
|
recordId,
|
|
341
|
-
|
|
427
|
+
// Binary richtext values are tagged as canonical JSON BEFORE the
|
|
428
|
+
// operation is content-hashed, so the hash input, persisted JSON,
|
|
429
|
+
// and wire payload are the identical value.
|
|
430
|
+
data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
|
|
342
431
|
previousData: null,
|
|
343
432
|
sequenceNumber,
|
|
344
433
|
causalDeps,
|
|
@@ -347,14 +436,19 @@ async function executeInsert(ctx, data) {
|
|
|
347
436
|
ctx.clock
|
|
348
437
|
);
|
|
349
438
|
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
350
|
-
const serializedData = serializeRecord(
|
|
439
|
+
const serializedData = serializeRecord(writeData, ctx.definition.fields);
|
|
351
440
|
const version = serializeRowVersion(operation.timestamp);
|
|
352
441
|
record = {
|
|
353
442
|
id: recordId,
|
|
354
443
|
...serializedData,
|
|
355
444
|
_created_at: operation.timestamp.wallTime,
|
|
356
445
|
_updated_at: operation.timestamp.wallTime,
|
|
357
|
-
_version: version
|
|
446
|
+
_version: version,
|
|
447
|
+
// Stamp every inserted field with this operation's version so later
|
|
448
|
+
// per-field LWW compares against a real writer, not the fallback.
|
|
449
|
+
_field_versions: serializeFieldVersions(
|
|
450
|
+
fieldVersionsForFields(Object.keys(serializedData), version)
|
|
451
|
+
)
|
|
358
452
|
};
|
|
359
453
|
const builtInsert = buildInsertQuery(ctx.collection, record);
|
|
360
454
|
const opInsert = buildInsertQuery(
|
|
@@ -367,7 +461,7 @@ async function executeInsert(ctx, data) {
|
|
|
367
461
|
ctx.onMutation(ctx.collection, operation);
|
|
368
462
|
return {
|
|
369
463
|
id: recordId,
|
|
370
|
-
...
|
|
464
|
+
...writeData,
|
|
371
465
|
createdAt: operation.timestamp.wallTime,
|
|
372
466
|
updatedAt: operation.timestamp.wallTime
|
|
373
467
|
};
|
|
@@ -461,6 +555,7 @@ import {
|
|
|
461
555
|
isAtomicOp,
|
|
462
556
|
resolveAtomicOp,
|
|
463
557
|
toAtomicOp,
|
|
558
|
+
transformSecretFieldsForWrite as transformSecretFieldsForWrite2,
|
|
464
559
|
validateRecord as validateRecord2
|
|
465
560
|
} from "@korajs/core";
|
|
466
561
|
async function executeUpdate(ctx, id, data) {
|
|
@@ -497,6 +592,11 @@ async function executeUpdate(ctx, id, data) {
|
|
|
497
592
|
resolvedData[key] = value;
|
|
498
593
|
}
|
|
499
594
|
}
|
|
595
|
+
const writeData = await transformSecretFieldsForWrite2(
|
|
596
|
+
resolvedData,
|
|
597
|
+
ctx.definition,
|
|
598
|
+
ctx.secretKeyProvider
|
|
599
|
+
);
|
|
500
600
|
const hasAtomicOps = Object.keys(atomicOps).length > 0;
|
|
501
601
|
const causalDeps = resolveCausalDeps(ctx);
|
|
502
602
|
let operation;
|
|
@@ -508,8 +608,11 @@ async function executeUpdate(ctx, id, data) {
|
|
|
508
608
|
type: "update",
|
|
509
609
|
collection: ctx.collection,
|
|
510
610
|
recordId: id,
|
|
511
|
-
|
|
512
|
-
|
|
611
|
+
// Binary richtext values (new and previous) are tagged as canonical
|
|
612
|
+
// JSON BEFORE the operation is content-hashed, so the hash input,
|
|
613
|
+
// persisted JSON, and wire payload are the identical value.
|
|
614
|
+
data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
|
|
615
|
+
previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
|
|
513
616
|
sequenceNumber,
|
|
514
617
|
causalDeps,
|
|
515
618
|
schemaVersion: ctx.schema.version,
|
|
@@ -518,12 +621,17 @@ async function executeUpdate(ctx, id, data) {
|
|
|
518
621
|
ctx.clock
|
|
519
622
|
);
|
|
520
623
|
ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
|
|
521
|
-
const serializedChanges = serializeRecord(
|
|
624
|
+
const serializedChanges = serializeRecord(writeData, ctx.definition.fields);
|
|
522
625
|
const version = serializeRowVersion(operation.timestamp);
|
|
626
|
+
const mergedFieldVersions = parseFieldVersions(currentRow._field_versions);
|
|
627
|
+
for (const changedField of Object.keys(serializedChanges)) {
|
|
628
|
+
mergedFieldVersions[changedField] = version;
|
|
629
|
+
}
|
|
523
630
|
const updateQuery = buildUpdateQuery(ctx.collection, id, {
|
|
524
631
|
...serializedChanges,
|
|
525
632
|
_updated_at: operation.timestamp.wallTime,
|
|
526
|
-
_version: version
|
|
633
|
+
_version: version,
|
|
634
|
+
_field_versions: serializeFieldVersions(mergedFieldVersions)
|
|
527
635
|
});
|
|
528
636
|
const opInsert = buildInsertQuery(
|
|
529
637
|
`_kora_ops_${ctx.collection}`,
|
|
@@ -552,6 +660,7 @@ export {
|
|
|
552
660
|
buildCountQuery,
|
|
553
661
|
buildInsertQuery,
|
|
554
662
|
buildUpdateQuery,
|
|
663
|
+
buildFieldFastForwardUpdateQuery,
|
|
555
664
|
buildSoftDeleteQuery,
|
|
556
665
|
buildLwwUpdateQuery,
|
|
557
666
|
buildLwwSoftDeleteQuery,
|
|
@@ -561,10 +670,14 @@ export {
|
|
|
561
670
|
readSequenceNumber,
|
|
562
671
|
resolveCausalDeps,
|
|
563
672
|
executeDelete,
|
|
673
|
+
parseFieldVersions,
|
|
674
|
+
serializeFieldVersions,
|
|
675
|
+
fieldVersionsForFields,
|
|
676
|
+
resolvePerFieldLww,
|
|
564
677
|
executeInsert,
|
|
565
678
|
InvalidStateTransitionError,
|
|
566
679
|
validateStateTransition,
|
|
567
680
|
validateUpdateStateMachine,
|
|
568
681
|
executeUpdate
|
|
569
682
|
};
|
|
570
|
-
//# sourceMappingURL=chunk-
|
|
683
|
+
//# sourceMappingURL=chunk-ZLWAYQEQ.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 { 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 ${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 `${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 ${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 sql = `INSERT INTO ${collection} (${columns.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) => `${col} = ?`)\n\tconst params = [...Object.values(changes), id]\n\n\tconst sql = `UPDATE ${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) => `${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 ${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 ${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 ${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) => `${col} = ?`)\n\tconst lww = lwwVersionWhereClause(remoteVersion)\n\tconst sql = `UPDATE ${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 ${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\tswitch (operator) {\n\t\tcase '$eq':\n\t\t\tif (sqlValue === null) {\n\t\t\t\treturn `${fieldName} IS NULL`\n\t\t\t}\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${fieldName} = ?`\n\t\tcase '$ne':\n\t\t\tif (sqlValue === null) {\n\t\t\t\treturn `${fieldName} IS NOT NULL`\n\t\t\t}\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${fieldName} != ?`\n\t\tcase '$gt':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${fieldName} > ?`\n\t\tcase '$gte':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${fieldName} >= ?`\n\t\tcase '$lt':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${fieldName} < ?`\n\t\tcase '$lte':\n\t\t\tparams.push(sqlValue)\n\t\t\treturn `${fieldName} <= ?`\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 `${fieldName} 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 } 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 ${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\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 ${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 ${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":["createOperation","createOperation","createOperation","transformSecretFieldsForWrite","validateRecord","validateRecord","transformSecretFieldsForWrite","createOperation"]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BlobRefMetadata, BlobRef } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a blob read fails its integrity check: the bytes stored under a
|
|
5
|
+
* hash do not actually hash to that value (corruption or tampering).
|
|
6
|
+
*/
|
|
7
|
+
declare class BlobIntegrityError extends Error {
|
|
8
|
+
readonly expectedHash: string;
|
|
9
|
+
readonly actualHash: string;
|
|
10
|
+
constructor(expectedHash: string, actualHash: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A content-addressed store for the bytes behind `blob` fields.
|
|
14
|
+
*
|
|
15
|
+
* Blobs are keyed by the SHA-256 hash of their content, so identical content is
|
|
16
|
+
* stored exactly once (deduplication) and a peer never needs to transfer bytes
|
|
17
|
+
* it already holds. Reads are integrity-checked: the returned bytes are verified
|
|
18
|
+
* to hash to the requested key.
|
|
19
|
+
*/
|
|
20
|
+
interface ContentAddressedBlobStore {
|
|
21
|
+
/**
|
|
22
|
+
* Store bytes and return their content-addressed reference. Storing content
|
|
23
|
+
* that already exists is a no-op that returns the same reference (dedup).
|
|
24
|
+
*/
|
|
25
|
+
put(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>;
|
|
26
|
+
/**
|
|
27
|
+
* Retrieve the bytes for a hash, or null if absent. The returned bytes are
|
|
28
|
+
* verified to hash to the requested value; a mismatch throws
|
|
29
|
+
* {@link BlobIntegrityError} rather than returning corrupt data.
|
|
30
|
+
*/
|
|
31
|
+
get(hash: string): Promise<Uint8Array | null>;
|
|
32
|
+
/** Whether the store holds content for the given hash. */
|
|
33
|
+
has(hash: string): Promise<boolean>;
|
|
34
|
+
/** Remove the content for a hash. Returns whether anything was removed. */
|
|
35
|
+
delete(hash: string): Promise<boolean>;
|
|
36
|
+
/** Number of distinct blobs held. */
|
|
37
|
+
size(): Promise<number>;
|
|
38
|
+
/**
|
|
39
|
+
* List the hashes of every blob currently held. Used by garbage collection to
|
|
40
|
+
* find content no live reference points to. Order is unspecified.
|
|
41
|
+
*/
|
|
42
|
+
list(): Promise<string[]>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* In-memory {@link ContentAddressedBlobStore}. The reference backend used for
|
|
46
|
+
* tests and for environments without a persistent blob backend yet. Persistent
|
|
47
|
+
* backends (OPFS client-side, filesystem/S3 server-side) implement the same
|
|
48
|
+
* interface.
|
|
49
|
+
*/
|
|
50
|
+
declare class MemoryBlobStore implements ContentAddressedBlobStore {
|
|
51
|
+
private readonly blobs;
|
|
52
|
+
put(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>;
|
|
53
|
+
get(hash: string): Promise<Uint8Array | null>;
|
|
54
|
+
has(hash: string): Promise<boolean>;
|
|
55
|
+
delete(hash: string): Promise<boolean>;
|
|
56
|
+
size(): Promise<number>;
|
|
57
|
+
list(): Promise<string[]>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { BlobIntegrityError as B, type ContentAddressedBlobStore as C, MemoryBlobStore as M };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BlobRefMetadata, BlobRef } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a blob read fails its integrity check: the bytes stored under a
|
|
5
|
+
* hash do not actually hash to that value (corruption or tampering).
|
|
6
|
+
*/
|
|
7
|
+
declare class BlobIntegrityError extends Error {
|
|
8
|
+
readonly expectedHash: string;
|
|
9
|
+
readonly actualHash: string;
|
|
10
|
+
constructor(expectedHash: string, actualHash: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A content-addressed store for the bytes behind `blob` fields.
|
|
14
|
+
*
|
|
15
|
+
* Blobs are keyed by the SHA-256 hash of their content, so identical content is
|
|
16
|
+
* stored exactly once (deduplication) and a peer never needs to transfer bytes
|
|
17
|
+
* it already holds. Reads are integrity-checked: the returned bytes are verified
|
|
18
|
+
* to hash to the requested key.
|
|
19
|
+
*/
|
|
20
|
+
interface ContentAddressedBlobStore {
|
|
21
|
+
/**
|
|
22
|
+
* Store bytes and return their content-addressed reference. Storing content
|
|
23
|
+
* that already exists is a no-op that returns the same reference (dedup).
|
|
24
|
+
*/
|
|
25
|
+
put(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>;
|
|
26
|
+
/**
|
|
27
|
+
* Retrieve the bytes for a hash, or null if absent. The returned bytes are
|
|
28
|
+
* verified to hash to the requested value; a mismatch throws
|
|
29
|
+
* {@link BlobIntegrityError} rather than returning corrupt data.
|
|
30
|
+
*/
|
|
31
|
+
get(hash: string): Promise<Uint8Array | null>;
|
|
32
|
+
/** Whether the store holds content for the given hash. */
|
|
33
|
+
has(hash: string): Promise<boolean>;
|
|
34
|
+
/** Remove the content for a hash. Returns whether anything was removed. */
|
|
35
|
+
delete(hash: string): Promise<boolean>;
|
|
36
|
+
/** Number of distinct blobs held. */
|
|
37
|
+
size(): Promise<number>;
|
|
38
|
+
/**
|
|
39
|
+
* List the hashes of every blob currently held. Used by garbage collection to
|
|
40
|
+
* find content no live reference points to. Order is unspecified.
|
|
41
|
+
*/
|
|
42
|
+
list(): Promise<string[]>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* In-memory {@link ContentAddressedBlobStore}. The reference backend used for
|
|
46
|
+
* tests and for environments without a persistent blob backend yet. Persistent
|
|
47
|
+
* backends (OPFS client-side, filesystem/S3 server-side) implement the same
|
|
48
|
+
* interface.
|
|
49
|
+
*/
|
|
50
|
+
declare class MemoryBlobStore implements ContentAddressedBlobStore {
|
|
51
|
+
private readonly blobs;
|
|
52
|
+
put(bytes: Uint8Array, metadata?: BlobRefMetadata): Promise<BlobRef>;
|
|
53
|
+
get(hash: string): Promise<Uint8Array | null>;
|
|
54
|
+
has(hash: string): Promise<boolean>;
|
|
55
|
+
delete(hash: string): Promise<boolean>;
|
|
56
|
+
size(): Promise<number>;
|
|
57
|
+
list(): Promise<string[]>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { BlobIntegrityError as B, type ContentAddressedBlobStore as C, MemoryBlobStore as M };
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
exportAudit,
|
|
4
4
|
readAuditExportManifest,
|
|
5
5
|
verifyAuditExportChecksum
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-FFYA7J5H.js";
|
|
7
7
|
import "./chunk-MYSJN3DF.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-6NKLDBTY.js";
|
|
9
9
|
export {
|
|
10
10
|
decodeAuditExport,
|
|
11
11
|
exportAudit,
|
|
12
12
|
readAuditExportManifest,
|
|
13
13
|
verifyAuditExportChecksum
|
|
14
14
|
};
|
|
15
|
-
//# sourceMappingURL=export-audit-
|
|
15
|
+
//# sourceMappingURL=export-audit-AJ7RORNK.js.map
|