stabilize-orm 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cache.ts ADDED
@@ -0,0 +1,110 @@
1
+ import Redis from "ioredis";
2
+ import { type CacheConfig, type CacheStats } from "./types";
3
+ import { ConsoleLogger, type Logger } from "./logger";
4
+
5
+ export class Cache {
6
+ private redis: Redis | null = null;
7
+ private ttl: number;
8
+ private prefix: string;
9
+ private strategy: "cache-aside" | "write-through";
10
+ private logger: Logger;
11
+ private hits: number = 0;
12
+ private misses: number = 0;
13
+
14
+ constructor(config: CacheConfig, logger: Logger = new ConsoleLogger()) {
15
+ this.ttl = config.ttl;
16
+ this.prefix = config.cachePrefix || "cache:";
17
+ this.strategy = config.strategy || "cache-aside";
18
+ this.logger = logger;
19
+
20
+ if (config.enabled && config.redisUrl) {
21
+ this.redis = new Redis(config.redisUrl, { lazyConnect: true });
22
+ this.redis.on("error", (error) => this.logger.logError(error));
23
+ }
24
+ }
25
+
26
+ getStrategy() {
27
+ return this.strategy;
28
+ }
29
+
30
+ async get<T>(key: string): Promise<T | null> {
31
+ if (!this.redis) return null;
32
+
33
+ try {
34
+ const data = await this.redis.get(this.prefix + key);
35
+ if (data) {
36
+ this.hits++;
37
+ this.logger.logDebug(`Cache hit for key: ${key}`);
38
+ return JSON.parse(data) as T;
39
+ }
40
+ this.misses++;
41
+ this.logger.logDebug(`Cache miss for key: ${key}`);
42
+ return null;
43
+ } catch (error) {
44
+ this.logger.logError(error as Error);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ async set<T>(key: string, value: T, ttl: number = this.ttl): Promise<void> {
50
+ if (!this.redis) return;
51
+
52
+ try {
53
+ await this.redis.set(this.prefix + key, JSON.stringify(value), "EX", ttl);
54
+ this.logger.logDebug(`Cache set for key: ${key}`);
55
+ } catch (error) {
56
+ this.logger.logError(error as Error);
57
+ }
58
+ }
59
+
60
+ async invalidate(keys: string[]): Promise<void> {
61
+ if (!this.redis) return;
62
+
63
+ try {
64
+ const pipeline = this.redis.pipeline();
65
+ for (const key of keys) {
66
+ pipeline.del(this.prefix + key);
67
+ this.logger.logDebug(`Cache invalidated for key: ${key}`);
68
+ }
69
+ await pipeline.exec();
70
+ } catch (error) {
71
+ this.logger.logError(error as Error);
72
+ }
73
+ }
74
+
75
+ async invalidatePattern(pattern: string): Promise<void> {
76
+ if (!this.redis) return;
77
+
78
+ try {
79
+ const keys = await this.redis.keys(this.prefix + pattern);
80
+ if (keys.length > 0) {
81
+ const pipeline = this.redis.pipeline();
82
+ for (const key of keys) {
83
+ pipeline.del(key);
84
+ this.logger.logDebug(`Cache invalidated for pattern: ${pattern}`);
85
+ }
86
+ await pipeline.exec();
87
+ }
88
+ } catch (error) {
89
+ this.logger.logError(error as Error);
90
+ }
91
+ }
92
+
93
+ async getStats(): Promise<CacheStats> {
94
+ if (!this.redis) return { hits: 0, misses: 0, keys: 0 };
95
+ try {
96
+ const keys = await this.redis.keys(this.prefix + "*");
97
+ return { hits: this.hits, misses: this.misses, keys: keys.length };
98
+ } catch (error) {
99
+ this.logger.logError(error as Error);
100
+ return { hits: this.hits, misses: this.misses, keys: 0 };
101
+ }
102
+ }
103
+
104
+ async disconnect(): Promise<void> {
105
+ if (this.redis) {
106
+ await this.redis.quit();
107
+ this.logger.logInfo("Redis connection closed");
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,594 @@
1
+ #!/usr/bin/env bun
2
+ import { program } from "commander";
3
+ import {
4
+ generateMigration,
5
+ Stabilize,
6
+ DBType,
7
+ type DBConfig,
8
+ LogLevel,
9
+ type LoggerConfig,
10
+ runMigrations, // Added runMigrations import
11
+ } from "../src";
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", // 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
31
+ };
32
+
33
+
34
+ program.version("1.0.5").description("Stabilize ORM CLI");
35
+
36
+ // Helper function to load configuration
37
+ async function loadConfig(configPath: string): Promise<{ config: DBConfig, loggerConfig: LoggerConfig, orm: Stabilize }> {
38
+ const absoluteConfigPath = path.resolve(process.cwd(), configPath);
39
+ const configModule = await import(absoluteConfigPath);
40
+ const config: DBConfig = configModule.default || configModule;
41
+
42
+ const logLevel = program.opts().logLevel as LogLevel;
43
+
44
+ const loggerConfig: LoggerConfig = {
45
+ level: logLevel,
46
+ filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
47
+ maxFileSize: 5 * 1024 * 1024,
48
+ maxFiles: 3,
49
+ };
50
+
51
+ const orm = new Stabilize(
52
+ config,
53
+ { enabled: false, ttl: 60 },
54
+ loggerConfig,
55
+ );
56
+
57
+ return { config, loggerConfig, orm };
58
+ }
59
+
60
+ // --------------------------------------------------------------------------------------------------
61
+ // COMMAND: GENERATE
62
+ // --------------------------------------------------------------------------------------------------
63
+
64
+ program
65
+ .command("generate <type> <name>")
66
+ .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
+ )
72
+ .action(async (type: string, name: string) => {
73
+ try {
74
+ if (type === "migration") {
75
+ 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
+ );
84
+ const migrationDir = path.resolve(process.cwd(), "migrations");
85
+ 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
+ );
95
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Migration generated: ${C.GREEN}${migrationFile}${C.RESET}`);
96
+
97
+ } else if (type === "model") {
98
+ const modelDir = path.resolve(process.cwd(), "models");
99
+ await fs.mkdir(modelDir, { recursive: true });
100
+ const modelFile = path.join(modelDir, `${name}.ts`);
101
+ const modelContent = `
102
+ import { Model, Column, Required } from 'stabilize-orm';
103
+
104
+ @Model('${name.toLowerCase()}s')
105
+ export class ${name} {
106
+ @Column('id', 'INTEGER')
107
+ id?: number;
108
+
109
+ @Column('name', 'TEXT')
110
+ @Required()
111
+ name: string;
112
+ }
113
+ `;
114
+ await fs.writeFile(modelFile, modelContent);
115
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Model generated: ${C.GREEN}${modelFile}${C.RESET}`);
116
+
117
+ } else if (type === "seed") {
118
+ const seedDir = path.resolve(process.cwd(), "seeds");
119
+ 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
+ );
125
+ const seedContent = `
126
+ import { Stabilize } from 'stabilize-orm';
127
+ import { ${name} } from '../models/${name}'; // Assuming model path
128
+
129
+ export const dependencies = [];
130
+
131
+ export async function seed(orm: Stabilize) {
132
+ const repo = orm.getRepository(${name});
133
+ await repo.bulkCreate([
134
+ { name: '${name} 1' },
135
+ { name: '${name} 2' },
136
+ ], { batchSize: 100 });
137
+
138
+ await orm['client'].query(
139
+ \`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)\`,
140
+ ['${timestamp}_${name.toLowerCase()}', new Date().toISOString()]
141
+ );
142
+ }
143
+
144
+ export async function rollback(orm: Stabilize) {
145
+ 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 });
149
+
150
+ await orm['client'].query(
151
+ \`DELETE FROM seed_history WHERE name = ?\`,
152
+ ['${timestamp}_${name.toLowerCase()}']
153
+ );
154
+ }
155
+ `;
156
+ await fs.writeFile(seedFile, seedContent);
157
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Seed generated: ${C.GREEN}${seedFile}${C.RESET}`);
158
+ } else {
159
+ console.error(`${C.BG_RED} ERROR ${C.RESET} Invalid type. Use ${C.YELLOW}"model", "migration", or "seed"${C.RESET}.`);
160
+ }
161
+ } catch (error) {
162
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Error generating file:`, error);
163
+ }
164
+ });
165
+
166
+
167
+ // --------------------------------------------------------------------------------------------------
168
+ // COMMAND: MIGRATE
169
+ // --------------------------------------------------------------------------------------------------
170
+
171
+ program
172
+ .command("migrate")
173
+ .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
+ )
184
+ .action(async (options) => {
185
+ let orm: Stabilize | null = null;
186
+ 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 });
191
+
192
+ const migrationDir = path.resolve(process.cwd(), "migrations");
193
+ const migrationFiles = await glob(`${migrationDir}/*.ts`);
194
+
195
+ 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);
200
+ }
201
+
202
+ if (migrations.length === 0) {
203
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No migration files found in migrations/ directory.`);
204
+ await orm.close();
205
+ return;
206
+ }
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.`);
214
+ await orm.close();
215
+
216
+ } catch (error) {
217
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Migration failed:`, error);
218
+ if (orm) await orm.close();
219
+ process.exit(1);
220
+ }
221
+ });
222
+
223
+
224
+ // --------------------------------------------------------------------------------------------------
225
+ // COMMAND: MIGRATE:ROLLBACK (One step back)
226
+ // --------------------------------------------------------------------------------------------------
227
+
228
+ program
229
+ .command("migrate:rollback")
230
+ .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
+ )
241
+ .action(async (options) => {
242
+ let orm: Stabilize | null = null;
243
+ try {
244
+ const { config } = await loadConfig(options.config);
245
+ orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
246
+
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`,
250
+ );
251
+
252
+ if (latestApplied.length === 0) {
253
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No migrations to rollback.`);
254
+ await orm.close();
255
+ return;
256
+ }
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.`);
267
+ await orm.close();
268
+ return;
269
+ }
270
+
271
+ // 3. Load the rollback (down) query
272
+ const migrationModule = await import(migrationFile);
273
+ 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;
280
+ }
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)
285
+ 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
+ );
294
+ });
295
+
296
+ console.log(`${C.BG_GREEN} SUCCESS ${C.RESET} Rolled back migration: ${C.GREEN}${migrationName}${C.RESET}`);
297
+ await orm.close();
298
+
299
+ } catch (error) {
300
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Migration rollback failed:`, error);
301
+ if (orm) await orm.close();
302
+ process.exit(1);
303
+ }
304
+ });
305
+
306
+
307
+ // --------------------------------------------------------------------------------------------------
308
+ // COMMAND: SEED
309
+ // --------------------------------------------------------------------------------------------------
310
+
311
+ program
312
+ .command("seed")
313
+ .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
+ )
324
+ .action(async (options) => {
325
+ let orm: Stabilize | null = null;
326
+ 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
+ `);
337
+
338
+ const seedDir = path.resolve(process.cwd(), "seeds");
339
+ const seedFiles = await glob(`${seedDir}/*.ts`);
340
+ if (seedFiles.length === 0) {
341
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No seed files found in seeds/ directory.`);
342
+ await orm.close();
343
+ return;
344
+ }
345
+
346
+ const seedGraph = new Map<
347
+ string,
348
+ { file: string; dependencies: string[] }
349
+ >();
350
+ for (const file of seedFiles) {
351
+ const seedName = path.basename(file, ".ts");
352
+ const seedModule = await import(file);
353
+ seedGraph.set(seedName, {
354
+ file,
355
+ dependencies: seedModule.dependencies || [],
356
+ });
357
+ }
358
+
359
+ const orderedSeeds = topologicalSort(seedGraph);
360
+
361
+ console.log(`${C.BLUE} INFO ${C.RESET} Running ${C.BRIGHT}${orderedSeeds.length}${C.RESET} seed files (sorted by dependency)...`);
362
+ let seedsApplied = 0;
363
+ for (const seedName of orderedSeeds) {
364
+ 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}`);
371
+ continue;
372
+ }
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;
377
+ if (typeof seedFn === "function") {
378
+ 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'.`,
383
+ );
384
+ }
385
+ }
386
+
387
+ console.log(`\n${C.BG_GREEN} SUCCESS ${C.RESET} Seeding completed. ${C.GREEN}${seedsApplied} new seeds applied.${C.RESET}`);
388
+ await orm.close();
389
+ } catch (error) {
390
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Seeding failed:`, error);
391
+ if (orm) await orm.close();
392
+ process.exit(1);
393
+ }
394
+ });
395
+
396
+ // --------------------------------------------------------------------------------------------------
397
+ // COMMAND: SEED:ROLLBACK (One step back)
398
+ // --------------------------------------------------------------------------------------------------
399
+
400
+ program
401
+ .command("seed:rollback")
402
+ .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
+ )
413
+ .action(async (options) => {
414
+ let orm: Stabilize | null = null;
415
+ 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
+ }
433
+
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) {
439
+ console.log(`${C.YELLOW} WARNING ${C.RESET} No seeds to rollback.`);
440
+ await orm.close();
441
+ return;
442
+ }
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.`);
448
+ await orm.close();
449
+ return;
450
+ }
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
+ );
463
+ }
464
+
465
+ await orm.close();
466
+ } catch (error) {
467
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Seed rollback failed:`, error);
468
+ if (orm) await orm.close();
469
+ process.exit(1);
470
+ }
471
+ });
472
+
473
+
474
+ // --------------------------------------------------------------------------------------------------
475
+ // COMMAND: STATUS
476
+ // --------------------------------------------------------------------------------------------------
477
+
478
+ program
479
+ .command("status")
480
+ .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
+ )
491
+ .action(async (options) => {
492
+ let orm: Stabilize | null = null;
493
+ try {
494
+ const { config } = await loadConfig(options.config);
495
+ orm = new Stabilize(config, { enabled: false, ttl: 60 }, { level: options.logLevel as LogLevel });
496
+
497
+ 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
+
509
+ console.log(`\n${C.BRIGHT}Migration Status:${C.RESET}`);
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}`);
522
+ }
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
+ const seedDir = path.resolve(process.cwd(), "seeds");
534
+ const seedFiles = await glob(`${seedDir}/*.ts`);
535
+ console.log(`\n${C.BRIGHT}Seed Status:${C.RESET}`);
536
+ 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}`);
548
+ }
549
+
550
+ await orm.close();
551
+ } catch (error) {
552
+ console.error(`${C.BG_RED} FATAL ${C.RESET} Status check failed:`, error);
553
+ if (orm) await orm.close();
554
+ process.exit(1);
555
+ }
556
+ });
557
+
558
+
559
+ // --------------------------------------------------------------------------------------------------
560
+ // TOPOLOGICAL SORT HELPER
561
+ // --------------------------------------------------------------------------------------------------
562
+
563
+ function topologicalSort(
564
+ graph: Map<string, { file: string; dependencies: string[] }>,
565
+ ): string[] {
566
+ const result: string[] = [];
567
+ const visited = new Set<string>();
568
+ const temp = new Set<string>();
569
+
570
+ function visit(node: string) {
571
+ if (temp.has(node))
572
+ throw new Error(`Circular dependency detected at ${node}`);
573
+ if (!visited.has(node)) {
574
+ 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}`);
579
+ visit(dep);
580
+ }
581
+ temp.delete(node);
582
+ visited.add(node);
583
+ result.push(node);
584
+ }
585
+ }
586
+
587
+ for (const node of graph.keys()) {
588
+ if (!visited.has(node)) visit(node);
589
+ }
590
+
591
+ return result;
592
+ }
593
+
594
+ program.parse(process.argv);