stabilize-orm 1.1.3 → 1.1.5

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/types.ts CHANGED
@@ -1,14 +1,52 @@
1
- // src/types.ts
1
+ /**
2
+ * @file types.ts
3
+ * @description Contains all shared type definitions and enums for the Stabilize ORM.
4
+ * @author ElectronSz
5
+ */
6
+
2
7
  export enum DBType {
3
- SQLite = "sqlite",
4
- MySQL = "mysql",
5
8
  Postgres = "postgres",
9
+ MySQL = "mysql",
10
+ SQLite = "sqlite",
11
+ }
12
+
13
+ export enum LogLevel {
14
+ Debug,
15
+ Info,
16
+ Warn,
17
+ Error,
18
+ }
19
+
20
+ export enum RelationType {
21
+ OneToOne,
22
+ OneToMany,
23
+ ManyToOne,
24
+ ManyToMany,
25
+ }
26
+
27
+ /**
28
+ * An enumeration of abstract data types that are mapped to database-specific types.
29
+ * This allows models to be defined in a database-agnostic way.
30
+ */
31
+ export enum DataTypes {
32
+ STRING, // Maps to VARCHAR or TEXT
33
+ TEXT, // Maps to TEXT
34
+ INTEGER, // Maps to INTEGER or INT
35
+ BIGINT, // Maps to BIGINT
36
+ FLOAT, // Maps to REAL or FLOAT
37
+ DOUBLE, // Maps to DOUBLE PRECISION
38
+ DECIMAL, // Maps to DECIMAL or NUMERIC
39
+ BOOLEAN, // Maps to BOOLEAN or TINYINT/INTEGER
40
+ DATE, // Maps to DATE or TEXT
41
+ DATETIME, // Maps to TIMESTAMP, DATETIME, or TEXT
42
+ JSON, // Maps to JSON, JSONB, or TEXT
43
+ UUID, // Maps to UUID or VARCHAR(36)
44
+ BLOB, // Maps to BYTEA or BLOB
6
45
  }
7
46
 
8
47
  export interface DBConfig {
9
48
  type: DBType;
10
49
  connectionString: string;
11
- poolSize?: number;
12
50
  retryAttempts?: number;
13
51
  retryDelay?: number;
14
52
  maxJitter?: number;
@@ -22,6 +60,9 @@ export interface CacheConfig {
22
60
  strategy?: "cache-aside" | "write-through";
23
61
  }
24
62
 
63
+ /**
64
+ * Configuration for the logger.
65
+ */
25
66
  export interface LoggerConfig {
26
67
  level?: LogLevel;
27
68
  filePath?: string;
@@ -29,38 +70,25 @@ export interface LoggerConfig {
29
70
  maxFiles?: number;
30
71
  }
31
72
 
32
- export enum LogLevel {
33
- ERROR = "error",
34
- WARN = "warn",
35
- INFO = "info",
36
- DEBUG = "debug",
37
- }
38
-
39
- export interface CacheStats {
40
- hits: number;
41
- misses: number;
42
- keys: number;
43
- }
44
-
45
- export enum RelationType {
46
- OneToOne = "one-to-one",
47
- OneToMany = "one-to-many",
48
- ManyToOne = "many-to-one",
49
- ManyToMany = "many-to-many",
73
+ export interface PoolMetrics {
74
+ activeConnections: number;
75
+ idleConnections: number;
76
+ totalConnections: number;
50
77
  }
51
78
 
52
79
  export interface QueryHint {
53
- type: "INDEX" | "FORCE_INDEX" | "USE_INDEX";
80
+ type: string;
54
81
  value: string;
55
82
  }
56
83
 
57
- export interface PoolMetrics {
58
- activeConnections: number;
59
- idleConnections: number;
60
- totalConnections: number;
84
+ export interface CacheStats {
85
+ hits: number;
86
+ misses: number;
87
+ keys: number;
61
88
  }
62
89
 
63
90
  export interface Migration {
91
+ name: string;
64
92
  up: string[];
65
93
  down: string[];
66
94
  }
@@ -68,9 +96,10 @@ export interface Migration {
68
96
  export class StabilizeError extends Error {
69
97
  constructor(
70
98
  message: string,
71
- public readonly code?: string,
99
+ public code: string,
100
+ public originalError?: Error,
72
101
  ) {
73
102
  super(message);
74
103
  this.name = "StabilizeError";
75
104
  }
76
- }
105
+ }
@@ -1,563 +0,0 @@
1
- #!/usr/bin/env bun
2
- import 'reflect-metadata'; // MUST BE FIRST — Enables decorator metadata reflection
3
-
4
- import { program } from "commander";
5
- import {
6
- generateMigration,
7
- Stabilize,
8
- runMigrations,
9
- ModelKey
10
- } from "../";
11
- import { LogLevel, type DBConfig, type LoggerConfig, DBType } from "../types";
12
- import * as fs from "fs/promises";
13
- import * as path from "path";
14
- import { glob } from "glob";
15
-
16
- // --- ANSI Color and Styling Helpers ---
17
- const C = {
18
- RESET: "\x1b[0m",
19
- BRIGHT: "\x1b[1m",
20
- DIM: "\x1b[2m",
21
- RED: "\x1b[31m",
22
- GREEN: "\x1b[32m",
23
- YELLOW: "\x1b[33m",
24
- BLUE: "\x1b[34m",
25
- MAGENTA: "\x1b[35m",
26
- CYAN: "\x1b[36m",
27
- WHITE: "\x1b[37m",
28
- BG_GREEN: "\x1b[42m\x1b[30m",
29
- BG_RED: "\x1b[41m\x1b[37m",
30
- BG_YELLOW: "\x1b[43m\x1b[30m",
31
- };
32
-
33
- // DB-aware migrations table
34
- function getMigrationsTableSQL(dbType: DBType) {
35
- switch (dbType) {
36
- case DBType.Postgres:
37
- return `CREATE TABLE IF NOT EXISTS migrations (
38
- id SERIAL PRIMARY KEY,
39
- name TEXT NOT NULL UNIQUE,
40
- applied_at TIMESTAMP NOT NULL
41
- )`;
42
- case DBType.MySQL:
43
- return `CREATE TABLE IF NOT EXISTS migrations (
44
- id INT AUTO_INCREMENT PRIMARY KEY,
45
- name VARCHAR(255) NOT NULL UNIQUE,
46
- applied_at DATETIME NOT NULL
47
- )`;
48
- case DBType.SQLite:
49
- default:
50
- return `CREATE TABLE IF NOT EXISTS migrations (
51
- id INTEGER PRIMARY KEY AUTOINCREMENT,
52
- name TEXT NOT NULL UNIQUE,
53
- applied_at TEXT NOT NULL
54
- )`;
55
- }
56
- }
57
- function getSeedHistoryTableSQL(dbType: DBType) {
58
- switch (dbType) {
59
- case DBType.Postgres:
60
- return `CREATE TABLE IF NOT EXISTS seed_history (
61
- id SERIAL PRIMARY KEY,
62
- name TEXT NOT NULL UNIQUE,
63
- applied_at TIMESTAMP NOT NULL
64
- )`;
65
- case DBType.MySQL:
66
- return `CREATE TABLE IF NOT EXISTS seed_history (
67
- id INT AUTO_INCREMENT PRIMARY KEY,
68
- name VARCHAR(255) NOT NULL UNIQUE,
69
- applied_at DATETIME NOT NULL
70
- )`;
71
- case DBType.SQLite:
72
- default:
73
- return `CREATE TABLE IF NOT EXISTS seed_history (
74
- id INTEGER PRIMARY KEY AUTOINCREMENT,
75
- name TEXT NOT NULL UNIQUE,
76
- applied_at TEXT NOT NULL
77
- )`;
78
- }
79
- }
80
-
81
- // Helper function to load configuration
82
- async function loadConfig(configPath: string): Promise<{ config: DBConfig; loggerConfig: LoggerConfig; orm: Stabilize }> {
83
- const absoluteConfigPath = path.resolve(process.cwd(), configPath);
84
- const configModule = await import(absoluteConfigPath);
85
- const config: DBConfig = configModule.default || configModule;
86
- const logLevel = program.opts().logLevel as LogLevel || "info";
87
- const loggerConfig: LoggerConfig = {
88
- level: logLevel,
89
- filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
90
- maxFileSize: 5 * 1024 * 1024,
91
- maxFiles: 3,
92
- };
93
- const orm = new Stabilize(
94
- config,
95
- { enabled: false, ttl: 60 },
96
- loggerConfig,
97
- );
98
- return { config, loggerConfig, orm };
99
- }
100
-
101
- // --------------------------------------------------------------------------------------------------
102
- // COMMAND: GENERATE
103
- // --------------------------------------------------------------------------------------------------
104
-
105
- program
106
- .command("generate <type> <name>")
107
- .description("Generate a model, migration, or seed file")
108
- .option("-l, --log-level <level>", "Log level (error, warn, info, debug)", "info")
109
- .action(async (type: string, name: string) => {
110
- try {
111
- if (type === "migration") {
112
- const modelPath = path.resolve(process.cwd(), "models", `${name}.ts`);
113
- let modelClass: any;
114
- try {
115
- const modelModule = await import(modelPath);
116
- modelClass = Object.values(modelModule).find(val => typeof val === "function" && val.prototype);
117
- if (!modelClass) throw new Error("No class exported in model file.");
118
- } catch (err: any) {
119
- console.error(`${C.BG_RED} ERROR ${C.RESET} Failed to import model: ${modelPath}. Ensure file exists and is valid. Details: ${err.message}`);
120
- return;
121
- }
122
- const tableName = Reflect.getMetadata(ModelKey, modelClass);
123
- console.log("Errors are calculated", tableName)
124
- if (!tableName) {
125
- console.error(`${C.BG_RED} ERROR ${C.RESET} Model class '${name}' in ${modelPath} is not decorated with @Model or metadata is missing.`);
126
- console.log(`${C.YELLOW} TIP ${C.RESET} - Ensure @Model('${name.toLowerCase()}s') is on the class.\n - Regenerate with 'generate model ${name}'.\n - Import 'reflect-metadata' in your model file or entry point if needed.`);
127
- return;
128
- }
129
- // Determine dbType
130
- const dbType = (await loadConfig("config/database.ts")).config.type ?? DBType.SQLite;
131
- let migration: any;
132
- try {
133
- migration = await generateMigration(modelClass, `create_${name.toLowerCase()}`);
134
- } catch (genErr: any) {
135
- console.error(`${C.BG_RED} ERROR ${C.RESET} Migration generation failed: ${genErr.message}`);
136
- if (genErr.message.includes('decorated') || genErr.message.includes('Model')) {
137
- console.log(`${C.YELLOW} TIP ${C.RESET} Ensure reflect-metadata is installed and imported globally.`);
138
- }
139
- return;
140
- }
141
- const migrationDir = path.resolve(process.cwd(), "migrations");
142
- await fs.mkdir(migrationDir, { recursive: true });
143
- const timestamp = new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 14);
144
- const migrationFileName = `${timestamp}_${name.toLowerCase()}`;
145
- const migrationFile = path.join(migrationDir, `${migrationFileName}.ts`);
146
- const migrationContent = `
147
- import { Migration } from 'stabilize-orm/src/types';
148
-
149
- const migration: Migration = {
150
- name: '${migrationFileName}',
151
- up: async (client: any) => {
152
- ${migration.up.map((q: string) => `await client.query(\`${q}\`);`).join("\n ")}
153
- },
154
- down: async (client: any) => {
155
- ${migration.down.map((q: string) => `await client.query(\`${q}\`);`).join("\n ")}
156
- }
157
- };
158
-
159
- export default migration;
160
- `.trim();
161
- await fs.writeFile(migrationFile, migrationContent + "\n");
162
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Migration generated: ${C.GREEN}${migrationFile}${C.RESET}`);
163
-
164
- } else if (type === "model") {
165
- const modelDir = path.resolve(process.cwd(), "models");
166
- await fs.mkdir(modelDir, { recursive: true });
167
- const modelFile = path.join(modelDir, `${name}.ts`);
168
- const modelContent = `
169
- import 'reflect-metadata'; // Required for decorators
170
- import { Model, Column, Required } from 'stabilize-orm';
171
-
172
- @Model('${name.toLowerCase()}s')
173
- export class ${name} {
174
- @Column('id', 'TEXT', { primaryKey: true })
175
- @Required()
176
- id: string = crypto.randomUUID();
177
-
178
- @Column('name', 'TEXT')
179
- @Required()
180
- name?: string;
181
-
182
- @Column('created_at', 'TEXT')
183
- createdAt?: string;
184
-
185
- @Column('updated_at', 'TEXT')
186
- updatedAt?: string;
187
- }
188
- `.trim() + "\n";
189
- await fs.writeFile(modelFile, modelContent);
190
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Model generated: ${C.GREEN}${modelFile}${C.RESET}`);
191
-
192
- } else if (type === "seed") {
193
- const seedDir = path.resolve(process.cwd(), "seeds");
194
- await fs.mkdir(seedDir, { recursive: true });
195
- const timestamp = new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 14);
196
- const seedFileName = `${timestamp}_${name.toLowerCase()}`;
197
- const seedFile = path.join(seedDir, `${seedFileName}.ts`);
198
- const seedContent = `
199
- import { Stabilize } from 'stabilize-orm';
200
- import { ${name} } from '../models/${name}';
201
- import { randomUUID } from 'crypto';
202
-
203
- export const dependencies: string[] = [];
204
-
205
- // Use UUIDs in seed data
206
- export async function seed(orm: Stabilize) {
207
- const repo = orm.getRepository(${name});
208
- await repo.bulkCreate([
209
- { id: randomUUID(), name: '${name} 1', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
210
- { id: randomUUID(), name: '${name} 2', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
211
- ], { batchSize: 100 });
212
-
213
- await orm.client.query(
214
- \`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)\`,
215
- ['${seedFileName}', new Date().toISOString()]
216
- );
217
- }
218
-
219
- export async function rollback(orm: Stabilize) {
220
- const repo = orm.getRepository(${name});
221
- const entities = await repo.find().execute(orm.client);
222
- const ids = entities.map((e: any) => e.id).filter(Boolean);
223
- if (ids.length > 0) {
224
- await repo.bulkDelete(ids, { batchSize: 100 });
225
- }
226
-
227
- await orm.client.query(
228
- \`DELETE FROM seed_history WHERE name = ?\`,
229
- ['${seedFileName}']
230
- );
231
- }
232
- `.trim() + "\n";
233
- await fs.writeFile(seedFile, seedContent);
234
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Seed generated: ${C.GREEN}${seedFile}${C.RESET}`);
235
-
236
- } else {
237
- console.error(`${C.BG_RED} ERROR ${C.RESET} Invalid type. Use ${C.YELLOW}"model", "migration", or "seed"${C.RESET}.`);
238
- }
239
- } catch (error: any) {
240
- console.error(`${C.BG_RED} FATAL ${C.RESET} Error generating file:`, error.message);
241
- }
242
- });
243
-
244
- // --------------------------------------------------------------------------------------------------
245
- // COMMAND: MIGRATE
246
- // --------------------------------------------------------------------------------------------------
247
-
248
- program
249
- .command("migrate")
250
- .description("Apply all pending migrations")
251
- .option("-c, --config <path>", "Path to database config file", "config/database.ts")
252
- .option("-l, --log-level <level>", "Log level (error, warn, info, debug)", "info")
253
- .action(async (options) => {
254
- let orm: Stabilize | null = null;
255
- try {
256
- const { orm: loadedOrm, config } = await loadConfig(options.config);
257
- orm = loadedOrm;
258
- const dbType = config.type ?? DBType.SQLite;
259
- await orm.client.query(getMigrationsTableSQL(dbType));
260
-
261
- const migrationDir = path.resolve(process.cwd(), "migrations");
262
- const migrationFiles = (await glob(`${migrationDir}/*.ts`)).sort();
263
- const migrations = [];
264
- for (const file of migrationFiles) {
265
- const migrationName = path.basename(file, ".ts");
266
- const applied = await orm.client.query(`SELECT name FROM migrations WHERE name = ?`, [migrationName]);
267
- if (applied.length > 0) continue;
268
- let migrationModule;
269
- try {
270
- migrationModule = await import(file);
271
- } catch (err) {
272
- console.error(`Failed to load migration ${file}:`, err);
273
- continue;
274
- }
275
- const migration = migrationModule.default || migrationModule;
276
- if (!migration || typeof migration.up !== "function") {
277
- console.warn(`Invalid migration format in ${file}. Skipping.`);
278
- continue;
279
- }
280
- migrations.push({ name: migrationName, up: migration.up });
281
- }
282
- if (migrations.length === 0) {
283
- console.log(`${C.YELLOW} WARNING ${C.RESET} No pending migrations found.`);
284
- await orm.close();
285
- return;
286
- }
287
- console.log(`${C.BLUE} INFO ${C.RESET} Applying ${C.BRIGHT}${migrations.length}${C.RESET} migration(s)...`);
288
- for (const mig of migrations) {
289
- console.log(` Applying: ${C.CYAN}${mig.name}${C.RESET}`);
290
- await orm.transaction(async () => {
291
- await mig.up(orm?.client);
292
- await orm?.client.query(
293
- `INSERT INTO migrations (name, applied_at) VALUES (?, ?)`,
294
- [mig.name, new Date().toISOString()]
295
- );
296
- });
297
- }
298
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} All migrations applied successfully.`);
299
- await orm.close();
300
-
301
- } catch (error: any) {
302
- console.error(`${C.BG_RED} FATAL ${C.RESET} Migration failed:`, error.message);
303
- if (orm) await orm.close();
304
- process.exit(1);
305
- }
306
- });
307
-
308
- // --------------------------------------------------------------------------------------------------
309
- // COMMAND: MIGRATE:ROLLBACK
310
- // --------------------------------------------------------------------------------------------------
311
-
312
- program
313
- .command("migrate:rollback")
314
- .description("Rollback the most recently applied migration")
315
- .option("-c, --config <path>", "Path to database config file", "config/database.ts")
316
- .option("-l, --log-level <level>", "Log level (error, warn, info, debug)", "info")
317
- .action(async (options) => {
318
- let orm: Stabilize | null = null;
319
- try {
320
- const { orm: loadedOrm, config } = await loadConfig(options.config);
321
- orm = loadedOrm;
322
- const dbType = config.type ?? DBType.SQLite;
323
- await orm.client.query(getMigrationsTableSQL(dbType));
324
-
325
- const latest = await orm.client.query(
326
- `SELECT name FROM migrations ORDER BY applied_at DESC LIMIT 1`
327
- );
328
- if (latest.length === 0) {
329
- console.log(`${C.YELLOW} WARNING ${C.RESET} No migrations to rollback.`);
330
- await orm.close();
331
- return;
332
- }
333
- const migrationName = latest[0].name;
334
- const migrationFile = path.resolve(process.cwd(), "migrations", `${migrationName}.ts`);
335
- let migrationModule;
336
- try {
337
- migrationModule = await import(migrationFile);
338
- } catch (err) {
339
- console.error(`${C.BG_RED} ERROR ${C.RESET} Migration file not found: ${migrationFile}`);
340
- await orm.close();
341
- return;
342
- }
343
- const migration = migrationModule.default || migrationModule;
344
- if (typeof migration.down !== "function") {
345
- console.error(`${C.BG_RED} ERROR ${C.RESET} Migration ${migrationName} missing down function.`);
346
- await orm.close();
347
- return;
348
- }
349
- console.log(`${C.BLUE} INFO ${C.RESET} Rolling back: ${C.YELLOW}${migrationName}${C.RESET}`);
350
- await orm.transaction(async () => {
351
- await migration.down(orm?.client);
352
- await orm?.client.query(`DELETE FROM migrations WHERE name = ?`, [migrationName]);
353
- });
354
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Rolled back: ${C.GREEN}${migrationName}${C.RESET}`);
355
- await orm.close();
356
-
357
- } catch (error: any) {
358
- console.error(`${C.BG_RED} FATAL ${C.RESET} Rollback failed:`, error.message);
359
- if (orm) await orm.close();
360
- process.exit(1);
361
- }
362
- });
363
-
364
- // --------------------------------------------------------------------------------------------------
365
- // COMMAND: SEED
366
- // --------------------------------------------------------------------------------------------------
367
-
368
- program
369
- .command("seed")
370
- .description("Run seed files to populate the database")
371
- .option("-c, --config <path>", "Path to database config file", "config/database.ts")
372
- .option("-l, --log-level <level>", "Log level (error, warn, info, debug)", "info")
373
- .action(async (options) => {
374
- let orm: Stabilize | null = null;
375
- try {
376
- const { orm: loadedOrm, config } = await loadConfig(options.config);
377
- orm = loadedOrm;
378
- const dbType = config.type ?? DBType.SQLite;
379
- await orm.client.query(getSeedHistoryTableSQL(dbType));
380
-
381
- const seedDir = path.resolve(process.cwd(), "seeds");
382
- const seedFiles = await glob(`${seedDir}/*.ts`);
383
- if (seedFiles.length === 0) {
384
- console.log(`${C.YELLOW} WARNING ${C.RESET} No seed files found.`);
385
- await orm.close();
386
- return;
387
- }
388
- const seedGraph = new Map<string, { file: string; dependencies: string[] }>();
389
- for (const file of seedFiles) {
390
- const seedName = path.basename(file, ".ts");
391
- let mod;
392
- try {
393
- mod = await import(file);
394
- } catch (err) {
395
- console.warn(`Failed to load seed ${file}:`, err);
396
- continue;
397
- }
398
- seedGraph.set(seedName, {
399
- file,
400
- dependencies: mod.dependencies || [],
401
- });
402
- }
403
- const orderedSeeds = topologicalSort(seedGraph);
404
-
405
- let appliedCount = 0;
406
- for (const seedName of orderedSeeds) {
407
- const { file } = seedGraph.get(seedName)!;
408
- const alreadyApplied = await orm.client.query(`SELECT 1 FROM seed_history WHERE name = ?`, [seedName]);
409
- if (alreadyApplied.length > 0) {
410
- console.log(` ${C.DIM}Skipped:${C.RESET} ${seedName}`);
411
- continue;
412
- }
413
- console.log(` ${C.BRIGHT}Running:${C.RESET} ${C.MAGENTA}${seedName}${C.RESET}`);
414
- const mod = await import(file);
415
- const seedFn = mod.seed || mod.default;
416
- if (typeof seedFn === "function") {
417
- await seedFn(orm);
418
- await orm.client.query(
419
- `INSERT INTO seed_history (name, applied_at) VALUES (?, ?)`,
420
- [seedName, new Date().toISOString()]
421
- );
422
- appliedCount++;
423
- } else {
424
- console.error(`${C.BG_RED} ERROR ${C.RESET} Invalid seed export in ${file}`);
425
- }
426
- }
427
- console.log(`\n${C.BG_GREEN} SUCCESS ${C.RESET} Seeding complete. Applied: ${appliedCount}`);
428
- await orm.close();
429
-
430
- } catch (error: any) {
431
- console.error(`${C.BG_RED} FATAL ${C.RESET} Seeding failed:`, error.message);
432
- if (orm) await orm.close();
433
- process.exit(1);
434
- }
435
- });
436
-
437
- // --------------------------------------------------------------------------------------------------
438
- // COMMAND: SEED:ROLLBACK
439
- // --------------------------------------------------------------------------------------------------
440
-
441
- program
442
- .command("seed:rollback")
443
- .description("Rollback the most recently applied seed")
444
- .option("-c, --config <path>", "Path to database config file", "config/database.ts")
445
- .option("-l, --log-level <level>", "Log level (error, warn, info, debug)", "info")
446
- .action(async (options) => {
447
- let orm: Stabilize | null = null;
448
- try {
449
- const { orm: loadedOrm, config } = await loadConfig(options.config);
450
- orm = loadedOrm;
451
- const dbType = config.type ?? DBType.SQLite;
452
- await orm.client.query(getSeedHistoryTableSQL(dbType));
453
-
454
- const latest = await orm.client.query(`SELECT name FROM seed_history ORDER BY applied_at DESC LIMIT 1`);
455
- if (latest.length === 0) {
456
- console.log(`${C.YELLOW} WARNING ${C.RESET} No seeds to rollback.`);
457
- await orm.close();
458
- return;
459
- }
460
- const seedName = latest[0].name;
461
- const seedFile = path.resolve(process.cwd(), "seeds", `${seedName}.ts`);
462
- let mod;
463
- try {
464
- mod = await import(seedFile);
465
- } catch (err) {
466
- console.error(`${C.BG_RED} ERROR ${C.RESET} Seed file not found: ${seedFile}`);
467
- await orm.close();
468
- return;
469
- }
470
- const rollbackFn = mod.rollback;
471
- if (typeof rollbackFn !== "function") {
472
- console.error(`${C.BG_RED} ERROR ${C.RESET} Missing rollback function in ${seedName}`);
473
- await orm.close();
474
- return;
475
- }
476
- console.log(`${C.BLUE} INFO ${C.RESET} Rolling back seed: ${C.YELLOW}${seedName}${C.RESET}`);
477
- await rollbackFn(orm);
478
- await orm.client.query(`DELETE FROM seed_history WHERE name = ?`, [seedName]);
479
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Seed rolled back: ${C.GREEN}${seedName}${C.RESET}`);
480
- await orm.close();
481
-
482
- } catch (error: any) {
483
- console.error(`${C.BG_RED} FATAL ${C.RESET} Rollback failed:`, error.message);
484
- if (orm) await orm.close();
485
- process.exit(1);
486
- }
487
- });
488
-
489
- // --------------------------------------------------------------------------------------------------
490
- // COMMAND: STATUS
491
- // --------------------------------------------------------------------------------------------------
492
-
493
- program
494
- .command("status")
495
- .description("Display status of migrations and seeds")
496
- .option("-c, --config <path>", "Path to database config file", "config/database.ts")
497
- .option("-l, --log-level <level>", "Log level (error, warn, info, debug)", "info")
498
- .action(async (options) => {
499
- let orm: Stabilize | null = null;
500
- try {
501
- const { orm: loadedOrm, config } = await loadConfig(options.config);
502
- orm = loadedOrm;
503
- const dbType = config.type ?? DBType.SQLite;
504
- await orm.client.query(getMigrationsTableSQL(dbType));
505
- await orm.client.query(getSeedHistoryTableSQL(dbType));
506
-
507
- const migrationDir = path.resolve(process.cwd(), "migrations");
508
- const migrationFiles = (await glob(`${migrationDir}/*.ts`)).map(f => path.basename(f, ".ts")).sort();
509
- console.log(`\n${C.BRIGHT}Migration Status:${C.RESET}`);
510
- console.log(`---------------------------------`);
511
- for (const name of migrationFiles) {
512
- const res = await orm.client.query(`SELECT 1 FROM migrations WHERE name = ?`, [name]);
513
- const status = res.length > 0 ? `${C.BG_GREEN} APPLIED ${C.RESET}` : `${C.BG_YELLOW} PENDING ${C.RESET}`;
514
- console.log(`${status} ${C.WHITE}${name}${C.RESET}`);
515
- }
516
- const seedDir = path.resolve(process.cwd(), "seeds");
517
- const seedFiles = (await glob(`${seedDir}/*.ts`)).map(f => path.basename(f, ".ts")).sort();
518
- console.log(`\n${C.BRIGHT}Seed Status:${C.RESET}`);
519
- console.log(`---------------------------------`);
520
- for (const name of seedFiles) {
521
- const res = await orm.client.query(`SELECT 1 FROM seed_history WHERE name = ?`, [name]);
522
- const status = res.length > 0 ? `${C.BG_GREEN} APPLIED ${C.RESET}` : `${C.BG_YELLOW} PENDING ${C.RESET}`;
523
- console.log(`${status} ${C.WHITE}${name}${C.RESET}`);
524
- }
525
- await orm.close();
526
- } catch (error: any) {
527
- console.error(`${C.BG_RED} FATAL ${C.RESET} Status check failed:`, error.message);
528
- if (orm) await orm.close();
529
- process.exit(1);
530
- }
531
- });
532
-
533
- // --------------------------------------------------------------------------------------------------
534
- // TOPOLOGICAL SORT HELPER
535
- // --------------------------------------------------------------------------------------------------
536
-
537
- function topologicalSort(
538
- graph: Map<string, { file: string; dependencies: string[] }>,
539
- ): string[] {
540
- const result: string[] = [];
541
- const visited = new Set<string>();
542
- const temp = new Set<string>();
543
- function visit(node: string) {
544
- if (temp.has(node)) throw new Error(`Circular dependency detected: ${node}`);
545
- if (!visited.has(node)) {
546
- temp.add(node);
547
- const deps = graph.get(node)?.dependencies || [];
548
- for (const dep of deps) {
549
- if (!graph.has(dep)) throw new Error(`Dependency ${dep} not found for ${node}`);
550
- visit(dep);
551
- }
552
- temp.delete(node);
553
- visited.add(node);
554
- result.push(node);
555
- }
556
- }
557
- for (const node of graph.keys()) {
558
- if (!visited.has(node)) visit(node);
559
- }
560
- return result;
561
- }
562
-
563
- program.parse(process.argv);