tina4-nodejs 3.13.14 → 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.
@@ -1,4 +1,9 @@
1
- import { getAdapter, getNamedAdapter, setAdapter, parseDatabaseUrl } from "./database.js";
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.fetch(sql, params, limit, offset);
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.query(sql, params);
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.query(sql, params);
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.startTransaction();
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.commit(); return; }
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.execute(`UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pkCol}" = ?`, values);
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
- const result = db.execute(
535
- `INSERT INTO "${ModelClass.tableName}" (${columns}) VALUES (${placeholders})`,
536
- values,
537
- ) as { lastInsertRowid?: number };
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
- // (which on PG would be a sequence value that doesn't apply
543
- // to this row).
544
- if (result.lastInsertRowid && pkField?.autoIncrement) {
545
- this[pk] = result.lastInsertRowid;
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.commit();
572
+ await adapterCommit(db);
549
573
  } catch (e) {
550
- db.rollback();
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.startTransaction();
595
+ await adapterStartTransaction(db);
572
596
  try {
573
597
  if (ModelClass.softDelete) {
574
- db.execute(
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.execute(
604
+ await adapterExecute(db,
581
605
  `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
582
606
  [pkValue],
583
607
  );
584
608
  }
585
- db.commit();
609
+ await adapterCommit(db);
586
610
  } catch (e) {
587
- db.rollback();
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
- const cached = this._relCache[relName];
628
- if (cached === undefined) {
629
- // Try lazy load via instance methods
630
- const related = this._lazyLoadRelationship(relName);
631
- if (related === undefined) continue;
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.tableExists(this.tableName)) return true;
750
+ if (await adapterTableExists(db, this.tableName)) return true;
728
751
 
729
- if (typeof db.createTable === "function") {
730
- // Remap field keys to DB column names if fieldMapping is defined
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.createTable(this.tableName, mappedFields);
737
- } else {
738
- // Fallback: build SQL manually
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
- const sql = `CREATE TABLE IF NOT EXISTS "${this.tableName}" (${colDefs.join(", ")})`;
765
- db.startTransaction();
766
- try {
767
- db.execute(sql);
768
- db.commit();
769
- } catch (e) {
770
- db.rollback();
771
- throw e;
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.query(querySql, params);
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.query(sql, params);
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.startTransaction();
917
+ await adapterStartTransaction(db);
890
918
  try {
891
- db.execute(
919
+ await adapterExecute(db,
892
920
  `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
893
921
  [pkValue],
894
922
  );
895
- db.commit();
923
+ await adapterCommit(db);
896
924
  } catch (e) {
897
- db.rollback();
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.startTransaction();
949
+ await adapterStartTransaction(db);
922
950
  try {
923
- db.execute(
951
+ await adapterExecute(db,
924
952
  `UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pkCol}" = ?`,
925
953
  [pkValue],
926
954
  );
927
- db.commit();
955
+ await adapterCommit(db);
928
956
  } catch (e) {
929
- db.rollback();
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.query(sql, params);
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.query(sql, params);
990
- return rows.length > 0 ? (rows[0] as any).cnt : 0;
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.query(sql, [pkValue]);
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.query(sql, [pkValue]);
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.query(sql, [fkValue]);
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.query(sql, pkValues);
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.query(sql, fkValues);
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
  /**