tina4-nodejs 3.13.14 → 3.13.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +42 -36
- package/package.json +1 -1
- package/packages/cli/src/commands/migrate.ts +7 -5
- package/packages/cli/src/commands/migrateRollback.ts +3 -3
- package/packages/core/src/server.ts +1 -1
- package/packages/orm/src/adapters/postgres.ts +29 -0
- package/packages/orm/src/autoCrud.ts +32 -16
- package/packages/orm/src/baseModel.ts +233 -197
- package/packages/orm/src/database.ts +189 -70
- package/packages/orm/src/databaseResult.ts +24 -0
- package/packages/orm/src/index.ts +6 -0
- package/packages/orm/src/migration.ts +128 -75
- package/packages/orm/src/queryBuilder.ts +12 -9
- package/packages/orm/src/seeder.ts +2 -1
|
@@ -3,7 +3,12 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import type { FieldDefinition, DatabaseAdapter } from "./types.js";
|
|
4
4
|
import type { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
5
5
|
import type { DiscoveredModel } from "./model.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getAdapter,
|
|
8
|
+
adapterQuery, adapterExecute, adapterTableExists,
|
|
9
|
+
adapterCreateTable, adapterColumns,
|
|
10
|
+
adapterStartTransaction, adapterCommit, adapterRollback,
|
|
11
|
+
} from "./database.js";
|
|
7
12
|
|
|
8
13
|
// ---------------------------------------------------------------------------
|
|
9
14
|
// Firebird ALTER TABLE ADD idempotency check
|
|
@@ -74,8 +79,8 @@ async function shouldSkipForFirebird(
|
|
|
74
79
|
/**
|
|
75
80
|
* Sync model definitions to the database (create tables, add columns).
|
|
76
81
|
*/
|
|
77
|
-
export function syncModels(models: DiscoveredModel[]): void {
|
|
78
|
-
const adapter = getAdapter()
|
|
82
|
+
export async function syncModels(models: DiscoveredModel[]): Promise<void> {
|
|
83
|
+
const adapter = getAdapter();
|
|
79
84
|
|
|
80
85
|
for (const { definition } of models) {
|
|
81
86
|
const { tableName, fields, softDelete, fieldMapping } = definition;
|
|
@@ -100,17 +105,26 @@ export function syncModels(models: DiscoveredModel[]): void {
|
|
|
100
105
|
dbFields[dbCol] = def;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
if (!adapter
|
|
104
|
-
|
|
108
|
+
if (!(await adapterTableExists(adapter, tableName))) {
|
|
109
|
+
// adapterCreateTable prefers createTableAsync — engine-aware DDL on
|
|
110
|
+
// PostgreSQL/MySQL/MSSQL/Firebird (TIMESTAMP/BOOLEAN/SERIAL etc.).
|
|
111
|
+
await adapterCreateTable(adapter, tableName, dbFields);
|
|
105
112
|
console.log(` Created table: ${tableName}`);
|
|
106
113
|
} else {
|
|
107
|
-
// Check for new columns
|
|
108
|
-
|
|
109
|
-
const
|
|
114
|
+
// Check for new columns. SQLite exposes the legacy getTableColumns/
|
|
115
|
+
// addColumn helpers; other engines use columns()/ALTER TABLE.
|
|
116
|
+
const existingCols = (adapter as any).getTableColumns
|
|
117
|
+
? (adapter as SQLiteAdapter).getTableColumns(tableName)
|
|
118
|
+
: await adapterColumns(adapter, tableName);
|
|
119
|
+
const existingNames = new Set(existingCols.map((c) => c.name.toLowerCase()));
|
|
110
120
|
|
|
111
121
|
for (const [colName, def] of Object.entries(dbFields)) {
|
|
112
|
-
if (!existingNames.has(colName)) {
|
|
113
|
-
adapter.addColumn
|
|
122
|
+
if (!existingNames.has(colName.toLowerCase())) {
|
|
123
|
+
if ((adapter as any).addColumn) {
|
|
124
|
+
(adapter as any).addColumn(tableName, colName, def);
|
|
125
|
+
} else {
|
|
126
|
+
await adapterExecute(adapter, buildAddColumnSql(adapter, tableName, colName, def));
|
|
127
|
+
}
|
|
114
128
|
console.log(` Added column: ${tableName}.${colName}`);
|
|
115
129
|
}
|
|
116
130
|
}
|
|
@@ -118,6 +132,35 @@ export function syncModels(models: DiscoveredModel[]): void {
|
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Build an engine-aware `ALTER TABLE ... ADD COLUMN` statement for adapters
|
|
137
|
+
* that don't expose the SQLite-style addColumn() helper.
|
|
138
|
+
*/
|
|
139
|
+
function buildAddColumnSql(
|
|
140
|
+
adapter: DatabaseAdapter,
|
|
141
|
+
table: string,
|
|
142
|
+
colName: string,
|
|
143
|
+
def: FieldDefinition,
|
|
144
|
+
): string {
|
|
145
|
+
const engine = adapter.constructor.name;
|
|
146
|
+
const isPg = engine === "PostgresAdapter";
|
|
147
|
+
const typeMap: Record<string, string> = isPg
|
|
148
|
+
? { integer: "INTEGER", string: def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)", text: "TEXT", number: "DOUBLE PRECISION", numeric: "DOUBLE PRECISION", boolean: "BOOLEAN", datetime: "TIMESTAMP" }
|
|
149
|
+
: { integer: "INTEGER", string: def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)", text: "TEXT", number: "DOUBLE PRECISION", numeric: "DOUBLE PRECISION", boolean: "INTEGER", datetime: "TIMESTAMP" };
|
|
150
|
+
const sqlType = typeMap[def.type] ?? "TEXT";
|
|
151
|
+
let sql = `ALTER TABLE "${table}" ADD COLUMN "${colName}" ${sqlType}`;
|
|
152
|
+
if (def.default !== undefined && def.default !== "now") {
|
|
153
|
+
const dv =
|
|
154
|
+
typeof def.default === "string" ? `'${def.default}'`
|
|
155
|
+
: typeof def.default === "boolean" ? (isPg ? (def.default ? "TRUE" : "FALSE") : (def.default ? "1" : "0"))
|
|
156
|
+
: String(def.default);
|
|
157
|
+
sql += ` DEFAULT ${dv}`;
|
|
158
|
+
} else if (def.default === "now") {
|
|
159
|
+
sql += " DEFAULT CURRENT_TIMESTAMP";
|
|
160
|
+
}
|
|
161
|
+
return sql;
|
|
162
|
+
}
|
|
163
|
+
|
|
121
164
|
/**
|
|
122
165
|
* Migration tracking table name.
|
|
123
166
|
*/
|
|
@@ -126,25 +169,25 @@ const MIGRATION_TABLE = "tina4_migration";
|
|
|
126
169
|
/**
|
|
127
170
|
* Ensure the migration tracking table exists with batch support.
|
|
128
171
|
*/
|
|
129
|
-
export function ensureMigrationTable(): void {
|
|
172
|
+
export async function ensureMigrationTable(): Promise<void> {
|
|
130
173
|
const adapter = getAdapter();
|
|
131
|
-
if (!adapter
|
|
174
|
+
if (!(await adapterTableExists(adapter, MIGRATION_TABLE))) {
|
|
132
175
|
if (isFirebirdAdapter(adapter)) {
|
|
133
176
|
// Firebird: no AUTOINCREMENT, no TEXT type, use generator for IDs
|
|
134
177
|
try {
|
|
135
|
-
adapter
|
|
136
|
-
try { adapter
|
|
178
|
+
await adapterExecute(adapter, "CREATE GENERATOR GEN_TINA4_MIGRATION_ID");
|
|
179
|
+
try { await adapterExecute(adapter, "COMMIT"); } catch { /* ignore */ }
|
|
137
180
|
} catch {
|
|
138
181
|
// Generator may already exist
|
|
139
182
|
}
|
|
140
|
-
adapter
|
|
183
|
+
await adapterExecute(adapter, `CREATE TABLE "${MIGRATION_TABLE}" (
|
|
141
184
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
142
185
|
name VARCHAR(500) NOT NULL,
|
|
143
186
|
batch INTEGER NOT NULL DEFAULT 1,
|
|
144
187
|
applied_at VARCHAR(50) NOT NULL
|
|
145
188
|
)`);
|
|
146
189
|
} else {
|
|
147
|
-
(adapter
|
|
190
|
+
await adapterCreateTable(adapter, MIGRATION_TABLE, {
|
|
148
191
|
id: { type: "integer", primaryKey: true, autoIncrement: true },
|
|
149
192
|
name: { type: "string", required: true },
|
|
150
193
|
batch: { type: "integer", required: true },
|
|
@@ -154,10 +197,12 @@ export function ensureMigrationTable(): void {
|
|
|
154
197
|
} else {
|
|
155
198
|
// Ensure batch column exists on older tables that only had passed/description
|
|
156
199
|
try {
|
|
157
|
-
const cols = (adapter as
|
|
158
|
-
|
|
200
|
+
const cols = (adapter as any).getTableColumns
|
|
201
|
+
? (adapter as SQLiteAdapter).getTableColumns(MIGRATION_TABLE)
|
|
202
|
+
: await adapterColumns(adapter, MIGRATION_TABLE);
|
|
203
|
+
const colNames = new Set(cols.map((c) => c.name.toLowerCase()));
|
|
159
204
|
if (!colNames.has("batch")) {
|
|
160
|
-
adapter
|
|
205
|
+
await adapterExecute(adapter, `ALTER TABLE "${MIGRATION_TABLE}" ADD COLUMN batch INTEGER NOT NULL DEFAULT 1`);
|
|
161
206
|
}
|
|
162
207
|
} catch {
|
|
163
208
|
// ignore — column may already exist
|
|
@@ -168,9 +213,9 @@ export function ensureMigrationTable(): void {
|
|
|
168
213
|
/**
|
|
169
214
|
* Get the current batch number (max batch + 1).
|
|
170
215
|
*/
|
|
171
|
-
export function getNextBatch(): number {
|
|
216
|
+
export async function getNextBatch(): Promise<number> {
|
|
172
217
|
const adapter = getAdapter();
|
|
173
|
-
const rows =
|
|
218
|
+
const rows = await adapterQuery<{ max_batch: number | null }>(adapter,
|
|
174
219
|
`SELECT MAX(batch) as max_batch FROM "${MIGRATION_TABLE}"`,
|
|
175
220
|
);
|
|
176
221
|
return (rows[0]?.max_batch ?? 0) + 1;
|
|
@@ -179,9 +224,9 @@ export function getNextBatch(): number {
|
|
|
179
224
|
/**
|
|
180
225
|
* Check if a migration has already been applied.
|
|
181
226
|
*/
|
|
182
|
-
export function isMigrationApplied(name: string): boolean {
|
|
227
|
+
export async function isMigrationApplied(name: string): Promise<boolean> {
|
|
183
228
|
const adapter = getAdapter();
|
|
184
|
-
const rows = adapter
|
|
229
|
+
const rows = await adapterQuery(adapter,
|
|
185
230
|
`SELECT id FROM "${MIGRATION_TABLE}" WHERE name = ?`,
|
|
186
231
|
[name],
|
|
187
232
|
);
|
|
@@ -191,20 +236,20 @@ export function isMigrationApplied(name: string): boolean {
|
|
|
191
236
|
/**
|
|
192
237
|
* Record a migration as applied.
|
|
193
238
|
*/
|
|
194
|
-
export function recordMigration(name: string, batch: number, passed: number = 1): void {
|
|
239
|
+
export async function recordMigration(name: string, batch: number, passed: number = 1): Promise<void> {
|
|
195
240
|
const adapter = getAdapter();
|
|
196
241
|
if (isFirebirdAdapter(adapter)) {
|
|
197
242
|
// Firebird: generate ID from sequence
|
|
198
|
-
const rows =
|
|
243
|
+
const rows = await adapterQuery<{ NEXT_ID: number }>(adapter,
|
|
199
244
|
"SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE",
|
|
200
245
|
);
|
|
201
246
|
const nextId = rows[0]?.NEXT_ID ?? 1;
|
|
202
|
-
adapter
|
|
247
|
+
await adapterExecute(adapter,
|
|
203
248
|
`INSERT INTO "${MIGRATION_TABLE}" (id, name, batch) VALUES (?, ?, ?)`,
|
|
204
249
|
[nextId, name, batch],
|
|
205
250
|
);
|
|
206
251
|
} else {
|
|
207
|
-
adapter
|
|
252
|
+
await adapterExecute(adapter,
|
|
208
253
|
`INSERT INTO "${MIGRATION_TABLE}" (name, batch) VALUES (?, ?)`,
|
|
209
254
|
[name, batch],
|
|
210
255
|
);
|
|
@@ -214,30 +259,30 @@ export function recordMigration(name: string, batch: number, passed: number = 1)
|
|
|
214
259
|
/**
|
|
215
260
|
* Apply a migration (run its up function and record it).
|
|
216
261
|
*/
|
|
217
|
-
export function applyMigration(
|
|
262
|
+
export async function applyMigration(
|
|
218
263
|
name: string,
|
|
219
|
-
up: () => void
|
|
264
|
+
up: () => void | Promise<void>,
|
|
220
265
|
batch: number,
|
|
221
|
-
): void {
|
|
222
|
-
if (isMigrationApplied(name)) {
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
if (await isMigrationApplied(name)) {
|
|
223
268
|
return;
|
|
224
269
|
}
|
|
225
|
-
up();
|
|
226
|
-
recordMigration(name, batch);
|
|
270
|
+
await up();
|
|
271
|
+
await recordMigration(name, batch);
|
|
227
272
|
}
|
|
228
273
|
|
|
229
274
|
/**
|
|
230
275
|
* Get all migrations from the last batch.
|
|
231
276
|
*/
|
|
232
|
-
export function getLastBatchMigrations(): Array<{ id: number; name: string; batch: number }
|
|
277
|
+
export async function getLastBatchMigrations(): Promise<Array<{ id: number; name: string; batch: number }>> {
|
|
233
278
|
const adapter = getAdapter();
|
|
234
|
-
const rows =
|
|
279
|
+
const rows = await adapterQuery<{ max_batch: number | null }>(adapter,
|
|
235
280
|
`SELECT MAX(batch) as max_batch FROM "${MIGRATION_TABLE}"`,
|
|
236
281
|
);
|
|
237
282
|
const lastBatch = rows[0]?.max_batch;
|
|
238
283
|
if (lastBatch === null || lastBatch === undefined) return [];
|
|
239
284
|
|
|
240
|
-
return
|
|
285
|
+
return adapterQuery<{ id: number; name: string; batch: number }>(adapter,
|
|
241
286
|
`SELECT id, name, batch FROM "${MIGRATION_TABLE}" WHERE batch = ? ORDER BY id DESC`,
|
|
242
287
|
[lastBatch],
|
|
243
288
|
);
|
|
@@ -246,9 +291,9 @@ export function getLastBatchMigrations(): Array<{ id: number; name: string; batc
|
|
|
246
291
|
/**
|
|
247
292
|
* Remove a migration record (used during rollback).
|
|
248
293
|
*/
|
|
249
|
-
export function removeMigrationRecord(name: string): void {
|
|
294
|
+
export async function removeMigrationRecord(name: string): Promise<void> {
|
|
250
295
|
const adapter = getAdapter();
|
|
251
|
-
adapter
|
|
296
|
+
await adapterExecute(adapter,
|
|
252
297
|
`DELETE FROM "${MIGRATION_TABLE}" WHERE name = ?`,
|
|
253
298
|
[name],
|
|
254
299
|
);
|
|
@@ -267,21 +312,21 @@ export function removeMigrationRecord(name: string): void {
|
|
|
267
312
|
* @param delimiter - SQL statement delimiter (default: ";")
|
|
268
313
|
* @returns Array of rolled-back migration names
|
|
269
314
|
*/
|
|
270
|
-
export function rollback(
|
|
271
|
-
migrationsDir?: string | Map<string, () => void
|
|
315
|
+
export async function rollback(
|
|
316
|
+
migrationsDir?: string | Map<string, () => void | Promise<void>>,
|
|
272
317
|
delimiter?: string,
|
|
273
|
-
): string[] {
|
|
318
|
+
): Promise<string[]> {
|
|
274
319
|
// Handle legacy API: if first arg is a Map, use old behaviour
|
|
275
320
|
if (migrationsDir instanceof Map) {
|
|
276
321
|
const downFunctions = migrationsDir;
|
|
277
|
-
const migrations = getLastBatchMigrations();
|
|
322
|
+
const migrations = await getLastBatchMigrations();
|
|
278
323
|
const rolledBack: string[] = [];
|
|
279
324
|
for (const migration of migrations) {
|
|
280
325
|
const down = downFunctions.get(migration.name);
|
|
281
326
|
if (down) {
|
|
282
|
-
down();
|
|
327
|
+
await down();
|
|
283
328
|
}
|
|
284
|
-
removeMigrationRecord(migration.name);
|
|
329
|
+
await removeMigrationRecord(migration.name);
|
|
285
330
|
rolledBack.push(migration.name);
|
|
286
331
|
}
|
|
287
332
|
return rolledBack;
|
|
@@ -290,7 +335,7 @@ export function rollback(
|
|
|
290
335
|
const dir = resolve(migrationsDir ?? "migrations");
|
|
291
336
|
const delim = delimiter ?? ";";
|
|
292
337
|
const db = getAdapter();
|
|
293
|
-
const migrations = getLastBatchMigrations();
|
|
338
|
+
const migrations = await getLastBatchMigrations();
|
|
294
339
|
const rolledBack: string[] = [];
|
|
295
340
|
|
|
296
341
|
for (const migration of migrations) {
|
|
@@ -303,14 +348,14 @@ export function rollback(
|
|
|
303
348
|
if (sqlContent) {
|
|
304
349
|
const statements = splitStatements(sqlContent, delim);
|
|
305
350
|
try {
|
|
306
|
-
db
|
|
351
|
+
await adapterStartTransaction(db);
|
|
307
352
|
for (const stmt of statements) {
|
|
308
|
-
db
|
|
353
|
+
await adapterExecute(db, stmt);
|
|
309
354
|
}
|
|
310
|
-
db
|
|
355
|
+
await adapterCommit(db);
|
|
311
356
|
} catch (err) {
|
|
312
357
|
try {
|
|
313
|
-
db
|
|
358
|
+
await adapterRollback(db);
|
|
314
359
|
} catch {
|
|
315
360
|
// rollback may fail if auto-rolled-back
|
|
316
361
|
}
|
|
@@ -323,7 +368,7 @@ export function rollback(
|
|
|
323
368
|
console.warn(` Warning: No .down.sql file found for ${migration.name} — skipping SQL execution`);
|
|
324
369
|
}
|
|
325
370
|
|
|
326
|
-
removeMigrationRecord(migration.name);
|
|
371
|
+
await removeMigrationRecord(migration.name);
|
|
327
372
|
rolledBack.push(migration.name);
|
|
328
373
|
}
|
|
329
374
|
|
|
@@ -333,9 +378,9 @@ export function rollback(
|
|
|
333
378
|
/**
|
|
334
379
|
* Get all applied migrations.
|
|
335
380
|
*/
|
|
336
|
-
export function getAppliedMigrations(): Array<{ id: number; name: string; batch: number; applied_at: string }
|
|
381
|
+
export async function getAppliedMigrations(): Promise<Array<{ id: number; name: string; batch: number; applied_at: string }>> {
|
|
337
382
|
const adapter = getAdapter();
|
|
338
|
-
return
|
|
383
|
+
return adapterQuery<{ id: number; name: string; batch: number; applied_at: string }>(adapter,
|
|
339
384
|
`SELECT * FROM "${MIGRATION_TABLE}" ORDER BY id ASC`,
|
|
340
385
|
);
|
|
341
386
|
}
|
|
@@ -465,23 +510,31 @@ export async function migrate(
|
|
|
465
510
|
}
|
|
466
511
|
|
|
467
512
|
// Ensure tracking table with batch support
|
|
468
|
-
if (!db
|
|
513
|
+
if (!(await adapterTableExists(db, MIGRATION_TABLE))) {
|
|
469
514
|
if (isFirebirdAdapter(db)) {
|
|
470
515
|
// Firebird: no AUTOINCREMENT, no TEXT type, use generator for IDs
|
|
471
516
|
try {
|
|
472
|
-
db
|
|
473
|
-
try { db
|
|
517
|
+
await adapterExecute(db, "CREATE GENERATOR GEN_TINA4_MIGRATION_ID");
|
|
518
|
+
try { await adapterExecute(db, "COMMIT"); } catch { /* ignore */ }
|
|
474
519
|
} catch {
|
|
475
520
|
// Generator may already exist
|
|
476
521
|
}
|
|
477
|
-
db
|
|
522
|
+
await adapterExecute(db, `CREATE TABLE "${MIGRATION_TABLE}" (
|
|
478
523
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
479
524
|
name VARCHAR(500) NOT NULL,
|
|
480
525
|
batch INTEGER NOT NULL DEFAULT 1,
|
|
481
526
|
applied_at VARCHAR(50) NOT NULL
|
|
482
527
|
)`);
|
|
528
|
+
} else if (db.constructor.name === "PostgresAdapter") {
|
|
529
|
+
// PostgreSQL: SERIAL + TIMESTAMP (not AUTOINCREMENT/TEXT).
|
|
530
|
+
await adapterExecute(db, `CREATE TABLE IF NOT EXISTS "${MIGRATION_TABLE}" (
|
|
531
|
+
id SERIAL PRIMARY KEY,
|
|
532
|
+
name TEXT NOT NULL,
|
|
533
|
+
batch INTEGER NOT NULL DEFAULT 1,
|
|
534
|
+
applied_at TEXT NOT NULL
|
|
535
|
+
)`);
|
|
483
536
|
} else {
|
|
484
|
-
db
|
|
537
|
+
await adapterExecute(db, `CREATE TABLE IF NOT EXISTS "${MIGRATION_TABLE}" (
|
|
485
538
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
486
539
|
name TEXT NOT NULL,
|
|
487
540
|
batch INTEGER NOT NULL DEFAULT 1,
|
|
@@ -491,7 +544,7 @@ export async function migrate(
|
|
|
491
544
|
} else {
|
|
492
545
|
// Migrate old schema: if table has 'description' + 'passed' columns, migrate data
|
|
493
546
|
try {
|
|
494
|
-
|
|
547
|
+
await adapterQuery<Record<string, unknown>>(db,
|
|
495
548
|
`SELECT * FROM "${MIGRATION_TABLE}" LIMIT 0`,
|
|
496
549
|
);
|
|
497
550
|
// Check column names by querying pragma or just try adding batch
|
|
@@ -510,7 +563,7 @@ export async function migrate(
|
|
|
510
563
|
// Determine the batch number for this run
|
|
511
564
|
let currentBatch = 1;
|
|
512
565
|
try {
|
|
513
|
-
const batchRows =
|
|
566
|
+
const batchRows = await adapterQuery<{ max_batch: number | null }>(db,
|
|
514
567
|
`SELECT MAX(batch) as max_batch FROM "${MIGRATION_TABLE}"`,
|
|
515
568
|
);
|
|
516
569
|
currentBatch = (batchRows[0]?.max_batch ?? 0) + 1;
|
|
@@ -525,7 +578,7 @@ export async function migrate(
|
|
|
525
578
|
// Check if already applied — support both 'name' and legacy 'description' column
|
|
526
579
|
let alreadyApplied = false;
|
|
527
580
|
try {
|
|
528
|
-
const existing =
|
|
581
|
+
const existing = await adapterQuery<{ id: number }>(db,
|
|
529
582
|
`SELECT id FROM "${MIGRATION_TABLE}" WHERE name = ?`,
|
|
530
583
|
[migrationId],
|
|
531
584
|
);
|
|
@@ -533,7 +586,7 @@ export async function migrate(
|
|
|
533
586
|
} catch {
|
|
534
587
|
// Might be old schema with 'description' column instead of 'name'
|
|
535
588
|
try {
|
|
536
|
-
const existing =
|
|
589
|
+
const existing = await adapterQuery<{ id: number; passed: number }>(db,
|
|
537
590
|
`SELECT id, passed FROM "${MIGRATION_TABLE}" WHERE description = ?`,
|
|
538
591
|
[migrationId],
|
|
539
592
|
);
|
|
@@ -541,7 +594,7 @@ export async function migrate(
|
|
|
541
594
|
alreadyApplied = true;
|
|
542
595
|
} else if (existing.length > 0 && existing[0].passed === 0) {
|
|
543
596
|
// Failed record from old schema — remove to retry
|
|
544
|
-
db
|
|
597
|
+
await adapterExecute(db,
|
|
545
598
|
`DELETE FROM "${MIGRATION_TABLE}" WHERE description = ?`,
|
|
546
599
|
[migrationId],
|
|
547
600
|
);
|
|
@@ -565,7 +618,7 @@ export async function migrate(
|
|
|
565
618
|
const statements = splitStatements(sqlContent, delimiter);
|
|
566
619
|
|
|
567
620
|
try {
|
|
568
|
-
db
|
|
621
|
+
await adapterStartTransaction(db);
|
|
569
622
|
|
|
570
623
|
for (const stmt of statements) {
|
|
571
624
|
// Firebird lacks IF NOT EXISTS for ALTER TABLE ADD.
|
|
@@ -576,7 +629,7 @@ export async function migrate(
|
|
|
576
629
|
console.log(` Migration ${file}: ${skipReason}`);
|
|
577
630
|
continue;
|
|
578
631
|
}
|
|
579
|
-
db
|
|
632
|
+
await adapterExecute(db, stmt);
|
|
580
633
|
}
|
|
581
634
|
|
|
582
635
|
// Record as applied with batch number
|
|
@@ -584,33 +637,33 @@ export async function migrate(
|
|
|
584
637
|
try {
|
|
585
638
|
if (isFirebirdAdapter(db)) {
|
|
586
639
|
// Firebird: generate ID from sequence
|
|
587
|
-
const idRows =
|
|
640
|
+
const idRows = await adapterQuery<{ NEXT_ID: number }>(db,
|
|
588
641
|
"SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE",
|
|
589
642
|
);
|
|
590
643
|
const nextId = idRows[0]?.NEXT_ID ?? 1;
|
|
591
|
-
db
|
|
644
|
+
await adapterExecute(db,
|
|
592
645
|
`INSERT INTO "${MIGRATION_TABLE}" (id, name, batch, applied_at) VALUES (?, ?, ?, ?)`,
|
|
593
646
|
[nextId, migrationId, currentBatch, now],
|
|
594
647
|
);
|
|
595
648
|
} else {
|
|
596
|
-
db
|
|
649
|
+
await adapterExecute(db,
|
|
597
650
|
`INSERT INTO "${MIGRATION_TABLE}" (name, batch, applied_at) VALUES (?, ?, ?)`,
|
|
598
651
|
[migrationId, currentBatch, now],
|
|
599
652
|
);
|
|
600
653
|
}
|
|
601
654
|
} catch {
|
|
602
655
|
// Old schema fallback — try description/content/passed columns
|
|
603
|
-
db
|
|
656
|
+
await adapterExecute(db,
|
|
604
657
|
`INSERT INTO "${MIGRATION_TABLE}" (description, content, passed, run_at) VALUES (?, ?, 1, ?)`,
|
|
605
658
|
[migrationId, sqlContent, now],
|
|
606
659
|
);
|
|
607
660
|
}
|
|
608
661
|
|
|
609
|
-
db
|
|
662
|
+
await adapterCommit(db);
|
|
610
663
|
result.applied.push(file);
|
|
611
664
|
} catch (err) {
|
|
612
665
|
try {
|
|
613
|
-
db
|
|
666
|
+
await adapterRollback(db);
|
|
614
667
|
} catch {
|
|
615
668
|
// rollback may fail if transaction was auto-rolled-back
|
|
616
669
|
}
|
|
@@ -646,7 +699,7 @@ export async function status(
|
|
|
646
699
|
}
|
|
647
700
|
|
|
648
701
|
// Ensure tracking table exists
|
|
649
|
-
if (!db
|
|
702
|
+
if (!(await adapterTableExists(db, MIGRATION_TABLE))) {
|
|
650
703
|
// No table means nothing has been run — all files are pending
|
|
651
704
|
const files = sortMigrationFiles(
|
|
652
705
|
readdirSync(dir).filter((f) => f.endsWith(".sql") && !f.endsWith(".down.sql")),
|
|
@@ -663,7 +716,7 @@ export async function status(
|
|
|
663
716
|
// Get all applied migration names from the DB
|
|
664
717
|
const appliedNames = new Set<string>();
|
|
665
718
|
try {
|
|
666
|
-
const rows =
|
|
719
|
+
const rows = await adapterQuery<{ name: string }>(db,
|
|
667
720
|
`SELECT name FROM "${MIGRATION_TABLE}"`,
|
|
668
721
|
);
|
|
669
722
|
for (const row of rows) {
|
|
@@ -672,7 +725,7 @@ export async function status(
|
|
|
672
725
|
} catch {
|
|
673
726
|
// Old schema with 'description' column
|
|
674
727
|
try {
|
|
675
|
-
const rows =
|
|
728
|
+
const rows = await adapterQuery<{ description: string; passed: number }>(db,
|
|
676
729
|
`SELECT description, passed FROM "${MIGRATION_TABLE}" WHERE passed = 1`,
|
|
677
730
|
);
|
|
678
731
|
for (const row of rows) {
|
|
@@ -848,10 +901,10 @@ export class Migration {
|
|
|
848
901
|
async rollback(steps = 1): Promise<string[]> {
|
|
849
902
|
const db = this.db ?? (await import("./database.js")).getAdapter();
|
|
850
903
|
// If tracking table doesn't exist yet there's nothing to roll back
|
|
851
|
-
if (!db
|
|
904
|
+
if (!(await adapterTableExists(db, MIGRATION_TABLE))) return [];
|
|
852
905
|
const rolled: string[] = [];
|
|
853
906
|
for (let i = 0; i < steps; i++) {
|
|
854
|
-
const batch = rollback(this.dir, this.delimiter);
|
|
907
|
+
const batch = await rollback(this.dir, this.delimiter);
|
|
855
908
|
if (batch.length === 0) break;
|
|
856
909
|
rolled.push(...batch);
|
|
857
910
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import type { DatabaseAdapter } from "./types.js";
|
|
21
|
-
import { getAdapter } from "./database.js";
|
|
21
|
+
import { getAdapter, adapterFetch, adapterFetchOne } from "./database.js";
|
|
22
22
|
|
|
23
23
|
export class QueryBuilder {
|
|
24
24
|
private table: string;
|
|
@@ -202,12 +202,13 @@ export class QueryBuilder {
|
|
|
202
202
|
*
|
|
203
203
|
* @returns Array of row objects.
|
|
204
204
|
*/
|
|
205
|
-
get<T = Record<string, unknown>>(): T[] {
|
|
205
|
+
async get<T = Record<string, unknown>>(): Promise<T[]> {
|
|
206
206
|
this.ensureDb();
|
|
207
207
|
const sql = this.toSql();
|
|
208
208
|
const allParams = [...this.params, ...this.havingParams];
|
|
209
209
|
|
|
210
|
-
return
|
|
210
|
+
return adapterFetch<T>(
|
|
211
|
+
this.db!,
|
|
211
212
|
sql,
|
|
212
213
|
allParams.length > 0 ? allParams : undefined,
|
|
213
214
|
this.limitVal,
|
|
@@ -220,12 +221,13 @@ export class QueryBuilder {
|
|
|
220
221
|
*
|
|
221
222
|
* @returns A single row object, or null.
|
|
222
223
|
*/
|
|
223
|
-
first<T = Record<string, unknown>>(): T | null {
|
|
224
|
+
async first<T = Record<string, unknown>>(): Promise<T | null> {
|
|
224
225
|
this.ensureDb();
|
|
225
226
|
const sql = this.toSql();
|
|
226
227
|
const allParams = [...this.params, ...this.havingParams];
|
|
227
228
|
|
|
228
|
-
return
|
|
229
|
+
return adapterFetchOne<T>(
|
|
230
|
+
this.db!,
|
|
229
231
|
sql,
|
|
230
232
|
allParams.length > 0 ? allParams : undefined,
|
|
231
233
|
);
|
|
@@ -236,7 +238,7 @@ export class QueryBuilder {
|
|
|
236
238
|
*
|
|
237
239
|
* @returns Number of matching rows.
|
|
238
240
|
*/
|
|
239
|
-
count(): number {
|
|
241
|
+
async count(): Promise<number> {
|
|
240
242
|
this.ensureDb();
|
|
241
243
|
|
|
242
244
|
// Build a count query by replacing columns
|
|
@@ -247,7 +249,8 @@ export class QueryBuilder {
|
|
|
247
249
|
|
|
248
250
|
const allParams = [...this.params, ...this.havingParams];
|
|
249
251
|
|
|
250
|
-
const row =
|
|
252
|
+
const row = await adapterFetchOne<Record<string, unknown>>(
|
|
253
|
+
this.db!,
|
|
251
254
|
sql,
|
|
252
255
|
allParams.length > 0 ? allParams : undefined,
|
|
253
256
|
);
|
|
@@ -264,8 +267,8 @@ export class QueryBuilder {
|
|
|
264
267
|
*
|
|
265
268
|
* @returns True if at least one row matches.
|
|
266
269
|
*/
|
|
267
|
-
exists(): boolean {
|
|
268
|
-
return this.count() > 0;
|
|
270
|
+
async exists(): Promise<boolean> {
|
|
271
|
+
return (await this.count()) > 0;
|
|
269
272
|
}
|
|
270
273
|
|
|
271
274
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Zero external dependencies.
|
|
3
3
|
|
|
4
4
|
import { FakeData } from "./fakeData.js";
|
|
5
|
+
import { adapterExecute } from "./database.js";
|
|
5
6
|
import type { DatabaseAdapter, FieldDefinition } from "./types.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -54,7 +55,7 @@ export async function seedTable(
|
|
|
54
55
|
const placeholders = columns.map(() => "?").join(", ");
|
|
55
56
|
const values = columns.map((c) => row[c]);
|
|
56
57
|
|
|
57
|
-
db
|
|
58
|
+
await adapterExecute(db,
|
|
58
59
|
`INSERT INTO "${tableName}" (${colList}) VALUES (${placeholders})`,
|
|
59
60
|
values,
|
|
60
61
|
);
|