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

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.
@@ -1,5 +1,5 @@
1
- import { p as KoraEventEmitter } from '../events-CqDkr1LN.cjs';
2
- import { S as ScopeMap } from '../build-scope-map-DtTr9C7A.cjs';
1
+ import { p as KoraEventEmitter } from '../events-DhblJN4r.cjs';
2
+ import { S as ScopeMap } from '../build-scope-map-K25AyQXb.cjs';
3
3
 
4
4
  /** Sync status shape consumed by framework binding hooks (matches {@link SyncStatusInfo} in `@korajs/sync`). */
5
5
  interface KoraBindingSyncStatus {
@@ -1,5 +1,5 @@
1
- import { p as KoraEventEmitter } from '../events-CqDkr1LN.js';
2
- import { S as ScopeMap } from '../build-scope-map-DvOHbYjn.js';
1
+ import { p as KoraEventEmitter } from '../events-DhblJN4r.js';
2
+ import { S as ScopeMap } from '../build-scope-map-BvdTzmm4.js';
3
3
 
4
4
  /** Sync status shape consumed by framework binding hooks (matches {@link SyncStatusInfo} in `@korajs/sync`). */
5
5
  interface KoraBindingSyncStatus {
@@ -1,4 +1,4 @@
1
- import { S as SchemaDefinition } from './events-CqDkr1LN.js';
1
+ import { S as SchemaDefinition } from './events-DhblJN4r.js';
2
2
 
3
3
  /**
4
4
  * Per-collection scope map: `{ collectionName: { field: value, ... } }`
@@ -1,4 +1,4 @@
1
- import { S as SchemaDefinition } from './events-CqDkr1LN.cjs';
1
+ import { S as SchemaDefinition } from './events-DhblJN4r.cjs';
2
2
 
3
3
  /**
4
4
  * Per-collection scope map: `{ collectionName: { field: value, ... } }`
@@ -826,6 +826,21 @@ type KoraEvent = {
826
826
  code: string;
827
827
  message: string;
828
828
  retriable: boolean;
829
+ } | {
830
+ /**
831
+ * The server rejected one of THIS client's outbound operations before it
832
+ * became authoritative. The op has been diverted out of the pending sync
833
+ * queue into the durable rejected store (kept, not retried); the app can
834
+ * surface the reason and decide whether to roll back the optimistic local
835
+ * write or let the user edit and resubmit.
836
+ */
837
+ type: 'sync:operation-rejected';
838
+ operationId: string;
839
+ collection: string;
840
+ recordId: string;
841
+ code: string;
842
+ message: string;
843
+ retriable: boolean;
829
844
  } | {
830
845
  type: 'query:subscribed';
831
846
  queryId: string;
@@ -880,6 +895,34 @@ type KoraEvent = {
880
895
  type: 'store:quota-exceeded';
881
896
  dbName: string;
882
897
  message: string;
898
+ } | {
899
+ /**
900
+ * OPFS persistence was unavailable, so the store fell back to a
901
+ * NON-PERSISTENT in-memory database. Anything written this session is lost
902
+ * on reload. This is emitted instead of failing silently so the condition
903
+ * is observable rather than a data-loss trap.
904
+ */
905
+ type: 'store:opfs-unavailable';
906
+ dbName: string;
907
+ /**
908
+ * Why OPFS could not be used. `lock-conflict` means another runtime on this
909
+ * origin already holds the OPFS pool for this database; `timeout` means the
910
+ * VFS install did not complete in time (common in headless CI); `unsupported`
911
+ * means the runtime has no usable OPFS.
912
+ */
913
+ reason: 'lock-conflict' | 'timeout' | 'unsupported';
914
+ message: string;
915
+ } | {
916
+ /**
917
+ * Another runtime on this origin was already using this database name, so
918
+ * this runtime attached to it as a follower and now SHARES that one
919
+ * database. That is intended for multiple tabs of the SAME app; it is a bug
920
+ * if these are logically separate apps, which should each use a distinct
921
+ * store name (`store: { name: '...' }`) to stay isolated.
922
+ */
923
+ type: 'store:db-name-collision';
924
+ dbName: string;
925
+ message: string;
883
926
  } | {
884
927
  type: 'replay:completed';
885
928
  targetOperationId: string;
@@ -826,6 +826,21 @@ type KoraEvent = {
826
826
  code: string;
827
827
  message: string;
828
828
  retriable: boolean;
829
+ } | {
830
+ /**
831
+ * The server rejected one of THIS client's outbound operations before it
832
+ * became authoritative. The op has been diverted out of the pending sync
833
+ * queue into the durable rejected store (kept, not retried); the app can
834
+ * surface the reason and decide whether to roll back the optimistic local
835
+ * write or let the user edit and resubmit.
836
+ */
837
+ type: 'sync:operation-rejected';
838
+ operationId: string;
839
+ collection: string;
840
+ recordId: string;
841
+ code: string;
842
+ message: string;
843
+ retriable: boolean;
829
844
  } | {
830
845
  type: 'query:subscribed';
831
846
  queryId: string;
@@ -880,6 +895,34 @@ type KoraEvent = {
880
895
  type: 'store:quota-exceeded';
881
896
  dbName: string;
882
897
  message: string;
898
+ } | {
899
+ /**
900
+ * OPFS persistence was unavailable, so the store fell back to a
901
+ * NON-PERSISTENT in-memory database. Anything written this session is lost
902
+ * on reload. This is emitted instead of failing silently so the condition
903
+ * is observable rather than a data-loss trap.
904
+ */
905
+ type: 'store:opfs-unavailable';
906
+ dbName: string;
907
+ /**
908
+ * Why OPFS could not be used. `lock-conflict` means another runtime on this
909
+ * origin already holds the OPFS pool for this database; `timeout` means the
910
+ * VFS install did not complete in time (common in headless CI); `unsupported`
911
+ * means the runtime has no usable OPFS.
912
+ */
913
+ reason: 'lock-conflict' | 'timeout' | 'unsupported';
914
+ message: string;
915
+ } | {
916
+ /**
917
+ * Another runtime on this origin was already using this database name, so
918
+ * this runtime attached to it as a follower and now SHARES that one
919
+ * database. That is intended for multiple tabs of the SAME app; it is a bug
920
+ * if these are logically separate apps, which should each use a distinct
921
+ * store name (`store: { name: '...' }`) to stay isolated.
922
+ */
923
+ type: 'store:db-name-collision';
924
+ dbName: string;
925
+ message: string;
883
926
  } | {
884
927
  type: 'replay:completed';
885
928
  targetOperationId: string;
package/dist/index.cjs CHANGED
@@ -94,6 +94,7 @@ __export(index_exports, {
94
94
  migrate: () => migrate,
95
95
  migrationStepsToSQL: () => migrationStepsToSQL,
96
96
  op: () => op,
97
+ quoteIdent: () => quoteIdent,
97
98
  resolveAtomicOp: () => resolveAtomicOp,
98
99
  revealSecret: () => revealSecret,
99
100
  rollbackStepsToSQL: () => rollbackStepsToSQL,
@@ -1310,7 +1311,7 @@ function validateStateMachineDefinition(collectionName, sm, fields) {
1310
1311
  }
1311
1312
 
1312
1313
  // src/schema/define.ts
1313
- var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
1314
+ var COLLECTION_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
1314
1315
  var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
1315
1316
  var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
1316
1317
  function defineSchema(input) {
@@ -1375,7 +1376,7 @@ function validateVersion(version) {
1375
1376
  function validateCollectionName(name) {
1376
1377
  if (!COLLECTION_NAME_RE.test(name)) {
1377
1378
  throw new SchemaValidationError(
1378
- `Collection name "${name}" is invalid. Must be lowercase, start with a letter, and contain only letters, numbers, and underscores.`,
1379
+ `Collection name "${name}" is invalid. Must start with a letter and contain only letters, numbers, and underscores.`,
1379
1380
  { collection: name }
1380
1381
  );
1381
1382
  }
@@ -1567,6 +1568,11 @@ function validateRelation(name, relation, collections) {
1567
1568
  }
1568
1569
  }
1569
1570
 
1571
+ // src/schema/quote-ident.ts
1572
+ function quoteIdent(name) {
1573
+ return `"${name.replace(/"/g, '""')}"`;
1574
+ }
1575
+
1570
1576
  // src/schema/sql-gen.ts
1571
1577
  function generateSQL(collectionName, collection, relations) {
1572
1578
  const statements = [];
@@ -1578,7 +1584,7 @@ function generateSQL(collectionName, collection, relations) {
1578
1584
  if (relations) {
1579
1585
  for (const rel of Object.values(relations)) {
1580
1586
  if (rel.from === collectionName && rel.field === fieldName) {
1581
- colDef += ` REFERENCES ${rel.to}(id)`;
1587
+ colDef += ` REFERENCES ${quoteIdent(rel.to)}(id)`;
1582
1588
  fkFields.push(fieldName);
1583
1589
  break;
1584
1590
  }
@@ -1591,36 +1597,40 @@ function generateSQL(collectionName, collection, relations) {
1591
1597
  columns.push("_version TEXT NOT NULL DEFAULT ''");
1592
1598
  columns.push("_field_versions TEXT NOT NULL DEFAULT '{}'");
1593
1599
  columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
1594
- statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
1600
+ statements.push(
1601
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(collectionName)} (
1595
1602
  ${columns.join(",\n ")}
1596
- )`);
1603
+ )`
1604
+ );
1597
1605
  for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
1598
1606
  const colDef = columnDefinition(fieldName, descriptor);
1599
- statements.push(`--kora:safe-alter
1600
- ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
1607
+ statements.push(
1608
+ `--kora:safe-alter
1609
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN ${colDef}`
1610
+ );
1601
1611
  }
1602
1612
  statements.push(
1603
1613
  `--kora:safe-alter
1604
- ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
1614
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
1605
1615
  );
1606
1616
  statements.push(
1607
1617
  `--kora:safe-alter
1608
- ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1618
+ ALTER TABLE ${quoteIdent(collectionName)} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '{}'`
1609
1619
  );
1610
1620
  for (const indexField of collection.indexes) {
1611
1621
  statements.push(
1612
- `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
1622
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${quoteIdent(collectionName)} (${quoteIdent(indexField)})`
1613
1623
  );
1614
1624
  }
1615
1625
  for (const fkField of fkFields) {
1616
1626
  if (!indexedFields.has(fkField)) {
1617
1627
  statements.push(
1618
- `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${collectionName} (${fkField})`
1628
+ `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${fkField} ON ${quoteIdent(collectionName)} (${quoteIdent(fkField)})`
1619
1629
  );
1620
1630
  }
1621
1631
  }
1622
1632
  statements.push(
1623
- `CREATE TABLE IF NOT EXISTS _kora_ops_${collectionName} (
1633
+ `CREATE TABLE IF NOT EXISTS ${quoteIdent(`_kora_ops_${collectionName}`)} (
1624
1634
  id TEXT PRIMARY KEY NOT NULL,
1625
1635
  node_id TEXT NOT NULL,
1626
1636
  type TEXT NOT NULL,
@@ -1634,7 +1644,7 @@ ALTER TABLE ${collectionName} ADD COLUMN _field_versions TEXT NOT NULL DEFAULT '
1634
1644
  )`
1635
1645
  );
1636
1646
  statements.push(
1637
- `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON _kora_ops_${collectionName} (record_id)`
1647
+ `CREATE INDEX IF NOT EXISTS idx_kora_ops_${collectionName}_record_id ON ${quoteIdent(`_kora_ops_${collectionName}`)} (record_id)`
1638
1648
  );
1639
1649
  return statements;
1640
1650
  }
@@ -1652,6 +1662,9 @@ function generateFullDDL(schema) {
1652
1662
  statements.push(
1653
1663
  "CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
1654
1664
  );
1665
+ statements.push(
1666
+ "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)"
1667
+ );
1655
1668
  statements.push(
1656
1669
  "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
1670
  );
@@ -1668,7 +1681,7 @@ function generateFullDDL(schema) {
1668
1681
  }
1669
1682
  function columnDefinition(fieldName, descriptor) {
1670
1683
  const sqlType = mapFieldType(descriptor);
1671
- const parts = [fieldName, sqlType];
1684
+ const parts = [quoteIdent(fieldName), sqlType];
1672
1685
  if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
1673
1686
  parts.push("NOT NULL");
1674
1687
  }
@@ -1677,7 +1690,7 @@ function columnDefinition(fieldName, descriptor) {
1677
1690
  }
1678
1691
  if (descriptor.kind === "enum" && descriptor.enumValues) {
1679
1692
  const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
1680
- parts.push(`CHECK (${fieldName} IN (${values}))`);
1693
+ parts.push(`CHECK (${quoteIdent(fieldName)} IN (${values}))`);
1681
1694
  }
1682
1695
  return parts.join(" ");
1683
1696
  }
@@ -2932,14 +2945,18 @@ function migrationStepsToSQL(steps) {
2932
2945
  statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
2933
2946
  break;
2934
2947
  case "removeField":
2935
- statements.push(`ALTER TABLE ${step.collection} DROP COLUMN ${step.field}`);
2948
+ statements.push(
2949
+ `ALTER TABLE ${quoteIdent(step.collection)} DROP COLUMN ${quoteIdent(step.field)}`
2950
+ );
2936
2951
  break;
2937
2952
  case "renameField":
2938
- statements.push(`ALTER TABLE ${step.collection} RENAME COLUMN ${step.from} TO ${step.to}`);
2953
+ statements.push(
2954
+ `ALTER TABLE ${quoteIdent(step.collection)} RENAME COLUMN ${quoteIdent(step.from)} TO ${quoteIdent(step.to)}`
2955
+ );
2939
2956
  break;
2940
2957
  case "addIndex":
2941
2958
  statements.push(
2942
- `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
2959
+ `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${quoteIdent(step.collection)} (${quoteIdent(step.field)})`
2943
2960
  );
2944
2961
  break;
2945
2962
  case "removeIndex":
@@ -2957,13 +2974,13 @@ function rollbackStepsToSQL(migration) {
2957
2974
  }
2958
2975
  function addFieldSQL(collection, field, descriptor) {
2959
2976
  const sqlType = mapFieldType2(descriptor);
2960
- const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
2977
+ const parts = [`ALTER TABLE ${quoteIdent(collection)} ADD COLUMN ${quoteIdent(field)}`, sqlType];
2961
2978
  if (descriptor.defaultValue !== void 0) {
2962
2979
  parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
2963
2980
  }
2964
2981
  if (descriptor.kind === "enum" && descriptor.enumValues) {
2965
2982
  const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
2966
- parts.push(`CHECK (${field} IN (${values}))`);
2983
+ parts.push(`CHECK (${quoteIdent(field)} IN (${values}))`);
2967
2984
  }
2968
2985
  return parts.join(" ");
2969
2986
  }
@@ -3314,6 +3331,7 @@ function generateProtoDefinitions(schema) {
3314
3331
  migrate,
3315
3332
  migrationStepsToSQL,
3316
3333
  op,
3334
+ quoteIdent,
3317
3335
  resolveAtomicOp,
3318
3336
  revealSecret,
3319
3337
  rollbackStepsToSQL,