@objectstack/driver-sql 9.11.0 → 10.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/dist/index.d.mts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +173 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +173 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
package/dist/index.mjs
CHANGED
|
@@ -49,6 +49,20 @@ var SqlDriver = class {
|
|
|
49
49
|
this.numericFields = {};
|
|
50
50
|
this.dateFields = {};
|
|
51
51
|
this.datetimeFields = {};
|
|
52
|
+
/**
|
|
53
|
+
* Federation read path (ADR-0015). For external objects whose physical
|
|
54
|
+
* remote table differs from the object name, these map between the two so
|
|
55
|
+
* {@link getBuilder} targets the remote table while the coercion maps above
|
|
56
|
+
* stay keyed by OBJECT name (matching formatInput/formatOutput). Empty for
|
|
57
|
+
* managed objects, so the managed query path is unchanged.
|
|
58
|
+
*/
|
|
59
|
+
this.physicalTableByObject = {};
|
|
60
|
+
this.physicalSchemaByObject = {};
|
|
61
|
+
this.objectByPhysicalTable = {};
|
|
62
|
+
/** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
|
|
63
|
+
this.fieldColumnByObject = {};
|
|
64
|
+
/** External columnMap inverse: physical remote column -> logical field (for read output remap). */
|
|
65
|
+
this.columnFieldByObject = {};
|
|
52
66
|
this.tablesWithTimestamps = /* @__PURE__ */ new Set();
|
|
53
67
|
/**
|
|
54
68
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -273,6 +287,13 @@ var SqlDriver = class {
|
|
|
273
287
|
// ===================================
|
|
274
288
|
async connect() {
|
|
275
289
|
await this.ensureDatabaseExists();
|
|
290
|
+
if (this.isSqlite) {
|
|
291
|
+
try {
|
|
292
|
+
await this.knex.raw("PRAGMA auto_vacuum = INCREMENTAL");
|
|
293
|
+
} catch (e) {
|
|
294
|
+
this.logger.warn("Failed to set PRAGMA auto_vacuum=INCREMENTAL", e);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
276
297
|
}
|
|
277
298
|
async checkHealth() {
|
|
278
299
|
try {
|
|
@@ -298,7 +319,7 @@ var SqlDriver = class {
|
|
|
298
319
|
if (query.orderBy && Array.isArray(query.orderBy)) {
|
|
299
320
|
for (const item of query.orderBy) {
|
|
300
321
|
if (item.field) {
|
|
301
|
-
b.orderBy(this.mapSortField(item.field), item.order || "asc");
|
|
322
|
+
b.orderBy(this.remoteColumn(object, item.field, this.mapSortField(item.field)), item.order || "asc");
|
|
302
323
|
}
|
|
303
324
|
}
|
|
304
325
|
}
|
|
@@ -375,7 +396,7 @@ var SqlDriver = class {
|
|
|
375
396
|
this.injectTenantOnInsert(object, toInsert, options);
|
|
376
397
|
await this.fillAutoNumberFields(object, toInsert, options);
|
|
377
398
|
const builder = this.getBuilder(object, options);
|
|
378
|
-
const formatted = this.formatInput(object, toInsert);
|
|
399
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toInsert));
|
|
379
400
|
const result = await builder.insert(formatted).returning("*");
|
|
380
401
|
return this.formatOutput(object, result[0]);
|
|
381
402
|
}
|
|
@@ -572,8 +593,8 @@ var SqlDriver = class {
|
|
|
572
593
|
* tenant scopes it for isolation.
|
|
573
594
|
*/
|
|
574
595
|
async fillAutoNumberFields(object, row, options) {
|
|
575
|
-
const tableName = StorageNameMapping.resolveTableName({ name: object });
|
|
576
|
-
const cfgs = this.autoNumberFields[
|
|
596
|
+
const tableName = this.physicalTableByObject[object] ?? StorageNameMapping.resolveTableName({ name: object });
|
|
597
|
+
const cfgs = this.autoNumberFields[object] || this.autoNumberFields[tableName];
|
|
577
598
|
if (!cfgs || cfgs.length === 0) return;
|
|
578
599
|
const parentTrx = options?.transaction;
|
|
579
600
|
const timezone = options?.timezone;
|
|
@@ -607,7 +628,7 @@ var SqlDriver = class {
|
|
|
607
628
|
this.auditMissingTenant(object, "update", options);
|
|
608
629
|
const builder = this.getBuilder(object, options).where("id", id);
|
|
609
630
|
this.applyTenantScope(builder, object, options);
|
|
610
|
-
const formatted = this.formatInput(object, data);
|
|
631
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, data));
|
|
611
632
|
if (this.tablesWithTimestamps.has(object)) {
|
|
612
633
|
if (this.isSqlite) {
|
|
613
634
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -633,7 +654,7 @@ var SqlDriver = class {
|
|
|
633
654
|
this.auditMissingTenant(object, "upsert", options);
|
|
634
655
|
this.injectTenantOnInsert(object, toUpsert, options);
|
|
635
656
|
await this.fillAutoNumberFields(object, toUpsert, options);
|
|
636
|
-
const formatted = this.formatInput(object, toUpsert);
|
|
657
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toUpsert));
|
|
637
658
|
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
|
|
638
659
|
const builder = this.getBuilder(object, options);
|
|
639
660
|
await builder.insert(formatted).onConflict(mergeKeys).merge();
|
|
@@ -916,6 +937,89 @@ var SqlDriver = class {
|
|
|
916
937
|
/**
|
|
917
938
|
* Batch-initialise tables from an array of object definitions.
|
|
918
939
|
*/
|
|
940
|
+
/**
|
|
941
|
+
* DDL-free metadata registration for a federated (external) object — the
|
|
942
|
+
* read-path counterpart to {@link initObjects} (ADR-0015 federation).
|
|
943
|
+
*
|
|
944
|
+
* `initObjects` is gated by `assertSchemaMutable` and therefore throws for
|
|
945
|
+
* any non-`managed` driver, which left external objects with NO read-coercion
|
|
946
|
+
* metadata and the query path resolving to a table named after the object
|
|
947
|
+
* instead of its remote table. This populates the same coercion maps (keyed
|
|
948
|
+
* by OBJECT name, matching formatInput/formatOutput/coerceFilterValue) and
|
|
949
|
+
* records the physical remote table (`external.remoteName`, optionally
|
|
950
|
+
* `external.remoteSchema`) so {@link getBuilder} targets it — WITHOUT running
|
|
951
|
+
* any DDL (createTable/alterTable/columnInfo). Keep the field-classification
|
|
952
|
+
* below in sync with initObjects() if the field-type -> storage mapping changes.
|
|
953
|
+
*/
|
|
954
|
+
registerExternalObject(schema) {
|
|
955
|
+
const key = schema.name;
|
|
956
|
+
const remoteName = schema.external?.remoteName || schema.name;
|
|
957
|
+
const remoteSchema = schema.external?.remoteSchema;
|
|
958
|
+
this.physicalTableByObject[key] = remoteName;
|
|
959
|
+
this.objectByPhysicalTable[remoteName] = key;
|
|
960
|
+
if (remoteSchema) {
|
|
961
|
+
if (this.isSqlite) {
|
|
962
|
+
this.logger.warn(
|
|
963
|
+
`[sql-driver] external object "${key}" declares remoteSchema="${remoteSchema}" but SQLite has no schema namespace; ignoring (treating "${remoteName}" as a bare table).`
|
|
964
|
+
);
|
|
965
|
+
} else {
|
|
966
|
+
this.physicalSchemaByObject[key] = remoteSchema;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const columnMap = schema.external?.columnMap;
|
|
970
|
+
if (columnMap && typeof columnMap === "object" && Object.keys(columnMap).length > 0) {
|
|
971
|
+
const fieldToCol = {};
|
|
972
|
+
const colToField = {};
|
|
973
|
+
for (const [remoteCol, localField] of Object.entries(columnMap)) {
|
|
974
|
+
if (typeof localField === "string" && localField) {
|
|
975
|
+
fieldToCol[localField] = remoteCol;
|
|
976
|
+
colToField[remoteCol] = localField;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
this.fieldColumnByObject[key] = fieldToCol;
|
|
980
|
+
this.columnFieldByObject[key] = colToField;
|
|
981
|
+
}
|
|
982
|
+
const jsonCols = [];
|
|
983
|
+
const booleanCols = [];
|
|
984
|
+
const numericCols = [];
|
|
985
|
+
const dateCols = [];
|
|
986
|
+
const datetimeCols = [];
|
|
987
|
+
const autoNumberCols = [];
|
|
988
|
+
const tenancyDecl = schema?.tenancy;
|
|
989
|
+
let tenantField = null;
|
|
990
|
+
if (tenancyDecl && tenancyDecl.enabled !== false && tenancyDecl.tenantField) {
|
|
991
|
+
const declared = String(tenancyDecl.tenantField);
|
|
992
|
+
if (schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, declared)) {
|
|
993
|
+
tenantField = declared;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (!tenantField) {
|
|
997
|
+
const hasOrgField = !!(schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, "organization_id"));
|
|
998
|
+
tenantField = hasOrgField ? "organization_id" : null;
|
|
999
|
+
}
|
|
1000
|
+
if (schema.fields) {
|
|
1001
|
+
for (const [name, field] of Object.entries(schema.fields)) {
|
|
1002
|
+
const type = field.type || "string";
|
|
1003
|
+
if (this.isJsonField(type, field)) jsonCols.push(name);
|
|
1004
|
+
if (type === "boolean" || type === "toggle") booleanCols.push(name);
|
|
1005
|
+
if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) numericCols.push(name);
|
|
1006
|
+
if (type === "date") dateCols.push(name);
|
|
1007
|
+
if (type === "datetime") datetimeCols.push(name);
|
|
1008
|
+
if (type === "auto_number" || type === "autonumber") {
|
|
1009
|
+
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1010
|
+
const fmt = rawFmt || "{0000}";
|
|
1011
|
+
autoNumberCols.push({ name, format: fmt, tokens: parseAutonumberFormat(fmt), tenantField });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
this.jsonFields[key] = jsonCols;
|
|
1016
|
+
this.booleanFields[key] = booleanCols;
|
|
1017
|
+
this.numericFields[key] = numericCols;
|
|
1018
|
+
this.autoNumberFields[key] = autoNumberCols;
|
|
1019
|
+
this.tenantFieldByTable[key] = tenantField;
|
|
1020
|
+
if (dateCols.length) this.dateFields[key] = new Set(dateCols);
|
|
1021
|
+
if (datetimeCols.length) this.datetimeFields[key] = new Set(datetimeCols);
|
|
1022
|
+
}
|
|
919
1023
|
async initObjects(objects) {
|
|
920
1024
|
var _a, _b;
|
|
921
1025
|
this.assertSchemaMutable("initObjects");
|
|
@@ -1157,7 +1261,12 @@ var SqlDriver = class {
|
|
|
1157
1261
|
return this.knex;
|
|
1158
1262
|
}
|
|
1159
1263
|
getBuilder(object, options) {
|
|
1160
|
-
|
|
1264
|
+
const physical = this.physicalTableByObject[object] ?? object;
|
|
1265
|
+
let builder = this.knex(physical);
|
|
1266
|
+
const remoteSchema = this.physicalSchemaByObject[object];
|
|
1267
|
+
if (remoteSchema) {
|
|
1268
|
+
builder = builder.withSchema(remoteSchema);
|
|
1269
|
+
}
|
|
1161
1270
|
if (options?.transaction) {
|
|
1162
1271
|
builder = builder.transacting(options.transaction);
|
|
1163
1272
|
}
|
|
@@ -1256,6 +1365,20 @@ var SqlDriver = class {
|
|
|
1256
1365
|
if (typeof t === "string") return t;
|
|
1257
1366
|
return null;
|
|
1258
1367
|
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Coercion-map key for a builder. Coercion maps (date/datetime) are keyed by
|
|
1370
|
+
* OBJECT name, but after the federation change {@link getBuilder} targets the
|
|
1371
|
+
* physical remote table, so a builder reports the remote name. Map it back to
|
|
1372
|
+
* the object name for external objects; identity for managed ones (no reverse
|
|
1373
|
+
* entry). Note datetime coercion is a SQLite-only concern (see
|
|
1374
|
+
* coerceFilterValue), and SQLite external tables are bare-named, so this is
|
|
1375
|
+
* exact where it matters.
|
|
1376
|
+
*/
|
|
1377
|
+
coercionKey(builder) {
|
|
1378
|
+
const physical = this.tableNameForBuilder(builder);
|
|
1379
|
+
if (physical == null) return null;
|
|
1380
|
+
return this.objectByPhysicalTable[physical] ?? physical;
|
|
1381
|
+
}
|
|
1259
1382
|
/**
|
|
1260
1383
|
* Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
|
|
1261
1384
|
* calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
|
|
@@ -1349,7 +1472,7 @@ var SqlDriver = class {
|
|
|
1349
1472
|
}
|
|
1350
1473
|
applyFilters(builder, filters) {
|
|
1351
1474
|
if (!filters) return;
|
|
1352
|
-
const table = this.
|
|
1475
|
+
const table = this.coercionKey(builder);
|
|
1353
1476
|
if (!Array.isArray(filters) && typeof filters === "object") {
|
|
1354
1477
|
const hasMongoOperators = Object.keys(filters).some(
|
|
1355
1478
|
(k) => k.startsWith("$") || typeof filters[k] === "object" && filters[k] !== null && Object.keys(filters[k]).some((op) => op.startsWith("$"))
|
|
@@ -1360,7 +1483,7 @@ var SqlDriver = class {
|
|
|
1360
1483
|
}
|
|
1361
1484
|
for (const [key, value] of Object.entries(filters)) {
|
|
1362
1485
|
if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
|
|
1363
|
-
builder.where(key, this.coerceFilterValue(table, key, value));
|
|
1486
|
+
builder.where(this.remoteColumn(table, key, key), this.coerceFilterValue(table, key, value));
|
|
1364
1487
|
}
|
|
1365
1488
|
return;
|
|
1366
1489
|
}
|
|
@@ -1376,8 +1499,9 @@ var SqlDriver = class {
|
|
|
1376
1499
|
const [fieldRaw, op, value] = item;
|
|
1377
1500
|
const isCriterion = typeof fieldRaw === "string" && typeof op === "string";
|
|
1378
1501
|
if (isCriterion) {
|
|
1379
|
-
const
|
|
1380
|
-
const
|
|
1502
|
+
const localField = this.mapSortField(fieldRaw);
|
|
1503
|
+
const field = this.remoteColumn(table, fieldRaw, localField);
|
|
1504
|
+
const coerced = this.coerceFilterValue(table, localField, value);
|
|
1381
1505
|
const apply = (b) => {
|
|
1382
1506
|
const method = nextJoin === "or" ? "orWhere" : "where";
|
|
1383
1507
|
const methodIn = nextJoin === "or" ? "orWhereIn" : "whereIn";
|
|
@@ -1429,7 +1553,7 @@ var SqlDriver = class {
|
|
|
1429
1553
|
}
|
|
1430
1554
|
applyFilterCondition(builder, condition, logicalOp = "and", tableHint) {
|
|
1431
1555
|
if (!condition || typeof condition !== "object") return;
|
|
1432
|
-
const table = tableHint ?? this.
|
|
1556
|
+
const table = tableHint ?? this.coercionKey(builder);
|
|
1433
1557
|
for (const [key, value] of Object.entries(condition)) {
|
|
1434
1558
|
if (key === "$and" && Array.isArray(value)) {
|
|
1435
1559
|
builder.where((qb) => {
|
|
@@ -1449,10 +1573,11 @@ var SqlDriver = class {
|
|
|
1449
1573
|
}
|
|
1450
1574
|
});
|
|
1451
1575
|
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1452
|
-
const
|
|
1576
|
+
const localField = this.mapSortField(key);
|
|
1577
|
+
const field = this.remoteColumn(table, key, localField);
|
|
1453
1578
|
for (const [op, opValue] of Object.entries(value)) {
|
|
1454
1579
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1455
|
-
const coerced = this.coerceFilterValue(table,
|
|
1580
|
+
const coerced = this.coerceFilterValue(table, localField, opValue);
|
|
1456
1581
|
switch (op) {
|
|
1457
1582
|
case "$eq":
|
|
1458
1583
|
builder[method](field, coerced);
|
|
@@ -1490,9 +1615,10 @@ var SqlDriver = class {
|
|
|
1490
1615
|
}
|
|
1491
1616
|
}
|
|
1492
1617
|
} else {
|
|
1493
|
-
const
|
|
1618
|
+
const localField = this.mapSortField(key);
|
|
1619
|
+
const field = this.remoteColumn(table, key, localField);
|
|
1494
1620
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1495
|
-
builder[method](field, this.coerceFilterValue(table,
|
|
1621
|
+
builder[method](field, this.coerceFilterValue(table, localField, value));
|
|
1496
1622
|
}
|
|
1497
1623
|
}
|
|
1498
1624
|
}
|
|
@@ -1502,6 +1628,28 @@ var SqlDriver = class {
|
|
|
1502
1628
|
if (field === "updatedAt") return "updated_at";
|
|
1503
1629
|
return field;
|
|
1504
1630
|
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Physical column for a logical field on an external object that declares an
|
|
1633
|
+
* `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
|
|
1634
|
+
* per-site resolution) when the object has no columnMap, so managed objects
|
|
1635
|
+
* and external objects without a columnMap are byte-for-byte unchanged.
|
|
1636
|
+
*/
|
|
1637
|
+
remoteColumn(object, field, fallback) {
|
|
1638
|
+
const m = object ? this.fieldColumnByObject[object] : void 0;
|
|
1639
|
+
return m && m[field] || fallback;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Remap a write payload's logical field keys to physical remote columns for an
|
|
1643
|
+
* external object with a columnMap. No-op otherwise. Applied AFTER formatInput
|
|
1644
|
+
* (whose value coercion is keyed by logical field name).
|
|
1645
|
+
*/
|
|
1646
|
+
applyWriteColumnMap(object, data) {
|
|
1647
|
+
const m = this.fieldColumnByObject[object];
|
|
1648
|
+
if (!m || !data || typeof data !== "object") return data;
|
|
1649
|
+
const out = {};
|
|
1650
|
+
for (const [k, v] of Object.entries(data)) out[m[k] ?? k] = v;
|
|
1651
|
+
return out;
|
|
1652
|
+
}
|
|
1505
1653
|
mapAggregateFunc(func) {
|
|
1506
1654
|
switch (func) {
|
|
1507
1655
|
case "count":
|
|
@@ -1758,6 +1906,15 @@ var SqlDriver = class {
|
|
|
1758
1906
|
}
|
|
1759
1907
|
formatOutput(object, data) {
|
|
1760
1908
|
if (!data) return data;
|
|
1909
|
+
const colToField = this.columnFieldByObject[object];
|
|
1910
|
+
if (colToField && typeof data === "object") {
|
|
1911
|
+
for (const [remoteCol, localField] of Object.entries(colToField)) {
|
|
1912
|
+
if (remoteCol !== localField && Object.prototype.hasOwnProperty.call(data, remoteCol)) {
|
|
1913
|
+
data[localField] = data[remoteCol];
|
|
1914
|
+
delete data[remoteCol];
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1761
1918
|
if (this.isSqlite) {
|
|
1762
1919
|
const jsonFields = this.jsonFields[object];
|
|
1763
1920
|
if (jsonFields && jsonFields.length > 0) {
|