@korajs/core 0.3.2 → 0.5.0
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/{chunk-KQYXWUFV.js → chunk-H4FXU5OP.js} +66 -20
- package/dist/chunk-H4FXU5OP.js.map +1 -0
- package/dist/events-BeIEDJBW.d.cts +887 -0
- package/dist/events-BeIEDJBW.d.ts +887 -0
- package/dist/index.cjs +1410 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +564 -98
- package/dist/index.d.ts +564 -98
- package/dist/index.js +1287 -25
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +56 -17
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/package.json +2 -1
- package/dist/chunk-KQYXWUFV.js.map +0 -1
- package/dist/events-7Fhdjxd2.d.cts +0 -327
- package/dist/events-7Fhdjxd2.d.ts +0 -327
package/dist/index.cjs
CHANGED
|
@@ -20,36 +20,70 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
APPLY_FAILURE_CODES: () => APPLY_FAILURE_CODES,
|
|
24
|
+
APPLY_RESULTS: () => APPLY_RESULTS,
|
|
23
25
|
ArrayFieldBuilder: () => ArrayFieldBuilder,
|
|
24
26
|
CONNECTION_QUALITIES: () => CONNECTION_QUALITIES,
|
|
27
|
+
CausalTracker: () => CausalTracker,
|
|
25
28
|
ClockDriftError: () => ClockDriftError,
|
|
26
29
|
EnumFieldBuilder: () => EnumFieldBuilder,
|
|
27
30
|
FieldBuilder: () => FieldBuilder,
|
|
28
31
|
HybridLogicalClock: () => HybridLogicalClock,
|
|
32
|
+
KORA_ERROR_FIX_SUGGESTIONS: () => KORA_ERROR_FIX_SUGGESTIONS,
|
|
29
33
|
KoraError: () => KoraError,
|
|
30
34
|
MERGE_STRATEGIES: () => MERGE_STRATEGIES,
|
|
31
35
|
MergeConflictError: () => MergeConflictError,
|
|
36
|
+
MigrationBuilder: () => MigrationBuilder,
|
|
37
|
+
MigrationRollbackError: () => MigrationRollbackError,
|
|
32
38
|
OperationError: () => OperationError,
|
|
39
|
+
RollbackBuilder: () => RollbackBuilder,
|
|
33
40
|
SchemaValidationError: () => SchemaValidationError,
|
|
34
41
|
StorageError: () => StorageError,
|
|
35
42
|
SyncError: () => SyncError,
|
|
36
43
|
advanceVector: () => advanceVector,
|
|
44
|
+
applyOperationTransforms: () => applyOperationTransforms,
|
|
45
|
+
buildScopeMap: () => buildScopeMap,
|
|
46
|
+
buildStateMachineConstraints: () => buildStateMachineConstraints,
|
|
47
|
+
canAutoRollback: () => canAutoRollback,
|
|
48
|
+
collectSchemaScopeFields: () => collectSchemaScopeFields,
|
|
49
|
+
collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
|
|
37
50
|
computeDelta: () => computeDelta,
|
|
38
51
|
createOperation: () => createOperation,
|
|
52
|
+
createReversibleMigration: () => createReversibleMigration,
|
|
39
53
|
createVersionVector: () => createVersionVector,
|
|
54
|
+
defaultApplyFailureReason: () => defaultApplyFailureReason,
|
|
55
|
+
defaultSequenceFormat: () => defaultSequenceFormat,
|
|
40
56
|
defineSchema: () => defineSchema,
|
|
41
57
|
deserializeVector: () => deserializeVector,
|
|
42
58
|
dominates: () => dominates,
|
|
59
|
+
extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
|
|
43
60
|
extractTimestamp: () => extractTimestamp,
|
|
61
|
+
formatSequenceValue: () => formatSequenceValue,
|
|
44
62
|
generateFullDDL: () => generateFullDDL,
|
|
63
|
+
generateProtoDefinitions: () => generateProtoDefinitions,
|
|
64
|
+
generateRollbackSteps: () => generateRollbackSteps,
|
|
45
65
|
generateSQL: () => generateSQL,
|
|
46
66
|
generateUUIDv7: () => generateUUIDv7,
|
|
67
|
+
getCollectionScopeBindings: () => getCollectionScopeBindings,
|
|
68
|
+
getKoraErrorFix: () => getKoraErrorFix,
|
|
69
|
+
getTransitionMap: () => getTransitionMap,
|
|
70
|
+
hasSchemaSyncRules: () => hasSchemaSyncRules,
|
|
71
|
+
isApplyFailure: () => isApplyFailure,
|
|
72
|
+
isAtomicOp: () => isAtomicOp,
|
|
73
|
+
isCollectionSyncScoped: () => isCollectionSyncScoped,
|
|
47
74
|
isValidOperation: () => isValidOperation,
|
|
48
75
|
isValidUUIDv7: () => isValidUUIDv7,
|
|
49
76
|
mergeVectors: () => mergeVectors,
|
|
77
|
+
migrate: () => migrate,
|
|
78
|
+
migrationStepsToSQL: () => migrationStepsToSQL,
|
|
79
|
+
op: () => op,
|
|
80
|
+
resolveAtomicOp: () => resolveAtomicOp,
|
|
81
|
+
rollbackStepsToSQL: () => rollbackStepsToSQL,
|
|
50
82
|
serializeVector: () => serializeVector,
|
|
51
83
|
t: () => t,
|
|
84
|
+
toAtomicOp: () => toAtomicOp,
|
|
52
85
|
validateRecord: () => validateRecord,
|
|
86
|
+
validateTransition: () => validateTransition,
|
|
53
87
|
vectorsEqual: () => vectorsEqual,
|
|
54
88
|
verifyOperationIntegrity: () => verifyOperationIntegrity
|
|
55
89
|
});
|
|
@@ -65,6 +99,23 @@ var MERGE_STRATEGIES = [
|
|
|
65
99
|
];
|
|
66
100
|
var CONNECTION_QUALITIES = ["excellent", "good", "fair", "poor", "offline"];
|
|
67
101
|
|
|
102
|
+
// src/errors/error-fixes.ts
|
|
103
|
+
var KORA_ERROR_FIX_SUGGESTIONS = {
|
|
104
|
+
SCHEMA_VALIDATION: "Review your defineSchema() definition: every collection needs at least one field and a positive version.",
|
|
105
|
+
OPERATION_ERROR: "Check the operation payload matches your schema field types and required fields.",
|
|
106
|
+
MERGE_CONFLICT: "Add a custom resolver for the conflicting field in defineSchema(), or adjust constraint onConflict rules.",
|
|
107
|
+
SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
|
|
108
|
+
STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
|
|
109
|
+
CLOCK_DRIFT: "Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.",
|
|
110
|
+
PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
|
|
111
|
+
MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
|
|
112
|
+
INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
|
|
113
|
+
APP_NOT_READY: "Await app.ready before querying or mutating collections, or wrap the tree in <KoraProvider app={app}>."
|
|
114
|
+
};
|
|
115
|
+
function getKoraErrorFix(code) {
|
|
116
|
+
return KORA_ERROR_FIX_SUGGESTIONS[code];
|
|
117
|
+
}
|
|
118
|
+
|
|
68
119
|
// src/errors/errors.ts
|
|
69
120
|
var KoraError = class extends Error {
|
|
70
121
|
constructor(message, code, context) {
|
|
@@ -75,6 +126,16 @@ var KoraError = class extends Error {
|
|
|
75
126
|
}
|
|
76
127
|
code;
|
|
77
128
|
context;
|
|
129
|
+
/**
|
|
130
|
+
* Actionable hint for resolving this error (from registry or error context).
|
|
131
|
+
*/
|
|
132
|
+
get fix() {
|
|
133
|
+
const fromContext = this.context?.fix;
|
|
134
|
+
if (typeof fromContext === "string" && fromContext.length > 0) {
|
|
135
|
+
return fromContext;
|
|
136
|
+
}
|
|
137
|
+
return getKoraErrorFix(this.code);
|
|
138
|
+
}
|
|
78
139
|
};
|
|
79
140
|
var SchemaValidationError = class extends KoraError {
|
|
80
141
|
constructor(message, context) {
|
|
@@ -172,6 +233,7 @@ var HybridLogicalClock = class {
|
|
|
172
233
|
*/
|
|
173
234
|
receive(remote) {
|
|
174
235
|
const physicalTime = this.timeSource.now();
|
|
236
|
+
const wasColdStart = this.wallTime === 0;
|
|
175
237
|
if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
|
|
176
238
|
this.wallTime = physicalTime;
|
|
177
239
|
this.logical = 0;
|
|
@@ -183,7 +245,9 @@ var HybridLogicalClock = class {
|
|
|
183
245
|
} else {
|
|
184
246
|
this.logical++;
|
|
185
247
|
}
|
|
186
|
-
|
|
248
|
+
if (!wasColdStart) {
|
|
249
|
+
this.checkDrift(physicalTime);
|
|
250
|
+
}
|
|
187
251
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
188
252
|
}
|
|
189
253
|
/**
|
|
@@ -232,6 +296,50 @@ var HybridLogicalClock = class {
|
|
|
232
296
|
}
|
|
233
297
|
};
|
|
234
298
|
|
|
299
|
+
// src/causal/causal-tracker.ts
|
|
300
|
+
var CausalTracker = class {
|
|
301
|
+
lastOpIdByCollection = /* @__PURE__ */ new Map();
|
|
302
|
+
lastTransactionOpId = null;
|
|
303
|
+
/**
|
|
304
|
+
* Start a new transaction boundary. Clears in-transaction op ids.
|
|
305
|
+
*/
|
|
306
|
+
beginTransaction() {
|
|
307
|
+
this.lastTransactionOpId = null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Clear the transaction boundary without recording ops (after rollback).
|
|
311
|
+
*/
|
|
312
|
+
clearTransaction() {
|
|
313
|
+
this.lastTransactionOpId = null;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Compute causal dependencies for the next operation in a collection.
|
|
317
|
+
* Uses direct parents only: collection head and the previous op in the open transaction.
|
|
318
|
+
*/
|
|
319
|
+
nextCausalDeps(collection, inTransaction) {
|
|
320
|
+
const deps = [];
|
|
321
|
+
const lastInCollection = this.lastOpIdByCollection.get(collection);
|
|
322
|
+
if (lastInCollection !== void 0) {
|
|
323
|
+
deps.push(lastInCollection);
|
|
324
|
+
}
|
|
325
|
+
if (inTransaction && this.lastTransactionOpId !== null) {
|
|
326
|
+
if (!deps.includes(this.lastTransactionOpId)) {
|
|
327
|
+
deps.push(this.lastTransactionOpId);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return deps;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Record an operation after it has been created and assigned an id.
|
|
334
|
+
*/
|
|
335
|
+
afterOperation(collection, operationId, inTransaction) {
|
|
336
|
+
this.lastOpIdByCollection.set(collection, operationId);
|
|
337
|
+
if (inTransaction) {
|
|
338
|
+
this.lastTransactionOpId = operationId;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
235
343
|
// src/identifiers/uuid-v7.ts
|
|
236
344
|
var defaultRandom = globalThis.crypto;
|
|
237
345
|
function generateUUIDv7(timestamp = Date.now(), randomSource = defaultRandom) {
|
|
@@ -268,23 +376,61 @@ function formatUUID(bytes) {
|
|
|
268
376
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
269
377
|
}
|
|
270
378
|
|
|
379
|
+
// src/operations/apply-result.ts
|
|
380
|
+
var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
|
|
381
|
+
var APPLY_FAILURE_CODES = {
|
|
382
|
+
APPLY_FAILED: "APPLY_FAILED",
|
|
383
|
+
APPLY_SKIPPED: "APPLY_SKIPPED",
|
|
384
|
+
APPLY_REJECTED: "APPLY_REJECTED",
|
|
385
|
+
APPLY_DEFERRED: "APPLY_DEFERRED",
|
|
386
|
+
CLOCK_DRIFT: "CLOCK_DRIFT",
|
|
387
|
+
REFERENTIAL_INTEGRITY: "REFERENTIAL_INTEGRITY",
|
|
388
|
+
SCHEMA_MISMATCH: "SCHEMA_MISMATCH"
|
|
389
|
+
};
|
|
390
|
+
function isApplyFailure(result) {
|
|
391
|
+
return result === "skipped" || result === "rejected" || result === "deferred";
|
|
392
|
+
}
|
|
393
|
+
function defaultApplyFailureReason(result, overrides) {
|
|
394
|
+
const base = result === "deferred" ? {
|
|
395
|
+
code: APPLY_FAILURE_CODES.APPLY_DEFERRED,
|
|
396
|
+
message: "Operation apply was deferred.",
|
|
397
|
+
retriable: true
|
|
398
|
+
} : result === "rejected" ? {
|
|
399
|
+
code: APPLY_FAILURE_CODES.APPLY_REJECTED,
|
|
400
|
+
message: "Operation apply was rejected.",
|
|
401
|
+
retriable: false
|
|
402
|
+
} : {
|
|
403
|
+
code: APPLY_FAILURE_CODES.APPLY_SKIPPED,
|
|
404
|
+
message: "Operation apply was skipped.",
|
|
405
|
+
retriable: false
|
|
406
|
+
};
|
|
407
|
+
return { ...base, ...overrides };
|
|
408
|
+
}
|
|
409
|
+
|
|
271
410
|
// src/operations/content-hash.ts
|
|
272
411
|
async function computeOperationId(input, timestamp) {
|
|
273
|
-
const
|
|
412
|
+
const hashInput = {
|
|
274
413
|
type: input.type,
|
|
275
414
|
collection: input.collection,
|
|
276
415
|
recordId: input.recordId,
|
|
277
416
|
data: input.data,
|
|
278
417
|
timestamp,
|
|
279
418
|
nodeId: input.nodeId
|
|
280
|
-
}
|
|
419
|
+
};
|
|
420
|
+
if (input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0) {
|
|
421
|
+
hashInput.atomicOps = input.atomicOps;
|
|
422
|
+
}
|
|
423
|
+
const canonical = canonicalize(hashInput);
|
|
281
424
|
const encoded = new TextEncoder().encode(canonical);
|
|
282
425
|
const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
|
|
283
426
|
return bufferToHex(hashBuffer);
|
|
284
427
|
}
|
|
285
428
|
function canonicalize(obj) {
|
|
286
|
-
if (obj === null
|
|
287
|
-
return
|
|
429
|
+
if (obj === null) {
|
|
430
|
+
return "null";
|
|
431
|
+
}
|
|
432
|
+
if (obj === void 0) {
|
|
433
|
+
return "null";
|
|
288
434
|
}
|
|
289
435
|
if (typeof obj !== "object") {
|
|
290
436
|
return JSON.stringify(obj);
|
|
@@ -296,7 +442,7 @@ function canonicalize(obj) {
|
|
|
296
442
|
const keys = Object.keys(obj).sort();
|
|
297
443
|
const pairs = keys.map((key) => {
|
|
298
444
|
const value = obj[key];
|
|
299
|
-
return `${JSON.stringify(key)}:${canonicalize(value)}`;
|
|
445
|
+
return `${JSON.stringify(key)}:${canonicalize(value === void 0 ? null : value)}`;
|
|
300
446
|
});
|
|
301
447
|
return `{${pairs.join(",")}}`;
|
|
302
448
|
}
|
|
@@ -322,7 +468,10 @@ async function createOperation(input, clock) {
|
|
|
322
468
|
timestamp,
|
|
323
469
|
sequenceNumber: input.sequenceNumber,
|
|
324
470
|
causalDeps: [...input.causalDeps],
|
|
325
|
-
schemaVersion: input.schemaVersion
|
|
471
|
+
schemaVersion: input.schemaVersion,
|
|
472
|
+
...input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0 ? { atomicOps: { ...input.atomicOps } } : {},
|
|
473
|
+
...input.transactionId !== void 0 ? { transactionId: input.transactionId } : {},
|
|
474
|
+
...input.mutationName !== void 0 ? { mutationName: input.mutationName } : {}
|
|
326
475
|
};
|
|
327
476
|
return deepFreeze(operation);
|
|
328
477
|
}
|
|
@@ -390,29 +539,31 @@ function validateOperationParams(input) {
|
|
|
390
539
|
});
|
|
391
540
|
}
|
|
392
541
|
}
|
|
393
|
-
async function verifyOperationIntegrity(
|
|
542
|
+
async function verifyOperationIntegrity(op2) {
|
|
394
543
|
const input = {
|
|
395
|
-
nodeId:
|
|
396
|
-
type:
|
|
397
|
-
collection:
|
|
398
|
-
recordId:
|
|
399
|
-
data:
|
|
400
|
-
previousData:
|
|
401
|
-
sequenceNumber:
|
|
402
|
-
causalDeps:
|
|
403
|
-
schemaVersion:
|
|
544
|
+
nodeId: op2.nodeId,
|
|
545
|
+
type: op2.type,
|
|
546
|
+
collection: op2.collection,
|
|
547
|
+
recordId: op2.recordId,
|
|
548
|
+
data: op2.data,
|
|
549
|
+
previousData: op2.previousData,
|
|
550
|
+
sequenceNumber: op2.sequenceNumber,
|
|
551
|
+
causalDeps: op2.causalDeps,
|
|
552
|
+
schemaVersion: op2.schemaVersion,
|
|
553
|
+
...op2.atomicOps !== void 0 ? { atomicOps: op2.atomicOps } : {}
|
|
404
554
|
};
|
|
405
|
-
const serializedTs = HybridLogicalClock.serialize(
|
|
555
|
+
const serializedTs = HybridLogicalClock.serialize(op2.timestamp);
|
|
406
556
|
const expectedId = await computeOperationId(input, serializedTs);
|
|
407
|
-
return
|
|
557
|
+
return op2.id === expectedId;
|
|
408
558
|
}
|
|
409
559
|
function isValidOperation(value) {
|
|
410
560
|
if (typeof value !== "object" || value === null) return false;
|
|
411
|
-
const
|
|
412
|
-
return typeof
|
|
561
|
+
const op2 = value;
|
|
562
|
+
return typeof op2.id === "string" && typeof op2.nodeId === "string" && (op2.type === "insert" || op2.type === "update" || op2.type === "delete") && typeof op2.collection === "string" && typeof op2.recordId === "string" && typeof op2.sequenceNumber === "number" && Array.isArray(op2.causalDeps) && typeof op2.schemaVersion === "number" && typeof op2.timestamp === "object" && op2.timestamp !== null;
|
|
413
563
|
}
|
|
414
564
|
function deepFreeze(obj) {
|
|
415
565
|
if (typeof obj !== "object" || obj === null) return obj;
|
|
566
|
+
if (ArrayBuffer.isView(obj)) return obj;
|
|
416
567
|
Object.freeze(obj);
|
|
417
568
|
for (const value of Object.values(obj)) {
|
|
418
569
|
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
@@ -422,10 +573,257 @@ function deepFreeze(obj) {
|
|
|
422
573
|
return obj;
|
|
423
574
|
}
|
|
424
575
|
|
|
576
|
+
// src/operations/atomic-ops.ts
|
|
577
|
+
var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
|
|
578
|
+
function isAtomicOp(value) {
|
|
579
|
+
return typeof value === "object" && value !== null && KORA_ATOMIC_OP in value && value[KORA_ATOMIC_OP] === true;
|
|
580
|
+
}
|
|
581
|
+
function resolveAtomicOp(currentValue, sentinel) {
|
|
582
|
+
switch (sentinel.type) {
|
|
583
|
+
case "increment": {
|
|
584
|
+
const current = typeof currentValue === "number" ? currentValue : 0;
|
|
585
|
+
const operand = sentinel.value;
|
|
586
|
+
return current + operand;
|
|
587
|
+
}
|
|
588
|
+
case "max": {
|
|
589
|
+
const current = typeof currentValue === "number" ? currentValue : Number.NEGATIVE_INFINITY;
|
|
590
|
+
return Math.max(current, sentinel.value);
|
|
591
|
+
}
|
|
592
|
+
case "min": {
|
|
593
|
+
const current = typeof currentValue === "number" ? currentValue : Number.POSITIVE_INFINITY;
|
|
594
|
+
return Math.min(current, sentinel.value);
|
|
595
|
+
}
|
|
596
|
+
case "append": {
|
|
597
|
+
const current = Array.isArray(currentValue) ? [...currentValue] : [];
|
|
598
|
+
current.push(sentinel.value);
|
|
599
|
+
return current;
|
|
600
|
+
}
|
|
601
|
+
case "remove": {
|
|
602
|
+
if (!Array.isArray(currentValue)) return [];
|
|
603
|
+
return currentValue.filter((item) => item !== sentinel.value);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function createSentinel(type, value) {
|
|
608
|
+
return Object.freeze({
|
|
609
|
+
[KORA_ATOMIC_OP]: true,
|
|
610
|
+
type,
|
|
611
|
+
value
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
var op = {
|
|
615
|
+
/**
|
|
616
|
+
* Increment a number field by the given amount.
|
|
617
|
+
* Concurrent increments compose: the sum of all deltas is applied to the base.
|
|
618
|
+
*
|
|
619
|
+
* @param n - The amount to increment (use negative values to decrement)
|
|
620
|
+
*/
|
|
621
|
+
increment(n) {
|
|
622
|
+
return createSentinel("increment", n);
|
|
623
|
+
},
|
|
624
|
+
/**
|
|
625
|
+
* Decrement a number field by the given amount.
|
|
626
|
+
* Syntactic sugar for `op.increment(-n)`.
|
|
627
|
+
*
|
|
628
|
+
* @param n - The amount to decrement
|
|
629
|
+
*/
|
|
630
|
+
decrement(n) {
|
|
631
|
+
return createSentinel("increment", -n);
|
|
632
|
+
},
|
|
633
|
+
/**
|
|
634
|
+
* Set the field to the maximum of the current value and the given value.
|
|
635
|
+
* Concurrent max operations take the maximum of all values.
|
|
636
|
+
*
|
|
637
|
+
* @param n - The value to compare against the current value
|
|
638
|
+
*/
|
|
639
|
+
max(n) {
|
|
640
|
+
return createSentinel("max", n);
|
|
641
|
+
},
|
|
642
|
+
/**
|
|
643
|
+
* Set the field to the minimum of the current value and the given value.
|
|
644
|
+
* Concurrent min operations take the minimum of all values.
|
|
645
|
+
*
|
|
646
|
+
* @param n - The value to compare against the current value
|
|
647
|
+
*/
|
|
648
|
+
min(n) {
|
|
649
|
+
return createSentinel("min", n);
|
|
650
|
+
},
|
|
651
|
+
/**
|
|
652
|
+
* Append an item to an array field.
|
|
653
|
+
* Concurrent appends include all appended items.
|
|
654
|
+
*
|
|
655
|
+
* @param item - The item to append to the array
|
|
656
|
+
*/
|
|
657
|
+
append(item) {
|
|
658
|
+
return createSentinel("append", item);
|
|
659
|
+
},
|
|
660
|
+
/**
|
|
661
|
+
* Remove an item from an array field (by value equality).
|
|
662
|
+
* Concurrent removes of the same item are idempotent.
|
|
663
|
+
*
|
|
664
|
+
* @param item - The item to remove from the array
|
|
665
|
+
*/
|
|
666
|
+
remove(item) {
|
|
667
|
+
return createSentinel("remove", item);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
function toAtomicOp(sentinel) {
|
|
671
|
+
return { type: sentinel.type, value: sentinel.value };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/scopes/sync-scope-bindings.ts
|
|
675
|
+
function hasSchemaSyncRules(schema) {
|
|
676
|
+
return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
|
|
677
|
+
}
|
|
678
|
+
function getCollectionScopeBindings(schema, collectionName) {
|
|
679
|
+
const syncRule = schema.sync?.[collectionName];
|
|
680
|
+
if (syncRule && Object.keys(syncRule.where).length > 0) {
|
|
681
|
+
return { ...syncRule.where };
|
|
682
|
+
}
|
|
683
|
+
const scopeFields = schema.collections[collectionName]?.scope ?? [];
|
|
684
|
+
if (scopeFields.length === 0) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const bindings = {};
|
|
688
|
+
for (const field of scopeFields) {
|
|
689
|
+
bindings[field] = field;
|
|
690
|
+
}
|
|
691
|
+
return bindings;
|
|
692
|
+
}
|
|
693
|
+
function isCollectionSyncScoped(schema, collectionName) {
|
|
694
|
+
if (hasSchemaSyncRules(schema)) {
|
|
695
|
+
if (schema.sync?.[collectionName]) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
const legacyScope = schema.collections[collectionName]?.scope ?? [];
|
|
699
|
+
return legacyScope.length > 0;
|
|
700
|
+
}
|
|
701
|
+
return getCollectionScopeBindings(schema, collectionName) !== null;
|
|
702
|
+
}
|
|
703
|
+
function collectSchemaScopeValueKeys(schema) {
|
|
704
|
+
const keys = /* @__PURE__ */ new Set();
|
|
705
|
+
for (const collection of Object.values(schema.collections)) {
|
|
706
|
+
for (const field of collection.scope) {
|
|
707
|
+
keys.add(field);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (schema.sync) {
|
|
711
|
+
for (const rule of Object.values(schema.sync)) {
|
|
712
|
+
for (const scopeKey of Object.values(rule.where)) {
|
|
713
|
+
keys.add(scopeKey);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return [...keys];
|
|
718
|
+
}
|
|
719
|
+
function normalizeSyncRuleWhere(collectionName, where) {
|
|
720
|
+
const normalized = {};
|
|
721
|
+
for (const [field, scopeKey] of Object.entries(where)) {
|
|
722
|
+
if (scopeKey === true) {
|
|
723
|
+
normalized[field] = field;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (typeof scopeKey === "string" && scopeKey.length > 0) {
|
|
727
|
+
normalized[field] = scopeKey;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
throw new Error(
|
|
731
|
+
`Invalid sync rule for collection "${collectionName}": where.${field} must be true or a non-empty scope key string`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return normalized;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/state-machine/state-machine.ts
|
|
738
|
+
function validateTransition(constraint, fromValue, toValue) {
|
|
739
|
+
const from = String(fromValue);
|
|
740
|
+
const to = String(toValue);
|
|
741
|
+
const allowedTargets = constraint.transitions[from] ?? [];
|
|
742
|
+
return {
|
|
743
|
+
valid: allowedTargets.includes(to),
|
|
744
|
+
from,
|
|
745
|
+
to,
|
|
746
|
+
field: constraint.field,
|
|
747
|
+
collection: constraint.collection,
|
|
748
|
+
allowedTargets
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function buildStateMachineConstraints(schema) {
|
|
752
|
+
const constraints = [];
|
|
753
|
+
for (const [collectionName, collection] of Object.entries(schema.collections)) {
|
|
754
|
+
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
755
|
+
if (descriptor.kind === "enum" && descriptor.transitions !== null) {
|
|
756
|
+
constraints.push({
|
|
757
|
+
field: fieldName,
|
|
758
|
+
collection: collectionName,
|
|
759
|
+
transitions: descriptor.transitions
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return constraints;
|
|
765
|
+
}
|
|
766
|
+
function getTransitionMap(schema, collection, field) {
|
|
767
|
+
const collectionDef = schema.collections[collection];
|
|
768
|
+
if (!collectionDef) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const fieldDef = collectionDef.fields[field];
|
|
772
|
+
if (!fieldDef || fieldDef.kind !== "enum") {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
return fieldDef.transitions;
|
|
776
|
+
}
|
|
777
|
+
function validateStateMachineDefinition(collectionName, sm, fields) {
|
|
778
|
+
const fieldDef = fields[sm.field];
|
|
779
|
+
if (!fieldDef) {
|
|
780
|
+
throw new SchemaValidationError(
|
|
781
|
+
`State machine field "${sm.field}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(fields).join(", ")}`,
|
|
782
|
+
{ collection: collectionName, field: sm.field }
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
if (fieldDef.kind !== "enum") {
|
|
786
|
+
throw new SchemaValidationError(
|
|
787
|
+
`State machine field "${sm.field}" in collection "${collectionName}" must be an enum field, but got "${fieldDef.kind}"`,
|
|
788
|
+
{ collection: collectionName, field: sm.field, kind: fieldDef.kind }
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
const enumValues = fieldDef.enumValues;
|
|
792
|
+
if (!enumValues || enumValues.length === 0) {
|
|
793
|
+
throw new SchemaValidationError(
|
|
794
|
+
`State machine field "${sm.field}" in collection "${collectionName}" has no enum values defined`,
|
|
795
|
+
{ collection: collectionName, field: sm.field }
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const validValues = new Set(enumValues);
|
|
799
|
+
for (const [state, targets] of Object.entries(sm.transitions)) {
|
|
800
|
+
if (!validValues.has(state)) {
|
|
801
|
+
throw new SchemaValidationError(
|
|
802
|
+
`State machine transition source "${state}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
|
|
803
|
+
{ collection: collectionName, field: sm.field, state }
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
for (const target of targets) {
|
|
807
|
+
if (!validValues.has(target)) {
|
|
808
|
+
throw new SchemaValidationError(
|
|
809
|
+
`State machine transition target "${target}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
|
|
810
|
+
{ collection: collectionName, field: sm.field, state, target }
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (sm.onInvalidTransition !== "reject" && sm.onInvalidTransition !== "last-valid-state") {
|
|
816
|
+
throw new SchemaValidationError(
|
|
817
|
+
`State machine onInvalidTransition must be "reject" or "last-valid-state", got "${sm.onInvalidTransition}" in collection "${collectionName}"`,
|
|
818
|
+
{ collection: collectionName, onInvalidTransition: sm.onInvalidTransition }
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
425
823
|
// src/schema/define.ts
|
|
426
824
|
var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
427
825
|
var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
428
|
-
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_deleted"]);
|
|
826
|
+
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
|
|
429
827
|
function defineSchema(input) {
|
|
430
828
|
validateVersion(input.version);
|
|
431
829
|
const collections = {};
|
|
@@ -443,7 +841,40 @@ function defineSchema(input) {
|
|
|
443
841
|
relations[name] = { ...relationInput };
|
|
444
842
|
}
|
|
445
843
|
}
|
|
446
|
-
|
|
844
|
+
const migrations = {};
|
|
845
|
+
if (input.migrations) {
|
|
846
|
+
for (const [versionStr, migration] of Object.entries(input.migrations)) {
|
|
847
|
+
const version = Number(versionStr);
|
|
848
|
+
if (!Number.isInteger(version) || version < 2) {
|
|
849
|
+
throw new SchemaValidationError(
|
|
850
|
+
`Migration key "${versionStr}" is invalid. Must be an integer >= 2 (version 1 is the initial schema).`,
|
|
851
|
+
{ version: versionStr }
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
if (version > input.version) {
|
|
855
|
+
throw new SchemaValidationError(
|
|
856
|
+
`Migration version ${version} exceeds schema version ${input.version}. Migrations must target versions <= schema version.`,
|
|
857
|
+
{ migrationVersion: version, schemaVersion: input.version }
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
if (!migration.steps || migration.steps.length === 0) {
|
|
861
|
+
throw new SchemaValidationError(
|
|
862
|
+
`Migration for version ${version} has no steps. Use migrate() to define at least one step.`,
|
|
863
|
+
{ version }
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
migrations[version] = migration;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const sync = buildSyncRules(input.sync, collections);
|
|
870
|
+
applySyncRulesToCollectionScope(collections, sync);
|
|
871
|
+
return {
|
|
872
|
+
version: input.version,
|
|
873
|
+
collections,
|
|
874
|
+
relations,
|
|
875
|
+
migrations,
|
|
876
|
+
...sync ? { sync } : {}
|
|
877
|
+
};
|
|
447
878
|
}
|
|
448
879
|
function validateVersion(version) {
|
|
449
880
|
if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
|
|
@@ -505,7 +936,81 @@ function buildCollection(name, input) {
|
|
|
505
936
|
resolvers[fieldName] = resolver;
|
|
506
937
|
}
|
|
507
938
|
}
|
|
508
|
-
|
|
939
|
+
const scope = [];
|
|
940
|
+
if (input.scope) {
|
|
941
|
+
for (const scopeField of input.scope) {
|
|
942
|
+
if (!(scopeField in fields)) {
|
|
943
|
+
throw new SchemaValidationError(
|
|
944
|
+
`Scope field "${scopeField}" does not exist in collection "${name}". Available fields: ${Object.keys(fields).join(", ")}`,
|
|
945
|
+
{ collection: name, field: scopeField }
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
scope.push(scopeField);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
let stateMachine;
|
|
952
|
+
if (input.stateMachine) {
|
|
953
|
+
validateStateMachineDefinition(name, input.stateMachine, fields);
|
|
954
|
+
stateMachine = { ...input.stateMachine };
|
|
955
|
+
}
|
|
956
|
+
return { fields, indexes, constraints, resolvers, scope, stateMachine };
|
|
957
|
+
}
|
|
958
|
+
function buildSyncRules(syncInput, collections) {
|
|
959
|
+
if (!syncInput) {
|
|
960
|
+
return void 0;
|
|
961
|
+
}
|
|
962
|
+
const sync = {};
|
|
963
|
+
for (const [collectionName, ruleInput] of Object.entries(syncInput)) {
|
|
964
|
+
if (!(collectionName in collections)) {
|
|
965
|
+
throw new SchemaValidationError(
|
|
966
|
+
`Sync rule references collection "${collectionName}" which does not exist. Available collections: ${Object.keys(collections).join(", ")}`,
|
|
967
|
+
{ collection: collectionName }
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
if (!ruleInput.where || Object.keys(ruleInput.where).length === 0) {
|
|
971
|
+
throw new SchemaValidationError(
|
|
972
|
+
`Sync rule for collection "${collectionName}" must declare at least one where binding`,
|
|
973
|
+
{ collection: collectionName }
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const collection = collections[collectionName];
|
|
977
|
+
if (!collection) {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
for (const fieldName of Object.keys(ruleInput.where)) {
|
|
981
|
+
if (!(fieldName in collection.fields)) {
|
|
982
|
+
throw new SchemaValidationError(
|
|
983
|
+
`Sync rule where field "${fieldName}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(collection.fields).join(", ")}`,
|
|
984
|
+
{ collection: collectionName, field: fieldName }
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
try {
|
|
989
|
+
sync[collectionName] = { where: normalizeSyncRuleWhere(collectionName, ruleInput.where) };
|
|
990
|
+
} catch (error) {
|
|
991
|
+
throw new SchemaValidationError(
|
|
992
|
+
error instanceof Error ? error.message : "Invalid sync rule",
|
|
993
|
+
{ collection: collectionName }
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return sync;
|
|
998
|
+
}
|
|
999
|
+
function applySyncRulesToCollectionScope(collections, sync) {
|
|
1000
|
+
if (!sync) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
for (const [collectionName, rule] of Object.entries(sync)) {
|
|
1004
|
+
const collection = collections[collectionName];
|
|
1005
|
+
if (!collection) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
for (const fieldName of Object.keys(rule.where)) {
|
|
1009
|
+
if (!collection.scope.includes(fieldName)) {
|
|
1010
|
+
collection.scope.push(fieldName);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
509
1014
|
}
|
|
510
1015
|
function validateFieldName(collection, fieldName) {
|
|
511
1016
|
if (RESERVED_FIELDS.has(fieldName)) {
|
|
@@ -594,6 +1099,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
594
1099
|
}
|
|
595
1100
|
columns.push("_created_at INTEGER NOT NULL");
|
|
596
1101
|
columns.push("_updated_at INTEGER NOT NULL");
|
|
1102
|
+
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
597
1103
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
598
1104
|
statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
|
|
599
1105
|
${columns.join(",\n ")}
|
|
@@ -603,6 +1109,10 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
603
1109
|
statements.push(`--kora:safe-alter
|
|
604
1110
|
ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
|
|
605
1111
|
}
|
|
1112
|
+
statements.push(
|
|
1113
|
+
`--kora:safe-alter
|
|
1114
|
+
ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1115
|
+
);
|
|
606
1116
|
for (const indexField of collection.indexes) {
|
|
607
1117
|
statements.push(
|
|
608
1118
|
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
|
|
@@ -639,6 +1149,21 @@ function generateFullDDL(schema) {
|
|
|
639
1149
|
statements.push(
|
|
640
1150
|
"CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
|
|
641
1151
|
);
|
|
1152
|
+
statements.push(
|
|
1153
|
+
"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)"
|
|
1154
|
+
);
|
|
1155
|
+
statements.push(
|
|
1156
|
+
"CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
|
|
1157
|
+
);
|
|
1158
|
+
statements.push(
|
|
1159
|
+
"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)"
|
|
1160
|
+
);
|
|
1161
|
+
statements.push(
|
|
1162
|
+
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
|
|
1163
|
+
);
|
|
1164
|
+
statements.push(
|
|
1165
|
+
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
|
|
1166
|
+
);
|
|
642
1167
|
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
643
1168
|
statements.push(...generateSQL(name, collection, schema.relations));
|
|
644
1169
|
}
|
|
@@ -692,23 +1217,47 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
692
1217
|
_required;
|
|
693
1218
|
_defaultValue;
|
|
694
1219
|
_auto;
|
|
695
|
-
|
|
1220
|
+
_mergeStrategy;
|
|
1221
|
+
constructor(kind, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
696
1222
|
this._kind = kind;
|
|
697
1223
|
this._required = required;
|
|
698
1224
|
this._defaultValue = defaultValue;
|
|
699
1225
|
this._auto = auto;
|
|
1226
|
+
this._mergeStrategy = mergeStrategy;
|
|
700
1227
|
}
|
|
701
1228
|
/** Mark this field as optional (not required on insert) */
|
|
702
1229
|
optional() {
|
|
703
|
-
return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto);
|
|
1230
|
+
return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto, this._mergeStrategy);
|
|
704
1231
|
}
|
|
705
1232
|
/** Set a default value for this field. Implicitly makes the field optional. */
|
|
706
1233
|
default(value) {
|
|
707
|
-
return new _FieldBuilder(this._kind, false, value, this._auto);
|
|
1234
|
+
return new _FieldBuilder(this._kind, false, value, this._auto, this._mergeStrategy);
|
|
708
1235
|
}
|
|
709
1236
|
/** Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot set auto fields. */
|
|
710
1237
|
auto() {
|
|
711
|
-
return new _FieldBuilder(this._kind, false, void 0, true);
|
|
1238
|
+
return new _FieldBuilder(this._kind, false, void 0, true, this._mergeStrategy);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Declare a merge strategy for this field.
|
|
1242
|
+
* Controls how concurrent modifications are resolved during sync.
|
|
1243
|
+
*
|
|
1244
|
+
* @param strategy - The merge strategy to use:
|
|
1245
|
+
* - `'lww'`: Last-write-wins (default for scalar fields)
|
|
1246
|
+
* - `'counter'`: Sum of deltas from base (for numbers)
|
|
1247
|
+
* - `'max'`: Keep the maximum value (for numbers/timestamps)
|
|
1248
|
+
* - `'min'`: Keep the minimum value (for numbers/timestamps)
|
|
1249
|
+
* - `'union'`: Set-union merge (default for arrays)
|
|
1250
|
+
* - `'append-only'`: Concatenate additions (for arrays)
|
|
1251
|
+
* - `'server-authoritative'`: Always prefer the remote/server value
|
|
1252
|
+
*/
|
|
1253
|
+
merge(strategy) {
|
|
1254
|
+
return new _FieldBuilder(
|
|
1255
|
+
this._kind,
|
|
1256
|
+
this._required,
|
|
1257
|
+
this._defaultValue,
|
|
1258
|
+
this._auto,
|
|
1259
|
+
strategy
|
|
1260
|
+
);
|
|
712
1261
|
}
|
|
713
1262
|
/** @internal Build the final FieldDescriptor. Used by defineSchema(). */
|
|
714
1263
|
_build() {
|
|
@@ -718,24 +1267,102 @@ var FieldBuilder = class _FieldBuilder {
|
|
|
718
1267
|
defaultValue: this._defaultValue,
|
|
719
1268
|
auto: this._auto,
|
|
720
1269
|
enumValues: null,
|
|
721
|
-
itemKind: null
|
|
1270
|
+
itemKind: null,
|
|
1271
|
+
mergeStrategy: this._mergeStrategy,
|
|
1272
|
+
transitions: null
|
|
722
1273
|
};
|
|
723
1274
|
}
|
|
724
1275
|
};
|
|
725
1276
|
var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
726
1277
|
_enumValues;
|
|
727
|
-
|
|
728
|
-
|
|
1278
|
+
_transitions;
|
|
1279
|
+
constructor(values, required = true, defaultValue = void 0, auto = false, mergeStrategy = null, transitions = null) {
|
|
1280
|
+
super("enum", required, defaultValue, auto, mergeStrategy);
|
|
729
1281
|
this._enumValues = values;
|
|
1282
|
+
this._transitions = transitions;
|
|
730
1283
|
}
|
|
731
1284
|
optional() {
|
|
732
|
-
return new _EnumFieldBuilder(
|
|
1285
|
+
return new _EnumFieldBuilder(
|
|
1286
|
+
this._enumValues,
|
|
1287
|
+
false,
|
|
1288
|
+
this._defaultValue,
|
|
1289
|
+
this._auto,
|
|
1290
|
+
this._mergeStrategy,
|
|
1291
|
+
this._transitions
|
|
1292
|
+
);
|
|
733
1293
|
}
|
|
734
1294
|
default(value) {
|
|
735
|
-
return new _EnumFieldBuilder(
|
|
1295
|
+
return new _EnumFieldBuilder(
|
|
1296
|
+
this._enumValues,
|
|
1297
|
+
false,
|
|
1298
|
+
value,
|
|
1299
|
+
this._auto,
|
|
1300
|
+
this._mergeStrategy,
|
|
1301
|
+
this._transitions
|
|
1302
|
+
);
|
|
736
1303
|
}
|
|
737
1304
|
auto() {
|
|
738
|
-
return new _EnumFieldBuilder(
|
|
1305
|
+
return new _EnumFieldBuilder(
|
|
1306
|
+
this._enumValues,
|
|
1307
|
+
false,
|
|
1308
|
+
void 0,
|
|
1309
|
+
true,
|
|
1310
|
+
this._mergeStrategy,
|
|
1311
|
+
this._transitions
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
merge(strategy) {
|
|
1315
|
+
return new _EnumFieldBuilder(
|
|
1316
|
+
this._enumValues,
|
|
1317
|
+
this._required,
|
|
1318
|
+
this._defaultValue,
|
|
1319
|
+
this._auto,
|
|
1320
|
+
strategy,
|
|
1321
|
+
this._transitions
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Declare allowed state transitions for this enum field.
|
|
1326
|
+
* Enables state machine validation during mutations and merges.
|
|
1327
|
+
*
|
|
1328
|
+
* @param map - Map of state to allowed next states
|
|
1329
|
+
*
|
|
1330
|
+
* @example
|
|
1331
|
+
* ```typescript
|
|
1332
|
+
* t.enum(['draft', 'pending', 'confirmed', 'cancelled']).transitions({
|
|
1333
|
+
* draft: ['pending', 'cancelled'],
|
|
1334
|
+
* pending: ['confirmed', 'cancelled'],
|
|
1335
|
+
* confirmed: [],
|
|
1336
|
+
* cancelled: [],
|
|
1337
|
+
* })
|
|
1338
|
+
* ```
|
|
1339
|
+
*/
|
|
1340
|
+
transitions(map) {
|
|
1341
|
+
const validValues = new Set(this._enumValues);
|
|
1342
|
+
for (const [state, targets] of Object.entries(map)) {
|
|
1343
|
+
if (!validValues.has(state)) {
|
|
1344
|
+
throw new SchemaValidationError(
|
|
1345
|
+
`Invalid source state "${state}" in transition map. Valid values: ${[...validValues].join(", ")}`,
|
|
1346
|
+
{ state, validValues: [...validValues] }
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
for (const target of targets) {
|
|
1350
|
+
if (!validValues.has(target)) {
|
|
1351
|
+
throw new SchemaValidationError(
|
|
1352
|
+
`Invalid target state "${target}" in transition from "${state}". Valid values: ${[...validValues].join(", ")}`,
|
|
1353
|
+
{ state, target, validValues: [...validValues] }
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return new _EnumFieldBuilder(
|
|
1359
|
+
this._enumValues,
|
|
1360
|
+
this._required,
|
|
1361
|
+
this._defaultValue,
|
|
1362
|
+
this._auto,
|
|
1363
|
+
this._mergeStrategy,
|
|
1364
|
+
map
|
|
1365
|
+
);
|
|
739
1366
|
}
|
|
740
1367
|
_build() {
|
|
741
1368
|
return {
|
|
@@ -744,14 +1371,16 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
|
744
1371
|
defaultValue: this._defaultValue,
|
|
745
1372
|
auto: this._auto,
|
|
746
1373
|
enumValues: this._enumValues,
|
|
747
|
-
itemKind: null
|
|
1374
|
+
itemKind: null,
|
|
1375
|
+
mergeStrategy: this._mergeStrategy,
|
|
1376
|
+
transitions: this._transitions
|
|
748
1377
|
};
|
|
749
1378
|
}
|
|
750
1379
|
};
|
|
751
1380
|
var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
752
1381
|
_itemKind;
|
|
753
|
-
constructor(itemBuilder, required = true, defaultValue = void 0, auto = false) {
|
|
754
|
-
super("array", required, defaultValue, auto);
|
|
1382
|
+
constructor(itemBuilder, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
|
|
1383
|
+
super("array", required, defaultValue, auto, mergeStrategy);
|
|
755
1384
|
this._itemKind = itemBuilder._build().kind;
|
|
756
1385
|
}
|
|
757
1386
|
optional() {
|
|
@@ -759,14 +1388,36 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
759
1388
|
new FieldBuilder(this._itemKind),
|
|
760
1389
|
false,
|
|
761
1390
|
this._defaultValue,
|
|
762
|
-
this._auto
|
|
1391
|
+
this._auto,
|
|
1392
|
+
this._mergeStrategy
|
|
763
1393
|
);
|
|
764
1394
|
}
|
|
765
1395
|
default(value) {
|
|
766
|
-
return new _ArrayFieldBuilder(
|
|
1396
|
+
return new _ArrayFieldBuilder(
|
|
1397
|
+
new FieldBuilder(this._itemKind),
|
|
1398
|
+
false,
|
|
1399
|
+
value,
|
|
1400
|
+
this._auto,
|
|
1401
|
+
this._mergeStrategy
|
|
1402
|
+
);
|
|
767
1403
|
}
|
|
768
1404
|
auto() {
|
|
769
|
-
return new _ArrayFieldBuilder(
|
|
1405
|
+
return new _ArrayFieldBuilder(
|
|
1406
|
+
new FieldBuilder(this._itemKind),
|
|
1407
|
+
false,
|
|
1408
|
+
void 0,
|
|
1409
|
+
true,
|
|
1410
|
+
this._mergeStrategy
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
merge(strategy) {
|
|
1414
|
+
return new _ArrayFieldBuilder(
|
|
1415
|
+
new FieldBuilder(this._itemKind),
|
|
1416
|
+
this._required,
|
|
1417
|
+
this._defaultValue,
|
|
1418
|
+
this._auto,
|
|
1419
|
+
strategy
|
|
1420
|
+
);
|
|
770
1421
|
}
|
|
771
1422
|
_build() {
|
|
772
1423
|
return {
|
|
@@ -775,7 +1426,9 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
|
|
|
775
1426
|
defaultValue: this._defaultValue,
|
|
776
1427
|
auto: this._auto,
|
|
777
1428
|
enumValues: null,
|
|
778
|
-
itemKind: this._itemKind
|
|
1429
|
+
itemKind: this._itemKind,
|
|
1430
|
+
mergeStrategy: this._mergeStrategy,
|
|
1431
|
+
transitions: null
|
|
779
1432
|
};
|
|
780
1433
|
}
|
|
781
1434
|
};
|
|
@@ -829,10 +1482,14 @@ function validateRecord(collection, collectionDef, data, operationType) {
|
|
|
829
1482
|
}
|
|
830
1483
|
if (operationType === "update") {
|
|
831
1484
|
if (hasValue) {
|
|
832
|
-
if (value
|
|
1485
|
+
if (isAtomicOp(value)) {
|
|
1486
|
+
result[fieldName] = value;
|
|
1487
|
+
} else if (value !== void 0 && value !== null) {
|
|
833
1488
|
validateFieldValue(collection, fieldName, descriptor, value);
|
|
1489
|
+
result[fieldName] = value;
|
|
1490
|
+
} else {
|
|
1491
|
+
result[fieldName] = value;
|
|
834
1492
|
}
|
|
835
|
-
result[fieldName] = value;
|
|
836
1493
|
}
|
|
837
1494
|
continue;
|
|
838
1495
|
}
|
|
@@ -995,34 +1652,34 @@ function matchesJsType(value, expected) {
|
|
|
995
1652
|
function topologicalSort(operations) {
|
|
996
1653
|
if (operations.length <= 1) return [...operations];
|
|
997
1654
|
const opMap = /* @__PURE__ */ new Map();
|
|
998
|
-
for (const
|
|
999
|
-
opMap.set(
|
|
1655
|
+
for (const op2 of operations) {
|
|
1656
|
+
opMap.set(op2.id, op2);
|
|
1000
1657
|
}
|
|
1001
1658
|
const inDegree = /* @__PURE__ */ new Map();
|
|
1002
1659
|
const dependents = /* @__PURE__ */ new Map();
|
|
1003
|
-
for (const
|
|
1004
|
-
if (!inDegree.has(
|
|
1005
|
-
inDegree.set(
|
|
1660
|
+
for (const op2 of operations) {
|
|
1661
|
+
if (!inDegree.has(op2.id)) {
|
|
1662
|
+
inDegree.set(op2.id, 0);
|
|
1006
1663
|
}
|
|
1007
|
-
if (!dependents.has(
|
|
1008
|
-
dependents.set(
|
|
1664
|
+
if (!dependents.has(op2.id)) {
|
|
1665
|
+
dependents.set(op2.id, []);
|
|
1009
1666
|
}
|
|
1010
|
-
for (const depId of
|
|
1667
|
+
for (const depId of op2.causalDeps) {
|
|
1011
1668
|
if (opMap.has(depId)) {
|
|
1012
|
-
inDegree.set(
|
|
1669
|
+
inDegree.set(op2.id, (inDegree.get(op2.id) ?? 0) + 1);
|
|
1013
1670
|
const deps = dependents.get(depId);
|
|
1014
1671
|
if (deps) {
|
|
1015
|
-
deps.push(
|
|
1672
|
+
deps.push(op2.id);
|
|
1016
1673
|
} else {
|
|
1017
|
-
dependents.set(depId, [
|
|
1674
|
+
dependents.set(depId, [op2.id]);
|
|
1018
1675
|
}
|
|
1019
1676
|
}
|
|
1020
1677
|
}
|
|
1021
1678
|
}
|
|
1022
1679
|
const heap = new MinHeap(compareByTimestamp);
|
|
1023
|
-
for (const
|
|
1024
|
-
if ((inDegree.get(
|
|
1025
|
-
heap.push(
|
|
1680
|
+
for (const op2 of operations) {
|
|
1681
|
+
if ((inDegree.get(op2.id) ?? 0) === 0) {
|
|
1682
|
+
heap.push(op2);
|
|
1026
1683
|
}
|
|
1027
1684
|
}
|
|
1028
1685
|
const result = [];
|
|
@@ -1034,8 +1691,8 @@ function topologicalSort(operations) {
|
|
|
1034
1691
|
const deg = (inDegree.get(depId) ?? 0) - 1;
|
|
1035
1692
|
inDegree.set(depId, deg);
|
|
1036
1693
|
if (deg === 0) {
|
|
1037
|
-
const
|
|
1038
|
-
if (
|
|
1694
|
+
const op2 = opMap.get(depId);
|
|
1695
|
+
if (op2) heap.push(op2);
|
|
1039
1696
|
}
|
|
1040
1697
|
}
|
|
1041
1698
|
}
|
|
@@ -1076,28 +1733,30 @@ var MinHeap = class {
|
|
|
1076
1733
|
return top;
|
|
1077
1734
|
}
|
|
1078
1735
|
bubbleUp(index) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
this.
|
|
1083
|
-
|
|
1736
|
+
let current = index;
|
|
1737
|
+
while (current > 0) {
|
|
1738
|
+
const parentIndex = current - 1 >> 1;
|
|
1739
|
+
if (this.cmp(this.data[current], this.data[parentIndex]) >= 0) break;
|
|
1740
|
+
this.swap(current, parentIndex);
|
|
1741
|
+
current = parentIndex;
|
|
1084
1742
|
}
|
|
1085
1743
|
}
|
|
1086
1744
|
sinkDown(index) {
|
|
1087
1745
|
const length = this.data.length;
|
|
1746
|
+
let current = index;
|
|
1088
1747
|
while (true) {
|
|
1089
|
-
let smallest =
|
|
1090
|
-
const left = 2 *
|
|
1091
|
-
const right = 2 *
|
|
1748
|
+
let smallest = current;
|
|
1749
|
+
const left = 2 * current + 1;
|
|
1750
|
+
const right = 2 * current + 2;
|
|
1092
1751
|
if (left < length && this.cmp(this.data[left], this.data[smallest]) < 0) {
|
|
1093
1752
|
smallest = left;
|
|
1094
1753
|
}
|
|
1095
1754
|
if (right < length && this.cmp(this.data[right], this.data[smallest]) < 0) {
|
|
1096
1755
|
smallest = right;
|
|
1097
1756
|
}
|
|
1098
|
-
if (smallest ===
|
|
1099
|
-
this.swap(
|
|
1100
|
-
|
|
1757
|
+
if (smallest === current) break;
|
|
1758
|
+
this.swap(current, smallest);
|
|
1759
|
+
current = smallest;
|
|
1101
1760
|
}
|
|
1102
1761
|
}
|
|
1103
1762
|
swap(i, j) {
|
|
@@ -1136,12 +1795,13 @@ function vectorsEqual(a, b) {
|
|
|
1136
1795
|
}
|
|
1137
1796
|
return true;
|
|
1138
1797
|
}
|
|
1139
|
-
function computeDelta(localVector, remoteVector, operationLog) {
|
|
1798
|
+
async function computeDelta(localVector, remoteVector, operationLog) {
|
|
1140
1799
|
const missing = [];
|
|
1141
1800
|
for (const [nodeId, localSeq] of localVector) {
|
|
1142
1801
|
const remoteSeq = remoteVector.get(nodeId) ?? 0;
|
|
1143
1802
|
if (localSeq > remoteSeq) {
|
|
1144
|
-
|
|
1803
|
+
const ops = await operationLog.getRange(nodeId, remoteSeq + 1, localSeq);
|
|
1804
|
+
missing.push(...ops);
|
|
1145
1805
|
}
|
|
1146
1806
|
}
|
|
1147
1807
|
return topologicalSort(missing);
|
|
@@ -1154,38 +1814,716 @@ function deserializeVector(s) {
|
|
|
1154
1814
|
const entries = JSON.parse(s);
|
|
1155
1815
|
return new Map(entries);
|
|
1156
1816
|
}
|
|
1817
|
+
|
|
1818
|
+
// src/scopes/build-scope-map.ts
|
|
1819
|
+
function buildScopeMap(schema, scopeValues) {
|
|
1820
|
+
const result = {};
|
|
1821
|
+
const partialSync = hasSchemaSyncRules(schema);
|
|
1822
|
+
for (const collName of Object.keys(schema.collections)) {
|
|
1823
|
+
if (partialSync && !isCollectionSyncScoped(schema, collName)) {
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
const bindings = getCollectionScopeBindings(schema, collName);
|
|
1827
|
+
if (bindings) {
|
|
1828
|
+
const collScope = {};
|
|
1829
|
+
for (const [field, scopeKey] of Object.entries(bindings)) {
|
|
1830
|
+
if (scopeKey in scopeValues) {
|
|
1831
|
+
collScope[field] = scopeValues[scopeKey];
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
result[collName] = collScope;
|
|
1835
|
+
} else {
|
|
1836
|
+
result[collName] = {};
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return result;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/scopes/extract-scope-from-claims.ts
|
|
1843
|
+
function collectSchemaScopeFields(schema) {
|
|
1844
|
+
return collectSchemaScopeValueKeys(schema);
|
|
1845
|
+
}
|
|
1846
|
+
function extractScopeValuesFromClaims(schema, claims) {
|
|
1847
|
+
const scopeFields = collectSchemaScopeValueKeys(schema);
|
|
1848
|
+
if (scopeFields.length === 0) {
|
|
1849
|
+
return {};
|
|
1850
|
+
}
|
|
1851
|
+
const nestedScope = claims.scope;
|
|
1852
|
+
const scopeObject = typeof nestedScope === "object" && nestedScope !== null && !Array.isArray(nestedScope) ? nestedScope : {};
|
|
1853
|
+
const result = {};
|
|
1854
|
+
for (const field of scopeFields) {
|
|
1855
|
+
if (field in claims) {
|
|
1856
|
+
result[field] = claims[field];
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
if (field in scopeObject) {
|
|
1860
|
+
result[field] = scopeObject[field];
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
if (field === "userId" && typeof claims.sub === "string") {
|
|
1864
|
+
result.userId = claims.sub;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return result;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/sequences/sequence-format.ts
|
|
1871
|
+
function formatSequenceValue(template, counter, nodeId, now) {
|
|
1872
|
+
const date = now ?? /* @__PURE__ */ new Date();
|
|
1873
|
+
const yyyy = String(date.getUTCFullYear());
|
|
1874
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
1875
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
1876
|
+
const dateStr = `${yyyy}${mm}${dd}`;
|
|
1877
|
+
return template.replace(/\{([^}]+)\}/g, (_match, token) => {
|
|
1878
|
+
if (token === "date") return dateStr;
|
|
1879
|
+
if (token === "node4") return nodeId.slice(0, 4);
|
|
1880
|
+
if (token === "node8") return nodeId.slice(0, 8);
|
|
1881
|
+
if (token === "seq") return String(counter).padStart(4, "0");
|
|
1882
|
+
if (token.startsWith("seq:")) {
|
|
1883
|
+
const width = Number.parseInt(token.slice(4), 10);
|
|
1884
|
+
if (Number.isNaN(width) || width < 1) {
|
|
1885
|
+
return String(counter).padStart(4, "0");
|
|
1886
|
+
}
|
|
1887
|
+
return String(counter).padStart(width, "0");
|
|
1888
|
+
}
|
|
1889
|
+
return `{${token}}`;
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
function defaultSequenceFormat(name) {
|
|
1893
|
+
return `${name}-{seq:4}`;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/migration/apply-operation-transforms.ts
|
|
1897
|
+
var MAX_TRANSFORM_STEPS = 32;
|
|
1898
|
+
function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
|
|
1899
|
+
let current = operation;
|
|
1900
|
+
for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
|
|
1901
|
+
if (current.schemaVersion === targetSchemaVersion) {
|
|
1902
|
+
return current;
|
|
1903
|
+
}
|
|
1904
|
+
const transform = transforms.find(
|
|
1905
|
+
(candidate) => candidate.fromVersion === current.schemaVersion
|
|
1906
|
+
);
|
|
1907
|
+
if (!transform) {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
const next = transform.transform(current);
|
|
1911
|
+
if (next === null) {
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
current = next;
|
|
1915
|
+
}
|
|
1916
|
+
return null;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/migrations/migration-builder.ts
|
|
1920
|
+
var RollbackBuilder = class {
|
|
1921
|
+
_steps = [];
|
|
1922
|
+
/**
|
|
1923
|
+
* Add a field during rollback (to reverse a removeField).
|
|
1924
|
+
*/
|
|
1925
|
+
addField(collection, field, builder) {
|
|
1926
|
+
this._steps.push({ type: "addField", collection, field, descriptor: builder._build() });
|
|
1927
|
+
return this;
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Remove a field during rollback (to reverse an addField).
|
|
1931
|
+
*/
|
|
1932
|
+
removeField(collection, field) {
|
|
1933
|
+
this._steps.push({ type: "removeField", collection, field });
|
|
1934
|
+
return this;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Rename a field during rollback (to reverse a renameField).
|
|
1938
|
+
*/
|
|
1939
|
+
renameField(collection, from, to) {
|
|
1940
|
+
this._steps.push({ type: "renameField", collection, from, to });
|
|
1941
|
+
return this;
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Add an index during rollback (to reverse a removeIndex).
|
|
1945
|
+
*/
|
|
1946
|
+
addIndex(collection, field) {
|
|
1947
|
+
this._steps.push({ type: "addIndex", collection, field });
|
|
1948
|
+
return this;
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Remove an index during rollback (to reverse an addIndex).
|
|
1952
|
+
*/
|
|
1953
|
+
removeIndex(collection, field) {
|
|
1954
|
+
this._steps.push({ type: "removeIndex", collection, field });
|
|
1955
|
+
return this;
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Run a backfill during rollback (to reverse a previous backfill).
|
|
1959
|
+
*/
|
|
1960
|
+
backfill(collection, transform) {
|
|
1961
|
+
this._steps.push({ type: "backfill", collection, transform });
|
|
1962
|
+
return this;
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Get the accumulated rollback steps.
|
|
1966
|
+
*/
|
|
1967
|
+
_getSteps() {
|
|
1968
|
+
return [...this._steps];
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
var MigrationBuilder = class _MigrationBuilder {
|
|
1972
|
+
steps;
|
|
1973
|
+
rollbackSteps;
|
|
1974
|
+
safelyReversible;
|
|
1975
|
+
constructor(steps = [], rollbackSteps, safelyReversible) {
|
|
1976
|
+
this.steps = steps;
|
|
1977
|
+
this.rollbackSteps = rollbackSteps;
|
|
1978
|
+
this.safelyReversible = safelyReversible ?? this._computeSafelyReversible(steps, rollbackSteps);
|
|
1979
|
+
}
|
|
1980
|
+
/**
|
|
1981
|
+
* Add a new field to a collection.
|
|
1982
|
+
* The field builder provides the type and default value.
|
|
1983
|
+
*/
|
|
1984
|
+
addField(collection, field, builder) {
|
|
1985
|
+
return new _MigrationBuilder([
|
|
1986
|
+
...this.steps,
|
|
1987
|
+
{ type: "addField", collection, field, descriptor: builder._build() }
|
|
1988
|
+
]);
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Remove a field from a collection.
|
|
1992
|
+
* Optionally accepts a field builder to preserve the descriptor for rollback.
|
|
1993
|
+
* Without the descriptor, rollback of this step requires a custom `.down()`.
|
|
1994
|
+
*
|
|
1995
|
+
* @param collection - The collection name
|
|
1996
|
+
* @param field - The field name to remove
|
|
1997
|
+
* @param builder - Optional field builder preserving the type info for rollback
|
|
1998
|
+
*/
|
|
1999
|
+
removeField(collection, field, builder) {
|
|
2000
|
+
const step = builder ? { type: "removeField", collection, field, descriptor: builder._build() } : { type: "removeField", collection, field };
|
|
2001
|
+
return new _MigrationBuilder([...this.steps, step]);
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Rename a field in a collection.
|
|
2005
|
+
* Implemented as ALTER TABLE RENAME COLUMN (SQLite 3.25+).
|
|
2006
|
+
*/
|
|
2007
|
+
renameField(collection, from, to) {
|
|
2008
|
+
return new _MigrationBuilder([...this.steps, { type: "renameField", collection, from, to }]);
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Add an index on a field.
|
|
2012
|
+
*/
|
|
2013
|
+
addIndex(collection, field) {
|
|
2014
|
+
return new _MigrationBuilder([...this.steps, { type: "addIndex", collection, field }]);
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Remove an index on a field.
|
|
2018
|
+
*/
|
|
2019
|
+
removeIndex(collection, field) {
|
|
2020
|
+
return new _MigrationBuilder([...this.steps, { type: "removeIndex", collection, field }]);
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Backfill records in a collection using a transform function.
|
|
2024
|
+
* The transform receives each record and returns the fields to update.
|
|
2025
|
+
* Runs after structural changes (addField, renameField, etc.).
|
|
2026
|
+
*
|
|
2027
|
+
* @param collection - The collection name
|
|
2028
|
+
* @param transform - Forward transform function
|
|
2029
|
+
* @param reverseTransform - Optional reverse transform for rollback support
|
|
2030
|
+
*/
|
|
2031
|
+
backfill(collection, transform, reverseTransform) {
|
|
2032
|
+
return new _MigrationBuilder([
|
|
2033
|
+
...this.steps,
|
|
2034
|
+
{ type: "backfill", collection, transform, reverseTransform }
|
|
2035
|
+
]);
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Define explicit rollback steps for this migration.
|
|
2039
|
+
* If not called, inverse steps are auto-generated from forward steps.
|
|
2040
|
+
*
|
|
2041
|
+
* @param fn - Function that receives a RollbackBuilder to define rollback steps
|
|
2042
|
+
* @returns A new MigrationBuilder with the rollback steps attached
|
|
2043
|
+
*
|
|
2044
|
+
* @example
|
|
2045
|
+
* ```typescript
|
|
2046
|
+
* migrate()
|
|
2047
|
+
* .addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
|
|
2048
|
+
* .addIndex('todos', 'priority')
|
|
2049
|
+
* .down((rollback) => {
|
|
2050
|
+
* rollback
|
|
2051
|
+
* .removeIndex('todos', 'priority')
|
|
2052
|
+
* .removeField('todos', 'priority')
|
|
2053
|
+
* })
|
|
2054
|
+
* ```
|
|
2055
|
+
*/
|
|
2056
|
+
down(fn) {
|
|
2057
|
+
const rollbackBuilder = new RollbackBuilder();
|
|
2058
|
+
fn(rollbackBuilder);
|
|
2059
|
+
const rbSteps = rollbackBuilder._getSteps();
|
|
2060
|
+
return new _MigrationBuilder(this.steps, rbSteps, true);
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Determine if a migration is safely reversible based on its steps.
|
|
2064
|
+
* A migration is not safely reversible if:
|
|
2065
|
+
* - It has a backfill step without a reverseTransform and no explicit rollback
|
|
2066
|
+
* - It has a removeField without a stored descriptor and no explicit rollback
|
|
2067
|
+
*/
|
|
2068
|
+
_computeSafelyReversible(steps, rollbackSteps) {
|
|
2069
|
+
if (rollbackSteps !== void 0) {
|
|
2070
|
+
return true;
|
|
2071
|
+
}
|
|
2072
|
+
for (const step of steps) {
|
|
2073
|
+
if (step.type === "backfill" && !step.reverseTransform) {
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
if (step.type === "removeField" && !step.descriptor) {
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
return true;
|
|
2081
|
+
}
|
|
2082
|
+
};
|
|
2083
|
+
function migrate() {
|
|
2084
|
+
return new MigrationBuilder();
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// src/migrations/migration-rollback.ts
|
|
2088
|
+
var MigrationRollbackError = class extends KoraError {
|
|
2089
|
+
constructor(step) {
|
|
2090
|
+
super(
|
|
2091
|
+
`Cannot auto-generate rollback for "${step.type}" step on collection "${step.collection}". Provide an explicit .down() definition for this migration.`,
|
|
2092
|
+
"MIGRATION_ROLLBACK",
|
|
2093
|
+
{ stepType: step.type, collection: step.collection }
|
|
2094
|
+
);
|
|
2095
|
+
this.name = "MigrationRollbackError";
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
function canAutoRollback(step) {
|
|
2099
|
+
switch (step.type) {
|
|
2100
|
+
case "addField":
|
|
2101
|
+
case "addIndex":
|
|
2102
|
+
case "removeIndex":
|
|
2103
|
+
case "renameField":
|
|
2104
|
+
return true;
|
|
2105
|
+
case "removeField":
|
|
2106
|
+
case "backfill":
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function generateRollbackSteps(forwardSteps) {
|
|
2111
|
+
const rollbackSteps = [];
|
|
2112
|
+
for (let i = forwardSteps.length - 1; i >= 0; i--) {
|
|
2113
|
+
const step = forwardSteps[i];
|
|
2114
|
+
if (step === void 0) continue;
|
|
2115
|
+
const rollback = generateSingleRollbackStep(step);
|
|
2116
|
+
rollbackSteps.push(rollback);
|
|
2117
|
+
}
|
|
2118
|
+
return rollbackSteps;
|
|
2119
|
+
}
|
|
2120
|
+
function generateSingleRollbackStep(step) {
|
|
2121
|
+
switch (step.type) {
|
|
2122
|
+
case "addField":
|
|
2123
|
+
return { type: "removeField", collection: step.collection, field: step.field };
|
|
2124
|
+
case "removeField":
|
|
2125
|
+
if (step.descriptor) {
|
|
2126
|
+
return {
|
|
2127
|
+
type: "addField",
|
|
2128
|
+
collection: step.collection,
|
|
2129
|
+
field: step.field,
|
|
2130
|
+
descriptor: step.descriptor
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
throw new MigrationRollbackError(step);
|
|
2134
|
+
case "renameField":
|
|
2135
|
+
return { type: "renameField", collection: step.collection, from: step.to, to: step.from };
|
|
2136
|
+
case "addIndex":
|
|
2137
|
+
return { type: "removeIndex", collection: step.collection, field: step.field };
|
|
2138
|
+
case "removeIndex":
|
|
2139
|
+
return { type: "addIndex", collection: step.collection, field: step.field };
|
|
2140
|
+
case "backfill":
|
|
2141
|
+
if (step.reverseTransform) {
|
|
2142
|
+
return {
|
|
2143
|
+
type: "backfill",
|
|
2144
|
+
collection: step.collection,
|
|
2145
|
+
transform: step.reverseTransform
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
throw new MigrationRollbackError(step);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function createReversibleMigration(upSteps, downSteps, fromVersion, toVersion) {
|
|
2152
|
+
const down = downSteps !== null ? [...downSteps] : generateRollbackSteps(upSteps);
|
|
2153
|
+
return {
|
|
2154
|
+
up: [...upSteps],
|
|
2155
|
+
down,
|
|
2156
|
+
fromVersion,
|
|
2157
|
+
toVersion
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// src/migrations/migration-sql.ts
|
|
2162
|
+
function migrationStepsToSQL(steps) {
|
|
2163
|
+
const statements = [];
|
|
2164
|
+
for (const step of steps) {
|
|
2165
|
+
switch (step.type) {
|
|
2166
|
+
case "addField":
|
|
2167
|
+
statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
|
|
2168
|
+
break;
|
|
2169
|
+
case "removeField":
|
|
2170
|
+
statements.push(`ALTER TABLE ${step.collection} DROP COLUMN ${step.field}`);
|
|
2171
|
+
break;
|
|
2172
|
+
case "renameField":
|
|
2173
|
+
statements.push(`ALTER TABLE ${step.collection} RENAME COLUMN ${step.from} TO ${step.to}`);
|
|
2174
|
+
break;
|
|
2175
|
+
case "addIndex":
|
|
2176
|
+
statements.push(
|
|
2177
|
+
`CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
|
|
2178
|
+
);
|
|
2179
|
+
break;
|
|
2180
|
+
case "removeIndex":
|
|
2181
|
+
statements.push(`DROP INDEX IF EXISTS idx_${step.collection}_${step.field}`);
|
|
2182
|
+
break;
|
|
2183
|
+
case "backfill":
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return statements;
|
|
2188
|
+
}
|
|
2189
|
+
function rollbackStepsToSQL(migration) {
|
|
2190
|
+
const rollbackSteps = migration.rollbackSteps ?? generateRollbackSteps(migration.steps);
|
|
2191
|
+
return migrationStepsToSQL(rollbackSteps);
|
|
2192
|
+
}
|
|
2193
|
+
function addFieldSQL(collection, field, descriptor) {
|
|
2194
|
+
const sqlType = mapFieldType2(descriptor);
|
|
2195
|
+
const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
|
|
2196
|
+
if (descriptor.defaultValue !== void 0) {
|
|
2197
|
+
parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
|
|
2198
|
+
}
|
|
2199
|
+
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
2200
|
+
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
2201
|
+
parts.push(`CHECK (${field} IN (${values}))`);
|
|
2202
|
+
}
|
|
2203
|
+
return parts.join(" ");
|
|
2204
|
+
}
|
|
2205
|
+
function mapFieldType2(descriptor) {
|
|
2206
|
+
switch (descriptor.kind) {
|
|
2207
|
+
case "string":
|
|
2208
|
+
return "TEXT";
|
|
2209
|
+
case "number":
|
|
2210
|
+
return "REAL";
|
|
2211
|
+
case "boolean":
|
|
2212
|
+
return "INTEGER";
|
|
2213
|
+
case "enum":
|
|
2214
|
+
return "TEXT";
|
|
2215
|
+
case "timestamp":
|
|
2216
|
+
return "INTEGER";
|
|
2217
|
+
case "array":
|
|
2218
|
+
return "TEXT";
|
|
2219
|
+
case "richtext":
|
|
2220
|
+
return "BLOB";
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
function sqlDefault2(value) {
|
|
2224
|
+
if (value === null) return "NULL";
|
|
2225
|
+
if (typeof value === "string") return `'${value}'`;
|
|
2226
|
+
if (typeof value === "number") return String(value);
|
|
2227
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
2228
|
+
return `'${JSON.stringify(value)}'`;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// src/codegen/proto-generator.ts
|
|
2232
|
+
var SCALAR_TYPE_MAP = {
|
|
2233
|
+
string: "string",
|
|
2234
|
+
number: "double",
|
|
2235
|
+
boolean: "bool",
|
|
2236
|
+
timestamp: "int64",
|
|
2237
|
+
richtext: "bytes"
|
|
2238
|
+
};
|
|
2239
|
+
var ARRAY_ITEM_TYPE_MAP = {
|
|
2240
|
+
string: "string",
|
|
2241
|
+
number: "double",
|
|
2242
|
+
boolean: "bool",
|
|
2243
|
+
timestamp: "int64"
|
|
2244
|
+
};
|
|
2245
|
+
function toPascalCase(name) {
|
|
2246
|
+
return name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
2247
|
+
}
|
|
2248
|
+
function toSnakeCase(name) {
|
|
2249
|
+
return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
2250
|
+
}
|
|
2251
|
+
function resolveProtoType(fieldName, descriptor, messageName) {
|
|
2252
|
+
if (descriptor.kind === "enum") {
|
|
2253
|
+
return `${messageName}${toPascalCase(fieldName)}`;
|
|
2254
|
+
}
|
|
2255
|
+
if (descriptor.kind === "array") {
|
|
2256
|
+
const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
|
|
2257
|
+
return itemType;
|
|
2258
|
+
}
|
|
2259
|
+
return SCALAR_TYPE_MAP[descriptor.kind];
|
|
2260
|
+
}
|
|
2261
|
+
function generateEnumBlock(enumTypeName, enumValues, indent) {
|
|
2262
|
+
const lines = [];
|
|
2263
|
+
lines.push(`${indent}enum ${enumTypeName} {`);
|
|
2264
|
+
lines.push(`${indent} ${enumTypeName.toUpperCase()}_UNSPECIFIED = 0;`);
|
|
2265
|
+
for (let i = 0; i < enumValues.length; i++) {
|
|
2266
|
+
const value = enumValues[i];
|
|
2267
|
+
if (value === void 0) continue;
|
|
2268
|
+
lines.push(`${indent} ${enumTypeName.toUpperCase()}_${value.toUpperCase()} = ${i + 1};`);
|
|
2269
|
+
}
|
|
2270
|
+
lines.push(`${indent}}`);
|
|
2271
|
+
return lines.join("\n");
|
|
2272
|
+
}
|
|
2273
|
+
function generateCollectionMessage(collectionName, collection, typeMap) {
|
|
2274
|
+
const messageName = `${toPascalCase(collectionName)}Record`;
|
|
2275
|
+
const lines = [];
|
|
2276
|
+
const nestedEnums = [];
|
|
2277
|
+
lines.push(`message ${messageName} {`);
|
|
2278
|
+
lines.push(" string id = 1;");
|
|
2279
|
+
typeMap.set(`${collectionName}.id`, "string");
|
|
2280
|
+
let fieldNumber = 2;
|
|
2281
|
+
const entries = Object.entries(collection.fields);
|
|
2282
|
+
for (const [fieldName, descriptor] of entries) {
|
|
2283
|
+
const protoFieldName = toSnakeCase(fieldName);
|
|
2284
|
+
const protoType = resolveProtoType(fieldName, descriptor, messageName);
|
|
2285
|
+
const mapKey = `${collectionName}.${fieldName}`;
|
|
2286
|
+
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
2287
|
+
const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
|
|
2288
|
+
nestedEnums.push(generateEnumBlock(enumTypeName, descriptor.enumValues, " "));
|
|
2289
|
+
typeMap.set(mapKey, enumTypeName);
|
|
2290
|
+
lines.push(` ${enumTypeName} ${protoFieldName} = ${fieldNumber};`);
|
|
2291
|
+
} else if (descriptor.kind === "array") {
|
|
2292
|
+
typeMap.set(mapKey, `repeated ${protoType}`);
|
|
2293
|
+
lines.push(` repeated ${protoType} ${protoFieldName} = ${fieldNumber};`);
|
|
2294
|
+
} else {
|
|
2295
|
+
typeMap.set(mapKey, protoType);
|
|
2296
|
+
lines.push(` ${protoType} ${protoFieldName} = ${fieldNumber};`);
|
|
2297
|
+
}
|
|
2298
|
+
fieldNumber++;
|
|
2299
|
+
}
|
|
2300
|
+
if (nestedEnums.length > 0) {
|
|
2301
|
+
lines.push("");
|
|
2302
|
+
for (const enumBlock of nestedEnums) {
|
|
2303
|
+
lines.push(enumBlock);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
lines.push("}");
|
|
2307
|
+
return lines.join("\n");
|
|
2308
|
+
}
|
|
2309
|
+
var KORA_OPERATION_MESSAGE = `message KoraOperation {
|
|
2310
|
+
string id = 1;
|
|
2311
|
+
string node_id = 2;
|
|
2312
|
+
string type = 3;
|
|
2313
|
+
string collection = 4;
|
|
2314
|
+
string record_id = 5;
|
|
2315
|
+
bytes data = 6;
|
|
2316
|
+
bytes previous_data = 7;
|
|
2317
|
+
int64 wall_time = 8;
|
|
2318
|
+
int32 logical = 9;
|
|
2319
|
+
string timestamp_node_id = 10;
|
|
2320
|
+
int64 sequence_number = 11;
|
|
2321
|
+
repeated string causal_deps = 12;
|
|
2322
|
+
int32 schema_version = 13;
|
|
2323
|
+
}`;
|
|
2324
|
+
var OPERATION_BATCH_MESSAGE = `message OperationBatch {
|
|
2325
|
+
repeated KoraOperation operations = 1;
|
|
2326
|
+
bool is_final = 2;
|
|
2327
|
+
}`;
|
|
2328
|
+
var HANDSHAKE_MESSAGE = `message HandshakeMessage {
|
|
2329
|
+
map<string, int64> version_vector = 1;
|
|
2330
|
+
int32 schema_version = 2;
|
|
2331
|
+
string node_id = 3;
|
|
2332
|
+
}`;
|
|
2333
|
+
var HANDSHAKE_RESPONSE_MESSAGE = `message HandshakeResponse {
|
|
2334
|
+
map<string, int64> version_vector = 1;
|
|
2335
|
+
int32 schema_version = 2;
|
|
2336
|
+
}`;
|
|
2337
|
+
var ACKNOWLEDGMENT_MESSAGE = `message Acknowledgment {
|
|
2338
|
+
int64 sequence_number = 1;
|
|
2339
|
+
string node_id = 2;
|
|
2340
|
+
}`;
|
|
2341
|
+
function buildJsonDescriptor(schema) {
|
|
2342
|
+
const nested = {};
|
|
2343
|
+
for (const [collectionName, collection] of Object.entries(schema.collections)) {
|
|
2344
|
+
const messageName = `${toPascalCase(collectionName)}Record`;
|
|
2345
|
+
const fields = {
|
|
2346
|
+
id: { type: "string", id: 1 }
|
|
2347
|
+
};
|
|
2348
|
+
const nestedTypes = {};
|
|
2349
|
+
let fieldNumber = 2;
|
|
2350
|
+
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
2351
|
+
const protoFieldName = toSnakeCase(fieldName);
|
|
2352
|
+
const fieldDef = { id: fieldNumber };
|
|
2353
|
+
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
2354
|
+
const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
|
|
2355
|
+
fieldDef.type = enumTypeName;
|
|
2356
|
+
const enumValuesObj = {
|
|
2357
|
+
[`${enumTypeName.toUpperCase()}_UNSPECIFIED`]: 0
|
|
2358
|
+
};
|
|
2359
|
+
for (let i = 0; i < descriptor.enumValues.length; i++) {
|
|
2360
|
+
const val = descriptor.enumValues[i];
|
|
2361
|
+
if (val === void 0) continue;
|
|
2362
|
+
enumValuesObj[`${enumTypeName.toUpperCase()}_${val.toUpperCase()}`] = i + 1;
|
|
2363
|
+
}
|
|
2364
|
+
nestedTypes[enumTypeName] = { values: enumValuesObj };
|
|
2365
|
+
} else if (descriptor.kind === "array") {
|
|
2366
|
+
const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
|
|
2367
|
+
fieldDef.type = itemType;
|
|
2368
|
+
fieldDef.rule = "repeated";
|
|
2369
|
+
} else {
|
|
2370
|
+
fieldDef.type = SCALAR_TYPE_MAP[descriptor.kind];
|
|
2371
|
+
}
|
|
2372
|
+
fields[protoFieldName] = fieldDef;
|
|
2373
|
+
fieldNumber++;
|
|
2374
|
+
}
|
|
2375
|
+
const messageDescriptor = { fields };
|
|
2376
|
+
if (Object.keys(nestedTypes).length > 0) {
|
|
2377
|
+
messageDescriptor.nested = nestedTypes;
|
|
2378
|
+
}
|
|
2379
|
+
nested[messageName] = messageDescriptor;
|
|
2380
|
+
}
|
|
2381
|
+
nested.KoraOperation = {
|
|
2382
|
+
fields: {
|
|
2383
|
+
id: { type: "string", id: 1 },
|
|
2384
|
+
node_id: { type: "string", id: 2 },
|
|
2385
|
+
type: { type: "string", id: 3 },
|
|
2386
|
+
collection: { type: "string", id: 4 },
|
|
2387
|
+
record_id: { type: "string", id: 5 },
|
|
2388
|
+
data: { type: "bytes", id: 6 },
|
|
2389
|
+
previous_data: { type: "bytes", id: 7 },
|
|
2390
|
+
wall_time: { type: "int64", id: 8 },
|
|
2391
|
+
logical: { type: "int32", id: 9 },
|
|
2392
|
+
timestamp_node_id: { type: "string", id: 10 },
|
|
2393
|
+
sequence_number: { type: "int64", id: 11 },
|
|
2394
|
+
causal_deps: { type: "string", id: 12, rule: "repeated" },
|
|
2395
|
+
schema_version: { type: "int32", id: 13 }
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
nested.OperationBatch = {
|
|
2399
|
+
fields: {
|
|
2400
|
+
operations: { type: "KoraOperation", id: 1, rule: "repeated" },
|
|
2401
|
+
is_final: { type: "bool", id: 2 }
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
nested.HandshakeMessage = {
|
|
2405
|
+
fields: {
|
|
2406
|
+
version_vector: { keyType: "string", type: "int64", id: 1 },
|
|
2407
|
+
schema_version: { type: "int32", id: 2 },
|
|
2408
|
+
node_id: { type: "string", id: 3 }
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
nested.HandshakeResponse = {
|
|
2412
|
+
fields: {
|
|
2413
|
+
version_vector: { keyType: "string", type: "int64", id: 1 },
|
|
2414
|
+
schema_version: { type: "int32", id: 2 }
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
nested.Acknowledgment = {
|
|
2418
|
+
fields: {
|
|
2419
|
+
sequence_number: { type: "int64", id: 1 },
|
|
2420
|
+
node_id: { type: "string", id: 2 }
|
|
2421
|
+
}
|
|
2422
|
+
};
|
|
2423
|
+
return {
|
|
2424
|
+
nested: {
|
|
2425
|
+
kora: { nested }
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
function generateProtoDefinitions(schema) {
|
|
2430
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
2431
|
+
const sections = [];
|
|
2432
|
+
sections.push('syntax = "proto3";');
|
|
2433
|
+
sections.push("");
|
|
2434
|
+
sections.push("package kora;");
|
|
2435
|
+
const collectionEntries = Object.entries(schema.collections);
|
|
2436
|
+
if (collectionEntries.length > 0) {
|
|
2437
|
+
sections.push("");
|
|
2438
|
+
sections.push("// Collection record messages");
|
|
2439
|
+
for (const [collectionName, collection] of collectionEntries) {
|
|
2440
|
+
sections.push("");
|
|
2441
|
+
sections.push(generateCollectionMessage(collectionName, collection, typeMap));
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
sections.push("");
|
|
2445
|
+
sections.push("// Sync protocol messages");
|
|
2446
|
+
sections.push("");
|
|
2447
|
+
sections.push(KORA_OPERATION_MESSAGE);
|
|
2448
|
+
sections.push("");
|
|
2449
|
+
sections.push(OPERATION_BATCH_MESSAGE);
|
|
2450
|
+
sections.push("");
|
|
2451
|
+
sections.push(HANDSHAKE_MESSAGE);
|
|
2452
|
+
sections.push("");
|
|
2453
|
+
sections.push(HANDSHAKE_RESPONSE_MESSAGE);
|
|
2454
|
+
sections.push("");
|
|
2455
|
+
sections.push(ACKNOWLEDGMENT_MESSAGE);
|
|
2456
|
+
const proto = `${sections.join("\n")}
|
|
2457
|
+
`;
|
|
2458
|
+
const jsonDescriptor = buildJsonDescriptor(schema);
|
|
2459
|
+
return { proto, typeMap, jsonDescriptor };
|
|
2460
|
+
}
|
|
1157
2461
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1158
2462
|
0 && (module.exports = {
|
|
2463
|
+
APPLY_FAILURE_CODES,
|
|
2464
|
+
APPLY_RESULTS,
|
|
1159
2465
|
ArrayFieldBuilder,
|
|
1160
2466
|
CONNECTION_QUALITIES,
|
|
2467
|
+
CausalTracker,
|
|
1161
2468
|
ClockDriftError,
|
|
1162
2469
|
EnumFieldBuilder,
|
|
1163
2470
|
FieldBuilder,
|
|
1164
2471
|
HybridLogicalClock,
|
|
2472
|
+
KORA_ERROR_FIX_SUGGESTIONS,
|
|
1165
2473
|
KoraError,
|
|
1166
2474
|
MERGE_STRATEGIES,
|
|
1167
2475
|
MergeConflictError,
|
|
2476
|
+
MigrationBuilder,
|
|
2477
|
+
MigrationRollbackError,
|
|
1168
2478
|
OperationError,
|
|
2479
|
+
RollbackBuilder,
|
|
1169
2480
|
SchemaValidationError,
|
|
1170
2481
|
StorageError,
|
|
1171
2482
|
SyncError,
|
|
1172
2483
|
advanceVector,
|
|
2484
|
+
applyOperationTransforms,
|
|
2485
|
+
buildScopeMap,
|
|
2486
|
+
buildStateMachineConstraints,
|
|
2487
|
+
canAutoRollback,
|
|
2488
|
+
collectSchemaScopeFields,
|
|
2489
|
+
collectSchemaScopeValueKeys,
|
|
1173
2490
|
computeDelta,
|
|
1174
2491
|
createOperation,
|
|
2492
|
+
createReversibleMigration,
|
|
1175
2493
|
createVersionVector,
|
|
2494
|
+
defaultApplyFailureReason,
|
|
2495
|
+
defaultSequenceFormat,
|
|
1176
2496
|
defineSchema,
|
|
1177
2497
|
deserializeVector,
|
|
1178
2498
|
dominates,
|
|
2499
|
+
extractScopeValuesFromClaims,
|
|
1179
2500
|
extractTimestamp,
|
|
2501
|
+
formatSequenceValue,
|
|
1180
2502
|
generateFullDDL,
|
|
2503
|
+
generateProtoDefinitions,
|
|
2504
|
+
generateRollbackSteps,
|
|
1181
2505
|
generateSQL,
|
|
1182
2506
|
generateUUIDv7,
|
|
2507
|
+
getCollectionScopeBindings,
|
|
2508
|
+
getKoraErrorFix,
|
|
2509
|
+
getTransitionMap,
|
|
2510
|
+
hasSchemaSyncRules,
|
|
2511
|
+
isApplyFailure,
|
|
2512
|
+
isAtomicOp,
|
|
2513
|
+
isCollectionSyncScoped,
|
|
1183
2514
|
isValidOperation,
|
|
1184
2515
|
isValidUUIDv7,
|
|
1185
2516
|
mergeVectors,
|
|
2517
|
+
migrate,
|
|
2518
|
+
migrationStepsToSQL,
|
|
2519
|
+
op,
|
|
2520
|
+
resolveAtomicOp,
|
|
2521
|
+
rollbackStepsToSQL,
|
|
1186
2522
|
serializeVector,
|
|
1187
2523
|
t,
|
|
2524
|
+
toAtomicOp,
|
|
1188
2525
|
validateRecord,
|
|
2526
|
+
validateTransition,
|
|
1189
2527
|
vectorsEqual,
|
|
1190
2528
|
verifyOperationIntegrity
|
|
1191
2529
|
});
|