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.
- package/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md +23 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- package/.github/workflows/ci-cd.yml +4 -55
- package/CHANGELOG.md +23 -0
- package/CODE_OF_CONDUCT.md +87 -0
- package/CONTRIBUTING.md +48 -0
- package/FUNDING.md +14 -0
- package/README.md +273 -32
- package/SECURITY.md +35 -0
- package/SUPPORT.md +18 -0
- package/bun.lock +6 -0
- package/cli/stabilize-cli.ts +291 -322
- package/client.ts +5 -30
- package/migrations.ts +65 -11
- package/package.json +5 -3
package/cli/stabilize-cli.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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",
|
|
29
|
-
BG_RED: "\x1b[41m\x1b[37m",
|
|
30
|
-
BG_YELLOW: "\x1b[43m\x1b[30m"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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', '
|
|
107
|
-
|
|
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
|
|
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
|
|
122
|
-
|
|
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}';
|
|
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
|
|
213
|
+
await orm.client.query(
|
|
139
214
|
\`INSERT INTO seed_history (name, applied_at) VALUES (?, ?)\`,
|
|
140
|
-
['${
|
|
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
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
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
|
|
227
|
+
await orm.client.query(
|
|
151
228
|
\`DELETE FROM seed_history WHERE name = ?\`,
|
|
152
|
-
['${
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
orm
|
|
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
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
283
|
+
console.log(`${C.YELLOW} WARNING ${C.RESET} No pending migrations found.`);
|
|
204
284
|
await orm.close();
|
|
205
285
|
return;
|
|
206
286
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
248
|
-
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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}
|
|
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
|
-
|
|
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 =
|
|
329
|
-
|
|
330
|
-
await orm
|
|
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
|
|
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
|
-
|
|
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:
|
|
400
|
+
dependencies: mod.dependencies || [],
|
|
356
401
|
});
|
|
357
402
|
}
|
|
358
|
-
|
|
359
403
|
const orderedSeeds = topologicalSort(seedGraph);
|
|
360
404
|
|
|
361
|
-
|
|
362
|
-
let seedsApplied = 0;
|
|
405
|
+
let appliedCount = 0;
|
|
363
406
|
for (const seedName of orderedSeeds) {
|
|
364
407
|
const { file } = seedGraph.get(seedName)!;
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
375
|
-
const
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
435
|
-
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
512
|
-
const
|
|
513
|
-
const
|
|
514
|
-
|
|
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
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
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
|
|
576
|
-
for (const dep of
|
|
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);
|