@objectstack/driver-sql 9.10.0 → 9.11.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 +43 -8
- package/dist/index.d.ts +43 -8
- package/dist/index.js +118 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SchemaMode, QueryAST, DriverOptions } from '@objectstack/spec/data';
|
|
1
|
+
import { AutonumberToken, SchemaMode, QueryAST, DriverOptions } from '@objectstack/spec/data';
|
|
2
2
|
import { IDataDriver } from '@objectstack/spec/contracts';
|
|
3
3
|
import knex, { Knex } from 'knex';
|
|
4
4
|
|
|
@@ -119,12 +119,19 @@ declare class SqlDriver implements IDataDriver {
|
|
|
119
119
|
protected autoNumberFields: Record<string, Array<{
|
|
120
120
|
name: string;
|
|
121
121
|
format: string;
|
|
122
|
-
|
|
123
|
-
padWidth: number;
|
|
122
|
+
tokens: AutonumberToken[];
|
|
124
123
|
tenantField: string | null;
|
|
125
124
|
}>>;
|
|
126
125
|
/** Whether the sequences table has been ensured this process. */
|
|
127
126
|
protected sequencesTableReady: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Whether `_objectstack_sequences` is the current `key_hash`-keyed shape.
|
|
129
|
+
* Set on a fresh create or a successful in-place migration. If a legacy table
|
|
130
|
+
* could NOT be migrated, this stays false: fixed-prefix sequences (empty
|
|
131
|
+
* scope) keep working via the legacy `(object, tenant_id, field)` key, while a
|
|
132
|
+
* per-scope write raises an actionable error rather than corrupting counters.
|
|
133
|
+
*/
|
|
134
|
+
protected sequencesHasKeyHash: boolean;
|
|
128
135
|
/** In-flight ensure promise; deduplicates concurrent first calls. */
|
|
129
136
|
protected sequencesTableEnsurePromise: Promise<void> | null;
|
|
130
137
|
/**
|
|
@@ -214,8 +221,34 @@ declare class SqlDriver implements IDataDriver {
|
|
|
214
221
|
/**
|
|
215
222
|
* Ensure the sequence-counter table exists. Idempotent and cheap after
|
|
216
223
|
* the first call (cached via `sequencesTableReady`).
|
|
224
|
+
*
|
|
225
|
+
* The row key is `key_hash` — a SHA-256 of `(object, tenant_id, field, scope)`
|
|
226
|
+
* where `scope` is the rendered autonumber prefix (date/field tokens before
|
|
227
|
+
* the `{0000}` slot), so a new day/group/parent starts a fresh counter. A
|
|
228
|
+
* single 64-char hashed primary key (rather than the four raw columns, which
|
|
229
|
+
* blow past MySQL's 3072-byte index limit under utf8mb4 and bound how long a
|
|
230
|
+
* `{field}` scope may be) keys every dialect uniformly and lets `scope` be a
|
|
231
|
+
* generous non-indexed column. Fixed-prefix formats use the empty scope and
|
|
232
|
+
* keep their single global counter (backward compatible).
|
|
217
233
|
*/
|
|
218
234
|
protected ensureSequencesTable(): Promise<void>;
|
|
235
|
+
/** SHA-256 of the composite counter key — the table's single-column PK. */
|
|
236
|
+
protected sequenceKeyHash(object: string, tenantId: string, field: string, scope: string): string;
|
|
237
|
+
/** Create the current `key_hash`-keyed sequences table shape. */
|
|
238
|
+
protected createSequencesTable(table: string): Promise<void>;
|
|
239
|
+
/**
|
|
240
|
+
* Migrate a pre-existing `_objectstack_sequences` table to the current
|
|
241
|
+
* `key_hash`-keyed shape. Handles both the original 3-column table (no
|
|
242
|
+
* `scope`) and an interim 4-column `(object, tenant_id, field, scope)` table:
|
|
243
|
+
* every legacy row is read, its `key_hash` computed in app code (no portable
|
|
244
|
+
* SQL hash exists), and re-inserted into a freshly built table that then
|
|
245
|
+
* replaces the original. Idempotent — a no-op once `key_hash` is present.
|
|
246
|
+
*
|
|
247
|
+
* If the rebuild fails, `sequencesHasKeyHash` stays false: fixed-prefix
|
|
248
|
+
* sequences keep working via the legacy key and per-scope writes error
|
|
249
|
+
* actionably (see getNextSequenceValue), rather than corrupting data.
|
|
250
|
+
*/
|
|
251
|
+
protected ensureSequencesKeyHashShape(): Promise<void>;
|
|
219
252
|
/**
|
|
220
253
|
* Bootstrap helper: scan the data table for the highest numeric suffix
|
|
221
254
|
* matching `prefix` (optionally scoped to a tenant). Used the first time
|
|
@@ -237,12 +270,14 @@ declare class SqlDriver implements IDataDriver {
|
|
|
237
270
|
* Gaps are tolerated by design — a rolled-back insert "burns" a number,
|
|
238
271
|
* matching standard sequence semantics.
|
|
239
272
|
*/
|
|
240
|
-
protected getNextSequenceValue(object: string, tableName: string, field: string, prefix: string, tenantField: string | null, tenantId: string | null, parentTrx?: Knex.Transaction): Promise<number>;
|
|
273
|
+
protected getNextSequenceValue(object: string, tableName: string, field: string, prefix: string, tenantField: string | null, tenantId: string | null, parentTrx?: Knex.Transaction, scope?: string): Promise<number>;
|
|
241
274
|
/**
|
|
242
|
-
* For each `auto_number` field
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
275
|
+
* For each `auto_number` field the caller left empty, render the format and
|
|
276
|
+
* reserve the next counter value. The counter is scoped to the rendered
|
|
277
|
+
* prefix (date tokens like `{YYYYMMDD}` in the request's business timezone,
|
|
278
|
+
* plus `{field}` interpolation from the row), so it resets per period/group;
|
|
279
|
+
* the full rendered prefix bootstraps the counter from existing data, and the
|
|
280
|
+
* tenant scopes it for isolation.
|
|
246
281
|
*/
|
|
247
282
|
protected fillAutoNumberFields(object: string, row: Record<string, any>, options?: DriverOptions): Promise<void>;
|
|
248
283
|
update(object: string, id: string | number, data: Record<string, any>, options?: DriverOptions): Promise<any>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SchemaMode, QueryAST, DriverOptions } from '@objectstack/spec/data';
|
|
1
|
+
import { AutonumberToken, SchemaMode, QueryAST, DriverOptions } from '@objectstack/spec/data';
|
|
2
2
|
import { IDataDriver } from '@objectstack/spec/contracts';
|
|
3
3
|
import knex, { Knex } from 'knex';
|
|
4
4
|
|
|
@@ -119,12 +119,19 @@ declare class SqlDriver implements IDataDriver {
|
|
|
119
119
|
protected autoNumberFields: Record<string, Array<{
|
|
120
120
|
name: string;
|
|
121
121
|
format: string;
|
|
122
|
-
|
|
123
|
-
padWidth: number;
|
|
122
|
+
tokens: AutonumberToken[];
|
|
124
123
|
tenantField: string | null;
|
|
125
124
|
}>>;
|
|
126
125
|
/** Whether the sequences table has been ensured this process. */
|
|
127
126
|
protected sequencesTableReady: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Whether `_objectstack_sequences` is the current `key_hash`-keyed shape.
|
|
129
|
+
* Set on a fresh create or a successful in-place migration. If a legacy table
|
|
130
|
+
* could NOT be migrated, this stays false: fixed-prefix sequences (empty
|
|
131
|
+
* scope) keep working via the legacy `(object, tenant_id, field)` key, while a
|
|
132
|
+
* per-scope write raises an actionable error rather than corrupting counters.
|
|
133
|
+
*/
|
|
134
|
+
protected sequencesHasKeyHash: boolean;
|
|
128
135
|
/** In-flight ensure promise; deduplicates concurrent first calls. */
|
|
129
136
|
protected sequencesTableEnsurePromise: Promise<void> | null;
|
|
130
137
|
/**
|
|
@@ -214,8 +221,34 @@ declare class SqlDriver implements IDataDriver {
|
|
|
214
221
|
/**
|
|
215
222
|
* Ensure the sequence-counter table exists. Idempotent and cheap after
|
|
216
223
|
* the first call (cached via `sequencesTableReady`).
|
|
224
|
+
*
|
|
225
|
+
* The row key is `key_hash` — a SHA-256 of `(object, tenant_id, field, scope)`
|
|
226
|
+
* where `scope` is the rendered autonumber prefix (date/field tokens before
|
|
227
|
+
* the `{0000}` slot), so a new day/group/parent starts a fresh counter. A
|
|
228
|
+
* single 64-char hashed primary key (rather than the four raw columns, which
|
|
229
|
+
* blow past MySQL's 3072-byte index limit under utf8mb4 and bound how long a
|
|
230
|
+
* `{field}` scope may be) keys every dialect uniformly and lets `scope` be a
|
|
231
|
+
* generous non-indexed column. Fixed-prefix formats use the empty scope and
|
|
232
|
+
* keep their single global counter (backward compatible).
|
|
217
233
|
*/
|
|
218
234
|
protected ensureSequencesTable(): Promise<void>;
|
|
235
|
+
/** SHA-256 of the composite counter key — the table's single-column PK. */
|
|
236
|
+
protected sequenceKeyHash(object: string, tenantId: string, field: string, scope: string): string;
|
|
237
|
+
/** Create the current `key_hash`-keyed sequences table shape. */
|
|
238
|
+
protected createSequencesTable(table: string): Promise<void>;
|
|
239
|
+
/**
|
|
240
|
+
* Migrate a pre-existing `_objectstack_sequences` table to the current
|
|
241
|
+
* `key_hash`-keyed shape. Handles both the original 3-column table (no
|
|
242
|
+
* `scope`) and an interim 4-column `(object, tenant_id, field, scope)` table:
|
|
243
|
+
* every legacy row is read, its `key_hash` computed in app code (no portable
|
|
244
|
+
* SQL hash exists), and re-inserted into a freshly built table that then
|
|
245
|
+
* replaces the original. Idempotent — a no-op once `key_hash` is present.
|
|
246
|
+
*
|
|
247
|
+
* If the rebuild fails, `sequencesHasKeyHash` stays false: fixed-prefix
|
|
248
|
+
* sequences keep working via the legacy key and per-scope writes error
|
|
249
|
+
* actionably (see getNextSequenceValue), rather than corrupting data.
|
|
250
|
+
*/
|
|
251
|
+
protected ensureSequencesKeyHashShape(): Promise<void>;
|
|
219
252
|
/**
|
|
220
253
|
* Bootstrap helper: scan the data table for the highest numeric suffix
|
|
221
254
|
* matching `prefix` (optionally scoped to a tenant). Used the first time
|
|
@@ -237,12 +270,14 @@ declare class SqlDriver implements IDataDriver {
|
|
|
237
270
|
* Gaps are tolerated by design — a rolled-back insert "burns" a number,
|
|
238
271
|
* matching standard sequence semantics.
|
|
239
272
|
*/
|
|
240
|
-
protected getNextSequenceValue(object: string, tableName: string, field: string, prefix: string, tenantField: string | null, tenantId: string | null, parentTrx?: Knex.Transaction): Promise<number>;
|
|
273
|
+
protected getNextSequenceValue(object: string, tableName: string, field: string, prefix: string, tenantField: string | null, tenantId: string | null, parentTrx?: Knex.Transaction, scope?: string): Promise<number>;
|
|
241
274
|
/**
|
|
242
|
-
* For each `auto_number` field
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
275
|
+
* For each `auto_number` field the caller left empty, render the format and
|
|
276
|
+
* reserve the next counter value. The counter is scoped to the rendered
|
|
277
|
+
* prefix (date tokens like `{YYYYMMDD}` in the request's business timezone,
|
|
278
|
+
* plus `{field}` interpolation from the row), so it resets per period/group;
|
|
279
|
+
* the full rendered prefix bootstraps the counter from existing data, and the
|
|
280
|
+
* tenant scopes it for isolation.
|
|
246
281
|
*/
|
|
247
282
|
protected fillAutoNumberFields(object: string, row: Record<string, any>, options?: DriverOptions): Promise<void>;
|
|
248
283
|
update(object: string, id: string | number, data: Record<string, any>, options?: DriverOptions): Promise<any>;
|
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"));
|
|
@@ -103,6 +104,14 @@ var SqlDriver = class {
|
|
|
103
104
|
this.autoNumberFields = {};
|
|
104
105
|
/** Whether the sequences table has been ensured this process. */
|
|
105
106
|
this.sequencesTableReady = false;
|
|
107
|
+
/**
|
|
108
|
+
* Whether `_objectstack_sequences` is the current `key_hash`-keyed shape.
|
|
109
|
+
* Set on a fresh create or a successful in-place migration. If a legacy table
|
|
110
|
+
* could NOT be migrated, this stays false: fixed-prefix sequences (empty
|
|
111
|
+
* scope) keep working via the legacy `(object, tenant_id, field)` key, while a
|
|
112
|
+
* per-scope write raises an actionable error rather than corrupting counters.
|
|
113
|
+
*/
|
|
114
|
+
this.sequencesHasKeyHash = false;
|
|
106
115
|
/** In-flight ensure promise; deduplicates concurrent first calls. */
|
|
107
116
|
this.sequencesTableEnsurePromise = null;
|
|
108
117
|
/**
|
|
@@ -410,6 +419,15 @@ var SqlDriver = class {
|
|
|
410
419
|
/**
|
|
411
420
|
* Ensure the sequence-counter table exists. Idempotent and cheap after
|
|
412
421
|
* the first call (cached via `sequencesTableReady`).
|
|
422
|
+
*
|
|
423
|
+
* The row key is `key_hash` — a SHA-256 of `(object, tenant_id, field, scope)`
|
|
424
|
+
* where `scope` is the rendered autonumber prefix (date/field tokens before
|
|
425
|
+
* the `{0000}` slot), so a new day/group/parent starts a fresh counter. A
|
|
426
|
+
* single 64-char hashed primary key (rather than the four raw columns, which
|
|
427
|
+
* blow past MySQL's 3072-byte index limit under utf8mb4 and bound how long a
|
|
428
|
+
* `{field}` scope may be) keys every dialect uniformly and lets `scope` be a
|
|
429
|
+
* generous non-indexed column. Fixed-prefix formats use the empty scope and
|
|
430
|
+
* keep their single global counter (backward compatible).
|
|
413
431
|
*/
|
|
414
432
|
async ensureSequencesTable() {
|
|
415
433
|
if (this.sequencesTableReady) return;
|
|
@@ -421,18 +439,15 @@ var SqlDriver = class {
|
|
|
421
439
|
const exists = await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
422
440
|
if (!exists) {
|
|
423
441
|
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
|
-
});
|
|
442
|
+
await this.createSequencesTable(SEQUENCES_TABLE);
|
|
443
|
+
this.sequencesHasKeyHash = true;
|
|
432
444
|
} catch (err) {
|
|
433
445
|
const stillMissing = !await this.knex.schema.hasTable(SEQUENCES_TABLE);
|
|
434
446
|
if (stillMissing) throw err;
|
|
447
|
+
await this.ensureSequencesKeyHashShape();
|
|
435
448
|
}
|
|
449
|
+
} else {
|
|
450
|
+
await this.ensureSequencesKeyHashShape();
|
|
436
451
|
}
|
|
437
452
|
this.sequencesTableReady = true;
|
|
438
453
|
})();
|
|
@@ -442,6 +457,71 @@ var SqlDriver = class {
|
|
|
442
457
|
this.sequencesTableEnsurePromise = null;
|
|
443
458
|
}
|
|
444
459
|
}
|
|
460
|
+
/** SHA-256 of the composite counter key — the table's single-column PK. */
|
|
461
|
+
sequenceKeyHash(object, tenantId, field, scope) {
|
|
462
|
+
return (0, import_node_crypto.createHash)("sha256").update(`${object}${tenantId}${field}${scope}`).digest("hex");
|
|
463
|
+
}
|
|
464
|
+
/** Create the current `key_hash`-keyed sequences table shape. */
|
|
465
|
+
async createSequencesTable(table) {
|
|
466
|
+
await this.knex.schema.createTable(table, (t) => {
|
|
467
|
+
t.string("key_hash", 64).notNullable().primary();
|
|
468
|
+
t.string("object").notNullable();
|
|
469
|
+
t.string("tenant_id").notNullable();
|
|
470
|
+
t.string("field").notNullable();
|
|
471
|
+
t.string("scope", 1024).notNullable().defaultTo("");
|
|
472
|
+
t.bigInteger("last_value").notNullable().defaultTo(0);
|
|
473
|
+
t.timestamp("updated_at").defaultTo(this.knex.fn.now());
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Migrate a pre-existing `_objectstack_sequences` table to the current
|
|
478
|
+
* `key_hash`-keyed shape. Handles both the original 3-column table (no
|
|
479
|
+
* `scope`) and an interim 4-column `(object, tenant_id, field, scope)` table:
|
|
480
|
+
* every legacy row is read, its `key_hash` computed in app code (no portable
|
|
481
|
+
* SQL hash exists), and re-inserted into a freshly built table that then
|
|
482
|
+
* replaces the original. Idempotent — a no-op once `key_hash` is present.
|
|
483
|
+
*
|
|
484
|
+
* If the rebuild fails, `sequencesHasKeyHash` stays false: fixed-prefix
|
|
485
|
+
* sequences keep working via the legacy key and per-scope writes error
|
|
486
|
+
* actionably (see getNextSequenceValue), rather than corrupting data.
|
|
487
|
+
*/
|
|
488
|
+
async ensureSequencesKeyHashShape() {
|
|
489
|
+
if (await this.knex.schema.hasColumn(SEQUENCES_TABLE, "key_hash")) {
|
|
490
|
+
this.sequencesHasKeyHash = true;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const hasScope = await this.knex.schema.hasColumn(SEQUENCES_TABLE, "scope");
|
|
494
|
+
const TMP = `${SEQUENCES_TABLE}__rebuild`;
|
|
495
|
+
try {
|
|
496
|
+
const rows = await this.knex(SEQUENCES_TABLE).select("*");
|
|
497
|
+
await this.knex.schema.dropTableIfExists(TMP);
|
|
498
|
+
await this.createSequencesTable(TMP);
|
|
499
|
+
const migrated = rows.map((r) => {
|
|
500
|
+
const scope = hasScope && r.scope != null ? String(r.scope) : "";
|
|
501
|
+
return {
|
|
502
|
+
key_hash: this.sequenceKeyHash(String(r.object), String(r.tenant_id), String(r.field), scope),
|
|
503
|
+
object: r.object,
|
|
504
|
+
tenant_id: r.tenant_id,
|
|
505
|
+
field: r.field,
|
|
506
|
+
scope,
|
|
507
|
+
last_value: r.last_value ?? 0,
|
|
508
|
+
updated_at: r.updated_at ?? this.knex.fn.now()
|
|
509
|
+
};
|
|
510
|
+
});
|
|
511
|
+
if (migrated.length > 0) await this.knex(TMP).insert(migrated);
|
|
512
|
+
await this.knex.schema.dropTable(SEQUENCES_TABLE);
|
|
513
|
+
await this.knex.schema.renameTable(TMP, SEQUENCES_TABLE);
|
|
514
|
+
this.sequencesHasKeyHash = true;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
this.sequencesHasKeyHash = false;
|
|
517
|
+
await this.knex.schema.dropTableIfExists(TMP).catch(() => {
|
|
518
|
+
});
|
|
519
|
+
this.logger.warn(
|
|
520
|
+
`[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.`,
|
|
521
|
+
{ error: String(err) }
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
445
525
|
/**
|
|
446
526
|
* Bootstrap helper: scan the data table for the highest numeric suffix
|
|
447
527
|
* matching `prefix` (optionally scoped to a tenant). Used the first time
|
|
@@ -479,10 +559,16 @@ var SqlDriver = class {
|
|
|
479
559
|
* Gaps are tolerated by design — a rolled-back insert "burns" a number,
|
|
480
560
|
* matching standard sequence semantics.
|
|
481
561
|
*/
|
|
482
|
-
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx) {
|
|
562
|
+
async getNextSequenceValue(object, tableName, field, prefix, tenantField, tenantId, parentTrx, scope = "") {
|
|
483
563
|
await this.ensureSequencesTable();
|
|
484
564
|
const resolvedTenantId = tenantField && tenantId ? String(tenantId) : GLOBAL_TENANT;
|
|
485
|
-
|
|
565
|
+
if (scope !== "" && !this.sequencesHasKeyHash) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`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.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const key = this.sequencesHasKeyHash ? { key_hash: this.sequenceKeyHash(tableName, resolvedTenantId, field, scope) } : { object: tableName, tenant_id: resolvedTenantId, field };
|
|
571
|
+
const insertRow = this.sequencesHasKeyHash ? { ...key, object: tableName, tenant_id: resolvedTenantId, field, scope } : { ...key };
|
|
486
572
|
const runner = parentTrx ?? this.knex;
|
|
487
573
|
return runner.transaction(async (trx) => {
|
|
488
574
|
let existing;
|
|
@@ -502,7 +588,7 @@ var SqlDriver = class {
|
|
|
502
588
|
);
|
|
503
589
|
const initial = seedMax + 1;
|
|
504
590
|
try {
|
|
505
|
-
await trx(SEQUENCES_TABLE).insert({ ...
|
|
591
|
+
await trx(SEQUENCES_TABLE).insert({ ...insertRow, last_value: initial });
|
|
506
592
|
return initial;
|
|
507
593
|
} catch (err) {
|
|
508
594
|
existing = await trx(SEQUENCES_TABLE).where(key).forUpdate().first();
|
|
@@ -515,31 +601,43 @@ var SqlDriver = class {
|
|
|
515
601
|
});
|
|
516
602
|
}
|
|
517
603
|
/**
|
|
518
|
-
* For each `auto_number` field
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
604
|
+
* For each `auto_number` field the caller left empty, render the format and
|
|
605
|
+
* reserve the next counter value. The counter is scoped to the rendered
|
|
606
|
+
* prefix (date tokens like `{YYYYMMDD}` in the request's business timezone,
|
|
607
|
+
* plus `{field}` interpolation from the row), so it resets per period/group;
|
|
608
|
+
* the full rendered prefix bootstraps the counter from existing data, and the
|
|
609
|
+
* tenant scopes it for isolation.
|
|
522
610
|
*/
|
|
523
611
|
async fillAutoNumberFields(object, row, options) {
|
|
524
612
|
const tableName = import_system.StorageNameMapping.resolveTableName({ name: object });
|
|
525
613
|
const cfgs = this.autoNumberFields[tableName] || this.autoNumberFields[object];
|
|
526
614
|
if (!cfgs || cfgs.length === 0) return;
|
|
527
615
|
const parentTrx = options?.transaction;
|
|
616
|
+
const timezone = options?.timezone;
|
|
617
|
+
const now = /* @__PURE__ */ new Date();
|
|
528
618
|
for (const cfg of cfgs) {
|
|
529
619
|
if (row[cfg.name] !== void 0 && row[cfg.name] !== null && row[cfg.name] !== "") continue;
|
|
620
|
+
const missing = (0, import_data.missingFieldValues)(cfg.tokens, row);
|
|
621
|
+
if (missing.length > 0) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`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.`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
530
626
|
const rowTenant = cfg.tenantField ? row[cfg.tenantField] : void 0;
|
|
531
627
|
const optTenant = options?.tenantId;
|
|
532
628
|
const tenantId = rowTenant != null && rowTenant !== "" ? String(rowTenant) : optTenant != null && optTenant !== "" ? String(optTenant) : null;
|
|
629
|
+
const probe = (0, import_data.renderAutonumber)({ tokens: cfg.tokens, seq: 0, record: row, now, timezone });
|
|
533
630
|
const next = await this.getNextSequenceValue(
|
|
534
631
|
object,
|
|
535
632
|
tableName,
|
|
536
633
|
cfg.name,
|
|
537
|
-
|
|
634
|
+
probe.prefix,
|
|
538
635
|
cfg.tenantField,
|
|
539
636
|
tenantId,
|
|
540
|
-
parentTrx
|
|
637
|
+
parentTrx,
|
|
638
|
+
probe.scope
|
|
541
639
|
);
|
|
542
|
-
row[cfg.name] =
|
|
640
|
+
row[cfg.name] = (0, import_data.renderAutonumber)({ tokens: cfg.tokens, seq: next, record: row, now, timezone }).value;
|
|
543
641
|
}
|
|
544
642
|
}
|
|
545
643
|
async update(object, id, data, options) {
|
|
@@ -898,10 +996,8 @@ var SqlDriver = class {
|
|
|
898
996
|
if (type === "auto_number" || type === "autonumber") {
|
|
899
997
|
const rawFmt = typeof field.autonumberFormat === "string" && field.autonumberFormat ? field.autonumberFormat : typeof field.format === "string" && field.format ? field.format : "";
|
|
900
998
|
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 });
|
|
999
|
+
const tokens = (0, import_data.parseAutonumberFormat)(fmt);
|
|
1000
|
+
autoNumberCols.push({ name, format: fmt, tokens, tenantField });
|
|
905
1001
|
}
|
|
906
1002
|
}
|
|
907
1003
|
}
|