@korajs/core 1.0.0-beta.12 → 1.0.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -13
- package/dist/bindings/index.d.cts +26 -6
- package/dist/bindings/index.d.ts +26 -6
- package/dist/{build-scope-map-CItP1gsz.d.cts → build-scope-map-C9zBTJ1R.d.cts} +1 -1
- package/dist/{build-scope-map-Cv5HfCQP.d.ts → build-scope-map-CZaMyZLV.d.ts} +1 -1
- package/dist/{chunk-5IICSH6H.js → chunk-QUD3DMSD.js} +668 -24
- package/dist/chunk-QUD3DMSD.js.map +1 -0
- package/dist/events-Da29Dt_3.d.cts +1737 -0
- package/dist/events-Da29Dt_3.d.ts +1737 -0
- package/dist/index.cjs +2548 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +810 -220
- package/dist/index.d.ts +810 -220
- package/dist/index.js +2028 -334
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +857 -85
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +251 -21
- package/dist/internal.d.ts +251 -21
- package/dist/internal.js +447 -1
- package/dist/internal.js.map +1 -1
- package/dist/sql-gen-BOeuiyqu.d.cts +622 -0
- package/dist/sql-gen-CXTA6w-w.d.ts +622 -0
- package/package.json +2 -1
- package/dist/chunk-5IICSH6H.js.map +0 -1
- package/dist/events-D7JCtQSD.d.cts +0 -1010
- package/dist/events-D7JCtQSD.d.ts +0 -1010
- package/dist/operation-BX4ok7U4.d.ts +0 -164
- package/dist/operation-BXvJ6RiA.d.cts +0 -164
|
@@ -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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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(
|
|
392
|
-
validateOperationParams(
|
|
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
|
|
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
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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-
|
|
1297
|
+
//# sourceMappingURL=chunk-QUD3DMSD.js.map
|