@objectstack/driver-sql 10.2.0 → 11.0.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 +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +199 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +199 -38
- 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
|
|
@@ -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);
|
|
@@ -1088,6 +1149,37 @@ var SqlDriver = class {
|
|
|
1088
1149
|
this.assertSchemaMutable("dropTable");
|
|
1089
1150
|
await this.knex.schema.dropTableIfExists(object);
|
|
1090
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Resolve the per-table tenant-isolation column for a schema, honoring an
|
|
1154
|
+
* explicit tenancy opt-out. Single source of truth for both {@link initObjects}
|
|
1155
|
+
* and {@link registerExternalObject} (they previously inlined this logic and
|
|
1156
|
+
* drifted).
|
|
1157
|
+
*
|
|
1158
|
+
* Precedence:
|
|
1159
|
+
* 1. `tenancy.enabled === false` → `null` (NO driver-level org scope), even
|
|
1160
|
+
* when the object carries an `organization_id` column. Platform-global
|
|
1161
|
+
* objects (e.g. `sys_license`) keep an optional, often-NULL org FK but must
|
|
1162
|
+
* NOT be tenant-scoped: otherwise an authenticated caller's active-org
|
|
1163
|
+
* `DriverOptions.tenantId` injects `WHERE organization_id = <org>` and every
|
|
1164
|
+
* NULL-org / cross-org row silently disappears (the platform admin then
|
|
1165
|
+
* reads zero licenses while an unscoped/anonymous read still sees them).
|
|
1166
|
+
* The declarative branch below already respected `enabled !== false`; the
|
|
1167
|
+
* implicit `organization_id` fallback did not — this closes that gap.
|
|
1168
|
+
* 2. Declared `tenancy.tenantField` (when that field exists on the object).
|
|
1169
|
+
* 3. Implicit `organization_id` column detection (legacy objects whose
|
|
1170
|
+
* multi-tenant column was injected by the kernel without a spec migration).
|
|
1171
|
+
*/
|
|
1172
|
+
computeTenantField(schema) {
|
|
1173
|
+
const tenancyDecl = schema?.tenancy;
|
|
1174
|
+
if (tenancyDecl?.enabled === false) return null;
|
|
1175
|
+
const fields = schema?.fields;
|
|
1176
|
+
if (tenancyDecl?.tenantField) {
|
|
1177
|
+
const declared = String(tenancyDecl.tenantField);
|
|
1178
|
+
if (fields && Object.prototype.hasOwnProperty.call(fields, declared)) return declared;
|
|
1179
|
+
}
|
|
1180
|
+
if (fields && Object.prototype.hasOwnProperty.call(fields, "organization_id")) return "organization_id";
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1091
1183
|
/**
|
|
1092
1184
|
* Batch-initialise tables from an array of object definitions.
|
|
1093
1185
|
*/
|
|
@@ -1138,19 +1230,9 @@ var SqlDriver = class {
|
|
|
1138
1230
|
const numericCols = [];
|
|
1139
1231
|
const dateCols = [];
|
|
1140
1232
|
const datetimeCols = [];
|
|
1233
|
+
const timeCols = [];
|
|
1141
1234
|
const autoNumberCols = [];
|
|
1142
|
-
const
|
|
1143
|
-
let tenantField = null;
|
|
1144
|
-
if (tenancyDecl && tenancyDecl.enabled !== false && tenancyDecl.tenantField) {
|
|
1145
|
-
const declared = String(tenancyDecl.tenantField);
|
|
1146
|
-
if (schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, declared)) {
|
|
1147
|
-
tenantField = declared;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
if (!tenantField) {
|
|
1151
|
-
const hasOrgField = !!(schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, "organization_id"));
|
|
1152
|
-
tenantField = hasOrgField ? "organization_id" : null;
|
|
1153
|
-
}
|
|
1235
|
+
const tenantField = this.computeTenantField(schema);
|
|
1154
1236
|
if (schema.fields) {
|
|
1155
1237
|
for (const [name, field] of Object.entries(schema.fields)) {
|
|
1156
1238
|
const type = field.type || "string";
|
|
@@ -1159,6 +1241,7 @@ var SqlDriver = class {
|
|
|
1159
1241
|
if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) numericCols.push(name);
|
|
1160
1242
|
if (type === "date") dateCols.push(name);
|
|
1161
1243
|
if (type === "datetime") datetimeCols.push(name);
|
|
1244
|
+
if (type === "time") timeCols.push(name);
|
|
1162
1245
|
if (type === "auto_number" || type === "autonumber") {
|
|
1163
1246
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1164
1247
|
const fmt = rawFmt || "{0000}";
|
|
@@ -1173,9 +1256,10 @@ var SqlDriver = class {
|
|
|
1173
1256
|
this.tenantFieldByTable[key] = tenantField;
|
|
1174
1257
|
if (dateCols.length) this.dateFields[key] = new Set(dateCols);
|
|
1175
1258
|
if (datetimeCols.length) this.datetimeFields[key] = new Set(datetimeCols);
|
|
1259
|
+
if (timeCols.length) this.timeFields[key] = new Set(timeCols);
|
|
1176
1260
|
}
|
|
1177
1261
|
async initObjects(objects) {
|
|
1178
|
-
var _a, _b;
|
|
1262
|
+
var _a, _b, _c;
|
|
1179
1263
|
this.assertSchemaMutable("initObjects");
|
|
1180
1264
|
await this.ensureDatabaseExists();
|
|
1181
1265
|
for (const obj of objects) {
|
|
@@ -1188,18 +1272,7 @@ var SqlDriver = class {
|
|
|
1188
1272
|
const booleanCols = [];
|
|
1189
1273
|
const numericCols = [];
|
|
1190
1274
|
const autoNumberCols = [];
|
|
1191
|
-
const
|
|
1192
|
-
let tenantField = null;
|
|
1193
|
-
if (tenancyDecl && tenancyDecl.enabled !== false && tenancyDecl.tenantField) {
|
|
1194
|
-
const declared = String(tenancyDecl.tenantField);
|
|
1195
|
-
if (obj.fields && Object.prototype.hasOwnProperty.call(obj.fields, declared)) {
|
|
1196
|
-
tenantField = declared;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
if (!tenantField) {
|
|
1200
|
-
const hasOrgField = !!(obj.fields && Object.prototype.hasOwnProperty.call(obj.fields, "organization_id"));
|
|
1201
|
-
tenantField = hasOrgField ? "organization_id" : null;
|
|
1202
|
-
}
|
|
1275
|
+
const tenantField = this.computeTenantField(obj);
|
|
1203
1276
|
if (obj.fields) {
|
|
1204
1277
|
for (const [name, field] of Object.entries(obj.fields)) {
|
|
1205
1278
|
const type = field.type || "string";
|
|
@@ -1218,6 +1291,9 @@ var SqlDriver = class {
|
|
|
1218
1291
|
if (type === "datetime") {
|
|
1219
1292
|
((_b = this.datetimeFields)[tableName] ?? (_b[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
1220
1293
|
}
|
|
1294
|
+
if (type === "time") {
|
|
1295
|
+
((_c = this.timeFields)[tableName] ?? (_c[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
1296
|
+
}
|
|
1221
1297
|
if (type === "auto_number" || type === "autonumber") {
|
|
1222
1298
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1223
1299
|
const fmt = rawFmt || "{0000}";
|
|
@@ -1703,8 +1779,7 @@ var SqlDriver = class {
|
|
|
1703
1779
|
}
|
|
1704
1780
|
isMultiTenantMode() {
|
|
1705
1781
|
if (this._multiTenantMode === void 0) {
|
|
1706
|
-
|
|
1707
|
-
this._multiTenantMode = String(raw).toLowerCase() !== "false";
|
|
1782
|
+
this._multiTenantMode = (0, import_types.resolveMultiOrgEnabled)();
|
|
1708
1783
|
}
|
|
1709
1784
|
return this._multiTenantMode;
|
|
1710
1785
|
}
|
|
@@ -1820,6 +1895,32 @@ var SqlDriver = class {
|
|
|
1820
1895
|
}
|
|
1821
1896
|
return value;
|
|
1822
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
|
+
}
|
|
1823
1924
|
/**
|
|
1824
1925
|
* Normalise a filter value for a single column so the comparison the
|
|
1825
1926
|
* driver sends to SQLite matches the on-disk representation.
|
|
@@ -2104,6 +2205,41 @@ var SqlDriver = class {
|
|
|
2104
2205
|
return sql;
|
|
2105
2206
|
}
|
|
2106
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
|
+
}
|
|
2107
2243
|
createColumn(table, name, field) {
|
|
2108
2244
|
if (field.multiple) {
|
|
2109
2245
|
table.json(name);
|
|
@@ -2161,7 +2297,12 @@ var SqlDriver = class {
|
|
|
2161
2297
|
case "time":
|
|
2162
2298
|
col = table.time(name);
|
|
2163
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.
|
|
2164
2304
|
case "lookup":
|
|
2305
|
+
case "user":
|
|
2165
2306
|
col = table.string(name);
|
|
2166
2307
|
if (field.reference_to) {
|
|
2167
2308
|
table.foreign(name).references("id").inTable(field.reference_to);
|
|
@@ -2183,12 +2324,12 @@ var SqlDriver = class {
|
|
|
2183
2324
|
if (col) {
|
|
2184
2325
|
if (field.unique) col.unique();
|
|
2185
2326
|
if (field.required) col.notNullable();
|
|
2186
|
-
if ((type === "datetime" || type === "date" || type === "time") &&
|
|
2187
|
-
col.defaultTo(this.
|
|
2327
|
+
if ((type === "datetime" || type === "date" || type === "time") && isNowDefaultValue(field.defaultValue)) {
|
|
2328
|
+
col.defaultTo(this.nowColumnDefault(type));
|
|
2188
2329
|
} else if (field.defaultValue !== void 0 && field.defaultValue !== null) {
|
|
2189
2330
|
const dv = field.defaultValue;
|
|
2190
|
-
if (
|
|
2191
|
-
col.defaultTo(this.
|
|
2331
|
+
if (isNowDefaultValue(dv)) {
|
|
2332
|
+
col.defaultTo(this.nowColumnDefault(type));
|
|
2192
2333
|
} else if (typeof dv !== "object") {
|
|
2193
2334
|
col.defaultTo(dv);
|
|
2194
2335
|
}
|
|
@@ -2362,6 +2503,17 @@ var SqlDriver = class {
|
|
|
2362
2503
|
}
|
|
2363
2504
|
}
|
|
2364
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
|
+
}
|
|
2365
2517
|
}
|
|
2366
2518
|
const dateFields = this.dateFields[object];
|
|
2367
2519
|
if (dateFields && dateFields.size > 0) {
|
|
@@ -2372,6 +2524,15 @@ var SqlDriver = class {
|
|
|
2372
2524
|
if (normalized !== v) data[field] = normalized;
|
|
2373
2525
|
}
|
|
2374
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
|
+
}
|
|
2375
2536
|
return data;
|
|
2376
2537
|
}
|
|
2377
2538
|
// ── Introspection internals ─────────────────────────────────────────────────
|