@orkestrel/database 0.0.4 → 0.0.6

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,29 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _src_core = require("../core/index.cjs");
2
3
  let _orkestrel_contract = require("@orkestrel/contract");
3
4
  let node_crypto = require("node:crypto");
4
- let _src_core = require("../core/index.cjs");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
7
7
  let _orkestrel_sqlite = require("@orkestrel/sqlite");
8
- //#region src/server/helpers.ts
9
- /**
10
- * Generate a fresh unique key — a v4 UUID string, backed by `node:crypto`.
11
- *
12
- * @remarks
13
- * Supply this as {@link import('@orkestrel/database').DatabaseOptions.key} so a table mints
14
- * a key when a written row lacks its primary-key value. Strings work as keys on
15
- * every backend; supply your own key values directly to use numeric keys instead.
16
- *
17
- * @returns A new UUID string
18
- *
19
- * @example
20
- * ```ts
21
- * const db = createDatabase({ driver, tables, key: generateKey })
22
- * ```
23
- */
24
- function generateKey() {
25
- return (0, node_crypto.randomUUID)();
26
- }
8
+ //#region src/server/constants.ts
27
9
  /**
28
10
  * The declared {@link ColumnType}s whose SQL EQUALITY comparisons (`equals` /
29
11
  * `not` / `any` / `none`) and `starts` / `ends` compiles are provably
@@ -46,12 +28,12 @@ function generateKey() {
46
28
  * values are BMP-only, or a custom SQLite collation matching `compareValues`
47
29
  * is registered) could restore native text ranges/ordering.
48
30
  */
49
- var EXACT_COLUMN_TYPES = [
31
+ var EXACT_COLUMN_TYPES = Object.freeze([
50
32
  "text",
51
33
  "integer",
52
34
  "real",
53
35
  "boolean"
54
- ];
36
+ ]);
55
37
  /**
56
38
  * The declared {@link ColumnType}s whose SQL RANGE comparisons
57
39
  * (`above` / `below` / `from` / `to` / `between`) and `ORDER BY` compiles are
@@ -60,11 +42,41 @@ var EXACT_COLUMN_TYPES = [
60
42
  * (code-point) vs. JS `<` (code-unit) divergence on supplementary-plane
61
43
  * characters.
62
44
  */
63
- var EXACT_RANGE_COLUMN_TYPES = [
45
+ var EXACT_RANGE_COLUMN_TYPES = Object.freeze([
64
46
  "integer",
65
47
  "real",
66
48
  "boolean"
67
- ];
49
+ ]);
50
+ /**
51
+ * The reserved metadata table the {@link SQLiteDriver} creates on `open` to
52
+ * persist its stamped `DriverMeta` (`version` + declared schema JSON) — the
53
+ * SQLite realization of the `meta` / `stamp` driver hooks.
54
+ *
55
+ * @remarks
56
+ * A single-row table (`id = 1`). A user table named `_meta` collides with the
57
+ * reservation — the caller's concern to avoid, documented on the driver class.
58
+ */
59
+ var META_TABLE = "_meta";
60
+ //#endregion
61
+ //#region src/server/helpers.ts
62
+ /**
63
+ * Generate a fresh unique key — a v4 UUID string, backed by `node:crypto`.
64
+ *
65
+ * @remarks
66
+ * Supply this as {@link import('@orkestrel/database').DatabaseOptions.key} so a table mints
67
+ * a key when a written row lacks its primary-key value. Strings work as keys on
68
+ * every backend; supply your own key values directly to use numeric keys instead.
69
+ *
70
+ * @returns A new UUID string
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const db = createDatabase({ driver, tables, key: generateKey })
75
+ * ```
76
+ */
77
+ function generateKey() {
78
+ return (0, node_crypto.randomUUID)();
79
+ }
68
80
  /**
69
81
  * Whether a value's runtime type matches a column's declared exact type —
70
82
  * the operand side of the declared-type-trust proof.
@@ -254,8 +266,10 @@ function quote(identifier) {
254
266
  */
255
267
  function fieldColumn(path) {
256
268
  if ((0, _orkestrel_contract.isString)(path)) return quote(path);
257
- const rest = path.slice(1).map((key) => "." + key.replaceAll("'", "''")).join("");
258
- return "json_extract(" + quote(path[0]) + ", '$" + rest + "')";
269
+ const [column, ...nested] = path;
270
+ if (column === void 0) throw new _src_core.DatabaseError("VALIDATION", "A field path must contain at least one column");
271
+ const rest = nested.map((key) => "." + key.replaceAll("'", "''")).join("");
272
+ return "json_extract(" + quote(column) + ", '$" + rest + "')";
259
273
  }
260
274
  /**
261
275
  * Compile an {@link AggregateFunction} over a {@link FieldPath} to its SQL
@@ -373,6 +387,37 @@ function encodeRow(row, schema) {
373
387
  return result;
374
388
  }
375
389
  /**
390
+ * Extract a stored row's values in a declared positional order.
391
+ *
392
+ * @remarks
393
+ * SQLite statements bind arrays positionally. Every requested column must be
394
+ * present in `row`; an incomplete backend row is a typed `DRIVER` fault carrying
395
+ * the table and missing column in its context.
396
+ *
397
+ * @param row - The stored SQLite row
398
+ * @param names - The column names in binding order
399
+ * @param table - The owning table name for fault context
400
+ * @returns The row values in the same order as `names`
401
+ * @throws A `DRIVER` {@link DatabaseError} when a requested column is missing
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * extractValues({ id: 'u1', age: 36 }, ['age', 'id'], 'users') // [36, 'u1']
406
+ * ```
407
+ */
408
+ function extractValues(row, names, table) {
409
+ const values = [];
410
+ for (const name of names) {
411
+ const value = row[name];
412
+ if (value === void 0) throw new _src_core.DatabaseError("DRIVER", "SQLite row is missing a declared column", {
413
+ table,
414
+ column: name
415
+ });
416
+ values.push(value);
417
+ }
418
+ return values;
419
+ }
420
+ /**
376
421
  * Decode a stored {@link SQLiteRow} back to a {@link Row} by its table's schema.
377
422
  *
378
423
  * @remarks
@@ -395,7 +440,9 @@ function encodeRow(row, schema) {
395
440
  function decodeRow(row, schema) {
396
441
  const result = {};
397
442
  for (const column of schema.columns) {
398
- const decoded = decodeValue(row[column.name], column.type);
443
+ const value = row[column.name];
444
+ if (value === void 0) continue;
445
+ const decoded = decodeValue(value, column.type);
399
446
  if (decoded !== void 0) result[column.name] = decoded;
400
447
  }
401
448
  return result;
@@ -568,8 +615,10 @@ function stepToSchema(schema, step) {
568
615
  * ```
569
616
  */
570
617
  function jsonTypeColumn(path) {
571
- const rest = path.slice(1).map((key) => "." + key.replaceAll("'", "''")).join("");
572
- return "json_type(" + quote(path[0]) + ", '$" + rest + "')";
618
+ const [column, ...nested] = path;
619
+ if (column === void 0) throw new _src_core.DatabaseError("VALIDATION", "A field path must contain at least one column");
620
+ const rest = nested.map((key) => "." + key.replaceAll("'", "''")).join("");
621
+ return "json_type(" + quote(column) + ", '$" + rest + "')";
573
622
  }
574
623
  /**
575
624
  * Escape `\`, `%`, and `_` (each with a leading `\`) so a `starts` / `ends`
@@ -704,21 +753,21 @@ function fragment(condition, schema) {
704
753
  const column = fieldColumn(condition.column);
705
754
  const nested = !(0, _orkestrel_contract.isString)(condition.column);
706
755
  const declared = (0, _orkestrel_contract.isString)(condition.column) ? declaredType(condition.column, schema) : void 0;
707
- const encode = (value) => encodeValue(value, nested ? valueType(value) : declared ?? "json");
708
756
  const first = condition.values[0];
709
757
  const second = condition.values[1];
710
758
  const nullOperand = first === null || first === void 0;
711
759
  const jsonType = !(0, _orkestrel_contract.isString)(condition.column) ? jsonTypeColumn(condition.column) : "";
760
+ let sql;
761
+ let values;
712
762
  switch (condition.operator) {
713
763
  case "equals":
714
764
  if (nullOperand && nested) return {
715
765
  sql: jsonType + " = 'null'",
716
766
  params: []
717
767
  };
718
- return {
719
- sql: column + " = ?",
720
- params: [encode(first)]
721
- };
768
+ sql = column + " = ?";
769
+ values = [first];
770
+ break;
722
771
  case "not":
723
772
  if (nullOperand) {
724
773
  if (nested) return {
@@ -730,38 +779,37 @@ function fragment(condition, schema) {
730
779
  params: []
731
780
  };
732
781
  }
733
- return {
734
- sql: "(" + column + " != ? OR " + column + " IS NULL)",
735
- params: [encode(first)]
736
- };
737
- case "above": return {
738
- sql: column + " > ?",
739
- params: [encode(first)]
740
- };
741
- case "below": return {
742
- sql: "(" + column + " < ? OR " + column + " IS NULL)",
743
- params: [encode(first)]
744
- };
745
- case "from": return {
746
- sql: column + " >= ?",
747
- params: [encode(first)]
748
- };
749
- case "to": return {
750
- sql: "(" + column + " <= ? OR " + column + " IS NULL)",
751
- params: [encode(first)]
752
- };
753
- case "between": return {
754
- sql: column + " BETWEEN ? AND ?",
755
- params: [encode(first), encode(second)]
756
- };
757
- case "like": return {
758
- sql: column + " LIKE ?",
759
- params: [encode(first)]
760
- };
761
- case "glob": return {
762
- sql: column + " GLOB ?",
763
- params: [encode(first)]
764
- };
782
+ sql = "(" + column + " != ? OR " + column + " IS NULL)";
783
+ values = [first];
784
+ break;
785
+ case "above":
786
+ sql = column + " > ?";
787
+ values = [first];
788
+ break;
789
+ case "below":
790
+ sql = "(" + column + " < ? OR " + column + " IS NULL)";
791
+ values = [first];
792
+ break;
793
+ case "from":
794
+ sql = column + " >= ?";
795
+ values = [first];
796
+ break;
797
+ case "to":
798
+ sql = "(" + column + " <= ? OR " + column + " IS NULL)";
799
+ values = [first];
800
+ break;
801
+ case "between":
802
+ sql = column + " BETWEEN ? AND ?";
803
+ values = [first, second];
804
+ break;
805
+ case "like":
806
+ sql = column + " LIKE ?";
807
+ values = [first];
808
+ break;
809
+ case "glob":
810
+ sql = column + " GLOB ?";
811
+ values = [first];
812
+ break;
765
813
  case "starts": {
766
814
  const text = (0, _orkestrel_contract.isString)(first) ? first : "";
767
815
  if (text === "") return {
@@ -769,10 +817,9 @@ function fragment(condition, schema) {
769
817
  params: []
770
818
  };
771
819
  const length = Array.from(text).length;
772
- return {
773
- sql: "(typeof(" + column + ") = 'text' AND substr(" + column + ", 1, " + length + ") = ?)",
774
- params: [encode(first)]
775
- };
820
+ sql = "(typeof(" + column + ") = 'text' AND substr(" + column + ", 1, " + length + ") = ?)";
821
+ values = [first];
822
+ break;
776
823
  }
777
824
  case "ends": {
778
825
  const text = (0, _orkestrel_contract.isString)(first) ? first : "";
@@ -781,29 +828,26 @@ function fragment(condition, schema) {
781
828
  params: []
782
829
  };
783
830
  const length = Array.from(text).length;
784
- return {
785
- sql: "(typeof(" + column + ") = 'text' AND substr(" + column + ", -" + length + ") = ?)",
786
- params: [encode(first)]
787
- };
831
+ sql = "(typeof(" + column + ") = 'text' AND substr(" + column + ", -" + length + ") = ?)";
832
+ values = [first];
833
+ break;
788
834
  }
789
835
  case "any":
790
836
  if (condition.values.length === 0) return {
791
837
  sql: "0",
792
838
  params: []
793
839
  };
794
- return {
795
- sql: column + " IN (" + condition.values.map(() => "?").join(", ") + ")",
796
- params: condition.values.map(encode)
797
- };
840
+ sql = column + " IN (" + condition.values.map(() => "?").join(", ") + ")";
841
+ values = condition.values;
842
+ break;
798
843
  case "none":
799
844
  if (condition.values.length === 0) return {
800
845
  sql: "1",
801
846
  params: []
802
847
  };
803
- return {
804
- sql: "(" + column + " NOT IN (" + condition.values.map(() => "?").join(", ") + ") OR " + column + " IS NULL)",
805
- params: condition.values.map(encode)
806
- };
848
+ sql = "(" + column + " NOT IN (" + condition.values.map(() => "?").join(", ") + ") OR " + column + " IS NULL)";
849
+ values = condition.values;
850
+ break;
807
851
  case "absent": return {
808
852
  sql: column + " IS NULL",
809
853
  params: []
@@ -813,6 +857,10 @@ function fragment(condition, schema) {
813
857
  params: []
814
858
  };
815
859
  }
860
+ return {
861
+ sql,
862
+ params: values.map((value) => encodeValue(value, nested ? valueType(value) : declared ?? "json"))
863
+ };
816
864
  }
817
865
  /**
818
866
  * Fold the conditions into one WHERE clause, parenthesizing progressively
@@ -836,16 +884,17 @@ function fragment(condition, schema) {
836
884
  * ```
837
885
  */
838
886
  function compileWhere(conditions, schema) {
839
- if (conditions.length === 0) return {
887
+ const [first, ...remaining] = conditions;
888
+ if (first === void 0) return {
840
889
  sql: "",
841
890
  params: []
842
891
  };
843
- const head = fragment(conditions[0], schema);
892
+ const head = fragment(first, schema);
844
893
  let clause = head.sql;
845
894
  const params = [...head.params];
846
- for (let index = 1; index < conditions.length; index += 1) {
847
- const next = fragment(conditions[index], schema);
848
- const operator = conditions[index].connector === "or" ? "OR" : "AND";
895
+ for (const condition of remaining) {
896
+ const next = fragment(condition, schema);
897
+ const operator = condition.connector === "or" ? "OR" : "AND";
849
898
  clause = "(" + clause + " " + operator + " " + next.sql + ")";
850
899
  params.push(...next.params);
851
900
  }
@@ -962,18 +1011,6 @@ function compileCriteria(criteria, schema) {
962
1011
  };
963
1012
  }
964
1013
  //#endregion
965
- //#region src/server/constants.ts
966
- /**
967
- * The reserved metadata table the {@link SQLiteDriver} creates on `open` to
968
- * persist its stamped `DriverMeta` (`version` + declared schema JSON) — the
969
- * SQLite realization of the `meta` / `stamp` driver hooks.
970
- *
971
- * @remarks
972
- * A single-row table (`id = 1`). A user table named `_meta` collides with the
973
- * reservation — the caller's concern to avoid, documented on the driver class.
974
- */
975
- var META_TABLE = "_meta";
976
- //#endregion
977
1014
  //#region src/server/drivers/JSONDriver.ts
978
1015
  /**
979
1016
  * A persistent {@link DriverInterface} backed by a single JSON file — the
@@ -1013,6 +1050,7 @@ var JSONDriver = class {
1013
1050
  #flushCount = 0;
1014
1051
  #chain = Promise.resolve();
1015
1052
  #deferring = false;
1053
+ #transaction;
1016
1054
  constructor(path) {
1017
1055
  this.#path = path;
1018
1056
  }
@@ -1081,22 +1119,12 @@ var JSONDriver = class {
1081
1119
  async transaction() {
1082
1120
  if (this.#deferring) throw new _src_core.DatabaseError("CONFLICT", "A transaction is already active on this driver", {});
1083
1121
  const rollback = await this.#memory.snapshot();
1122
+ const token = {};
1084
1123
  this.#deferring = true;
1085
- let settled = false;
1124
+ this.#transaction = token;
1086
1125
  return {
1087
- commit: async () => {
1088
- if (settled) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1089
- settled = true;
1090
- this.#deferring = false;
1091
- await this.#flush();
1092
- },
1093
- rollback: async () => {
1094
- if (settled) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1095
- settled = true;
1096
- await rollback();
1097
- this.#deferring = false;
1098
- await this.#flush();
1099
- }
1126
+ commit: this.#commit.bind(this, token),
1127
+ rollback: this.#rollback.bind(this, token, rollback)
1100
1128
  };
1101
1129
  }
1102
1130
  async snapshot(tables) {
@@ -1149,6 +1177,19 @@ var JSONDriver = class {
1149
1177
  this.#schema = schema;
1150
1178
  await this.#flush();
1151
1179
  }
1180
+ async #commit(token) {
1181
+ if (this.#transaction !== token) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1182
+ this.#transaction = void 0;
1183
+ this.#deferring = false;
1184
+ await this.#flush();
1185
+ }
1186
+ async #rollback(token, rollback) {
1187
+ if (this.#transaction !== token) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1188
+ this.#transaction = void 0;
1189
+ await rollback();
1190
+ this.#deferring = false;
1191
+ await this.#flush();
1192
+ }
1152
1193
  async #load() {
1153
1194
  let raw;
1154
1195
  try {
@@ -1261,7 +1302,7 @@ var SQLiteDriver = class {
1261
1302
  #options;
1262
1303
  #database;
1263
1304
  #schema = /* @__PURE__ */ new Map();
1264
- #transacting = false;
1305
+ #transaction;
1265
1306
  constructor(path, options) {
1266
1307
  this.#path = path;
1267
1308
  this.#options = options ?? {};
@@ -1272,9 +1313,9 @@ var SQLiteDriver = class {
1272
1313
  this.#database?.close();
1273
1314
  const database = (0, _orkestrel_sqlite.createSQLiteDatabase)({
1274
1315
  path: this.#path,
1275
- readonly: this.#options.readonly,
1276
- timeout: this.#options.timeout,
1277
- foreignKeys: this.#options.foreignKeys
1316
+ ...this.#options.readonly !== void 0 ? { readonly: this.#options.readonly } : {},
1317
+ ...this.#options.timeout !== void 0 ? { timeout: this.#options.timeout } : {},
1318
+ ...this.#options.foreignKeys !== void 0 ? { foreignKeys: this.#options.foreignKeys } : {}
1278
1319
  });
1279
1320
  database.connect();
1280
1321
  for (const [name, value] of Object.entries(this.#options.pragmas ?? {})) database.pragma(name, value);
@@ -1308,7 +1349,7 @@ var SQLiteDriver = class {
1308
1349
  [schema.primary]: key
1309
1350
  }, schema);
1310
1351
  const names = schema.columns.map((column) => column.name);
1311
- const values = names.map((name) => encoded[name]);
1352
+ const values = extractValues(encoded, names, table);
1312
1353
  this.#require().prepare("INSERT OR REPLACE INTO " + quote(table) + " (" + names.map(quote).join(", ") + ") VALUES (" + names.map(() => "?").join(", ") + ")").run(values);
1313
1354
  });
1314
1355
  }
@@ -1388,8 +1429,8 @@ var SQLiteDriver = class {
1388
1429
  if (conditions.every((condition) => isExactCondition(condition, schema))) {
1389
1430
  const compiled = compileCriteria({
1390
1431
  conditions,
1391
- limit: criteria.limit,
1392
- offset: criteria.offset
1432
+ ...criteria.limit !== void 0 ? { limit: criteria.limit } : {},
1433
+ ...criteria.offset !== void 0 ? { offset: criteria.offset } : {}
1393
1434
  }, schema);
1394
1435
  for (const row of this.#require().prepare("SELECT * FROM " + quote(table) + (compiled.sql === "" ? "" : " " + compiled.sql)).iterate(compiled.params)) yield decodeRow(row, schema);
1395
1436
  return;
@@ -1421,21 +1462,11 @@ var SQLiteDriver = class {
1421
1462
  async transaction() {
1422
1463
  const database = this.#require();
1423
1464
  this.#guard(() => database.exec("BEGIN"));
1424
- let settled = false;
1425
- this.#transacting = true;
1465
+ const token = {};
1466
+ this.#transaction = token;
1426
1467
  return {
1427
- commit: async () => {
1428
- if (settled) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1429
- settled = true;
1430
- this.#transacting = false;
1431
- this.#guard(() => database.exec("COMMIT"));
1432
- },
1433
- rollback: async () => {
1434
- if (settled) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1435
- settled = true;
1436
- this.#transacting = false;
1437
- this.#guard(() => database.exec("ROLLBACK"));
1438
- }
1468
+ commit: this.#commit.bind(this, token, database),
1469
+ rollback: this.#rollback.bind(this, token, database)
1439
1470
  };
1440
1471
  }
1441
1472
  /**
@@ -1465,7 +1496,7 @@ var SQLiteDriver = class {
1465
1496
  const database = this.#require();
1466
1497
  const schema = new Map(this.#schema);
1467
1498
  this.#guard(() => {
1468
- if (this.#transacting) this.#applyPlan(database, plan, schema);
1499
+ if (this.#transaction !== void 0) this.#applyPlan(database, plan, schema);
1469
1500
  else database.transaction(() => this.#applyPlan(database, plan, schema));
1470
1501
  });
1471
1502
  this.#schema = schema;
@@ -1525,11 +1556,21 @@ var SQLiteDriver = class {
1525
1556
  for (const [name, snapshot] of captured) {
1526
1557
  current.exec("DELETE FROM " + quote(name));
1527
1558
  const statement = current.prepare("INSERT OR REPLACE INTO " + quote(name) + " (" + snapshot.names.map(quote).join(", ") + ") VALUES (" + snapshot.names.map(() => "?").join(", ") + ")");
1528
- for (const row of snapshot.rows) statement.run(snapshot.names.map((column) => row[column]));
1559
+ for (const row of snapshot.rows) statement.run(extractValues(row, snapshot.names, name));
1529
1560
  }
1530
1561
  });
1531
1562
  };
1532
1563
  }
1564
+ async #commit(token, database) {
1565
+ if (this.#transaction !== token) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1566
+ this.#transaction = void 0;
1567
+ this.#guard(() => database.exec("COMMIT"));
1568
+ }
1569
+ async #rollback(token, database) {
1570
+ if (this.#transaction !== token) throw new _src_core.DatabaseError("CONFLICT", "Transaction already settled", {});
1571
+ this.#transaction = void 0;
1572
+ this.#guard(() => database.exec("ROLLBACK"));
1573
+ }
1533
1574
  #guard(run) {
1534
1575
  try {
1535
1576
  return run();
@@ -1678,6 +1719,7 @@ exports.decodeValue = decodeValue;
1678
1719
  exports.encodeRow = encodeRow;
1679
1720
  exports.encodeValue = encodeValue;
1680
1721
  exports.escapeLike = escapeLike;
1722
+ exports.extractValues = extractValues;
1681
1723
  exports.fieldColumn = fieldColumn;
1682
1724
  exports.fragment = fragment;
1683
1725
  exports.generateKey = generateKey;