@objectstack/driver-sql 10.3.0 → 11.1.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/LICENSE +202 -93
- package/dist/index.d.mts +60 -1
- package/dist/index.d.ts +60 -1
- package/dist/index.js +167 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +167 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -43,6 +43,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
43
43
|
var import_data = require("@objectstack/spec/data");
|
|
44
44
|
var import_system = require("@objectstack/spec/system");
|
|
45
45
|
var import_shared = require("@objectstack/spec/shared");
|
|
46
|
+
var import_types = require("@objectstack/types");
|
|
46
47
|
|
|
47
48
|
// src/schema-drift.ts
|
|
48
49
|
var BUILTIN_COLUMNS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
@@ -182,6 +183,39 @@ var NUMERIC_SCALAR_TYPES = /* @__PURE__ */ new Set([
|
|
|
182
183
|
"slider",
|
|
183
184
|
"progress"
|
|
184
185
|
]);
|
|
186
|
+
var AUDIT_TIMESTAMP_COLUMNS = ["created_at", "updated_at"];
|
|
187
|
+
function repairNaiveUtcAuditTimestamp(value) {
|
|
188
|
+
if (typeof value !== "string") return value;
|
|
189
|
+
const s = value.trim();
|
|
190
|
+
if (s === "") return value;
|
|
191
|
+
if (/[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return value;
|
|
192
|
+
const m = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/.exec(s);
|
|
193
|
+
if (!m) return value;
|
|
194
|
+
const d = /* @__PURE__ */ new Date(`${m[1]}T${m[2]}Z`);
|
|
195
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString();
|
|
196
|
+
}
|
|
197
|
+
function isNowDefaultValue(v) {
|
|
198
|
+
return typeof v === "string" && /^now\(\)$/i.test(v.trim());
|
|
199
|
+
}
|
|
200
|
+
function normalizeSqliteDatetimeOutput(value) {
|
|
201
|
+
if (value == null) return value;
|
|
202
|
+
if (typeof value === "number") {
|
|
203
|
+
if (!Number.isFinite(value)) return value;
|
|
204
|
+
const d = new Date(value);
|
|
205
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString();
|
|
206
|
+
}
|
|
207
|
+
if (value instanceof Date) {
|
|
208
|
+
return Number.isNaN(value.getTime()) ? value : value.toISOString();
|
|
209
|
+
}
|
|
210
|
+
if (typeof value !== "string") return value;
|
|
211
|
+
const s = value.trim();
|
|
212
|
+
if (s === "") return value;
|
|
213
|
+
if (/^-?\d+$/.test(s)) {
|
|
214
|
+
const d = new Date(Number(s));
|
|
215
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString();
|
|
216
|
+
}
|
|
217
|
+
return repairNaiveUtcAuditTimestamp(s);
|
|
218
|
+
}
|
|
185
219
|
var SqlDriver = class {
|
|
186
220
|
constructor(config) {
|
|
187
221
|
// IDataDriver metadata
|
|
@@ -192,6 +226,7 @@ var SqlDriver = class {
|
|
|
192
226
|
this.numericFields = {};
|
|
193
227
|
this.dateFields = {};
|
|
194
228
|
this.datetimeFields = {};
|
|
229
|
+
this.timeFields = {};
|
|
195
230
|
/**
|
|
196
231
|
* Federation read path (ADR-0015). For external objects whose physical
|
|
197
232
|
* remote table differs from the object name, these map between the two so
|
|
@@ -461,7 +496,7 @@ var SqlDriver = class {
|
|
|
461
496
|
await this.knex.destroy();
|
|
462
497
|
}
|
|
463
498
|
// ===================================
|
|
464
|
-
// CRUD —
|
|
499
|
+
// CRUD — IDataDriver core
|
|
465
500
|
// ===================================
|
|
466
501
|
async find(object, query, options) {
|
|
467
502
|
const buildBase = () => {
|
|
@@ -551,6 +586,7 @@ var SqlDriver = class {
|
|
|
551
586
|
await this.fillAutoNumberFields(object, toInsert, options);
|
|
552
587
|
const builder = this.getBuilder(object, options);
|
|
553
588
|
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toInsert));
|
|
589
|
+
this.stampInsertTimestamps(object, formatted);
|
|
554
590
|
const result = await builder.insert(formatted).returning("*");
|
|
555
591
|
return this.formatOutput(object, result[0]);
|
|
556
592
|
}
|
|
@@ -778,18 +814,39 @@ var SqlDriver = class {
|
|
|
778
814
|
row[cfg.name] = (0, import_data.renderAutonumber)({ tokens: cfg.tokens, seq: next, record: row, now, timezone }).value;
|
|
779
815
|
}
|
|
780
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Stamp the builtin audit timestamps to one canonical ISO-8601-with-`Z`
|
|
819
|
+
* instant on the SQLite write paths (`create`/`bulkCreate`/`upsert`), so
|
|
820
|
+
* INSERT and UPDATE agree on a single zone-explicit format.
|
|
821
|
+
*
|
|
822
|
+
* Without this, an insert that omits `created_at`/`updated_at` falls back to
|
|
823
|
+
* the column's `CURRENT_TIMESTAMP` default, which on SQLite renders a
|
|
824
|
+
* zone-NAIVE, space-separated `'YYYY-MM-DD HH:MM:SS'` (no millis, no zone) —
|
|
825
|
+
* the same ambiguity the old UPDATE stamp had. Stamping app-side (rather than
|
|
826
|
+
* changing the column default) fixes this for EXISTING tenant databases
|
|
827
|
+
* immediately, since their tables keep the legacy default. Legacy/raw rows
|
|
828
|
+
* still written zone-naive are repaired on read by
|
|
829
|
+
* `repairNaiveUtcAuditTimestamp`.
|
|
830
|
+
*
|
|
831
|
+
* Only fills a slot the caller left empty — an explicit value (a seed fixture,
|
|
832
|
+
* the sys_metadata writer, a service outbox) is preserved. No-op for
|
|
833
|
+
* timestamp-less objects and for Postgres/MySQL, whose native `now()` column
|
|
834
|
+
* default already stores a zone-aware TIMESTAMP.
|
|
835
|
+
*/
|
|
836
|
+
stampInsertTimestamps(object, formatted) {
|
|
837
|
+
if (!this.isSqlite || !this.tablesWithTimestamps.has(object)) return;
|
|
838
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
839
|
+
for (const col of AUDIT_TIMESTAMP_COLUMNS) {
|
|
840
|
+
if (formatted[col] === void 0 || formatted[col] === null) formatted[col] = iso;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
781
843
|
async update(object, id, data, options) {
|
|
782
844
|
this.auditMissingTenant(object, "update", options);
|
|
783
845
|
const builder = this.getBuilder(object, options).where("id", id);
|
|
784
846
|
this.applyTenantScope(builder, object, options);
|
|
785
847
|
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, data));
|
|
786
848
|
if (this.tablesWithTimestamps.has(object)) {
|
|
787
|
-
|
|
788
|
-
const now = /* @__PURE__ */ new Date();
|
|
789
|
-
formatted.updated_at = now.toISOString().replace("T", " ").replace("Z", "");
|
|
790
|
-
} else {
|
|
791
|
-
formatted.updated_at = this.knex.fn.now();
|
|
792
|
-
}
|
|
849
|
+
formatted.updated_at = this.isSqlite ? (/* @__PURE__ */ new Date()).toISOString() : this.knex.fn.now();
|
|
793
850
|
}
|
|
794
851
|
await builder.update(formatted);
|
|
795
852
|
const readback = this.getBuilder(object, options).where("id", id);
|
|
@@ -809,9 +866,12 @@ var SqlDriver = class {
|
|
|
809
866
|
this.injectTenantOnInsert(object, toUpsert, options);
|
|
810
867
|
await this.fillAutoNumberFields(object, toUpsert, options);
|
|
811
868
|
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toUpsert));
|
|
869
|
+
this.stampInsertTimestamps(object, formatted);
|
|
812
870
|
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
|
|
813
871
|
const builder = this.getBuilder(object, options);
|
|
814
|
-
|
|
872
|
+
const mergeColumns = Object.keys(formatted).filter((c) => c !== "created_at");
|
|
873
|
+
const insertion = builder.insert(formatted).onConflict(mergeKeys);
|
|
874
|
+
await (mergeColumns.length > 0 ? insertion.merge(mergeColumns) : insertion.merge());
|
|
815
875
|
const readback = this.getBuilder(object, options).where("id", toUpsert.id);
|
|
816
876
|
this.applyTenantScope(readback, object, options);
|
|
817
877
|
const result = await readback.first();
|
|
@@ -833,6 +893,7 @@ var SqlDriver = class {
|
|
|
833
893
|
if (row && typeof row === "object") {
|
|
834
894
|
this.injectTenantOnInsert(object, row, options);
|
|
835
895
|
await this.fillAutoNumberFields(object, row, options);
|
|
896
|
+
this.stampInsertTimestamps(object, row);
|
|
836
897
|
}
|
|
837
898
|
}
|
|
838
899
|
const builder = this.getBuilder(object, options);
|
|
@@ -1169,6 +1230,7 @@ var SqlDriver = class {
|
|
|
1169
1230
|
const numericCols = [];
|
|
1170
1231
|
const dateCols = [];
|
|
1171
1232
|
const datetimeCols = [];
|
|
1233
|
+
const timeCols = [];
|
|
1172
1234
|
const autoNumberCols = [];
|
|
1173
1235
|
const tenantField = this.computeTenantField(schema);
|
|
1174
1236
|
if (schema.fields) {
|
|
@@ -1179,6 +1241,7 @@ var SqlDriver = class {
|
|
|
1179
1241
|
if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) numericCols.push(name);
|
|
1180
1242
|
if (type === "date") dateCols.push(name);
|
|
1181
1243
|
if (type === "datetime") datetimeCols.push(name);
|
|
1244
|
+
if (type === "time") timeCols.push(name);
|
|
1182
1245
|
if (type === "auto_number" || type === "autonumber") {
|
|
1183
1246
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1184
1247
|
const fmt = rawFmt || "{0000}";
|
|
@@ -1193,9 +1256,10 @@ var SqlDriver = class {
|
|
|
1193
1256
|
this.tenantFieldByTable[key] = tenantField;
|
|
1194
1257
|
if (dateCols.length) this.dateFields[key] = new Set(dateCols);
|
|
1195
1258
|
if (datetimeCols.length) this.datetimeFields[key] = new Set(datetimeCols);
|
|
1259
|
+
if (timeCols.length) this.timeFields[key] = new Set(timeCols);
|
|
1196
1260
|
}
|
|
1197
1261
|
async initObjects(objects) {
|
|
1198
|
-
var _a, _b;
|
|
1262
|
+
var _a, _b, _c;
|
|
1199
1263
|
this.assertSchemaMutable("initObjects");
|
|
1200
1264
|
await this.ensureDatabaseExists();
|
|
1201
1265
|
for (const obj of objects) {
|
|
@@ -1227,6 +1291,9 @@ var SqlDriver = class {
|
|
|
1227
1291
|
if (type === "datetime") {
|
|
1228
1292
|
((_b = this.datetimeFields)[tableName] ?? (_b[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
1229
1293
|
}
|
|
1294
|
+
if (type === "time") {
|
|
1295
|
+
((_c = this.timeFields)[tableName] ?? (_c[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
1296
|
+
}
|
|
1230
1297
|
if (type === "auto_number" || type === "autonumber") {
|
|
1231
1298
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1232
1299
|
const fmt = rawFmt || "{0000}";
|
|
@@ -1712,8 +1779,7 @@ var SqlDriver = class {
|
|
|
1712
1779
|
}
|
|
1713
1780
|
isMultiTenantMode() {
|
|
1714
1781
|
if (this._multiTenantMode === void 0) {
|
|
1715
|
-
|
|
1716
|
-
this._multiTenantMode = String(raw).toLowerCase() !== "false";
|
|
1782
|
+
this._multiTenantMode = (0, import_types.resolveMultiOrgEnabled)();
|
|
1717
1783
|
}
|
|
1718
1784
|
return this._multiTenantMode;
|
|
1719
1785
|
}
|
|
@@ -1829,6 +1895,32 @@ var SqlDriver = class {
|
|
|
1829
1895
|
}
|
|
1830
1896
|
return value;
|
|
1831
1897
|
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Read-side repair for a `Field.time` value to its wall-clock time-of-day
|
|
1900
|
+
* (`Field.time` is a tz-naive time-of-day, not an instant — #2004). This is a
|
|
1901
|
+
* deliberately NARROW, read-only normalization (no write/filter counterpart):
|
|
1902
|
+
* it only strips a leading `YYYY-MM-DD` date — exactly what a legacy
|
|
1903
|
+
* `defaultValue: 'NOW()'` column took when the default was still the full
|
|
1904
|
+
* `CURRENT_TIMESTAMP` (or a full ISO datetime that leaked into the column) —
|
|
1905
|
+
* and any trailing zone, leaving the time portion. A value that is ALREADY a
|
|
1906
|
+
* bare time-of-day (`HH:MM[:SS[.fff]]`, with or without `Z`/offset) is returned
|
|
1907
|
+
* untouched, so the common case never changes and no write/read asymmetry is
|
|
1908
|
+
* introduced. A `Date`/epoch-ms (defensive — a Date bound to a time column)
|
|
1909
|
+
* maps to its UTC time-of-day. `null`/unrecognised shapes pass through.
|
|
1910
|
+
*/
|
|
1911
|
+
toTimeOnly(value) {
|
|
1912
|
+
if (value == null) return value;
|
|
1913
|
+
if (value instanceof Date) {
|
|
1914
|
+
return Number.isNaN(value.getTime()) ? value : value.toISOString().slice(11, 19);
|
|
1915
|
+
}
|
|
1916
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1917
|
+
const d = new Date(value);
|
|
1918
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString().slice(11, 19);
|
|
1919
|
+
}
|
|
1920
|
+
if (typeof value !== "string") return value;
|
|
1921
|
+
const m = /^\d{4}-\d{2}-\d{2}[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)(?:[Zz]|[+-]\d{2}:?\d{2})?$/.exec(value.trim());
|
|
1922
|
+
return m ? m[1] : value;
|
|
1923
|
+
}
|
|
1832
1924
|
/**
|
|
1833
1925
|
* Normalise a filter value for a single column so the comparison the
|
|
1834
1926
|
* driver sends to SQLite matches the on-disk representation.
|
|
@@ -2113,6 +2205,41 @@ var SqlDriver = class {
|
|
|
2113
2205
|
return sql;
|
|
2114
2206
|
}
|
|
2115
2207
|
// ── Column creation helper ──────────────────────────────────────────────────
|
|
2208
|
+
/**
|
|
2209
|
+
* The driver-native column DEFAULT for a `defaultValue: 'NOW()'` field.
|
|
2210
|
+
*
|
|
2211
|
+
* Postgres/MySQL use native `now()` — a real zone-aware TIMESTAMP that never
|
|
2212
|
+
* had the ambiguity below. SQLite has no timestamp type and `knex.fn.now()`
|
|
2213
|
+
* compiles to `CURRENT_TIMESTAMP`, which renders a timezone-NAIVE,
|
|
2214
|
+
* space-separated `'YYYY-MM-DD HH:MM:SS'` (no millis, no zone). `Date.parse`
|
|
2215
|
+
* reads such a zone-less string as LOCAL time, so a stored UTC wall-clock
|
|
2216
|
+
* shifts by the host offset on a non-UTC runtime — the same class of bug
|
|
2217
|
+
* ADR-0074 fixed for the builtin audit columns. Emit a canonical instead:
|
|
2218
|
+
* - datetime → ISO-8601 with explicit `Z` (`2026-06-26T10:34:13.891Z`),
|
|
2219
|
+
* matching `new Date().toISOString()` and the value
|
|
2220
|
+
* `formatInput`'s `NOW()` safety-net writes;
|
|
2221
|
+
* - date → `YYYY-MM-DD` UTC calendar day (matches `toDateOnly`, so the
|
|
2222
|
+
* stored default already equals what an explicit write stores);
|
|
2223
|
+
* - time → `HH:MM:SS.fff` UTC time-of-day (not a full timestamp).
|
|
2224
|
+
*
|
|
2225
|
+
* NOTE: a DDL default only governs NEWLY-created columns. An existing column
|
|
2226
|
+
* keeps its legacy `CURRENT_TIMESTAMP` default and still emits naive text on a
|
|
2227
|
+
* defaulted insert; `formatOutput` repairs those to canonical on read
|
|
2228
|
+
* (`normalizeSqliteDatetimeOutput` for datetime, `toDateOnly` for date), so
|
|
2229
|
+
* reads are uniform without a schema migration.
|
|
2230
|
+
*/
|
|
2231
|
+
nowColumnDefault(type) {
|
|
2232
|
+
if (!this.isSqlite) return this.knex.fn.now();
|
|
2233
|
+
switch (type) {
|
|
2234
|
+
case "date":
|
|
2235
|
+
return this.knex.raw("(strftime('%Y-%m-%d', 'now'))");
|
|
2236
|
+
case "time":
|
|
2237
|
+
return this.knex.raw("(strftime('%H:%M:%f', 'now'))");
|
|
2238
|
+
// datetime (and any non-temporal field that opts into NOW()): canonical instant.
|
|
2239
|
+
default:
|
|
2240
|
+
return this.knex.raw("(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2116
2243
|
createColumn(table, name, field) {
|
|
2117
2244
|
if (field.multiple) {
|
|
2118
2245
|
table.json(name);
|
|
@@ -2170,7 +2297,12 @@ var SqlDriver = class {
|
|
|
2170
2297
|
case "time":
|
|
2171
2298
|
col = table.time(name);
|
|
2172
2299
|
break;
|
|
2300
|
+
// `user` is a lookup specialized to sys_user (ADR: lookup → sys_user). Same
|
|
2301
|
+
// physical storage as any lookup: a string column holding the related row id
|
|
2302
|
+
// (multiple ⇒ JSON, handled at the top of createColumn). No bespoke storage
|
|
2303
|
+
// primitive — it shares this exact DDL path so reads/$expand/FK stay uniform.
|
|
2173
2304
|
case "lookup":
|
|
2305
|
+
case "user":
|
|
2174
2306
|
col = table.string(name);
|
|
2175
2307
|
if (field.reference_to) {
|
|
2176
2308
|
table.foreign(name).references("id").inTable(field.reference_to);
|
|
@@ -2192,12 +2324,12 @@ var SqlDriver = class {
|
|
|
2192
2324
|
if (col) {
|
|
2193
2325
|
if (field.unique) col.unique();
|
|
2194
2326
|
if (field.required) col.notNullable();
|
|
2195
|
-
if ((type === "datetime" || type === "date" || type === "time") &&
|
|
2196
|
-
col.defaultTo(this.
|
|
2327
|
+
if ((type === "datetime" || type === "date" || type === "time") && isNowDefaultValue(field.defaultValue)) {
|
|
2328
|
+
col.defaultTo(this.nowColumnDefault(type));
|
|
2197
2329
|
} else if (field.defaultValue !== void 0 && field.defaultValue !== null) {
|
|
2198
2330
|
const dv = field.defaultValue;
|
|
2199
|
-
if (
|
|
2200
|
-
col.defaultTo(this.
|
|
2331
|
+
if (isNowDefaultValue(dv)) {
|
|
2332
|
+
col.defaultTo(this.nowColumnDefault(type));
|
|
2201
2333
|
} else if (typeof dv !== "object") {
|
|
2202
2334
|
col.defaultTo(dv);
|
|
2203
2335
|
}
|
|
@@ -2371,6 +2503,17 @@ var SqlDriver = class {
|
|
|
2371
2503
|
}
|
|
2372
2504
|
}
|
|
2373
2505
|
}
|
|
2506
|
+
for (const col of AUDIT_TIMESTAMP_COLUMNS) {
|
|
2507
|
+
if (data[col] !== void 0) data[col] = repairNaiveUtcAuditTimestamp(data[col]);
|
|
2508
|
+
}
|
|
2509
|
+
const datetimeFields = this.datetimeFields[object];
|
|
2510
|
+
if (datetimeFields && datetimeFields.size > 0) {
|
|
2511
|
+
for (const field of datetimeFields) {
|
|
2512
|
+
if (data[field] !== void 0) {
|
|
2513
|
+
data[field] = normalizeSqliteDatetimeOutput(data[field]);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2374
2517
|
}
|
|
2375
2518
|
const dateFields = this.dateFields[object];
|
|
2376
2519
|
if (dateFields && dateFields.size > 0) {
|
|
@@ -2381,6 +2524,15 @@ var SqlDriver = class {
|
|
|
2381
2524
|
if (normalized !== v) data[field] = normalized;
|
|
2382
2525
|
}
|
|
2383
2526
|
}
|
|
2527
|
+
const timeFields = this.timeFields[object];
|
|
2528
|
+
if (timeFields && timeFields.size > 0) {
|
|
2529
|
+
for (const field of timeFields) {
|
|
2530
|
+
const v = data[field];
|
|
2531
|
+
if (v == null) continue;
|
|
2532
|
+
const normalized = this.toTimeOnly(v);
|
|
2533
|
+
if (normalized !== v) data[field] = normalized;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2384
2536
|
return data;
|
|
2385
2537
|
}
|
|
2386
2538
|
// ── Introspection internals ─────────────────────────────────────────────────
|