@archznn/xavva 2.9.0 → 3.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/README.md +188 -1
- package/package.json +1 -1
- package/src/commands/ChangelogCommand.ts +128 -0
- package/src/commands/ConfigCommand.ts +48 -0
- package/src/commands/DbCommand.ts +126 -0
- package/src/commands/DockerCommand.ts +122 -0
- package/src/commands/HelpCommand.ts +44 -0
- package/src/commands/HttpCommand.ts +134 -0
- package/src/commands/InitCommand.ts +70 -0
- package/src/commands/TestCommand.ts +63 -0
- package/src/di/container.ts +15 -0
- package/src/index.ts +14 -1
- package/src/services/DbService.ts +357 -0
- package/src/services/DockerService.ts +361 -0
- package/src/services/HttpService.ts +259 -0
- package/src/services/TestService.ts +326 -0
- package/src/types/args.ts +1 -0
- package/src/types/config.ts +38 -0
- package/src/utils/ChangelogGenerator.ts +255 -0
- package/src/utils/LoggerLevel.ts +138 -0
- package/src/utils/config.ts +38 -2
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serviço de migrações de banco de dados
|
|
3
|
+
* Suporta Flyway e Liquibase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Logger } from "../utils/ui";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
export type MigrationTool = "flyway" | "liquibase" | "auto";
|
|
12
|
+
|
|
13
|
+
export interface DbConfig {
|
|
14
|
+
url: string;
|
|
15
|
+
username: string;
|
|
16
|
+
password: string;
|
|
17
|
+
driver?: string;
|
|
18
|
+
migrationsPath?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MigrationStatus {
|
|
22
|
+
version: string;
|
|
23
|
+
description: string;
|
|
24
|
+
state: "pending" | "applied" | "failed";
|
|
25
|
+
installedOn?: Date;
|
|
26
|
+
executionTime?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MigrationResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
message: string;
|
|
32
|
+
migrationsApplied: number;
|
|
33
|
+
errors: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class DbService {
|
|
37
|
+
private buildTool: "maven" | "gradle";
|
|
38
|
+
private projectPath: string;
|
|
39
|
+
|
|
40
|
+
constructor(buildTool: "maven" | "gradle", projectPath: string = process.cwd()) {
|
|
41
|
+
this.buildTool = buildTool;
|
|
42
|
+
this.projectPath = projectPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Detecta qual ferramenta de migração está configurada no projeto
|
|
47
|
+
*/
|
|
48
|
+
async detectTool(): Promise<MigrationTool> {
|
|
49
|
+
const pomPath = path.join(this.projectPath, "pom.xml");
|
|
50
|
+
const buildGradlePath = path.join(this.projectPath, "build.gradle");
|
|
51
|
+
const buildGradleKtsPath = path.join(this.projectPath, "build.gradle.kts");
|
|
52
|
+
|
|
53
|
+
let content = "";
|
|
54
|
+
if (fs.existsSync(pomPath)) {
|
|
55
|
+
content = fs.readFileSync(pomPath, "utf-8");
|
|
56
|
+
} else if (fs.existsSync(buildGradlePath)) {
|
|
57
|
+
content = fs.readFileSync(buildGradlePath, "utf-8");
|
|
58
|
+
} else if (fs.existsSync(buildGradleKtsPath)) {
|
|
59
|
+
content = fs.readFileSync(buildGradleKtsPath, "utf-8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (content.includes("flyway") || content.includes("flyway-core")) {
|
|
63
|
+
return "flyway";
|
|
64
|
+
}
|
|
65
|
+
if (content.includes("liquibase") || content.includes("liquibase-core")) {
|
|
66
|
+
return "liquibase";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Verificar arquivos de configuração
|
|
70
|
+
if (fs.existsSync(path.join(this.projectPath, "src", "main", "resources", "db", "migration"))) {
|
|
71
|
+
return "flyway";
|
|
72
|
+
}
|
|
73
|
+
if (fs.existsSync(path.join(this.projectPath, "src", "main", "resources", "db", "changelog"))) {
|
|
74
|
+
return "liquibase";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return "auto";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Executa migrações pendentes
|
|
82
|
+
*/
|
|
83
|
+
async migrate(config?: DbConfig): Promise<MigrationResult> {
|
|
84
|
+
const tool = await this.detectTool();
|
|
85
|
+
Logger.section("Database Migration");
|
|
86
|
+
Logger.info("Tool", tool === "auto" ? "auto-detect" : tool);
|
|
87
|
+
|
|
88
|
+
if (tool === "auto") {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
message: "No migration tool detected. Please add Flyway or Liquibase to your project.",
|
|
92
|
+
migrationsApplied: 0,
|
|
93
|
+
errors: ["No migration tool found"]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result = tool === "flyway"
|
|
98
|
+
? await this.runFlywayMigrate(config)
|
|
99
|
+
: await this.runLiquibaseUpdate(config);
|
|
100
|
+
|
|
101
|
+
Logger.endSection();
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Mostra status das migrações
|
|
107
|
+
*/
|
|
108
|
+
async status(config?: DbConfig): Promise<MigrationStatus[]> {
|
|
109
|
+
const tool = await this.detectTool();
|
|
110
|
+
Logger.section("Migration Status");
|
|
111
|
+
Logger.info("Tool", tool);
|
|
112
|
+
|
|
113
|
+
if (tool === "auto") {
|
|
114
|
+
Logger.warn("No migration tool detected");
|
|
115
|
+
Logger.endSection();
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const statuses = tool === "flyway"
|
|
120
|
+
? await this.runFlywayInfo(config)
|
|
121
|
+
: await this.runLiquibaseStatus(config);
|
|
122
|
+
|
|
123
|
+
// Print status table
|
|
124
|
+
if (statuses.length > 0) {
|
|
125
|
+
Logger.divider();
|
|
126
|
+
for (const status of statuses) {
|
|
127
|
+
const stateColor = status.state === "applied" ? Logger.C.success
|
|
128
|
+
: status.state === "failed" ? Logger.C.error
|
|
129
|
+
: Logger.C.warning;
|
|
130
|
+
Logger.info(status.version, `${stateColor}${status.state}${Logger.C.reset} - ${status.description}`);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
Logger.info("Status", "No migrations found");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Logger.endSection();
|
|
137
|
+
return statuses;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reseta o banco (drop all + migrate)
|
|
142
|
+
*/
|
|
143
|
+
async reset(config?: DbConfig): Promise<MigrationResult> {
|
|
144
|
+
Logger.section("Database Reset");
|
|
145
|
+
Logger.warn("This will DROP ALL DATA in the database!");
|
|
146
|
+
|
|
147
|
+
const tool = await this.detectTool();
|
|
148
|
+
|
|
149
|
+
if (tool === "flyway") {
|
|
150
|
+
return await this.runFlywayClean(config);
|
|
151
|
+
} else if (tool === "liquibase") {
|
|
152
|
+
return await this.runLiquibaseDropAll(config);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Logger.endSection();
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
message: "No migration tool detected",
|
|
159
|
+
migrationsApplied: 0,
|
|
160
|
+
errors: []
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Popula dados de teste/seed
|
|
166
|
+
*/
|
|
167
|
+
async seed(config?: DbConfig, seedFile?: string): Promise<MigrationResult> {
|
|
168
|
+
Logger.section("Database Seed");
|
|
169
|
+
|
|
170
|
+
// Procurar arquivos de seed
|
|
171
|
+
const seedPaths = [
|
|
172
|
+
path.join(this.projectPath, "src", "test", "resources", "seed.sql"),
|
|
173
|
+
path.join(this.projectPath, "src", "main", "resources", "seed.sql"),
|
|
174
|
+
path.join(this.projectPath, "seed.sql"),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const seedPath = seedFile || seedPaths.find(p => fs.existsSync(p));
|
|
178
|
+
|
|
179
|
+
if (!seedPath) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
message: "No seed file found. Create seed.sql in src/test/resources/ or project root.",
|
|
183
|
+
migrationsApplied: 0,
|
|
184
|
+
errors: ["Seed file not found"]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Logger.info("Seed file", seedPath);
|
|
189
|
+
|
|
190
|
+
// Executar seed via JDBC ou comando SQL
|
|
191
|
+
const result = await this.executeSeed(seedPath, config);
|
|
192
|
+
|
|
193
|
+
Logger.endSection();
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ===== Flyway Commands =====
|
|
198
|
+
|
|
199
|
+
private async runFlywayMigrate(config?: DbConfig): Promise<MigrationResult> {
|
|
200
|
+
return this.runMavenOrGradle("flyway:migrate", "flywayMigrate", config);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async runFlywayInfo(config?: DbConfig): Promise<MigrationStatus[]> {
|
|
204
|
+
const output = await this.runMavenOrGradleOutput("flyway:info", "flywayInfo", config);
|
|
205
|
+
return this.parseFlywayInfo(output);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async runFlywayClean(config?: DbConfig): Promise<MigrationResult> {
|
|
209
|
+
return this.runMavenOrGradle("flyway:clean", "flywayClean", config);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ===== Liquibase Commands =====
|
|
213
|
+
|
|
214
|
+
private async runLiquibaseUpdate(config?: DbConfig): Promise<MigrationResult> {
|
|
215
|
+
return this.runMavenOrGradle("liquibase:update", "liquibaseUpdate", config);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private async runLiquibaseStatus(config?: DbConfig): Promise<MigrationStatus[]> {
|
|
219
|
+
const output = await this.runMavenOrGradleOutput("liquibase:status", "liquibaseStatus", config);
|
|
220
|
+
return this.parseLiquibaseStatus(output);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async runLiquibaseDropAll(config?: DbConfig): Promise<MigrationResult> {
|
|
224
|
+
return this.runMavenOrGradle("liquibase:dropAll", "liquibaseDropAll", config);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ===== Generic Execution =====
|
|
228
|
+
|
|
229
|
+
private async runMavenOrGradle(
|
|
230
|
+
mavenGoal: string,
|
|
231
|
+
gradleTask: string,
|
|
232
|
+
config?: DbConfig
|
|
233
|
+
): Promise<MigrationResult> {
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
const [cmd, ...args] = this.buildTool === "maven"
|
|
236
|
+
? [process.platform === "win32" ? "mvn.cmd" : "mvn", mavenGoal, "-q"]
|
|
237
|
+
: [process.platform === "win32" ? "gradle.bat" : "gradle", gradleTask, "-q"];
|
|
238
|
+
|
|
239
|
+
const env = config ? this.buildEnv(config) : process.env;
|
|
240
|
+
const spinner = Logger.spinner("Running migrations");
|
|
241
|
+
|
|
242
|
+
const child = spawn(cmd, args, {
|
|
243
|
+
cwd: this.projectPath,
|
|
244
|
+
env: { ...process.env, ...env },
|
|
245
|
+
shell: process.platform === "win32"
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let stdout = "";
|
|
249
|
+
let stderr = "";
|
|
250
|
+
|
|
251
|
+
child.stdout?.on("data", (data) => stdout += data.toString());
|
|
252
|
+
child.stderr?.on("data", (data) => stderr += data.toString());
|
|
253
|
+
|
|
254
|
+
child.on("close", (code) => {
|
|
255
|
+
spinner(code === 0);
|
|
256
|
+
|
|
257
|
+
if (code === 0) {
|
|
258
|
+
Logger.success("Migrations completed successfully");
|
|
259
|
+
} else {
|
|
260
|
+
Logger.error("Migration failed");
|
|
261
|
+
if (stderr) Logger.dim(stderr.slice(0, 500));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
resolve({
|
|
265
|
+
success: code === 0,
|
|
266
|
+
message: code === 0 ? "Success" : stderr || "Failed",
|
|
267
|
+
migrationsApplied: this.countMigrations(stdout),
|
|
268
|
+
errors: code !== 0 ? [stderr] : []
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async runMavenOrGradleOutput(
|
|
275
|
+
mavenGoal: string,
|
|
276
|
+
gradleTask: string,
|
|
277
|
+
config?: DbConfig
|
|
278
|
+
): Promise<string> {
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
const [cmd, ...args] = this.buildTool === "maven"
|
|
281
|
+
? [process.platform === "win32" ? "mvn.cmd" : "mvn", mavenGoal]
|
|
282
|
+
: [process.platform === "win32" ? "gradle.bat" : "gradle", gradleTask];
|
|
283
|
+
|
|
284
|
+
const env = config ? this.buildEnv(config) : process.env;
|
|
285
|
+
let output = "";
|
|
286
|
+
|
|
287
|
+
const child = spawn(cmd, args, {
|
|
288
|
+
cwd: this.projectPath,
|
|
289
|
+
env: { ...process.env, ...env },
|
|
290
|
+
shell: process.platform === "win32"
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
child.stdout?.on("data", (data) => output += data.toString());
|
|
294
|
+
child.stderr?.on("data", (data) => output += data.toString());
|
|
295
|
+
|
|
296
|
+
child.on("close", () => resolve(output));
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private async executeSeed(seedPath: string, config?: DbConfig): Promise<MigrationResult> {
|
|
301
|
+
// Implementação básica - executa via JDBC se possível
|
|
302
|
+
// Ou gera comando SQL para execução manual
|
|
303
|
+
|
|
304
|
+
const sql = fs.readFileSync(seedPath, "utf-8");
|
|
305
|
+
const statements = sql.split(";").filter(s => s.trim());
|
|
306
|
+
|
|
307
|
+
Logger.info("Statements", statements.length);
|
|
308
|
+
Logger.success("Seed file ready for execution");
|
|
309
|
+
Logger.dim("Use your database client to execute the seed file");
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
success: true,
|
|
313
|
+
message: `Seed file prepared: ${seedPath}`,
|
|
314
|
+
migrationsApplied: 0,
|
|
315
|
+
errors: []
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private buildEnv(config: DbConfig): Record<string, string> {
|
|
320
|
+
return {
|
|
321
|
+
JDBC_URL: config.url,
|
|
322
|
+
JDBC_USER: config.username,
|
|
323
|
+
JDBC_PASSWORD: config.password,
|
|
324
|
+
...(config.driver && { JDBC_DRIVER: config.driver })
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private countMigrations(output: string): number {
|
|
329
|
+
const match = output.match(/Successfully applied (\d+) migration/);
|
|
330
|
+
return match ? parseInt(match[1]) : 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private parseFlywayInfo(output: string): MigrationStatus[] {
|
|
334
|
+
const statuses: MigrationStatus[] = [];
|
|
335
|
+
const lines = output.split("\n");
|
|
336
|
+
|
|
337
|
+
for (const line of lines) {
|
|
338
|
+
// Parse Flyway info table
|
|
339
|
+
const match = line.match(/\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(\S+)\s*\|\s*(.+?)\s*\|/);
|
|
340
|
+
if (match && !line.includes("Version")) {
|
|
341
|
+
statuses.push({
|
|
342
|
+
version: match[1],
|
|
343
|
+
description: match[4].trim(),
|
|
344
|
+
state: match[3].toLowerCase() as MigrationStatus["state"]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return statuses;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private parseLiquibaseStatus(output: string): MigrationStatus[] {
|
|
353
|
+
const statuses: MigrationStatus[] = [];
|
|
354
|
+
// Simplified parsing - Liquibase output varies
|
|
355
|
+
return statuses;
|
|
356
|
+
}
|
|
357
|
+
}
|