tina4-nodejs 3.1.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +30 -2
  3. package/package.json +1 -1
  4. package/packages/cli/src/bin.ts +13 -1
  5. package/packages/cli/src/commands/migrate.ts +19 -5
  6. package/packages/cli/src/commands/migrateCreate.ts +29 -28
  7. package/packages/cli/src/commands/migrateRollback.ts +59 -0
  8. package/packages/cli/src/commands/migrateStatus.ts +62 -0
  9. package/packages/core/public/js/tina4-dev-admin.min.js +1 -1
  10. package/packages/core/src/auth.ts +44 -10
  11. package/packages/core/src/devAdmin.ts +14 -16
  12. package/packages/core/src/errorOverlay.ts +17 -15
  13. package/packages/core/src/index.ts +9 -2
  14. package/packages/core/src/queue.ts +127 -25
  15. package/packages/core/src/queueBackends/mongoBackend.ts +223 -0
  16. package/packages/core/src/request.ts +3 -3
  17. package/packages/core/src/routeDiscovery.ts +2 -1
  18. package/packages/core/src/router.ts +90 -51
  19. package/packages/core/src/server.ts +62 -4
  20. package/packages/core/src/session.ts +17 -1
  21. package/packages/core/src/sessionHandlers/databaseHandler.ts +134 -0
  22. package/packages/core/src/sessionHandlers/redisHandler.ts +230 -0
  23. package/packages/core/src/types.ts +12 -6
  24. package/packages/core/src/websocket.ts +11 -2
  25. package/packages/core/src/websocketConnection.ts +4 -2
  26. package/packages/frond/src/engine.ts +66 -1
  27. package/packages/orm/src/autoCrud.ts +17 -12
  28. package/packages/orm/src/baseModel.ts +99 -21
  29. package/packages/orm/src/database.ts +197 -69
  30. package/packages/orm/src/databaseResult.ts +207 -0
  31. package/packages/orm/src/index.ts +6 -3
  32. package/packages/orm/src/migration.ts +296 -71
  33. package/packages/orm/src/model.ts +1 -0
  34. package/packages/orm/src/types.ts +1 -0
@@ -15,6 +15,7 @@ import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from ".
15
15
  * static hasOne = [{ model: "Profile", foreignKey: "user_id" }];
16
16
  * static hasMany = [{ model: "Post", foreignKey: "author_id" }];
17
17
  * static _db = "secondary";
18
+ * static fieldMapping = { firstName: "first_name", lastName: "last_name" };
18
19
  * }
19
20
  */
20
21
  export class BaseModel {
@@ -27,6 +28,14 @@ export class BaseModel {
27
28
  static belongsTo?: RelationshipDefinition[];
28
29
  static _db?: string;
29
30
 
31
+ /**
32
+ * Maps JS property names to database column names.
33
+ * Example: { firstName: "first_name" } means the JS property `firstName`
34
+ * corresponds to the database column `first_name`.
35
+ * Properties not listed here use the property name as-is.
36
+ */
37
+ static fieldMapping: Record<string, string> = {};
38
+
30
39
  /** Instance data */
31
40
  [key: string]: unknown;
32
41
 
@@ -35,12 +44,52 @@ export class BaseModel {
35
44
 
36
45
  constructor(data?: Record<string, unknown>) {
37
46
  if (data) {
47
+ const ModelClass = this.constructor as typeof BaseModel;
48
+ const reverseMapping = ModelClass.getReverseMapping();
38
49
  for (const [key, value] of Object.entries(data)) {
39
- this[key] = value;
50
+ // If this DB column has a mapping, use the JS property name instead
51
+ const jsProp = reverseMapping[key] ?? key;
52
+ this[jsProp] = value;
40
53
  }
41
54
  }
42
55
  }
43
56
 
57
+ /**
58
+ * Get the database column name for a JS property.
59
+ * Returns the mapped column name, or the property name if no mapping exists.
60
+ */
61
+ static getDbColumn(prop: string): string {
62
+ return this.fieldMapping[prop] ?? prop;
63
+ }
64
+
65
+ /**
66
+ * Get all instance data converted to database column names.
67
+ * Uses fieldMapping to translate JS property names to DB column names.
68
+ */
69
+ getDbData(): Record<string, unknown> {
70
+ const ModelClass = this.constructor as typeof BaseModel;
71
+ const result: Record<string, unknown> = {};
72
+ for (const key of Object.keys(ModelClass.fields)) {
73
+ if (this[key] !== undefined) {
74
+ const dbCol = ModelClass.getDbColumn(key);
75
+ result[dbCol] = this[key];
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+
81
+ /**
82
+ * Get the reverse mapping (DB column → JS property).
83
+ * Flips fieldMapping so that { firstName: "first_name" } becomes { first_name: "firstName" }.
84
+ */
85
+ static getReverseMapping(): Record<string, string> {
86
+ const reverse: Record<string, string> = {};
87
+ for (const [jsProp, dbCol] of Object.entries(this.fieldMapping)) {
88
+ reverse[dbCol] = jsProp;
89
+ }
90
+ return reverse;
91
+ }
92
+
44
93
  /**
45
94
  * Get the database adapter for this model.
46
95
  */
@@ -52,12 +101,19 @@ export class BaseModel {
52
101
  }
53
102
 
54
103
  /**
55
- * Get the primary key field name.
104
+ * Get the primary key field name (JS property name).
56
105
  */
57
106
  protected static getPkField(): string {
58
107
  return Object.entries(this.fields).find(([, def]) => def.primaryKey)?.[0] ?? "id";
59
108
  }
60
109
 
110
+ /**
111
+ * Get the primary key database column name (applies fieldMapping).
112
+ */
113
+ protected static getPkColumn(): string {
114
+ return this.getDbColumn(this.getPkField());
115
+ }
116
+
61
117
  /**
62
118
  * Find a record by primary key.
63
119
  * @param id Primary key value.
@@ -67,7 +123,8 @@ export class BaseModel {
67
123
  const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
68
124
  const db = ModelClass.getDb();
69
125
  const pk = ModelClass.getPkField();
70
- let sql = `SELECT * FROM "${ModelClass.tableName}" WHERE "${pk}" = ?`;
126
+ const pkCol = ModelClass.getPkColumn();
127
+ let sql = `SELECT * FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`;
71
128
 
72
129
  if (ModelClass.softDelete) {
73
130
  sql += ` AND is_deleted = 0`;
@@ -129,6 +186,7 @@ export class BaseModel {
129
186
  const ModelClass = this.constructor as typeof BaseModel;
130
187
  const db = ModelClass.getDb();
131
188
  const pk = ModelClass.getPkField();
189
+ const pkCol = ModelClass.getPkColumn();
132
190
  const pkValue = this[pk];
133
191
  this._relCache = {}; // Clear relationship cache on save
134
192
 
@@ -139,17 +197,17 @@ export class BaseModel {
139
197
  );
140
198
  if (updateFields.length === 0) return;
141
199
 
142
- const setClause = updateFields.map(([k]) => `"${k}" = ?`).join(", ");
200
+ const setClause = updateFields.map(([k]) => `"${ModelClass.getDbColumn(k)}" = ?`).join(", ");
143
201
  const values = [...updateFields.map(([k]) => this[k]), pkValue];
144
202
 
145
- db.execute(`UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pk}" = ?`, values);
203
+ db.execute(`UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pkCol}" = ?`, values);
146
204
  } else {
147
205
  // Insert
148
206
  const insertFields = Object.entries(ModelClass.fields).filter(
149
207
  ([name, def]) => !(def.primaryKey && def.autoIncrement) && this[name] !== undefined,
150
208
  );
151
209
 
152
- const columns = insertFields.map(([k]) => `"${k}"`).join(", ");
210
+ const columns = insertFields.map(([k]) => `"${ModelClass.getDbColumn(k)}"`).join(", ");
153
211
  const placeholders = insertFields.map(() => "?").join(", ");
154
212
  const values = insertFields.map(([k]) => this[k]);
155
213
 
@@ -171,6 +229,7 @@ export class BaseModel {
171
229
  const ModelClass = this.constructor as typeof BaseModel;
172
230
  const db = ModelClass.getDb();
173
231
  const pk = ModelClass.getPkField();
232
+ const pkCol = ModelClass.getPkColumn();
174
233
  const pkValue = this[pk];
175
234
 
176
235
  if (pkValue === undefined || pkValue === null) {
@@ -179,13 +238,13 @@ export class BaseModel {
179
238
 
180
239
  if (ModelClass.softDelete) {
181
240
  db.execute(
182
- `UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE "${pk}" = ?`,
241
+ `UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE "${pkCol}" = ?`,
183
242
  [pkValue],
184
243
  );
185
244
  this.is_deleted = 1;
186
245
  } else {
187
246
  db.execute(
188
- `DELETE FROM "${ModelClass.tableName}" WHERE "${pk}" = ?`,
247
+ `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
189
248
  [pkValue],
190
249
  );
191
250
  }
@@ -317,7 +376,13 @@ export class BaseModel {
317
376
  if (db.tableExists(this.tableName)) return;
318
377
 
319
378
  if (typeof db.createTable === "function") {
320
- db.createTable(this.tableName, this.fields);
379
+ // Remap field keys to DB column names if fieldMapping is defined
380
+ const mappedFields: Record<string, FieldDefinition> = {};
381
+ for (const [fieldName, def] of Object.entries(this.fields)) {
382
+ const dbCol = this.getDbColumn(fieldName);
383
+ mappedFields[dbCol] = def;
384
+ }
385
+ db.createTable(this.tableName, mappedFields);
321
386
  } else {
322
387
  // Fallback: build SQL manually
323
388
  const typeMap: Record<string, string> = {
@@ -331,9 +396,10 @@ export class BaseModel {
331
396
  };
332
397
 
333
398
  const colDefs: string[] = [];
334
- for (const [colName, def] of Object.entries(this.fields)) {
399
+ for (const [fieldName, def] of Object.entries(this.fields)) {
400
+ const dbCol = this.getDbColumn(fieldName);
335
401
  const sqlType = typeMap[def.type] || "TEXT";
336
- const parts = [`"${colName}" ${sqlType}`];
402
+ const parts = [`"${dbCol}" ${sqlType}`];
337
403
  if (def.primaryKey) parts.push("PRIMARY KEY");
338
404
  if (def.autoIncrement) parts.push("AUTOINCREMENT");
339
405
  if (def.required && !def.primaryKey) parts.push("NOT NULL");
@@ -382,6 +448,7 @@ export class BaseModel {
382
448
  const ModelClass = this.constructor as typeof BaseModel;
383
449
  const db = ModelClass.getDb();
384
450
  const pk = ModelClass.getPkField();
451
+ const pkCol = ModelClass.getPkColumn();
385
452
  const pkValue = this[pk];
386
453
 
387
454
  if (pkValue === undefined || pkValue === null) {
@@ -389,7 +456,7 @@ export class BaseModel {
389
456
  }
390
457
 
391
458
  db.execute(
392
- `DELETE FROM "${ModelClass.tableName}" WHERE "${pk}" = ?`,
459
+ `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
393
460
  [pkValue],
394
461
  );
395
462
  }
@@ -405,6 +472,7 @@ export class BaseModel {
405
472
 
406
473
  const db = ModelClass.getDb();
407
474
  const pk = ModelClass.getPkField();
475
+ const pkCol = ModelClass.getPkColumn();
408
476
  const pkValue = this[pk];
409
477
 
410
478
  if (pkValue === undefined || pkValue === null) {
@@ -412,7 +480,7 @@ export class BaseModel {
412
480
  }
413
481
 
414
482
  db.execute(
415
- `UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pk}" = ?`,
483
+ `UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pkCol}" = ?`,
416
484
  [pkValue],
417
485
  );
418
486
  this.is_deleted = 0;
@@ -570,15 +638,19 @@ export class BaseModel {
570
638
  relatedClass: typeof BaseModel & (new (data?: Record<string, unknown>) => R),
571
639
  foreignKey: string,
572
640
  ): R | null {
573
- const fkValue = this[foreignKey];
641
+ // foreignKey is a DB column name — resolve to JS property name on this model
642
+ const ModelClass = this.constructor as typeof BaseModel;
643
+ const reverseMap = ModelClass.getReverseMapping();
644
+ const fkProp = reverseMap[foreignKey] ?? foreignKey;
645
+ const fkValue = this[fkProp];
574
646
 
575
647
  if (fkValue === undefined || fkValue === null) {
576
648
  return null;
577
649
  }
578
650
 
579
651
  const db = relatedClass.getDb();
580
- const relatedPk = relatedClass.getPkField();
581
- let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPk}" = ?`;
652
+ const relatedPkCol = relatedClass.getPkColumn();
653
+ let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPkCol}" = ?`;
582
654
  if (relatedClass.softDelete) {
583
655
  sql += ` AND is_deleted = 0`;
584
656
  }
@@ -727,10 +799,12 @@ export class BaseModel {
727
799
  relatedClass._eagerLoad(related, nested);
728
800
  }
729
801
 
730
- // Group by FK
802
+ // Group by FK — fk is a DB column name, resolve to JS property name on the related model
803
+ const relatedReverseMap = relatedClass.getReverseMapping();
804
+ const fkProp = relatedReverseMap[fk] ?? fk;
731
805
  const grouped: Record<string, BaseModel[]> = {};
732
806
  for (const record of related) {
733
- const fkVal = String(record[fk]);
807
+ const fkVal = String(record[fkProp]);
734
808
  if (!grouped[fkVal]) grouped[fkVal] = [];
735
809
  grouped[fkVal].push(record);
736
810
  }
@@ -745,16 +819,20 @@ export class BaseModel {
745
819
  }
746
820
  }
747
821
  } else if (relType === "belongsTo") {
822
+ // fk is a DB column name on the current model — resolve to JS property name
823
+ const ownerReverseMap = ModelClass.getReverseMapping();
824
+ const fkProp = ownerReverseMap[fk] ?? fk;
748
825
  const fkValues = [...new Set(
749
826
  instances
750
- .map((inst) => inst[fk])
827
+ .map((inst) => inst[fkProp])
751
828
  .filter((v) => v !== undefined && v !== null),
752
829
  )];
753
830
  if (fkValues.length === 0) continue;
754
831
 
755
832
  const relatedPk = relatedClass.getPkField();
833
+ const relatedPkCol = relatedClass.getPkColumn();
756
834
  const placeholders = fkValues.map(() => "?").join(",");
757
- let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPk}" IN (${placeholders})`;
835
+ let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPkCol}" IN (${placeholders})`;
758
836
  if (relatedClass.softDelete) {
759
837
  sql += ` AND is_deleted = 0`;
760
838
  }
@@ -772,7 +850,7 @@ export class BaseModel {
772
850
  }
773
851
 
774
852
  for (const inst of instances) {
775
- const fkVal = inst[fk];
853
+ const fkVal = inst[fkProp];
776
854
  inst._relCache[relName] = fkVal !== undefined && fkVal !== null
777
855
  ? lookup[String(fkVal)] ?? null
778
856
  : null;
@@ -1,4 +1,5 @@
1
- import type { DatabaseAdapter } from "./types.js";
1
+ import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult } from "./types.js";
2
+ import { DatabaseResult } from "./databaseResult.js";
2
3
 
3
4
  let activeAdapter: DatabaseAdapter | null = null;
4
5
  const namedAdapters: Map<string, DatabaseAdapter> = new Map();
@@ -171,15 +172,201 @@ export function parseDatabaseUrl(url: string, username?: string, password?: stri
171
172
  return result;
172
173
  }
173
174
 
175
+ /**
176
+ * A wrapper class around a DatabaseAdapter that provides a clean, high-level API.
177
+ *
178
+ * Mirrors the Database class in Python/Ruby Tina4 implementations.
179
+ *
180
+ * Usage:
181
+ * const db = await Database.create("sqlite:///path/to/db.sqlite");
182
+ * const rows = db.fetch("SELECT * FROM users WHERE active = ?", [true], 10, 0);
183
+ * const user = db.fetchOne("SELECT * FROM users WHERE id = ?", [1]);
184
+ * db.insert("users", { name: "Alice", email: "alice@example.com" });
185
+ * db.update("users", { name: "Bob" }, { id: 1 });
186
+ * db.delete("users", { id: 1 });
187
+ * db.close();
188
+ */
189
+ export class Database {
190
+ private adapter: DatabaseAdapter;
191
+
192
+ /**
193
+ * Create a Database wrapping an existing adapter.
194
+ * For creating a Database from a URL, use the async static factories:
195
+ * Database.create(url) or Database.fromEnv()
196
+ */
197
+ constructor(adapter: DatabaseAdapter) {
198
+ this.adapter = adapter;
199
+ }
200
+
201
+ /**
202
+ * Async factory: creates a Database from a connection URL.
203
+ * Works with all adapter types (sqlite, postgres, mysql, mssql, firebird).
204
+ */
205
+ static async create(url: string, username?: string, password?: string): Promise<Database> {
206
+ const adapter = await createAdapterFromUrl(url, username, password);
207
+ setAdapter(adapter);
208
+ return new Database(adapter);
209
+ }
210
+
211
+ /**
212
+ * Create a Database from an environment variable.
213
+ * @param envKey - Name of the env var holding the connection URL. Defaults to "DATABASE_URL".
214
+ */
215
+ static async fromEnv(envKey = "DATABASE_URL"): Promise<Database> {
216
+ const url = process.env[envKey];
217
+ if (!url) {
218
+ throw new Error(`Environment variable "${envKey}" is not set.`);
219
+ }
220
+ return Database.create(url);
221
+ }
222
+
223
+ /** Get the underlying adapter (for advanced / escape-hatch usage). */
224
+ getAdapter(): DatabaseAdapter {
225
+ return this.adapter;
226
+ }
227
+
228
+ /** Query rows with optional pagination. Returns a DatabaseResult wrapper. */
229
+ fetch(sql: string, params?: unknown[], limit?: number, offset?: number): DatabaseResult {
230
+ const rows = this.adapter.fetch<Record<string, unknown>>(sql, params, limit, offset);
231
+ return new DatabaseResult(rows, undefined, undefined, limit, offset, this.adapter, sql);
232
+ }
233
+
234
+ /** Fetch a single row or null. */
235
+ fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
236
+ return this.adapter.fetchOne<T>(sql, params);
237
+ }
238
+
239
+ /** Execute a statement (INSERT, UPDATE, DELETE, DDL). */
240
+ execute(sql: string, params?: unknown[]): unknown {
241
+ return this.adapter.execute(sql, params);
242
+ }
243
+
244
+ /** Insert a row into a table. */
245
+ insert(table: string, data: Record<string, unknown>): DatabaseWriteResult {
246
+ return this.adapter.insert(table, data);
247
+ }
248
+
249
+ /** Update rows in a table matching filter. */
250
+ update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown>): DatabaseWriteResult {
251
+ return this.adapter.update(table, data, filter ?? {});
252
+ }
253
+
254
+ /** Delete rows from a table matching filter. */
255
+ delete(table: string, filter?: Record<string, unknown>): DatabaseWriteResult {
256
+ return this.adapter.delete(table, filter ?? {});
257
+ }
258
+
259
+ /** Close the database connection. */
260
+ close(): void {
261
+ this.adapter.close();
262
+ }
263
+
264
+ /** Start a transaction. */
265
+ startTransaction(): void {
266
+ this.adapter.startTransaction();
267
+ }
268
+
269
+ /** Commit the current transaction. */
270
+ commit(): void {
271
+ this.adapter.commit();
272
+ }
273
+
274
+ /** Rollback the current transaction. */
275
+ rollback(): void {
276
+ this.adapter.rollback();
277
+ }
278
+
279
+ /** Check if a table exists. */
280
+ tableExists(name: string): boolean {
281
+ return this.adapter.tableExists(name);
282
+ }
283
+
284
+ /** List all tables in the database. */
285
+ getTables(): string[] {
286
+ return this.adapter.tables();
287
+ }
288
+
289
+ /** Get the last auto-increment id. */
290
+ getLastId(): string | number {
291
+ const id = this.adapter.lastInsertId();
292
+ if (id === null) return 0;
293
+ return typeof id === "bigint" ? id.toString() : id;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Internal helper: create a DatabaseAdapter from a parsed URL.
299
+ * Extracted from initDatabase so Database.create() can reuse it.
300
+ */
301
+ async function createAdapterFromUrl(url: string, username?: string, password?: string): Promise<DatabaseAdapter> {
302
+ const parsed = parseDatabaseUrl(url, username, password);
303
+
304
+ switch (parsed.type) {
305
+ case "sqlite": {
306
+ const { SQLiteAdapter } = await import("./adapters/sqlite.js");
307
+ return new SQLiteAdapter(parsed.path ?? "./data/tina4.db");
308
+ }
309
+ case "postgres": {
310
+ const { PostgresAdapter } = await import("./adapters/postgres.js");
311
+ const adapter = new PostgresAdapter({
312
+ host: parsed.host,
313
+ port: parsed.port,
314
+ user: parsed.user,
315
+ password: parsed.password,
316
+ database: parsed.database,
317
+ });
318
+ await adapter.connect();
319
+ return adapter;
320
+ }
321
+ case "mysql": {
322
+ const { MysqlAdapter } = await import("./adapters/mysql.js");
323
+ const adapter = new MysqlAdapter({
324
+ host: parsed.host,
325
+ port: parsed.port,
326
+ user: parsed.user,
327
+ password: parsed.password,
328
+ database: parsed.database,
329
+ });
330
+ await adapter.connect();
331
+ return adapter;
332
+ }
333
+ case "mssql": {
334
+ const { MssqlAdapter } = await import("./adapters/mssql.js");
335
+ const adapter = new MssqlAdapter({
336
+ host: parsed.host,
337
+ port: parsed.port,
338
+ user: parsed.user,
339
+ password: parsed.password,
340
+ database: parsed.database,
341
+ });
342
+ await adapter.connect();
343
+ return adapter;
344
+ }
345
+ case "firebird": {
346
+ const { FirebirdAdapter } = await import("./adapters/firebird.js");
347
+ const adapter = new FirebirdAdapter({
348
+ host: parsed.host,
349
+ port: parsed.port,
350
+ user: parsed.user,
351
+ password: parsed.password,
352
+ database: parsed.database,
353
+ });
354
+ await adapter.connect();
355
+ return adapter;
356
+ }
357
+ }
358
+ }
359
+
174
360
  /**
175
361
  * Initialize the database from a config object or DATABASE_URL env var.
362
+ * Now returns a Database wrapper instance.
176
363
  *
177
364
  * Priority:
178
365
  * 1. config.url (explicit URL)
179
366
  * 2. process.env.DATABASE_URL
180
367
  * 3. config.type + config.path (legacy)
181
368
  */
182
- export async function initDatabase(config?: DatabaseConfig): Promise<DatabaseAdapter> {
369
+ export async function initDatabase(config?: DatabaseConfig): Promise<Database> {
183
370
  // Resolve credentials: config.user > config.username > env DATABASE_USERNAME
184
371
  const resolvedUser = config?.user ?? config?.username ?? process.env.DATABASE_USERNAME;
185
372
  const resolvedPassword = config?.password ?? process.env.DATABASE_PASSWORD;
@@ -188,68 +375,9 @@ export async function initDatabase(config?: DatabaseConfig): Promise<DatabaseAda
188
375
  const url = config?.url ?? process.env.DATABASE_URL;
189
376
 
190
377
  if (url) {
191
- const parsed = parseDatabaseUrl(url, resolvedUser, resolvedPassword);
192
-
193
- switch (parsed.type) {
194
- case "sqlite": {
195
- const { SQLiteAdapter } = await import("./adapters/sqlite.js");
196
- const adapter = new SQLiteAdapter(parsed.path ?? "./data/tina4.db");
197
- setAdapter(adapter);
198
- return adapter;
199
- }
200
- case "postgres": {
201
- const { PostgresAdapter } = await import("./adapters/postgres.js");
202
- const adapter = new PostgresAdapter({
203
- host: parsed.host,
204
- port: parsed.port,
205
- user: parsed.user,
206
- password: parsed.password,
207
- database: parsed.database,
208
- });
209
- await adapter.connect();
210
- setAdapter(adapter);
211
- return adapter;
212
- }
213
- case "mysql": {
214
- const { MysqlAdapter } = await import("./adapters/mysql.js");
215
- const adapter = new MysqlAdapter({
216
- host: parsed.host,
217
- port: parsed.port,
218
- user: parsed.user,
219
- password: parsed.password,
220
- database: parsed.database,
221
- });
222
- await adapter.connect();
223
- setAdapter(adapter);
224
- return adapter;
225
- }
226
- case "mssql": {
227
- const { MssqlAdapter } = await import("./adapters/mssql.js");
228
- const adapter = new MssqlAdapter({
229
- host: parsed.host,
230
- port: parsed.port,
231
- user: parsed.user,
232
- password: parsed.password,
233
- database: parsed.database,
234
- });
235
- await adapter.connect();
236
- setAdapter(adapter);
237
- return adapter;
238
- }
239
- case "firebird": {
240
- const { FirebirdAdapter } = await import("./adapters/firebird.js");
241
- const adapter = new FirebirdAdapter({
242
- host: parsed.host,
243
- port: parsed.port,
244
- user: parsed.user,
245
- password: parsed.password,
246
- database: parsed.database,
247
- });
248
- await adapter.connect();
249
- setAdapter(adapter);
250
- return adapter;
251
- }
252
- }
378
+ const adapter = await createAdapterFromUrl(url, resolvedUser, resolvedPassword);
379
+ setAdapter(adapter);
380
+ return new Database(adapter);
253
381
  }
254
382
 
255
383
  // Legacy config path — normalize "sqlserver" to "mssql"
@@ -261,7 +389,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<DatabaseAda
261
389
  const { SQLiteAdapter } = await import("./adapters/sqlite.js");
262
390
  const adapter = new SQLiteAdapter(config?.path ?? "./data/tina4.db");
263
391
  setAdapter(adapter);
264
- return adapter;
392
+ return new Database(adapter);
265
393
  }
266
394
  case "postgres": {
267
395
  const { PostgresAdapter } = await import("./adapters/postgres.js");
@@ -274,7 +402,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<DatabaseAda
274
402
  });
275
403
  await adapter.connect();
276
404
  setAdapter(adapter);
277
- return adapter;
405
+ return new Database(adapter);
278
406
  }
279
407
  case "mysql": {
280
408
  const { MysqlAdapter } = await import("./adapters/mysql.js");
@@ -287,7 +415,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<DatabaseAda
287
415
  });
288
416
  await adapter.connect();
289
417
  setAdapter(adapter);
290
- return adapter;
418
+ return new Database(adapter);
291
419
  }
292
420
  case "mssql": {
293
421
  const { MssqlAdapter } = await import("./adapters/mssql.js");
@@ -300,7 +428,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<DatabaseAda
300
428
  });
301
429
  await adapter.connect();
302
430
  setAdapter(adapter);
303
- return adapter;
431
+ return new Database(adapter);
304
432
  }
305
433
  case "firebird": {
306
434
  const { FirebirdAdapter } = await import("./adapters/firebird.js");
@@ -313,7 +441,7 @@ export async function initDatabase(config?: DatabaseConfig): Promise<DatabaseAda
313
441
  });
314
442
  await adapter.connect();
315
443
  setAdapter(adapter);
316
- return adapter;
444
+ return new Database(adapter);
317
445
  }
318
446
  default:
319
447
  throw new Error(`Unknown database type: ${type}`);