@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/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
+ AppNotReadyError,
2
3
  ClockDriftError,
3
4
  HybridLogicalClock,
5
+ KORA_ERROR_FIX_SUGGESTIONS,
4
6
  KoraError,
5
7
  MergeConflictError,
6
8
  OperationError,
@@ -8,10 +10,11 @@ import {
8
10
  StorageError,
9
11
  SyncError,
10
12
  createOperation,
13
+ getKoraErrorFix,
11
14
  isValidOperation,
12
15
  topologicalSort,
13
16
  verifyOperationIntegrity
14
- } from "./chunk-CFP5YFXC.js";
17
+ } from "./chunk-3VIOZT7D.js";
15
18
 
16
19
  // src/types.ts
17
20
  var MERGE_STRATEGIES = [
@@ -23,6 +26,50 @@ var MERGE_STRATEGIES = [
23
26
  ];
24
27
  var CONNECTION_QUALITIES = ["excellent", "good", "fair", "poor", "offline"];
25
28
 
29
+ // src/causal/causal-tracker.ts
30
+ var CausalTracker = class {
31
+ lastOpIdByCollection = /* @__PURE__ */ new Map();
32
+ lastTransactionOpId = null;
33
+ /**
34
+ * Start a new transaction boundary. Clears in-transaction op ids.
35
+ */
36
+ beginTransaction() {
37
+ this.lastTransactionOpId = null;
38
+ }
39
+ /**
40
+ * Clear the transaction boundary without recording ops (after rollback).
41
+ */
42
+ clearTransaction() {
43
+ this.lastTransactionOpId = null;
44
+ }
45
+ /**
46
+ * Compute causal dependencies for the next operation in a collection.
47
+ * Uses direct parents only: collection head and the previous op in the open transaction.
48
+ */
49
+ nextCausalDeps(collection, inTransaction) {
50
+ const deps = [];
51
+ const lastInCollection = this.lastOpIdByCollection.get(collection);
52
+ if (lastInCollection !== void 0) {
53
+ deps.push(lastInCollection);
54
+ }
55
+ if (inTransaction && this.lastTransactionOpId !== null) {
56
+ if (!deps.includes(this.lastTransactionOpId)) {
57
+ deps.push(this.lastTransactionOpId);
58
+ }
59
+ }
60
+ return deps;
61
+ }
62
+ /**
63
+ * Record an operation after it has been created and assigned an id.
64
+ */
65
+ afterOperation(collection, operationId, inTransaction) {
66
+ this.lastOpIdByCollection.set(collection, operationId);
67
+ if (inTransaction) {
68
+ this.lastTransactionOpId = operationId;
69
+ }
70
+ }
71
+ };
72
+
26
73
  // src/identifiers/uuid-v7.ts
27
74
  var defaultRandom = globalThis.crypto;
28
75
  function generateUUIDv7(timestamp = Date.now(), randomSource = defaultRandom) {
@@ -59,6 +106,37 @@ function formatUUID(bytes) {
59
106
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
60
107
  }
61
108
 
109
+ // src/operations/apply-result.ts
110
+ var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
111
+ var APPLY_FAILURE_CODES = {
112
+ APPLY_FAILED: "APPLY_FAILED",
113
+ APPLY_SKIPPED: "APPLY_SKIPPED",
114
+ APPLY_REJECTED: "APPLY_REJECTED",
115
+ APPLY_DEFERRED: "APPLY_DEFERRED",
116
+ CLOCK_DRIFT: "CLOCK_DRIFT",
117
+ REFERENTIAL_INTEGRITY: "REFERENTIAL_INTEGRITY",
118
+ SCHEMA_MISMATCH: "SCHEMA_MISMATCH"
119
+ };
120
+ function isApplyFailure(result) {
121
+ return result === "skipped" || result === "rejected" || result === "deferred";
122
+ }
123
+ function defaultApplyFailureReason(result, overrides) {
124
+ const base = result === "deferred" ? {
125
+ code: APPLY_FAILURE_CODES.APPLY_DEFERRED,
126
+ message: "Operation apply was deferred.",
127
+ retriable: true
128
+ } : result === "rejected" ? {
129
+ code: APPLY_FAILURE_CODES.APPLY_REJECTED,
130
+ message: "Operation apply was rejected.",
131
+ retriable: false
132
+ } : {
133
+ code: APPLY_FAILURE_CODES.APPLY_SKIPPED,
134
+ message: "Operation apply was skipped.",
135
+ retriable: false
136
+ };
137
+ return { ...base, ...overrides };
138
+ }
139
+
62
140
  // src/operations/atomic-ops.ts
63
141
  var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
64
142
  function isAtomicOp(value) {
@@ -157,6 +235,69 @@ function toAtomicOp(sentinel) {
157
235
  return { type: sentinel.type, value: sentinel.value };
158
236
  }
159
237
 
238
+ // src/scopes/sync-scope-bindings.ts
239
+ function hasSchemaSyncRules(schema) {
240
+ return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
241
+ }
242
+ function getCollectionScopeBindings(schema, collectionName) {
243
+ const syncRule = schema.sync?.[collectionName];
244
+ if (syncRule && Object.keys(syncRule.where).length > 0) {
245
+ return { ...syncRule.where };
246
+ }
247
+ const scopeFields = schema.collections[collectionName]?.scope ?? [];
248
+ if (scopeFields.length === 0) {
249
+ return null;
250
+ }
251
+ const bindings = {};
252
+ for (const field of scopeFields) {
253
+ bindings[field] = field;
254
+ }
255
+ return bindings;
256
+ }
257
+ function isCollectionSyncScoped(schema, collectionName) {
258
+ if (hasSchemaSyncRules(schema)) {
259
+ if (schema.sync?.[collectionName]) {
260
+ return true;
261
+ }
262
+ const legacyScope = schema.collections[collectionName]?.scope ?? [];
263
+ return legacyScope.length > 0;
264
+ }
265
+ return getCollectionScopeBindings(schema, collectionName) !== null;
266
+ }
267
+ function collectSchemaScopeValueKeys(schema) {
268
+ const keys = /* @__PURE__ */ new Set();
269
+ for (const collection of Object.values(schema.collections)) {
270
+ for (const field of collection.scope) {
271
+ keys.add(field);
272
+ }
273
+ }
274
+ if (schema.sync) {
275
+ for (const rule of Object.values(schema.sync)) {
276
+ for (const scopeKey of Object.values(rule.where)) {
277
+ keys.add(scopeKey);
278
+ }
279
+ }
280
+ }
281
+ return [...keys];
282
+ }
283
+ function normalizeSyncRuleWhere(collectionName, where) {
284
+ const normalized = {};
285
+ for (const [field, scopeKey] of Object.entries(where)) {
286
+ if (scopeKey === true) {
287
+ normalized[field] = field;
288
+ continue;
289
+ }
290
+ if (typeof scopeKey === "string" && scopeKey.length > 0) {
291
+ normalized[field] = scopeKey;
292
+ continue;
293
+ }
294
+ throw new Error(
295
+ `Invalid sync rule for collection "${collectionName}": where.${field} must be true or a non-empty scope key string`
296
+ );
297
+ }
298
+ return normalized;
299
+ }
300
+
160
301
  // src/state-machine/state-machine.ts
161
302
  function validateTransition(constraint, fromValue, toValue) {
162
303
  const from = String(fromValue);
@@ -246,7 +387,7 @@ function validateStateMachineDefinition(collectionName, sm, fields) {
246
387
  // src/schema/define.ts
247
388
  var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
248
389
  var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
249
- var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_deleted"]);
390
+ var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
250
391
  function defineSchema(input) {
251
392
  validateVersion(input.version);
252
393
  const collections = {};
@@ -289,7 +430,15 @@ function defineSchema(input) {
289
430
  migrations[version] = migration;
290
431
  }
291
432
  }
292
- return { version: input.version, collections, relations, migrations };
433
+ const sync = buildSyncRules(input.sync, collections);
434
+ applySyncRulesToCollectionScope(collections, sync);
435
+ return {
436
+ version: input.version,
437
+ collections,
438
+ relations,
439
+ migrations,
440
+ ...sync ? { sync } : {}
441
+ };
293
442
  }
294
443
  function validateVersion(version) {
295
444
  if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
@@ -370,6 +519,63 @@ function buildCollection(name, input) {
370
519
  }
371
520
  return { fields, indexes, constraints, resolvers, scope, stateMachine };
372
521
  }
522
+ function buildSyncRules(syncInput, collections) {
523
+ if (!syncInput) {
524
+ return void 0;
525
+ }
526
+ const sync = {};
527
+ for (const [collectionName, ruleInput] of Object.entries(syncInput)) {
528
+ if (!(collectionName in collections)) {
529
+ throw new SchemaValidationError(
530
+ `Sync rule references collection "${collectionName}" which does not exist. Available collections: ${Object.keys(collections).join(", ")}`,
531
+ { collection: collectionName }
532
+ );
533
+ }
534
+ if (!ruleInput.where || Object.keys(ruleInput.where).length === 0) {
535
+ throw new SchemaValidationError(
536
+ `Sync rule for collection "${collectionName}" must declare at least one where binding`,
537
+ { collection: collectionName }
538
+ );
539
+ }
540
+ const collection = collections[collectionName];
541
+ if (!collection) {
542
+ continue;
543
+ }
544
+ for (const fieldName of Object.keys(ruleInput.where)) {
545
+ if (!(fieldName in collection.fields)) {
546
+ throw new SchemaValidationError(
547
+ `Sync rule where field "${fieldName}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(collection.fields).join(", ")}`,
548
+ { collection: collectionName, field: fieldName }
549
+ );
550
+ }
551
+ }
552
+ try {
553
+ sync[collectionName] = { where: normalizeSyncRuleWhere(collectionName, ruleInput.where) };
554
+ } catch (error) {
555
+ throw new SchemaValidationError(
556
+ error instanceof Error ? error.message : "Invalid sync rule",
557
+ { collection: collectionName }
558
+ );
559
+ }
560
+ }
561
+ return sync;
562
+ }
563
+ function applySyncRulesToCollectionScope(collections, sync) {
564
+ if (!sync) {
565
+ return;
566
+ }
567
+ for (const [collectionName, rule] of Object.entries(sync)) {
568
+ const collection = collections[collectionName];
569
+ if (!collection) {
570
+ continue;
571
+ }
572
+ for (const fieldName of Object.keys(rule.where)) {
573
+ if (!collection.scope.includes(fieldName)) {
574
+ collection.scope.push(fieldName);
575
+ }
576
+ }
577
+ }
578
+ }
373
579
  function validateFieldName(collection, fieldName) {
374
580
  if (RESERVED_FIELDS.has(fieldName)) {
375
581
  throw new SchemaValidationError(
@@ -457,6 +663,7 @@ function generateSQL(collectionName, collection, relations) {
457
663
  }
458
664
  columns.push("_created_at INTEGER NOT NULL");
459
665
  columns.push("_updated_at INTEGER NOT NULL");
666
+ columns.push("_version TEXT NOT NULL DEFAULT ''");
460
667
  columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
461
668
  statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
462
669
  ${columns.join(",\n ")}
@@ -466,6 +673,10 @@ function generateSQL(collectionName, collection, relations) {
466
673
  statements.push(`--kora:safe-alter
467
674
  ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
468
675
  }
676
+ statements.push(
677
+ `--kora:safe-alter
678
+ ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
679
+ );
469
680
  for (const indexField of collection.indexes) {
470
681
  statements.push(
471
682
  `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
@@ -505,6 +716,18 @@ function generateFullDDL(schema) {
505
716
  statements.push(
506
717
  "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)"
507
718
  );
719
+ statements.push(
720
+ "CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
721
+ );
722
+ statements.push(
723
+ "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)"
724
+ );
725
+ statements.push(
726
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
727
+ );
728
+ statements.push(
729
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
730
+ );
508
731
  for (const [name, collection] of Object.entries(schema.collections)) {
509
732
  statements.push(...generateSQL(name, collection, schema.relations));
510
733
  }
@@ -633,10 +856,24 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
633
856
  );
634
857
  }
635
858
  default(value) {
636
- return new _EnumFieldBuilder(this._enumValues, false, value, this._auto, this._mergeStrategy, this._transitions);
859
+ return new _EnumFieldBuilder(
860
+ this._enumValues,
861
+ false,
862
+ value,
863
+ this._auto,
864
+ this._mergeStrategy,
865
+ this._transitions
866
+ );
637
867
  }
638
868
  auto() {
639
- return new _EnumFieldBuilder(this._enumValues, false, void 0, true, this._mergeStrategy, this._transitions);
869
+ return new _EnumFieldBuilder(
870
+ this._enumValues,
871
+ false,
872
+ void 0,
873
+ true,
874
+ this._mergeStrategy,
875
+ this._transitions
876
+ );
640
877
  }
641
878
  merge(strategy) {
642
879
  return new _EnumFieldBuilder(
@@ -1004,12 +1241,13 @@ function vectorsEqual(a, b) {
1004
1241
  }
1005
1242
  return true;
1006
1243
  }
1007
- function computeDelta(localVector, remoteVector, operationLog) {
1244
+ async function computeDelta(localVector, remoteVector, operationLog) {
1008
1245
  const missing = [];
1009
1246
  for (const [nodeId, localSeq] of localVector) {
1010
1247
  const remoteSeq = remoteVector.get(nodeId) ?? 0;
1011
1248
  if (localSeq > remoteSeq) {
1012
- missing.push(...operationLog.getRange(nodeId, remoteSeq + 1, localSeq));
1249
+ const ops = await operationLog.getRange(nodeId, remoteSeq + 1, localSeq);
1250
+ missing.push(...ops);
1013
1251
  }
1014
1252
  }
1015
1253
  return topologicalSort(missing);
@@ -1026,12 +1264,17 @@ function deserializeVector(s) {
1026
1264
  // src/scopes/build-scope-map.ts
1027
1265
  function buildScopeMap(schema, scopeValues) {
1028
1266
  const result = {};
1029
- for (const [collName, collDef] of Object.entries(schema.collections)) {
1030
- if (collDef.scope.length > 0) {
1267
+ const partialSync = hasSchemaSyncRules(schema);
1268
+ for (const collName of Object.keys(schema.collections)) {
1269
+ if (partialSync && !isCollectionSyncScoped(schema, collName)) {
1270
+ continue;
1271
+ }
1272
+ const bindings = getCollectionScopeBindings(schema, collName);
1273
+ if (bindings) {
1031
1274
  const collScope = {};
1032
- for (const field of collDef.scope) {
1033
- if (field in scopeValues) {
1034
- collScope[field] = scopeValues[field];
1275
+ for (const [field, scopeKey] of Object.entries(bindings)) {
1276
+ if (scopeKey in scopeValues) {
1277
+ collScope[field] = scopeValues[scopeKey];
1035
1278
  }
1036
1279
  }
1037
1280
  result[collName] = collScope;
@@ -1042,6 +1285,34 @@ function buildScopeMap(schema, scopeValues) {
1042
1285
  return result;
1043
1286
  }
1044
1287
 
1288
+ // src/scopes/extract-scope-from-claims.ts
1289
+ function collectSchemaScopeFields(schema) {
1290
+ return collectSchemaScopeValueKeys(schema);
1291
+ }
1292
+ function extractScopeValuesFromClaims(schema, claims) {
1293
+ const scopeFields = collectSchemaScopeValueKeys(schema);
1294
+ if (scopeFields.length === 0) {
1295
+ return {};
1296
+ }
1297
+ const nestedScope = claims.scope;
1298
+ const scopeObject = typeof nestedScope === "object" && nestedScope !== null && !Array.isArray(nestedScope) ? nestedScope : {};
1299
+ const result = {};
1300
+ for (const field of scopeFields) {
1301
+ if (field in claims) {
1302
+ result[field] = claims[field];
1303
+ continue;
1304
+ }
1305
+ if (field in scopeObject) {
1306
+ result[field] = scopeObject[field];
1307
+ continue;
1308
+ }
1309
+ if (field === "userId" && typeof claims.sub === "string") {
1310
+ result.userId = claims.sub;
1311
+ }
1312
+ }
1313
+ return result;
1314
+ }
1315
+
1045
1316
  // src/sequences/sequence-format.ts
1046
1317
  function formatSequenceValue(template, counter, nodeId, now) {
1047
1318
  const date = now ?? /* @__PURE__ */ new Date();
@@ -1068,6 +1339,29 @@ function defaultSequenceFormat(name) {
1068
1339
  return `${name}-{seq:4}`;
1069
1340
  }
1070
1341
 
1342
+ // src/migration/apply-operation-transforms.ts
1343
+ var MAX_TRANSFORM_STEPS = 32;
1344
+ function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
1345
+ let current = operation;
1346
+ for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
1347
+ if (current.schemaVersion === targetSchemaVersion) {
1348
+ return current;
1349
+ }
1350
+ const transform = transforms.find(
1351
+ (candidate) => candidate.fromVersion === current.schemaVersion
1352
+ );
1353
+ if (!transform) {
1354
+ return null;
1355
+ }
1356
+ const next = transform.transform(current);
1357
+ if (next === null) {
1358
+ return null;
1359
+ }
1360
+ current = next;
1361
+ }
1362
+ return null;
1363
+ }
1364
+
1071
1365
  // src/migrations/migration-builder.ts
1072
1366
  var RollbackBuilder = class {
1073
1367
  _steps = [];
@@ -1611,12 +1905,17 @@ function generateProtoDefinitions(schema) {
1611
1905
  return { proto, typeMap, jsonDescriptor };
1612
1906
  }
1613
1907
  export {
1908
+ APPLY_FAILURE_CODES,
1909
+ APPLY_RESULTS,
1910
+ AppNotReadyError,
1614
1911
  ArrayFieldBuilder,
1615
1912
  CONNECTION_QUALITIES,
1913
+ CausalTracker,
1616
1914
  ClockDriftError,
1617
1915
  EnumFieldBuilder,
1618
1916
  FieldBuilder,
1619
1917
  HybridLogicalClock,
1918
+ KORA_ERROR_FIX_SUGGESTIONS,
1620
1919
  KoraError,
1621
1920
  MERGE_STRATEGIES,
1622
1921
  MergeConflictError,
@@ -1628,17 +1927,22 @@ export {
1628
1927
  StorageError,
1629
1928
  SyncError,
1630
1929
  advanceVector,
1930
+ applyOperationTransforms,
1631
1931
  buildScopeMap,
1632
1932
  buildStateMachineConstraints,
1633
1933
  canAutoRollback,
1934
+ collectSchemaScopeFields,
1935
+ collectSchemaScopeValueKeys,
1634
1936
  computeDelta,
1635
1937
  createOperation,
1636
1938
  createReversibleMigration,
1637
1939
  createVersionVector,
1940
+ defaultApplyFailureReason,
1638
1941
  defaultSequenceFormat,
1639
1942
  defineSchema,
1640
1943
  deserializeVector,
1641
1944
  dominates,
1945
+ extractScopeValuesFromClaims,
1642
1946
  extractTimestamp,
1643
1947
  formatSequenceValue,
1644
1948
  generateFullDDL,
@@ -1646,8 +1950,13 @@ export {
1646
1950
  generateRollbackSteps,
1647
1951
  generateSQL,
1648
1952
  generateUUIDv7,
1953
+ getCollectionScopeBindings,
1954
+ getKoraErrorFix,
1649
1955
  getTransitionMap,
1956
+ hasSchemaSyncRules,
1957
+ isApplyFailure,
1650
1958
  isAtomicOp,
1959
+ isCollectionSyncScoped,
1651
1960
  isValidOperation,
1652
1961
  isValidUUIDv7,
1653
1962
  mergeVectors,