stabilize-orm 1.1.0 → 1.1.2

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