@objectstack/driver-sql 6.1.1 → 6.3.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 +25 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +96 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +96 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -90,6 +90,8 @@ declare class SqlDriver implements IDataDriver {
|
|
|
90
90
|
protected config: Knex.Config;
|
|
91
91
|
protected jsonFields: Record<string, string[]>;
|
|
92
92
|
protected booleanFields: Record<string, string[]>;
|
|
93
|
+
protected dateFields: Record<string, Set<string>>;
|
|
94
|
+
protected datetimeFields: Record<string, Set<string>>;
|
|
93
95
|
protected tablesWithTimestamps: Set<string>;
|
|
94
96
|
/**
|
|
95
97
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -332,8 +334,30 @@ declare class SqlDriver implements IDataDriver {
|
|
|
332
334
|
* `OS_TENANT_AUDIT=0`) to silence intentionally.
|
|
333
335
|
*/
|
|
334
336
|
protected auditMissingTenant(object: string, op: 'create' | 'update' | 'delete' | 'bulkCreate' | 'bulkDelete' | 'updateMany' | 'deleteMany' | 'upsert', options?: DriverOptions): void;
|
|
337
|
+
/**
|
|
338
|
+
* Resolve the underlying table name for a Knex query builder so we can
|
|
339
|
+
* look up column type metadata (date/datetime maps populated during
|
|
340
|
+
* `initObjects`). Returns null when the builder is not table-scoped yet.
|
|
341
|
+
*/
|
|
342
|
+
protected tableNameForBuilder(builder: any): string | null;
|
|
343
|
+
/**
|
|
344
|
+
* Normalise a filter value for a single column so the comparison the
|
|
345
|
+
* driver sends to SQLite matches the on-disk representation.
|
|
346
|
+
*
|
|
347
|
+
* The platform stores `Field.datetime()` values as INTEGER milliseconds
|
|
348
|
+
* (the result of passing a JS `Date` through better-sqlite3) but date
|
|
349
|
+
* macros like `{last_quarter_start}` expand to an ISO `YYYY-MM-DD` string
|
|
350
|
+
* client-side. Without coercion the SQL becomes `published_at >= '2026-…'`
|
|
351
|
+
* which collapses to a TEXT-vs-INTEGER affinity compare and never
|
|
352
|
+
* matches. We translate the ISO/Date/numeric inputs into the storage
|
|
353
|
+
* type so the comparison works.
|
|
354
|
+
*
|
|
355
|
+
* For `Field.date()` we keep ISO TEXT but normalise Date objects to
|
|
356
|
+
* `YYYY-MM-DD` for the same reason.
|
|
357
|
+
*/
|
|
358
|
+
protected coerceFilterValue(table: string | null, field: string, value: any): any;
|
|
335
359
|
protected applyFilters(builder: Knex.QueryBuilder, filters: any): void;
|
|
336
|
-
protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or'): void;
|
|
360
|
+
protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or', tableHint?: string | null): void;
|
|
337
361
|
protected mapSortField(field: string): string;
|
|
338
362
|
protected mapAggregateFunc(func: string): string;
|
|
339
363
|
protected buildWindowFunction(spec: any): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ declare class SqlDriver implements IDataDriver {
|
|
|
90
90
|
protected config: Knex.Config;
|
|
91
91
|
protected jsonFields: Record<string, string[]>;
|
|
92
92
|
protected booleanFields: Record<string, string[]>;
|
|
93
|
+
protected dateFields: Record<string, Set<string>>;
|
|
94
|
+
protected datetimeFields: Record<string, Set<string>>;
|
|
93
95
|
protected tablesWithTimestamps: Set<string>;
|
|
94
96
|
/**
|
|
95
97
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -332,8 +334,30 @@ declare class SqlDriver implements IDataDriver {
|
|
|
332
334
|
* `OS_TENANT_AUDIT=0`) to silence intentionally.
|
|
333
335
|
*/
|
|
334
336
|
protected auditMissingTenant(object: string, op: 'create' | 'update' | 'delete' | 'bulkCreate' | 'bulkDelete' | 'updateMany' | 'deleteMany' | 'upsert', options?: DriverOptions): void;
|
|
337
|
+
/**
|
|
338
|
+
* Resolve the underlying table name for a Knex query builder so we can
|
|
339
|
+
* look up column type metadata (date/datetime maps populated during
|
|
340
|
+
* `initObjects`). Returns null when the builder is not table-scoped yet.
|
|
341
|
+
*/
|
|
342
|
+
protected tableNameForBuilder(builder: any): string | null;
|
|
343
|
+
/**
|
|
344
|
+
* Normalise a filter value for a single column so the comparison the
|
|
345
|
+
* driver sends to SQLite matches the on-disk representation.
|
|
346
|
+
*
|
|
347
|
+
* The platform stores `Field.datetime()` values as INTEGER milliseconds
|
|
348
|
+
* (the result of passing a JS `Date` through better-sqlite3) but date
|
|
349
|
+
* macros like `{last_quarter_start}` expand to an ISO `YYYY-MM-DD` string
|
|
350
|
+
* client-side. Without coercion the SQL becomes `published_at >= '2026-…'`
|
|
351
|
+
* which collapses to a TEXT-vs-INTEGER affinity compare and never
|
|
352
|
+
* matches. We translate the ISO/Date/numeric inputs into the storage
|
|
353
|
+
* type so the comparison works.
|
|
354
|
+
*
|
|
355
|
+
* For `Field.date()` we keep ISO TEXT but normalise Date objects to
|
|
356
|
+
* `YYYY-MM-DD` for the same reason.
|
|
357
|
+
*/
|
|
358
|
+
protected coerceFilterValue(table: string | null, field: string, value: any): any;
|
|
335
359
|
protected applyFilters(builder: Knex.QueryBuilder, filters: any): void;
|
|
336
|
-
protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or'): void;
|
|
360
|
+
protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or', tableHint?: string | null): void;
|
|
337
361
|
protected mapSortField(field: string): string;
|
|
338
362
|
protected mapAggregateFunc(func: string): string;
|
|
339
363
|
protected buildWindowFunction(spec: any): string;
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,8 @@ var SqlDriver = class {
|
|
|
49
49
|
this.version = "1.0.0";
|
|
50
50
|
this.jsonFields = {};
|
|
51
51
|
this.booleanFields = {};
|
|
52
|
+
this.dateFields = {};
|
|
53
|
+
this.datetimeFields = {};
|
|
52
54
|
this.tablesWithTimestamps = /* @__PURE__ */ new Set();
|
|
53
55
|
/**
|
|
54
56
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -783,6 +785,7 @@ var SqlDriver = class {
|
|
|
783
785
|
* Batch-initialise tables from an array of object definitions.
|
|
784
786
|
*/
|
|
785
787
|
async initObjects(objects) {
|
|
788
|
+
var _a, _b;
|
|
786
789
|
await this.ensureDatabaseExists();
|
|
787
790
|
for (const obj of objects) {
|
|
788
791
|
const tableName = import_system.StorageNameMapping.resolveTableName(obj);
|
|
@@ -810,6 +813,12 @@ var SqlDriver = class {
|
|
|
810
813
|
if (type === "boolean") {
|
|
811
814
|
booleanCols.push(name);
|
|
812
815
|
}
|
|
816
|
+
if (type === "date") {
|
|
817
|
+
((_a = this.dateFields)[tableName] ?? (_a[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
818
|
+
}
|
|
819
|
+
if (type === "datetime") {
|
|
820
|
+
((_b = this.datetimeFields)[tableName] ?? (_b[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
821
|
+
}
|
|
813
822
|
if (type === "auto_number" || type === "autonumber") {
|
|
814
823
|
const fmt = typeof field.format === "string" && field.format ? field.format : "{0000}";
|
|
815
824
|
const m = fmt.match(/\{(0+)\}/);
|
|
@@ -997,19 +1006,83 @@ var SqlDriver = class {
|
|
|
997
1006
|
);
|
|
998
1007
|
}
|
|
999
1008
|
// ── Filter helpers ──────────────────────────────────────────────────────────
|
|
1009
|
+
/**
|
|
1010
|
+
* Resolve the underlying table name for a Knex query builder so we can
|
|
1011
|
+
* look up column type metadata (date/datetime maps populated during
|
|
1012
|
+
* `initObjects`). Returns null when the builder is not table-scoped yet.
|
|
1013
|
+
*/
|
|
1014
|
+
tableNameForBuilder(builder) {
|
|
1015
|
+
const t = builder?._single?.table;
|
|
1016
|
+
if (typeof t === "string") return t;
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Normalise a filter value for a single column so the comparison the
|
|
1021
|
+
* driver sends to SQLite matches the on-disk representation.
|
|
1022
|
+
*
|
|
1023
|
+
* The platform stores `Field.datetime()` values as INTEGER milliseconds
|
|
1024
|
+
* (the result of passing a JS `Date` through better-sqlite3) but date
|
|
1025
|
+
* macros like `{last_quarter_start}` expand to an ISO `YYYY-MM-DD` string
|
|
1026
|
+
* client-side. Without coercion the SQL becomes `published_at >= '2026-…'`
|
|
1027
|
+
* which collapses to a TEXT-vs-INTEGER affinity compare and never
|
|
1028
|
+
* matches. We translate the ISO/Date/numeric inputs into the storage
|
|
1029
|
+
* type so the comparison works.
|
|
1030
|
+
*
|
|
1031
|
+
* For `Field.date()` we keep ISO TEXT but normalise Date objects to
|
|
1032
|
+
* `YYYY-MM-DD` for the same reason.
|
|
1033
|
+
*/
|
|
1034
|
+
coerceFilterValue(table, field, value) {
|
|
1035
|
+
if (value == null || !table) return value;
|
|
1036
|
+
if (Array.isArray(value)) return value.map((v) => this.coerceFilterValue(table, field, v));
|
|
1037
|
+
const isDatetime = this.datetimeFields[table]?.has(field);
|
|
1038
|
+
const isDate = this.dateFields[table]?.has(field);
|
|
1039
|
+
if (!isDatetime && !isDate) return value;
|
|
1040
|
+
const toMs = (v) => {
|
|
1041
|
+
if (v instanceof Date) return v.getTime();
|
|
1042
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1043
|
+
if (typeof v === "string") {
|
|
1044
|
+
const trimmed = v.trim();
|
|
1045
|
+
if (trimmed === "") return null;
|
|
1046
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
1047
|
+
const n2 = Number(trimmed);
|
|
1048
|
+
if (Number.isFinite(n2)) return n2;
|
|
1049
|
+
}
|
|
1050
|
+
const iso = /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? `${trimmed}T00:00:00.000Z` : trimmed;
|
|
1051
|
+
const n = Date.parse(iso);
|
|
1052
|
+
return Number.isFinite(n) ? n : null;
|
|
1053
|
+
}
|
|
1054
|
+
return null;
|
|
1055
|
+
};
|
|
1056
|
+
if (isDatetime) {
|
|
1057
|
+
const ms = toMs(value);
|
|
1058
|
+
return ms == null ? value : ms;
|
|
1059
|
+
}
|
|
1060
|
+
if (value instanceof Date) {
|
|
1061
|
+
const y = value.getUTCFullYear();
|
|
1062
|
+
const m = String(value.getUTCMonth() + 1).padStart(2, "0");
|
|
1063
|
+
const d = String(value.getUTCDate()).padStart(2, "0");
|
|
1064
|
+
return `${y}-${m}-${d}`;
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof value === "string") {
|
|
1067
|
+
const trimmed = value.trim();
|
|
1068
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return trimmed.slice(0, 10);
|
|
1069
|
+
}
|
|
1070
|
+
return value;
|
|
1071
|
+
}
|
|
1000
1072
|
applyFilters(builder, filters) {
|
|
1001
1073
|
if (!filters) return;
|
|
1074
|
+
const table = this.tableNameForBuilder(builder);
|
|
1002
1075
|
if (!Array.isArray(filters) && typeof filters === "object") {
|
|
1003
1076
|
const hasMongoOperators = Object.keys(filters).some(
|
|
1004
1077
|
(k) => k.startsWith("$") || typeof filters[k] === "object" && filters[k] !== null && Object.keys(filters[k]).some((op) => op.startsWith("$"))
|
|
1005
1078
|
);
|
|
1006
1079
|
if (hasMongoOperators) {
|
|
1007
|
-
this.applyFilterCondition(builder, filters);
|
|
1080
|
+
this.applyFilterCondition(builder, filters, "and", table);
|
|
1008
1081
|
return;
|
|
1009
1082
|
}
|
|
1010
1083
|
for (const [key, value] of Object.entries(filters)) {
|
|
1011
1084
|
if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
|
|
1012
|
-
builder.where(key, value);
|
|
1085
|
+
builder.where(key, this.coerceFilterValue(table, key, value));
|
|
1013
1086
|
}
|
|
1014
1087
|
return;
|
|
1015
1088
|
}
|
|
@@ -1026,6 +1099,7 @@ var SqlDriver = class {
|
|
|
1026
1099
|
const isCriterion = typeof fieldRaw === "string" && typeof op === "string";
|
|
1027
1100
|
if (isCriterion) {
|
|
1028
1101
|
const field = this.mapSortField(fieldRaw);
|
|
1102
|
+
const coerced = this.coerceFilterValue(table, field, value);
|
|
1029
1103
|
const apply = (b) => {
|
|
1030
1104
|
const method = nextJoin === "or" ? "orWhere" : "where";
|
|
1031
1105
|
const methodIn = nextJoin === "or" ? "orWhereIn" : "whereIn";
|
|
@@ -1036,19 +1110,19 @@ var SqlDriver = class {
|
|
|
1036
1110
|
}
|
|
1037
1111
|
switch (op) {
|
|
1038
1112
|
case "=":
|
|
1039
|
-
b[method](field,
|
|
1113
|
+
b[method](field, coerced);
|
|
1040
1114
|
break;
|
|
1041
1115
|
case "!=":
|
|
1042
|
-
b[method](field, "<>",
|
|
1116
|
+
b[method](field, "<>", coerced);
|
|
1043
1117
|
break;
|
|
1044
1118
|
case "in":
|
|
1045
|
-
b[methodIn](field,
|
|
1119
|
+
b[methodIn](field, coerced);
|
|
1046
1120
|
break;
|
|
1047
1121
|
case "nin":
|
|
1048
|
-
b[methodNotIn](field,
|
|
1122
|
+
b[methodNotIn](field, coerced);
|
|
1049
1123
|
break;
|
|
1050
1124
|
default:
|
|
1051
|
-
b[method](field, op,
|
|
1125
|
+
b[method](field, op, coerced);
|
|
1052
1126
|
}
|
|
1053
1127
|
};
|
|
1054
1128
|
apply(builder);
|
|
@@ -1062,14 +1136,15 @@ var SqlDriver = class {
|
|
|
1062
1136
|
}
|
|
1063
1137
|
}
|
|
1064
1138
|
}
|
|
1065
|
-
applyFilterCondition(builder, condition, logicalOp = "and") {
|
|
1139
|
+
applyFilterCondition(builder, condition, logicalOp = "and", tableHint) {
|
|
1066
1140
|
if (!condition || typeof condition !== "object") return;
|
|
1141
|
+
const table = tableHint ?? this.tableNameForBuilder(builder);
|
|
1067
1142
|
for (const [key, value] of Object.entries(condition)) {
|
|
1068
1143
|
if (key === "$and" && Array.isArray(value)) {
|
|
1069
1144
|
builder.where((qb) => {
|
|
1070
1145
|
for (const sub of value) {
|
|
1071
1146
|
qb.where((subQb) => {
|
|
1072
|
-
this.applyFilterCondition(subQb, sub, "and");
|
|
1147
|
+
this.applyFilterCondition(subQb, sub, "and", table);
|
|
1073
1148
|
});
|
|
1074
1149
|
}
|
|
1075
1150
|
});
|
|
@@ -1078,7 +1153,7 @@ var SqlDriver = class {
|
|
|
1078
1153
|
builder[method]((qb) => {
|
|
1079
1154
|
for (const sub of value) {
|
|
1080
1155
|
qb.orWhere((subQb) => {
|
|
1081
|
-
this.applyFilterCondition(subQb, sub, "or");
|
|
1156
|
+
this.applyFilterCondition(subQb, sub, "or", table);
|
|
1082
1157
|
});
|
|
1083
1158
|
}
|
|
1084
1159
|
});
|
|
@@ -1086,46 +1161,47 @@ var SqlDriver = class {
|
|
|
1086
1161
|
const field = this.mapSortField(key);
|
|
1087
1162
|
for (const [op, opValue] of Object.entries(value)) {
|
|
1088
1163
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1164
|
+
const coerced = this.coerceFilterValue(table, field, opValue);
|
|
1089
1165
|
switch (op) {
|
|
1090
1166
|
case "$eq":
|
|
1091
|
-
builder[method](field,
|
|
1167
|
+
builder[method](field, coerced);
|
|
1092
1168
|
break;
|
|
1093
1169
|
case "$ne":
|
|
1094
|
-
builder[method](field, "<>",
|
|
1170
|
+
builder[method](field, "<>", coerced);
|
|
1095
1171
|
break;
|
|
1096
1172
|
case "$gt":
|
|
1097
|
-
builder[method](field, ">",
|
|
1173
|
+
builder[method](field, ">", coerced);
|
|
1098
1174
|
break;
|
|
1099
1175
|
case "$gte":
|
|
1100
|
-
builder[method](field, ">=",
|
|
1176
|
+
builder[method](field, ">=", coerced);
|
|
1101
1177
|
break;
|
|
1102
1178
|
case "$lt":
|
|
1103
|
-
builder[method](field, "<",
|
|
1179
|
+
builder[method](field, "<", coerced);
|
|
1104
1180
|
break;
|
|
1105
1181
|
case "$lte":
|
|
1106
|
-
builder[method](field, "<=",
|
|
1182
|
+
builder[method](field, "<=", coerced);
|
|
1107
1183
|
break;
|
|
1108
1184
|
case "$in": {
|
|
1109
1185
|
const mIn = logicalOp === "or" ? "orWhereIn" : "whereIn";
|
|
1110
|
-
builder[mIn](field,
|
|
1186
|
+
builder[mIn](field, coerced);
|
|
1111
1187
|
break;
|
|
1112
1188
|
}
|
|
1113
1189
|
case "$nin": {
|
|
1114
1190
|
const mNotIn = logicalOp === "or" ? "orWhereNotIn" : "whereNotIn";
|
|
1115
|
-
builder[mNotIn](field,
|
|
1191
|
+
builder[mNotIn](field, coerced);
|
|
1116
1192
|
break;
|
|
1117
1193
|
}
|
|
1118
1194
|
case "$contains":
|
|
1119
1195
|
builder[method](field, "like", `%${opValue}%`);
|
|
1120
1196
|
break;
|
|
1121
1197
|
default:
|
|
1122
|
-
builder[method](field,
|
|
1198
|
+
builder[method](field, coerced);
|
|
1123
1199
|
}
|
|
1124
1200
|
}
|
|
1125
1201
|
} else {
|
|
1126
1202
|
const field = this.mapSortField(key);
|
|
1127
1203
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1128
|
-
builder[method](field, value);
|
|
1204
|
+
builder[method](field, this.coerceFilterValue(table, field, value));
|
|
1129
1205
|
}
|
|
1130
1206
|
}
|
|
1131
1207
|
}
|