@objectstack/driver-sql 9.9.1 → 9.10.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.d.mts CHANGED
@@ -98,6 +98,7 @@ declare class SqlDriver implements IDataDriver {
98
98
  protected config: Knex.Config;
99
99
  protected jsonFields: Record<string, string[]>;
100
100
  protected booleanFields: Record<string, string[]>;
101
+ protected numericFields: Record<string, string[]>;
101
102
  protected dateFields: Record<string, Set<string>>;
102
103
  protected datetimeFields: Record<string, Set<string>>;
103
104
  protected tablesWithTimestamps: Set<string>;
@@ -429,6 +430,27 @@ declare class SqlDriver implements IDataDriver {
429
430
  * `YYYY-MM-DD` for the same reason.
430
431
  */
431
432
  protected coerceFilterValue(table: string | null, field: string, value: any): any;
433
+ /**
434
+ * Public, dialect-correct temporal filter-value coercion for callers that
435
+ * build SQL *outside* the normal `find()`/`applyFilters()` path — chiefly the
436
+ * analytics native-SQL strategy, which compiles a raw `SELECT … WHERE col >= $N`
437
+ * and binds the value directly, bypassing `coerceFilterValue`.
438
+ *
439
+ * Given a logical object (table) name, a field name and a filter value
440
+ * (typically an ISO date/datetime string from a dashboard relative-date
441
+ * token like `{12_months_ago}`), this returns the value in the column's
442
+ * on-disk storage form:
443
+ * - SQLite `Field.datetime` → epoch milliseconds (INTEGER), so the
444
+ * comparison matches the stored integer rather than failing a
445
+ * TEXT-vs-INTEGER affinity compare.
446
+ * - `Field.date` (any dialect) → `YYYY-MM-DD` text.
447
+ * - Native-timestamp dialects / non-temporal fields → value unchanged.
448
+ *
449
+ * This is a thin, intentionally narrow wrapper over the same `coerceFilterValue`
450
+ * the driver already uses, so there is exactly one source of truth for the
451
+ * storage convention and the analytics path can never drift from CRUD.
452
+ */
453
+ temporalFilterValue(objectName: string, field: string, value: any): any;
432
454
  protected applyFilters(builder: Knex.QueryBuilder, filters: any): void;
433
455
  /**
434
456
  * Apply a `contains` substring match as a parameterized `LIKE '%…%'`, escaping
package/dist/index.d.ts CHANGED
@@ -98,6 +98,7 @@ declare class SqlDriver implements IDataDriver {
98
98
  protected config: Knex.Config;
99
99
  protected jsonFields: Record<string, string[]>;
100
100
  protected booleanFields: Record<string, string[]>;
101
+ protected numericFields: Record<string, string[]>;
101
102
  protected dateFields: Record<string, Set<string>>;
102
103
  protected datetimeFields: Record<string, Set<string>>;
103
104
  protected tablesWithTimestamps: Set<string>;
@@ -429,6 +430,27 @@ declare class SqlDriver implements IDataDriver {
429
430
  * `YYYY-MM-DD` for the same reason.
430
431
  */
431
432
  protected coerceFilterValue(table: string | null, field: string, value: any): any;
433
+ /**
434
+ * Public, dialect-correct temporal filter-value coercion for callers that
435
+ * build SQL *outside* the normal `find()`/`applyFilters()` path — chiefly the
436
+ * analytics native-SQL strategy, which compiles a raw `SELECT … WHERE col >= $N`
437
+ * and binds the value directly, bypassing `coerceFilterValue`.
438
+ *
439
+ * Given a logical object (table) name, a field name and a filter value
440
+ * (typically an ISO date/datetime string from a dashboard relative-date
441
+ * token like `{12_months_ago}`), this returns the value in the column's
442
+ * on-disk storage form:
443
+ * - SQLite `Field.datetime` → epoch milliseconds (INTEGER), so the
444
+ * comparison matches the stored integer rather than failing a
445
+ * TEXT-vs-INTEGER affinity compare.
446
+ * - `Field.date` (any dialect) → `YYYY-MM-DD` text.
447
+ * - Native-timestamp dialects / non-temporal fields → value unchanged.
448
+ *
449
+ * This is a thin, intentionally narrow wrapper over the same `coerceFilterValue`
450
+ * the driver already uses, so there is exactly one source of truth for the
451
+ * storage convention and the analytics path can never drift from CRUD.
452
+ */
453
+ temporalFilterValue(objectName: string, field: string, value: any): any;
432
454
  protected applyFilters(builder: Knex.QueryBuilder, filters: any): void;
433
455
  /**
434
456
  * Apply a `contains` substring match as a parameterized `LIKE '%…%'`, escaping
package/dist/index.js CHANGED
@@ -48,9 +48,12 @@ var JSON_COLUMN_TYPES = /* @__PURE__ */ new Set([
48
48
  "json",
49
49
  "object",
50
50
  "array",
51
+ "record",
51
52
  "image",
52
53
  "file",
53
54
  "avatar",
55
+ "video",
56
+ "audio",
54
57
  "location",
55
58
  "address",
56
59
  "composite",
@@ -60,6 +63,18 @@ var JSON_COLUMN_TYPES = /* @__PURE__ */ new Set([
60
63
  "repeater",
61
64
  "vector"
62
65
  ]);
66
+ var NUMERIC_SCALAR_TYPES = /* @__PURE__ */ new Set([
67
+ "integer",
68
+ "int",
69
+ "float",
70
+ "number",
71
+ "currency",
72
+ "percent",
73
+ "summary",
74
+ "rating",
75
+ "slider",
76
+ "progress"
77
+ ]);
63
78
  var SqlDriver = class {
64
79
  constructor(config) {
65
80
  // IDataDriver metadata
@@ -67,6 +82,7 @@ var SqlDriver = class {
67
82
  this.version = "1.0.0";
68
83
  this.jsonFields = {};
69
84
  this.booleanFields = {};
85
+ this.numericFields = {};
70
86
  this.dateFields = {};
71
87
  this.datetimeFields = {};
72
88
  this.tablesWithTimestamps = /* @__PURE__ */ new Set();
@@ -847,6 +863,7 @@ var SqlDriver = class {
847
863
  const tableName = import_system.StorageNameMapping.resolveTableName(obj);
848
864
  const jsonCols = [];
849
865
  const booleanCols = [];
866
+ const numericCols = [];
850
867
  const autoNumberCols = [];
851
868
  const tenancyDecl = obj?.tenancy;
852
869
  let tenantField = null;
@@ -866,9 +883,12 @@ var SqlDriver = class {
866
883
  if (this.isJsonField(type, field)) {
867
884
  jsonCols.push(name);
868
885
  }
869
- if (type === "boolean") {
886
+ if (type === "boolean" || type === "toggle") {
870
887
  booleanCols.push(name);
871
888
  }
889
+ if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) {
890
+ numericCols.push(name);
891
+ }
872
892
  if (type === "date") {
873
893
  ((_a = this.dateFields)[tableName] ?? (_a[tableName] = /* @__PURE__ */ new Set())).add(name);
874
894
  }
@@ -887,6 +907,7 @@ var SqlDriver = class {
887
907
  }
888
908
  this.jsonFields[tableName] = jsonCols;
889
909
  this.booleanFields[tableName] = booleanCols;
910
+ this.numericFields[tableName] = numericCols;
890
911
  this.autoNumberFields[tableName] = autoNumberCols;
891
912
  this.tenantFieldByTable[tableName] = tenantField;
892
913
  let exists = await this.knex.schema.hasTable(tableName);
@@ -1238,11 +1259,35 @@ var SqlDriver = class {
1238
1259
  return null;
1239
1260
  };
1240
1261
  if (isDatetime) {
1262
+ if (!this.isSqlite) return value;
1241
1263
  const ms = toMs(value);
1242
1264
  return ms == null ? value : ms;
1243
1265
  }
1244
1266
  return this.toDateOnly(value);
1245
1267
  }
1268
+ /**
1269
+ * Public, dialect-correct temporal filter-value coercion for callers that
1270
+ * build SQL *outside* the normal `find()`/`applyFilters()` path — chiefly the
1271
+ * analytics native-SQL strategy, which compiles a raw `SELECT … WHERE col >= $N`
1272
+ * and binds the value directly, bypassing `coerceFilterValue`.
1273
+ *
1274
+ * Given a logical object (table) name, a field name and a filter value
1275
+ * (typically an ISO date/datetime string from a dashboard relative-date
1276
+ * token like `{12_months_ago}`), this returns the value in the column's
1277
+ * on-disk storage form:
1278
+ * - SQLite `Field.datetime` → epoch milliseconds (INTEGER), so the
1279
+ * comparison matches the stored integer rather than failing a
1280
+ * TEXT-vs-INTEGER affinity compare.
1281
+ * - `Field.date` (any dialect) → `YYYY-MM-DD` text.
1282
+ * - Native-timestamp dialects / non-temporal fields → value unchanged.
1283
+ *
1284
+ * This is a thin, intentionally narrow wrapper over the same `coerceFilterValue`
1285
+ * the driver already uses, so there is exactly one source of truth for the
1286
+ * storage convention and the analytics path can never drift from CRUD.
1287
+ */
1288
+ temporalFilterValue(objectName, field, value) {
1289
+ return this.coerceFilterValue(objectName, field, value);
1290
+ }
1246
1291
  applyFilters(builder, filters) {
1247
1292
  if (!filters) return;
1248
1293
  const table = this.tableNameForBuilder(builder);
@@ -1464,9 +1509,23 @@ var SqlDriver = class {
1464
1509
  case "number":
1465
1510
  case "currency":
1466
1511
  case "percent":
1512
+ // `rating`/`slider`/`progress` are authored as numeric scalars (a star
1513
+ // count, a slider position, a percent-of-completion). Without an explicit
1514
+ // case they fell to `default → table.string`, giving the column TEXT
1515
+ // affinity so SQLite coerced the written number to a string ('4' not 4) —
1516
+ // a silent type-fidelity leak the value-loss tests didn't catch. REAL
1517
+ // affinity round-trips them as JS numbers (#field-zoo).
1518
+ case "rating":
1519
+ case "slider":
1520
+ case "progress":
1467
1521
  col = table.float(name);
1468
1522
  break;
1523
+ // `toggle` is a boolean rendered as a switch. Same leak as above (TEXT
1524
+ // affinity stored '1'); a boolean column gives NUMERIC affinity and the
1525
+ // `booleanFields` read-coercion below converts the stored 1/0 back to a
1526
+ // real JS boolean.
1469
1527
  case "boolean":
1528
+ case "toggle":
1470
1529
  col = table.boolean(name);
1471
1530
  break;
1472
1531
  case "date":
@@ -1478,22 +1537,6 @@ var SqlDriver = class {
1478
1537
  case "time":
1479
1538
  col = table.time(name);
1480
1539
  break;
1481
- case "json":
1482
- case "object":
1483
- case "array":
1484
- case "image":
1485
- case "file":
1486
- case "avatar":
1487
- case "location":
1488
- case "address":
1489
- case "composite":
1490
- case "multiselect":
1491
- case "checkboxes":
1492
- case "tags":
1493
- case "repeater":
1494
- case "vector":
1495
- col = table.json(name);
1496
- break;
1497
1540
  case "lookup":
1498
1541
  col = table.string(name);
1499
1542
  if (field.reference_to) {
@@ -1511,7 +1554,7 @@ var SqlDriver = class {
1511
1554
  return;
1512
1555
  // Virtual — no column
1513
1556
  default:
1514
- col = table.string(name);
1557
+ col = JSON_COLUMN_TYPES.has(type) ? table.json(name) : table.string(name);
1515
1558
  }
1516
1559
  if (col) {
1517
1560
  if (field.unique) col.unique();
@@ -1676,6 +1719,16 @@ var SqlDriver = class {
1676
1719
  }
1677
1720
  }
1678
1721
  }
1722
+ const numericFields = this.numericFields[object];
1723
+ if (numericFields && numericFields.length > 0) {
1724
+ for (const field of numericFields) {
1725
+ const v = data[field];
1726
+ if (typeof v === "string" && v.trim() !== "") {
1727
+ const n = Number(v);
1728
+ if (!Number.isNaN(n)) data[field] = n;
1729
+ }
1730
+ }
1731
+ }
1679
1732
  }
1680
1733
  const dateFields = this.dateFields[object];
1681
1734
  if (dateFields && dateFields.size > 0) {