@korajs/core 0.4.0 → 0.6.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/bindings/index.cjs +105 -0
- package/dist/bindings/index.cjs.map +1 -0
- package/dist/bindings/index.d.cts +101 -0
- package/dist/bindings/index.d.ts +101 -0
- package/dist/bindings/index.js +78 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/build-scope-map-B1CcncZN.d.cts +33 -0
- package/dist/build-scope-map-BmgAT82h.d.ts +33 -0
- package/dist/{chunk-CFP5YFXC.js → chunk-3VIOZT7D.js} +50 -5
- package/dist/chunk-3VIOZT7D.js.map +1 -0
- package/dist/{events-BMulupSB.d.cts → events-D-FWfFg8.d.cts} +46 -94
- package/dist/{events-BMulupSB.d.ts → events-D-FWfFg8.d.ts} +46 -94
- package/dist/index.cjs +378 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -50
- package/dist/index.d.ts +178 -50
- package/dist/index.js +321 -12
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +37 -4
- 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/dist/operation-3-ZJJf3w.d.cts +94 -0
- package/dist/operation-CygOwIjZ.d.ts +94 -0
- package/package.json +12 -1
- package/dist/chunk-CFP5YFXC.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -20,12 +20,17 @@ 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,
|
|
25
|
+
AppNotReadyError: () => AppNotReadyError,
|
|
23
26
|
ArrayFieldBuilder: () => ArrayFieldBuilder,
|
|
24
27
|
CONNECTION_QUALITIES: () => CONNECTION_QUALITIES,
|
|
28
|
+
CausalTracker: () => CausalTracker,
|
|
25
29
|
ClockDriftError: () => ClockDriftError,
|
|
26
30
|
EnumFieldBuilder: () => EnumFieldBuilder,
|
|
27
31
|
FieldBuilder: () => FieldBuilder,
|
|
28
32
|
HybridLogicalClock: () => HybridLogicalClock,
|
|
33
|
+
KORA_ERROR_FIX_SUGGESTIONS: () => KORA_ERROR_FIX_SUGGESTIONS,
|
|
29
34
|
KoraError: () => KoraError,
|
|
30
35
|
MERGE_STRATEGIES: () => MERGE_STRATEGIES,
|
|
31
36
|
MergeConflictError: () => MergeConflictError,
|
|
@@ -37,17 +42,22 @@ __export(index_exports, {
|
|
|
37
42
|
StorageError: () => StorageError,
|
|
38
43
|
SyncError: () => SyncError,
|
|
39
44
|
advanceVector: () => advanceVector,
|
|
45
|
+
applyOperationTransforms: () => applyOperationTransforms,
|
|
40
46
|
buildScopeMap: () => buildScopeMap,
|
|
41
47
|
buildStateMachineConstraints: () => buildStateMachineConstraints,
|
|
42
48
|
canAutoRollback: () => canAutoRollback,
|
|
49
|
+
collectSchemaScopeFields: () => collectSchemaScopeFields,
|
|
50
|
+
collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
|
|
43
51
|
computeDelta: () => computeDelta,
|
|
44
52
|
createOperation: () => createOperation,
|
|
45
53
|
createReversibleMigration: () => createReversibleMigration,
|
|
46
54
|
createVersionVector: () => createVersionVector,
|
|
55
|
+
defaultApplyFailureReason: () => defaultApplyFailureReason,
|
|
47
56
|
defaultSequenceFormat: () => defaultSequenceFormat,
|
|
48
57
|
defineSchema: () => defineSchema,
|
|
49
58
|
deserializeVector: () => deserializeVector,
|
|
50
59
|
dominates: () => dominates,
|
|
60
|
+
extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
|
|
51
61
|
extractTimestamp: () => extractTimestamp,
|
|
52
62
|
formatSequenceValue: () => formatSequenceValue,
|
|
53
63
|
generateFullDDL: () => generateFullDDL,
|
|
@@ -55,8 +65,13 @@ __export(index_exports, {
|
|
|
55
65
|
generateRollbackSteps: () => generateRollbackSteps,
|
|
56
66
|
generateSQL: () => generateSQL,
|
|
57
67
|
generateUUIDv7: () => generateUUIDv7,
|
|
68
|
+
getCollectionScopeBindings: () => getCollectionScopeBindings,
|
|
69
|
+
getKoraErrorFix: () => getKoraErrorFix,
|
|
58
70
|
getTransitionMap: () => getTransitionMap,
|
|
71
|
+
hasSchemaSyncRules: () => hasSchemaSyncRules,
|
|
72
|
+
isApplyFailure: () => isApplyFailure,
|
|
59
73
|
isAtomicOp: () => isAtomicOp,
|
|
74
|
+
isCollectionSyncScoped: () => isCollectionSyncScoped,
|
|
60
75
|
isValidOperation: () => isValidOperation,
|
|
61
76
|
isValidUUIDv7: () => isValidUUIDv7,
|
|
62
77
|
mergeVectors: () => mergeVectors,
|
|
@@ -85,6 +100,23 @@ var MERGE_STRATEGIES = [
|
|
|
85
100
|
];
|
|
86
101
|
var CONNECTION_QUALITIES = ["excellent", "good", "fair", "poor", "offline"];
|
|
87
102
|
|
|
103
|
+
// src/errors/error-fixes.ts
|
|
104
|
+
var KORA_ERROR_FIX_SUGGESTIONS = {
|
|
105
|
+
SCHEMA_VALIDATION: "Review your defineSchema() definition: every collection needs at least one field and a positive version.",
|
|
106
|
+
OPERATION_ERROR: "Check the operation payload matches your schema field types and required fields.",
|
|
107
|
+
MERGE_CONFLICT: "Add a custom resolver for the conflicting field in defineSchema(), or adjust constraint onConflict rules.",
|
|
108
|
+
SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
|
|
109
|
+
STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
|
|
110
|
+
CLOCK_DRIFT: "Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.",
|
|
111
|
+
PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
|
|
112
|
+
MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
|
|
113
|
+
INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
|
|
114
|
+
APP_NOT_READY: "Await app.ready before querying or mutating collections, or wrap the tree in <KoraProvider app={app}>."
|
|
115
|
+
};
|
|
116
|
+
function getKoraErrorFix(code) {
|
|
117
|
+
return KORA_ERROR_FIX_SUGGESTIONS[code];
|
|
118
|
+
}
|
|
119
|
+
|
|
88
120
|
// src/errors/errors.ts
|
|
89
121
|
var KoraError = class extends Error {
|
|
90
122
|
constructor(message, code, context) {
|
|
@@ -95,6 +127,16 @@ var KoraError = class extends Error {
|
|
|
95
127
|
}
|
|
96
128
|
code;
|
|
97
129
|
context;
|
|
130
|
+
/**
|
|
131
|
+
* Actionable hint for resolving this error (from registry or error context).
|
|
132
|
+
*/
|
|
133
|
+
get fix() {
|
|
134
|
+
const fromContext = this.context?.fix;
|
|
135
|
+
if (typeof fromContext === "string" && fromContext.length > 0) {
|
|
136
|
+
return fromContext;
|
|
137
|
+
}
|
|
138
|
+
return getKoraErrorFix(this.code);
|
|
139
|
+
}
|
|
98
140
|
};
|
|
99
141
|
var SchemaValidationError = class extends KoraError {
|
|
100
142
|
constructor(message, context) {
|
|
@@ -136,6 +178,14 @@ var StorageError = class extends KoraError {
|
|
|
136
178
|
this.name = "StorageError";
|
|
137
179
|
}
|
|
138
180
|
};
|
|
181
|
+
var AppNotReadyError = class extends KoraError {
|
|
182
|
+
constructor(detail) {
|
|
183
|
+
super(detail, "APP_NOT_READY", {
|
|
184
|
+
fix: "Await app.ready, or wrap your UI in <KoraProvider app={app}> before calling useQuery()."
|
|
185
|
+
});
|
|
186
|
+
this.name = "AppNotReadyError";
|
|
187
|
+
}
|
|
188
|
+
};
|
|
139
189
|
var ClockDriftError = class extends KoraError {
|
|
140
190
|
constructor(currentHlcTime, physicalTime) {
|
|
141
191
|
const driftSeconds = Math.round((currentHlcTime - physicalTime) / 1e3);
|
|
@@ -192,6 +242,7 @@ var HybridLogicalClock = class {
|
|
|
192
242
|
*/
|
|
193
243
|
receive(remote) {
|
|
194
244
|
const physicalTime = this.timeSource.now();
|
|
245
|
+
const wasColdStart = this.wallTime === 0;
|
|
195
246
|
if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
|
|
196
247
|
this.wallTime = physicalTime;
|
|
197
248
|
this.logical = 0;
|
|
@@ -203,7 +254,9 @@ var HybridLogicalClock = class {
|
|
|
203
254
|
} else {
|
|
204
255
|
this.logical++;
|
|
205
256
|
}
|
|
206
|
-
|
|
257
|
+
if (!wasColdStart) {
|
|
258
|
+
this.checkDrift(physicalTime);
|
|
259
|
+
}
|
|
207
260
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
208
261
|
}
|
|
209
262
|
/**
|
|
@@ -252,6 +305,50 @@ var HybridLogicalClock = class {
|
|
|
252
305
|
}
|
|
253
306
|
};
|
|
254
307
|
|
|
308
|
+
// src/causal/causal-tracker.ts
|
|
309
|
+
var CausalTracker = class {
|
|
310
|
+
lastOpIdByCollection = /* @__PURE__ */ new Map();
|
|
311
|
+
lastTransactionOpId = null;
|
|
312
|
+
/**
|
|
313
|
+
* Start a new transaction boundary. Clears in-transaction op ids.
|
|
314
|
+
*/
|
|
315
|
+
beginTransaction() {
|
|
316
|
+
this.lastTransactionOpId = null;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Clear the transaction boundary without recording ops (after rollback).
|
|
320
|
+
*/
|
|
321
|
+
clearTransaction() {
|
|
322
|
+
this.lastTransactionOpId = null;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Compute causal dependencies for the next operation in a collection.
|
|
326
|
+
* Uses direct parents only: collection head and the previous op in the open transaction.
|
|
327
|
+
*/
|
|
328
|
+
nextCausalDeps(collection, inTransaction) {
|
|
329
|
+
const deps = [];
|
|
330
|
+
const lastInCollection = this.lastOpIdByCollection.get(collection);
|
|
331
|
+
if (lastInCollection !== void 0) {
|
|
332
|
+
deps.push(lastInCollection);
|
|
333
|
+
}
|
|
334
|
+
if (inTransaction && this.lastTransactionOpId !== null) {
|
|
335
|
+
if (!deps.includes(this.lastTransactionOpId)) {
|
|
336
|
+
deps.push(this.lastTransactionOpId);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return deps;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Record an operation after it has been created and assigned an id.
|
|
343
|
+
*/
|
|
344
|
+
afterOperation(collection, operationId, inTransaction) {
|
|
345
|
+
this.lastOpIdByCollection.set(collection, operationId);
|
|
346
|
+
if (inTransaction) {
|
|
347
|
+
this.lastTransactionOpId = operationId;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
255
352
|
// src/identifiers/uuid-v7.ts
|
|
256
353
|
var defaultRandom = globalThis.crypto;
|
|
257
354
|
function generateUUIDv7(timestamp = Date.now(), randomSource = defaultRandom) {
|
|
@@ -288,6 +385,37 @@ function formatUUID(bytes) {
|
|
|
288
385
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
289
386
|
}
|
|
290
387
|
|
|
388
|
+
// src/operations/apply-result.ts
|
|
389
|
+
var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
|
|
390
|
+
var APPLY_FAILURE_CODES = {
|
|
391
|
+
APPLY_FAILED: "APPLY_FAILED",
|
|
392
|
+
APPLY_SKIPPED: "APPLY_SKIPPED",
|
|
393
|
+
APPLY_REJECTED: "APPLY_REJECTED",
|
|
394
|
+
APPLY_DEFERRED: "APPLY_DEFERRED",
|
|
395
|
+
CLOCK_DRIFT: "CLOCK_DRIFT",
|
|
396
|
+
REFERENTIAL_INTEGRITY: "REFERENTIAL_INTEGRITY",
|
|
397
|
+
SCHEMA_MISMATCH: "SCHEMA_MISMATCH"
|
|
398
|
+
};
|
|
399
|
+
function isApplyFailure(result) {
|
|
400
|
+
return result === "skipped" || result === "rejected" || result === "deferred";
|
|
401
|
+
}
|
|
402
|
+
function defaultApplyFailureReason(result, overrides) {
|
|
403
|
+
const base = result === "deferred" ? {
|
|
404
|
+
code: APPLY_FAILURE_CODES.APPLY_DEFERRED,
|
|
405
|
+
message: "Operation apply was deferred.",
|
|
406
|
+
retriable: true
|
|
407
|
+
} : result === "rejected" ? {
|
|
408
|
+
code: APPLY_FAILURE_CODES.APPLY_REJECTED,
|
|
409
|
+
message: "Operation apply was rejected.",
|
|
410
|
+
retriable: false
|
|
411
|
+
} : {
|
|
412
|
+
code: APPLY_FAILURE_CODES.APPLY_SKIPPED,
|
|
413
|
+
message: "Operation apply was skipped.",
|
|
414
|
+
retriable: false
|
|
415
|
+
};
|
|
416
|
+
return { ...base, ...overrides };
|
|
417
|
+
}
|
|
418
|
+
|
|
291
419
|
// src/operations/content-hash.ts
|
|
292
420
|
async function computeOperationId(input, timestamp) {
|
|
293
421
|
const hashInput = {
|
|
@@ -307,8 +435,11 @@ async function computeOperationId(input, timestamp) {
|
|
|
307
435
|
return bufferToHex(hashBuffer);
|
|
308
436
|
}
|
|
309
437
|
function canonicalize(obj) {
|
|
310
|
-
if (obj === null
|
|
311
|
-
return
|
|
438
|
+
if (obj === null) {
|
|
439
|
+
return "null";
|
|
440
|
+
}
|
|
441
|
+
if (obj === void 0) {
|
|
442
|
+
return "null";
|
|
312
443
|
}
|
|
313
444
|
if (typeof obj !== "object") {
|
|
314
445
|
return JSON.stringify(obj);
|
|
@@ -320,7 +451,7 @@ function canonicalize(obj) {
|
|
|
320
451
|
const keys = Object.keys(obj).sort();
|
|
321
452
|
const pairs = keys.map((key) => {
|
|
322
453
|
const value = obj[key];
|
|
323
|
-
return `${JSON.stringify(key)}:${canonicalize(value)}`;
|
|
454
|
+
return `${JSON.stringify(key)}:${canonicalize(value === void 0 ? null : value)}`;
|
|
324
455
|
});
|
|
325
456
|
return `{${pairs.join(",")}}`;
|
|
326
457
|
}
|
|
@@ -441,6 +572,7 @@ function isValidOperation(value) {
|
|
|
441
572
|
}
|
|
442
573
|
function deepFreeze(obj) {
|
|
443
574
|
if (typeof obj !== "object" || obj === null) return obj;
|
|
575
|
+
if (ArrayBuffer.isView(obj)) return obj;
|
|
444
576
|
Object.freeze(obj);
|
|
445
577
|
for (const value of Object.values(obj)) {
|
|
446
578
|
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
@@ -548,6 +680,69 @@ function toAtomicOp(sentinel) {
|
|
|
548
680
|
return { type: sentinel.type, value: sentinel.value };
|
|
549
681
|
}
|
|
550
682
|
|
|
683
|
+
// src/scopes/sync-scope-bindings.ts
|
|
684
|
+
function hasSchemaSyncRules(schema) {
|
|
685
|
+
return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
|
|
686
|
+
}
|
|
687
|
+
function getCollectionScopeBindings(schema, collectionName) {
|
|
688
|
+
const syncRule = schema.sync?.[collectionName];
|
|
689
|
+
if (syncRule && Object.keys(syncRule.where).length > 0) {
|
|
690
|
+
return { ...syncRule.where };
|
|
691
|
+
}
|
|
692
|
+
const scopeFields = schema.collections[collectionName]?.scope ?? [];
|
|
693
|
+
if (scopeFields.length === 0) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
const bindings = {};
|
|
697
|
+
for (const field of scopeFields) {
|
|
698
|
+
bindings[field] = field;
|
|
699
|
+
}
|
|
700
|
+
return bindings;
|
|
701
|
+
}
|
|
702
|
+
function isCollectionSyncScoped(schema, collectionName) {
|
|
703
|
+
if (hasSchemaSyncRules(schema)) {
|
|
704
|
+
if (schema.sync?.[collectionName]) {
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
const legacyScope = schema.collections[collectionName]?.scope ?? [];
|
|
708
|
+
return legacyScope.length > 0;
|
|
709
|
+
}
|
|
710
|
+
return getCollectionScopeBindings(schema, collectionName) !== null;
|
|
711
|
+
}
|
|
712
|
+
function collectSchemaScopeValueKeys(schema) {
|
|
713
|
+
const keys = /* @__PURE__ */ new Set();
|
|
714
|
+
for (const collection of Object.values(schema.collections)) {
|
|
715
|
+
for (const field of collection.scope) {
|
|
716
|
+
keys.add(field);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (schema.sync) {
|
|
720
|
+
for (const rule of Object.values(schema.sync)) {
|
|
721
|
+
for (const scopeKey of Object.values(rule.where)) {
|
|
722
|
+
keys.add(scopeKey);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return [...keys];
|
|
727
|
+
}
|
|
728
|
+
function normalizeSyncRuleWhere(collectionName, where) {
|
|
729
|
+
const normalized = {};
|
|
730
|
+
for (const [field, scopeKey] of Object.entries(where)) {
|
|
731
|
+
if (scopeKey === true) {
|
|
732
|
+
normalized[field] = field;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (typeof scopeKey === "string" && scopeKey.length > 0) {
|
|
736
|
+
normalized[field] = scopeKey;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
throw new Error(
|
|
740
|
+
`Invalid sync rule for collection "${collectionName}": where.${field} must be true or a non-empty scope key string`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
return normalized;
|
|
744
|
+
}
|
|
745
|
+
|
|
551
746
|
// src/state-machine/state-machine.ts
|
|
552
747
|
function validateTransition(constraint, fromValue, toValue) {
|
|
553
748
|
const from = String(fromValue);
|
|
@@ -637,7 +832,7 @@ function validateStateMachineDefinition(collectionName, sm, fields) {
|
|
|
637
832
|
// src/schema/define.ts
|
|
638
833
|
var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
639
834
|
var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
640
|
-
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_deleted"]);
|
|
835
|
+
var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
|
|
641
836
|
function defineSchema(input) {
|
|
642
837
|
validateVersion(input.version);
|
|
643
838
|
const collections = {};
|
|
@@ -680,7 +875,15 @@ function defineSchema(input) {
|
|
|
680
875
|
migrations[version] = migration;
|
|
681
876
|
}
|
|
682
877
|
}
|
|
683
|
-
|
|
878
|
+
const sync = buildSyncRules(input.sync, collections);
|
|
879
|
+
applySyncRulesToCollectionScope(collections, sync);
|
|
880
|
+
return {
|
|
881
|
+
version: input.version,
|
|
882
|
+
collections,
|
|
883
|
+
relations,
|
|
884
|
+
migrations,
|
|
885
|
+
...sync ? { sync } : {}
|
|
886
|
+
};
|
|
684
887
|
}
|
|
685
888
|
function validateVersion(version) {
|
|
686
889
|
if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
|
|
@@ -761,6 +964,63 @@ function buildCollection(name, input) {
|
|
|
761
964
|
}
|
|
762
965
|
return { fields, indexes, constraints, resolvers, scope, stateMachine };
|
|
763
966
|
}
|
|
967
|
+
function buildSyncRules(syncInput, collections) {
|
|
968
|
+
if (!syncInput) {
|
|
969
|
+
return void 0;
|
|
970
|
+
}
|
|
971
|
+
const sync = {};
|
|
972
|
+
for (const [collectionName, ruleInput] of Object.entries(syncInput)) {
|
|
973
|
+
if (!(collectionName in collections)) {
|
|
974
|
+
throw new SchemaValidationError(
|
|
975
|
+
`Sync rule references collection "${collectionName}" which does not exist. Available collections: ${Object.keys(collections).join(", ")}`,
|
|
976
|
+
{ collection: collectionName }
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
if (!ruleInput.where || Object.keys(ruleInput.where).length === 0) {
|
|
980
|
+
throw new SchemaValidationError(
|
|
981
|
+
`Sync rule for collection "${collectionName}" must declare at least one where binding`,
|
|
982
|
+
{ collection: collectionName }
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const collection = collections[collectionName];
|
|
986
|
+
if (!collection) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
for (const fieldName of Object.keys(ruleInput.where)) {
|
|
990
|
+
if (!(fieldName in collection.fields)) {
|
|
991
|
+
throw new SchemaValidationError(
|
|
992
|
+
`Sync rule where field "${fieldName}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(collection.fields).join(", ")}`,
|
|
993
|
+
{ collection: collectionName, field: fieldName }
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
sync[collectionName] = { where: normalizeSyncRuleWhere(collectionName, ruleInput.where) };
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
throw new SchemaValidationError(
|
|
1001
|
+
error instanceof Error ? error.message : "Invalid sync rule",
|
|
1002
|
+
{ collection: collectionName }
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return sync;
|
|
1007
|
+
}
|
|
1008
|
+
function applySyncRulesToCollectionScope(collections, sync) {
|
|
1009
|
+
if (!sync) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
for (const [collectionName, rule] of Object.entries(sync)) {
|
|
1013
|
+
const collection = collections[collectionName];
|
|
1014
|
+
if (!collection) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
for (const fieldName of Object.keys(rule.where)) {
|
|
1018
|
+
if (!collection.scope.includes(fieldName)) {
|
|
1019
|
+
collection.scope.push(fieldName);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
764
1024
|
function validateFieldName(collection, fieldName) {
|
|
765
1025
|
if (RESERVED_FIELDS.has(fieldName)) {
|
|
766
1026
|
throw new SchemaValidationError(
|
|
@@ -848,6 +1108,7 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
848
1108
|
}
|
|
849
1109
|
columns.push("_created_at INTEGER NOT NULL");
|
|
850
1110
|
columns.push("_updated_at INTEGER NOT NULL");
|
|
1111
|
+
columns.push("_version TEXT NOT NULL DEFAULT ''");
|
|
851
1112
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
852
1113
|
statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
|
|
853
1114
|
${columns.join(",\n ")}
|
|
@@ -857,6 +1118,10 @@ function generateSQL(collectionName, collection, relations) {
|
|
|
857
1118
|
statements.push(`--kora:safe-alter
|
|
858
1119
|
ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
|
|
859
1120
|
}
|
|
1121
|
+
statements.push(
|
|
1122
|
+
`--kora:safe-alter
|
|
1123
|
+
ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
|
|
1124
|
+
);
|
|
860
1125
|
for (const indexField of collection.indexes) {
|
|
861
1126
|
statements.push(
|
|
862
1127
|
`CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
|
|
@@ -896,6 +1161,18 @@ function generateFullDDL(schema) {
|
|
|
896
1161
|
statements.push(
|
|
897
1162
|
"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)"
|
|
898
1163
|
);
|
|
1164
|
+
statements.push(
|
|
1165
|
+
"CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
|
|
1166
|
+
);
|
|
1167
|
+
statements.push(
|
|
1168
|
+
"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)"
|
|
1169
|
+
);
|
|
1170
|
+
statements.push(
|
|
1171
|
+
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
|
|
1172
|
+
);
|
|
1173
|
+
statements.push(
|
|
1174
|
+
"CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
|
|
1175
|
+
);
|
|
899
1176
|
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
900
1177
|
statements.push(...generateSQL(name, collection, schema.relations));
|
|
901
1178
|
}
|
|
@@ -1024,10 +1301,24 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
|
|
|
1024
1301
|
);
|
|
1025
1302
|
}
|
|
1026
1303
|
default(value) {
|
|
1027
|
-
return new _EnumFieldBuilder(
|
|
1304
|
+
return new _EnumFieldBuilder(
|
|
1305
|
+
this._enumValues,
|
|
1306
|
+
false,
|
|
1307
|
+
value,
|
|
1308
|
+
this._auto,
|
|
1309
|
+
this._mergeStrategy,
|
|
1310
|
+
this._transitions
|
|
1311
|
+
);
|
|
1028
1312
|
}
|
|
1029
1313
|
auto() {
|
|
1030
|
-
return new _EnumFieldBuilder(
|
|
1314
|
+
return new _EnumFieldBuilder(
|
|
1315
|
+
this._enumValues,
|
|
1316
|
+
false,
|
|
1317
|
+
void 0,
|
|
1318
|
+
true,
|
|
1319
|
+
this._mergeStrategy,
|
|
1320
|
+
this._transitions
|
|
1321
|
+
);
|
|
1031
1322
|
}
|
|
1032
1323
|
merge(strategy) {
|
|
1033
1324
|
return new _EnumFieldBuilder(
|
|
@@ -1513,12 +1804,13 @@ function vectorsEqual(a, b) {
|
|
|
1513
1804
|
}
|
|
1514
1805
|
return true;
|
|
1515
1806
|
}
|
|
1516
|
-
function computeDelta(localVector, remoteVector, operationLog) {
|
|
1807
|
+
async function computeDelta(localVector, remoteVector, operationLog) {
|
|
1517
1808
|
const missing = [];
|
|
1518
1809
|
for (const [nodeId, localSeq] of localVector) {
|
|
1519
1810
|
const remoteSeq = remoteVector.get(nodeId) ?? 0;
|
|
1520
1811
|
if (localSeq > remoteSeq) {
|
|
1521
|
-
|
|
1812
|
+
const ops = await operationLog.getRange(nodeId, remoteSeq + 1, localSeq);
|
|
1813
|
+
missing.push(...ops);
|
|
1522
1814
|
}
|
|
1523
1815
|
}
|
|
1524
1816
|
return topologicalSort(missing);
|
|
@@ -1535,12 +1827,17 @@ function deserializeVector(s) {
|
|
|
1535
1827
|
// src/scopes/build-scope-map.ts
|
|
1536
1828
|
function buildScopeMap(schema, scopeValues) {
|
|
1537
1829
|
const result = {};
|
|
1538
|
-
|
|
1539
|
-
|
|
1830
|
+
const partialSync = hasSchemaSyncRules(schema);
|
|
1831
|
+
for (const collName of Object.keys(schema.collections)) {
|
|
1832
|
+
if (partialSync && !isCollectionSyncScoped(schema, collName)) {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
const bindings = getCollectionScopeBindings(schema, collName);
|
|
1836
|
+
if (bindings) {
|
|
1540
1837
|
const collScope = {};
|
|
1541
|
-
for (const field of
|
|
1542
|
-
if (
|
|
1543
|
-
collScope[field] = scopeValues[
|
|
1838
|
+
for (const [field, scopeKey] of Object.entries(bindings)) {
|
|
1839
|
+
if (scopeKey in scopeValues) {
|
|
1840
|
+
collScope[field] = scopeValues[scopeKey];
|
|
1544
1841
|
}
|
|
1545
1842
|
}
|
|
1546
1843
|
result[collName] = collScope;
|
|
@@ -1551,6 +1848,34 @@ function buildScopeMap(schema, scopeValues) {
|
|
|
1551
1848
|
return result;
|
|
1552
1849
|
}
|
|
1553
1850
|
|
|
1851
|
+
// src/scopes/extract-scope-from-claims.ts
|
|
1852
|
+
function collectSchemaScopeFields(schema) {
|
|
1853
|
+
return collectSchemaScopeValueKeys(schema);
|
|
1854
|
+
}
|
|
1855
|
+
function extractScopeValuesFromClaims(schema, claims) {
|
|
1856
|
+
const scopeFields = collectSchemaScopeValueKeys(schema);
|
|
1857
|
+
if (scopeFields.length === 0) {
|
|
1858
|
+
return {};
|
|
1859
|
+
}
|
|
1860
|
+
const nestedScope = claims.scope;
|
|
1861
|
+
const scopeObject = typeof nestedScope === "object" && nestedScope !== null && !Array.isArray(nestedScope) ? nestedScope : {};
|
|
1862
|
+
const result = {};
|
|
1863
|
+
for (const field of scopeFields) {
|
|
1864
|
+
if (field in claims) {
|
|
1865
|
+
result[field] = claims[field];
|
|
1866
|
+
continue;
|
|
1867
|
+
}
|
|
1868
|
+
if (field in scopeObject) {
|
|
1869
|
+
result[field] = scopeObject[field];
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (field === "userId" && typeof claims.sub === "string") {
|
|
1873
|
+
result.userId = claims.sub;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return result;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1554
1879
|
// src/sequences/sequence-format.ts
|
|
1555
1880
|
function formatSequenceValue(template, counter, nodeId, now) {
|
|
1556
1881
|
const date = now ?? /* @__PURE__ */ new Date();
|
|
@@ -1577,6 +1902,29 @@ function defaultSequenceFormat(name) {
|
|
|
1577
1902
|
return `${name}-{seq:4}`;
|
|
1578
1903
|
}
|
|
1579
1904
|
|
|
1905
|
+
// src/migration/apply-operation-transforms.ts
|
|
1906
|
+
var MAX_TRANSFORM_STEPS = 32;
|
|
1907
|
+
function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
|
|
1908
|
+
let current = operation;
|
|
1909
|
+
for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
|
|
1910
|
+
if (current.schemaVersion === targetSchemaVersion) {
|
|
1911
|
+
return current;
|
|
1912
|
+
}
|
|
1913
|
+
const transform = transforms.find(
|
|
1914
|
+
(candidate) => candidate.fromVersion === current.schemaVersion
|
|
1915
|
+
);
|
|
1916
|
+
if (!transform) {
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
const next = transform.transform(current);
|
|
1920
|
+
if (next === null) {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
current = next;
|
|
1924
|
+
}
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1580
1928
|
// src/migrations/migration-builder.ts
|
|
1581
1929
|
var RollbackBuilder = class {
|
|
1582
1930
|
_steps = [];
|
|
@@ -2121,12 +2469,17 @@ function generateProtoDefinitions(schema) {
|
|
|
2121
2469
|
}
|
|
2122
2470
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2123
2471
|
0 && (module.exports = {
|
|
2472
|
+
APPLY_FAILURE_CODES,
|
|
2473
|
+
APPLY_RESULTS,
|
|
2474
|
+
AppNotReadyError,
|
|
2124
2475
|
ArrayFieldBuilder,
|
|
2125
2476
|
CONNECTION_QUALITIES,
|
|
2477
|
+
CausalTracker,
|
|
2126
2478
|
ClockDriftError,
|
|
2127
2479
|
EnumFieldBuilder,
|
|
2128
2480
|
FieldBuilder,
|
|
2129
2481
|
HybridLogicalClock,
|
|
2482
|
+
KORA_ERROR_FIX_SUGGESTIONS,
|
|
2130
2483
|
KoraError,
|
|
2131
2484
|
MERGE_STRATEGIES,
|
|
2132
2485
|
MergeConflictError,
|
|
@@ -2138,17 +2491,22 @@ function generateProtoDefinitions(schema) {
|
|
|
2138
2491
|
StorageError,
|
|
2139
2492
|
SyncError,
|
|
2140
2493
|
advanceVector,
|
|
2494
|
+
applyOperationTransforms,
|
|
2141
2495
|
buildScopeMap,
|
|
2142
2496
|
buildStateMachineConstraints,
|
|
2143
2497
|
canAutoRollback,
|
|
2498
|
+
collectSchemaScopeFields,
|
|
2499
|
+
collectSchemaScopeValueKeys,
|
|
2144
2500
|
computeDelta,
|
|
2145
2501
|
createOperation,
|
|
2146
2502
|
createReversibleMigration,
|
|
2147
2503
|
createVersionVector,
|
|
2504
|
+
defaultApplyFailureReason,
|
|
2148
2505
|
defaultSequenceFormat,
|
|
2149
2506
|
defineSchema,
|
|
2150
2507
|
deserializeVector,
|
|
2151
2508
|
dominates,
|
|
2509
|
+
extractScopeValuesFromClaims,
|
|
2152
2510
|
extractTimestamp,
|
|
2153
2511
|
formatSequenceValue,
|
|
2154
2512
|
generateFullDDL,
|
|
@@ -2156,8 +2514,13 @@ function generateProtoDefinitions(schema) {
|
|
|
2156
2514
|
generateRollbackSteps,
|
|
2157
2515
|
generateSQL,
|
|
2158
2516
|
generateUUIDv7,
|
|
2517
|
+
getCollectionScopeBindings,
|
|
2518
|
+
getKoraErrorFix,
|
|
2159
2519
|
getTransitionMap,
|
|
2520
|
+
hasSchemaSyncRules,
|
|
2521
|
+
isApplyFailure,
|
|
2160
2522
|
isAtomicOp,
|
|
2523
|
+
isCollectionSyncScoped,
|
|
2161
2524
|
isValidOperation,
|
|
2162
2525
|
isValidUUIDv7,
|
|
2163
2526
|
mergeVectors,
|