@korajs/core 1.0.0-beta.1 → 1.0.0-beta.11

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.cjs CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  StorageError: () => StorageError,
47
47
  SyncError: () => SyncError,
48
48
  advanceVector: () => advanceVector,
49
+ applyAtomicOp: () => applyAtomicOp,
49
50
  applyOperationTransforms: () => applyOperationTransforms,
50
51
  base64ToBytes: () => base64ToBytes,
51
52
  buildScopeMap: () => buildScopeMap,
@@ -94,6 +95,8 @@ __export(index_exports, {
94
95
  migrate: () => migrate,
95
96
  migrationStepsToSQL: () => migrationStepsToSQL,
96
97
  op: () => op,
98
+ quoteIdent: () => quoteIdent,
99
+ replayOperationsForRecord: () => replayOperationsForRecord,
97
100
  resolveAtomicOp: () => resolveAtomicOp,
98
101
  revealSecret: () => revealSecret,
99
102
  rollbackStepsToSQL: () => rollbackStepsToSQL,
@@ -967,32 +970,36 @@ var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
967
970
  function isAtomicOp(value) {
968
971
  return typeof value === "object" && value !== null && KORA_ATOMIC_OP in value && value[KORA_ATOMIC_OP] === true;
969
972
  }
970
- function resolveAtomicOp(currentValue, sentinel) {
971
- switch (sentinel.type) {
973
+ function applyAtomicOp(currentValue, atomicOp) {
974
+ switch (atomicOp.type) {
972
975
  case "increment": {
973
976
  const current = typeof currentValue === "number" ? currentValue : 0;
974
- const operand = sentinel.value;
977
+ const operand = atomicOp.value;
975
978
  return current + operand;
976
979
  }
977
980
  case "max": {
978
981
  const current = typeof currentValue === "number" ? currentValue : Number.NEGATIVE_INFINITY;
979
- return Math.max(current, sentinel.value);
982
+ return Math.max(current, atomicOp.value);
980
983
  }
981
984
  case "min": {
982
985
  const current = typeof currentValue === "number" ? currentValue : Number.POSITIVE_INFINITY;
983
- return Math.min(current, sentinel.value);
986
+ return Math.min(current, atomicOp.value);
984
987
  }
985
988
  case "append": {
986
989
  const current = Array.isArray(currentValue) ? [...currentValue] : [];
987
- current.push(sentinel.value);
990
+ current.push(atomicOp.value);
988
991
  return current;
989
992
  }
990
993
  case "remove": {
991
994
  if (!Array.isArray(currentValue)) return [];
992
- return currentValue.filter((item) => item !== sentinel.value);
995
+ const target = JSON.stringify(atomicOp.value);
996
+ return currentValue.filter((item) => JSON.stringify(item) !== target);
993
997
  }
994
998
  }
995
999
  }
1000
+ function resolveAtomicOp(currentValue, sentinel) {
1001
+ return applyAtomicOp(currentValue, { type: sentinel.type, value: sentinel.value });
1002
+ }
996
1003
  function createSentinel(type, value) {
997
1004
  return Object.freeze({
998
1005
  [KORA_ATOMIC_OP]: true,
@@ -1060,6 +1067,46 @@ function toAtomicOp(sentinel) {
1060
1067
  return { type: sentinel.type, value: sentinel.value };
1061
1068
  }
1062
1069
 
1070
+ // src/operations/replay-record.ts
1071
+ function replayOperationsForRecord(ops) {
1072
+ let record = null;
1073
+ let deleted = false;
1074
+ const lastWriterKind = {};
1075
+ for (const op2 of ops) {
1076
+ switch (op2.type) {
1077
+ case "insert":
1078
+ if (op2.data) {
1079
+ record = { ...op2.data };
1080
+ deleted = false;
1081
+ for (const field of Object.keys(op2.data)) {
1082
+ lastWriterKind[field] = "lww";
1083
+ }
1084
+ }
1085
+ break;
1086
+ case "update":
1087
+ if (op2.data) {
1088
+ const next = { ...record ?? {} };
1089
+ for (const [field, value] of Object.entries(op2.data)) {
1090
+ const atomicOp = op2.atomicOps?.[field];
1091
+ if (atomicOp && lastWriterKind[field] === `atomic:${atomicOp.type}`) {
1092
+ next[field] = applyAtomicOp(next[field], atomicOp);
1093
+ } else {
1094
+ next[field] = value;
1095
+ }
1096
+ lastWriterKind[field] = atomicOp ? `atomic:${atomicOp.type}` : "lww";
1097
+ }
1098
+ record = next;
1099
+ deleted = false;
1100
+ }
1101
+ break;
1102
+ case "delete":
1103
+ deleted = true;
1104
+ break;
1105
+ }
1106
+ }
1107
+ return deleted ? null : record;
1108
+ }
1109
+
1063
1110
  // src/operations/op-data-binary.ts
1064
1111
  function isKoraBytesValue(value) {
1065
1112
  if (typeof value !== "object" || value === null) {
@@ -1310,7 +1357,7 @@ function validateStateMachineDefinition(collectionName, sm, fields) {
1310
1357
  }
1311
1358
 
1312
1359
  // src/schema/define.ts
1313
- var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
1360
+ var COLLECTION_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
1314
1361
  var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
1315
1362
  var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
1316
1363
  function defineSchema(input) {
@@ -1375,7 +1422,7 @@ function validateVersion(version) {
1375
1422
  function validateCollectionName(name) {
1376
1423
  if (!COLLECTION_NAME_RE.test(name)) {
1377
1424
  throw new SchemaValidationError(
1378
- `Collection name "${name}" is invalid. Must be lowercase, start with a letter, and contain only letters, numbers, and underscores.`,
1425
+ `Collection name "${name}" is invalid. Must start with a letter and contain only letters, numbers, and underscores.`,
1379
1426
  { collection: name }
1380
1427
  );
1381
1428
  }
@@ -1567,6 +1614,11 @@ function validateRelation(name, relation, collections) {
1567
1614
  }
1568
1615
  }
1569
1616
 
1617
+ // src/schema/quote-ident.ts
1618
+ function quoteIdent(name) {
1619
+ return `"${name.replace(/"/g, '""')}"`;
1620
+ }
1621
+
1570
1622
  // src/schema/sql-gen.ts
1571
1623
  function generateSQL(collectionName, collection, relations) {
1572
1624
  const statements = [];
@@ -1578,7 +1630,7 @@ function generateSQL(collectionName, collection, relations) {
1578
1630
  if (relations) {
1579
1631
  for (const rel of Object.values(relations)) {
1580
1632
  if (rel.from === collectionName && rel.field === fieldName) {
1581
- colDef += ` REFERENCES ${rel.to}(id)`;
1633
+ colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
1582
1634
  fkFields.push(fieldName);
1583
1635
  break;
1584
1636
  }
@@ -1591,36 +1643,40 @@ function generateSQL(collectionName, collection, relations) {
1591
1643
  columns.push("_version TEXT NOT NULL DEFAULT ''");
1592
1644
  columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
1593
1645
  columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
1594
- statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
1646
+ statements.push(
1647
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
1595
1648
  ${columns.join(",\n ")}
1596
- )`);
1649
+ )`
1650
+ );
1597
1651
  for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
1598
1652
  const colDef = columnDefinition(fieldName, descriptor);
1599
- statements.push(`--kora:safe-alter
1600
- ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
1653
+ statements.push(
1654
+ `--kora:safe-alter
1655
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
1656
+ );
1601
1657
  }
1602
1658
  statements.push(
1603
1659
  `--kora:safe-alter
1604
- ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
1660
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
1605
1661
  );
1606
1662
  statements.push(
1607
1663
  `--kora:safe-alter
1608
- ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1664
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1609
1665
  );
1610
1666
  for (const indexField of collection.indexes) {
1611
1667
  statements.push(
1612
- `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
1668
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
1613
1669
  );
1614
1670
  }
1615
1671
  for (const fkField of fkFields) {
1616
1672
  if (!indexedFields.has(fkField)) {
1617
1673
  statements.push(
1618
- `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${collectionName} (${fkField})`
1674
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
1619
1675
  );
1620
1676
  }
1621
1677
  }
1622
1678
  statements.push(
1623
- `CREATE TABLE IF NOT EXISTS _kora_ops_${collectionName} (
1679
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
1624
1680
  id TEXT PRIMARY KEY NOT NULL,
1625
1681
  node_id TEXT NOT NULL,
1626
1682
  type TEXT NOT NULL,
@@ -1634,7 +1690,7 @@ ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '
1634
1690
  )`
1635
1691
  );
1636
1692
  statements.push(
1637
- `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
1693
+ `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
1638
1694
  );
1639
1695
  return statements;
1640
1696
  }
@@ -1652,6 +1708,9 @@ function generateFullDDL(schema) {
1652
1708
  statements.push(
1653
1709
  "CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
1654
1710
  );
1711
+ statements.push(
1712
+ "CREATE TABLE IF NOT EXISTS _kora_sync_rejected (\n operation_id TEXT PRIMARY KEY NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n code TEXT NOT NULL,\n message TEXT NOT NULL,\n retriable INTEGER NOT NULL,\n rejected_at INTEGER NOT NULL\n)"
1713
+ );
1655
1714
  statements.push(
1656
1715
  "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)"
1657
1716
  );
@@ -1668,7 +1727,7 @@ function generateFullDDL(schema) {
1668
1727
  }
1669
1728
  function columnDefinition(fieldName, descriptor) {
1670
1729
  const sqlType = mapFieldType(descriptor);
1671
- const parts = [fieldName, sqlType];
1730
+ const parts = [quoteIdent(fieldName), sqlType];
1672
1731
  if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
1673
1732
  parts.push("NOT NULL");
1674
1733
  }
@@ -1677,7 +1736,7 @@ function columnDefinition(fieldName, descriptor) {
1677
1736
  }
1678
1737
  if (descriptor.kind === "enum" && descriptor.enumValues) {
1679
1738
  const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
1680
- parts.push(`CHECK (${fieldName} IN (${values}))`);
1739
+ parts.push(`CHECK (${quoteIdent(fieldName)} IN (${values}))`);
1681
1740
  }
1682
1741
  return parts.join(" ");
1683
1742
  }
@@ -2319,10 +2378,17 @@ function validateFieldValue(collection, fieldName, descriptor, value) {
2319
2378
  break;
2320
2379
  }
2321
2380
  case "json": {
2322
- if (!isJsonSerializable(value)) {
2381
+ const nonJson = findNonJsonSerializable(value, fieldName);
2382
+ if (nonJson) {
2323
2383
  throw new SchemaValidationError(
2324
- `Field "${fieldName}" in collection "${collection}" must be JSON-serializable, got ${describeType(value)}`,
2325
- { collection, field: fieldName, expectedType: "json", receivedType: describeType(value) }
2384
+ `Field "${fieldName}" in collection "${collection}" must be JSON-serializable; found ${nonJson.receivedType} at "${nonJson.path}"`,
2385
+ {
2386
+ collection,
2387
+ field: fieldName,
2388
+ expectedType: "json",
2389
+ receivedType: nonJson.receivedType,
2390
+ path: nonJson.path
2391
+ }
2326
2392
  );
2327
2393
  }
2328
2394
  break;
@@ -2359,21 +2425,41 @@ function isPlainObject(value) {
2359
2425
  const proto = Object.getPrototypeOf(value);
2360
2426
  return proto === null || proto === Object.prototype;
2361
2427
  }
2362
- function isJsonSerializable(value) {
2428
+ function findNonJsonSerializable(value, path, seen = /* @__PURE__ */ new WeakSet()) {
2363
2429
  if (value === null) {
2364
- return true;
2430
+ return null;
2365
2431
  }
2366
2432
  const type = typeof value;
2367
2433
  if (type === "string" || type === "number" || type === "boolean") {
2368
- return Number.isFinite(value) || type !== "number";
2434
+ if (type === "number" && !Number.isFinite(value)) {
2435
+ return { path, receivedType: describeType(value) };
2436
+ }
2437
+ return null;
2438
+ }
2439
+ if (type === "undefined" || type === "function" || type === "symbol" || type === "bigint") {
2440
+ return { path, receivedType: describeType(value) };
2369
2441
  }
2370
2442
  if (Array.isArray(value)) {
2371
- return value.every(isJsonSerializable);
2443
+ for (let index = 0; index < value.length; index++) {
2444
+ const invalid = findNonJsonSerializable(value[index], `${path}[${index}]`, seen);
2445
+ if (invalid) return invalid;
2446
+ }
2447
+ return null;
2372
2448
  }
2373
2449
  if (type === "object") {
2374
- return Object.values(value).every(isJsonSerializable);
2450
+ const objectValue = value;
2451
+ if (seen.has(objectValue)) {
2452
+ return { path, receivedType: "circular object" };
2453
+ }
2454
+ seen.add(objectValue);
2455
+ for (const [key, nestedValue] of Object.entries(objectValue)) {
2456
+ const invalid = findNonJsonSerializable(nestedValue, `${path}.${key}`, seen);
2457
+ if (invalid) return invalid;
2458
+ }
2459
+ seen.delete(objectValue);
2460
+ return null;
2375
2461
  }
2376
- return false;
2462
+ return { path, receivedType: describeType(value) };
2377
2463
  }
2378
2464
  function describeType(value) {
2379
2465
  if (value === null) {
@@ -2932,14 +3018,18 @@ function migrationStepsToSQL(steps) {
2932
3018
  statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
2933
3019
  break;
2934
3020
  case "removeField":
2935
- statements.push(`ALTER TABLE ${step.collection} DROP COLUMN ${step.field}`);
3021
+ statements.push(
3022
+ `ALTER TABLE ${quoteIdent(step.collection)} DROP COLUMN ${quoteIdent(step.field)}`
3023
+ );
2936
3024
  break;
2937
3025
  case "renameField":
2938
- statements.push(`ALTER TABLE ${step.collection} RENAME COLUMN ${step.from} TO ${step.to}`);
3026
+ statements.push(
3027
+ `ALTER TABLE ${quoteIdent(step.collection)} RENAME COLUMN ${quoteIdent(step.from)} TO ${quoteIdent(step.to)}`
3028
+ );
2939
3029
  break;
2940
3030
  case "addIndex":
2941
3031
  statements.push(
2942
- `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
3032
+ `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${quoteIdent(step.collection)} (${quoteIdent(step.field)})`
2943
3033
  );
2944
3034
  break;
2945
3035
  case "removeIndex":
@@ -2957,13 +3047,13 @@ function rollbackStepsToSQL(migration) {
2957
3047
  }
2958
3048
  function addFieldSQL(collection, field, descriptor) {
2959
3049
  const sqlType = mapFieldType2(descriptor);
2960
- const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
3050
+ const parts = [`ALTER TABLE ${quoteIdent(collection)} ADD COLUMN ${quoteIdent(field)}`, sqlType];
2961
3051
  if (descriptor.defaultValue !== void 0) {
2962
3052
  parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
2963
3053
  }
2964
3054
  if (descriptor.kind === "enum" && descriptor.enumValues) {
2965
3055
  const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
2966
- parts.push(`CHECK (${field} IN (${values}))`);
3056
+ parts.push(`CHECK (${quoteIdent(field)} IN (${values}))`);
2967
3057
  }
2968
3058
  return parts.join(" ");
2969
3059
  }
@@ -3266,6 +3356,7 @@ function generateProtoDefinitions(schema) {
3266
3356
  StorageError,
3267
3357
  SyncError,
3268
3358
  advanceVector,
3359
+ applyAtomicOp,
3269
3360
  applyOperationTransforms,
3270
3361
  base64ToBytes,
3271
3362
  buildScopeMap,
@@ -3314,6 +3405,8 @@ function generateProtoDefinitions(schema) {
3314
3405
  migrate,
3315
3406
  migrationStepsToSQL,
3316
3407
  op,
3408
+ quoteIdent,
3409
+ replayOperationsForRecord,
3317
3410
  resolveAtomicOp,
3318
3411
  revealSecret,
3319
3412
  rollbackStepsToSQL,