@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.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { parseAutonumberFormat, renderAutonumber, missingFieldValues } from "@objectstack/spec/data";
|
|
3
3
|
import { StorageNameMapping } from "@objectstack/spec/system";
|
|
4
4
|
import { ExternalSchemaModeViolationError } from "@objectstack/spec/shared";
|
|
5
|
+
import { resolveMultiOrgEnabled } from "@objectstack/types";
|
|
5
6
|
|
|
6
7
|
// src/schema-drift.ts
|
|
7
8
|
var BUILTIN_COLUMNS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
@@ -141,6 +142,39 @@ var NUMERIC_SCALAR_TYPES = /* @__PURE__ */ new Set([
|
|
|
141
142
|
"slider",
|
|
142
143
|
"progress"
|
|
143
144
|
]);
|
|
145
|
+
var AUDIT_TIMESTAMP_COLUMNS = ["created_at", "updated_at"];
|
|
146
|
+
function repairNaiveUtcAuditTimestamp(value) {
|
|
147
|
+
if (typeof value !== "string") return value;
|
|
148
|
+
const s = value.trim();
|
|
149
|
+
if (s === "") return value;
|
|
150
|
+
if (/[Zz]$/.test(s) || /[+-]\d{2}:?\d{2}$/.test(s)) return value;
|
|
151
|
+
const m = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/.exec(s);
|
|
152
|
+
if (!m) return value;
|
|
153
|
+
const d = /* @__PURE__ */ new Date(`${m[1]}T${m[2]}Z`);
|
|
154
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString();
|
|
155
|
+
}
|
|
156
|
+
function isNowDefaultValue(v) {
|
|
157
|
+
return typeof v === "string" && /^now\(\)$/i.test(v.trim());
|
|
158
|
+
}
|
|
159
|
+
function normalizeSqliteDatetimeOutput(value) {
|
|
160
|
+
if (value == null) return value;
|
|
161
|
+
if (typeof value === "number") {
|
|
162
|
+
if (!Number.isFinite(value)) return value;
|
|
163
|
+
const d = new Date(value);
|
|
164
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString();
|
|
165
|
+
}
|
|
166
|
+
if (value instanceof Date) {
|
|
167
|
+
return Number.isNaN(value.getTime()) ? value : value.toISOString();
|
|
168
|
+
}
|
|
169
|
+
if (typeof value !== "string") return value;
|
|
170
|
+
const s = value.trim();
|
|
171
|
+
if (s === "") return value;
|
|
172
|
+
if (/^-?\d+$/.test(s)) {
|
|
173
|
+
const d = new Date(Number(s));
|
|
174
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString();
|
|
175
|
+
}
|
|
176
|
+
return repairNaiveUtcAuditTimestamp(s);
|
|
177
|
+
}
|
|
144
178
|
var SqlDriver = class {
|
|
145
179
|
constructor(config) {
|
|
146
180
|
// IDataDriver metadata
|
|
@@ -151,6 +185,7 @@ var SqlDriver = class {
|
|
|
151
185
|
this.numericFields = {};
|
|
152
186
|
this.dateFields = {};
|
|
153
187
|
this.datetimeFields = {};
|
|
188
|
+
this.timeFields = {};
|
|
154
189
|
/**
|
|
155
190
|
* Federation read path (ADR-0015). For external objects whose physical
|
|
156
191
|
* remote table differs from the object name, these map between the two so
|
|
@@ -510,6 +545,7 @@ var SqlDriver = class {
|
|
|
510
545
|
await this.fillAutoNumberFields(object, toInsert, options);
|
|
511
546
|
const builder = this.getBuilder(object, options);
|
|
512
547
|
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toInsert));
|
|
548
|
+
this.stampInsertTimestamps(object, formatted);
|
|
513
549
|
const result = await builder.insert(formatted).returning("*");
|
|
514
550
|
return this.formatOutput(object, result[0]);
|
|
515
551
|
}
|
|
@@ -737,18 +773,39 @@ var SqlDriver = class {
|
|
|
737
773
|
row[cfg.name] = renderAutonumber({ tokens: cfg.tokens, seq: next, record: row, now, timezone }).value;
|
|
738
774
|
}
|
|
739
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Stamp the builtin audit timestamps to one canonical ISO-8601-with-`Z`
|
|
778
|
+
* instant on the SQLite write paths (`create`/`bulkCreate`/`upsert`), so
|
|
779
|
+
* INSERT and UPDATE agree on a single zone-explicit format.
|
|
780
|
+
*
|
|
781
|
+
* Without this, an insert that omits `created_at`/`updated_at` falls back to
|
|
782
|
+
* the column's `CURRENT_TIMESTAMP` default, which on SQLite renders a
|
|
783
|
+
* zone-NAIVE, space-separated `'YYYY-MM-DD HH:MM:SS'` (no millis, no zone) —
|
|
784
|
+
* the same ambiguity the old UPDATE stamp had. Stamping app-side (rather than
|
|
785
|
+
* changing the column default) fixes this for EXISTING tenant databases
|
|
786
|
+
* immediately, since their tables keep the legacy default. Legacy/raw rows
|
|
787
|
+
* still written zone-naive are repaired on read by
|
|
788
|
+
* `repairNaiveUtcAuditTimestamp`.
|
|
789
|
+
*
|
|
790
|
+
* Only fills a slot the caller left empty — an explicit value (a seed fixture,
|
|
791
|
+
* the sys_metadata writer, a service outbox) is preserved. No-op for
|
|
792
|
+
* timestamp-less objects and for Postgres/MySQL, whose native `now()` column
|
|
793
|
+
* default already stores a zone-aware TIMESTAMP.
|
|
794
|
+
*/
|
|
795
|
+
stampInsertTimestamps(object, formatted) {
|
|
796
|
+
if (!this.isSqlite || !this.tablesWithTimestamps.has(object)) return;
|
|
797
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
798
|
+
for (const col of AUDIT_TIMESTAMP_COLUMNS) {
|
|
799
|
+
if (formatted[col] === void 0 || formatted[col] === null) formatted[col] = iso;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
740
802
|
async update(object, id, data, options) {
|
|
741
803
|
this.auditMissingTenant(object, "update", options);
|
|
742
804
|
const builder = this.getBuilder(object, options).where("id", id);
|
|
743
805
|
this.applyTenantScope(builder, object, options);
|
|
744
806
|
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, data));
|
|
745
807
|
if (this.tablesWithTimestamps.has(object)) {
|
|
746
|
-
|
|
747
|
-
const now = /* @__PURE__ */ new Date();
|
|
748
|
-
formatted.updated_at = now.toISOString().replace("T", " ").replace("Z", "");
|
|
749
|
-
} else {
|
|
750
|
-
formatted.updated_at = this.knex.fn.now();
|
|
751
|
-
}
|
|
808
|
+
formatted.updated_at = this.isSqlite ? (/* @__PURE__ */ new Date()).toISOString() : this.knex.fn.now();
|
|
752
809
|
}
|
|
753
810
|
await builder.update(formatted);
|
|
754
811
|
const readback = this.getBuilder(object, options).where("id", id);
|
|
@@ -768,9 +825,12 @@ var SqlDriver = class {
|
|
|
768
825
|
this.injectTenantOnInsert(object, toUpsert, options);
|
|
769
826
|
await this.fillAutoNumberFields(object, toUpsert, options);
|
|
770
827
|
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toUpsert));
|
|
828
|
+
this.stampInsertTimestamps(object, formatted);
|
|
771
829
|
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
|
|
772
830
|
const builder = this.getBuilder(object, options);
|
|
773
|
-
|
|
831
|
+
const mergeColumns = Object.keys(formatted).filter((c) => c !== "created_at");
|
|
832
|
+
const insertion = builder.insert(formatted).onConflict(mergeKeys);
|
|
833
|
+
await (mergeColumns.length > 0 ? insertion.merge(mergeColumns) : insertion.merge());
|
|
774
834
|
const readback = this.getBuilder(object, options).where("id", toUpsert.id);
|
|
775
835
|
this.applyTenantScope(readback, object, options);
|
|
776
836
|
const result = await readback.first();
|
|
@@ -792,6 +852,7 @@ var SqlDriver = class {
|
|
|
792
852
|
if (row && typeof row === "object") {
|
|
793
853
|
this.injectTenantOnInsert(object, row, options);
|
|
794
854
|
await this.fillAutoNumberFields(object, row, options);
|
|
855
|
+
this.stampInsertTimestamps(object, row);
|
|
795
856
|
}
|
|
796
857
|
}
|
|
797
858
|
const builder = this.getBuilder(object, options);
|
|
@@ -1047,6 +1108,37 @@ var SqlDriver = class {
|
|
|
1047
1108
|
this.assertSchemaMutable("dropTable");
|
|
1048
1109
|
await this.knex.schema.dropTableIfExists(object);
|
|
1049
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Resolve the per-table tenant-isolation column for a schema, honoring an
|
|
1113
|
+
* explicit tenancy opt-out. Single source of truth for both {@link initObjects}
|
|
1114
|
+
* and {@link registerExternalObject} (they previously inlined this logic and
|
|
1115
|
+
* drifted).
|
|
1116
|
+
*
|
|
1117
|
+
* Precedence:
|
|
1118
|
+
* 1. `tenancy.enabled === false` → `null` (NO driver-level org scope), even
|
|
1119
|
+
* when the object carries an `organization_id` column. Platform-global
|
|
1120
|
+
* objects (e.g. `sys_license`) keep an optional, often-NULL org FK but must
|
|
1121
|
+
* NOT be tenant-scoped: otherwise an authenticated caller's active-org
|
|
1122
|
+
* `DriverOptions.tenantId` injects `WHERE organization_id = <org>` and every
|
|
1123
|
+
* NULL-org / cross-org row silently disappears (the platform admin then
|
|
1124
|
+
* reads zero licenses while an unscoped/anonymous read still sees them).
|
|
1125
|
+
* The declarative branch below already respected `enabled !== false`; the
|
|
1126
|
+
* implicit `organization_id` fallback did not — this closes that gap.
|
|
1127
|
+
* 2. Declared `tenancy.tenantField` (when that field exists on the object).
|
|
1128
|
+
* 3. Implicit `organization_id` column detection (legacy objects whose
|
|
1129
|
+
* multi-tenant column was injected by the kernel without a spec migration).
|
|
1130
|
+
*/
|
|
1131
|
+
computeTenantField(schema) {
|
|
1132
|
+
const tenancyDecl = schema?.tenancy;
|
|
1133
|
+
if (tenancyDecl?.enabled === false) return null;
|
|
1134
|
+
const fields = schema?.fields;
|
|
1135
|
+
if (tenancyDecl?.tenantField) {
|
|
1136
|
+
const declared = String(tenancyDecl.tenantField);
|
|
1137
|
+
if (fields && Object.prototype.hasOwnProperty.call(fields, declared)) return declared;
|
|
1138
|
+
}
|
|
1139
|
+
if (fields && Object.prototype.hasOwnProperty.call(fields, "organization_id")) return "organization_id";
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1050
1142
|
/**
|
|
1051
1143
|
* Batch-initialise tables from an array of object definitions.
|
|
1052
1144
|
*/
|
|
@@ -1097,19 +1189,9 @@ var SqlDriver = class {
|
|
|
1097
1189
|
const numericCols = [];
|
|
1098
1190
|
const dateCols = [];
|
|
1099
1191
|
const datetimeCols = [];
|
|
1192
|
+
const timeCols = [];
|
|
1100
1193
|
const autoNumberCols = [];
|
|
1101
|
-
const
|
|
1102
|
-
let tenantField = null;
|
|
1103
|
-
if (tenancyDecl && tenancyDecl.enabled !== false && tenancyDecl.tenantField) {
|
|
1104
|
-
const declared = String(tenancyDecl.tenantField);
|
|
1105
|
-
if (schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, declared)) {
|
|
1106
|
-
tenantField = declared;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
if (!tenantField) {
|
|
1110
|
-
const hasOrgField = !!(schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, "organization_id"));
|
|
1111
|
-
tenantField = hasOrgField ? "organization_id" : null;
|
|
1112
|
-
}
|
|
1194
|
+
const tenantField = this.computeTenantField(schema);
|
|
1113
1195
|
if (schema.fields) {
|
|
1114
1196
|
for (const [name, field] of Object.entries(schema.fields)) {
|
|
1115
1197
|
const type = field.type || "string";
|
|
@@ -1118,6 +1200,7 @@ var SqlDriver = class {
|
|
|
1118
1200
|
if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) numericCols.push(name);
|
|
1119
1201
|
if (type === "date") dateCols.push(name);
|
|
1120
1202
|
if (type === "datetime") datetimeCols.push(name);
|
|
1203
|
+
if (type === "time") timeCols.push(name);
|
|
1121
1204
|
if (type === "auto_number" || type === "autonumber") {
|
|
1122
1205
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1123
1206
|
const fmt = rawFmt || "{0000}";
|
|
@@ -1132,9 +1215,10 @@ var SqlDriver = class {
|
|
|
1132
1215
|
this.tenantFieldByTable[key] = tenantField;
|
|
1133
1216
|
if (dateCols.length) this.dateFields[key] = new Set(dateCols);
|
|
1134
1217
|
if (datetimeCols.length) this.datetimeFields[key] = new Set(datetimeCols);
|
|
1218
|
+
if (timeCols.length) this.timeFields[key] = new Set(timeCols);
|
|
1135
1219
|
}
|
|
1136
1220
|
async initObjects(objects) {
|
|
1137
|
-
var _a, _b;
|
|
1221
|
+
var _a, _b, _c;
|
|
1138
1222
|
this.assertSchemaMutable("initObjects");
|
|
1139
1223
|
await this.ensureDatabaseExists();
|
|
1140
1224
|
for (const obj of objects) {
|
|
@@ -1147,18 +1231,7 @@ var SqlDriver = class {
|
|
|
1147
1231
|
const booleanCols = [];
|
|
1148
1232
|
const numericCols = [];
|
|
1149
1233
|
const autoNumberCols = [];
|
|
1150
|
-
const
|
|
1151
|
-
let tenantField = null;
|
|
1152
|
-
if (tenancyDecl && tenancyDecl.enabled !== false && tenancyDecl.tenantField) {
|
|
1153
|
-
const declared = String(tenancyDecl.tenantField);
|
|
1154
|
-
if (obj.fields && Object.prototype.hasOwnProperty.call(obj.fields, declared)) {
|
|
1155
|
-
tenantField = declared;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
if (!tenantField) {
|
|
1159
|
-
const hasOrgField = !!(obj.fields && Object.prototype.hasOwnProperty.call(obj.fields, "organization_id"));
|
|
1160
|
-
tenantField = hasOrgField ? "organization_id" : null;
|
|
1161
|
-
}
|
|
1234
|
+
const tenantField = this.computeTenantField(obj);
|
|
1162
1235
|
if (obj.fields) {
|
|
1163
1236
|
for (const [name, field] of Object.entries(obj.fields)) {
|
|
1164
1237
|
const type = field.type || "string";
|
|
@@ -1177,6 +1250,9 @@ var SqlDriver = class {
|
|
|
1177
1250
|
if (type === "datetime") {
|
|
1178
1251
|
((_b = this.datetimeFields)[tableName] ?? (_b[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
1179
1252
|
}
|
|
1253
|
+
if (type === "time") {
|
|
1254
|
+
((_c = this.timeFields)[tableName] ?? (_c[tableName] = /* @__PURE__ */ new Set())).add(name);
|
|
1255
|
+
}
|
|
1180
1256
|
if (type === "auto_number" || type === "autonumber") {
|
|
1181
1257
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1182
1258
|
const fmt = rawFmt || "{0000}";
|
|
@@ -1662,8 +1738,7 @@ var SqlDriver = class {
|
|
|
1662
1738
|
}
|
|
1663
1739
|
isMultiTenantMode() {
|
|
1664
1740
|
if (this._multiTenantMode === void 0) {
|
|
1665
|
-
|
|
1666
|
-
this._multiTenantMode = String(raw).toLowerCase() !== "false";
|
|
1741
|
+
this._multiTenantMode = resolveMultiOrgEnabled();
|
|
1667
1742
|
}
|
|
1668
1743
|
return this._multiTenantMode;
|
|
1669
1744
|
}
|
|
@@ -1779,6 +1854,32 @@ var SqlDriver = class {
|
|
|
1779
1854
|
}
|
|
1780
1855
|
return value;
|
|
1781
1856
|
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Read-side repair for a `Field.time` value to its wall-clock time-of-day
|
|
1859
|
+
* (`Field.time` is a tz-naive time-of-day, not an instant — #2004). This is a
|
|
1860
|
+
* deliberately NARROW, read-only normalization (no write/filter counterpart):
|
|
1861
|
+
* it only strips a leading `YYYY-MM-DD` date — exactly what a legacy
|
|
1862
|
+
* `defaultValue: 'NOW()'` column took when the default was still the full
|
|
1863
|
+
* `CURRENT_TIMESTAMP` (or a full ISO datetime that leaked into the column) —
|
|
1864
|
+
* and any trailing zone, leaving the time portion. A value that is ALREADY a
|
|
1865
|
+
* bare time-of-day (`HH:MM[:SS[.fff]]`, with or without `Z`/offset) is returned
|
|
1866
|
+
* untouched, so the common case never changes and no write/read asymmetry is
|
|
1867
|
+
* introduced. A `Date`/epoch-ms (defensive — a Date bound to a time column)
|
|
1868
|
+
* maps to its UTC time-of-day. `null`/unrecognised shapes pass through.
|
|
1869
|
+
*/
|
|
1870
|
+
toTimeOnly(value) {
|
|
1871
|
+
if (value == null) return value;
|
|
1872
|
+
if (value instanceof Date) {
|
|
1873
|
+
return Number.isNaN(value.getTime()) ? value : value.toISOString().slice(11, 19);
|
|
1874
|
+
}
|
|
1875
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1876
|
+
const d = new Date(value);
|
|
1877
|
+
return Number.isNaN(d.getTime()) ? value : d.toISOString().slice(11, 19);
|
|
1878
|
+
}
|
|
1879
|
+
if (typeof value !== "string") return value;
|
|
1880
|
+
const m = /^\d{4}-\d{2}-\d{2}[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)(?:[Zz]|[+-]\d{2}:?\d{2})?$/.exec(value.trim());
|
|
1881
|
+
return m ? m[1] : value;
|
|
1882
|
+
}
|
|
1782
1883
|
/**
|
|
1783
1884
|
* Normalise a filter value for a single column so the comparison the
|
|
1784
1885
|
* driver sends to SQLite matches the on-disk representation.
|
|
@@ -2063,6 +2164,41 @@ var SqlDriver = class {
|
|
|
2063
2164
|
return sql;
|
|
2064
2165
|
}
|
|
2065
2166
|
// ── Column creation helper ──────────────────────────────────────────────────
|
|
2167
|
+
/**
|
|
2168
|
+
* The driver-native column DEFAULT for a `defaultValue: 'NOW()'` field.
|
|
2169
|
+
*
|
|
2170
|
+
* Postgres/MySQL use native `now()` — a real zone-aware TIMESTAMP that never
|
|
2171
|
+
* had the ambiguity below. SQLite has no timestamp type and `knex.fn.now()`
|
|
2172
|
+
* compiles to `CURRENT_TIMESTAMP`, which renders a timezone-NAIVE,
|
|
2173
|
+
* space-separated `'YYYY-MM-DD HH:MM:SS'` (no millis, no zone). `Date.parse`
|
|
2174
|
+
* reads such a zone-less string as LOCAL time, so a stored UTC wall-clock
|
|
2175
|
+
* shifts by the host offset on a non-UTC runtime — the same class of bug
|
|
2176
|
+
* ADR-0074 fixed for the builtin audit columns. Emit a canonical instead:
|
|
2177
|
+
* - datetime → ISO-8601 with explicit `Z` (`2026-06-26T10:34:13.891Z`),
|
|
2178
|
+
* matching `new Date().toISOString()` and the value
|
|
2179
|
+
* `formatInput`'s `NOW()` safety-net writes;
|
|
2180
|
+
* - date → `YYYY-MM-DD` UTC calendar day (matches `toDateOnly`, so the
|
|
2181
|
+
* stored default already equals what an explicit write stores);
|
|
2182
|
+
* - time → `HH:MM:SS.fff` UTC time-of-day (not a full timestamp).
|
|
2183
|
+
*
|
|
2184
|
+
* NOTE: a DDL default only governs NEWLY-created columns. An existing column
|
|
2185
|
+
* keeps its legacy `CURRENT_TIMESTAMP` default and still emits naive text on a
|
|
2186
|
+
* defaulted insert; `formatOutput` repairs those to canonical on read
|
|
2187
|
+
* (`normalizeSqliteDatetimeOutput` for datetime, `toDateOnly` for date), so
|
|
2188
|
+
* reads are uniform without a schema migration.
|
|
2189
|
+
*/
|
|
2190
|
+
nowColumnDefault(type) {
|
|
2191
|
+
if (!this.isSqlite) return this.knex.fn.now();
|
|
2192
|
+
switch (type) {
|
|
2193
|
+
case "date":
|
|
2194
|
+
return this.knex.raw("(strftime('%Y-%m-%d', 'now'))");
|
|
2195
|
+
case "time":
|
|
2196
|
+
return this.knex.raw("(strftime('%H:%M:%f', 'now'))");
|
|
2197
|
+
// datetime (and any non-temporal field that opts into NOW()): canonical instant.
|
|
2198
|
+
default:
|
|
2199
|
+
return this.knex.raw("(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))");
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2066
2202
|
createColumn(table, name, field) {
|
|
2067
2203
|
if (field.multiple) {
|
|
2068
2204
|
table.json(name);
|
|
@@ -2120,7 +2256,12 @@ var SqlDriver = class {
|
|
|
2120
2256
|
case "time":
|
|
2121
2257
|
col = table.time(name);
|
|
2122
2258
|
break;
|
|
2259
|
+
// `user` is a lookup specialized to sys_user (ADR: lookup → sys_user). Same
|
|
2260
|
+
// physical storage as any lookup: a string column holding the related row id
|
|
2261
|
+
// (multiple ⇒ JSON, handled at the top of createColumn). No bespoke storage
|
|
2262
|
+
// primitive — it shares this exact DDL path so reads/$expand/FK stay uniform.
|
|
2123
2263
|
case "lookup":
|
|
2264
|
+
case "user":
|
|
2124
2265
|
col = table.string(name);
|
|
2125
2266
|
if (field.reference_to) {
|
|
2126
2267
|
table.foreign(name).references("id").inTable(field.reference_to);
|
|
@@ -2142,12 +2283,12 @@ var SqlDriver = class {
|
|
|
2142
2283
|
if (col) {
|
|
2143
2284
|
if (field.unique) col.unique();
|
|
2144
2285
|
if (field.required) col.notNullable();
|
|
2145
|
-
if ((type === "datetime" || type === "date" || type === "time") &&
|
|
2146
|
-
col.defaultTo(this.
|
|
2286
|
+
if ((type === "datetime" || type === "date" || type === "time") && isNowDefaultValue(field.defaultValue)) {
|
|
2287
|
+
col.defaultTo(this.nowColumnDefault(type));
|
|
2147
2288
|
} else if (field.defaultValue !== void 0 && field.defaultValue !== null) {
|
|
2148
2289
|
const dv = field.defaultValue;
|
|
2149
|
-
if (
|
|
2150
|
-
col.defaultTo(this.
|
|
2290
|
+
if (isNowDefaultValue(dv)) {
|
|
2291
|
+
col.defaultTo(this.nowColumnDefault(type));
|
|
2151
2292
|
} else if (typeof dv !== "object") {
|
|
2152
2293
|
col.defaultTo(dv);
|
|
2153
2294
|
}
|
|
@@ -2321,6 +2462,17 @@ var SqlDriver = class {
|
|
|
2321
2462
|
}
|
|
2322
2463
|
}
|
|
2323
2464
|
}
|
|
2465
|
+
for (const col of AUDIT_TIMESTAMP_COLUMNS) {
|
|
2466
|
+
if (data[col] !== void 0) data[col] = repairNaiveUtcAuditTimestamp(data[col]);
|
|
2467
|
+
}
|
|
2468
|
+
const datetimeFields = this.datetimeFields[object];
|
|
2469
|
+
if (datetimeFields && datetimeFields.size > 0) {
|
|
2470
|
+
for (const field of datetimeFields) {
|
|
2471
|
+
if (data[field] !== void 0) {
|
|
2472
|
+
data[field] = normalizeSqliteDatetimeOutput(data[field]);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2324
2476
|
}
|
|
2325
2477
|
const dateFields = this.dateFields[object];
|
|
2326
2478
|
if (dateFields && dateFields.size > 0) {
|
|
@@ -2331,6 +2483,15 @@ var SqlDriver = class {
|
|
|
2331
2483
|
if (normalized !== v) data[field] = normalized;
|
|
2332
2484
|
}
|
|
2333
2485
|
}
|
|
2486
|
+
const timeFields = this.timeFields[object];
|
|
2487
|
+
if (timeFields && timeFields.size > 0) {
|
|
2488
|
+
for (const field of timeFields) {
|
|
2489
|
+
const v = data[field];
|
|
2490
|
+
if (v == null) continue;
|
|
2491
|
+
const normalized = this.toTimeOnly(v);
|
|
2492
|
+
if (normalized !== v) data[field] = normalized;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2334
2495
|
return data;
|
|
2335
2496
|
}
|
|
2336
2497
|
// ── Introspection internals ─────────────────────────────────────────────────
|