stabilize-orm 1.0.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.
@@ -0,0 +1,401 @@
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
+ } from "../src";
11
+ import * as fs from "fs/promises";
12
+ import * as path from "path";
13
+ import { glob } from "glob";
14
+
15
+ program.version("1.0.5").description("Stabilize ORM CLI");
16
+
17
+ program
18
+ .command("generate <type> <name>")
19
+ .description("Generate a model, migration, or seed")
20
+ .option(
21
+ "-l, --log-level <level>",
22
+ "Log level (error, warn, info, debug)",
23
+ "info",
24
+ )
25
+ .action(async (type: string, name: string, options) => {
26
+ const loggerConfig: LoggerConfig = {
27
+ level: options.logLevel as LogLevel,
28
+ filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
29
+ maxFileSize: 5 * 1024 * 1024,
30
+ maxFiles: 3,
31
+ };
32
+
33
+ if (type === "migration") {
34
+ const modelPath = path.resolve(process.cwd(), "models", `${name}.ts`);
35
+ try {
36
+ const modelModule = await import(modelPath);
37
+ const model = Object.values(modelModule)[0] as new (
38
+ ...args: any[]
39
+ ) => any;
40
+ const migration = await generateMigration(
41
+ model,
42
+ `create_${name.toLowerCase()}`,
43
+ );
44
+ const migrationDir = path.resolve(process.cwd(), "migrations");
45
+ await fs.mkdir(migrationDir, { recursive: true });
46
+ const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
47
+ const migrationFile = path.join(
48
+ migrationDir,
49
+ `${timestamp}_${name.toLowerCase()}.ts`,
50
+ );
51
+ await fs.writeFile(
52
+ migrationFile,
53
+ `export default ${JSON.stringify(migration, null, 2)};`,
54
+ );
55
+ console.log(`Migration generated: ${migrationFile}`);
56
+ } catch (error) {
57
+ console.error("Error generating migration:", error);
58
+ }
59
+ } else if (type === "model") {
60
+ const modelDir = path.resolve(process.cwd(), "models");
61
+ await fs.mkdir(modelDir, { recursive: true });
62
+ const modelFile = path.join(modelDir, `${name}.ts`);
63
+ const modelContent = `
64
+ import { Model, Column, Required } from 'stabilize-orm';
65
+
66
+ @Model('${name.toLowerCase()}s')
67
+ export class ${name} {
68
+ @Column('id', 'INTEGER')
69
+ id?: number;
70
+
71
+ @Column('name', 'TEXT')
72
+ @Required()
73
+ name: string;
74
+ }
75
+ `;
76
+ await fs.writeFile(modelFile, modelContent);
77
+ console.log(`Model generated: ${modelFile}`);
78
+ } else if (type === "seed") {
79
+ const seedDir = path.resolve(process.cwd(), "seeds");
80
+ await fs.mkdir(seedDir, { recursive: true });
81
+ const timestamp = new Date().toISOString().replace(/[-:T.]/g, "");
82
+ const seedFile = path.join(
83
+ seedDir,
84
+ `${timestamp}_${name.toLowerCase()}.ts`,
85
+ );
86
+ const seedContent = `
87
+ import { Stabilize } from 'stabilize-orm';
88
+
89
+ export const dependencies = [];
90
+
91
+ export async function seed(orm: Stabilize) {
92
+ const repo = orm.getRepository(${name});
93
+ await repo.bulkCreate([
94
+ { name: '${name} 1' },
95
+ { name: '${name} 2' },
96
+ ], { batchSize: 100 });
97
+
98
+ await orm['client'].query(
99
+ \`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)\`,
100
+ ['${timestamp}_${name.toLowerCase()}', new Date().toISOString()]
101
+ );
102
+ }
103
+
104
+ export async function rollback(orm: Stabilize) {
105
+ const repo = orm.getRepository(${name});
106
+ const entities = await repo.find().execute(orm['client']);
107
+ await repo.bulkDelete(entities.map(e => e.id!), { batchSize: 100 });
108
+
109
+ await orm['client'].query(
110
+ \`DELETE FROM seed_history WHERE name = ?\`,
111
+ ['${timestamp}_${name.toLowerCase()}']
112
+ );
113
+ }
114
+ `;
115
+ await fs.writeFile(seedFile, seedContent);
116
+ console.log(`Seed generated: ${seedFile}`);
117
+ } else {
118
+ console.error('Invalid type. Use "model", "migration", or "seed".');
119
+ }
120
+ });
121
+
122
+ program
123
+ .command("seed")
124
+ .description("Run seed files to populate the database")
125
+ .option(
126
+ "-c, --config <path>",
127
+ "Path to database config file",
128
+ "config/database.ts",
129
+ )
130
+ .option(
131
+ "-l, --log-level <level>",
132
+ "Log level (error, warn, info, debug)",
133
+ "info",
134
+ )
135
+ .action(async (options) => {
136
+ try {
137
+ const configPath = path.resolve(process.cwd(), options.config);
138
+ const configModule = await import(configPath);
139
+ const config: DBConfig = configModule.default || configModule;
140
+
141
+ const loggerConfig: LoggerConfig = {
142
+ level: options.logLevel as LogLevel,
143
+ filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
144
+ maxFileSize: 5 * 1024 * 1024,
145
+ maxFiles: 3,
146
+ };
147
+
148
+ const orm = new Stabilize(
149
+ config,
150
+ { enabled: false, ttl: 60 },
151
+ loggerConfig,
152
+ );
153
+
154
+ await orm["client"].query(`
155
+ CREATE TABLE IF NOT EXISTS seed_history (
156
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
157
+ name TEXT NOT NULL,
158
+ applied_at TEXT NOT NULL
159
+ )
160
+ `);
161
+
162
+ const seedDir = path.resolve(process.cwd(), "seeds");
163
+ const seedFiles = await glob(`${seedDir}/*.ts`);
164
+ if (seedFiles.length === 0) {
165
+ console.log("No seed files found in seeds/ directory.");
166
+ await orm.close();
167
+ return;
168
+ }
169
+
170
+ const seedGraph = new Map<
171
+ string,
172
+ { file: string; dependencies: string[] }
173
+ >();
174
+ for (const file of seedFiles) {
175
+ const seedName = path.basename(file, ".ts");
176
+ const seedModule = await import(file);
177
+ seedGraph.set(seedName, {
178
+ file,
179
+ dependencies: seedModule.dependencies || [],
180
+ });
181
+ }
182
+
183
+ const orderedSeeds = topologicalSort(seedGraph);
184
+
185
+ console.log(`Running ${orderedSeeds.length} seed files...`);
186
+ for (const seedName of orderedSeeds) {
187
+ const { file } = seedGraph.get(seedName)!;
188
+ const applied = await orm["client"].query<{ id: number }>(
189
+ `SELECT id FROM seed_history WHERE name = ?`,
190
+ [seedName],
191
+ );
192
+ if (applied.length > 0) {
193
+ console.log(`Skipping already applied seed: ${seedName}`);
194
+ continue;
195
+ }
196
+
197
+ console.log(`Executing seed: ${seedName}`);
198
+ const seedModule = await import(file);
199
+ const seedFn = seedModule.default || seedModule.seed;
200
+ if (typeof seedFn === "function") {
201
+ await seedFn(orm);
202
+ } else {
203
+ console.error(
204
+ `Seed file ${file} must export a default function or a function named 'seed'.`,
205
+ );
206
+ }
207
+ }
208
+
209
+ console.log("Seeding completed successfully.");
210
+ await orm.close();
211
+ } catch (error) {
212
+ console.error("Seeding failed:", error);
213
+ process.exit(1);
214
+ }
215
+ });
216
+
217
+ program
218
+ .command("seed:rollback")
219
+ .description("Rollback the most recently applied seed")
220
+ .option(
221
+ "-c, --config <path>",
222
+ "Path to database config file",
223
+ "config/database.ts",
224
+ )
225
+ .option(
226
+ "-l, --log-level <level>",
227
+ "Log level (error, warn, info, debug)",
228
+ "info",
229
+ )
230
+ .action(async (options) => {
231
+ try {
232
+ const configPath = path.resolve(process.cwd(), options.config);
233
+ const configModule = await import(configPath);
234
+ const config: DBConfig = configModule.default || configModule;
235
+
236
+ const loggerConfig: LoggerConfig = {
237
+ level: options.logLevel as LogLevel,
238
+ filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
239
+ maxFileSize: 5 * 1024 * 1024,
240
+ maxFiles: 3,
241
+ };
242
+
243
+ const orm = new Stabilize(
244
+ config,
245
+ { enabled: false, ttl: 60 },
246
+ loggerConfig,
247
+ );
248
+
249
+ const seedDir = path.resolve(process.cwd(), "seeds");
250
+ const seedFiles = await glob(`${seedDir}/*.ts`);
251
+ const seedGraph = new Map<
252
+ string,
253
+ { file: string; dependencies: string[] }
254
+ >();
255
+ for (const file of seedFiles) {
256
+ const seedName = path.basename(file, ".ts");
257
+ const seedModule = await import(file);
258
+ seedGraph.set(seedName, {
259
+ file,
260
+ dependencies: seedModule.dependencies || [],
261
+ });
262
+ }
263
+
264
+ const latestSeed = await orm["client"].query<{ name: string }>(
265
+ `SELECT name FROM seed_history ORDER BY applied_at DESC LIMIT 1`,
266
+ );
267
+
268
+ if (latestSeed.length === 0) {
269
+ console.log("No seeds to rollback.");
270
+ await orm.close();
271
+ return;
272
+ }
273
+
274
+ const seedName = latestSeed[0]!.name;
275
+ const seedFile = seedGraph.get(seedName)?.file;
276
+ if (!seedFile) {
277
+ console.error(`Seed file for ${seedName} not found.`);
278
+ await orm.close();
279
+ return;
280
+ }
281
+
282
+ console.log(`Rolling back seed: ${seedName}`);
283
+ const seedModule = await import(seedFile);
284
+ const rollbackFn = seedModule.rollback;
285
+ if (typeof rollbackFn === "function") {
286
+ await rollbackFn(orm);
287
+ console.log(`Rolled back seed: ${seedName}`);
288
+ } else {
289
+ console.error(
290
+ `Seed file ${seedFile} must export a 'rollback' function.`,
291
+ );
292
+ }
293
+
294
+ await orm.close();
295
+ } catch (error) {
296
+ console.error("Seed rollback failed:", error);
297
+ process.exit(1);
298
+ }
299
+ });
300
+
301
+ program
302
+ .command("status")
303
+ .description("Display status of migrations and seeds")
304
+ .option(
305
+ "-c, --config <path>",
306
+ "Path to database config file",
307
+ "config/database.ts",
308
+ )
309
+ .option(
310
+ "-l, --log-level <level>",
311
+ "Log level (error, warn, info, debug)",
312
+ "info",
313
+ )
314
+ .action(async (options) => {
315
+ try {
316
+ const configPath = path.resolve(process.cwd(), options.config);
317
+ const configModule = await import(configPath);
318
+ const config: DBConfig = configModule.default || configModule;
319
+
320
+ const loggerConfig: LoggerConfig = {
321
+ level: options.logLevel as LogLevel,
322
+ filePath: path.resolve(process.cwd(), "logs/stabilize.log"),
323
+ maxFileSize: 1 * 1024 * 1024,
324
+ maxFiles: 3,
325
+ };
326
+
327
+ const orm = new Stabilize(
328
+ config,
329
+ { enabled: false, ttl: 60 },
330
+ loggerConfig,
331
+ );
332
+
333
+ const migrationDir = path.resolve(process.cwd(), "migrations");
334
+ const migrationFiles = await glob(`${migrationDir}/*.ts`);
335
+ console.log("\nMigration Status:");
336
+ console.log("-----------------");
337
+ for (const file of migrationFiles.sort()) {
338
+ const migrationName = path.basename(file, ".ts");
339
+ const applied = await orm["client"].query(
340
+ `SELECT name FROM migrations WHERE name = ?`,
341
+ [migrationName],
342
+ );
343
+ console.log(
344
+ `${migrationName}: ${applied.length > 0 ? "Applied" : "Pending"}`,
345
+ );
346
+ }
347
+
348
+ const seedDir = path.resolve(process.cwd(), "seeds");
349
+ const seedFiles = await glob(`${seedDir}/*.ts`);
350
+ console.log("\nSeed Status:");
351
+ console.log("-------------");
352
+ for (const file of seedFiles.sort()) {
353
+ const seedName = path.basename(file, ".ts");
354
+ const applied = await orm["client"].query(
355
+ `SELECT name FROM seed_history WHERE name = ?`,
356
+ [seedName],
357
+ );
358
+ console.log(
359
+ `${seedName}: ${applied.length > 0 ? "Applied" : "Pending"}`,
360
+ );
361
+ }
362
+
363
+ await orm.close();
364
+ } catch (error) {
365
+ console.error("Status check failed:", error);
366
+ process.exit(1);
367
+ }
368
+ });
369
+
370
+ function topologicalSort(
371
+ graph: Map<string, { file: string; dependencies: string[] }>,
372
+ ): string[] {
373
+ const result: string[] = [];
374
+ const visited = new Set<string>();
375
+ const temp = new Set<string>();
376
+
377
+ function visit(node: string) {
378
+ if (temp.has(node))
379
+ throw new Error(`Circular dependency detected at ${node}`);
380
+ if (!visited.has(node)) {
381
+ temp.add(node);
382
+ const { dependencies } = graph.get(node)!;
383
+ for (const dep of dependencies) {
384
+ if (!graph.has(dep))
385
+ throw new Error(`Dependency ${dep} not found for ${node}`);
386
+ visit(dep);
387
+ }
388
+ temp.delete(node);
389
+ visited.add(node);
390
+ result.push(node);
391
+ }
392
+ }
393
+
394
+ for (const node of graph.keys()) {
395
+ if (!visited.has(node)) visit(node);
396
+ }
397
+
398
+ return result;
399
+ }
400
+
401
+ program.parse(process.argv);