@korajs/core 1.0.0-beta.0 → 1.0.0-beta.10
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/dist/bindings/index.d.cts +6 -3
- package/dist/bindings/index.d.ts +6 -3
- package/dist/{build-scope-map-BIeawJzC.d.cts → build-scope-map-B8dpCt6P.d.cts} +1 -1
- package/dist/{build-scope-map-DOf4JLTh.d.ts → build-scope-map-CDpIGOsf.d.ts} +1 -1
- package/dist/{events-BynBOsO3.d.cts → events-3z0Tema3.d.cts} +169 -3
- package/dist/{events-BynBOsO3.d.ts → events-3z0Tema3.d.ts} +169 -3
- package/dist/index.cjs +648 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +222 -7
- package/dist/index.d.ts +222 -7
- package/dist/index.js +633 -29
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/{operation-BpZlYSpe.d.cts → operation-BvBgl-4c.d.cts} +1 -1
- package/dist/{operation-D5WOZYvy.d.ts → operation-ClhEbhoV.d.ts} +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
MergeConflictError: () => MergeConflictError,
|
|
39
39
|
MigrationBuilder: () => MigrationBuilder,
|
|
40
40
|
MigrationRollbackError: () => MigrationRollbackError,
|
|
41
|
+
MissingSecretKeyError: () => MissingSecretKeyError,
|
|
41
42
|
OperationError: () => OperationError,
|
|
42
43
|
RemoteClockDriftError: () => RemoteClockDriftError,
|
|
43
44
|
RollbackBuilder: () => RollbackBuilder,
|
|
@@ -45,6 +46,7 @@ __export(index_exports, {
|
|
|
45
46
|
StorageError: () => StorageError,
|
|
46
47
|
SyncError: () => SyncError,
|
|
47
48
|
advanceVector: () => advanceVector,
|
|
49
|
+
applyAtomicOp: () => applyAtomicOp,
|
|
48
50
|
applyOperationTransforms: () => applyOperationTransforms,
|
|
49
51
|
base64ToBytes: () => base64ToBytes,
|
|
50
52
|
buildScopeMap: () => buildScopeMap,
|
|
@@ -54,16 +56,19 @@ __export(index_exports, {
|
|
|
54
56
|
collectSchemaScopeFields: () => collectSchemaScopeFields,
|
|
55
57
|
collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
|
|
56
58
|
computeDelta: () => computeDelta,
|
|
59
|
+
createBlobRef: () => createBlobRef,
|
|
57
60
|
createOperation: () => createOperation,
|
|
58
61
|
createReversibleMigration: () => createReversibleMigration,
|
|
59
62
|
createVersionVector: () => createVersionVector,
|
|
60
63
|
decodeBytesFromOpData: () => decodeBytesFromOpData,
|
|
64
|
+
decryptSecret: () => decryptSecret,
|
|
61
65
|
defaultApplyFailureReason: () => defaultApplyFailureReason,
|
|
62
66
|
defaultSequenceFormat: () => defaultSequenceFormat,
|
|
63
67
|
defineSchema: () => defineSchema,
|
|
64
68
|
deserializeVector: () => deserializeVector,
|
|
65
69
|
dominates: () => dominates,
|
|
66
70
|
encodeBytesForOpData: () => encodeBytesForOpData,
|
|
71
|
+
encryptSecret: () => encryptSecret,
|
|
67
72
|
extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
|
|
68
73
|
extractTimestamp: () => extractTimestamp,
|
|
69
74
|
formatSequenceValue: () => formatSequenceValue,
|
|
@@ -76,8 +81,11 @@ __export(index_exports, {
|
|
|
76
81
|
getKoraErrorFix: () => getKoraErrorFix,
|
|
77
82
|
getTransitionMap: () => getTransitionMap,
|
|
78
83
|
hasSchemaSyncRules: () => hasSchemaSyncRules,
|
|
84
|
+
hashBlob: () => hashBlob,
|
|
85
|
+
hashSecret: () => hashSecret,
|
|
79
86
|
isApplyFailure: () => isApplyFailure,
|
|
80
87
|
isAtomicOp: () => isAtomicOp,
|
|
88
|
+
isBlobRef: () => isBlobRef,
|
|
81
89
|
isCollectionSyncScoped: () => isCollectionSyncScoped,
|
|
82
90
|
isKoraBytesValue: () => isKoraBytesValue,
|
|
83
91
|
isLegacyNumericByteObject: () => isLegacyNumericByteObject,
|
|
@@ -87,15 +95,21 @@ __export(index_exports, {
|
|
|
87
95
|
migrate: () => migrate,
|
|
88
96
|
migrationStepsToSQL: () => migrationStepsToSQL,
|
|
89
97
|
op: () => op,
|
|
98
|
+
quoteIdent: () => quoteIdent,
|
|
99
|
+
replayOperationsForRecord: () => replayOperationsForRecord,
|
|
90
100
|
resolveAtomicOp: () => resolveAtomicOp,
|
|
101
|
+
revealSecret: () => revealSecret,
|
|
91
102
|
rollbackStepsToSQL: () => rollbackStepsToSQL,
|
|
92
103
|
serializeVector: () => serializeVector,
|
|
93
104
|
t: () => t,
|
|
94
105
|
toAtomicOp: () => toAtomicOp,
|
|
106
|
+
transformSecretFieldsForWrite: () => transformSecretFieldsForWrite,
|
|
95
107
|
validateRecord: () => validateRecord,
|
|
96
108
|
validateTransition: () => validateTransition,
|
|
97
109
|
vectorsEqual: () => vectorsEqual,
|
|
98
|
-
verifyOperationIntegrity: () => verifyOperationIntegrity
|
|
110
|
+
verifyOperationIntegrity: () => verifyOperationIntegrity,
|
|
111
|
+
verifySecret: () => verifySecret,
|
|
112
|
+
verifySecretValue: () => verifySecretValue
|
|
99
113
|
});
|
|
100
114
|
module.exports = __toCommonJS(index_exports);
|
|
101
115
|
|
|
@@ -534,6 +548,226 @@ function formatUUID(bytes) {
|
|
|
534
548
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
535
549
|
}
|
|
536
550
|
|
|
551
|
+
// src/blob/blob-ref.ts
|
|
552
|
+
async function hashBlob(bytes) {
|
|
553
|
+
const view = new Uint8Array(bytes.byteLength);
|
|
554
|
+
view.set(bytes);
|
|
555
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", view);
|
|
556
|
+
const out = new Uint8Array(digest);
|
|
557
|
+
return Array.from(out, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
558
|
+
}
|
|
559
|
+
async function createBlobRef(bytes, metadata = {}) {
|
|
560
|
+
const hash = await hashBlob(bytes);
|
|
561
|
+
const ref = { hash, size: bytes.byteLength };
|
|
562
|
+
if (metadata.mimeType !== void 0) {
|
|
563
|
+
ref.mimeType = metadata.mimeType;
|
|
564
|
+
}
|
|
565
|
+
if (metadata.filename !== void 0) {
|
|
566
|
+
ref.filename = metadata.filename;
|
|
567
|
+
}
|
|
568
|
+
return ref;
|
|
569
|
+
}
|
|
570
|
+
function isBlobRef(value) {
|
|
571
|
+
if (typeof value !== "object" || value === null) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
const ref = value;
|
|
575
|
+
if (typeof ref.hash !== "string" || !/^[0-9a-f]{64}$/.test(ref.hash)) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
if (typeof ref.size !== "number" || !Number.isInteger(ref.size) || ref.size < 0) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (ref.mimeType !== void 0 && typeof ref.mimeType !== "string") {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
if (ref.filename !== void 0 && typeof ref.filename !== "string") {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
if (ref.manifestHash !== void 0 && (typeof ref.manifestHash !== "string" || !/^[0-9a-f]{64}$/.test(ref.manifestHash))) {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/secret/secret-crypto.ts
|
|
594
|
+
var PBKDF2_ITERATIONS = 21e4;
|
|
595
|
+
var SALT_BYTES = 16;
|
|
596
|
+
var IV_BYTES = 12;
|
|
597
|
+
var HASH_BYTES = 32;
|
|
598
|
+
function toHex(bytes) {
|
|
599
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
600
|
+
}
|
|
601
|
+
function fromHex(hex) {
|
|
602
|
+
if (hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) {
|
|
603
|
+
throw new Error("Invalid hex string");
|
|
604
|
+
}
|
|
605
|
+
const out = new Uint8Array(hex.length / 2);
|
|
606
|
+
for (let i = 0; i < out.length; i++) {
|
|
607
|
+
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
function randomBytes(n) {
|
|
612
|
+
const out = new Uint8Array(n);
|
|
613
|
+
globalThis.crypto.getRandomValues(out);
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
function timingSafeEqual(a, b) {
|
|
617
|
+
if (a.length !== b.length) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
let diff = 0;
|
|
621
|
+
for (let i = 0; i < a.length; i++) {
|
|
622
|
+
diff |= a[i] ^ b[i];
|
|
623
|
+
}
|
|
624
|
+
return diff === 0;
|
|
625
|
+
}
|
|
626
|
+
async function deriveAesKey(secretKey, salt) {
|
|
627
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
628
|
+
"raw",
|
|
629
|
+
new TextEncoder().encode(secretKey),
|
|
630
|
+
"PBKDF2",
|
|
631
|
+
false,
|
|
632
|
+
["deriveKey"]
|
|
633
|
+
);
|
|
634
|
+
return globalThis.crypto.subtle.deriveKey(
|
|
635
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
636
|
+
baseKey,
|
|
637
|
+
{ name: "AES-GCM", length: 256 },
|
|
638
|
+
false,
|
|
639
|
+
["encrypt", "decrypt"]
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
async function encryptSecret(plaintext, secretKey) {
|
|
643
|
+
const salt = randomBytes(SALT_BYTES);
|
|
644
|
+
const iv = randomBytes(IV_BYTES);
|
|
645
|
+
const key = await deriveAesKey(secretKey, salt);
|
|
646
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
647
|
+
{ name: "AES-GCM", iv },
|
|
648
|
+
key,
|
|
649
|
+
new TextEncoder().encode(plaintext)
|
|
650
|
+
);
|
|
651
|
+
return `v1.${toHex(salt)}.${toHex(iv)}.${toHex(new Uint8Array(ciphertext))}`;
|
|
652
|
+
}
|
|
653
|
+
async function decryptSecret(encrypted, secretKey) {
|
|
654
|
+
const parts = encrypted.split(".");
|
|
655
|
+
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
656
|
+
throw new Error("Invalid encrypted secret format");
|
|
657
|
+
}
|
|
658
|
+
const salt = fromHex(parts[1]);
|
|
659
|
+
const iv = fromHex(parts[2]);
|
|
660
|
+
const ciphertext = fromHex(parts[3]);
|
|
661
|
+
const key = await deriveAesKey(secretKey, salt);
|
|
662
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
663
|
+
{ name: "AES-GCM", iv },
|
|
664
|
+
key,
|
|
665
|
+
ciphertext
|
|
666
|
+
);
|
|
667
|
+
return new TextDecoder().decode(plaintext);
|
|
668
|
+
}
|
|
669
|
+
async function hashSecret(plaintext) {
|
|
670
|
+
const salt = randomBytes(SALT_BYTES);
|
|
671
|
+
const hash = await pbkdf2(plaintext, salt);
|
|
672
|
+
return `v1.${toHex(salt)}.${toHex(hash)}`;
|
|
673
|
+
}
|
|
674
|
+
async function verifySecret(plaintext, stored) {
|
|
675
|
+
const parts = stored.split(".");
|
|
676
|
+
if (parts.length !== 3 || parts[0] !== "v1") {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
let salt;
|
|
680
|
+
let expected;
|
|
681
|
+
try {
|
|
682
|
+
salt = fromHex(parts[1]);
|
|
683
|
+
expected = fromHex(parts[2]);
|
|
684
|
+
} catch {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
const actual = await pbkdf2(plaintext, salt);
|
|
688
|
+
return timingSafeEqual(actual, expected);
|
|
689
|
+
}
|
|
690
|
+
async function pbkdf2(plaintext, salt) {
|
|
691
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
692
|
+
"raw",
|
|
693
|
+
new TextEncoder().encode(plaintext),
|
|
694
|
+
"PBKDF2",
|
|
695
|
+
false,
|
|
696
|
+
["deriveBits"]
|
|
697
|
+
);
|
|
698
|
+
const bits = await globalThis.crypto.subtle.deriveBits(
|
|
699
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
700
|
+
baseKey,
|
|
701
|
+
HASH_BYTES * 8
|
|
702
|
+
);
|
|
703
|
+
return new Uint8Array(bits);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/secret/secret-transform.ts
|
|
707
|
+
var MissingSecretKeyError = class extends Error {
|
|
708
|
+
constructor(field) {
|
|
709
|
+
super(
|
|
710
|
+
`Secret field "${field}" is encrypted but no encryption key is configured. Provide sync.encryption.key (or a key provider) to createApp.`
|
|
711
|
+
);
|
|
712
|
+
this.field = field;
|
|
713
|
+
this.name = "MissingSecretKeyError";
|
|
714
|
+
}
|
|
715
|
+
field;
|
|
716
|
+
};
|
|
717
|
+
async function resolveKey(provider) {
|
|
718
|
+
if (!provider) {
|
|
719
|
+
return void 0;
|
|
720
|
+
}
|
|
721
|
+
return provider();
|
|
722
|
+
}
|
|
723
|
+
async function transformSecretFieldsForWrite(data, collectionDef, keyProvider) {
|
|
724
|
+
const hasSecret = Object.values(collectionDef.fields).some((f) => f.kind === "secret");
|
|
725
|
+
if (!hasSecret) {
|
|
726
|
+
return data;
|
|
727
|
+
}
|
|
728
|
+
const result = { ...data };
|
|
729
|
+
let key;
|
|
730
|
+
let keyResolved = false;
|
|
731
|
+
for (const [field, value] of Object.entries(data)) {
|
|
732
|
+
const descriptor = collectionDef.fields[field];
|
|
733
|
+
if (descriptor?.kind !== "secret") {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (typeof value !== "string") {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const mode = descriptor.secretMode ?? "encrypted";
|
|
740
|
+
if (mode === "hashed") {
|
|
741
|
+
result[field] = await hashSecret(value);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (!keyResolved) {
|
|
745
|
+
key = await resolveKey(keyProvider);
|
|
746
|
+
keyResolved = true;
|
|
747
|
+
}
|
|
748
|
+
if (key === void 0) {
|
|
749
|
+
throw new MissingSecretKeyError(field);
|
|
750
|
+
}
|
|
751
|
+
result[field] = await encryptSecret(value, key);
|
|
752
|
+
}
|
|
753
|
+
return result;
|
|
754
|
+
}
|
|
755
|
+
async function revealSecret(storedValue, mode, keyProvider, field = "secret") {
|
|
756
|
+
if (mode === "hashed") {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`Secret field "${field}" is hashed (one-way) and cannot be revealed. Use verifySecretValue to check a candidate.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const key = await resolveKey(keyProvider);
|
|
762
|
+
if (key === void 0) {
|
|
763
|
+
throw new MissingSecretKeyError(field);
|
|
764
|
+
}
|
|
765
|
+
return decryptSecret(storedValue, key);
|
|
766
|
+
}
|
|
767
|
+
async function verifySecretValue(candidate, storedValue) {
|
|
768
|
+
return verifySecret(candidate, storedValue);
|
|
769
|
+
}
|
|
770
|
+
|
|
537
771
|
// src/operations/apply-result.ts
|
|
538
772
|
var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
|
|
539
773
|
var APPLY_FAILURE_CODES = {
|
|
@@ -736,32 +970,36 @@ var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
|
|
|
736
970
|
function isAtomicOp(value) {
|
|
737
971
|
return typeof value === "object" && value !== null && KORA_ATOMIC_OP in value && value[KORA_ATOMIC_OP] === true;
|
|
738
972
|
}
|
|
739
|
-
function
|
|
740
|
-
switch (
|
|
973
|
+
function applyAtomicOp(currentValue, atomicOp) {
|
|
974
|
+
switch (atomicOp.type) {
|
|
741
975
|
case "increment": {
|
|
742
976
|
const current = typeof currentValue === "number" ? currentValue : 0;
|
|
743
|
-
const operand =
|
|
977
|
+
const operand = atomicOp.value;
|
|
744
978
|
return current + operand;
|
|
745
979
|
}
|
|
746
980
|
case "max": {
|
|
747
981
|
const current = typeof currentValue === "number" ? currentValue : Number.NEGATIVE_INFINITY;
|
|
748
|
-
return Math.max(current,
|
|
982
|
+
return Math.max(current, atomicOp.value);
|
|
749
983
|
}
|
|
750
984
|
case "min": {
|
|
751
985
|
const current = typeof currentValue === "number" ? currentValue : Number.POSITIVE_INFINITY;
|
|
752
|
-
return Math.min(current,
|
|
986
|
+
return Math.min(current, atomicOp.value);
|
|
753
987
|
}
|
|
754
988
|
case "append": {
|
|
755
989
|
const current = Array.isArray(currentValue) ? [...currentValue] : [];
|
|
756
|
-
current.push(
|
|
990
|
+
current.push(atomicOp.value);
|
|
757
991
|
return current;
|
|
758
992
|
}
|
|
759
993
|
case "remove": {
|
|
760
994
|
if (!Array.isArray(currentValue)) return [];
|
|
761
|
-
|
|
995
|
+
const target = JSON.stringify(atomicOp.value);
|
|
996
|
+
return currentValue.filter((item) => JSON.stringify(item) !== target);
|
|
762
997
|
}
|
|
763
998
|
}
|
|
764
999
|
}
|
|
1000
|
+
function resolveAtomicOp(currentValue, sentinel) {
|
|
1001
|
+
return applyAtomicOp(currentValue, { type: sentinel.type, value: sentinel.value });
|
|
1002
|
+
}
|
|
765
1003
|
function createSentinel(type, value) {
|
|
766
1004
|
return Object.freeze({
|
|
767
1005
|
[KORA_ATOMIC_OP]: true,
|
|
@@ -829,6 +1067,46 @@ function toAtomicOp(sentinel) {
|
|
|
829
1067
|
return { type: sentinel.type, value: sentinel.value };
|
|
830
1068
|
}
|
|
831
1069
|
|
|
1070
|
+
// src/operations/replay-record.ts
|
|
1071
|
+
function replayOperationsForRecord(ops) {
|
|
1072
|
+
let record = null;
|
|
1073
|
+
let deleted = false;
|
|
1074
|
+
const lastWriterKind = {};
|
|
1075
|
+
for (const op2 of ops) {
|
|
1076
|
+
switch (op2.type) {
|
|
1077
|
+
case "insert":
|
|
1078
|
+
if (op2.data) {
|
|
1079
|
+
record = { ...op2.data };
|
|
1080
|
+
deleted = false;
|
|
1081
|
+
for (const field of Object.keys(op2.data)) {
|
|
1082
|
+
lastWriterKind[field] = "lww";
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
break;
|
|
1086
|
+
case "update":
|
|
1087
|
+
if (op2.data) {
|
|
1088
|
+
const next = { ...record ?? {} };
|
|
1089
|
+
for (const [field, value] of Object.entries(op2.data)) {
|
|
1090
|
+
const atomicOp = op2.atomicOps?.[field];
|
|
1091
|
+
if (atomicOp && lastWriterKind[field] === `atomic:${atomicOp.type}`) {
|
|
1092
|
+
next[field] = applyAtomicOp(next[field], atomicOp);
|
|
1093
|
+
} else {
|
|
1094
|
+
next[field] = value;
|
|
1095
|
+
}
|
|
1096
|
+
lastWriterKind[field] = atomicOp ? `atomic:${atomicOp.type}` : "lww";
|
|
1097
|
+
}
|
|
1098
|
+
record = next;
|
|
1099
|
+
deleted = false;
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
case "delete":
|
|
1103
|
+
deleted = true;
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return deleted ? null : record;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
832
1110
|
// src/operations/op-data-binary.ts
|
|
833
1111
|
function isKoraBytesValue(value) {
|
|
834
1112
|
if (typeof value !== "object" || value === null) {
|
|
@@ -1079,7 +1357,7 @@ function validateStateMachineDefinition(collectionName, sm, fields) {
|
|
|
1079
1357
|
}
|
|
1080
1358
|
|
|
1081
1359
|
// src/schema/define.ts
|
|
1082
|
-
var COLLECTION_NAME_RE = /^[
|
|
1360
|
+
var COLLECTION_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
1083
1361
|
var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
1084
1362
|
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
|
|
1085
1363
|
function defineSchema(input) {
|
|
@@ -1144,7 +1422,7 @@ function validateVersion(version) {
|
|
|
1144
1422
|
function validateCollectionName(name) {
|
|
1145
1423
|
if (!COLLECTION_NAME_RE.test(name)) {
|
|
1146
1424
|
throw new SchemaValidationError(
|
|
1147
|
-
`Collection name "${name}" is invalid. Must
|
|
1425
|
+
`Collection name "${name}" is invalid. Must start with a letter and contain only letters, numbers, and underscores.`,
|
|
1148
1426
|
{ collection: name }
|
|
1149
1427
|
);
|
|
1150
1428
|
}
|
|
@@ -1336,6 +1614,11 @@ function validateRelation(name, relation, collections) {
|
|
|
1336
1614
|
}
|
|
1337
1615
|
}
|
|
1338
1616
|
|
|
1617
|
+
// src/schema/quote-ident.ts
|
|
1618
|
+
function quoteIdent(name) {
|
|
1619
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1339
1622
|
// src/schema/sql-gen.ts
|
|
1340
1623
|
function generateSQL(collectionName, collection, relations) {
|
|
1341
1624
|
const statements = [];
|
|
@@ -1347,7 +1630,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
1347
1630
|
if (relations) {
|
|
1348
1631
|
for (const rel of Object.values(relations)) {
|
|
1349
1632
|
if (rel.from === collectionName && rel.field === fieldName) {
|
|
1350
|
-
colDef += ` REFERENCES ${rel.to}(id)`;
|
|
1633
|
+
colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
|
|
1351
1634
|
fkFields.push(fieldName);
|
|
1352
1635
|
break;
|
|
1353
1636
|
}
|
|
@@ -1360,36 +1643,40 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
1360
1643
|
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
1361
1644
|
columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
|
|
1362
1645
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
1363
|
-
statements.push(
|
|
1646
|
+
statements.push(
|
|
1647
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
|
|
1364
1648
|
${columns.join(",\n ")}
|
|
1365
|
-
)`
|
|
1649
|
+
)`
|
|
1650
|
+
);
|
|
1366
1651
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
1367
1652
|
const colDef = columnDefinition(fieldName, descriptor);
|
|
1368
|
-
statements.push(
|
|
1369
|
-
|
|
1653
|
+
statements.push(
|
|
1654
|
+
`--kora:safe-alter
|
|
1655
|
+
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
|
|
1656
|
+
);
|
|
1370
1657
|
}
|
|
1371
1658
|
statements.push(
|
|
1372
1659
|
`--kora:safe-alter
|
|
1373
|
-
ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1660
|
+
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1374
1661
|
);
|
|
1375
1662
|
statements.push(
|
|
1376
1663
|
`--kora:safe-alter
|
|
1377
|
-
ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
|
|
1664
|
+
ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
|
|
1378
1665
|
);
|
|
1379
1666
|
for (const indexField of collection.indexes) {
|
|
1380
1667
|
statements.push(
|
|
1381
|
-
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
|
|
1668
|
+
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
|
|
1382
1669
|
);
|
|
1383
1670
|
}
|
|
1384
1671
|
for (const fkField of fkFields) {
|
|
1385
1672
|
if (!indexedFields.has(fkField)) {
|
|
1386
1673
|
statements.push(
|
|
1387
|
-
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${collectionName} (${fkField})`
|
|
1674
|
+
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
|
|
1388
1675
|
);
|
|
1389
1676
|
}
|
|
1390
1677
|
}
|
|
1391
1678
|
statements.push(
|
|
1392
|
-
`CREATE TABLE IF NOT EXISTS _kora_ops_${collectionName} (
|
|
1679
|
+
`CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
|
|
1393
1680
|
id TEXT PRIMARY KEY NOT NULL,
|
|
1394
1681
|
node_id TEXT NOT NULL,
|
|
1395
1682
|
type TEXT NOT NULL,
|
|
@@ -1403,7 +1690,7 @@ ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '
|
|
|
1403
1690
|
)`
|
|
1404
1691
|
);
|
|
1405
1692
|
statements.push(
|
|
1406
|
-
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
|
|
1693
|
+
`CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
|
|
1407
1694
|
);
|
|
1408
1695
|
return statements;
|
|
1409
1696
|
}
|
|
@@ -1421,6 +1708,9 @@ function generateFullDDL(schema) {
|
|
|
1421
1708
|
statements.push(
|
|
1422
1709
|
"CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
|
|
1423
1710
|
);
|
|
1711
|
+
statements.push(
|
|
1712
|
+
"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)"
|
|
1713
|
+
);
|
|
1424
1714
|
statements.push(
|
|
1425
1715
|
"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)"
|
|
1426
1716
|
);
|
|
@@ -1437,7 +1727,7 @@ function generateFullDDL(schema) {
|
|
|
1437
1727
|
}
|
|
1438
1728
|
function columnDefinition(fieldName, descriptor) {
|
|
1439
1729
|
const sqlType = mapFieldType(descriptor);
|
|
1440
|
-
const parts = [fieldName, sqlType];
|
|
1730
|
+
const parts = [quoteIdent(fieldName), sqlType];
|
|
1441
1731
|
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
1442
1732
|
parts.push("NOT NULL");
|
|
1443
1733
|
}
|
|
@@ -1446,7 +1736,7 @@ function columnDefinition(fieldName, descriptor) {
|
|
|
1446
1736
|
}
|
|
1447
1737
|
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1448
1738
|
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
1449
|
-
parts.push(`CHECK (${fieldName} IN (${values}))`);
|
|
1739
|
+
parts.push(`CHECK (${quoteIdent(fieldName)} IN (${values}))`);
|
|
1450
1740
|
}
|
|
1451
1741
|
return parts.join(" ");
|
|
1452
1742
|
}
|
|
@@ -1465,6 +1755,17 @@ function mapFieldType(descriptor) {
|
|
|
1465
1755
|
case "array":
|
|
1466
1756
|
return "TEXT";
|
|
1467
1757
|
// JSON-serialized
|
|
1758
|
+
case "object":
|
|
1759
|
+
return "TEXT";
|
|
1760
|
+
// JSON-serialized structured object
|
|
1761
|
+
case "json":
|
|
1762
|
+
return "TEXT";
|
|
1763
|
+
// JSON-serialized dynamic-key value
|
|
1764
|
+
case "blob":
|
|
1765
|
+
return "TEXT";
|
|
1766
|
+
// JSON-serialized content-addressed BlobRef (bytes stored out of band)
|
|
1767
|
+
case "secret":
|
|
1768
|
+
return "TEXT";
|
|
1468
1769
|
case "richtext":
|
|
1469
1770
|
return "BLOB";
|
|
1470
1771
|
}
|
|
@@ -1698,6 +1999,160 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
1698
1999
|
};
|
|
1699
2000
|
}
|
|
1700
2001
|
};
|
|
2002
|
+
var ObjectFieldBuilder = class _ObjectFieldBuilder extends FieldBuilder {
|
|
2003
|
+
_fields;
|
|
2004
|
+
constructor(fields, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
2005
|
+
super("object", required, defaultValue, auto, mergeStrategy);
|
|
2006
|
+
this._fields = fields;
|
|
2007
|
+
}
|
|
2008
|
+
optional() {
|
|
2009
|
+
return new _ObjectFieldBuilder(
|
|
2010
|
+
this._fields,
|
|
2011
|
+
false,
|
|
2012
|
+
this._defaultValue,
|
|
2013
|
+
this._auto,
|
|
2014
|
+
this._mergeStrategy
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
default(value) {
|
|
2018
|
+
return new _ObjectFieldBuilder(this._fields, false, value, this._auto, this._mergeStrategy);
|
|
2019
|
+
}
|
|
2020
|
+
auto() {
|
|
2021
|
+
return new _ObjectFieldBuilder(this._fields, false, void 0, true, this._mergeStrategy);
|
|
2022
|
+
}
|
|
2023
|
+
merge(strategy) {
|
|
2024
|
+
return new _ObjectFieldBuilder(
|
|
2025
|
+
this._fields,
|
|
2026
|
+
this._required,
|
|
2027
|
+
this._defaultValue,
|
|
2028
|
+
this._auto,
|
|
2029
|
+
strategy
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
_build() {
|
|
2033
|
+
const nestedFields = {};
|
|
2034
|
+
for (const [key, builder] of Object.entries(this._fields)) {
|
|
2035
|
+
nestedFields[key] = builder._build();
|
|
2036
|
+
}
|
|
2037
|
+
return {
|
|
2038
|
+
kind: "object",
|
|
2039
|
+
required: this._required,
|
|
2040
|
+
defaultValue: this._defaultValue,
|
|
2041
|
+
auto: this._auto,
|
|
2042
|
+
enumValues: null,
|
|
2043
|
+
itemKind: null,
|
|
2044
|
+
mergeStrategy: this._mergeStrategy,
|
|
2045
|
+
transitions: null,
|
|
2046
|
+
nestedFields
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
var JsonFieldBuilder = class _JsonFieldBuilder extends FieldBuilder {
|
|
2051
|
+
optional() {
|
|
2052
|
+
return new _JsonFieldBuilder(
|
|
2053
|
+
"json",
|
|
2054
|
+
false,
|
|
2055
|
+
this._defaultValue,
|
|
2056
|
+
this._auto,
|
|
2057
|
+
this._mergeStrategy
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
default(value) {
|
|
2061
|
+
return new _JsonFieldBuilder(
|
|
2062
|
+
"json",
|
|
2063
|
+
false,
|
|
2064
|
+
value,
|
|
2065
|
+
this._auto,
|
|
2066
|
+
this._mergeStrategy
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
auto() {
|
|
2070
|
+
return new _JsonFieldBuilder("json", false, void 0, true, this._mergeStrategy);
|
|
2071
|
+
}
|
|
2072
|
+
merge(strategy) {
|
|
2073
|
+
return new _JsonFieldBuilder(
|
|
2074
|
+
"json",
|
|
2075
|
+
this._required,
|
|
2076
|
+
this._defaultValue,
|
|
2077
|
+
this._auto,
|
|
2078
|
+
strategy
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
_build() {
|
|
2082
|
+
return {
|
|
2083
|
+
kind: "json",
|
|
2084
|
+
required: this._required,
|
|
2085
|
+
defaultValue: this._defaultValue,
|
|
2086
|
+
auto: this._auto,
|
|
2087
|
+
enumValues: null,
|
|
2088
|
+
itemKind: null,
|
|
2089
|
+
mergeStrategy: this._mergeStrategy,
|
|
2090
|
+
transitions: null,
|
|
2091
|
+
nestedFields: null
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
var SecretFieldBuilder = class _SecretFieldBuilder extends FieldBuilder {
|
|
2096
|
+
_secretMode;
|
|
2097
|
+
constructor(secretMode = "encrypted", required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
2098
|
+
super("secret", required, defaultValue, auto, mergeStrategy);
|
|
2099
|
+
this._secretMode = secretMode;
|
|
2100
|
+
}
|
|
2101
|
+
/** Store this secret as a one-way salted hash (passwords: verify, never read back). */
|
|
2102
|
+
hashed() {
|
|
2103
|
+
return new _SecretFieldBuilder(
|
|
2104
|
+
"hashed",
|
|
2105
|
+
this._required,
|
|
2106
|
+
this._defaultValue,
|
|
2107
|
+
this._auto,
|
|
2108
|
+
this._mergeStrategy
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
/** Store this secret as reversible ciphertext (tokens/keys: decrypt to use). */
|
|
2112
|
+
encrypted() {
|
|
2113
|
+
return new _SecretFieldBuilder(
|
|
2114
|
+
"encrypted",
|
|
2115
|
+
this._required,
|
|
2116
|
+
this._defaultValue,
|
|
2117
|
+
this._auto,
|
|
2118
|
+
this._mergeStrategy
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
optional() {
|
|
2122
|
+
return new _SecretFieldBuilder(
|
|
2123
|
+
this._secretMode,
|
|
2124
|
+
false,
|
|
2125
|
+
this._defaultValue,
|
|
2126
|
+
this._auto,
|
|
2127
|
+
this._mergeStrategy
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
auto() {
|
|
2131
|
+
return new _SecretFieldBuilder(this._secretMode, false, void 0, true, this._mergeStrategy);
|
|
2132
|
+
}
|
|
2133
|
+
merge(strategy) {
|
|
2134
|
+
return new _SecretFieldBuilder(
|
|
2135
|
+
this._secretMode,
|
|
2136
|
+
this._required,
|
|
2137
|
+
this._defaultValue,
|
|
2138
|
+
this._auto,
|
|
2139
|
+
strategy
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
_build() {
|
|
2143
|
+
return {
|
|
2144
|
+
kind: "secret",
|
|
2145
|
+
required: this._required,
|
|
2146
|
+
defaultValue: this._defaultValue,
|
|
2147
|
+
auto: this._auto,
|
|
2148
|
+
enumValues: null,
|
|
2149
|
+
itemKind: null,
|
|
2150
|
+
mergeStrategy: this._mergeStrategy,
|
|
2151
|
+
transitions: null,
|
|
2152
|
+
secretMode: this._secretMode
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
1701
2156
|
var t = {
|
|
1702
2157
|
string() {
|
|
1703
2158
|
return new FieldBuilder("string", true, void 0, false);
|
|
@@ -1719,6 +2174,18 @@ var t = {
|
|
|
1719
2174
|
},
|
|
1720
2175
|
array(itemBuilder) {
|
|
1721
2176
|
return new ArrayFieldBuilder(itemBuilder, true, void 0, false);
|
|
2177
|
+
},
|
|
2178
|
+
object(fields) {
|
|
2179
|
+
return new ObjectFieldBuilder(fields, true, void 0, false);
|
|
2180
|
+
},
|
|
2181
|
+
json() {
|
|
2182
|
+
return new JsonFieldBuilder("json", true, void 0, false);
|
|
2183
|
+
},
|
|
2184
|
+
blob() {
|
|
2185
|
+
return new FieldBuilder("blob", true, void 0, false);
|
|
2186
|
+
},
|
|
2187
|
+
secret() {
|
|
2188
|
+
return new SecretFieldBuilder("encrypted", true, void 0, false);
|
|
1722
2189
|
}
|
|
1723
2190
|
};
|
|
1724
2191
|
|
|
@@ -1883,7 +2350,125 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
|
|
|
1883
2350
|
}
|
|
1884
2351
|
break;
|
|
1885
2352
|
}
|
|
2353
|
+
case "object": {
|
|
2354
|
+
if (!isPlainObject(value)) {
|
|
2355
|
+
throw new SchemaValidationError(
|
|
2356
|
+
`Field "${fieldName}" in collection "${collection}" must be a plain object, got ${describeType(value)}`,
|
|
2357
|
+
{
|
|
2358
|
+
collection,
|
|
2359
|
+
field: fieldName,
|
|
2360
|
+
expectedType: "object",
|
|
2361
|
+
receivedType: describeType(value)
|
|
2362
|
+
}
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
if (descriptor.nestedFields) {
|
|
2366
|
+
for (const [nestedName, nestedDescriptor] of Object.entries(descriptor.nestedFields)) {
|
|
2367
|
+
const nestedValue = value[nestedName];
|
|
2368
|
+
if (nestedValue !== void 0 && nestedValue !== null) {
|
|
2369
|
+
validateFieldValue(
|
|
2370
|
+
collection,
|
|
2371
|
+
`${fieldName}.${nestedName}`,
|
|
2372
|
+
nestedDescriptor,
|
|
2373
|
+
nestedValue
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
case "json": {
|
|
2381
|
+
const nonJson = findNonJsonSerializable(value, fieldName);
|
|
2382
|
+
if (nonJson) {
|
|
2383
|
+
throw new SchemaValidationError(
|
|
2384
|
+
`Field "${fieldName}" in collection "${collection}" must be JSON-serializable; found ${nonJson.receivedType} at "${nonJson.path}"`,
|
|
2385
|
+
{
|
|
2386
|
+
collection,
|
|
2387
|
+
field: fieldName,
|
|
2388
|
+
expectedType: "json",
|
|
2389
|
+
receivedType: nonJson.receivedType,
|
|
2390
|
+
path: nonJson.path
|
|
2391
|
+
}
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
case "blob": {
|
|
2397
|
+
if (!isBlobRef(value)) {
|
|
2398
|
+
throw new SchemaValidationError(
|
|
2399
|
+
`Field "${fieldName}" in collection "${collection}" must be a BlobRef (from the blob store), got ${describeType(value)}`,
|
|
2400
|
+
{ collection, field: fieldName, expectedType: "blob", receivedType: describeType(value) }
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
break;
|
|
2404
|
+
}
|
|
2405
|
+
case "secret": {
|
|
2406
|
+
if (typeof value !== "string") {
|
|
2407
|
+
throw new SchemaValidationError(
|
|
2408
|
+
`Field "${fieldName}" in collection "${collection}" must be a string, got ${describeType(value)}`,
|
|
2409
|
+
{
|
|
2410
|
+
collection,
|
|
2411
|
+
field: fieldName,
|
|
2412
|
+
expectedType: "secret",
|
|
2413
|
+
receivedType: describeType(value)
|
|
2414
|
+
}
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
break;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
function isPlainObject(value) {
|
|
2422
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2423
|
+
return false;
|
|
2424
|
+
}
|
|
2425
|
+
const proto = Object.getPrototypeOf(value);
|
|
2426
|
+
return proto === null || proto === Object.prototype;
|
|
2427
|
+
}
|
|
2428
|
+
function findNonJsonSerializable(value, path, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2429
|
+
if (value === null) {
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
const type = typeof value;
|
|
2433
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
2434
|
+
if (type === "number" && !Number.isFinite(value)) {
|
|
2435
|
+
return { path, receivedType: describeType(value) };
|
|
2436
|
+
}
|
|
2437
|
+
return null;
|
|
2438
|
+
}
|
|
2439
|
+
if (type === "undefined" || type === "function" || type === "symbol" || type === "bigint") {
|
|
2440
|
+
return { path, receivedType: describeType(value) };
|
|
2441
|
+
}
|
|
2442
|
+
if (Array.isArray(value)) {
|
|
2443
|
+
for (let index = 0; index < value.length; index++) {
|
|
2444
|
+
const invalid = findNonJsonSerializable(value[index], `${path}[${index}]`, seen);
|
|
2445
|
+
if (invalid) return invalid;
|
|
2446
|
+
}
|
|
2447
|
+
return null;
|
|
2448
|
+
}
|
|
2449
|
+
if (type === "object") {
|
|
2450
|
+
const objectValue = value;
|
|
2451
|
+
if (seen.has(objectValue)) {
|
|
2452
|
+
return { path, receivedType: "circular object" };
|
|
2453
|
+
}
|
|
2454
|
+
seen.add(objectValue);
|
|
2455
|
+
for (const [key, nestedValue] of Object.entries(objectValue)) {
|
|
2456
|
+
const invalid = findNonJsonSerializable(nestedValue, `${path}.${key}`, seen);
|
|
2457
|
+
if (invalid) return invalid;
|
|
2458
|
+
}
|
|
2459
|
+
seen.delete(objectValue);
|
|
2460
|
+
return null;
|
|
1886
2461
|
}
|
|
2462
|
+
return { path, receivedType: describeType(value) };
|
|
2463
|
+
}
|
|
2464
|
+
function describeType(value) {
|
|
2465
|
+
if (value === null) {
|
|
2466
|
+
return "null";
|
|
2467
|
+
}
|
|
2468
|
+
if (Array.isArray(value)) {
|
|
2469
|
+
return "array";
|
|
2470
|
+
}
|
|
2471
|
+
return typeof value;
|
|
1887
2472
|
}
|
|
1888
2473
|
function jsTypeForKind(kind) {
|
|
1889
2474
|
switch (kind) {
|
|
@@ -2433,14 +3018,18 @@ function migrationStepsToSQL(steps) {
|
|
|
2433
3018
|
statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
|
|
2434
3019
|
break;
|
|
2435
3020
|
case "removeField":
|
|
2436
|
-
statements.push(
|
|
3021
|
+
statements.push(
|
|
3022
|
+
`ALTER TABLE ${quoteIdent(step.collection)} DROP COLUMN ${quoteIdent(step.field)}`
|
|
3023
|
+
);
|
|
2437
3024
|
break;
|
|
2438
3025
|
case "renameField":
|
|
2439
|
-
statements.push(
|
|
3026
|
+
statements.push(
|
|
3027
|
+
`ALTER TABLE ${quoteIdent(step.collection)} RENAME COLUMN ${quoteIdent(step.from)} TO ${quoteIdent(step.to)}`
|
|
3028
|
+
);
|
|
2440
3029
|
break;
|
|
2441
3030
|
case "addIndex":
|
|
2442
3031
|
statements.push(
|
|
2443
|
-
`CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
|
|
3032
|
+
`CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${quoteIdent(step.collection)} (${quoteIdent(step.field)})`
|
|
2444
3033
|
);
|
|
2445
3034
|
break;
|
|
2446
3035
|
case "removeIndex":
|
|
@@ -2458,13 +3047,13 @@ function rollbackStepsToSQL(migration) {
|
|
|
2458
3047
|
}
|
|
2459
3048
|
function addFieldSQL(collection, field, descriptor) {
|
|
2460
3049
|
const sqlType = mapFieldType2(descriptor);
|
|
2461
|
-
const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
|
|
3050
|
+
const parts = [`ALTER TABLE ${quoteIdent(collection)} ADD COLUMN ${quoteIdent(field)}`, sqlType];
|
|
2462
3051
|
if (descriptor.defaultValue !== void 0) {
|
|
2463
3052
|
parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
|
|
2464
3053
|
}
|
|
2465
3054
|
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
2466
3055
|
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
2467
|
-
parts.push(`CHECK (${field} IN (${values}))`);
|
|
3056
|
+
parts.push(`CHECK (${quoteIdent(field)} IN (${values}))`);
|
|
2468
3057
|
}
|
|
2469
3058
|
return parts.join(" ");
|
|
2470
3059
|
}
|
|
@@ -2482,6 +3071,14 @@ function mapFieldType2(descriptor) {
|
|
|
2482
3071
|
return "INTEGER";
|
|
2483
3072
|
case "array":
|
|
2484
3073
|
return "TEXT";
|
|
3074
|
+
case "object":
|
|
3075
|
+
return "TEXT";
|
|
3076
|
+
case "json":
|
|
3077
|
+
return "TEXT";
|
|
3078
|
+
case "blob":
|
|
3079
|
+
return "TEXT";
|
|
3080
|
+
case "secret":
|
|
3081
|
+
return "TEXT";
|
|
2485
3082
|
case "richtext":
|
|
2486
3083
|
return "BLOB";
|
|
2487
3084
|
}
|
|
@@ -2500,7 +3097,14 @@ var SCALAR_TYPE_MAP = {
|
|
|
2500
3097
|
number: "double",
|
|
2501
3098
|
boolean: "bool",
|
|
2502
3099
|
timestamp: "int64",
|
|
2503
|
-
richtext: "bytes"
|
|
3100
|
+
richtext: "bytes",
|
|
3101
|
+
// object/json values travel as JSON-serialized strings on the proto wire.
|
|
3102
|
+
object: "string",
|
|
3103
|
+
json: "string",
|
|
3104
|
+
// A blob field carries a JSON-serialized content-addressed reference, not bytes.
|
|
3105
|
+
blob: "string",
|
|
3106
|
+
// A secret field's stored value is a JSON string (ciphertext or hash).
|
|
3107
|
+
secret: "string"
|
|
2504
3108
|
};
|
|
2505
3109
|
var ARRAY_ITEM_TYPE_MAP = {
|
|
2506
3110
|
string: "string",
|
|
@@ -2744,6 +3348,7 @@ function generateProtoDefinitions(schema) {
|
|
|
2744
3348
|
MergeConflictError,
|
|
2745
3349
|
MigrationBuilder,
|
|
2746
3350
|
MigrationRollbackError,
|
|
3351
|
+
MissingSecretKeyError,
|
|
2747
3352
|
OperationError,
|
|
2748
3353
|
RemoteClockDriftError,
|
|
2749
3354
|
RollbackBuilder,
|
|
@@ -2751,6 +3356,7 @@ function generateProtoDefinitions(schema) {
|
|
|
2751
3356
|
StorageError,
|
|
2752
3357
|
SyncError,
|
|
2753
3358
|
advanceVector,
|
|
3359
|
+
applyAtomicOp,
|
|
2754
3360
|
applyOperationTransforms,
|
|
2755
3361
|
base64ToBytes,
|
|
2756
3362
|
buildScopeMap,
|
|
@@ -2760,16 +3366,19 @@ function generateProtoDefinitions(schema) {
|
|
|
2760
3366
|
collectSchemaScopeFields,
|
|
2761
3367
|
collectSchemaScopeValueKeys,
|
|
2762
3368
|
computeDelta,
|
|
3369
|
+
createBlobRef,
|
|
2763
3370
|
createOperation,
|
|
2764
3371
|
createReversibleMigration,
|
|
2765
3372
|
createVersionVector,
|
|
2766
3373
|
decodeBytesFromOpData,
|
|
3374
|
+
decryptSecret,
|
|
2767
3375
|
defaultApplyFailureReason,
|
|
2768
3376
|
defaultSequenceFormat,
|
|
2769
3377
|
defineSchema,
|
|
2770
3378
|
deserializeVector,
|
|
2771
3379
|
dominates,
|
|
2772
3380
|
encodeBytesForOpData,
|
|
3381
|
+
encryptSecret,
|
|
2773
3382
|
extractScopeValuesFromClaims,
|
|
2774
3383
|
extractTimestamp,
|
|
2775
3384
|
formatSequenceValue,
|
|
@@ -2782,8 +3391,11 @@ function generateProtoDefinitions(schema) {
|
|
|
2782
3391
|
getKoraErrorFix,
|
|
2783
3392
|
getTransitionMap,
|
|
2784
3393
|
hasSchemaSyncRules,
|
|
3394
|
+
hashBlob,
|
|
3395
|
+
hashSecret,
|
|
2785
3396
|
isApplyFailure,
|
|
2786
3397
|
isAtomicOp,
|
|
3398
|
+
isBlobRef,
|
|
2787
3399
|
isCollectionSyncScoped,
|
|
2788
3400
|
isKoraBytesValue,
|
|
2789
3401
|
isLegacyNumericByteObject,
|
|
@@ -2793,14 +3405,20 @@ function generateProtoDefinitions(schema) {
|
|
|
2793
3405
|
migrate,
|
|
2794
3406
|
migrationStepsToSQL,
|
|
2795
3407
|
op,
|
|
3408
|
+
quoteIdent,
|
|
3409
|
+
replayOperationsForRecord,
|
|
2796
3410
|
resolveAtomicOp,
|
|
3411
|
+
revealSecret,
|
|
2797
3412
|
rollbackStepsToSQL,
|
|
2798
3413
|
serializeVector,
|
|
2799
3414
|
t,
|
|
2800
3415
|
toAtomicOp,
|
|
3416
|
+
transformSecretFieldsForWrite,
|
|
2801
3417
|
validateRecord,
|
|
2802
3418
|
validateTransition,
|
|
2803
3419
|
vectorsEqual,
|
|
2804
|
-
verifyOperationIntegrity
|
|
3420
|
+
verifyOperationIntegrity,
|
|
3421
|
+
verifySecret,
|
|
3422
|
+
verifySecretValue
|
|
2805
3423
|
});
|
|
2806
3424
|
//# sourceMappingURL=index.cjs.map
|