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.
- package/CLAUDE.md +1 -1
- package/README.md +30 -2
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +13 -1
- package/packages/cli/src/commands/migrate.ts +19 -5
- package/packages/cli/src/commands/migrateCreate.ts +29 -28
- package/packages/cli/src/commands/migrateRollback.ts +59 -0
- package/packages/cli/src/commands/migrateStatus.ts +62 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +1 -1
- package/packages/core/src/auth.ts +44 -10
- package/packages/core/src/devAdmin.ts +14 -16
- package/packages/core/src/errorOverlay.ts +17 -15
- package/packages/core/src/index.ts +9 -2
- package/packages/core/src/queue.ts +127 -25
- package/packages/core/src/queueBackends/mongoBackend.ts +223 -0
- package/packages/core/src/request.ts +3 -3
- package/packages/core/src/routeDiscovery.ts +2 -1
- package/packages/core/src/router.ts +90 -51
- package/packages/core/src/server.ts +62 -4
- package/packages/core/src/session.ts +17 -1
- package/packages/core/src/sessionHandlers/databaseHandler.ts +134 -0
- package/packages/core/src/sessionHandlers/redisHandler.ts +230 -0
- package/packages/core/src/types.ts +12 -6
- package/packages/core/src/websocket.ts +11 -2
- package/packages/core/src/websocketConnection.ts +4 -2
- package/packages/frond/src/engine.ts +66 -1
- package/packages/orm/src/autoCrud.ts +17 -12
- package/packages/orm/src/baseModel.ts +99 -21
- package/packages/orm/src/database.ts +197 -69
- package/packages/orm/src/databaseResult.ts +207 -0
- package/packages/orm/src/index.ts +6 -3
- package/packages/orm/src/migration.ts +296 -71
- package/packages/orm/src/model.ts +1 -0
- 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
|
|
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
|
-
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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
|
-
|
|
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 [
|
|
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 = [`"${
|
|
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 "${
|
|
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 "${
|
|
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
|
-
|
|
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
|
|
581
|
-
let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${
|
|
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[
|
|
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[
|
|
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 "${
|
|
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[
|
|
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<
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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}`);
|