tina4-nodejs 3.13.12 → 3.13.16
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/logger.ts +21 -8
- package/packages/core/src/middleware.ts +24 -3
- package/packages/core/src/server.ts +1 -1
- package/packages/orm/src/adapters/mssql.ts +12 -4
- package/packages/orm/src/adapters/mysql.ts +13 -4
- package/packages/orm/src/adapters/postgres.ts +18 -8
- package/packages/orm/src/adapters/sqlite.ts +22 -2
- package/packages/orm/src/autoCrud.ts +32 -16
- package/packages/orm/src/baseModel.ts +170 -187
- 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
- package/packages/orm/src/sqlTranslation.ts +16 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getAdapter, getNamedAdapter, setAdapter, parseDatabaseUrl,
|
|
3
|
+
adapterQuery, adapterFetch, adapterExecute, adapterFetchOne,
|
|
4
|
+
adapterStartTransaction, adapterCommit, adapterRollback,
|
|
5
|
+
adapterTableExists, adapterCreateTable, extractLastInsertId,
|
|
6
|
+
} from "./database.js";
|
|
2
7
|
import { validate as validateFields } from "./validation.js";
|
|
3
8
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
4
9
|
import { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
@@ -258,7 +263,7 @@ export class BaseModel {
|
|
|
258
263
|
* @param id Primary key value.
|
|
259
264
|
* @param include Optional array of relationship names to eager-load.
|
|
260
265
|
*/
|
|
261
|
-
static findById<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown, include?: string[]): T | null {
|
|
266
|
+
static async findById<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown, include?: string[]): Promise<T | null> {
|
|
262
267
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
263
268
|
const pkCol = ModelClass.getPkColumn();
|
|
264
269
|
let sql = `SELECT * FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`;
|
|
@@ -270,9 +275,9 @@ export class BaseModel {
|
|
|
270
275
|
sql += ` AND ${ModelClass.tableFilter}`;
|
|
271
276
|
}
|
|
272
277
|
|
|
273
|
-
const instance = ModelClass.selectOne<T>(sql, [id]);
|
|
278
|
+
const instance = await ModelClass.selectOne<T>(sql, [id]);
|
|
274
279
|
if (instance && include) {
|
|
275
|
-
ModelClass._eagerLoad([instance], include);
|
|
280
|
+
await ModelClass._eagerLoad([instance], include);
|
|
276
281
|
}
|
|
277
282
|
return instance;
|
|
278
283
|
}
|
|
@@ -283,12 +288,12 @@ export class BaseModel {
|
|
|
283
288
|
* Usage:
|
|
284
289
|
* const user = User.create({ name: "Alice", email: "alice@example.com" });
|
|
285
290
|
*/
|
|
286
|
-
static create<T extends BaseModel>(
|
|
291
|
+
static async create<T extends BaseModel>(
|
|
287
292
|
this: new (data?: Record<string, unknown>) => T,
|
|
288
293
|
data: Record<string, unknown> = {},
|
|
289
|
-
): T {
|
|
294
|
+
): Promise<T> {
|
|
290
295
|
const instance = new this(data) as T;
|
|
291
|
-
instance.save();
|
|
296
|
+
await instance.save();
|
|
292
297
|
return instance;
|
|
293
298
|
}
|
|
294
299
|
|
|
@@ -303,14 +308,14 @@ export class BaseModel {
|
|
|
303
308
|
*
|
|
304
309
|
* Use findById(id) for single-record primary key lookup.
|
|
305
310
|
*/
|
|
306
|
-
static find<T extends BaseModel>(
|
|
311
|
+
static async find<T extends BaseModel>(
|
|
307
312
|
this: new (data?: Record<string, unknown>) => T,
|
|
308
313
|
filter?: Record<string, unknown>,
|
|
309
314
|
limit = 100,
|
|
310
315
|
offset = 0,
|
|
311
316
|
orderBy?: string,
|
|
312
317
|
include?: string[],
|
|
313
|
-
): T[] {
|
|
318
|
+
): Promise<T[]> {
|
|
314
319
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
315
320
|
const db = ModelClass.getDb();
|
|
316
321
|
const conditions: string[] = [];
|
|
@@ -336,7 +341,7 @@ export class BaseModel {
|
|
|
336
341
|
sql += ` ORDER BY ${orderBy}`;
|
|
337
342
|
}
|
|
338
343
|
|
|
339
|
-
const rows = db
|
|
344
|
+
const rows = await adapterFetch(db, sql, params, limit, offset);
|
|
340
345
|
const data = (rows as any)?.data ?? rows;
|
|
341
346
|
const instances = (Array.isArray(data) ? data : []).map((row: Record<string, unknown>) => {
|
|
342
347
|
const inst = new this(row) as T;
|
|
@@ -345,7 +350,7 @@ export class BaseModel {
|
|
|
345
350
|
});
|
|
346
351
|
|
|
347
352
|
if (include) {
|
|
348
|
-
ModelClass._eagerLoad(instances as BaseModel[], include);
|
|
353
|
+
await ModelClass._eagerLoad(instances as BaseModel[], include);
|
|
349
354
|
}
|
|
350
355
|
|
|
351
356
|
return instances;
|
|
@@ -365,7 +370,7 @@ export class BaseModel {
|
|
|
365
370
|
*
|
|
366
371
|
* Returns true if found, false otherwise.
|
|
367
372
|
*/
|
|
368
|
-
load(filter?: string, params?: unknown[], include?: string[]): boolean {
|
|
373
|
+
async load(filter?: string, params?: unknown[], include?: string[]): Promise<boolean> {
|
|
369
374
|
const ModelClass = this.constructor as typeof BaseModel & (new (data?: Record<string, unknown>) => BaseModel);
|
|
370
375
|
const table = (ModelClass as any).tableName ?? (this as any).tableName;
|
|
371
376
|
|
|
@@ -381,7 +386,7 @@ export class BaseModel {
|
|
|
381
386
|
sql = `SELECT * FROM ${table} WHERE ${filter}`;
|
|
382
387
|
}
|
|
383
388
|
|
|
384
|
-
const result = ModelClass.selectOne(sql, params, include);
|
|
389
|
+
const result = await ModelClass.selectOne(sql, params, include);
|
|
385
390
|
if (!result) return false;
|
|
386
391
|
const data = (result as any).toJSON ? (result as any).toJSON() : result;
|
|
387
392
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -398,13 +403,13 @@ export class BaseModel {
|
|
|
398
403
|
* @param params Optional query parameters.
|
|
399
404
|
* @param include Optional array of relationship names to eager-load.
|
|
400
405
|
*/
|
|
401
|
-
static all<T extends BaseModel>(
|
|
406
|
+
static async all<T extends BaseModel>(
|
|
402
407
|
this: new (data?: Record<string, unknown>) => T,
|
|
403
408
|
where?: string,
|
|
404
409
|
params?: unknown[],
|
|
405
410
|
include?: string[],
|
|
406
411
|
orderBy?: string,
|
|
407
|
-
): T[] {
|
|
412
|
+
): Promise<T[]> {
|
|
408
413
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
409
414
|
const db = ModelClass.getDb();
|
|
410
415
|
|
|
@@ -423,10 +428,10 @@ export class BaseModel {
|
|
|
423
428
|
const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
|
|
424
429
|
const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
|
|
425
430
|
|
|
426
|
-
const rows = db
|
|
431
|
+
const rows = await adapterQuery(db, sql, params);
|
|
427
432
|
const instances = rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
428
433
|
if (include) {
|
|
429
|
-
ModelClass._eagerLoad(instances, include);
|
|
434
|
+
await ModelClass._eagerLoad(instances, include);
|
|
430
435
|
}
|
|
431
436
|
return instances;
|
|
432
437
|
}
|
|
@@ -441,14 +446,14 @@ export class BaseModel {
|
|
|
441
446
|
* @param offset Skip records (default 0)
|
|
442
447
|
* @param include Relationship names to eager-load
|
|
443
448
|
*/
|
|
444
|
-
static where<T extends BaseModel>(
|
|
449
|
+
static async where<T extends BaseModel>(
|
|
445
450
|
this: new (data?: Record<string, unknown>) => T,
|
|
446
451
|
conditions: string,
|
|
447
452
|
params?: unknown[],
|
|
448
453
|
limit: number = 20,
|
|
449
454
|
offset: number = 0,
|
|
450
455
|
include?: string[],
|
|
451
|
-
): T[] {
|
|
456
|
+
): Promise<T[]> {
|
|
452
457
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
453
458
|
const db = ModelClass.getDb();
|
|
454
459
|
|
|
@@ -463,10 +468,10 @@ export class BaseModel {
|
|
|
463
468
|
|
|
464
469
|
const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")} LIMIT ${limit} OFFSET ${offset}`;
|
|
465
470
|
|
|
466
|
-
const rows = db
|
|
471
|
+
const rows = await adapterQuery(db, sql, params);
|
|
467
472
|
const instances = rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
468
473
|
if (include) {
|
|
469
|
-
ModelClass._eagerLoad(instances, include);
|
|
474
|
+
await ModelClass._eagerLoad(instances, include);
|
|
470
475
|
}
|
|
471
476
|
return instances;
|
|
472
477
|
}
|
|
@@ -475,7 +480,7 @@ export class BaseModel {
|
|
|
475
480
|
* Save this instance (insert or update).
|
|
476
481
|
* Returns this on success (fluent), null on failure.
|
|
477
482
|
*/
|
|
478
|
-
save(): this | false {
|
|
483
|
+
async save(): Promise<this | false> {
|
|
479
484
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
480
485
|
const db = ModelClass.getDb();
|
|
481
486
|
const pk = ModelClass.getPkField();
|
|
@@ -498,7 +503,7 @@ export class BaseModel {
|
|
|
498
503
|
isUpdate = true;
|
|
499
504
|
} else {
|
|
500
505
|
try {
|
|
501
|
-
isUpdate = ModelClass.exists(pkValue);
|
|
506
|
+
isUpdate = await ModelClass.exists(pkValue);
|
|
502
507
|
} catch {
|
|
503
508
|
// If we can't tell (e.g. table doesn't exist yet), fall back
|
|
504
509
|
// to INSERT so the user sees the real driver error rather
|
|
@@ -508,19 +513,19 @@ export class BaseModel {
|
|
|
508
513
|
}
|
|
509
514
|
}
|
|
510
515
|
|
|
511
|
-
db
|
|
516
|
+
await adapterStartTransaction(db);
|
|
512
517
|
try {
|
|
513
518
|
if (isUpdate) {
|
|
514
519
|
// Update
|
|
515
520
|
const updateFields = Object.entries(ModelClass.fields).filter(
|
|
516
521
|
([name, def]) => !def.primaryKey && this[name] !== undefined,
|
|
517
522
|
);
|
|
518
|
-
if (updateFields.length === 0) { db
|
|
523
|
+
if (updateFields.length === 0) { await adapterCommit(db); return this; }
|
|
519
524
|
|
|
520
525
|
const setClause = updateFields.map(([k]) => `"${ModelClass.getDbColumn(k)}" = ?`).join(", ");
|
|
521
526
|
const values = [...updateFields.map(([k]) => this[k]), pkValue];
|
|
522
527
|
|
|
523
|
-
db
|
|
528
|
+
await adapterExecute(db, `UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pkCol}" = ?`, values);
|
|
524
529
|
} else {
|
|
525
530
|
// Insert
|
|
526
531
|
const insertFields = Object.entries(ModelClass.fields).filter(
|
|
@@ -531,23 +536,42 @@ export class BaseModel {
|
|
|
531
536
|
const placeholders = insertFields.map(() => "?").join(", ");
|
|
532
537
|
const values = insertFields.map(([k]) => this[k]);
|
|
533
538
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
)
|
|
539
|
+
// For auto-increment PKs on engines that need it (PostgreSQL),
|
|
540
|
+
// RETURNING the PK column lets us read the engine-assigned id back.
|
|
541
|
+
// SQLite ignores the RETURNING clause harmlessly (it supports it
|
|
542
|
+
// since 3.35) and we still prefer its lastInsertRowid; for other
|
|
543
|
+
// engines extractLastInsertId() reads rows[0].id.
|
|
544
|
+
const wantReturning = pkField?.autoIncrement && db.constructor.name !== "SQLiteAdapter";
|
|
545
|
+
const insertSql =
|
|
546
|
+
`INSERT INTO "${ModelClass.tableName}" (${columns}) VALUES (${placeholders})` +
|
|
547
|
+
(wantReturning ? ` RETURNING "${pkCol}"` : "");
|
|
548
|
+
|
|
549
|
+
const result = await adapterExecute(db, insertSql, values);
|
|
538
550
|
|
|
539
551
|
// v3.13.11 (issue #50.2): only adopt the engine-assigned ID
|
|
540
552
|
// for auto-increment PKs. A natural-key PK was already set by
|
|
541
|
-
// the caller; don't overwrite it with the driver's last_id
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
553
|
+
// the caller; don't overwrite it with the driver's last_id.
|
|
554
|
+
if (pkField?.autoIncrement) {
|
|
555
|
+
// RETURNING result: pg puts it in rows[0][pkCol]; normalise here.
|
|
556
|
+
let newId = extractLastInsertId(result);
|
|
557
|
+
if (newId === null && result && typeof result === "object") {
|
|
558
|
+
const rows = (result as any).rows;
|
|
559
|
+
if (Array.isArray(rows) && rows[0]) {
|
|
560
|
+
newId = rows[0][pkCol] ?? rows[0].id ?? null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (newId === null) {
|
|
564
|
+
// Fall back to the adapter's tracked last id (MySQL/MSSQL).
|
|
565
|
+
newId = db.lastInsertId();
|
|
566
|
+
}
|
|
567
|
+
if (newId !== null && newId !== undefined) {
|
|
568
|
+
this[pk] = newId;
|
|
569
|
+
}
|
|
546
570
|
}
|
|
547
571
|
}
|
|
548
|
-
db
|
|
572
|
+
await adapterCommit(db);
|
|
549
573
|
} catch (e) {
|
|
550
|
-
db
|
|
574
|
+
await adapterRollback(db);
|
|
551
575
|
return false;
|
|
552
576
|
}
|
|
553
577
|
(this as any)._exists = true;
|
|
@@ -557,7 +581,7 @@ export class BaseModel {
|
|
|
557
581
|
/**
|
|
558
582
|
* Delete this instance. Uses soft delete if configured.
|
|
559
583
|
*/
|
|
560
|
-
delete(): boolean {
|
|
584
|
+
async delete(): Promise<boolean> {
|
|
561
585
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
562
586
|
const db = ModelClass.getDb();
|
|
563
587
|
const pk = ModelClass.getPkField();
|
|
@@ -568,23 +592,23 @@ export class BaseModel {
|
|
|
568
592
|
throw new Error("Cannot delete a model without a primary key value");
|
|
569
593
|
}
|
|
570
594
|
|
|
571
|
-
db
|
|
595
|
+
await adapterStartTransaction(db);
|
|
572
596
|
try {
|
|
573
597
|
if (ModelClass.softDelete) {
|
|
574
|
-
db
|
|
598
|
+
await adapterExecute(db,
|
|
575
599
|
`UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE "${pkCol}" = ?`,
|
|
576
600
|
[pkValue],
|
|
577
601
|
);
|
|
578
602
|
this.is_deleted = 1;
|
|
579
603
|
} else {
|
|
580
|
-
db
|
|
604
|
+
await adapterExecute(db,
|
|
581
605
|
`DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
|
|
582
606
|
[pkValue],
|
|
583
607
|
);
|
|
584
608
|
}
|
|
585
|
-
db
|
|
609
|
+
await adapterCommit(db);
|
|
586
610
|
} catch (e) {
|
|
587
|
-
db
|
|
611
|
+
await adapterRollback(db);
|
|
588
612
|
throw e;
|
|
589
613
|
}
|
|
590
614
|
return true;
|
|
@@ -624,14 +648,13 @@ export class BaseModel {
|
|
|
624
648
|
}
|
|
625
649
|
|
|
626
650
|
for (const [relName, nested] of Object.entries(topLevel)) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
this._relCache[relName] = related;
|
|
633
|
-
}
|
|
651
|
+
// toDict stays synchronous (used in routes, templates, serialization).
|
|
652
|
+
// Relationships must be eager-loaded first (await Model._eagerLoad / the
|
|
653
|
+
// include arg on find/all/where) which fills _relCache. A relation that
|
|
654
|
+
// isn't cached is simply skipped here — async lazy-load on a sync
|
|
655
|
+
// serializer is not possible after the Option A async refactor.
|
|
634
656
|
const data = this._relCache[relName];
|
|
657
|
+
if (data === undefined) continue;
|
|
635
658
|
if (data === null || data === undefined) {
|
|
636
659
|
result[relName] = null;
|
|
637
660
|
} else if (Array.isArray(data)) {
|
|
@@ -722,54 +745,59 @@ export class BaseModel {
|
|
|
722
745
|
* Generate and execute CREATE TABLE DDL from the model's field definitions.
|
|
723
746
|
* Uses the adapter's createTable method if available, otherwise builds SQL directly.
|
|
724
747
|
*/
|
|
725
|
-
static createTable(): boolean {
|
|
748
|
+
static async createTable(): Promise<boolean> {
|
|
726
749
|
const db = this.getDb();
|
|
727
|
-
if (db
|
|
750
|
+
if (await adapterTableExists(db, this.tableName)) return true;
|
|
728
751
|
|
|
729
|
-
|
|
730
|
-
|
|
752
|
+
// Prefer the adapter's createTable — every adapter implements it and the
|
|
753
|
+
// async variants (PostgreSQL/MySQL/MSSQL/Firebird) emit engine-aware DDL
|
|
754
|
+
// (datetime → TIMESTAMP, boolean → native BOOLEAN, auto-increment → SERIAL
|
|
755
|
+
// etc. on PG). Remap field keys to DB column names if fieldMapping exists.
|
|
756
|
+
if (typeof db.createTable === "function" || typeof (db as any).createTableAsync === "function") {
|
|
731
757
|
const mappedFields: Record<string, FieldDefinition> = {};
|
|
732
758
|
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
733
759
|
const dbCol = this.getDbColumn(fieldName);
|
|
734
760
|
mappedFields[dbCol] = def;
|
|
735
761
|
}
|
|
736
|
-
db
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const typeMap: Record<string, string> = {
|
|
740
|
-
integer: "INTEGER",
|
|
741
|
-
string: "TEXT",
|
|
742
|
-
text: "TEXT",
|
|
743
|
-
number: "REAL",
|
|
744
|
-
numeric: "REAL",
|
|
745
|
-
boolean: "INTEGER",
|
|
746
|
-
datetime: "TEXT",
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
const colDefs: string[] = [];
|
|
750
|
-
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
751
|
-
const dbCol = this.getDbColumn(fieldName);
|
|
752
|
-
const sqlType = typeMap[def.type] || "TEXT";
|
|
753
|
-
const parts = [`"${dbCol}" ${sqlType}`];
|
|
754
|
-
if (def.primaryKey) parts.push("PRIMARY KEY");
|
|
755
|
-
if (def.autoIncrement) parts.push("AUTOINCREMENT");
|
|
756
|
-
if (def.required && !def.primaryKey) parts.push("NOT NULL");
|
|
757
|
-
if (def.default !== undefined) {
|
|
758
|
-
const dv = typeof def.default === "string" ? `'${def.default}'` : String(def.default);
|
|
759
|
-
parts.push(`DEFAULT ${dv}`);
|
|
760
|
-
}
|
|
761
|
-
colDefs.push(parts.join(" "));
|
|
762
|
-
}
|
|
762
|
+
await adapterCreateTable(db, this.tableName, mappedFields);
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
763
765
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
766
|
+
// Fallback: build SQL manually (SQLite-only dialect — used only when an
|
|
767
|
+
// adapter lacks createTable, which none currently do).
|
|
768
|
+
const typeMap: Record<string, string> = {
|
|
769
|
+
integer: "INTEGER",
|
|
770
|
+
string: "TEXT",
|
|
771
|
+
text: "TEXT",
|
|
772
|
+
number: "REAL",
|
|
773
|
+
numeric: "REAL",
|
|
774
|
+
boolean: "INTEGER",
|
|
775
|
+
datetime: "TEXT",
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
const colDefs: string[] = [];
|
|
779
|
+
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
780
|
+
const dbCol = this.getDbColumn(fieldName);
|
|
781
|
+
const sqlType = typeMap[def.type] || "TEXT";
|
|
782
|
+
const parts = [`"${dbCol}" ${sqlType}`];
|
|
783
|
+
if (def.primaryKey) parts.push("PRIMARY KEY");
|
|
784
|
+
if (def.autoIncrement) parts.push("AUTOINCREMENT");
|
|
785
|
+
if (def.required && !def.primaryKey) parts.push("NOT NULL");
|
|
786
|
+
if (def.default !== undefined) {
|
|
787
|
+
const dv = typeof def.default === "string" ? `'${def.default}'` : String(def.default);
|
|
788
|
+
parts.push(`DEFAULT ${dv}`);
|
|
772
789
|
}
|
|
790
|
+
colDefs.push(parts.join(" "));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const sql = `CREATE TABLE IF NOT EXISTS "${this.tableName}" (${colDefs.join(", ")})`;
|
|
794
|
+
await adapterStartTransaction(db);
|
|
795
|
+
try {
|
|
796
|
+
await adapterExecute(db, sql);
|
|
797
|
+
await adapterCommit(db);
|
|
798
|
+
} catch (e) {
|
|
799
|
+
await adapterRollback(db);
|
|
800
|
+
throw e;
|
|
773
801
|
}
|
|
774
802
|
return true;
|
|
775
803
|
}
|
|
@@ -777,9 +805,9 @@ export class BaseModel {
|
|
|
777
805
|
/**
|
|
778
806
|
* Find a record by primary key or throw an error if not found.
|
|
779
807
|
*/
|
|
780
|
-
static findOrFail<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown): T {
|
|
808
|
+
static async findOrFail<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown): Promise<T> {
|
|
781
809
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
782
|
-
const result = ModelClass.findById(id) as T | null;
|
|
810
|
+
const result = (await ModelClass.findById(id)) as T | null;
|
|
783
811
|
if (result === null) {
|
|
784
812
|
throw new Error(`${ModelClass.tableName}: record with id ${id} not found`);
|
|
785
813
|
}
|
|
@@ -789,9 +817,9 @@ export class BaseModel {
|
|
|
789
817
|
/**
|
|
790
818
|
* Return true if a record with the given primary key exists.
|
|
791
819
|
*/
|
|
792
|
-
static exists(pkValue: unknown): boolean {
|
|
820
|
+
static async exists(pkValue: unknown): Promise<boolean> {
|
|
793
821
|
const ModelClass = this as unknown as typeof BaseModel;
|
|
794
|
-
return ModelClass.findById(pkValue) !== null;
|
|
822
|
+
return (await ModelClass.findById(pkValue)) !== null;
|
|
795
823
|
}
|
|
796
824
|
|
|
797
825
|
/**
|
|
@@ -804,7 +832,7 @@ export class BaseModel {
|
|
|
804
832
|
* @param offset Records to skip (default 0).
|
|
805
833
|
* @param include Relationship names to eager-load on cache miss.
|
|
806
834
|
*/
|
|
807
|
-
static cached<T extends BaseModel>(
|
|
835
|
+
static async cached<T extends BaseModel>(
|
|
808
836
|
this: new (data?: Record<string, unknown>) => T,
|
|
809
837
|
sql: string,
|
|
810
838
|
params?: unknown[],
|
|
@@ -812,7 +840,7 @@ export class BaseModel {
|
|
|
812
840
|
limit = 20,
|
|
813
841
|
offset = 0,
|
|
814
842
|
include?: string[],
|
|
815
|
-
): T[] {
|
|
843
|
+
): Promise<T[]> {
|
|
816
844
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
817
845
|
if (!ModelClass._queryCache) {
|
|
818
846
|
ModelClass._queryCache = new QueryCache({ defaultTtl: ttl, maxSize: 500 });
|
|
@@ -824,10 +852,10 @@ export class BaseModel {
|
|
|
824
852
|
|
|
825
853
|
const db = ModelClass.getDb();
|
|
826
854
|
const querySql = `${sql} LIMIT ${limit} OFFSET ${offset}`;
|
|
827
|
-
const rows = db
|
|
855
|
+
const rows = await adapterQuery(db, querySql, params);
|
|
828
856
|
const results = rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
829
857
|
if (include && results.length > 0) {
|
|
830
|
-
ModelClass._eagerLoad(results as BaseModel[], include);
|
|
858
|
+
await ModelClass._eagerLoad(results as BaseModel[], include);
|
|
831
859
|
}
|
|
832
860
|
ModelClass._queryCache.set(key, results, ttl);
|
|
833
861
|
return results;
|
|
@@ -846,28 +874,28 @@ export class BaseModel {
|
|
|
846
874
|
/**
|
|
847
875
|
* Execute a raw SQL SELECT and return results as model instances.
|
|
848
876
|
*/
|
|
849
|
-
static select<T extends BaseModel>(
|
|
877
|
+
static async select<T extends BaseModel>(
|
|
850
878
|
this: new (data?: Record<string, unknown>) => T,
|
|
851
879
|
sql: string,
|
|
852
880
|
params?: unknown[],
|
|
853
|
-
): T[] {
|
|
881
|
+
): Promise<T[]> {
|
|
854
882
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
855
883
|
const db = ModelClass.getDb();
|
|
856
|
-
const rows = db
|
|
884
|
+
const rows = await adapterQuery(db, sql, params);
|
|
857
885
|
return rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
858
886
|
}
|
|
859
887
|
|
|
860
|
-
static selectOne<T extends BaseModel>(
|
|
888
|
+
static async selectOne<T extends BaseModel>(
|
|
861
889
|
this: new (data?: Record<string, unknown>) => T,
|
|
862
890
|
sql: string,
|
|
863
891
|
params?: unknown[],
|
|
864
892
|
include?: string[],
|
|
865
|
-
): T | null {
|
|
893
|
+
): Promise<T | null> {
|
|
866
894
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
867
|
-
const results = ModelClass.select<T>(sql, params);
|
|
895
|
+
const results = await ModelClass.select<T>(sql, params);
|
|
868
896
|
const instance = results[0] ?? null;
|
|
869
897
|
if (instance && include) {
|
|
870
|
-
ModelClass._eagerLoad([instance], include);
|
|
898
|
+
await ModelClass._eagerLoad([instance], include);
|
|
871
899
|
}
|
|
872
900
|
return instance;
|
|
873
901
|
}
|
|
@@ -875,7 +903,7 @@ export class BaseModel {
|
|
|
875
903
|
/**
|
|
876
904
|
* Permanently delete this instance, bypassing soft delete.
|
|
877
905
|
*/
|
|
878
|
-
forceDelete(): boolean {
|
|
906
|
+
async forceDelete(): Promise<boolean> {
|
|
879
907
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
880
908
|
const db = ModelClass.getDb();
|
|
881
909
|
const pk = ModelClass.getPkField();
|
|
@@ -886,15 +914,15 @@ export class BaseModel {
|
|
|
886
914
|
throw new Error("Cannot delete a model without a primary key value");
|
|
887
915
|
}
|
|
888
916
|
|
|
889
|
-
db
|
|
917
|
+
await adapterStartTransaction(db);
|
|
890
918
|
try {
|
|
891
|
-
db
|
|
919
|
+
await adapterExecute(db,
|
|
892
920
|
`DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
|
|
893
921
|
[pkValue],
|
|
894
922
|
);
|
|
895
|
-
db
|
|
923
|
+
await adapterCommit(db);
|
|
896
924
|
} catch (e) {
|
|
897
|
-
db
|
|
925
|
+
await adapterRollback(db);
|
|
898
926
|
throw e;
|
|
899
927
|
}
|
|
900
928
|
return true;
|
|
@@ -903,7 +931,7 @@ export class BaseModel {
|
|
|
903
931
|
/**
|
|
904
932
|
* Restore a soft-deleted record.
|
|
905
933
|
*/
|
|
906
|
-
restore(): boolean {
|
|
934
|
+
async restore(): Promise<boolean> {
|
|
907
935
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
908
936
|
if (!ModelClass.softDelete) {
|
|
909
937
|
throw new Error("restore() is only available on models with softDelete enabled");
|
|
@@ -918,15 +946,15 @@ export class BaseModel {
|
|
|
918
946
|
throw new Error("Cannot restore a model without a primary key value");
|
|
919
947
|
}
|
|
920
948
|
|
|
921
|
-
db
|
|
949
|
+
await adapterStartTransaction(db);
|
|
922
950
|
try {
|
|
923
|
-
db
|
|
951
|
+
await adapterExecute(db,
|
|
924
952
|
`UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pkCol}" = ?`,
|
|
925
953
|
[pkValue],
|
|
926
954
|
);
|
|
927
|
-
db
|
|
955
|
+
await adapterCommit(db);
|
|
928
956
|
} catch (e) {
|
|
929
|
-
db
|
|
957
|
+
await adapterRollback(db);
|
|
930
958
|
throw e;
|
|
931
959
|
}
|
|
932
960
|
this.is_deleted = 0;
|
|
@@ -936,13 +964,13 @@ export class BaseModel {
|
|
|
936
964
|
/**
|
|
937
965
|
* Find records including soft-deleted ones.
|
|
938
966
|
*/
|
|
939
|
-
static withTrashed<T extends BaseModel>(
|
|
967
|
+
static async withTrashed<T extends BaseModel>(
|
|
940
968
|
this: new (data?: Record<string, unknown>) => T,
|
|
941
969
|
conditions?: string,
|
|
942
970
|
params?: unknown[],
|
|
943
971
|
limit?: number,
|
|
944
972
|
offset?: number,
|
|
945
|
-
): T[] {
|
|
973
|
+
): Promise<T[]> {
|
|
946
974
|
const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
|
|
947
975
|
const db = ModelClass.getDb();
|
|
948
976
|
|
|
@@ -965,14 +993,14 @@ export class BaseModel {
|
|
|
965
993
|
sql += ` OFFSET ${offset}`;
|
|
966
994
|
}
|
|
967
995
|
|
|
968
|
-
const rows = db
|
|
996
|
+
const rows = await adapterQuery(db, sql, params);
|
|
969
997
|
return rows.map((row) => new ModelClass(row as Record<string, unknown>) as T);
|
|
970
998
|
}
|
|
971
999
|
|
|
972
1000
|
/**
|
|
973
1001
|
* Count records matching conditions (respects soft delete and table filter).
|
|
974
1002
|
*/
|
|
975
|
-
static count(conditions?: string, params?: unknown[]): number {
|
|
1003
|
+
static async count(conditions?: string, params?: unknown[]): Promise<number> {
|
|
976
1004
|
const db = this.getDb();
|
|
977
1005
|
const parts: string[] = [];
|
|
978
1006
|
if (this.softDelete) {
|
|
@@ -986,8 +1014,14 @@ export class BaseModel {
|
|
|
986
1014
|
}
|
|
987
1015
|
const whereClause = parts.length > 0 ? ` WHERE ${parts.join(" AND ")}` : "";
|
|
988
1016
|
const sql = `SELECT COUNT(*) as cnt FROM "${this.tableName}"${whereClause}`;
|
|
989
|
-
const rows = db
|
|
990
|
-
|
|
1017
|
+
const rows = await adapterQuery(db, sql, params);
|
|
1018
|
+
if (rows.length === 0) return 0;
|
|
1019
|
+
// PostgreSQL returns COUNT(*) as a bigint, which the `pg` driver hands
|
|
1020
|
+
// back as a string ("2"); Firebird upper-cases the alias. Coerce to a
|
|
1021
|
+
// Number and tolerate case so count() returns a real number on every engine.
|
|
1022
|
+
const row = rows[0] as Record<string, unknown>;
|
|
1023
|
+
const cnt = row.cnt ?? row.CNT ?? 0;
|
|
1024
|
+
return Number(cnt);
|
|
991
1025
|
}
|
|
992
1026
|
|
|
993
1027
|
/**
|
|
@@ -1012,11 +1046,11 @@ export class BaseModel {
|
|
|
1012
1046
|
/**
|
|
1013
1047
|
* Load a has-one related model instance.
|
|
1014
1048
|
*/
|
|
1015
|
-
hasOne<T extends BaseModel, R extends BaseModel>(
|
|
1049
|
+
async hasOne<T extends BaseModel, R extends BaseModel>(
|
|
1016
1050
|
this: T,
|
|
1017
1051
|
relatedClass: typeof BaseModel & (new (data?: Record<string, unknown>) => R),
|
|
1018
1052
|
foreignKey: string,
|
|
1019
|
-
): R | null {
|
|
1053
|
+
): Promise<R | null> {
|
|
1020
1054
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
1021
1055
|
const pk = ModelClass.getPkField();
|
|
1022
1056
|
const pkValue = this[pk];
|
|
@@ -1032,7 +1066,7 @@ export class BaseModel {
|
|
|
1032
1066
|
}
|
|
1033
1067
|
sql += ` LIMIT 1`;
|
|
1034
1068
|
|
|
1035
|
-
const rows = db
|
|
1069
|
+
const rows = await adapterQuery(db, sql, [pkValue]);
|
|
1036
1070
|
if (rows.length === 0) return null;
|
|
1037
1071
|
|
|
1038
1072
|
const related = new relatedClass(rows[0] as Record<string, unknown>) as R;
|
|
@@ -1044,13 +1078,13 @@ export class BaseModel {
|
|
|
1044
1078
|
/**
|
|
1045
1079
|
* Load has-many related model instances.
|
|
1046
1080
|
*/
|
|
1047
|
-
hasMany<T extends BaseModel, R extends BaseModel>(
|
|
1081
|
+
async hasMany<T extends BaseModel, R extends BaseModel>(
|
|
1048
1082
|
this: T,
|
|
1049
1083
|
relatedClass: typeof BaseModel & (new (data?: Record<string, unknown>) => R),
|
|
1050
1084
|
foreignKey: string,
|
|
1051
1085
|
limit: number = 100,
|
|
1052
1086
|
offset: number = 0,
|
|
1053
|
-
): R[] {
|
|
1087
|
+
): Promise<R[]> {
|
|
1054
1088
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
1055
1089
|
const pk = ModelClass.getPkField();
|
|
1056
1090
|
const pkValue = this[pk];
|
|
@@ -1066,7 +1100,7 @@ export class BaseModel {
|
|
|
1066
1100
|
}
|
|
1067
1101
|
sql += ` LIMIT ${limit} OFFSET ${offset}`;
|
|
1068
1102
|
|
|
1069
|
-
const rows = db
|
|
1103
|
+
const rows = await adapterQuery(db, sql, [pkValue]);
|
|
1070
1104
|
const related = rows.map((row) => new relatedClass(row as Record<string, unknown>) as R);
|
|
1071
1105
|
const relKey = relatedClass.tableName.toLowerCase();
|
|
1072
1106
|
this[relKey] = related;
|
|
@@ -1076,11 +1110,11 @@ export class BaseModel {
|
|
|
1076
1110
|
/**
|
|
1077
1111
|
* Load the parent model this instance belongs to.
|
|
1078
1112
|
*/
|
|
1079
|
-
belongsTo<T extends BaseModel, R extends BaseModel>(
|
|
1113
|
+
async belongsTo<T extends BaseModel, R extends BaseModel>(
|
|
1080
1114
|
this: T,
|
|
1081
1115
|
relatedClass: typeof BaseModel & (new (data?: Record<string, unknown>) => R),
|
|
1082
1116
|
foreignKey: string,
|
|
1083
|
-
): R | null {
|
|
1117
|
+
): Promise<R | null> {
|
|
1084
1118
|
// foreignKey is a DB column name — resolve to JS property name on this model
|
|
1085
1119
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
1086
1120
|
const reverseMap = ModelClass.getReverseMapping();
|
|
@@ -1099,7 +1133,7 @@ export class BaseModel {
|
|
|
1099
1133
|
}
|
|
1100
1134
|
sql += ` LIMIT 1`;
|
|
1101
1135
|
|
|
1102
|
-
const rows = db
|
|
1136
|
+
const rows = await adapterQuery(db, sql, [fkValue]);
|
|
1103
1137
|
if (rows.length === 0) return null;
|
|
1104
1138
|
|
|
1105
1139
|
const related = new relatedClass(rows[0] as Record<string, unknown>) as R;
|
|
@@ -1124,62 +1158,12 @@ export class BaseModel {
|
|
|
1124
1158
|
return BaseModel._modelRegistry[name] ?? null;
|
|
1125
1159
|
}
|
|
1126
1160
|
|
|
1127
|
-
/**
|
|
1128
|
-
* Lazy-load a single relationship by name (used by toDict with include).
|
|
1129
|
-
*/
|
|
1130
|
-
private _lazyLoadRelationship(relName: string): unknown {
|
|
1131
|
-
const ModelClass = this.constructor as typeof BaseModel;
|
|
1132
|
-
|
|
1133
|
-
// Apply FK registry so foreignKey fields auto-wire relationships
|
|
1134
|
-
ModelClass._processForeignKeys();
|
|
1135
|
-
ModelClass._applyFkRegistry();
|
|
1136
|
-
|
|
1137
|
-
// Check hasOne
|
|
1138
|
-
if (ModelClass.hasOne) {
|
|
1139
|
-
const rel = ModelClass.hasOne.find((r) => r.model.toLowerCase() === relName || r.model === relName);
|
|
1140
|
-
if (rel) {
|
|
1141
|
-
const relatedClass = BaseModel._modelRegistry[rel.model];
|
|
1142
|
-
if (relatedClass) {
|
|
1143
|
-
return this.hasOne(relatedClass as any, rel.foreignKey);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// Check hasMany
|
|
1149
|
-
if (ModelClass.hasMany) {
|
|
1150
|
-
const rel = ModelClass.hasMany.find((r) => {
|
|
1151
|
-
const base = r.model.toLowerCase();
|
|
1152
|
-
const key = _pluralRelKeys() ? base + "s" : base;
|
|
1153
|
-
return key === relName || base === relName || r.model === relName;
|
|
1154
|
-
});
|
|
1155
|
-
if (rel) {
|
|
1156
|
-
const relatedClass = BaseModel._modelRegistry[rel.model];
|
|
1157
|
-
if (relatedClass) {
|
|
1158
|
-
return this.hasMany(relatedClass as any, rel.foreignKey);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
// Check belongsTo
|
|
1164
|
-
if (ModelClass.belongsTo) {
|
|
1165
|
-
const rel = ModelClass.belongsTo.find((r) => r.model.toLowerCase() === relName || r.model === relName);
|
|
1166
|
-
if (rel) {
|
|
1167
|
-
const relatedClass = BaseModel._modelRegistry[rel.model];
|
|
1168
|
-
if (relatedClass) {
|
|
1169
|
-
return this.belongsTo(relatedClass as any, rel.foreignKey);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
return undefined;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1161
|
/**
|
|
1178
1162
|
* Eager load relationships for a collection of instances (prevents N+1).
|
|
1179
1163
|
* @param instances Array of model instances.
|
|
1180
1164
|
* @param include Array of relationship names (supports dot notation for nesting).
|
|
1181
1165
|
*/
|
|
1182
|
-
static _eagerLoad(instances: BaseModel[], include: string[]): void {
|
|
1166
|
+
static async _eagerLoad(instances: BaseModel[], include: string[]): Promise<void> {
|
|
1183
1167
|
if (instances.length === 0) return;
|
|
1184
1168
|
|
|
1185
1169
|
const ModelClass = instances[0].constructor as typeof BaseModel;
|
|
@@ -1244,12 +1228,12 @@ export class BaseModel {
|
|
|
1244
1228
|
sql += ` AND is_deleted = 0`;
|
|
1245
1229
|
}
|
|
1246
1230
|
|
|
1247
|
-
const rows = db
|
|
1231
|
+
const rows = await adapterQuery(db, sql, pkValues);
|
|
1248
1232
|
const related = rows.map((row) => new relatedClass(row as Record<string, unknown>));
|
|
1249
1233
|
|
|
1250
1234
|
// Eager load nested
|
|
1251
1235
|
if (nested.length > 0 && related.length > 0) {
|
|
1252
|
-
relatedClass._eagerLoad(related, nested);
|
|
1236
|
+
await relatedClass._eagerLoad(related, nested);
|
|
1253
1237
|
}
|
|
1254
1238
|
|
|
1255
1239
|
// Group by FK — fk is a DB column name, resolve to JS property name on the related model
|
|
@@ -1290,11 +1274,11 @@ export class BaseModel {
|
|
|
1290
1274
|
sql += ` AND is_deleted = 0`;
|
|
1291
1275
|
}
|
|
1292
1276
|
|
|
1293
|
-
const rows = db
|
|
1277
|
+
const rows = await adapterQuery(db, sql, fkValues);
|
|
1294
1278
|
const related = rows.map((row) => new relatedClass(row as Record<string, unknown>));
|
|
1295
1279
|
|
|
1296
1280
|
if (nested.length > 0 && related.length > 0) {
|
|
1297
|
-
relatedClass._eagerLoad(related, nested);
|
|
1281
|
+
await relatedClass._eagerLoad(related, nested);
|
|
1298
1282
|
}
|
|
1299
1283
|
|
|
1300
1284
|
const lookup: Record<string, BaseModel> = {};
|
|
@@ -1323,10 +1307,9 @@ export class BaseModel {
|
|
|
1323
1307
|
* @param instances Array of model instances to load relationships onto.
|
|
1324
1308
|
* @param includeList Array of relationship names (supports dot notation for nesting).
|
|
1325
1309
|
*/
|
|
1326
|
-
static eagerLoad(instances: BaseModel[], includeList: string[]): Promise<void> {
|
|
1310
|
+
static async eagerLoad(instances: BaseModel[], includeList: string[]): Promise<void> {
|
|
1327
1311
|
const ModelClass = this as unknown as typeof BaseModel;
|
|
1328
|
-
ModelClass._eagerLoad(instances, includeList);
|
|
1329
|
-
return Promise.resolve();
|
|
1312
|
+
await ModelClass._eagerLoad(instances, includeList);
|
|
1330
1313
|
}
|
|
1331
1314
|
|
|
1332
1315
|
/**
|