stabilize-orm 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +3 -1
- package/package.json +1 -1
- package/cli/stabilize-cli.ts +0 -431
- package/dist/cli/stabilize-cli.js +0 -3258
- package/dist/index.js +0 -3178
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
1
|
/**
|
|
3
2
|
* @file stabilize.ts
|
|
4
3
|
* @description The main entry point for the Stabilize ORM, tying together the client, cache, and repositories.
|
|
5
4
|
* @author ElectronSz
|
|
5
|
+
* @date 2025-10-15 20:35:34
|
|
6
6
|
*/
|
|
7
7
|
import { Cache } from "./cache";
|
|
8
8
|
import { DBClient } from "./client";
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type CacheConfig,
|
|
32
32
|
type LoggerConfig,
|
|
33
33
|
DBType,
|
|
34
|
+
DataTypes, // --- FIX: Import DataTypes here ---
|
|
34
35
|
StabilizeError,
|
|
35
36
|
type PoolMetrics,
|
|
36
37
|
type QueryHint,
|
|
@@ -149,6 +150,7 @@ export {
|
|
|
149
150
|
Cache,
|
|
150
151
|
ConsoleLogger,
|
|
151
152
|
DBType,
|
|
153
|
+
DataTypes,
|
|
152
154
|
LogLevel,
|
|
153
155
|
RelationType,
|
|
154
156
|
StabilizeError,
|
package/package.json
CHANGED
package/cli/stabilize-cli.ts
DELETED
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* @file stabilize-cli.ts
|
|
4
|
-
* @description The command-line interface for the Stabilize ORM.
|
|
5
|
-
* @author ElectronSz
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import 'reflect-metadata';
|
|
9
|
-
|
|
10
|
-
import { program } from "commander";
|
|
11
|
-
import { generateMigration, runMigrations, Stabilize, ModelKey, DBClient } from "../";
|
|
12
|
-
import { LogLevel, type DBConfig, type LoggerConfig, DBType, type Migration } from "../types";
|
|
13
|
-
import * as fs from "fs/promises";
|
|
14
|
-
import * as path from "path";
|
|
15
|
-
import { glob } from "glob";
|
|
16
|
-
import readline from "readline";
|
|
17
|
-
|
|
18
|
-
// --- CLI UI Toolkit ---
|
|
19
|
-
const C = {
|
|
20
|
-
RESET: "\x1b[0m", BRIGHT: "\x1b[1m", DIM: "\x1b[2m",
|
|
21
|
-
RED: "\x1b[31m", GREEN: "\x1b[32m", YELLOW: "\x1b[33m", BLUE: "\x1b[34m",
|
|
22
|
-
MAGENTA: "\x1b[35m", CYAN: "\x1b[36m", WHITE: "\x1b[37m",
|
|
23
|
-
BG_GREEN: "\x1b[42m\x1b[30m", BG_RED: "\x1b[41m\x1b[37m", BG_YELLOW: "\x1b[43m\x1b[30m",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const CLILogger = {
|
|
27
|
-
info: (message: string) => console.log(`${C.BLUE}ℹ${C.RESET} ${message}`),
|
|
28
|
-
success: (message: string) => console.log(`${C.GREEN}✔${C.RESET} ${C.GREEN}${message}${C.RESET}`),
|
|
29
|
-
warn: (message: string) => console.log(`${C.YELLOW}⚠${C.RESET} ${message}`),
|
|
30
|
-
error: (message: string, details?: string) => {
|
|
31
|
-
console.error(`\n${C.BG_RED} ERROR ${C.RESET} ${C.RED}${message}${C.RESET}`);
|
|
32
|
-
if (details) console.error(`${C.DIM}${details}${C.RESET}`);
|
|
33
|
-
console.log();
|
|
34
|
-
},
|
|
35
|
-
panic: (error: Error, command: string) => {
|
|
36
|
-
CLILogger.error(`A fatal error occurred in the '${command}' command.`, error.stack);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const spinner = {
|
|
42
|
-
chars: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
43
|
-
interval: 80,
|
|
44
|
-
_timer: null as Timer | null,
|
|
45
|
-
start: (message: string) => {
|
|
46
|
-
let i = 0;
|
|
47
|
-
process.stdout.write("\n");
|
|
48
|
-
spinner._timer = setInterval(() => {
|
|
49
|
-
process.stdout.write(`\r${C.CYAN}${spinner.chars[i++ % spinner.chars.length]}${C.RESET} ${message}`);
|
|
50
|
-
}, spinner.interval);
|
|
51
|
-
},
|
|
52
|
-
stop: (success: boolean, message: string) => {
|
|
53
|
-
if (spinner._timer) clearInterval(spinner._timer);
|
|
54
|
-
process.stdout.write(`\r${success ? `${C.GREEN}✔` : `${C.RED}✖`} ${message}\n\n`);
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// --- Core CLI Logic ---
|
|
59
|
-
|
|
60
|
-
async function loadConfig(configPath: string): Promise<{ config: DBConfig; orm: Stabilize }> {
|
|
61
|
-
try {
|
|
62
|
-
const absoluteConfigPath = path.resolve(process.cwd(), configPath);
|
|
63
|
-
const configModule = await import(absoluteConfigPath);
|
|
64
|
-
const config: DBConfig = configModule.default || configModule;
|
|
65
|
-
const logLevelKey = program.opts().logLevel as keyof typeof LogLevel;
|
|
66
|
-
const loggerConfig: LoggerConfig = { level: LogLevel[logLevelKey] };
|
|
67
|
-
const orm = new Stabilize(config, { enabled: false, ttl: 60 }, loggerConfig);
|
|
68
|
-
return { config, orm };
|
|
69
|
-
} catch (error) {
|
|
70
|
-
throw new Error(`Failed to load database configuration from '${configPath}'. Make sure the file exists and is valid.`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function confirm(question: string): Promise<boolean> {
|
|
75
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
76
|
-
return new Promise((resolve) => {
|
|
77
|
-
rl.question(`${C.YELLOW}⚠${C.RESET} ${question} ${C.DIM}(y/N)${C.RESET} `, (answer) => {
|
|
78
|
-
rl.close();
|
|
79
|
-
resolve(answer.toLowerCase() === 'y');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// --------------------------------------------------------------------------------------------------
|
|
85
|
-
// COMMAND: GENERATE
|
|
86
|
-
// --------------------------------------------------------------------------------------------------
|
|
87
|
-
program
|
|
88
|
-
.command("generate <type> <name>")
|
|
89
|
-
.description("Generate a new model, migration, or seed file.")
|
|
90
|
-
.action(async (type: string, name: string) => {
|
|
91
|
-
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
92
|
-
try {
|
|
93
|
-
if (type === "migration") {
|
|
94
|
-
const modelPath = path.resolve(process.cwd(), "models", `${name}.ts`);
|
|
95
|
-
const { config } = await loadConfig("config/database.ts");
|
|
96
|
-
const modelModule = await import(modelPath).catch(() => { throw new Error(`Model file not found at '${modelPath}'.`); });
|
|
97
|
-
const modelClass = modelModule[capitalizedName];
|
|
98
|
-
if (!modelClass || !Reflect.getMetadata(ModelKey, modelClass)) {
|
|
99
|
-
throw new Error(`Class '${capitalizedName}' in '${modelPath}' is not a valid @Model.`);
|
|
100
|
-
}
|
|
101
|
-
const migration = await generateMigration(modelClass, `create_${name}_table`, config.type);
|
|
102
|
-
const migrationDir = path.resolve(process.cwd(), "migrations");
|
|
103
|
-
await fs.mkdir(migrationDir, { recursive: true });
|
|
104
|
-
const timestamp = new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 14);
|
|
105
|
-
const fileName = `${timestamp}_create_${name}_table.json`;
|
|
106
|
-
migration.name = path.basename(fileName, ".json");
|
|
107
|
-
const filePath = path.join(migrationDir, fileName);
|
|
108
|
-
await fs.writeFile(filePath, JSON.stringify(migration, null, 2));
|
|
109
|
-
CLILogger.success(`Migration generated: ${filePath}`);
|
|
110
|
-
} else if (type === "model") {
|
|
111
|
-
const modelDir = path.resolve(process.cwd(), "models");
|
|
112
|
-
await fs.mkdir(modelDir, { recursive: true });
|
|
113
|
-
const filePath = path.join(modelDir, `${name}.ts`);
|
|
114
|
-
const content = `
|
|
115
|
-
import 'reflect-metadata';
|
|
116
|
-
import { Model, Column, DataTypes } from 'stabilize-orm';
|
|
117
|
-
|
|
118
|
-
@Model('${name.toLowerCase()}s')
|
|
119
|
-
export class ${capitalizedName} {
|
|
120
|
-
@Column({ type: DataTypes.INTEGER, name: 'id' })
|
|
121
|
-
id!: number;
|
|
122
|
-
|
|
123
|
-
@Column({ type: DataTypes.STRING, length: 100 })
|
|
124
|
-
name!: string;
|
|
125
|
-
|
|
126
|
-
@Column({ type: DataTypes.DATETIME, name: 'created_at' })
|
|
127
|
-
createdAt!: Date;
|
|
128
|
-
|
|
129
|
-
@Column({ type: DataTypes.DATETIME, name: 'updated_at' })
|
|
130
|
-
updatedAt!: Date;
|
|
131
|
-
}
|
|
132
|
-
`.trim() + "\n";
|
|
133
|
-
await fs.writeFile(filePath, content);
|
|
134
|
-
CLILogger.success(`Model generated: ${filePath}`);
|
|
135
|
-
} else if (type === "seed") {
|
|
136
|
-
const seedDir = path.resolve(process.cwd(), "seeds");
|
|
137
|
-
await fs.mkdir(seedDir, { recursive: true });
|
|
138
|
-
const timestamp = new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 14);
|
|
139
|
-
const fileName = `${timestamp}_${name}.ts`;
|
|
140
|
-
const filePath = path.join(seedDir, fileName);
|
|
141
|
-
const content = `
|
|
142
|
-
import { Stabilize } from 'stabilize-orm';
|
|
143
|
-
import { ${capitalizedName} } from './models/${name}';
|
|
144
|
-
|
|
145
|
-
export const dependencies: string[] = [];
|
|
146
|
-
|
|
147
|
-
export async function seed(orm: Stabilize): Promise<void> {
|
|
148
|
-
const repo = orm.getRepository(${capitalizedName});
|
|
149
|
-
await repo.bulkCreate([
|
|
150
|
-
{ name: '${capitalizedName} One' },
|
|
151
|
-
{ name: '${capitalizedName} Two' },
|
|
152
|
-
]);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function rollback(orm: Stabilize): Promise<void> {
|
|
156
|
-
await orm.client.query('DELETE FROM ${name.toLowerCase()}s WHERE name LIKE ?', ['${capitalizedName} %']);
|
|
157
|
-
}
|
|
158
|
-
`.trim() + "\n";
|
|
159
|
-
await fs.writeFile(filePath, content);
|
|
160
|
-
CLILogger.success(`Seed generated: ${filePath}`);
|
|
161
|
-
} else {
|
|
162
|
-
CLILogger.error(`Invalid type '${type}'. Use 'model', 'migration', or 'seed'.`);
|
|
163
|
-
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
CLILogger.panic(error as Error, "generate");
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// --------------------------------------------------------------------------------------------------
|
|
170
|
-
// COMMANDS: MIGRATE & ROLLBACK
|
|
171
|
-
// --------------------------------------------------------------------------------------------------
|
|
172
|
-
program
|
|
173
|
-
.command("migrate")
|
|
174
|
-
.description("Apply all pending database migrations.")
|
|
175
|
-
.option("-c, --config <path>", "Path to database config file", "config/database.ts")
|
|
176
|
-
.action(async (options) => {
|
|
177
|
-
let orm: Stabilize | null = null;
|
|
178
|
-
try {
|
|
179
|
-
const { orm: loadedOrm, config } = await loadConfig(options.config);
|
|
180
|
-
orm = loadedOrm;
|
|
181
|
-
const migrationDir = path.resolve(process.cwd(), "migrations");
|
|
182
|
-
const migrationFiles = (await glob(`${migrationDir}/*.json`)).sort();
|
|
183
|
-
const migrations: Migration[] = await Promise.all(
|
|
184
|
-
migrationFiles.map(async file => JSON.parse(await fs.readFile(file, 'utf-8')))
|
|
185
|
-
);
|
|
186
|
-
if (migrations.length === 0) {
|
|
187
|
-
CLILogger.warn("No migration files found.");
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
spinner.start("Applying migrations...");
|
|
191
|
-
await runMigrations(config, migrations);
|
|
192
|
-
spinner.stop(true, "All pending migrations applied.");
|
|
193
|
-
} catch (error) {
|
|
194
|
-
spinner.stop(false, "Migration process failed.");
|
|
195
|
-
CLILogger.panic(error as Error, "migrate");
|
|
196
|
-
} finally {
|
|
197
|
-
if (orm) await orm.close();
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
program
|
|
202
|
-
.command("migrate:rollback")
|
|
203
|
-
.description("Roll back the most recently applied migration.")
|
|
204
|
-
.option("-c, --config <path>", "Path to database config file", "config/database.ts")
|
|
205
|
-
.action(async (options) => {
|
|
206
|
-
let orm: Stabilize | null = null;
|
|
207
|
-
try {
|
|
208
|
-
const { orm: loadedOrm } = await loadConfig(options.config);
|
|
209
|
-
orm = loadedOrm;
|
|
210
|
-
spinner.start("Rolling back last migration...");
|
|
211
|
-
const [latest] = await orm.client.query<{ name: string }>(`SELECT name FROM migrations ORDER BY applied_at DESC, name DESC LIMIT 1`);
|
|
212
|
-
if (!latest) {
|
|
213
|
-
spinner.stop(false, "No migrations to roll back.");
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const migrationFile = path.resolve(process.cwd(), "migrations", `${latest.name}.json`);
|
|
217
|
-
const migration: Migration = JSON.parse(await fs.readFile(migrationFile, 'utf-8'));
|
|
218
|
-
await orm.transaction(async (txClient) => {
|
|
219
|
-
for (const query of migration.down) await txClient.query(query);
|
|
220
|
-
await txClient.query(`DELETE FROM migrations WHERE name = ?`, [latest.name]);
|
|
221
|
-
});
|
|
222
|
-
spinner.stop(true, `Rolled back: ${latest.name}`);
|
|
223
|
-
} catch (error) {
|
|
224
|
-
spinner.stop(false, "Rollback failed.");
|
|
225
|
-
CLILogger.panic(error as Error, "migrate:rollback");
|
|
226
|
-
} finally {
|
|
227
|
-
if (orm) await orm.close();
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// --------------------------------------------------------------------------------------------------
|
|
232
|
-
// COMMANDS: SEED & ROLLBACK
|
|
233
|
-
// --------------------------------------------------------------------------------------------------
|
|
234
|
-
program
|
|
235
|
-
.command("seed")
|
|
236
|
-
.description("Run all pending seed files.")
|
|
237
|
-
.option("-c, --config <path>", "Path to db config file", "config/database.ts")
|
|
238
|
-
.action(async (options) => {
|
|
239
|
-
let orm: Stabilize | null = null;
|
|
240
|
-
try {
|
|
241
|
-
const { orm: loadedOrm } = await loadConfig(options.config);
|
|
242
|
-
orm = loadedOrm;
|
|
243
|
-
await orm.client.query(`CREATE TABLE IF NOT EXISTS seed_history (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`);
|
|
244
|
-
const seedDir = path.resolve(process.cwd(), "seeds");
|
|
245
|
-
const seedFiles = await glob(`${seedDir}/*.ts`);
|
|
246
|
-
const graph = new Map<string, { file: string; dependencies: string[] }>();
|
|
247
|
-
for (const file of seedFiles) {
|
|
248
|
-
const name = path.basename(file, ".ts");
|
|
249
|
-
const mod = await import(file);
|
|
250
|
-
graph.set(name, { file, dependencies: mod.dependencies || [] });
|
|
251
|
-
}
|
|
252
|
-
const orderedSeeds = topologicalSort(graph);
|
|
253
|
-
const appliedSeeds = new Set((await orm.client.query<{ name: string }>(`SELECT name FROM seed_history`)).map(r => r.name));
|
|
254
|
-
const pendingSeeds = orderedSeeds.filter(s => !appliedSeeds.has(s));
|
|
255
|
-
if (pendingSeeds.length === 0) {
|
|
256
|
-
CLILogger.warn("No pending seeds to run.");
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
spinner.start(`Running ${pendingSeeds.length} seed(s)...`);
|
|
260
|
-
for (const seedName of pendingSeeds) {
|
|
261
|
-
const mod = await import(graph.get(seedName)!.file);
|
|
262
|
-
await orm.transaction(async (txClient) => {
|
|
263
|
-
const txOrm = new Stabilize(orm!.client.config, { enabled: false, ttl: 60 }, { level: LogLevel.Error }, txClient);
|
|
264
|
-
await mod.seed(txOrm);
|
|
265
|
-
await txClient.query(`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)`, [seedName, new Date().toISOString()]);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
spinner.stop(true, `Successfully applied ${pendingSeeds.length} seed(s).`);
|
|
269
|
-
} catch (error) {
|
|
270
|
-
spinner.stop(false, "Seeding process failed.");
|
|
271
|
-
CLILogger.panic(error as Error, "seed");
|
|
272
|
-
} finally {
|
|
273
|
-
if (orm) await orm.close();
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
program
|
|
278
|
-
.command("seed:rollback")
|
|
279
|
-
.description("Roll back the most recently applied seed.")
|
|
280
|
-
.option("-c, --config <path>", "Path to db config file", "config/database.ts")
|
|
281
|
-
.action(async (options) => {
|
|
282
|
-
let orm: Stabilize | null = null;
|
|
283
|
-
try {
|
|
284
|
-
const { orm: loadedOrm } = await loadConfig(options.config);
|
|
285
|
-
orm = loadedOrm;
|
|
286
|
-
spinner.start("Rolling back last seed...");
|
|
287
|
-
const [latest] = await orm!.client.query<{ name: string }>(`SELECT name FROM seed_history ORDER BY applied_at DESC, name DESC LIMIT 1`);
|
|
288
|
-
if (!latest) {
|
|
289
|
-
spinner.stop(false, "No seeds to roll back.");
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
const seedFile = path.resolve(process.cwd(), "seeds", `${latest.name}.ts`);
|
|
293
|
-
const mod = await import(seedFile);
|
|
294
|
-
if (typeof mod.rollback !== 'function') {
|
|
295
|
-
throw new Error(`Rollback function not found in '${latest.name}.ts'`);
|
|
296
|
-
}
|
|
297
|
-
await orm.transaction(async (txClient) => {
|
|
298
|
-
const txOrm = new Stabilize(orm!.client.config, { enabled: false, ttl: 60 }, { level: LogLevel.Error });
|
|
299
|
-
await mod.rollback(txOrm);
|
|
300
|
-
await txClient.query(`DELETE FROM seed_history WHERE name = ?`, [latest.name]);
|
|
301
|
-
});
|
|
302
|
-
spinner.stop(true, `Rolled back seed: ${latest.name}`);
|
|
303
|
-
} catch (error) {
|
|
304
|
-
spinner.stop(false, "Seed rollback failed.");
|
|
305
|
-
CLILogger.panic(error as Error, "seed:rollback");
|
|
306
|
-
} finally {
|
|
307
|
-
if (orm) await orm.close();
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// --------------------------------------------------------------------------------------------------
|
|
312
|
-
// COMMANDS: DB & STATUS
|
|
313
|
-
// --------------------------------------------------------------------------------------------------
|
|
314
|
-
program
|
|
315
|
-
.command("db:drop")
|
|
316
|
-
.description("Drop the database. USE WITH CAUTION.")
|
|
317
|
-
.option("-c, --config <path>", "Path to db config file", "config/database.ts")
|
|
318
|
-
.action(async (options) => {
|
|
319
|
-
try {
|
|
320
|
-
const { config } = await loadConfig(options.config);
|
|
321
|
-
const dbName = new URL(config.connectionString).pathname.substring(1);
|
|
322
|
-
if (!await confirm(`This will permanently delete the '${dbName}' database. Are you sure?`)) {
|
|
323
|
-
CLILogger.warn("Database drop cancelled.");
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
spinner.start(`Dropping database '${dbName}'...`);
|
|
327
|
-
if (config.type === DBType.SQLite) {
|
|
328
|
-
await fs.unlink(dbName).catch(() => {});
|
|
329
|
-
} else {
|
|
330
|
-
const adminConfig = { ...config };
|
|
331
|
-
const url = new URL(adminConfig.connectionString);
|
|
332
|
-
url.pathname = config.type === DBType.Postgres ? '/postgres' : '';
|
|
333
|
-
adminConfig.connectionString = url.toString();
|
|
334
|
-
const adminOrm = new Stabilize(adminConfig);
|
|
335
|
-
await adminOrm.client.query(`DROP DATABASE IF EXISTS "${dbName}"`);
|
|
336
|
-
await adminOrm.close();
|
|
337
|
-
}
|
|
338
|
-
spinner.stop(true, `Database '${dbName}' dropped successfully.`);
|
|
339
|
-
} catch (error) {
|
|
340
|
-
spinner.stop(false, "Failed to drop database.");
|
|
341
|
-
CLILogger.panic(error as Error, "db:drop");
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
program
|
|
346
|
-
.command("db:reset")
|
|
347
|
-
.description("Drop, create, migrate, and seed the database.")
|
|
348
|
-
.option("-c, --config <path>", "Path to db config file", "config/database.ts")
|
|
349
|
-
.action(async (options) => {
|
|
350
|
-
CLILogger.warn("This command will destroy and rebuild your database.");
|
|
351
|
-
try {
|
|
352
|
-
await program.commands.find(c => c.name() === 'db:drop')?.parseAsync(process.argv, { from: 'user' });
|
|
353
|
-
CLILogger.info("Creating database... (This is handled automatically by the driver on first connection)");
|
|
354
|
-
await program.commands.find(c => c.name() === 'migrate')?.parseAsync(process.argv, { from: 'user' });
|
|
355
|
-
await program.commands.find(c => c.name() === 'seed')?.parseAsync(process.argv, { from: 'user' });
|
|
356
|
-
CLILogger.success("Database reset complete.");
|
|
357
|
-
} catch (error) {
|
|
358
|
-
CLILogger.panic(error as Error, "db:reset");
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
program
|
|
363
|
-
.command("status")
|
|
364
|
-
.description("Show the status of all migrations and seeds.")
|
|
365
|
-
.option("-c, --config <path>", "Path to db config file", "config/database.ts")
|
|
366
|
-
.action(async (options) => {
|
|
367
|
-
let orm: Stabilize | null = null;
|
|
368
|
-
try {
|
|
369
|
-
const { orm: loadedOrm } = await loadConfig(options.config);
|
|
370
|
-
orm = loadedOrm;
|
|
371
|
-
await orm.client.query(`CREATE TABLE IF NOT EXISTS migrations (name TEXT PRIMARY KEY, applied_at TEXT)`);
|
|
372
|
-
await orm.client.query(`CREATE TABLE IF NOT EXISTS seed_history (name TEXT PRIMARY KEY, applied_at TEXT)`);
|
|
373
|
-
|
|
374
|
-
console.log(`\n${C.BRIGHT}Migration Status${C.RESET}`);
|
|
375
|
-
console.log(`---------------------------------`);
|
|
376
|
-
const migrationFiles = (await glob(`migrations/*.json`)).map(f => path.basename(f, ".json")).sort();
|
|
377
|
-
const appliedMigrations = new Set((await orm.client.query<{ name: string }>(`SELECT name FROM migrations`)).map(r => r.name));
|
|
378
|
-
migrationFiles.forEach(name => {
|
|
379
|
-
const status = appliedMigrations.has(name) ? `${C.BG_GREEN} APPLIED ${C.RESET}` : `${C.BG_YELLOW} PENDING ${C.RESET}`;
|
|
380
|
-
console.log(`${status} ${C.WHITE}${name}${C.RESET}`);
|
|
381
|
-
});
|
|
382
|
-
if (migrationFiles.length === 0) console.log(C.DIM + " No migration files found." + C.RESET);
|
|
383
|
-
|
|
384
|
-
console.log(`\n${C.BRIGHT}Seed Status${C.RESET}`);
|
|
385
|
-
console.log(`---------------------------------`);
|
|
386
|
-
const seedFiles = (await glob(`seeds/*.ts`)).map(f => path.basename(f, ".ts")).sort();
|
|
387
|
-
const appliedSeeds = new Set((await orm.client.query<{ name: string }>(`SELECT name FROM seed_history`)).map(r => r.name));
|
|
388
|
-
seedFiles.forEach(name => {
|
|
389
|
-
const status = appliedSeeds.has(name) ? `${C.BG_GREEN} APPLIED ${C.RESET}` : `${C.BG_YELLOW} PENDING ${C.RESET}`;
|
|
390
|
-
console.log(`${status} ${C.WHITE}${name}${C.RESET}`);
|
|
391
|
-
});
|
|
392
|
-
if (seedFiles.length === 0) console.log(C.DIM + " No seed files found." + C.RESET);
|
|
393
|
-
console.log();
|
|
394
|
-
|
|
395
|
-
} catch (error) {
|
|
396
|
-
CLILogger.panic(error as Error, "status");
|
|
397
|
-
} finally {
|
|
398
|
-
if (orm) await orm.close();
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// --------------------------------------------------------------------------------------------------
|
|
403
|
-
// TOPOLOGICAL SORT HELPER for seeding
|
|
404
|
-
// --------------------------------------------------------------------------------------------------
|
|
405
|
-
function topologicalSort(graph: Map<string, { file: string; dependencies: string[] }>): string[] {
|
|
406
|
-
const result: string[] = [];
|
|
407
|
-
const visited = new Set<string>();
|
|
408
|
-
const temp = new Set<string>();
|
|
409
|
-
function visit(node: string) {
|
|
410
|
-
if (temp.has(node)) throw new Error(`Circular dependency detected: ${node}`);
|
|
411
|
-
if (!visited.has(node)) {
|
|
412
|
-
temp.add(node);
|
|
413
|
-
const deps = graph.get(node)?.dependencies || [];
|
|
414
|
-
for (const dep of deps) {
|
|
415
|
-
if (!graph.has(dep)) throw new Error(`Dependency '${dep}' not found for seed '${node}'`);
|
|
416
|
-
visit(dep);
|
|
417
|
-
}
|
|
418
|
-
temp.delete(node);
|
|
419
|
-
visited.add(node);
|
|
420
|
-
result.push(node);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
for (const node of graph.keys()) {
|
|
424
|
-
if (!visited.has(node)) visit(node);
|
|
425
|
-
}
|
|
426
|
-
return result;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
program
|
|
430
|
-
.option("-l, --log-level <level>", "Global log level", "Info")
|
|
431
|
-
.parse(process.argv);
|