@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.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/sql-driver.ts
|
|
2
|
+
import { parseAutonumberFormat, renderAutonumber, missingFieldValues } from "@objectstack/spec/data";
|
|
2
3
|
import { StorageNameMapping } from "@objectstack/spec/system";
|
|
3
4
|
import { ExternalSchemaModeViolationError } from "@objectstack/spec/shared";
|
|
4
5
|
import knex from "knex";
|
|
@@ -48,6 +49,20 @@ var SqlDriver = class {
|
|
|
48
49
|
this.numericFields = {};
|
|
49
50
|
this.dateFields = {};
|
|
50
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 = {};
|
|
51
66
|
this.tablesWithTimestamps = /* @__PURE__ */ new Set();
|
|
52
67
|
/**
|
|
53
68
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -66,6 +81,14 @@ var SqlDriver = class {
|
|
|
66
81
|
this.autoNumberFields = {};
|
|
67
82
|
/** Whether the sequences table has been ensured this process. */
|
|
68
83
|
this.sequencesTableReady = false;
|
|
84
|
+
/**
|
|
85
|
+
* Whether `_objectstack_sequences` is the current `key_hash`-keyed shape.
|
|
86
|
+
* Set on a fresh create or a successful in-place migration. If a legacy table
|
|
87
|
+
* could NOT be migrated, this stays false: fixed-prefix sequences (empty
|
|
88
|
+
* scope) keep working via the legacy `(object, tenant_id, field)` key, while a
|
|
89
|
+
* per-scope write raises an actionable error rather than corrupting counters.
|
|
90
|
+
*/
|
|
91
|
+
this.sequencesHasKeyHash = false;
|
|
69
92
|
/** In-flight ensure promise; deduplicates concurrent first calls. */
|
|
70
93
|
this.sequencesTableEnsurePromise = null;
|
|
71
94
|
/**
|
|
@@ -264,6 +287,13 @@ var SqlDriver = class {
|
|
|
264
287
|
// ===================================
|
|
265
288
|
async connect() {
|
|
266
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
|
+
}
|
|
267
297
|
}
|
|
268
298
|
async checkHealth() {
|
|
269
299
|
try {
|
|
@@ -289,7 +319,7 @@ var SqlDriver = class {
|
|
|
289
319
|
if (query.orderBy && Array.isArray(query.orderBy)) {
|
|
290
320
|
for (const item of query.orderBy) {
|
|
291
321
|
if (item.field) {
|
|
292
|
-
b.orderBy(this.mapSortField(item.field), item.order || "asc");
|
|
322
|
+
b.orderBy(this.remoteColumn(object, item.field, this.mapSortField(item.field)), item.order || "asc");
|
|
293
323
|
}
|
|
294
324
|
}
|
|
295
325
|
}
|
|
@@ -366,13 +396,22 @@ var SqlDriver = class {
|
|
|
366
396
|
this.injectTenantOnInsert(object, toInsert, options);
|
|
367
397
|
await this.fillAutoNumberFields(object, toInsert, options);
|
|
368
398
|
const builder = this.getBuilder(object, options);
|
|
369
|
-
const formatted = this.formatInput(object, toInsert);
|
|
399
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toInsert));
|
|
370
400
|
const result = await builder.insert(formatted).returning("*");
|
|
371
401
|
return this.formatOutput(object, result[0]);
|
|
372
402
|
}
|
|
373
403
|
/**
|
|
374
404
|
* Ensure the sequence-counter table exists. Idempotent and cheap after
|
|
375
405
|
* the first call (cached via `sequencesTableReady`).
|
|
406
|
+
*
|
|
407
|
+
* The row key is `key_hash` — a SHA-256 of `(object, tenant_id, field, scope)`
|
|
408
|
+
* where `scope` is the rendered autonumber prefix (date/field tokens before
|
|
409
|
+
* the `{0000}` slot), so a new day/group/parent starts a fresh counter. A
|
|
410
|
+
* single 64-char hashed primary key (rather than the four raw columns, which
|
|
411
|
+
* blow past MySQL's 3072-byte index limit under utf8mb4 and bound how long a
|
|
412
|
+
* `{field}` scope may be) keys every dialect uniformly and lets `scope` be a
|
|
413
|
+
* generous non-indexed column. Fixed-prefix formats use the empty scope and
|
|
414
|
+
* keep their single global counter (backward compatible).
|
|
376
415
|
*/
|
|
377
416
|
async ensureSequencesTable() {
|
|
378
417
|
if (this.sequencesTableReady) return;
|
|
@@ -384,18 +423,15 @@ var SqlDriver = class {
|
|
|
384
423
|
const exists = await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
385
424
|
if (!exists) {
|
|
386
425
|
try {
|
|
387
|
-
await this.
|
|
388
|
-
|
|
389
|
-
t.string("tenant_id").notNullable();
|
|
390
|
-
t.string("field").notNullable();
|
|
391
|
-
t.bigInteger("last_value").notNullable().defaultTo(0);
|
|
392
|
-
t.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
393
|
-
t.primary(["object", "tenant_id", "field"]);
|
|
394
|
-
});
|
|
426
|
+
await this.createSequencesTable(SEQUENCES_TABLE);
|
|
427
|
+
this.sequencesHasKeyHash = true;
|
|
395
428
|
} catch (err) {
|
|
396
429
|
const stillMissing = !await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
397
430
|
if (stillMissing) throw err;
|
|
431
|
+
await this.ensureSequencesKeyHashShape();
|
|
398
432
|
}
|
|
433
|
+
} else {
|
|
434
|
+
await this.ensureSequencesKeyHashShape();
|
|
399
435
|
}
|
|
400
436
|
this.sequencesTableReady = true;
|
|
401
437
|
})();
|
|
@@ -405,6 +441,71 @@ var SqlDriver = class {
|
|
|
405
441
|
this.sequencesTableEnsurePromise = null;
|
|
406
442
|
}
|
|
407
443
|
}
|
|
444
|
+
/** SHA-256 of the composite counter key — the table's single-column PK. */
|
|
445
|
+
sequenceKeyHash(object, tenantId, field, scope) {
|
|
446
|
+
return createHash("sha256").update(`${object}${tenantId}${field}${scope}`).digest("hex");
|
|
447
|
+
}
|
|
448
|
+
/** Create the current `key_hash`-keyed sequences table shape. */
|
|
449
|
+
async createSequencesTable(table) {
|
|
450
|
+
await this.knex.schema.createTable(table, (t) => {
|
|
451
|
+
t.string("key_hash", 64).notNullable().primary();
|
|
452
|
+
t.string("object").notNullable();
|
|
453
|
+
t.string("tenant_id").notNullable();
|
|
454
|
+
t.string("field").notNullable();
|
|
455
|
+
t.string("scope", 1024).notNullable().defaultTo("");
|
|
456
|
+
t.bigInteger("last_value").notNullable().defaultTo(0);
|
|
457
|
+
t.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Migrate a pre-existing `_objectstack_sequences` table to the current
|
|
462
|
+
* `key_hash`-keyed shape. Handles both the original 3-column table (no
|
|
463
|
+
* `scope`) and an interim 4-column `(object, tenant_id, field, scope)` table:
|
|
464
|
+
* every legacy row is read, its `key_hash` computed in app code (no portable
|
|
465
|
+
* SQL hash exists), and re-inserted into a freshly built table that then
|
|
466
|
+
* replaces the original. Idempotent — a no-op once `key_hash` is present.
|
|
467
|
+
*
|
|
468
|
+
* If the rebuild fails, `sequencesHasKeyHash` stays false: fixed-prefix
|
|
469
|
+
* sequences keep working via the legacy key and per-scope writes error
|
|
470
|
+
* actionably (see getNextSequenceValue), rather than corrupting data.
|
|
471
|
+
*/
|
|
472
|
+
async ensureSequencesKeyHashShape() {
|
|
473
|
+
if (await this.knex.schema.hasColumn(SEQUENCES_TABLE, "key_hash")) {
|
|
474
|
+
this.sequencesHasKeyHash = true;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const hasScope = await this.knex.schema.hasColumn(SEQUENCES_TABLE, "scope");
|
|
478
|
+
const TMP = `${SEQUENCES_TABLE}__rebuild`;
|
|
479
|
+
try {
|
|
480
|
+
const rows = await this.knex(SEQUENCES_TABLE).select("*");
|
|
481
|
+
await this.knex.schema.dropTableIfExists(TMP);
|
|
482
|
+
await this.createSequencesTable(TMP);
|
|
483
|
+
const migrated = rows.map((r) => {
|
|
484
|
+
const scope = hasScope && r.scope != null ? String(r.scope) : "";
|
|
485
|
+
return {
|
|
486
|
+
key_hash: this.sequenceKeyHash(String(r.object), String(r.tenant_id), String(r.field), scope),
|
|
487
|
+
object: r.object,
|
|
488
|
+
tenant_id: r.tenant_id,
|
|
489
|
+
field: r.field,
|
|
490
|
+
scope,
|
|
491
|
+
last_value: r.last_value ?? 0,
|
|
492
|
+
updated_at: r.updated_at ?? this.knex.fn.now()
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
if (migrated.length > 0) await this.knex(TMP).insert(migrated);
|
|
496
|
+
await this.knex.schema.dropTable(SEQUENCES_TABLE);
|
|
497
|
+
await this.knex.schema.renameTable(TMP, SEQUENCES_TABLE);
|
|
498
|
+
this.sequencesHasKeyHash = true;
|
|
499
|
+
} catch (err) {
|
|
500
|
+
this.sequencesHasKeyHash = false;
|
|
501
|
+
await this.knex.schema.dropTableIfExists(TMP).catch(() => {
|
|
502
|
+
});
|
|
503
|
+
this.logger.warn(
|
|
504
|
+
`[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.`,
|
|
505
|
+
{ error: String(err) }
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
408
509
|
/**
|
|
409
510
|
* Bootstrap helper: scan the data table for the highest numeric suffix
|
|
410
511
|
* matching `prefix` (optionally scoped to a tenant). Used the first time
|
|
@@ -442,10 +543,16 @@ var SqlDriver = class {
|
|
|
442
543
|
* Gaps are tolerated by design — a rolled-back insert "burns" a number,
|
|
443
544
|
* matching standard sequence semantics.
|
|
444
545
|
*/
|
|
445
|
-
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx) {
|
|
546
|
+
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx, scope = "") {
|
|
446
547
|
await this.ensureSequencesTable();
|
|
447
548
|
const resolvedTenantId = tenantField && tenantId ? String(tenantId) : GLOBAL_TENANT;
|
|
448
|
-
|
|
549
|
+
if (scope !== "" && !this.sequencesHasKeyHash) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`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.`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const key = this.sequencesHasKeyHash ? { key_hash: this.sequenceKeyHash(tableName, resolvedTenantId, field, scope) } : { object: tableName, tenant_id: resolvedTenantId, field };
|
|
555
|
+
const insertRow = this.sequencesHasKeyHash ? { ...key, object: tableName, tenant_id: resolvedTenantId, field, scope } : { ...key };
|
|
449
556
|
const runner = parentTrx ?? this.knex;
|
|
450
557
|
return runner.transaction(async (trx) => {
|
|
451
558
|
let existing;
|
|
@@ -465,7 +572,7 @@ var SqlDriver = class {
|
|
|
465
572
|
);
|
|
466
573
|
const initial = seedMax + 1;
|
|
467
574
|
try {
|
|
468
|
-
await trx(SEQUENCES_TABLE).insert({ ...
|
|
575
|
+
await trx(SEQUENCES_TABLE).insert({ ...insertRow, last_value: initial });
|
|
469
576
|
return initial;
|
|
470
577
|
} catch (err) {
|
|
471
578
|
existing = await trx(SEQUENCES_TABLE).where(key).forUpdate().first();
|
|
@@ -478,38 +585,50 @@ var SqlDriver = class {
|
|
|
478
585
|
});
|
|
479
586
|
}
|
|
480
587
|
/**
|
|
481
|
-
* For each `auto_number` field
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
588
|
+
* For each `auto_number` field the caller left empty, render the format and
|
|
589
|
+
* reserve the next counter value. The counter is scoped to the rendered
|
|
590
|
+
* prefix (date tokens like `{YYYYMMDD}` in the request's business timezone,
|
|
591
|
+
* plus `{field}` interpolation from the row), so it resets per period/group;
|
|
592
|
+
* the full rendered prefix bootstraps the counter from existing data, and the
|
|
593
|
+
* tenant scopes it for isolation.
|
|
485
594
|
*/
|
|
486
595
|
async fillAutoNumberFields(object, row, options) {
|
|
487
|
-
const tableName = StorageNameMapping.resolveTableName({ name: object });
|
|
488
|
-
const cfgs = this.autoNumberFields[
|
|
596
|
+
const tableName = this.physicalTableByObject[object] ?? StorageNameMapping.resolveTableName({ name: object });
|
|
597
|
+
const cfgs = this.autoNumberFields[object] || this.autoNumberFields[tableName];
|
|
489
598
|
if (!cfgs || cfgs.length === 0) return;
|
|
490
599
|
const parentTrx = options?.transaction;
|
|
600
|
+
const timezone = options?.timezone;
|
|
601
|
+
const now = /* @__PURE__ */ new Date();
|
|
491
602
|
for (const cfg of cfgs) {
|
|
492
603
|
if (row[cfg.name] !== void 0 && row[cfg.name] !== null && row[cfg.name] !== "") continue;
|
|
604
|
+
const missing = missingFieldValues(cfg.tokens, row);
|
|
605
|
+
if (missing.length > 0) {
|
|
606
|
+
throw new Error(
|
|
607
|
+
`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.`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
493
610
|
const rowTenant = cfg.tenantField ? row[cfg.tenantField] : void 0;
|
|
494
611
|
const optTenant = options?.tenantId;
|
|
495
612
|
const tenantId = rowTenant != null && rowTenant !== "" ? String(rowTenant) : optTenant != null && optTenant !== "" ? String(optTenant) : null;
|
|
613
|
+
const probe = renderAutonumber({ tokens: cfg.tokens, seq: 0, record: row, now, timezone });
|
|
496
614
|
const next = await this.getNextSequenceValue(
|
|
497
615
|
object,
|
|
498
616
|
tableName,
|
|
499
617
|
cfg.name,
|
|
500
|
-
|
|
618
|
+
probe.prefix,
|
|
501
619
|
cfg.tenantField,
|
|
502
620
|
tenantId,
|
|
503
|
-
parentTrx
|
|
621
|
+
parentTrx,
|
|
622
|
+
probe.scope
|
|
504
623
|
);
|
|
505
|
-
row[cfg.name] =
|
|
624
|
+
row[cfg.name] = renderAutonumber({ tokens: cfg.tokens, seq: next, record: row, now, timezone }).value;
|
|
506
625
|
}
|
|
507
626
|
}
|
|
508
627
|
async update(object, id, data, options) {
|
|
509
628
|
this.auditMissingTenant(object, "update", options);
|
|
510
629
|
const builder = this.getBuilder(object, options).where("id", id);
|
|
511
630
|
this.applyTenantScope(builder, object, options);
|
|
512
|
-
const formatted = this.formatInput(object, data);
|
|
631
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, data));
|
|
513
632
|
if (this.tablesWithTimestamps.has(object)) {
|
|
514
633
|
if (this.isSqlite) {
|
|
515
634
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -535,7 +654,7 @@ var SqlDriver = class {
|
|
|
535
654
|
this.auditMissingTenant(object, "upsert", options);
|
|
536
655
|
this.injectTenantOnInsert(object, toUpsert, options);
|
|
537
656
|
await this.fillAutoNumberFields(object, toUpsert, options);
|
|
538
|
-
const formatted = this.formatInput(object, toUpsert);
|
|
657
|
+
const formatted = this.applyWriteColumnMap(object, this.formatInput(object, toUpsert));
|
|
539
658
|
const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ["id"];
|
|
540
659
|
const builder = this.getBuilder(object, options);
|
|
541
660
|
await builder.insert(formatted).onConflict(mergeKeys).merge();
|
|
@@ -818,6 +937,89 @@ var SqlDriver = class {
|
|
|
818
937
|
/**
|
|
819
938
|
* Batch-initialise tables from an array of object definitions.
|
|
820
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
|
+
}
|
|
821
1023
|
async initObjects(objects) {
|
|
822
1024
|
var _a, _b;
|
|
823
1025
|
this.assertSchemaMutable("initObjects");
|
|
@@ -861,10 +1063,8 @@ var SqlDriver = class {
|
|
|
861
1063
|
if (type === "auto_number" || type === "autonumber") {
|
|
862
1064
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
863
1065
|
const fmt = rawFmt || "{0000}";
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
const prefix = m ? fmt.slice(0, m.index ?? 0) : fmt;
|
|
867
|
-
autoNumberCols.push({ name, format: fmt, prefix, padWidth, tenantField });
|
|
1066
|
+
const tokens = parseAutonumberFormat(fmt);
|
|
1067
|
+
autoNumberCols.push({ name, format: fmt, tokens, tenantField });
|
|
868
1068
|
}
|
|
869
1069
|
}
|
|
870
1070
|
}
|
|
@@ -1061,7 +1261,12 @@ var SqlDriver = class {
|
|
|
1061
1261
|
return this.knex;
|
|
1062
1262
|
}
|
|
1063
1263
|
getBuilder(object, options) {
|
|
1064
|
-
|
|
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
|
+
}
|
|
1065
1270
|
if (options?.transaction) {
|
|
1066
1271
|
builder = builder.transacting(options.transaction);
|
|
1067
1272
|
}
|
|
@@ -1160,6 +1365,20 @@ var SqlDriver = class {
|
|
|
1160
1365
|
if (typeof t === "string") return t;
|
|
1161
1366
|
return null;
|
|
1162
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
|
+
}
|
|
1163
1382
|
/**
|
|
1164
1383
|
* Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
|
|
1165
1384
|
* calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
|
|
@@ -1253,7 +1472,7 @@ var SqlDriver = class {
|
|
|
1253
1472
|
}
|
|
1254
1473
|
applyFilters(builder, filters) {
|
|
1255
1474
|
if (!filters) return;
|
|
1256
|
-
const table = this.
|
|
1475
|
+
const table = this.coercionKey(builder);
|
|
1257
1476
|
if (!Array.isArray(filters) && typeof filters === "object") {
|
|
1258
1477
|
const hasMongoOperators = Object.keys(filters).some(
|
|
1259
1478
|
(k) => k.startsWith("$") || typeof filters[k] === "object" && filters[k] !== null && Object.keys(filters[k]).some((op) => op.startsWith("$"))
|
|
@@ -1264,7 +1483,7 @@ var SqlDriver = class {
|
|
|
1264
1483
|
}
|
|
1265
1484
|
for (const [key, value] of Object.entries(filters)) {
|
|
1266
1485
|
if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
|
|
1267
|
-
builder.where(key, this.coerceFilterValue(table, key, value));
|
|
1486
|
+
builder.where(this.remoteColumn(table, key, key), this.coerceFilterValue(table, key, value));
|
|
1268
1487
|
}
|
|
1269
1488
|
return;
|
|
1270
1489
|
}
|
|
@@ -1280,8 +1499,9 @@ var SqlDriver = class {
|
|
|
1280
1499
|
const [fieldRaw, op, value] = item;
|
|
1281
1500
|
const isCriterion = typeof fieldRaw === "string" && typeof op === "string";
|
|
1282
1501
|
if (isCriterion) {
|
|
1283
|
-
const
|
|
1284
|
-
const
|
|
1502
|
+
const localField = this.mapSortField(fieldRaw);
|
|
1503
|
+
const field = this.remoteColumn(table, fieldRaw, localField);
|
|
1504
|
+
const coerced = this.coerceFilterValue(table, localField, value);
|
|
1285
1505
|
const apply = (b) => {
|
|
1286
1506
|
const method = nextJoin === "or" ? "orWhere" : "where";
|
|
1287
1507
|
const methodIn = nextJoin === "or" ? "orWhereIn" : "whereIn";
|
|
@@ -1333,7 +1553,7 @@ var SqlDriver = class {
|
|
|
1333
1553
|
}
|
|
1334
1554
|
applyFilterCondition(builder, condition, logicalOp = "and", tableHint) {
|
|
1335
1555
|
if (!condition || typeof condition !== "object") return;
|
|
1336
|
-
const table = tableHint ?? this.
|
|
1556
|
+
const table = tableHint ?? this.coercionKey(builder);
|
|
1337
1557
|
for (const [key, value] of Object.entries(condition)) {
|
|
1338
1558
|
if (key === "$and" && Array.isArray(value)) {
|
|
1339
1559
|
builder.where((qb) => {
|
|
@@ -1353,10 +1573,11 @@ var SqlDriver = class {
|
|
|
1353
1573
|
}
|
|
1354
1574
|
});
|
|
1355
1575
|
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1356
|
-
const
|
|
1576
|
+
const localField = this.mapSortField(key);
|
|
1577
|
+
const field = this.remoteColumn(table, key, localField);
|
|
1357
1578
|
for (const [op, opValue] of Object.entries(value)) {
|
|
1358
1579
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1359
|
-
const coerced = this.coerceFilterValue(table,
|
|
1580
|
+
const coerced = this.coerceFilterValue(table, localField, opValue);
|
|
1360
1581
|
switch (op) {
|
|
1361
1582
|
case "$eq":
|
|
1362
1583
|
builder[method](field, coerced);
|
|
@@ -1394,9 +1615,10 @@ var SqlDriver = class {
|
|
|
1394
1615
|
}
|
|
1395
1616
|
}
|
|
1396
1617
|
} else {
|
|
1397
|
-
const
|
|
1618
|
+
const localField = this.mapSortField(key);
|
|
1619
|
+
const field = this.remoteColumn(table, key, localField);
|
|
1398
1620
|
const method = logicalOp === "or" ? "orWhere" : "where";
|
|
1399
|
-
builder[method](field, this.coerceFilterValue(table,
|
|
1621
|
+
builder[method](field, this.coerceFilterValue(table, localField, value));
|
|
1400
1622
|
}
|
|
1401
1623
|
}
|
|
1402
1624
|
}
|
|
@@ -1406,6 +1628,28 @@ var SqlDriver = class {
|
|
|
1406
1628
|
if (field === "updatedAt") return "updated_at";
|
|
1407
1629
|
return field;
|
|
1408
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
|
+
}
|
|
1409
1653
|
mapAggregateFunc(func) {
|
|
1410
1654
|
switch (func) {
|
|
1411
1655
|
case "count":
|
|
@@ -1662,6 +1906,15 @@ var SqlDriver = class {
|
|
|
1662
1906
|
}
|
|
1663
1907
|
formatOutput(object, data) {
|
|
1664
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
|
+
}
|
|
1665
1918
|
if (this.isSqlite) {
|
|
1666
1919
|
const jsonFields = this.jsonFields[object];
|
|
1667
1920
|
if (jsonFields && jsonFields.length > 0) {
|