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