@korajs/core 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.
@@ -49,6 +49,24 @@ var OperationError = class extends KoraError {
49
49
  this.name = "OperationError";
50
50
  }
51
51
  };
52
+ var OperationTooLargeError = class extends KoraError {
53
+ constructor(collection, recordId, bytes, maxBytes) {
54
+ super(
55
+ `A write to ${collection}/${recordId} would produce an operation of ${bytes} bytes, more than the ${maxBytes}-byte limit (maxOperationBytes). Nothing was written. Store large content as a blob (t.blob()) or split it across records.`,
56
+ "OPERATION_TOO_LARGE",
57
+ { collection, recordId, bytes, maxBytes }
58
+ );
59
+ this.collection = collection;
60
+ this.recordId = recordId;
61
+ this.bytes = bytes;
62
+ this.maxBytes = maxBytes;
63
+ this.name = "OperationTooLargeError";
64
+ }
65
+ collection;
66
+ recordId;
67
+ bytes;
68
+ maxBytes;
69
+ };
52
70
  var MergeConflictError = class extends KoraError {
53
71
  constructor(operationA, operationB, field) {
54
72
  super(
@@ -343,8 +361,118 @@ var HybridLogicalClock = class _HybridLogicalClock {
343
361
  }
344
362
  };
345
363
 
364
+ // src/operations/op-data-binary.ts
365
+ function isKoraBytesValue(value) {
366
+ if (typeof value !== "object" || value === null) {
367
+ return false;
368
+ }
369
+ const record = value;
370
+ return Object.keys(record).length === 1 && typeof record.$koraBytes === "string";
371
+ }
372
+ function isLegacyNumericByteObject(value) {
373
+ if (typeof value !== "object" || value === null || ArrayBuffer.isView(value)) {
374
+ return false;
375
+ }
376
+ const record = value;
377
+ const keys = Object.keys(record);
378
+ if (keys.length === 0) {
379
+ return false;
380
+ }
381
+ for (let i = 0; i < keys.length; i++) {
382
+ const byte = record[String(i)];
383
+ if (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255) {
384
+ return false;
385
+ }
386
+ }
387
+ return true;
388
+ }
389
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
390
+ var BASE64_REVERSE = new Map(
391
+ Array.from(BASE64_ALPHABET, (char, index) => [char, index])
392
+ );
393
+ function bytesToBase64(bytes) {
394
+ let out = "";
395
+ for (let i = 0; i < bytes.length; i += 3) {
396
+ const b0 = bytes[i] ?? 0;
397
+ const b1 = bytes[i + 1] ?? 0;
398
+ const b2 = bytes[i + 2] ?? 0;
399
+ const triple = b0 << 16 | b1 << 8 | b2;
400
+ out += BASE64_ALPHABET[triple >> 18 & 63] ?? "";
401
+ out += BASE64_ALPHABET[triple >> 12 & 63] ?? "";
402
+ out += i + 1 < bytes.length ? BASE64_ALPHABET[triple >> 6 & 63] ?? "" : "=";
403
+ out += i + 2 < bytes.length ? BASE64_ALPHABET[triple & 63] ?? "" : "=";
404
+ }
405
+ return out;
406
+ }
407
+ function base64ToBytes(base64) {
408
+ const cleaned = base64.replace(/=+$/, "");
409
+ const out = new Uint8Array(Math.floor(cleaned.length * 6 / 8));
410
+ let buffer = 0;
411
+ let bits = 0;
412
+ let index = 0;
413
+ for (const char of cleaned) {
414
+ const value = BASE64_REVERSE.get(char);
415
+ if (value === void 0) {
416
+ throw new OperationError(`Invalid base64 character "${char}" in tagged binary value.`, {
417
+ char
418
+ });
419
+ }
420
+ buffer = buffer << 6 | value;
421
+ bits += 6;
422
+ if (bits >= 8) {
423
+ bits -= 8;
424
+ out[index] = buffer >> bits & 255;
425
+ index += 1;
426
+ }
427
+ }
428
+ return out;
429
+ }
430
+ function encodeBytesForOpData(value) {
431
+ if (typeof value === "string") {
432
+ return value;
433
+ }
434
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
435
+ return { $koraBytes: bytesToBase64(bytes) };
436
+ }
437
+ function decodeBytesFromOpData(value) {
438
+ if (typeof value === "string") {
439
+ return value;
440
+ }
441
+ if (value instanceof Uint8Array) {
442
+ return value;
443
+ }
444
+ if (value instanceof ArrayBuffer) {
445
+ return new Uint8Array(value);
446
+ }
447
+ if (isKoraBytesValue(value)) {
448
+ return base64ToBytes(value.$koraBytes);
449
+ }
450
+ if (isLegacyNumericByteObject(value)) {
451
+ const keys = Object.keys(value);
452
+ const bytes = new Uint8Array(keys.length);
453
+ for (let i = 0; i < keys.length; i++) {
454
+ bytes[i] = value[String(i)] ?? 0;
455
+ }
456
+ return bytes;
457
+ }
458
+ throw new OperationError(
459
+ "Binary op-data value must be a string, Uint8Array, ArrayBuffer, tagged { $koraBytes } object, or legacy numeric-key byte object.",
460
+ { receivedType: typeof value }
461
+ );
462
+ }
463
+
346
464
  // src/operations/content-hash.ts
347
- async function computeOperationId(input, timestamp) {
465
+ var DEFAULT_OPERATION_HASH_VERSION = 2;
466
+ async function computeOperationId(input, timestampOrVersion) {
467
+ if (typeof timestampOrVersion === "string") {
468
+ return sha256Hex(canonicalize(v1HashInput(input, timestampOrVersion)));
469
+ }
470
+ const op = input;
471
+ const serializedTs = HybridLogicalClock.serialize(op.timestamp);
472
+ if (timestampOrVersion === 1) return sha256Hex(canonicalize(v1HashInput(op, serializedTs)));
473
+ return sha256Hex(canonicalize(v2HashInput(op, serializedTs)));
474
+ }
475
+ function v1HashInput(input, timestamp) {
348
476
  const hashInput = {
349
477
  type: input.type,
350
478
  collection: input.collection,
@@ -356,8 +484,30 @@ async function computeOperationId(input, timestamp) {
356
484
  if (input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0) {
357
485
  hashInput.atomicOps = input.atomicOps;
358
486
  }
359
- const canonical = canonicalize(hashInput);
360
- const encoded = new TextEncoder().encode(canonical);
487
+ return hashInput;
488
+ }
489
+ function v2HashInput(op, timestamp) {
490
+ const body = canonicalizeOperationBody(op);
491
+ const atomicOps = body.atomicOps !== void 0 && Object.keys(body.atomicOps).length > 0 ? body.atomicOps : null;
492
+ return {
493
+ hashVersion: 2,
494
+ type: op.type,
495
+ collection: op.collection,
496
+ recordId: op.recordId,
497
+ data: body.data,
498
+ previousData: body.previousData,
499
+ timestamp,
500
+ nodeId: op.nodeId,
501
+ sequenceNumber: op.sequenceNumber,
502
+ // A set of parents: their order carries no meaning, so a relay that reorders
503
+ // them neither breaks verification nor creates a second id for one op.
504
+ causalDeps: [...op.causalDeps].sort(),
505
+ schemaVersion: op.schemaVersion,
506
+ atomicOps
507
+ };
508
+ }
509
+ async function sha256Hex(canonical2) {
510
+ const encoded = new TextEncoder().encode(canonical2);
361
511
  const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
362
512
  return bufferToHex(hashBuffer);
363
513
  }
@@ -388,11 +538,12 @@ function bufferToHex(buffer) {
388
538
  }
389
539
 
390
540
  // src/operations/operation.ts
391
- async function createOperation(input, clock) {
392
- validateOperationParams(input);
541
+ async function createOperation(rawInput, clock, options = {}) {
542
+ validateOperationParams(rawInput);
543
+ const input = canonicalizeOperationBody(rawInput);
544
+ const hashVersion = options.hashVersion ?? DEFAULT_OPERATION_HASH_VERSION;
393
545
  const timestamp = clock.now();
394
- const serializedTs = HybridLogicalClock.serialize(timestamp);
395
- const id = await computeOperationId(input, serializedTs);
546
+ const id = hashVersion === 1 ? await computeOperationId(input, HybridLogicalClock.serialize(timestamp)) : await computeOperationId({ ...input, timestamp }, hashVersion);
396
547
  const operation = {
397
548
  id,
398
549
  nodeId: input.nodeId,
@@ -407,7 +558,8 @@ async function createOperation(input, clock) {
407
558
  schemaVersion: input.schemaVersion,
408
559
  ...input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0 ? { atomicOps: { ...input.atomicOps } } : {},
409
560
  ...input.transactionId !== void 0 ? { transactionId: input.transactionId } : {},
410
- ...input.mutationName !== void 0 ? { mutationName: input.mutationName } : {}
561
+ ...input.mutationName !== void 0 ? { mutationName: input.mutationName } : {},
562
+ ...hashVersion !== 1 ? { hashVersion } : {}
411
563
  };
412
564
  return deepFreeze(operation);
413
565
  }
@@ -475,23 +627,35 @@ function validateOperationParams(input) {
475
627
  });
476
628
  }
477
629
  }
478
- async function verifyOperationIntegrity(op) {
479
- const input = {
480
- nodeId: op.nodeId,
481
- type: op.type,
482
- collection: op.collection,
483
- recordId: op.recordId,
484
- data: op.data,
485
- previousData: op.previousData,
486
- sequenceNumber: op.sequenceNumber,
487
- causalDeps: op.causalDeps,
488
- schemaVersion: op.schemaVersion,
489
- ...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
490
- };
491
- const serializedTs = HybridLogicalClock.serialize(op.timestamp);
492
- const expectedId = await computeOperationId(input, serializedTs);
630
+ async function verifyOperationId(op) {
631
+ const version = op.hashVersion ?? 1;
632
+ if (version !== 1 && version !== 2) return false;
633
+ let expectedId;
634
+ try {
635
+ expectedId = await computeOperationId(
636
+ {
637
+ nodeId: op.nodeId,
638
+ type: op.type,
639
+ collection: op.collection,
640
+ recordId: op.recordId,
641
+ data: op.data,
642
+ previousData: op.previousData,
643
+ timestamp: op.timestamp,
644
+ sequenceNumber: op.sequenceNumber,
645
+ causalDeps: op.causalDeps,
646
+ schemaVersion: op.schemaVersion,
647
+ ...op.atomicOps !== void 0 ? { atomicOps: op.atomicOps } : {}
648
+ },
649
+ version
650
+ );
651
+ } catch {
652
+ return false;
653
+ }
493
654
  return op.id === expectedId;
494
655
  }
656
+ async function verifyOperationIntegrity(op) {
657
+ return verifyOperationId(op);
658
+ }
495
659
  function isValidOperation(value) {
496
660
  if (typeof value !== "object" || value === null) return false;
497
661
  const op = value;
@@ -509,6 +673,456 @@ function deepFreeze(obj) {
509
673
  return obj;
510
674
  }
511
675
 
676
+ // src/operations/canonical-body.ts
677
+ var NonCanonicalValueError = class extends KoraError {
678
+ constructor(message, path, receivedType) {
679
+ super(message, "NON_CANONICAL_VALUE", { path, receivedType });
680
+ this.path = path;
681
+ this.receivedType = receivedType;
682
+ this.name = "NonCanonicalValueError";
683
+ }
684
+ path;
685
+ receivedType;
686
+ };
687
+ function canonicalizeOperationBody(input) {
688
+ const isUpdate = input.type === "update";
689
+ const where = `${input.collection}/${input.recordId}`;
690
+ const data = canonicalTopLevel(input.data, isUpdate, "data", where);
691
+ const previousData = canonicalTopLevel(input.previousData, isUpdate, "previousData", where);
692
+ let atomicOps = input.atomicOps;
693
+ if (atomicOps !== void 0) {
694
+ const canonical2 = canonicalTopLevel(
695
+ atomicOps,
696
+ false,
697
+ "atomicOps",
698
+ where
699
+ );
700
+ atomicOps = canonical2 ?? void 0;
701
+ }
702
+ if (data === input.data && previousData === input.previousData && atomicOps === input.atomicOps) {
703
+ return input;
704
+ }
705
+ return {
706
+ ...input,
707
+ data,
708
+ previousData,
709
+ ...atomicOps !== void 0 ? { atomicOps } : {}
710
+ };
711
+ }
712
+ function canonicalValue(value, path) {
713
+ return canonical(value, path, /* @__PURE__ */ new Set());
714
+ }
715
+ function canonicalTopLevel(record, undefinedIsNull, label, where) {
716
+ if (record === null || record === void 0) return null;
717
+ if (!isPlainObject(record)) {
718
+ throw new NonCanonicalValueError(
719
+ `Operation ${label} of ${where} must be a plain object, got ${describe(record)}.`,
720
+ label,
721
+ describe(record)
722
+ );
723
+ }
724
+ let changed = false;
725
+ const out = {};
726
+ for (const [key, member] of Object.entries(record)) {
727
+ if (member === void 0) {
728
+ changed = true;
729
+ if (undefinedIsNull) out[key] = null;
730
+ continue;
731
+ }
732
+ const next = canonical(member, `${where}.${key}`, /* @__PURE__ */ new Set());
733
+ if (!Object.is(next, member)) changed = true;
734
+ out[key] = next;
735
+ }
736
+ if (Object.getPrototypeOf(record) !== Object.prototype) changed = true;
737
+ return changed ? out : record;
738
+ }
739
+ function canonical(value, path, ancestors) {
740
+ switch (typeof value) {
741
+ case "string":
742
+ case "boolean":
743
+ return value;
744
+ case "number":
745
+ if (!Number.isFinite(value)) {
746
+ throw refusal(path, String(value), 'Use a finite number, or null for "no value".');
747
+ }
748
+ return Object.is(value, -0) ? 0 : value;
749
+ case "undefined":
750
+ return null;
751
+ case "bigint":
752
+ throw refusal(path, "bigint", "Convert it to a number or a string.");
753
+ case "function":
754
+ throw refusal(path, "function", "Store data only, not functions.");
755
+ case "symbol":
756
+ throw refusal(path, "symbol", "Use a string instead.");
757
+ }
758
+ if (value === null) return null;
759
+ const object = value;
760
+ if (object instanceof Uint8Array) return { $koraBytes: bytesToBase64(object) };
761
+ if (object instanceof ArrayBuffer) return { $koraBytes: bytesToBase64(new Uint8Array(object)) };
762
+ if (object instanceof Date) {
763
+ const time = object.getTime();
764
+ if (Number.isNaN(time)) {
765
+ throw refusal(path, "invalid Date", "Pass a valid Date, or null.");
766
+ }
767
+ return object.toISOString();
768
+ }
769
+ if (ancestors.has(object)) {
770
+ throw refusal(path, "circular reference", "Remove the cycle: JSON cannot hold it.");
771
+ }
772
+ if (Array.isArray(object)) {
773
+ ancestors.add(object);
774
+ let changed2 = false;
775
+ const out2 = [];
776
+ for (let index = 0; index < object.length; index++) {
777
+ if (!(index in object)) {
778
+ ancestors.delete(object);
779
+ throw refusal(
780
+ `${path}[${String(index)}]`,
781
+ "array hole",
782
+ "Use a dense array (fill the gap with null)."
783
+ );
784
+ }
785
+ const item = object[index];
786
+ const next = canonical(item, `${path}[${String(index)}]`, ancestors);
787
+ if (!Object.is(next, item)) changed2 = true;
788
+ out2.push(next);
789
+ }
790
+ ancestors.delete(object);
791
+ return changed2 ? out2 : object;
792
+ }
793
+ if (!isPlainObject(object)) {
794
+ const type = describe(object);
795
+ throw refusal(
796
+ path,
797
+ type,
798
+ type === "Map" ? "Convert it with Object.fromEntries(map)." : type === "Set" ? "Convert it with [...set]." : "Convert it to a plain object (JSON data only)."
799
+ );
800
+ }
801
+ if (typeof object.toJSON === "function") {
802
+ throw refusal(path, "object with toJSON", "Store the value toJSON() returns instead.");
803
+ }
804
+ if (Object.prototype.hasOwnProperty.call(object, "__proto__")) {
805
+ throw refusal(`${path}.__proto__`, '"__proto__" key', "Rename the key.");
806
+ }
807
+ ancestors.add(object);
808
+ let changed = Object.getPrototypeOf(object) !== Object.prototype;
809
+ const out = {};
810
+ for (const [key, member] of Object.entries(object)) {
811
+ if (member === void 0) {
812
+ changed = true;
813
+ continue;
814
+ }
815
+ const next = canonical(member, `${path}.${key}`, ancestors);
816
+ if (!Object.is(next, member)) changed = true;
817
+ out[key] = next;
818
+ }
819
+ ancestors.delete(object);
820
+ return changed ? out : object;
821
+ }
822
+ function refusal(path, receivedType, fix) {
823
+ return new NonCanonicalValueError(
824
+ `Value at "${path}" (${receivedType}) has no JSON form, so it cannot be stored or synced unchanged. ${fix}`,
825
+ path,
826
+ receivedType
827
+ );
828
+ }
829
+ function isPlainObject(value) {
830
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
831
+ const proto = Object.getPrototypeOf(value);
832
+ return proto === Object.prototype || proto === null;
833
+ }
834
+ function describe(value) {
835
+ if (value === null) return "null";
836
+ if (Array.isArray(value)) return "array";
837
+ if (typeof value !== "object") return typeof value;
838
+ const ctor = value.constructor;
839
+ return typeof ctor?.name === "string" && ctor.name.length > 0 ? ctor.name : "object";
840
+ }
841
+ async function canonicalizeProvenLegacyClear(op) {
842
+ const restored = canonicalizeLegacyOperation(op);
843
+ if (restored === op) return op;
844
+ if (await verifyOperationId(op)) return op;
845
+ if (await verifyOperationId(restored)) return restored;
846
+ if (op.hashVersion === void 0 && await verifyOperationId({ ...restored, hashVersion: 2 })) {
847
+ return restored;
848
+ }
849
+ return op;
850
+ }
851
+ function canonicalizeLegacyOperation(op) {
852
+ if (op.type !== "update" || op.hashVersion === 2 || op.encrypted !== void 0) return op;
853
+ const previous = op.previousData;
854
+ if (previous === null || previous === void 0) return op;
855
+ const data = op.data ?? {};
856
+ const missing = Object.keys(previous).filter((key) => !(key in data));
857
+ if (missing.length === 0) return op;
858
+ const out = { ...data };
859
+ for (const key of missing) out[key] = null;
860
+ return { ...op, data: out };
861
+ }
862
+
863
+ // src/fold/field-kind.ts
864
+ var LWW_PLAN = {
865
+ kind: "reg",
866
+ appendOnly: false,
867
+ strategy: "lww",
868
+ tier: 1,
869
+ secret: false
870
+ };
871
+ function planField(collection, field) {
872
+ const descriptor = collection?.fields[field];
873
+ const secret = descriptor?.kind === "secret";
874
+ const resolver = collection?.resolvers?.[field];
875
+ if (resolver) {
876
+ return { kind: "res", appendOnly: false, resolver, strategy: "custom", tier: 3, secret };
877
+ }
878
+ if (!descriptor) return LWW_PLAN;
879
+ switch (descriptor.mergeStrategy) {
880
+ case "counter":
881
+ return { kind: "ctr", appendOnly: false, strategy: "schema-counter", tier: 1, secret };
882
+ case "max":
883
+ return { kind: "max", appendOnly: false, strategy: "schema-max", tier: 1, secret };
884
+ case "min":
885
+ return { kind: "min", appendOnly: false, strategy: "schema-min", tier: 1, secret };
886
+ case "append-only":
887
+ if (descriptor.kind === "array") {
888
+ return {
889
+ kind: "set",
890
+ appendOnly: true,
891
+ strategy: "schema-append-only",
892
+ tier: 1,
893
+ secret
894
+ };
895
+ }
896
+ return { ...LWW_PLAN, secret };
897
+ case "lww":
898
+ return { ...LWW_PLAN, secret };
899
+ case "server-authoritative":
900
+ return {
901
+ ...LWW_PLAN,
902
+ strategy: "schema-server-authoritative",
903
+ secret,
904
+ authoritative: true
905
+ };
906
+ default:
907
+ break;
908
+ }
909
+ switch (descriptor.kind) {
910
+ case "array":
911
+ return { kind: "set", appendOnly: false, strategy: "lww-element-set", tier: 1, secret };
912
+ case "object":
913
+ case "json":
914
+ return { kind: "map", appendOnly: false, strategy: "object-key-lww", tier: 1, secret };
915
+ case "richtext":
916
+ return { kind: "rt", appendOnly: false, strategy: "crdt-text", tier: 1, secret };
917
+ default:
918
+ return { ...LWW_PLAN, secret };
919
+ }
920
+ }
921
+
922
+ // src/fold/stamp.ts
923
+ function compareStamps(a, b) {
924
+ const ca = a.c ?? 0;
925
+ const cb = b.c ?? 0;
926
+ if (ca !== cb) return ca - cb;
927
+ if (a.t !== b.t) return a.t < b.t ? -1 : 1;
928
+ if (a.o !== b.o) return a.o < b.o ? -1 : 1;
929
+ return 0;
930
+ }
931
+ function maxStamp(a, b) {
932
+ if (a === null) return b;
933
+ if (b === null) return a;
934
+ return compareStamps(a, b) >= 0 ? a : b;
935
+ }
936
+ function minStamp(a, b) {
937
+ if (a === null) return b;
938
+ if (b === null) return a;
939
+ return compareStamps(a, b) <= 0 ? a : b;
940
+ }
941
+ function isAfter(a, b) {
942
+ if (a === null) return false;
943
+ if (b === null) return true;
944
+ return compareStamps(a, b) > 0;
945
+ }
946
+ function stampOf(timestamp, opId) {
947
+ return { t: HybridLogicalClock.serialize(timestamp), o: opId };
948
+ }
949
+ function stampTimestamp(stamp) {
950
+ return HybridLogicalClock.deserialize(stamp.t);
951
+ }
952
+
953
+ // src/schema/quote-ident.ts
954
+ function quoteIdent(name) {
955
+ return `"${name.replace(/"/g, '""')}"`;
956
+ }
957
+
958
+ // src/schema/sql-gen.ts
959
+ function generateSQL(collectionName, collection, relations) {
960
+ const statements = [];
961
+ const columns = ["id TEXT PRIMARY KEY NOT NULL"];
962
+ const indexedFields = new Set(collection.indexes);
963
+ const fkFields = [];
964
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
965
+ let colDef = columnDefinition(fieldName, descriptor);
966
+ if (relations) {
967
+ for (const rel of Object.values(relations)) {
968
+ if (rel.from === collectionName && rel.field === fieldName) {
969
+ colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
970
+ fkFields.push(fieldName);
971
+ break;
972
+ }
973
+ }
974
+ }
975
+ columns.push(colDef);
976
+ }
977
+ columns.push("_created_at INTEGER NOT NULL");
978
+ columns.push("_updated_at INTEGER NOT NULL");
979
+ columns.push("_version TEXT NOT NULL DEFAULT ''");
980
+ columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
981
+ columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
982
+ statements.push(
983
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
984
+ ${columns.join(",\n ")}
985
+ )`
986
+ );
987
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
988
+ const colDef = columnDefinition(fieldName, descriptor);
989
+ statements.push(
990
+ `--kora:safe-alter
991
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
992
+ );
993
+ }
994
+ statements.push(
995
+ `--kora:safe-alter
996
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
997
+ );
998
+ statements.push(
999
+ `--kora:safe-alter
1000
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1001
+ );
1002
+ for (const indexField of collection.indexes) {
1003
+ statements.push(
1004
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(collectionName, indexField))} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
1005
+ );
1006
+ }
1007
+ for (const fkField of fkFields) {
1008
+ if (!indexedFields.has(fkField)) {
1009
+ statements.push(
1010
+ `CREATE INDEX IF NOT EXISTS ${quoteIdent(collectionIndexName(collectionName, fkField))} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
1011
+ );
1012
+ }
1013
+ }
1014
+ statements.push(
1015
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
1016
+ id TEXT PRIMARY KEY NOT NULL,
1017
+ node_id TEXT NOT NULL,
1018
+ type TEXT NOT NULL,
1019
+ record_id TEXT NOT NULL,
1020
+ data TEXT,
1021
+ previous_data TEXT,
1022
+ timestamp TEXT NOT NULL,
1023
+ sequence_number INTEGER NOT NULL,
1024
+ causal_deps TEXT NOT NULL,
1025
+ schema_version INTEGER NOT NULL
1026
+ )`
1027
+ );
1028
+ statements.push(
1029
+ `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
1030
+ );
1031
+ return statements;
1032
+ }
1033
+ function collectionIndexName(collection, field) {
1034
+ return `idx_${collection.length}_${collection}_${field}`;
1035
+ }
1036
+ function legacyCollectionIndexName(collection, field) {
1037
+ return `idx_${collection}_${field}`;
1038
+ }
1039
+ var SCHEMA_CEILING_DIRECTIVE = "--kora:schema-ceiling";
1040
+ function generateFullDDL(schema) {
1041
+ const statements = [];
1042
+ statements.push(
1043
+ "CREATE TABLE IF NOT EXISTS _kora_meta (\n key TEXT PRIMARY KEY NOT NULL,\n value TEXT NOT NULL\n)"
1044
+ );
1045
+ statements.push(`${SCHEMA_CEILING_DIRECTIVE} ${schema.version}`);
1046
+ statements.push(
1047
+ "CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
1048
+ );
1049
+ statements.push(
1050
+ "CREATE TABLE IF NOT EXISTS _kora_sequences (\n name TEXT NOT NULL,\n scope TEXT NOT NULL DEFAULT '',\n node_id TEXT NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (name, scope, node_id)\n)"
1051
+ );
1052
+ statements.push(
1053
+ "CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
1054
+ );
1055
+ statements.push(
1056
+ "CREATE TABLE IF NOT EXISTS _kora_sync_rejected (\n operation_id TEXT PRIMARY KEY NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n code TEXT NOT NULL,\n message TEXT NOT NULL,\n retriable INTEGER NOT NULL,\n rejected_at INTEGER NOT NULL\n)"
1057
+ );
1058
+ statements.push(
1059
+ "CREATE TABLE IF NOT EXISTS _kora_audit_traces (\n id TEXT PRIMARY KEY NOT NULL,\n recorded_at INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n field TEXT NOT NULL,\n strategy TEXT NOT NULL,\n tier INTEGER NOT NULL,\n constraint_name TEXT,\n trace_json TEXT NOT NULL\n)"
1060
+ );
1061
+ statements.push(
1062
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
1063
+ );
1064
+ statements.push(
1065
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
1066
+ );
1067
+ for (const [name, collection] of Object.entries(schema.collections)) {
1068
+ statements.push(...generateSQL(name, collection, schema.relations));
1069
+ }
1070
+ return statements;
1071
+ }
1072
+ function columnDefinition(fieldName, descriptor) {
1073
+ const sqlType = mapFieldType(descriptor);
1074
+ const parts = [quoteIdent(fieldName), sqlType];
1075
+ if (descriptor.defaultValue !== void 0) {
1076
+ parts.push(`DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`);
1077
+ }
1078
+ return parts.join(" ");
1079
+ }
1080
+ function sqlStringLiteral(value) {
1081
+ return `'${value.replaceAll("'", "''")}'`;
1082
+ }
1083
+ function sqlDefaultLiteral(value) {
1084
+ if (value === null) return "NULL";
1085
+ if (typeof value === "string") return sqlStringLiteral(value);
1086
+ if (typeof value === "number") {
1087
+ return Number.isFinite(value) ? String(value) : "NULL";
1088
+ }
1089
+ if (typeof value === "boolean") return value ? "1" : "0";
1090
+ return sqlStringLiteral(JSON.stringify(value) ?? "null");
1091
+ }
1092
+ function enumCheckConstraint(fieldName, values) {
1093
+ return `CHECK (${quoteIdent(fieldName)} IN (${values.map(sqlStringLiteral).join(", ")}))`;
1094
+ }
1095
+ function mapFieldType(descriptor) {
1096
+ switch (descriptor.kind) {
1097
+ case "string":
1098
+ return "TEXT";
1099
+ case "number":
1100
+ return "REAL";
1101
+ case "boolean":
1102
+ return "INTEGER";
1103
+ case "enum":
1104
+ return "TEXT";
1105
+ case "timestamp":
1106
+ return "INTEGER";
1107
+ case "array":
1108
+ return "TEXT";
1109
+ // JSON-serialized
1110
+ case "object":
1111
+ return "TEXT";
1112
+ // JSON-serialized structured object
1113
+ case "json":
1114
+ return "TEXT";
1115
+ // JSON-serialized dynamic-key value
1116
+ case "blob":
1117
+ return "TEXT";
1118
+ // JSON-serialized content-addressed BlobRef (bytes stored out of band)
1119
+ case "secret":
1120
+ return "TEXT";
1121
+ case "richtext":
1122
+ return "BLOB";
1123
+ }
1124
+ }
1125
+
512
1126
  // src/version-vector/topological-sort.ts
513
1127
  function topologicalSort(operations) {
514
1128
  if (operations.length <= 1) return [...operations];
@@ -633,6 +1247,7 @@ export {
633
1247
  KoraError,
634
1248
  SchemaValidationError,
635
1249
  OperationError,
1250
+ OperationTooLargeError,
636
1251
  MergeConflictError,
637
1252
  SyncError,
638
1253
  StorageError,
@@ -642,12 +1257,41 @@ export {
642
1257
  ClockDriftError,
643
1258
  MAX_LOGICAL,
644
1259
  HybridLogicalClock,
1260
+ isKoraBytesValue,
1261
+ isLegacyNumericByteObject,
1262
+ bytesToBase64,
1263
+ base64ToBytes,
1264
+ encodeBytesForOpData,
1265
+ decodeBytesFromOpData,
1266
+ NonCanonicalValueError,
1267
+ canonicalizeOperationBody,
1268
+ canonicalValue,
1269
+ canonicalizeProvenLegacyClear,
1270
+ canonicalizeLegacyOperation,
1271
+ DEFAULT_OPERATION_HASH_VERSION,
645
1272
  computeOperationId,
646
1273
  canonicalize,
647
1274
  createOperation,
648
1275
  validateOperationParams,
1276
+ verifyOperationId,
649
1277
  verifyOperationIntegrity,
650
1278
  isValidOperation,
1279
+ planField,
1280
+ compareStamps,
1281
+ maxStamp,
1282
+ minStamp,
1283
+ isAfter,
1284
+ stampOf,
1285
+ stampTimestamp,
1286
+ quoteIdent,
1287
+ generateSQL,
1288
+ collectionIndexName,
1289
+ legacyCollectionIndexName,
1290
+ SCHEMA_CEILING_DIRECTIVE,
1291
+ generateFullDDL,
1292
+ sqlStringLiteral,
1293
+ sqlDefaultLiteral,
1294
+ enumCheckConstraint,
651
1295
  topologicalSort
652
1296
  };
653
- //# sourceMappingURL=chunk-5IICSH6H.js.map
1297
+ //# sourceMappingURL=chunk-QUD3DMSD.js.map