@objectstack/driver-sql 9.9.0 → 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 +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +71 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +71 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
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) {
|