@objectstack/driver-sql 9.10.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 +104 -8
- package/dist/index.d.ts +104 -8
- package/dist/index.js +291 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +291 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
38
|
// src/sql-driver.ts
|
|
39
|
+
var import_data = require("@objectstack/spec/data");
|
|
39
40
|
var import_system = require("@objectstack/spec/system");
|
|
40
41
|
var import_shared = require("@objectstack/spec/shared");
|
|
41
42
|
var import_knex = __toESM(require("knex"));
|
|
@@ -85,6 +86,20 @@ var SqlDriver = class {
|
|
|
85
86
|
this.numericFields = {};
|
|
86
87
|
this.dateFields = {};
|
|
87
88
|
this.datetimeFields = {};
|
|
89
|
+
/**
|
|
90
|
+
* Federation read path (ADR-0015). For external objects whose physical
|
|
91
|
+
* remote table differs from the object name, these map between the two so
|
|
92
|
+
* {@link getBuilder} targets the remote table while the coercion maps above
|
|
93
|
+
* stay keyed by OBJECT name (matching formatInput/formatOutput). Empty for
|
|
94
|
+
* managed objects, so the managed query path is unchanged.
|
|
95
|
+
*/
|
|
96
|
+
this.physicalTableByObject = {};
|
|
97
|
+
this.physicalSchemaByObject = {};
|
|
98
|
+
this.objectByPhysicalTable = {};
|
|
99
|
+
/** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
|
|
100
|
+
this.fieldColumnByObject = {};
|
|
101
|
+
/** External columnMap inverse: physical remote column -> logical field (for read output remap). */
|
|
102
|
+
this.columnFieldByObject = {};
|
|
88
103
|
this.tablesWithTimestamps = /* @__PURE__ */ new Set();
|
|
89
104
|
/**
|
|
90
105
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -103,6 +118,14 @@ var SqlDriver = class {
|
|
|
103
118
|
this.autoNumberFields = {};
|
|
104
119
|
/** Whether the sequences table has been ensured this process. */
|
|
105
120
|
this.sequencesTableReady = false;
|
|
121
|
+
/**
|
|
122
|
+
* Whether `_objectstack_sequences` is the current `key_hash`-keyed shape.
|
|
123
|
+
* Set on a fresh create or a successful in-place migration. If a legacy table
|
|
124
|
+
* could NOT be migrated, this stays false: fixed-prefix sequences (empty
|
|
125
|
+
* scope) keep working via the legacy `(object, tenant_id, field)` key, while a
|
|
126
|
+
* per-scope write raises an actionable error rather than corrupting counters.
|
|
127
|
+
*/
|
|
128
|
+
this.sequencesHasKeyHash = false;
|
|
106
129
|
/** In-flight ensure promise; deduplicates concurrent first calls. */
|
|
107
130
|
this.sequencesTableEnsurePromise = null;
|
|
108
131
|
/**
|
|
@@ -301,6 +324,13 @@ var SqlDriver = class {
|
|
|
301
324
|
// ===================================
|
|
302
325
|
async connect() {
|
|
303
326
|
await this.ensureDatabaseExists();
|
|
327
|
+
if (this.isSqlite) {
|
|
328
|
+
try {
|
|
329
|
+
await this.knex.raw("PRAGMA auto_vacuum = INCREMENTAL");
|
|
330
|
+
} catch (e) {
|
|
331
|
+
this.logger.warn("Failed to set PRAGMA auto_vacuum=INCREMENTAL", e);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
304
334
|
}
|
|
305
335
|
async checkHealth() {
|
|
306
336
|
try {
|
|
@@ -326,7 +356,7 @@ var SqlDriver = class {
|
|
|
326
356
|
if (query.orderBy && Array.isArray(query.orderBy)) {
|
|
327
357
|
for (const item of query.orderBy) {
|
|
328
358
|
if (item.field) {
|
|
329
|
-
b.orderBy(this.mapSortField(item.field), item.order || "asc");
|
|
359
|
+
b.orderBy(this.remoteColumn(object, item.field, this.mapSortField(item.field)), item.order || "asc");
|
|
330
360
|
}
|
|
331
361
|
}
|
|
332
362
|
}
|
|
@@ -403,13 +433,22 @@ var SqlDriver = class {
|
|
|
403
433
|
this.injectTenantOnInsert(object, toInsert, options);
|
|
404
434
|
await this.fillAutoNumberFields(object, toInsert, options);
|
|
405
435
|
const builder = this.getBuilder(object, options);
|
|
406
|
-
const formatted = this.formatInput(object, toInsert);
|
|
436
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toInsert));
|
|
407
437
|
const result = await builder.insert(formatted).returning("*");
|
|
408
438
|
return this.formatOutput(object, result[0]);
|
|
409
439
|
}
|
|
410
440
|
/**
|
|
411
441
|
* Ensure the sequence-counter table exists. Idempotent and cheap after
|
|
412
442
|
* the first call (cached via `sequencesTableReady`).
|
|
443
|
+
*
|
|
444
|
+
* The row key is `key_hash` — a SHA-256 of `(object, tenant_id, field, scope)`
|
|
445
|
+
* where `scope` is the rendered autonumber prefix (date/field tokens before
|
|
446
|
+
* the `{0000}` slot), so a new day/group/parent starts a fresh counter. A
|
|
447
|
+
* single 64-char hashed primary key (rather than the four raw columns, which
|
|
448
|
+
* blow past MySQL's 3072-byte index limit under utf8mb4 and bound how long a
|
|
449
|
+
* `{field}` scope may be) keys every dialect uniformly and lets `scope` be a
|
|
450
|
+
* generous non-indexed column. Fixed-prefix formats use the empty scope and
|
|
451
|
+
* keep their single global counter (backward compatible).
|
|
413
452
|
*/
|
|
414
453
|
async ensureSequencesTable() {
|
|
415
454
|
if (this.sequencesTableReady) return;
|
|
@@ -421,18 +460,15 @@ var SqlDriver = class {
|
|
|
421
460
|
const exists = await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
422
461
|
if (!exists) {
|
|
423
462
|
try {
|
|
424
|
-
await this.
|
|
425
|
-
|
|
426
|
-
t.string("tenant_id").notNullable();
|
|
427
|
-
t.string("field").notNullable();
|
|
428
|
-
t.bigInteger("last_value").notNullable().defaultTo(0);
|
|
429
|
-
t.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
430
|
-
t.primary(["object", "tenant_id", "field"]);
|
|
431
|
-
});
|
|
463
|
+
await this.createSequencesTable(SEQUENCES_TABLE);
|
|
464
|
+
this.sequencesHasKeyHash = true;
|
|
432
465
|
} catch (err) {
|
|
433
466
|
const stillMissing = !await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
434
467
|
if (stillMissing) throw err;
|
|
468
|
+
await this.ensureSequencesKeyHashShape();
|
|
435
469
|
}
|
|
470
|
+
} else {
|
|
471
|
+
await this.ensureSequencesKeyHashShape();
|
|
436
472
|
}
|
|
437
473
|
this.sequencesTableReady = true;
|
|
438
474
|
})();
|
|
@@ -442,6 +478,71 @@ var SqlDriver = class {
|
|
|
442
478
|
this.sequencesTableEnsurePromise = null;
|
|
443
479
|
}
|
|
444
480
|
}
|
|
481
|
+
/** SHA-256 of the composite counter key — the table's single-column PK. */
|
|
482
|
+
sequenceKeyHash(object, tenantId, field, scope) {
|
|
483
|
+
return (0, import_node_crypto.createHash)("sha256").update(`${object}${tenantId}${field}${scope}`).digest("hex");
|
|
484
|
+
}
|
|
485
|
+
/** Create the current `key_hash`-keyed sequences table shape. */
|
|
486
|
+
async createSequencesTable(table) {
|
|
487
|
+
await this.knex.schema.createTable(table, (t) => {
|
|
488
|
+
t.string("key_hash", 64).notNullable().primary();
|
|
489
|
+
t.string("object").notNullable();
|
|
490
|
+
t.string("tenant_id").notNullable();
|
|
491
|
+
t.string("field").notNullable();
|
|
492
|
+
t.string("scope", 1024).notNullable().defaultTo("");
|
|
493
|
+
t.bigInteger("last_value").notNullable().defaultTo(0);
|
|
494
|
+
t.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Migrate a pre-existing `_objectstack_sequences` table to the current
|
|
499
|
+
* `key_hash`-keyed shape. Handles both the original 3-column table (no
|
|
500
|
+
* `scope`) and an interim 4-column `(object, tenant_id, field, scope)` table:
|
|
501
|
+
* every legacy row is read, its `key_hash` computed in app code (no portable
|
|
502
|
+
* SQL hash exists), and re-inserted into a freshly built table that then
|
|
503
|
+
* replaces the original. Idempotent — a no-op once `key_hash` is present.
|
|
504
|
+
*
|
|
505
|
+
* If the rebuild fails, `sequencesHasKeyHash` stays false: fixed-prefix
|
|
506
|
+
* sequences keep working via the legacy key and per-scope writes error
|
|
507
|
+
* actionably (see getNextSequenceValue), rather than corrupting data.
|
|
508
|
+
*/
|
|
509
|
+
async ensureSequencesKeyHashShape() {
|
|
510
|
+
if (await this.knex.schema.hasColumn(SEQUENCES_TABLE, "key_hash")) {
|
|
511
|
+
this.sequencesHasKeyHash = true;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const hasScope = await this.knex.schema.hasColumn(SEQUENCES_TABLE, "scope");
|
|
515
|
+
const TMP = `${SEQUENCES_TABLE}__rebuild`;
|
|
516
|
+
try {
|
|
517
|
+
const rows = await this.knex(SEQUENCES_TABLE).select("*");
|
|
518
|
+
await this.knex.schema.dropTableIfExists(TMP);
|
|
519
|
+
await this.createSequencesTable(TMP);
|
|
520
|
+
const migrated = rows.map((r) => {
|
|
521
|
+
const scope = hasScope && r.scope != null ? String(r.scope) : "";
|
|
522
|
+
return {
|
|
523
|
+
key_hash: this.sequenceKeyHash(String(r.object), String(r.tenant_id), String(r.field), scope),
|
|
524
|
+
object: r.object,
|
|
525
|
+
tenant_id: r.tenant_id,
|
|
526
|
+
field: r.field,
|
|
527
|
+
scope,
|
|
528
|
+
last_value: r.last_value ?? 0,
|
|
529
|
+
updated_at: r.updated_at ?? this.knex.fn.now()
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
if (migrated.length > 0) await this.knex(TMP).insert(migrated);
|
|
533
|
+
await this.knex.schema.dropTable(SEQUENCES_TABLE);
|
|
534
|
+
await this.knex.schema.renameTable(TMP, SEQUENCES_TABLE);
|
|
535
|
+
this.sequencesHasKeyHash = true;
|
|
536
|
+
} catch (err) {
|
|
537
|
+
this.sequencesHasKeyHash = false;
|
|
538
|
+
await this.knex.schema.dropTableIfExists(TMP).catch(() => {
|
|
539
|
+
});
|
|
540
|
+
this.logger.warn(
|
|
541
|
+
`[autonumber] Failed to migrate ${SEQUENCES_TABLE} to the key_hash shape. Fixed-prefix autonumbers keep working; date/{field}/per-parent formats will error until the table is migrated.`,
|
|
542
|
+
{ error: String(err) }
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
445
546
|
/**
|
|
446
547
|
* Bootstrap helper: scan the data table for the highest numeric suffix
|
|
447
548
|
* matching `prefix` (optionally scoped to a tenant). Used the first time
|
|
@@ -479,10 +580,16 @@ var SqlDriver = class {
|
|
|
479
580
|
* Gaps are tolerated by design — a rolled-back insert "burns" a number,
|
|
480
581
|
* matching standard sequence semantics.
|
|
481
582
|
*/
|
|
482
|
-
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx) {
|
|
583
|
+
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx, scope = "") {
|
|
483
584
|
await this.ensureSequencesTable();
|
|
484
585
|
const resolvedTenantId = tenantField && tenantId ? String(tenantId) : GLOBAL_TENANT;
|
|
485
|
-
|
|
586
|
+
if (scope !== "" && !this.sequencesHasKeyHash) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Cannot generate a per-scope autonumber for "${object}.${field}": the ${SEQUENCES_TABLE} table is still the legacy shape. Migrate it to the key_hash shape before using date/{field}/per-parent formats.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const key = this.sequencesHasKeyHash ? { key_hash: this.sequenceKeyHash(tableName, resolvedTenantId, field, scope) } : { object: tableName, tenant_id: resolvedTenantId, field };
|
|
592
|
+
const insertRow = this.sequencesHasKeyHash ? { ...key, object: tableName, tenant_id: resolvedTenantId, field, scope } : { ...key };
|
|
486
593
|
const runner = parentTrx ?? this.knex;
|
|
487
594
|
return runner.transaction(async (trx) => {
|
|
488
595
|
let existing;
|
|
@@ -502,7 +609,7 @@ var SqlDriver = class {
|
|
|
502
609
|
);
|
|
503
610
|
const initial = seedMax + 1;
|
|
504
611
|
try {
|
|
505
|
-
await trx(SEQUENCES_TABLE).insert({ ...
|
|
612
|
+
await trx(SEQUENCES_TABLE).insert({ ...insertRow, last_value: initial });
|
|
506
613
|
return initial;
|
|
507
614
|
} catch (err) {
|
|
508
615
|
existing = await trx(SEQUENCES_TABLE).where(key).forUpdate().first();
|
|
@@ -515,38 +622,50 @@ var SqlDriver = class {
|
|
|
515
622
|
});
|
|
516
623
|
}
|
|
517
624
|
/**
|
|
518
|
-
* For each `auto_number` field
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
625
|
+
* For each `auto_number` field the caller left empty, render the format and
|
|
626
|
+
* reserve the next counter value. The counter is scoped to the rendered
|
|
627
|
+
* prefix (date tokens like `{YYYYMMDD}` in the request's business timezone,
|
|
628
|
+
* plus `{field}` interpolation from the row), so it resets per period/group;
|
|
629
|
+
* the full rendered prefix bootstraps the counter from existing data, and the
|
|
630
|
+
* tenant scopes it for isolation.
|
|
522
631
|
*/
|
|
523
632
|
async fillAutoNumberFields(object, row, options) {
|
|
524
|
-
const tableName = import_system.StorageNameMapping.resolveTableName({ name: object });
|
|
525
|
-
const cfgs = this.autoNumberFields[
|
|
633
|
+
const tableName = this.physicalTableByObject[object] ?? import_system.StorageNameMapping.resolveTableName({ name: object });
|
|
634
|
+
const cfgs = this.autoNumberFields[object] || this.autoNumberFields[tableName];
|
|
526
635
|
if (!cfgs || cfgs.length === 0) return;
|
|
527
636
|
const parentTrx = options?.transaction;
|
|
637
|
+
const timezone = options?.timezone;
|
|
638
|
+
const now = /* @__PURE__ */ new Date();
|
|
528
639
|
for (const cfg of cfgs) {
|
|
529
640
|
if (row[cfg.name] !== void 0 && row[cfg.name] !== null && row[cfg.name] !== "") continue;
|
|
641
|
+
const missing = (0, import_data.missingFieldValues)(cfg.tokens, row);
|
|
642
|
+
if (missing.length > 0) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`Cannot generate autonumber "${object}.${cfg.name}" (format "${cfg.format}"): referenced field(s) [${missing.join(", ")}] are empty on the record. Fields interpolated into an autonumber format must be set before the record is created.`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
530
647
|
const rowTenant = cfg.tenantField ? row[cfg.tenantField] : void 0;
|
|
531
648
|
const optTenant = options?.tenantId;
|
|
532
649
|
const tenantId = rowTenant != null && rowTenant !== "" ? String(rowTenant) : optTenant != null && optTenant !== "" ? String(optTenant) : null;
|
|
650
|
+
const probe = (0, import_data.renderAutonumber)({ tokens: cfg.tokens, seq: 0, record: row, now, timezone });
|
|
533
651
|
const next = await this.getNextSequenceValue(
|
|
534
652
|
object,
|
|
535
653
|
tableName,
|
|
536
654
|
cfg.name,
|
|
537
|
-
|
|
655
|
+
probe.prefix,
|
|
538
656
|
cfg.tenantField,
|
|
539
657
|
tenantId,
|
|
540
|
-
parentTrx
|
|
658
|
+
parentTrx,
|
|
659
|
+
probe.scope
|
|
541
660
|
);
|
|
542
|
-
row[cfg.name] =
|
|
661
|
+
row[cfg.name] = (0, import_data.renderAutonumber)({ tokens: cfg.tokens, seq: next, record: row, now, timezone }).value;
|
|
543
662
|
}
|
|
544
663
|
}
|
|
545
664
|
async update(object, id, data, options) {
|
|
546
665
|
this.auditMissingTenant(object, "update", options);
|
|
547
666
|
const builder = this.getBuilder(object, options).where("id", id);
|
|
548
667
|
this.applyTenantScope(builder, object, options);
|
|
549
|
-
const formatted = this.formatInput(object, data);
|
|
668
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, data));
|
|
550
669
|
if (this.tablesWithTimestamps.has(object)) {
|
|
551
670
|
if (this.isSqlite) {
|
|
552
671
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -572,7 +691,7 @@ var SqlDriver = class {
|
|
|
572
691
|
this.auditMissingTenant(object, "upsert", options);
|
|
573
692
|
this.injectTenantOnInsert(object, toUpsert, options);
|
|
574
693
|
await this.fillAutoNumberFields(object, toUpsert, options);
|
|
575
|
-
const formatted = this.formatInput(object, toUpsert);
|
|
694
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toUpsert));
|
|
576
695
|
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
|
|
577
696
|
const builder = this.getBuilder(object, options);
|
|
578
697
|
await builder.insert(formatted).onConflict(mergeKeys).merge();
|
|
@@ -855,6 +974,89 @@ var SqlDriver = class {
|
|
|
855
974
|
/**
|
|
856
975
|
* Batch-initialise tables from an array of object definitions.
|
|
857
976
|
*/
|
|
977
|
+
/**
|
|
978
|
+
* DDL-free metadata registration for a federated (external) object — the
|
|
979
|
+
* read-path counterpart to {@link initObjects} (ADR-0015 federation).
|
|
980
|
+
*
|
|
981
|
+
* `initObjects` is gated by `assertSchemaMutable` and therefore throws for
|
|
982
|
+
* any non-`managed` driver, which left external objects with NO read-coercion
|
|
983
|
+
* metadata and the query path resolving to a table named after the object
|
|
984
|
+
* instead of its remote table. This populates the same coercion maps (keyed
|
|
985
|
+
* by OBJECT name, matching formatInput/formatOutput/coerceFilterValue) and
|
|
986
|
+
* records the physical remote table (`external.remoteName`, optionally
|
|
987
|
+
* `external.remoteSchema`) so {@link getBuilder} targets it — WITHOUT running
|
|
988
|
+
* any DDL (createTable/alterTable/columnInfo). Keep the field-classification
|
|
989
|
+
* below in sync with initObjects() if the field-type -> storage mapping changes.
|
|
990
|
+
*/
|
|
991
|
+
registerExternalObject(schema) {
|
|
992
|
+
const key = schema.name;
|
|
993
|
+
const remoteName = schema.external?.remoteName || schema.name;
|
|
994
|
+
const remoteSchema = schema.external?.remoteSchema;
|
|
995
|
+
this.physicalTableByObject[key] = remoteName;
|
|
996
|
+
this.objectByPhysicalTable[remoteName] = key;
|
|
997
|
+
if (remoteSchema) {
|
|
998
|
+
if (this.isSqlite) {
|
|
999
|
+
this.logger.warn(
|
|
1000
|
+
`[sql-driver] external object "${key}" declares remoteSchema="${remoteSchema}" but SQLite has no schema namespace; ignoring (treating "${remoteName}" as a bare table).`
|
|
1001
|
+
);
|
|
1002
|
+
} else {
|
|
1003
|
+
this.physicalSchemaByObject[key] = remoteSchema;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const columnMap = schema.external?.columnMap;
|
|
1007
|
+
if (columnMap && typeof columnMap === "object" && Object.keys(columnMap).length > 0) {
|
|
1008
|
+
const fieldToCol = {};
|
|
1009
|
+
const colToField = {};
|
|
1010
|
+
for (const [remoteCol, localField] of Object.entries(columnMap)) {
|
|
1011
|
+
if (typeof localField === "string" && localField) {
|
|
1012
|
+
fieldToCol[localField] = remoteCol;
|
|
1013
|
+
colToField[remoteCol] = localField;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
this.fieldColumnByObject[key] = fieldToCol;
|
|
1017
|
+
this.columnFieldByObject[key] = colToField;
|
|
1018
|
+
}
|
|
1019
|
+
const jsonCols = [];
|
|
1020
|
+
const booleanCols = [];
|
|
1021
|
+
const numericCols = [];
|
|
1022
|
+
const dateCols = [];
|
|
1023
|
+
const datetimeCols = [];
|
|
1024
|
+
const autoNumberCols = [];
|
|
1025
|
+
const tenancyDecl = schema?.tenancy;
|
|
1026
|
+
let tenantField = null;
|
|
1027
|
+
if (tenancyDecl && tenancyDecl.enabled !== false && tenancyDecl.tenantField) {
|
|
1028
|
+
const declared = String(tenancyDecl.tenantField);
|
|
1029
|
+
if (schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, declared)) {
|
|
1030
|
+
tenantField = declared;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (!tenantField) {
|
|
1034
|
+
const hasOrgField = !!(schema.fields && Object.prototype.hasOwnProperty.call(schema.fields, "organization_id"));
|
|
1035
|
+
tenantField = hasOrgField ? "organization_id" : null;
|
|
1036
|
+
}
|
|
1037
|
+
if (schema.fields) {
|
|
1038
|
+
for (const [name, field] of Object.entries(schema.fields)) {
|
|
1039
|
+
const type = field.type || "string";
|
|
1040
|
+
if (this.isJsonField(type, field)) jsonCols.push(name);
|
|
1041
|
+
if (type === "boolean" || type === "toggle") booleanCols.push(name);
|
|
1042
|
+
if (NUMERIC_SCALAR_TYPES.has(type) && !field.multiple) numericCols.push(name);
|
|
1043
|
+
if (type === "date") dateCols.push(name);
|
|
1044
|
+
if (type === "datetime") datetimeCols.push(name);
|
|
1045
|
+
if (type === "auto_number" || type === "autonumber") {
|
|
1046
|
+
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
1047
|
+
const fmt = rawFmt || "{0000}";
|
|
1048
|
+
autoNumberCols.push({ name, format: fmt, tokens: (0, import_data.parseAutonumberFormat)(fmt), tenantField });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
this.jsonFields[key] = jsonCols;
|
|
1053
|
+
this.booleanFields[key] = booleanCols;
|
|
1054
|
+
this.numericFields[key] = numericCols;
|
|
1055
|
+
this.autoNumberFields[key] = autoNumberCols;
|
|
1056
|
+
this.tenantFieldByTable[key] = tenantField;
|
|
1057
|
+
if (dateCols.length) this.dateFields[key] = new Set(dateCols);
|
|
1058
|
+
if (datetimeCols.length) this.datetimeFields[key] = new Set(datetimeCols);
|
|
1059
|
+
}
|
|
858
1060
|
async initObjects(objects) {
|
|
859
1061
|
var _a, _b;
|
|
860
1062
|
this.assertSchemaMutable("initObjects");
|
|
@@ -898,10 +1100,8 @@ var SqlDriver = class {
|
|
|
898
1100
|
if (type === "auto_number" || type === "autonumber") {
|
|
899
1101
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
900
1102
|
const fmt = rawFmt || "{0000}";
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
const prefix = m ? fmt.slice(0, m.index ?? 0) : fmt;
|
|
904
|
-
autoNumberCols.push({ name, format: fmt, prefix, padWidth, tenantField });
|
|
1103
|
+
const tokens = (0, import_data.parseAutonumberFormat)(fmt);
|
|
1104
|
+
autoNumberCols.push({ name, format: fmt, tokens, tenantField });
|
|
905
1105
|
}
|
|
906
1106
|
}
|
|
907
1107
|
}
|
|
@@ -1098,7 +1298,12 @@ var SqlDriver = class {
|
|
|
1098
1298
|
return this.knex;
|
|
1099
1299
|
}
|
|
1100
1300
|
getBuilder(object, options) {
|
|
1101
|
-
|
|
1301
|
+
const physical = this.physicalTableByObject[object] ?? object;
|
|
1302
|
+
let builder = this.knex(physical);
|
|
1303
|
+
const remoteSchema = this.physicalSchemaByObject[object];
|
|
1304
|
+
if (remoteSchema) {
|
|
1305
|
+
builder = builder.withSchema(remoteSchema);
|
|
1306
|
+
}
|
|
1102
1307
|
if (options?.transaction) {
|
|
1103
1308
|
builder = builder.transacting(options.transaction);
|
|
1104
1309
|
}
|
|
@@ -1197,6 +1402,20 @@ var SqlDriver = class {
|
|
|
1197
1402
|
if (typeof t === "string") return t;
|
|
1198
1403
|
return null;
|
|
1199
1404
|
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Coercion-map key for a builder. Coercion maps (date/datetime) are keyed by
|
|
1407
|
+
* OBJECT name, but after the federation change {@link getBuilder} targets the
|
|
1408
|
+
* physical remote table, so a builder reports the remote name. Map it back to
|
|
1409
|
+
* the object name for external objects; identity for managed ones (no reverse
|
|
1410
|
+
* entry). Note datetime coercion is a SQLite-only concern (see
|
|
1411
|
+
* coerceFilterValue), and SQLite external tables are bare-named, so this is
|
|
1412
|
+
* exact where it matters.
|
|
1413
|
+
*/
|
|
1414
|
+
coercionKey(builder) {
|
|
1415
|
+
const physical = this.tableNameForBuilder(builder);
|
|
1416
|
+
if (physical == null) return null;
|
|
1417
|
+
return this.objectByPhysicalTable[physical] ?? physical;
|
|
1418
|
+
}
|
|
1200
1419
|
/**
|
|
1201
1420
|
* Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
|
|
1202
1421
|
* calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
|
|
@@ -1290,7 +1509,7 @@ var SqlDriver = class {
|
|
|
1290
1509
|
}
|
|
1291
1510
|
applyFilters(builder, filters) {
|
|
1292
1511
|
if (!filters) return;
|
|
1293
|
-
const table = this.
|
|
1512
|
+
const table = this.coercionKey(builder);
|
|
1294
1513
|
if (!Array.isArray(filters) && typeof filters === "object") {
|
|
1295
1514
|
const hasMongoOperators = Object.keys(filters).some(
|
|
1296
1515
|
(k) => k.startsWith("$") || typeof filters[k] === "object" && filters[k] !== null && Object.keys(filters[k]).some((op) => op.startsWith("$"))
|
|
@@ -1301,7 +1520,7 @@ var SqlDriver = class {
|
|
|
1301
1520
|
}
|
|
1302
1521
|
for (const [key, value] of Object.entries(filters)) {
|
|
1303
1522
|
if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
|
|
1304
|
-
builder.where(key, this.coerceFilterValue(table, key, value));
|
|
1523
|
+
builder.where(this.remoteColumn(table, key, key), this.coerceFilterValue(table, key, value));
|
|
1305
1524
|
}
|
|
1306
1525
|
return;
|
|
1307
1526
|
}
|
|
@@ -1317,8 +1536,9 @@ var SqlDriver = class {
|
|
|
1317
1536
|
const [fieldRaw, op, value] = item;
|
|
1318
1537
|
const isCriterion = typeof fieldRaw === "string" && typeof op === "string";
|
|
1319
1538
|
if (isCriterion) {
|
|
1320
|
-
const
|
|
1321
|
-
const
|
|
1539
|
+
const localField = this.mapSortField(fieldRaw);
|
|
1540
|
+
const field = this.remoteColumn(table, fieldRaw, localField);
|
|
1541
|
+
const coerced = this.coerceFilterValue(table, localField, value);
|
|
1322
1542
|
const apply = (b) => {
|
|
1323
1543
|
const method = nextJoin === "or" ? "orWhere" : "where";
|
|
1324
1544
|
const methodIn = nextJoin === "or" ? "orWhereIn" : "whereIn";
|
|
@@ -1370,7 +1590,7 @@ var SqlDriver = class {
|
|
|
1370
1590
|
}
|
|
1371
1591
|
applyFilterCondition(builder, condition, logicalOp = "and", tableHint) {
|
|
1372
1592
|
if (!condition || typeof condition !== "object") return;
|
|
1373
|
-
const table = tableHint ?? this.
|
|
1593
|
+
const table = tableHint ?? this.coercionKey(builder);
|
|
1374
1594
|
for (const [key, value] of Object.entries(condition)) {
|
|
1375
1595
|
if (key === "$and" && Array.isArray(value)) {
|
|
1376
1596
|
builder.where((qb) => {
|
|
@@ -1390,10 +1610,11 @@ var SqlDriver = class {
|
|
|
1390
1610
|
}
|
|
1391
1611
|
});
|
|
1392
1612
|
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1393
|
-
const
|
|
1613
|
+
const localField = this.mapSortField(key);
|
|
1614
|
+
const field = this.remoteColumn(table, key, localField);
|
|
1394
1615
|
for (const [op, opValue] of Object.entries(value)) {
|
|
1395
1616
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1396
|
-
const coerced = this.coerceFilterValue(table,
|
|
1617
|
+
const coerced = this.coerceFilterValue(table, localField, opValue);
|
|
1397
1618
|
switch (op) {
|
|
1398
1619
|
case "$eq":
|
|
1399
1620
|
builder[method](field, coerced);
|
|
@@ -1431,9 +1652,10 @@ var SqlDriver = class {
|
|
|
1431
1652
|
}
|
|
1432
1653
|
}
|
|
1433
1654
|
} else {
|
|
1434
|
-
const
|
|
1655
|
+
const localField = this.mapSortField(key);
|
|
1656
|
+
const field = this.remoteColumn(table, key, localField);
|
|
1435
1657
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1436
|
-
builder[method](field, this.coerceFilterValue(table,
|
|
1658
|
+
builder[method](field, this.coerceFilterValue(table, localField, value));
|
|
1437
1659
|
}
|
|
1438
1660
|
}
|
|
1439
1661
|
}
|
|
@@ -1443,6 +1665,28 @@ var SqlDriver = class {
|
|
|
1443
1665
|
if (field === "updatedAt") return "updated_at";
|
|
1444
1666
|
return field;
|
|
1445
1667
|
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Physical column for a logical field on an external object that declares an
|
|
1670
|
+
* `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
|
|
1671
|
+
* per-site resolution) when the object has no columnMap, so managed objects
|
|
1672
|
+
* and external objects without a columnMap are byte-for-byte unchanged.
|
|
1673
|
+
*/
|
|
1674
|
+
remoteColumn(object, field, fallback) {
|
|
1675
|
+
const m = object ? this.fieldColumnByObject[object] : void 0;
|
|
1676
|
+
return m && m[field] || fallback;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Remap a write payload's logical field keys to physical remote columns for an
|
|
1680
|
+
* external object with a columnMap. No-op otherwise. Applied AFTER formatInput
|
|
1681
|
+
* (whose value coercion is keyed by logical field name).
|
|
1682
|
+
*/
|
|
1683
|
+
applyWriteColumnMap(object, data) {
|
|
1684
|
+
const m = this.fieldColumnByObject[object];
|
|
1685
|
+
if (!m || !data || typeof data !== "object") return data;
|
|
1686
|
+
const out = {};
|
|
1687
|
+
for (const [k, v] of Object.entries(data)) out[m[k] ?? k] = v;
|
|
1688
|
+
return out;
|
|
1689
|
+
}
|
|
1446
1690
|
mapAggregateFunc(func) {
|
|
1447
1691
|
switch (func) {
|
|
1448
1692
|
case "count":
|
|
@@ -1699,6 +1943,15 @@ var SqlDriver = class {
|
|
|
1699
1943
|
}
|
|
1700
1944
|
formatOutput(object, data) {
|
|
1701
1945
|
if (!data) return data;
|
|
1946
|
+
const colToField = this.columnFieldByObject[object];
|
|
1947
|
+
if (colToField && typeof data === "object") {
|
|
1948
|
+
for (const [remoteCol, localField] of Object.entries(colToField)) {
|
|
1949
|
+
if (remoteCol !== localField && Object.prototype.hasOwnProperty.call(data, remoteCol)) {
|
|
1950
|
+
data[localField] = data[remoteCol];
|
|
1951
|
+
delete data[remoteCol];
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1702
1955
|
if (this.isSqlite) {
|
|
1703
1956
|
const jsonFields = this.jsonFields[object];
|
|
1704
1957
|
if (jsonFields && jsonFields.length > 0) {
|