tina4-nodejs 3.13.14 → 3.13.18
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/CLAUDE.md +42 -36
- package/package.json +1 -1
- package/packages/cli/src/commands/migrate.ts +7 -5
- package/packages/cli/src/commands/migrateRollback.ts +3 -3
- package/packages/core/src/server.ts +1 -1
- package/packages/orm/src/adapters/postgres.ts +29 -0
- package/packages/orm/src/autoCrud.ts +32 -16
- package/packages/orm/src/baseModel.ts +233 -197
- package/packages/orm/src/database.ts +189 -70
- package/packages/orm/src/databaseResult.ts +24 -0
- package/packages/orm/src/index.ts +6 -0
- package/packages/orm/src/migration.ts +128 -75
- package/packages/orm/src/queryBuilder.ts +12 -9
- package/packages/orm/src/seeder.ts +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult } from "./types.js";
|
|
2
|
+
import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, FieldDefinition } from "./types.js";
|
|
3
3
|
import { DatabaseResult } from "./databaseResult.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -22,6 +22,109 @@ export function stripTrailingSemicolons(sql: string): string {
|
|
|
22
22
|
return stripped;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Adapter bridge helpers (v3.14.0, Option A).
|
|
27
|
+
*
|
|
28
|
+
* The public Database/BaseModel/QueryBuilder API is async so it works on the
|
|
29
|
+
* async adapters (PostgreSQL/MySQL/MSSQL/Firebird/Mongo). SQLite implements
|
|
30
|
+
* only the synchronous methods (`node:sqlite` is sync); the async adapters
|
|
31
|
+
* implement only the `*Async` variants and make the sync methods throw.
|
|
32
|
+
*
|
|
33
|
+
* Each helper prefers the adapter's `*Async` method when present and awaits it,
|
|
34
|
+
* otherwise falls back to the sync method. For SQLite the fallback resolves
|
|
35
|
+
* instantly; for async adapters the awaited promise does the real work. This is
|
|
36
|
+
* the single chokepoint every public read/write flows through.
|
|
37
|
+
*/
|
|
38
|
+
export async function adapterFetch<T = Record<string, unknown>>(
|
|
39
|
+
adapter: DatabaseAdapter, sql: string, params?: unknown[], limit?: number, skip?: number,
|
|
40
|
+
): Promise<T[]> {
|
|
41
|
+
return (adapter as any).fetchAsync
|
|
42
|
+
? await (adapter as any).fetchAsync(sql, params, limit, skip)
|
|
43
|
+
: adapter.fetch<T>(sql, params, limit, skip);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function adapterQuery<T = Record<string, unknown>>(
|
|
47
|
+
adapter: DatabaseAdapter, sql: string, params?: unknown[],
|
|
48
|
+
): Promise<T[]> {
|
|
49
|
+
return (adapter as any).queryAsync
|
|
50
|
+
? await (adapter as any).queryAsync(sql, params)
|
|
51
|
+
: adapter.query<T>(sql, params);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function adapterFetchOne<T = Record<string, unknown>>(
|
|
55
|
+
adapter: DatabaseAdapter, sql: string, params?: unknown[],
|
|
56
|
+
): Promise<T | null> {
|
|
57
|
+
return (adapter as any).fetchOneAsync
|
|
58
|
+
? await (adapter as any).fetchOneAsync(sql, params)
|
|
59
|
+
: adapter.fetchOne<T>(sql, params);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function adapterExecute(
|
|
63
|
+
adapter: DatabaseAdapter, sql: string, params?: unknown[],
|
|
64
|
+
): Promise<unknown> {
|
|
65
|
+
return (adapter as any).executeAsync
|
|
66
|
+
? await (adapter as any).executeAsync(sql, params)
|
|
67
|
+
: adapter.execute(sql, params);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function adapterStartTransaction(adapter: DatabaseAdapter): Promise<void> {
|
|
71
|
+
if ((adapter as any).startTransactionAsync) await (adapter as any).startTransactionAsync();
|
|
72
|
+
else adapter.startTransaction();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function adapterCommit(adapter: DatabaseAdapter): Promise<void> {
|
|
76
|
+
if ((adapter as any).commitAsync) await (adapter as any).commitAsync();
|
|
77
|
+
else adapter.commit();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function adapterRollback(adapter: DatabaseAdapter): Promise<void> {
|
|
81
|
+
if ((adapter as any).rollbackAsync) await (adapter as any).rollbackAsync();
|
|
82
|
+
else adapter.rollback();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function adapterTableExists(adapter: DatabaseAdapter, name: string): Promise<boolean> {
|
|
86
|
+
return (adapter as any).tableExistsAsync
|
|
87
|
+
? await (adapter as any).tableExistsAsync(name)
|
|
88
|
+
: adapter.tableExists(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function adapterTables(adapter: DatabaseAdapter): Promise<string[]> {
|
|
92
|
+
return (adapter as any).tablesAsync
|
|
93
|
+
? await (adapter as any).tablesAsync()
|
|
94
|
+
: adapter.tables();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function adapterColumns(adapter: DatabaseAdapter, table: string): Promise<ColumnInfo[]> {
|
|
98
|
+
return (adapter as any).columnsAsync
|
|
99
|
+
? await (adapter as any).columnsAsync(table)
|
|
100
|
+
: adapter.columns(table);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function adapterCreateTable(
|
|
104
|
+
adapter: DatabaseAdapter, name: string, columns: Record<string, FieldDefinition>,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
if ((adapter as any).createTableAsync) await (adapter as any).createTableAsync(name, columns);
|
|
107
|
+
else adapter.createTable(name, columns);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract the engine-assigned auto-increment id from an `execute()` result.
|
|
112
|
+
*
|
|
113
|
+
* SQLite returns `{ lastInsertRowid }`. PostgreSQL (pg) returns a result whose
|
|
114
|
+
* `rows[0].id` holds the value when the statement had a `RETURNING` clause
|
|
115
|
+
* (insertAsync adds one). MySQL/MSSQL adapters set the adapter's lastInsertId,
|
|
116
|
+
* so callers fall back to `adapter.lastInsertId()` when the result has neither.
|
|
117
|
+
*/
|
|
118
|
+
export function extractLastInsertId(result: unknown): number | bigint | null {
|
|
119
|
+
if (result && typeof result === "object") {
|
|
120
|
+
const r = result as any;
|
|
121
|
+
if (r.lastInsertRowid !== undefined && r.lastInsertRowid !== null) return r.lastInsertRowid;
|
|
122
|
+
if (r.rows?.[0]?.id !== undefined && r.rows[0].id !== null) return r.rows[0].id;
|
|
123
|
+
if (r.lastInsertId !== undefined && r.lastInsertId !== null) return r.lastInsertId;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
25
128
|
let activeAdapter: DatabaseAdapter | null = null;
|
|
26
129
|
const namedAdapters: Map<string, DatabaseAdapter> = new Map();
|
|
27
130
|
|
|
@@ -422,21 +525,38 @@ export class Database {
|
|
|
422
525
|
this.close();
|
|
423
526
|
}
|
|
424
527
|
|
|
425
|
-
/** Query rows with optional pagination. Returns a DatabaseResult wrapper.
|
|
426
|
-
|
|
528
|
+
/** Query rows with optional pagination. Returns a DatabaseResult wrapper.
|
|
529
|
+
*
|
|
530
|
+
* Async since v3.14.0 (Option A): the public API awaits the adapter's
|
|
531
|
+
* `*Async` method when present (PostgreSQL/MySQL/MSSQL/Firebird/Mongo) and
|
|
532
|
+
* falls back to the synchronous method for SQLite (`node:sqlite` is sync, so
|
|
533
|
+
* the fallback resolves instantly). This is the breaking change that makes
|
|
534
|
+
* the wrapper work uniformly across every engine.
|
|
535
|
+
*/
|
|
536
|
+
async fetch(sql: string, params?: unknown[], limit?: number, offset?: number): Promise<DatabaseResult> {
|
|
427
537
|
// v3.13.12: strip trailing `;` before the adapter wraps with COUNT(*)
|
|
428
538
|
// or appends LIMIT/OFFSET. Without this, `"SELECT * FROM t;"` becomes
|
|
429
539
|
// `"SELECT * FROM t; LIMIT 100 OFFSET 0"` — a syntax error.
|
|
430
540
|
sql = stripTrailingSemicolons(sql);
|
|
431
541
|
const adapter = this.getNextAdapter();
|
|
432
|
-
|
|
433
|
-
|
|
542
|
+
try {
|
|
543
|
+
const rows = await adapterFetch(adapter, sql, params, limit, offset);
|
|
544
|
+
this.lastError = null;
|
|
545
|
+
return new DatabaseResult(rows, undefined, undefined, limit, offset, adapter, sql);
|
|
546
|
+
} catch (e: any) {
|
|
547
|
+
// v3.13.11 #49.2: fetch() records last_error like execute() does.
|
|
548
|
+
this.lastError = e?.message ?? String(e);
|
|
549
|
+
throw e;
|
|
550
|
+
}
|
|
434
551
|
}
|
|
435
552
|
|
|
436
553
|
/** Fetch a single row or null. */
|
|
437
|
-
fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
|
|
554
|
+
async fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
438
555
|
sql = stripTrailingSemicolons(sql);
|
|
439
|
-
|
|
556
|
+
const adapter = this.getNextAdapter();
|
|
557
|
+
return (adapter as any).fetchOneAsync
|
|
558
|
+
? await (adapter as any).fetchOneAsync<T>(sql, params)
|
|
559
|
+
: adapter.fetchOne<T>(sql, params);
|
|
440
560
|
}
|
|
441
561
|
|
|
442
562
|
/**
|
|
@@ -452,20 +572,20 @@ export class Database {
|
|
|
452
572
|
* Returns `[]` (not `null`) when no rows match. Cross-framework parity
|
|
453
573
|
* with Python `db.fetch_all()`, PHP `$db->fetchAll()`, and Ruby `db.fetch_all`.
|
|
454
574
|
*/
|
|
455
|
-
fetchAll<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, offset?: number): T[] {
|
|
456
|
-
return this.fetch(sql, params, limit, offset).records as T[];
|
|
575
|
+
async fetchAll<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, offset?: number): Promise<T[]> {
|
|
576
|
+
return (await this.fetch(sql, params, limit, offset)).records as T[];
|
|
457
577
|
}
|
|
458
578
|
|
|
459
579
|
/**
|
|
460
580
|
* Execute a write statement. Returns true/false for simple writes.
|
|
461
581
|
* If SQL contains RETURNING, CALL, EXEC, or SELECT, returns the result set.
|
|
462
582
|
*/
|
|
463
|
-
execute(sql: string, params?: unknown[]): boolean | unknown {
|
|
583
|
+
async execute(sql: string, params?: unknown[]): Promise<boolean | unknown> {
|
|
464
584
|
try {
|
|
465
585
|
const adapter = this.getNextAdapter();
|
|
466
|
-
const result = adapter
|
|
586
|
+
const result = await adapterExecute(adapter, sql, params);
|
|
467
587
|
if (this.autoCommit) {
|
|
468
|
-
try { adapter
|
|
588
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
469
589
|
}
|
|
470
590
|
this.lastError = null;
|
|
471
591
|
const upper = sql.trim().toUpperCase();
|
|
@@ -481,31 +601,37 @@ export class Database {
|
|
|
481
601
|
}
|
|
482
602
|
|
|
483
603
|
/** Insert a row into a table. */
|
|
484
|
-
insert(table: string, data: Record<string, unknown>): DatabaseWriteResult {
|
|
604
|
+
async insert(table: string, data: Record<string, unknown>): Promise<DatabaseWriteResult> {
|
|
485
605
|
const adapter = this.getNextAdapter();
|
|
486
|
-
const result = adapter
|
|
606
|
+
const result = (adapter as any).insertAsync
|
|
607
|
+
? await (adapter as any).insertAsync(table, data)
|
|
608
|
+
: adapter.insert(table, data);
|
|
487
609
|
if (this.autoCommit) {
|
|
488
|
-
try { adapter
|
|
610
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
489
611
|
}
|
|
490
612
|
return result;
|
|
491
613
|
}
|
|
492
614
|
|
|
493
615
|
/** Update rows in a table matching filter. */
|
|
494
|
-
update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown>, params?: unknown[]): DatabaseWriteResult {
|
|
616
|
+
async update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown>, params?: unknown[]): Promise<DatabaseWriteResult> {
|
|
495
617
|
const adapter = this.getNextAdapter();
|
|
496
|
-
const result = adapter
|
|
618
|
+
const result = (adapter as any).updateAsync
|
|
619
|
+
? await (adapter as any).updateAsync(table, data, filter ?? {}, params)
|
|
620
|
+
: adapter.update(table, data, filter ?? {}, params);
|
|
497
621
|
if (this.autoCommit) {
|
|
498
|
-
try { adapter
|
|
622
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
499
623
|
}
|
|
500
624
|
return result;
|
|
501
625
|
}
|
|
502
626
|
|
|
503
627
|
/** Delete rows from a table matching filter. */
|
|
504
|
-
delete(table: string, filter?: Record<string, unknown>, params?: unknown[]): DatabaseWriteResult {
|
|
628
|
+
async delete(table: string, filter?: Record<string, unknown>, params?: unknown[]): Promise<DatabaseWriteResult> {
|
|
505
629
|
const adapter = this.getNextAdapter();
|
|
506
|
-
const result = adapter
|
|
630
|
+
const result = (adapter as any).deleteAsync
|
|
631
|
+
? await (adapter as any).deleteAsync(table, filter ?? {}, params)
|
|
632
|
+
: adapter.delete(table, filter ?? {}, params);
|
|
507
633
|
if (this.autoCommit) {
|
|
508
|
-
try { adapter
|
|
634
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
509
635
|
}
|
|
510
636
|
return result;
|
|
511
637
|
}
|
|
@@ -529,7 +655,7 @@ export class Database {
|
|
|
529
655
|
* the whole transaction so executes and the final commit/rollback all run
|
|
530
656
|
* on the same connection (critical when pool > 0).
|
|
531
657
|
*/
|
|
532
|
-
startTransaction(): void {
|
|
658
|
+
async startTransaction(): Promise<void> {
|
|
533
659
|
// Pick an adapter using the normal selection logic, then pin it.
|
|
534
660
|
const adapter = this.getNextAdapter();
|
|
535
661
|
let store = this.txStore.getStore();
|
|
@@ -538,33 +664,33 @@ export class Database {
|
|
|
538
664
|
} else {
|
|
539
665
|
this.txStore.enterWith({ adapter });
|
|
540
666
|
}
|
|
541
|
-
adapter
|
|
667
|
+
await adapterStartTransaction(adapter);
|
|
542
668
|
}
|
|
543
669
|
|
|
544
670
|
/** Commit the current transaction and release the adapter pin. */
|
|
545
|
-
commit(): void {
|
|
671
|
+
async commit(): Promise<void> {
|
|
546
672
|
const adapter = this.getNextAdapter();
|
|
547
|
-
adapter
|
|
673
|
+
await adapterCommit(adapter);
|
|
548
674
|
const store = this.txStore.getStore();
|
|
549
675
|
if (store) store.adapter = null;
|
|
550
676
|
}
|
|
551
677
|
|
|
552
678
|
/** Rollback the current transaction and release the adapter pin. */
|
|
553
|
-
rollback(): void {
|
|
679
|
+
async rollback(): Promise<void> {
|
|
554
680
|
const adapter = this.getNextAdapter();
|
|
555
|
-
adapter
|
|
681
|
+
await adapterRollback(adapter);
|
|
556
682
|
const store = this.txStore.getStore();
|
|
557
683
|
if (store) store.adapter = null;
|
|
558
684
|
}
|
|
559
685
|
|
|
560
686
|
/** Check if a table exists. */
|
|
561
|
-
tableExists(name: string): boolean {
|
|
562
|
-
return this.getNextAdapter()
|
|
687
|
+
async tableExists(name: string): Promise<boolean> {
|
|
688
|
+
return adapterTableExists(this.getNextAdapter(), name);
|
|
563
689
|
}
|
|
564
690
|
|
|
565
691
|
/** List all tables in the database. */
|
|
566
|
-
getTables(): string[] {
|
|
567
|
-
return this.getNextAdapter()
|
|
692
|
+
async getTables(): Promise<string[]> {
|
|
693
|
+
return adapterTables(this.getNextAdapter());
|
|
568
694
|
}
|
|
569
695
|
|
|
570
696
|
/**
|
|
@@ -575,8 +701,8 @@ export class Database {
|
|
|
575
701
|
* @param tableName - Name of the table to inspect.
|
|
576
702
|
* @returns Array of column info objects: { name, type, nullable, default, primaryKey }.
|
|
577
703
|
*/
|
|
578
|
-
getColumns(tableName: string): { name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean }[] {
|
|
579
|
-
return this.getNextAdapter()
|
|
704
|
+
async getColumns(tableName: string): Promise<{ name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean }[]> {
|
|
705
|
+
return adapterColumns(this.getNextAdapter(), tableName);
|
|
580
706
|
}
|
|
581
707
|
|
|
582
708
|
/**
|
|
@@ -587,18 +713,18 @@ export class Database {
|
|
|
587
713
|
* @param paramSets - Array of parameter arrays, one per execution.
|
|
588
714
|
* @returns Array of results from each execution.
|
|
589
715
|
*/
|
|
590
|
-
executeMany(sql: string, paramSets: unknown[][] = []): unknown[] {
|
|
716
|
+
async executeMany(sql: string, paramSets: unknown[][] = []): Promise<unknown[]> {
|
|
591
717
|
const adapter = this.getNextAdapter();
|
|
592
718
|
const results: unknown[] = [];
|
|
593
719
|
|
|
594
|
-
adapter
|
|
720
|
+
await adapterStartTransaction(adapter);
|
|
595
721
|
try {
|
|
596
722
|
for (const params of paramSets) {
|
|
597
|
-
results.push(adapter
|
|
723
|
+
results.push(await adapterExecute(adapter, sql, params));
|
|
598
724
|
}
|
|
599
|
-
adapter
|
|
725
|
+
await adapterCommit(adapter);
|
|
600
726
|
} catch (e) {
|
|
601
|
-
adapter
|
|
727
|
+
await adapterRollback(adapter);
|
|
602
728
|
throw e;
|
|
603
729
|
}
|
|
604
730
|
|
|
@@ -639,24 +765,24 @@ export class Database {
|
|
|
639
765
|
* Used by sequenceNext() for race-safe ID generation on
|
|
640
766
|
* SQLite, MySQL, MSSQL, and as a PostgreSQL fallback.
|
|
641
767
|
*/
|
|
642
|
-
private ensureSequenceTable(): void {
|
|
768
|
+
private async ensureSequenceTable(): Promise<void> {
|
|
643
769
|
const adapter = this.getNextAdapter();
|
|
644
770
|
|
|
645
|
-
if (!adapter
|
|
771
|
+
if (!(await adapterTableExists(adapter, "tina4_sequences"))) {
|
|
646
772
|
if (this.dbType === "mssql") {
|
|
647
|
-
adapter
|
|
773
|
+
await adapterExecute(adapter,
|
|
648
774
|
"CREATE TABLE tina4_sequences (" +
|
|
649
775
|
"seq_name VARCHAR(200) NOT NULL PRIMARY KEY, " +
|
|
650
776
|
"current_value INTEGER NOT NULL DEFAULT 0)"
|
|
651
777
|
);
|
|
652
778
|
} else {
|
|
653
|
-
adapter
|
|
779
|
+
await adapterExecute(adapter,
|
|
654
780
|
"CREATE TABLE IF NOT EXISTS tina4_sequences (" +
|
|
655
781
|
"seq_name VARCHAR(200) NOT NULL PRIMARY KEY, " +
|
|
656
782
|
"current_value INTEGER NOT NULL DEFAULT 0)"
|
|
657
783
|
);
|
|
658
784
|
}
|
|
659
|
-
try { adapter
|
|
785
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
660
786
|
}
|
|
661
787
|
}
|
|
662
788
|
|
|
@@ -666,12 +792,12 @@ export class Database {
|
|
|
666
792
|
* If the sequence row doesn't exist yet, seeds it from MAX(pkColumn)
|
|
667
793
|
* of the given table (or 0 if the table is empty/missing).
|
|
668
794
|
*/
|
|
669
|
-
private sequenceNext(seqName: string, table?: string, pkColumn = "id"): number {
|
|
670
|
-
this.ensureSequenceTable();
|
|
795
|
+
private async sequenceNext(seqName: string, table?: string, pkColumn = "id"): Promise<number> {
|
|
796
|
+
await this.ensureSequenceTable();
|
|
671
797
|
const adapter = this.getNextAdapter();
|
|
672
798
|
|
|
673
799
|
// Check if the sequence row exists
|
|
674
|
-
const existing =
|
|
800
|
+
const existing = await adapterFetchOne<Record<string, unknown>>(adapter,
|
|
675
801
|
"SELECT current_value FROM tina4_sequences WHERE seq_name = ?",
|
|
676
802
|
[seqName]
|
|
677
803
|
);
|
|
@@ -681,7 +807,7 @@ export class Database {
|
|
|
681
807
|
let seedValue = 0;
|
|
682
808
|
if (table) {
|
|
683
809
|
try {
|
|
684
|
-
const maxRow =
|
|
810
|
+
const maxRow = await adapterFetchOne<Record<string, unknown>>(adapter,
|
|
685
811
|
`SELECT MAX(${pkColumn}) AS max_id FROM ${table}`
|
|
686
812
|
);
|
|
687
813
|
if (maxRow?.max_id != null) {
|
|
@@ -692,22 +818,22 @@ export class Database {
|
|
|
692
818
|
}
|
|
693
819
|
}
|
|
694
820
|
|
|
695
|
-
adapter
|
|
821
|
+
await adapterExecute(adapter,
|
|
696
822
|
"INSERT INTO tina4_sequences (seq_name, current_value) VALUES (?, ?)",
|
|
697
823
|
[seqName, seedValue]
|
|
698
824
|
);
|
|
699
|
-
try { adapter
|
|
825
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
700
826
|
}
|
|
701
827
|
|
|
702
828
|
// Atomic increment
|
|
703
|
-
adapter
|
|
829
|
+
await adapterExecute(adapter,
|
|
704
830
|
"UPDATE tina4_sequences SET current_value = current_value + 1 WHERE seq_name = ?",
|
|
705
831
|
[seqName]
|
|
706
832
|
);
|
|
707
|
-
try { adapter
|
|
833
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
708
834
|
|
|
709
835
|
// Read the new value
|
|
710
|
-
const row =
|
|
836
|
+
const row = await adapterFetchOne<Record<string, unknown>>(adapter,
|
|
711
837
|
"SELECT current_value FROM tina4_sequences WHERE seq_name = ?",
|
|
712
838
|
[seqName]
|
|
713
839
|
);
|
|
@@ -724,19 +850,12 @@ export class Database {
|
|
|
724
850
|
* (race-safe, replaces old MAX+1).
|
|
725
851
|
* - Returns 1 if the table is empty or does not exist.
|
|
726
852
|
*/
|
|
727
|
-
getNextId(table: string, pkColumn = "id", generatorName?: string): number {
|
|
853
|
+
async getNextId(table: string, pkColumn = "id", generatorName?: string): Promise<number> {
|
|
728
854
|
const adapter = this.getNextAdapter();
|
|
729
855
|
|
|
730
|
-
// MongoDB
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
if (this.dbType === "mongodb") {
|
|
734
|
-
throw new Error(
|
|
735
|
-
"getNextId() is not supported for MongoDB (async adapter). " +
|
|
736
|
-
"MongoDB uses ObjectId for _id by default. " +
|
|
737
|
-
"For integer sequences, use getNextIdAsync() or let MongoDB generate _id automatically.",
|
|
738
|
-
);
|
|
739
|
-
}
|
|
856
|
+
// MongoDB uses ObjectId for _id by default; integer sequences fall through
|
|
857
|
+
// to the tina4_sequences table strategy below (which works on Mongo too
|
|
858
|
+
// because the adapter implements the SQL-ish methods over collections).
|
|
740
859
|
|
|
741
860
|
// Firebird — use generators (atomic)
|
|
742
861
|
if (this.dbType === "firebird") {
|
|
@@ -744,12 +863,12 @@ export class Database {
|
|
|
744
863
|
|
|
745
864
|
// Auto-create the generator if it does not exist
|
|
746
865
|
try {
|
|
747
|
-
adapter
|
|
866
|
+
await adapterExecute(adapter, `CREATE GENERATOR ${genName}`);
|
|
748
867
|
} catch {
|
|
749
868
|
// Generator already exists — ignore
|
|
750
869
|
}
|
|
751
870
|
|
|
752
|
-
const row =
|
|
871
|
+
const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT GEN_ID(${genName}, 1) AS NEXT_ID FROM RDB$DATABASE`);
|
|
753
872
|
return Number(row?.NEXT_ID ?? row?.next_id ?? 1);
|
|
754
873
|
}
|
|
755
874
|
|
|
@@ -757,7 +876,7 @@ export class Database {
|
|
|
757
876
|
if (this.dbType === "postgres") {
|
|
758
877
|
const seqName = generatorName ?? `${table.toLowerCase()}_${pkColumn.toLowerCase()}_seq`;
|
|
759
878
|
try {
|
|
760
|
-
const row =
|
|
879
|
+
const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT nextval('${seqName}') AS next_id`);
|
|
761
880
|
if (row?.next_id != null) {
|
|
762
881
|
return Number(row.next_id);
|
|
763
882
|
}
|
|
@@ -767,13 +886,13 @@ export class Database {
|
|
|
767
886
|
|
|
768
887
|
// Auto-create sequence seeded from MAX
|
|
769
888
|
try {
|
|
770
|
-
const maxRow =
|
|
889
|
+
const maxRow = await adapterFetchOne<Record<string, unknown>>(adapter,
|
|
771
890
|
`SELECT COALESCE(MAX(${pkColumn}), 0) AS max_id FROM ${table}`
|
|
772
891
|
);
|
|
773
892
|
const start = maxRow?.max_id != null ? Number(maxRow.max_id) + 1 : 1;
|
|
774
|
-
adapter
|
|
775
|
-
try { adapter
|
|
776
|
-
const row =
|
|
893
|
+
await adapterExecute(adapter, `CREATE SEQUENCE ${seqName} START WITH ${start}`);
|
|
894
|
+
try { await adapterCommit(adapter); } catch { /* no active transaction */ }
|
|
895
|
+
const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT nextval('${seqName}') AS next_id`);
|
|
777
896
|
if (row?.next_id != null) {
|
|
778
897
|
return Number(row.next_id);
|
|
779
898
|
}
|
|
@@ -26,6 +26,9 @@ export class DatabaseResult implements Iterable<Record<string, unknown>> {
|
|
|
26
26
|
private readonly _sql?: string;
|
|
27
27
|
private _columnInfoCache?: ColumnInfoResult[];
|
|
28
28
|
|
|
29
|
+
// Index signature so `result[0]` type-checks; the Proxy below makes it work.
|
|
30
|
+
[index: number]: Record<string, unknown> | undefined;
|
|
31
|
+
|
|
29
32
|
constructor(
|
|
30
33
|
records?: Record<string, unknown>[],
|
|
31
34
|
columns?: string[],
|
|
@@ -43,6 +46,27 @@ export class DatabaseResult implements Iterable<Record<string, unknown>> {
|
|
|
43
46
|
this.offset = offset ?? 0;
|
|
44
47
|
this._adapter = adapter;
|
|
45
48
|
this._sql = sql;
|
|
49
|
+
|
|
50
|
+
// Array-like numeric index access: `result[0]` returns records[0].
|
|
51
|
+
// A Proxy forwards integer-string keys to the backing records array so the
|
|
52
|
+
// documented `const first = result[0]` works, while every method/property
|
|
53
|
+
// on the instance still resolves normally. Mirrors Python's __getitem__ and
|
|
54
|
+
// PHP's ArrayAccess on DatabaseResult.
|
|
55
|
+
return new Proxy(this, {
|
|
56
|
+
get(target, prop, receiver) {
|
|
57
|
+
if (typeof prop === "string" && /^-?\d+$/.test(prop)) {
|
|
58
|
+
const i = Number(prop);
|
|
59
|
+
return target.records[i < 0 ? target.records.length + i : i];
|
|
60
|
+
}
|
|
61
|
+
return Reflect.get(target, prop, receiver);
|
|
62
|
+
},
|
|
63
|
+
has(target, prop) {
|
|
64
|
+
if (typeof prop === "string" && /^\d+$/.test(prop)) {
|
|
65
|
+
return Number(prop) < target.records.length;
|
|
66
|
+
}
|
|
67
|
+
return Reflect.has(target, prop);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
/** JSON string of records. */
|
|
@@ -15,6 +15,12 @@ export { FetchResult } from "./types.js";
|
|
|
15
15
|
export { DatabaseResult } from "./databaseResult.js";
|
|
16
16
|
export type { ColumnInfoResult } from "./databaseResult.js";
|
|
17
17
|
export { Database, initDatabase, getAdapter, setAdapter, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons } from "./database.js";
|
|
18
|
+
export {
|
|
19
|
+
adapterFetch, adapterQuery, adapterFetchOne, adapterExecute,
|
|
20
|
+
adapterStartTransaction, adapterCommit, adapterRollback,
|
|
21
|
+
adapterTableExists, adapterTables, adapterColumns, adapterCreateTable,
|
|
22
|
+
extractLastInsertId,
|
|
23
|
+
} from "./database.js";
|
|
18
24
|
export type { DatabaseConfig, ParsedDatabaseUrl } from "./database.js";
|
|
19
25
|
export { discoverModels } from "./model.js";
|
|
20
26
|
export type { DiscoveredModel } from "./model.js";
|