stabilize-orm 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env bun
2
+ import 'reflect-metadata'; // MUST BE FIRST — Enables decorator metadata reflection
3
+
2
4
  import { program } from "commander";
3
5
  import {
4
6
  generateMigration,
5
7
  Stabilize,
6
- runMigrations
8
+ runMigrations,
9
+ ModelKey
7
10
  } from "../";
8
-
9
- import { LogLevel, type DBConfig, type LoggerConfig } from "../types";
11
+ import { LogLevel, type DBConfig, type LoggerConfig, DBType } from "../types";
10
12
  import * as fs from "fs/promises";
11
13
  import * as path from "path";
12
14
  import { glob } from "glob";
@@ -23,35 +25,76 @@ const C = {
23
25
  MAGENTA: "\x1b[35m",
24
26
  CYAN: "\x1b[36m",
25
27
  WHITE: "\x1b[37m",
26
- BG_GREEN: "\x1b[42m\x1b[30m", // Black text on Green background
27
- BG_RED: "\x1b[41m\x1b[37m", // White text on Red background
28
- BG_YELLOW: "\x1b[43m\x1b[30m",// Black text on Yellow background
28
+ BG_GREEN: "\x1b[42m\x1b[30m",
29
+ BG_RED: "\x1b[41m\x1b[37m",
30
+ BG_YELLOW: "\x1b[43m\x1b[30m",
29
31
  };
30
32
 
31
-
32
- program.version("1.0.5").description("Stabilize ORM CLI");
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
+ }
33
80
 
34
81
  // Helper function to load configuration
35
- async function loadConfig(configPath: string): Promise<{ config: DBConfig, loggerConfig: LoggerConfig, orm: Stabilize }> {
82
+ async function loadConfig(configPath: string): Promise<{ config: DBConfig; loggerConfig: LoggerConfig; orm: Stabilize }> {
36
83
  const absoluteConfigPath = path.resolve(process.cwd(), configPath);
37
84
  const configModule = await import(absoluteConfigPath);
38
85
  const config: DBConfig = configModule.default || configModule;
39
-
40
- const logLevel = program.opts().logLevel as LogLevel;
41
-
86
+ const logLevel = program.opts().logLevel as LogLevel || "info";
42
87
  const loggerConfig: LoggerConfig = {
43
88
  level: logLevel,
44
89
  filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
45
90
  maxFileSize: 5 * 1024 * 1024,
46
91
  maxFiles: 3,
47
92
  };
48
-
49
93
  const orm = new Stabilize(
50
94
  config,
51
95
  { enabled: false, ttl: 60 },
52
96
  loggerConfig,
53
97
  );
54
-
55
98
  return { config, loggerConfig, orm };
56
99
  }
57
100
 
@@ -62,34 +105,60 @@ async function loadConfig(configPath: string): Promise<{ config: DBConfig, logge
62
105
  program
63
106
  .command("generate <type> <name>")
64
107
  .description("Generate a model, migration, or seed file")
65
- .option(
66
- "-l, --log-level <level>",
67
- "Log level (error, warn, info, debug)",
68
- "info",
69
- )
108
+ .option("-l, --log-level <level>", "Log level (error, warn, info, debug)", "info")
70
109
  .action(async (type: string, name: string) => {
71
110
  try {
72
111
  if (type === "migration") {
73
112
  const modelPath = path.resolve(process.cwd(), "models", `${name}.ts`);
74
- const modelModule = await import(modelPath);
75
- const model = Object.values(modelModule)[0] as new (
76
- ...args: any[]
77
- ) => any;
78
- const migration = await generateMigration(
79
- model,
80
- `create_${name.toLowerCase()}`,
81
- );
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
+ }
82
141
  const migrationDir = path.resolve(process.cwd(), "migrations");
83
142
  await fs.mkdir(migrationDir, { recursive: true });
84
- const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
85
- const migrationFile = path.join(
86
- migrationDir,
87
- `${timestamp}_${name.toLowerCase()}.ts`,
88
- );
89
- await fs.writeFile(
90
- migrationFile,
91
- `import { Migration } from 'stabilize-orm/src/types';\n\nconst migration: Migration = ${JSON.stringify(migration, null, 2)};\n\nexport default migration;`,
92
- );
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");
93
162
  console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Migration generated: ${C.GREEN}${migrationFile}${C.RESET}`);
94
163
 
95
164
  } else if (type === "model") {
@@ -97,71 +166,81 @@ program
97
166
  await fs.mkdir(modelDir, { recursive: true });
98
167
  const modelFile = path.join(modelDir, `${name}.ts`);
99
168
  const modelContent = `
169
+ import 'reflect-metadata'; // Required for decorators
100
170
  import { Model, Column, Required } from 'stabilize-orm';
101
171
 
102
172
  @Model('${name.toLowerCase()}s')
103
173
  export class ${name} {
104
- @Column('id', 'INTEGER')
105
- id?: number;
174
+ @Column('id', 'TEXT', { primaryKey: true })
175
+ @Required()
176
+ id: string = crypto.randomUUID();
106
177
 
107
178
  @Column('name', 'TEXT')
108
179
  @Required()
109
- name: string;
180
+ name?: string;
181
+
182
+ @Column('created_at', 'TEXT')
183
+ createdAt?: string;
184
+
185
+ @Column('updated_at', 'TEXT')
186
+ updatedAt?: string;
110
187
  }
111
- `;
188
+ `.trim() + "\n";
112
189
  await fs.writeFile(modelFile, modelContent);
113
190
  console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Model generated: ${C.GREEN}${modelFile}${C.RESET}`);
114
191
 
115
192
  } else if (type === "seed") {
116
193
  const seedDir = path.resolve(process.cwd(), "seeds");
117
194
  await fs.mkdir(seedDir, { recursive: true });
118
- const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
119
- const seedFile = path.join(
120
- seedDir,
121
- `${timestamp}_${name.toLowerCase()}.ts`,
122
- );
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`);
123
198
  const seedContent = `
124
199
  import { Stabilize } from 'stabilize-orm';
125
- import { ${name} } from '../models/${name}'; // Assuming model path
200
+ import { ${name} } from '../models/${name}';
201
+ import { randomUUID } from 'crypto';
126
202
 
127
- export const dependencies = [];
203
+ export const dependencies: string[] = [];
128
204
 
205
+ // Use UUIDs in seed data
129
206
  export async function seed(orm: Stabilize) {
130
207
  const repo = orm.getRepository(${name});
131
208
  await repo.bulkCreate([
132
- { name: '${name} 1' },
133
- { name: '${name} 2' },
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() },
134
211
  ], { batchSize: 100 });
135
212
 
136
- await orm['client'].query(
213
+ await orm.client.query(
137
214
  \`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)\`,
138
- ['${timestamp}_${name.toLowerCase()}', new Date().toISOString()]
215
+ ['${seedFileName}', new Date().toISOString()]
139
216
  );
140
217
  }
141
218
 
142
219
  export async function rollback(orm: Stabilize) {
143
220
  const repo = orm.getRepository(${name});
144
- // NOTE: This rollback logic assumes your entities have an 'id' field for deletion.
145
- const entities = await repo.find().execute(orm['client']);
146
- await repo.bulkDelete(entities.map(e => e.id!), { batchSize: 100 });
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
+ }
147
226
 
148
- await orm['client'].query(
227
+ await orm.client.query(
149
228
  \`DELETE FROM seed_history WHERE name = ?\`,
150
- ['${timestamp}_${name.toLowerCase()}']
229
+ ['${seedFileName}']
151
230
  );
152
231
  }
153
- `;
232
+ `.trim() + "\n";
154
233
  await fs.writeFile(seedFile, seedContent);
155
234
  console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Seed generated: ${C.GREEN}${seedFile}${C.RESET}`);
235
+
156
236
  } else {
157
237
  console.error(`${C.BG_RED} ERROR ${C.RESET} Invalid type. Use ${C.YELLOW}"model", "migration", or "seed"${C.RESET}.`);
158
238
  }
159
- } catch (error) {
160
- console.error(`${C.BG_RED} FATAL ${C.RESET} Error generating file:`, error);
239
+ } catch (error: any) {
240
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Error generating file:`, error.message);
161
241
  }
162
242
  });
163
243
 
164
-
165
244
  // --------------------------------------------------------------------------------------------------
166
245
  // COMMAND: MIGRATE
167
246
  // --------------------------------------------------------------------------------------------------
@@ -169,139 +248,119 @@ export async function rollback(orm: Stabilize) {
169
248
  program
170
249
  .command("migrate")
171
250
  .description("Apply all pending migrations")
172
- .option(
173
- "-c, --config <path>",
174
- "Path to database config file",
175
- "config/database.ts",
176
- )
177
- .option(
178
- "-l, --log-level <level>",
179
- "Log level (error, warn, info, debug)",
180
- "info",
181
- )
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")
182
253
  .action(async (options) => {
183
254
  let orm: Stabilize | null = null;
184
255
  try {
185
- const { config } = await loadConfig(options.config);
186
-
187
- // Re-initialize ORM with correct logging configuration
188
- orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
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));
189
260
 
190
261
  const migrationDir = path.resolve(process.cwd(), "migrations");
191
- const migrationFiles = await glob(`${migrationDir}/*.ts`);
192
-
262
+ const migrationFiles = (await glob(`${migrationDir}/*.ts`)).sort();
193
263
  const migrations = [];
194
- for (const file of migrationFiles.sort()) {
195
- const migrationModule = await import(file);
196
- // Assuming migration files export a default object with { up: string[], down: string[] }
197
- migrations.push(migrationModule.default || migrationModule);
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 });
198
281
  }
199
-
200
282
  if (migrations.length === 0) {
201
- console.log(`${C.YELLOW} WARNING ${C.RESET} No migration files found in migrations/ directory.`);
283
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No pending migrations found.`);
202
284
  await orm.close();
203
285
  return;
204
286
  }
205
-
206
- console.log(`${C.BLUE} INFO ${C.RESET} Running ${C.BRIGHT}${migrations.length}${C.RESET} migration files...`);
207
-
208
- // runMigrations handles applying only the unapplied ones.
209
- await runMigrations(config, migrations);
210
-
211
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Migrations completed successfully.`);
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.`);
212
299
  await orm.close();
213
300
 
214
- } catch (error) {
215
- console.error(`${C.BG_RED} FATAL ${C.RESET} Migration failed:`, error);
301
+ } catch (error: any) {
302
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Migration failed:`, error.message);
216
303
  if (orm) await orm.close();
217
304
  process.exit(1);
218
305
  }
219
306
  });
220
307
 
221
-
222
308
  // --------------------------------------------------------------------------------------------------
223
- // COMMAND: MIGRATE:ROLLBACK (One step back)
309
+ // COMMAND: MIGRATE:ROLLBACK
224
310
  // --------------------------------------------------------------------------------------------------
225
311
 
226
312
  program
227
313
  .command("migrate:rollback")
228
314
  .description("Rollback the most recently applied migration")
229
- .option(
230
- "-c, --config <path>",
231
- "Path to database config file",
232
- "config/database.ts",
233
- )
234
- .option(
235
- "-l, --log-level <level>",
236
- "Log level (error, warn, info, debug)",
237
- "info",
238
- )
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")
239
317
  .action(async (options) => {
240
318
  let orm: Stabilize | null = null;
241
319
  try {
242
- const { config } = await loadConfig(options.config);
243
- orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
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));
244
324
 
245
- // 1. Get the last applied migration record from the DB
246
- const latestApplied = await orm["client"].query(
247
- `SELECT name FROM migrations ORDER BY applied_at DESC LIMIT 1`,
325
+ const latest = await orm.client.query(
326
+ `SELECT name FROM migrations ORDER BY applied_at DESC LIMIT 1`
248
327
  );
249
-
250
- if (latestApplied.length === 0) {
328
+ if (latest.length === 0) {
251
329
  console.log(`${C.YELLOW} WARNING ${C.RESET} No migrations to rollback.`);
252
330
  await orm.close();
253
331
  return;
254
332
  }
255
-
256
- const migrationName = latestApplied[0]!.name;
257
- const migrationDir = path.resolve(process.cwd(), "migrations");
258
- const migrationFiles = await glob(`${migrationDir}/*.ts`);
259
-
260
- // 2. Find the corresponding file
261
- const migrationFile = migrationFiles.find(f => path.basename(f, '.ts') === migrationName);
262
-
263
- if (!migrationFile) {
264
- console.error(`${C.BG_RED} ERROR ${C.RESET} Migration file for ${migrationName} not found.`);
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}`);
265
340
  await orm.close();
266
341
  return;
267
342
  }
268
-
269
- // 3. Load the rollback (down) query
270
- const migrationModule = await import(migrationFile);
271
343
  const migration = migrationModule.default || migrationModule;
272
- const downQueries: string[] = migration.down;
273
-
274
- if (!downQueries || downQueries.length === 0) {
275
- console.error(`${C.BG_RED} ERROR ${C.RESET} Migration ${migrationName} does not contain a 'down' array for rollback.`);
276
- await orm.close();
277
- return;
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;
278
348
  }
279
-
280
- console.log(`${C.BLUE} INFO ${C.RESET} Rolling back migration: ${C.YELLOW}${migrationName}${C.RESET}`);
281
-
282
- // 4. Run the down queries inside a transaction (best practice)
349
+ console.log(`${C.BLUE} INFO ${C.RESET} Rolling back: ${C.YELLOW}${migrationName}${C.RESET}`);
283
350
  await orm.transaction(async () => {
284
- for (const query of downQueries) {
285
- await orm!['client'].query(query, []);
286
- }
287
- // 5. Remove the migration record from the history table
288
- await orm!["client"].query(
289
- `DELETE FROM migrations WHERE name = ?`,
290
- [migrationName],
291
- );
351
+ await migration.down(orm?.client);
352
+ await orm?.client.query(`DELETE FROM migrations WHERE name = ?`, [migrationName]);
292
353
  });
293
-
294
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Rolled back migration: ${C.GREEN}${migrationName}${C.RESET}`);
354
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Rolled back: ${C.GREEN}${migrationName}${C.RESET}`);
295
355
  await orm.close();
296
356
 
297
- } catch (error) {
298
- console.error(`${C.BG_RED} FATAL ${C.RESET} Migration rollback failed:`, error);
357
+ } catch (error: any) {
358
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Rollback failed:`, error.message);
299
359
  if (orm) await orm.close();
300
360
  process.exit(1);
301
361
  }
302
362
  });
303
363
 
304
-
305
364
  // --------------------------------------------------------------------------------------------------
306
365
  // COMMAND: SEED
307
366
  // --------------------------------------------------------------------------------------------------
@@ -309,166 +368,124 @@ program
309
368
  program
310
369
  .command("seed")
311
370
  .description("Run seed files to populate the database")
312
- .option(
313
- "-c, --config <path>",
314
- "Path to database config file",
315
- "config/database.ts",
316
- )
317
- .option(
318
- "-l, --log-level <level>",
319
- "Log level (error, warn, info, debug)",
320
- "info",
321
- )
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")
322
373
  .action(async (options) => {
323
374
  let orm: Stabilize | null = null;
324
375
  try {
325
- const { config } = await loadConfig(options.config);
326
- orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
327
-
328
- await orm["client"].query(`
329
- CREATE TABLE IF NOT EXISTS seed_history (
330
- id INTEGER PRIMARY KEY AUTOINCREMENT,
331
- name TEXT NOT NULL,
332
- applied_at TEXT NOT NULL
333
- )
334
- `);
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));
335
380
 
336
381
  const seedDir = path.resolve(process.cwd(), "seeds");
337
382
  const seedFiles = await glob(`${seedDir}/*.ts`);
338
383
  if (seedFiles.length === 0) {
339
- console.log(`${C.YELLOW} WARNING ${C.RESET} No seed files found in seeds/ directory.`);
384
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No seed files found.`);
340
385
  await orm.close();
341
386
  return;
342
387
  }
343
-
344
- const seedGraph = new Map<
345
- string,
346
- { file: string; dependencies: string[] }
347
- >();
388
+ const seedGraph = new Map<string, { file: string; dependencies: string[] }>();
348
389
  for (const file of seedFiles) {
349
390
  const seedName = path.basename(file, ".ts");
350
- const seedModule = await import(file);
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
+ }
351
398
  seedGraph.set(seedName, {
352
399
  file,
353
- dependencies: seedModule.dependencies || [],
400
+ dependencies: mod.dependencies || [],
354
401
  });
355
402
  }
356
-
357
403
  const orderedSeeds = topologicalSort(seedGraph);
358
404
 
359
- console.log(`${C.BLUE} INFO ${C.RESET} Running ${C.BRIGHT}${orderedSeeds.length}${C.RESET} seed files (sorted by dependency)...`);
360
- let seedsApplied = 0;
405
+ let appliedCount = 0;
361
406
  for (const seedName of orderedSeeds) {
362
407
  const { file } = seedGraph.get(seedName)!;
363
- const applied = await orm["client"].query(
364
- `SELECT id FROM seed_history WHERE name = ?`,
365
- [seedName],
366
- );
367
- if (applied.length > 0) {
368
- console.log(` ${C.DIM}Skipping already applied seed:${C.RESET} ${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}`);
369
411
  continue;
370
412
  }
371
-
372
- console.log(` ${C.BRIGHT}Executing seed:${C.RESET} ${C.MAGENTA}${seedName}${C.RESET}`);
373
- const seedModule = await import(file);
374
- const seedFn = seedModule.default || seedModule.seed;
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;
375
416
  if (typeof seedFn === "function") {
376
417
  await seedFn(orm);
377
- seedsApplied++;
378
- } else {
379
- console.error(
380
- `${C.BG_RED} ERROR ${C.RESET} Seed file ${file} must export a default function or a function named 'seed'.`,
418
+ await orm.client.query(
419
+ `INSERT INTO seed_history (name, applied_at) VALUES (?, ?)`,
420
+ [seedName, new Date().toISOString()]
381
421
  );
422
+ appliedCount++;
423
+ } else {
424
+ console.error(`${C.BG_RED} ERROR ${C.RESET} Invalid seed export in ${file}`);
382
425
  }
383
426
  }
384
-
385
- console.log(`\n${C.BG_GREEN} SUCCESS ${C.RESET} Seeding completed. ${C.GREEN}${seedsApplied} new seeds applied.${C.RESET}`);
427
+ console.log(`\n${C.BG_GREEN} SUCCESS ${C.RESET} Seeding complete. Applied: ${appliedCount}`);
386
428
  await orm.close();
387
- } catch (error) {
388
- console.error(`${C.BG_RED} FATAL ${C.RESET} Seeding failed:`, error);
429
+
430
+ } catch (error: any) {
431
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Seeding failed:`, error.message);
389
432
  if (orm) await orm.close();
390
433
  process.exit(1);
391
434
  }
392
435
  });
393
436
 
394
437
  // --------------------------------------------------------------------------------------------------
395
- // COMMAND: SEED:ROLLBACK (One step back)
438
+ // COMMAND: SEED:ROLLBACK
396
439
  // --------------------------------------------------------------------------------------------------
397
440
 
398
441
  program
399
442
  .command("seed:rollback")
400
443
  .description("Rollback the most recently applied seed")
401
- .option(
402
- "-c, --config <path>",
403
- "Path to database config file",
404
- "config/database.ts",
405
- )
406
- .option(
407
- "-l, --log-level <level>",
408
- "Log level (error, warn, info, debug)",
409
- "info",
410
- )
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")
411
446
  .action(async (options) => {
412
447
  let orm: Stabilize | null = null;
413
448
  try {
414
- const { config } = await loadConfig(options.config);
415
- orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
416
-
417
- const seedDir = path.resolve(process.cwd(), "seeds");
418
- const seedFiles = await glob(`${seedDir}/*.ts`);
419
- const seedGraph = new Map<
420
- string,
421
- { file: string; dependencies: string[] }
422
- >();
423
- for (const file of seedFiles) {
424
- const seedName = path.basename(file, ".ts");
425
- const seedModule = await import(file);
426
- seedGraph.set(seedName, {
427
- file,
428
- dependencies: seedModule.dependencies || [],
429
- });
430
- }
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));
431
453
 
432
- const latestSeed = await orm["client"].query(
433
- `SELECT name FROM seed_history ORDER BY applied_at DESC LIMIT 1`,
434
- );
435
-
436
- if (latestSeed.length === 0) {
454
+ const latest = await orm.client.query(`SELECT name FROM seed_history ORDER BY applied_at DESC LIMIT 1`);
455
+ if (latest.length === 0) {
437
456
  console.log(`${C.YELLOW} WARNING ${C.RESET} No seeds to rollback.`);
438
457
  await orm.close();
439
458
  return;
440
459
  }
441
-
442
- const seedName = latestSeed[0]!.name;
443
- const seedFile = seedGraph.get(seedName)?.file;
444
- if (!seedFile) {
445
- console.error(`${C.BG_RED} ERROR ${C.RESET} Seed file for ${seedName} not found.`);
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}`);
446
467
  await orm.close();
447
468
  return;
448
469
  }
449
-
450
- console.log(`${C.BLUE} INFO ${C.RESET} Rolling back seed: ${C.YELLOW}${seedName}${C.RESET}`);
451
- const seedModule = await import(seedFile);
452
- const rollbackFn = seedModule.rollback;
453
-
454
- if (typeof rollbackFn === "function") {
455
- await rollbackFn(orm);
456
- console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Rolled back seed: ${C.GREEN}${seedName}${C.RESET}`);
457
- } else {
458
- console.error(
459
- `${C.BG_RED} ERROR ${C.RESET} Seed file ${seedFile} must export a 'rollback' function.`,
460
- );
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;
461
475
  }
462
-
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}`);
463
480
  await orm.close();
464
- } catch (error) {
465
- console.error(`${C.BG_RED} FATAL ${C.RESET} Seed rollback failed:`, error);
481
+
482
+ } catch (error: any) {
483
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Rollback failed:`, error.message);
466
484
  if (orm) await orm.close();
467
485
  process.exit(1);
468
486
  }
469
487
  });
470
488
 
471
-
472
489
  // --------------------------------------------------------------------------------------------------
473
490
  // COMMAND: STATUS
474
491
  // --------------------------------------------------------------------------------------------------
@@ -476,84 +493,43 @@ program
476
493
  program
477
494
  .command("status")
478
495
  .description("Display status of migrations and seeds")
479
- .option(
480
- "-c, --config <path>",
481
- "Path to database config file",
482
- "config/database.ts",
483
- )
484
- .option(
485
- "-l, --log-level <level>",
486
- "Log level (error, warn, info, debug)",
487
- "info",
488
- )
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")
489
498
  .action(async (options) => {
490
499
  let orm: Stabilize | null = null;
491
500
  try {
492
- const { config } = await loadConfig(options.config);
493
- orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
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));
494
506
 
495
507
  const migrationDir = path.resolve(process.cwd(), "migrations");
496
- const migrationFiles = await glob(`${migrationDir}/*.ts`);
497
-
498
- // Ensure migrations table exists for query purposes
499
- await orm["client"].query(`
500
- CREATE TABLE IF NOT EXISTS migrations (
501
- id INTEGER PRIMARY KEY AUTOINCREMENT,
502
- name TEXT NOT NULL,
503
- applied_at TEXT NOT NULL
504
- )
505
- `);
506
-
508
+ const migrationFiles = (await glob(`${migrationDir}/*.ts`)).map(f => path.basename(f, ".ts")).sort();
507
509
  console.log(`\n${C.BRIGHT}Migration Status:${C.RESET}`);
508
510
  console.log(`---------------------------------`);
509
- for (const file of migrationFiles.sort()) {
510
- const migrationName = path.basename(file, ".ts");
511
- const applied = await orm["client"].query(
512
- `SELECT name FROM migrations WHERE name = ?`,
513
- [migrationName],
514
- );
515
- const status = applied.length > 0
516
- ? `${C.BG_GREEN} APPLIED ${C.RESET}`
517
- : `${C.BG_YELLOW} PENDING ${C.RESET}`;
518
-
519
- console.log(`${status} ${C.WHITE}${migrationName}${C.RESET}`);
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}`);
520
515
  }
521
-
522
- // Ensure seed_history table exists for query purposes
523
- await orm["client"].query(`
524
- CREATE TABLE IF NOT EXISTS seed_history (
525
- id INTEGER PRIMARY KEY AUTOINCREMENT,
526
- name TEXT NOT NULL,
527
- applied_at TEXT NOT NULL
528
- )
529
- `);
530
-
531
516
  const seedDir = path.resolve(process.cwd(), "seeds");
532
- const seedFiles = await glob(`${seedDir}/*.ts`);
517
+ const seedFiles = (await glob(`${seedDir}/*.ts`)).map(f => path.basename(f, ".ts")).sort();
533
518
  console.log(`\n${C.BRIGHT}Seed Status:${C.RESET}`);
534
519
  console.log(`---------------------------------`);
535
- for (const file of seedFiles.sort()) {
536
- const seedName = path.basename(file, ".ts");
537
- const applied = await orm["client"].query(
538
- `SELECT name FROM seed_history WHERE name = ?`,
539
- [seedName],
540
- );
541
- const status = applied.length > 0
542
- ? `${C.BG_GREEN} APPLIED ${C.RESET}`
543
- : `${C.BG_YELLOW} PENDING ${C.RESET}`;
544
-
545
- console.log(`${status} ${C.WHITE}${seedName}${C.RESET}`);
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}`);
546
524
  }
547
-
548
525
  await orm.close();
549
- } catch (error) {
550
- console.error(`${C.BG_RED} FATAL ${C.RESET} Status check failed:`, error);
526
+ } catch (error: any) {
527
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Status check failed:`, error.message);
551
528
  if (orm) await orm.close();
552
529
  process.exit(1);
553
530
  }
554
531
  });
555
532
 
556
-
557
533
  // --------------------------------------------------------------------------------------------------
558
534
  // TOPOLOGICAL SORT HELPER
559
535
  // --------------------------------------------------------------------------------------------------
@@ -564,16 +540,13 @@ function topologicalSort(
564
540
  const result: string[] = [];
565
541
  const visited = new Set<string>();
566
542
  const temp = new Set<string>();
567
-
568
543
  function visit(node: string) {
569
- if (temp.has(node))
570
- throw new Error(`Circular dependency detected at ${node}`);
544
+ if (temp.has(node)) throw new Error(`Circular dependency detected: ${node}`);
571
545
  if (!visited.has(node)) {
572
546
  temp.add(node);
573
- const { dependencies } = graph.get(node)!;
574
- for (const dep of dependencies) {
575
- if (!graph.has(dep))
576
- throw new Error(`Dependency ${dep} not found for ${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}`);
577
550
  visit(dep);
578
551
  }
579
552
  temp.delete(node);
@@ -581,12 +554,10 @@ function topologicalSort(
581
554
  result.push(node);
582
555
  }
583
556
  }
584
-
585
557
  for (const node of graph.keys()) {
586
558
  if (!visited.has(node)) visit(node);
587
559
  }
588
-
589
560
  return result;
590
561
  }
591
562
 
592
- program.parse(process.argv);
563
+ program.parse(process.argv);