@seip/blue-bird 0.9.0 → 0.9.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/README.md +155 -20
- package/core/app.js +96 -15
- package/core/cli/docker.js +343 -25
- package/core/database.js +136 -0
- package/core/index.d.ts +132 -0
- package/core/router.js +33 -8
- package/core/ws.js +210 -0
- package/docker/nginx.conf +1 -0
- package/package.json +3 -1
package/core/cli/docker.js
CHANGED
|
@@ -278,7 +278,12 @@ async function logsCommand(service, followOpt) {
|
|
|
278
278
|
* @param {boolean} rootOpt - Flag for overriding database credentials to connect as root/postgres.
|
|
279
279
|
* @param {string} explicitService - Explicit target service if specified ('mysql' or 'postgres').
|
|
280
280
|
*/
|
|
281
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Handles interactive shell connections or smart queries into the MySQL or PostgreSQL container.
|
|
283
|
+
* @param {string[]} clientArgs - CLI arguments.
|
|
284
|
+
* @param {string} explicitService - Explicit target service if specified ('mysql', 'postgres', 'psql', 'db').
|
|
285
|
+
*/
|
|
286
|
+
async function dbClientCommand(clientArgs = [], explicitService) {
|
|
282
287
|
checkComposeFile();
|
|
283
288
|
const env = getEnvVars();
|
|
284
289
|
const dbType = explicitService === "postgres" || explicitService === "psql" ? "postgres" : (explicitService === "mysql" ? "mysql" : getDbType(env));
|
|
@@ -287,25 +292,111 @@ async function dbClientCommand(userOpt, passOpt, dbOpt, rootOpt, explicitService
|
|
|
287
292
|
return;
|
|
288
293
|
}
|
|
289
294
|
|
|
295
|
+
let userOpt, passOpt, dbOpt, rootOpt = false;
|
|
296
|
+
let limitOpt = null, whereOpt = null;
|
|
297
|
+
const positionalArgs = [];
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < clientArgs.length; i++) {
|
|
300
|
+
const arg = clientArgs[i];
|
|
301
|
+
if (arg === "-u" || arg === "--user") {
|
|
302
|
+
userOpt = clientArgs[++i];
|
|
303
|
+
} else if (arg.startsWith("--user=")) {
|
|
304
|
+
userOpt = arg.split("=")[1];
|
|
305
|
+
} else if (arg === "-p" || arg === "--password") {
|
|
306
|
+
passOpt = clientArgs[++i];
|
|
307
|
+
} else if (arg.startsWith("--password=")) {
|
|
308
|
+
passOpt = arg.split("=")[1];
|
|
309
|
+
} else if (arg === "-d" || arg === "--db") {
|
|
310
|
+
dbOpt = clientArgs[++i];
|
|
311
|
+
} else if (arg.startsWith("--db=")) {
|
|
312
|
+
dbOpt = arg.split("=")[1];
|
|
313
|
+
} else if (arg === "--root") {
|
|
314
|
+
rootOpt = true;
|
|
315
|
+
} else if (arg === "--limit" || arg === "-l") {
|
|
316
|
+
limitOpt = clientArgs[++i];
|
|
317
|
+
} else if (arg.startsWith("--limit=")) {
|
|
318
|
+
limitOpt = arg.split("=")[1];
|
|
319
|
+
} else if (arg === "--where" || arg === "-w") {
|
|
320
|
+
whereOpt = clientArgs[++i];
|
|
321
|
+
} else if (arg.startsWith("--where=")) {
|
|
322
|
+
whereOpt = arg.split("=")[1];
|
|
323
|
+
} else if (!arg.startsWith("-")) {
|
|
324
|
+
positionalArgs.push(arg);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
290
328
|
let dbUser = userOpt;
|
|
291
329
|
let dbPass = passOpt;
|
|
292
330
|
let dbName = dbOpt;
|
|
293
331
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
332
|
+
let sqlQuery = null;
|
|
333
|
+
if (positionalArgs.length > 0) {
|
|
334
|
+
const firstPos = positionalArgs[0].toLowerCase();
|
|
335
|
+
|
|
336
|
+
if (firstPos === "export" || firstPos === "dump") {
|
|
337
|
+
await exportDbCommand(dbType, positionalArgs[1], userOpt, passOpt, dbOpt);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (firstPos === "import" || firstPos === "restore") {
|
|
341
|
+
await importDbCommand(dbType, positionalArgs[1], userOpt, passOpt, dbOpt);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (firstPos === "tables") {
|
|
345
|
+
if (dbType === "postgres") {
|
|
346
|
+
sqlQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';";
|
|
347
|
+
} else {
|
|
348
|
+
sqlQuery = "SHOW TABLES;";
|
|
349
|
+
}
|
|
350
|
+
} else if (firstPos === "columns" || firstPos === "cols" || firstPos === "describe" || firstPos === "desc") {
|
|
351
|
+
const tableName = positionalArgs[1];
|
|
352
|
+
if (!tableName) {
|
|
353
|
+
console.error(chalk.red("Error: Please specify a table name. Example: npx blue-bird docker mysql columns users"));
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
if (dbType === "postgres") {
|
|
357
|
+
sqlQuery = `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = '${tableName}';`;
|
|
358
|
+
} else {
|
|
359
|
+
sqlQuery = `SHOW COLUMNS FROM ${tableName};`;
|
|
360
|
+
}
|
|
297
361
|
} else {
|
|
298
|
-
|
|
362
|
+
const rawInput = positionalArgs.join(" ").trim();
|
|
363
|
+
const isFullQuery = /^(select|show|desc|describe|explain|insert|update|delete|create|drop|alter|truncate)\b/i.test(rawInput) || rawInput.includes(" ");
|
|
364
|
+
if (isFullQuery) {
|
|
365
|
+
sqlQuery = rawInput;
|
|
366
|
+
} else {
|
|
367
|
+
const tableName = rawInput;
|
|
368
|
+
sqlQuery = `SELECT * FROM ${tableName}`;
|
|
369
|
+
if (whereOpt) {
|
|
370
|
+
sqlQuery += ` WHERE ${whereOpt}`;
|
|
371
|
+
}
|
|
372
|
+
if (limitOpt) {
|
|
373
|
+
sqlQuery += ` LIMIT ${limitOpt}`;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!sqlQuery.endsWith(";")) {
|
|
377
|
+
sqlQuery += ";";
|
|
378
|
+
}
|
|
299
379
|
}
|
|
300
|
-
|
|
380
|
+
}
|
|
301
381
|
|
|
302
|
-
|
|
303
|
-
|
|
382
|
+
if (dbType === "postgres") {
|
|
383
|
+
dbUser = rootOpt ? "postgres" : (dbUser || env.DB_USER || "postgres");
|
|
384
|
+
dbName = dbName || env.DB_NAME || "blue_bird";
|
|
304
385
|
|
|
305
386
|
const cmdArgs = ["compose", "exec", "postgres", "psql", `-U${dbUser}`];
|
|
306
387
|
if (dbName) {
|
|
307
388
|
cmdArgs.push("-d", dbName);
|
|
308
389
|
}
|
|
390
|
+
|
|
391
|
+
if (sqlQuery) {
|
|
392
|
+
console.log(chalk.cyan(`🔍 Executing PostgreSQL query on database '${dbName}':`));
|
|
393
|
+
console.log(chalk.gray(` ${sqlQuery}\n`));
|
|
394
|
+
cmdArgs.push("-c", sqlQuery);
|
|
395
|
+
} else {
|
|
396
|
+
const targetDb = dbName ? ` (database: ${dbName})` : "";
|
|
397
|
+
console.log(chalk.cyan(`Connecting to PostgreSQL shell (psql) in container as '${dbUser}'${targetDb}...`));
|
|
398
|
+
}
|
|
399
|
+
|
|
309
400
|
const code = await runCmd("docker", cmdArgs);
|
|
310
401
|
if (code !== 0) {
|
|
311
402
|
console.error(chalk.yellow("Make sure the PostgreSQL container is running: npx blue-bird docker start postgres"));
|
|
@@ -329,8 +420,14 @@ async function dbClientCommand(userOpt, passOpt, dbOpt, rootOpt, explicitService
|
|
|
329
420
|
cmdArgs.push(dbName);
|
|
330
421
|
}
|
|
331
422
|
|
|
332
|
-
|
|
333
|
-
|
|
423
|
+
if (sqlQuery) {
|
|
424
|
+
console.log(chalk.cyan(`🔍 Executing MySQL query on database '${dbName}':`));
|
|
425
|
+
console.log(chalk.gray(` ${sqlQuery}\n`));
|
|
426
|
+
cmdArgs.push("-t", "-e", sqlQuery);
|
|
427
|
+
} else {
|
|
428
|
+
const targetDb = dbName ? ` (database: ${dbName})` : "";
|
|
429
|
+
console.log(chalk.cyan(`Connecting to MySQL shell in container as '${dbUser}'${targetDb}...`));
|
|
430
|
+
}
|
|
334
431
|
|
|
335
432
|
const code = await runCmd("docker", cmdArgs);
|
|
336
433
|
if (code !== 0) {
|
|
@@ -340,6 +437,196 @@ async function dbClientCommand(userOpt, passOpt, dbOpt, rootOpt, explicitService
|
|
|
340
437
|
}
|
|
341
438
|
}
|
|
342
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Exports database schema & data into a .sql file inside backups/ folder.
|
|
442
|
+
* @param {string} dbType - Target db type ('mysql', 'postgres', 'none').
|
|
443
|
+
* @param {string} [filenameArg] - Custom backup filename.
|
|
444
|
+
* @param {string} [userOpt] - Custom db user.
|
|
445
|
+
* @param {string} [passOpt] - Custom db password.
|
|
446
|
+
* @param {string} [dbOpt] - Custom db name.
|
|
447
|
+
*/
|
|
448
|
+
async function exportDbCommand(dbType, filenameArg, userOpt, passOpt, dbOpt) {
|
|
449
|
+
checkComposeFile();
|
|
450
|
+
const env = getEnvVars();
|
|
451
|
+
const targetDbType = dbType === "none" ? getDbType(env) : dbType;
|
|
452
|
+
|
|
453
|
+
if (targetDbType === "none") {
|
|
454
|
+
console.error(chalk.yellow("[INFO] DB_TYPE is set to 'none'. No database available to export."));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const backupsDir = path.join(process.cwd(), "backups");
|
|
459
|
+
if (!fs.existsSync(backupsDir)) {
|
|
460
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let outputFile;
|
|
464
|
+
if (filenameArg) {
|
|
465
|
+
let name = filenameArg;
|
|
466
|
+
if (!name.endsWith(".sql")) name += ".sql";
|
|
467
|
+
if (path.isAbsolute(name)) {
|
|
468
|
+
outputFile = name;
|
|
469
|
+
} else if (name.includes("/") || name.includes("\\")) {
|
|
470
|
+
outputFile = path.resolve(process.cwd(), name);
|
|
471
|
+
} else {
|
|
472
|
+
outputFile = path.join(backupsDir, name);
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
|
|
476
|
+
outputFile = path.join(backupsDir, `backup_${targetDbType}_${timestamp}.sql`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const dbUser = userOpt || env.DB_USER || (targetDbType === "postgres" ? "postgres" : "root");
|
|
480
|
+
const dbPass = passOpt || env.DB_PASSWORD || (targetDbType === "postgres" ? "postgres" : "root");
|
|
481
|
+
const dbName = dbOpt || env.DB_NAME || "blue_bird";
|
|
482
|
+
|
|
483
|
+
let cmdArgs = [];
|
|
484
|
+
if (targetDbType === "postgres") {
|
|
485
|
+
cmdArgs = ["compose", "exec", "-T", "postgres", "pg_dump", `-U${dbUser}`, "-d", dbName];
|
|
486
|
+
} else {
|
|
487
|
+
cmdArgs = ["compose", "exec", "-T", "mysql", "mysqldump", `-u${dbUser}`, `-p${dbPass}`, dbName];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const relPath = path.relative(process.cwd(), outputFile);
|
|
491
|
+
console.log(chalk.cyan(`📦 Exporting ${targetDbType.toUpperCase()} database '${dbName}' to '${relPath}'...`));
|
|
492
|
+
|
|
493
|
+
const success = await exportDbToFile(cmdArgs, outputFile);
|
|
494
|
+
if (success) {
|
|
495
|
+
const stats = fs.statSync(outputFile);
|
|
496
|
+
const sizeKb = (stats.size / 1024).toFixed(2);
|
|
497
|
+
console.log(chalk.green(`\n✔ Database exported successfully!`));
|
|
498
|
+
console.log(chalk.cyan(` File: ${relPath} (${sizeKb} KB)`));
|
|
499
|
+
} else {
|
|
500
|
+
console.error(chalk.red(`\n✖ Database export failed.`));
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Streams stdout from container dump command into a local file.
|
|
507
|
+
*/
|
|
508
|
+
function exportDbToFile(cmdArgs, outputFile) {
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
const outStream = fs.createWriteStream(outputFile);
|
|
511
|
+
const proc = spawn("docker", cmdArgs, { stdio: ["inherit", "pipe", "pipe"], env: process.env });
|
|
512
|
+
proc.stdout.pipe(outStream);
|
|
513
|
+
|
|
514
|
+
let errOutput = "";
|
|
515
|
+
proc.stderr.on("data", (chunk) => {
|
|
516
|
+
errOutput += chunk.toString();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
proc.on("close", (code) => {
|
|
520
|
+
outStream.close();
|
|
521
|
+
if (code === 0) {
|
|
522
|
+
resolve(true);
|
|
523
|
+
} else {
|
|
524
|
+
if (errOutput) console.error(chalk.yellow(`Warning/Stderr: ${errOutput.trim()}`));
|
|
525
|
+
resolve(code === 0);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Imports a .sql file from backups/ folder into the container database.
|
|
533
|
+
* @param {string} dbType - Target db type ('mysql', 'postgres', 'none').
|
|
534
|
+
* @param {string} [filenameArg] - Custom backup filename.
|
|
535
|
+
* @param {string} [userOpt] - Custom db user.
|
|
536
|
+
* @param {string} [passOpt] - Custom db password.
|
|
537
|
+
* @param {string} [dbOpt] - Custom db name.
|
|
538
|
+
*/
|
|
539
|
+
async function importDbCommand(dbType, filenameArg, userOpt, passOpt, dbOpt) {
|
|
540
|
+
checkComposeFile();
|
|
541
|
+
const env = getEnvVars();
|
|
542
|
+
const targetDbType = dbType === "none" ? getDbType(env) : dbType;
|
|
543
|
+
|
|
544
|
+
if (targetDbType === "none") {
|
|
545
|
+
console.error(chalk.yellow("[INFO] DB_TYPE is set to 'none'. No database available to import into."));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const backupsDir = path.join(process.cwd(), "backups");
|
|
550
|
+
if (!fs.existsSync(backupsDir)) {
|
|
551
|
+
fs.mkdirSync(backupsDir, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let inputFile;
|
|
555
|
+
if (filenameArg) {
|
|
556
|
+
let name = filenameArg;
|
|
557
|
+
if (!name.endsWith(".sql") && !fs.existsSync(name)) name += ".sql";
|
|
558
|
+
if (fs.existsSync(name)) {
|
|
559
|
+
inputFile = path.resolve(process.cwd(), name);
|
|
560
|
+
} else if (fs.existsSync(path.join(backupsDir, name))) {
|
|
561
|
+
inputFile = path.join(backupsDir, name);
|
|
562
|
+
} else {
|
|
563
|
+
console.error(chalk.red(`Error: Backup file '${filenameArg}' not found in current directory or 'backups/' folder.`));
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
const files = fs.readdirSync(backupsDir)
|
|
568
|
+
.filter(f => f.endsWith(".sql"))
|
|
569
|
+
.map(f => ({ name: f, time: fs.statSync(path.join(backupsDir, f)).mtimeMs }))
|
|
570
|
+
.sort((a, b) => b.time - a.time);
|
|
571
|
+
|
|
572
|
+
if (files.length === 0) {
|
|
573
|
+
console.error(chalk.red(`Error: No .sql backup files found in 'backups/' directory.`));
|
|
574
|
+
console.log(chalk.yellow(`Usage: npx blue-bird docker import <file.sql>`));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
inputFile = path.join(backupsDir, files[0].name);
|
|
579
|
+
console.log(chalk.yellow(`[INFO] No file specified. Using most recent backup: '${files[0].name}'`));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const dbUser = userOpt || env.DB_USER || (targetDbType === "postgres" ? "postgres" : "root");
|
|
583
|
+
const dbPass = passOpt || env.DB_PASSWORD || (targetDbType === "postgres" ? "postgres" : "root");
|
|
584
|
+
const dbName = dbOpt || env.DB_NAME || "blue_bird";
|
|
585
|
+
|
|
586
|
+
let cmdArgs = [];
|
|
587
|
+
if (targetDbType === "postgres") {
|
|
588
|
+
cmdArgs = ["compose", "exec", "-T", "postgres", "psql", `-U${dbUser}`, "-d", dbName];
|
|
589
|
+
} else {
|
|
590
|
+
cmdArgs = ["compose", "exec", "-T", "mysql", "mysql", `-u${dbUser}`, `-p${dbPass}`, dbName];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const relPath = path.relative(process.cwd(), inputFile);
|
|
594
|
+
console.log(chalk.cyan(`📥 Importing SQL dump '${relPath}' into ${targetDbType.toUpperCase()} database '${dbName}'...`));
|
|
595
|
+
|
|
596
|
+
const success = await importDbFromFile(cmdArgs, inputFile);
|
|
597
|
+
if (success) {
|
|
598
|
+
console.log(chalk.green(`\n✔ Database imported successfully from '${relPath}'!`));
|
|
599
|
+
} else {
|
|
600
|
+
console.error(chalk.red(`\n✖ Database import failed.`));
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Streams a local .sql file into container stdin.
|
|
607
|
+
*/
|
|
608
|
+
function importDbFromFile(cmdArgs, inputFile) {
|
|
609
|
+
return new Promise((resolve) => {
|
|
610
|
+
const inStream = fs.createReadStream(inputFile);
|
|
611
|
+
const proc = spawn("docker", cmdArgs, { stdio: ["pipe", "inherit", "pipe"], env: process.env });
|
|
612
|
+
inStream.pipe(proc.stdin);
|
|
613
|
+
|
|
614
|
+
let errOutput = "";
|
|
615
|
+
proc.stderr.on("data", (chunk) => {
|
|
616
|
+
errOutput += chunk.toString();
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
proc.on("close", (code) => {
|
|
620
|
+
if (code === 0) {
|
|
621
|
+
resolve(true);
|
|
622
|
+
} else {
|
|
623
|
+
if (errOutput) console.error(chalk.yellow(`Warning/Stderr: ${errOutput.trim()}`));
|
|
624
|
+
resolve(code === 0);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
343
630
|
/**
|
|
344
631
|
* Handles cleaning up and pruning unused Docker resources.
|
|
345
632
|
* @param {boolean} forceOpt - Force cleaning without user confirmation.
|
|
@@ -385,12 +672,43 @@ async function pm2Command(pm2Args = []) {
|
|
|
385
672
|
}
|
|
386
673
|
|
|
387
674
|
/**
|
|
388
|
-
* Handles interactive shell connections into the Redis container.
|
|
675
|
+
* Handles interactive shell connections or smart query subcommands into the Redis container.
|
|
676
|
+
* @param {string[]} redisArgs - Subcommands or key parameters.
|
|
389
677
|
*/
|
|
390
|
-
async function redisCommand() {
|
|
678
|
+
async function redisCommand(redisArgs = []) {
|
|
391
679
|
checkComposeFile();
|
|
392
680
|
const cmdArgs = ["compose", "exec", "redis", "redis-cli"];
|
|
393
|
-
|
|
681
|
+
|
|
682
|
+
if (redisArgs.length > 0) {
|
|
683
|
+
const firstArg = redisArgs[0].toLowerCase();
|
|
684
|
+
|
|
685
|
+
if (firstArg === "monitor") {
|
|
686
|
+
console.log(chalk.cyan("📡 Monitoring live Redis commands... (Press Ctrl+C to exit)"));
|
|
687
|
+
cmdArgs.push("monitor");
|
|
688
|
+
} else if (firstArg === "keys") {
|
|
689
|
+
const pattern = redisArgs[1] || "*";
|
|
690
|
+
console.log(chalk.cyan(`🔑 Fetching Redis keys matching '${pattern}'...`));
|
|
691
|
+
cmdArgs.push("keys", pattern);
|
|
692
|
+
} else if (firstArg === "key") {
|
|
693
|
+
const keyName = redisArgs[1];
|
|
694
|
+
if (!keyName) {
|
|
695
|
+
console.error(chalk.red("Error: Please specify a key name. Example: npx blue-bird docker redis key session:123"));
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
console.log(chalk.cyan(`📄 Getting value for Redis key '${keyName}'...`));
|
|
699
|
+
cmdArgs.push("get", keyName);
|
|
700
|
+
} else {
|
|
701
|
+
cmdArgs.push(...redisArgs);
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
console.log(chalk.cyan("Connecting to Redis interactive terminal (redis-cli)..."));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const code = await runCmd("docker", cmdArgs);
|
|
708
|
+
if (code !== 0) {
|
|
709
|
+
console.error(chalk.yellow("Make sure the Redis container is running: npx blue-bird docker start redis"));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
394
712
|
}
|
|
395
713
|
|
|
396
714
|
/**
|
|
@@ -450,23 +768,23 @@ async function main() {
|
|
|
450
768
|
case "pm2":
|
|
451
769
|
await pm2Command(args.slice(1));
|
|
452
770
|
break;
|
|
771
|
+
case "export":
|
|
772
|
+
case "dump":
|
|
773
|
+
await exportDbCommand("none", args[1]);
|
|
774
|
+
break;
|
|
775
|
+
case "import":
|
|
776
|
+
case "restore":
|
|
777
|
+
await importDbCommand("none", args[1]);
|
|
778
|
+
break;
|
|
453
779
|
case "redis":
|
|
454
|
-
await redisCommand();
|
|
780
|
+
await redisCommand(args.slice(1));
|
|
455
781
|
break;
|
|
456
782
|
case "mysql":
|
|
457
783
|
case "postgres":
|
|
458
784
|
case "psql":
|
|
459
|
-
case "db":
|
|
460
|
-
|
|
461
|
-
for (let i = 1; i < args.length; i++) {
|
|
462
|
-
if (args[i] === "-u" || args[i] === "--user") user = args[++i];
|
|
463
|
-
else if (args[i] === "-p" || args[i] === "--password") password = args[++i];
|
|
464
|
-
else if (args[i] === "-d" || args[i] === "--db") db = args[++i];
|
|
465
|
-
else if (args[i] === "--root") root = true;
|
|
466
|
-
}
|
|
467
|
-
await dbClientCommand(user, password, db, root, command);
|
|
785
|
+
case "db":
|
|
786
|
+
await dbClientCommand(args.slice(1), command);
|
|
468
787
|
break;
|
|
469
|
-
}
|
|
470
788
|
case "df":
|
|
471
789
|
case "disk":
|
|
472
790
|
console.log(chalk.cyan("📊 Docker Disk Usage:"));
|
|
@@ -481,7 +799,7 @@ async function main() {
|
|
|
481
799
|
}
|
|
482
800
|
default:
|
|
483
801
|
console.log(chalk.yellow(`Unknown docker command: ${command}`));
|
|
484
|
-
console.log("Available commands: dev, start, stop, build, ps, logs, pm2, mysql/postgres/db, redis, df/disk, prune/clean");
|
|
802
|
+
console.log("Available commands: dev, start, stop, build, ps, logs, pm2, export/dump, import/restore, mysql/postgres/db, redis, df/disk, prune/clean");
|
|
485
803
|
}
|
|
486
804
|
}
|
|
487
805
|
|
package/core/database.js
CHANGED
|
@@ -258,6 +258,142 @@ class Database {
|
|
|
258
258
|
throw err;
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Executes a paginated SQL query.
|
|
264
|
+
* Runs an automatic count query to calculate total records and pages, then appends LIMIT and OFFSET.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} sql - SQL query string.
|
|
267
|
+
* @param {Array} [params=[]] - Query parameters.
|
|
268
|
+
* @param {Object} [options={}] - Pagination options: page, limit, cache.
|
|
269
|
+
* @returns {Promise<{data: Array, total: number, page: number, limit: number, totalPages: number}>}
|
|
270
|
+
* @example const result = await connection.paginate("SELECT * FROM users WHERE status = ?", ["active"], { page: 1, limit: 10 });
|
|
271
|
+
*/
|
|
272
|
+
async paginate(sql, params = [], options = {}) {
|
|
273
|
+
const page = Math.max(1, parseInt(options.page) || 1);
|
|
274
|
+
const limit = Math.max(1, parseInt(options.limit) || 10);
|
|
275
|
+
const offset = (page - 1) * limit;
|
|
276
|
+
|
|
277
|
+
const cleanSql = sql.trim().replace(/;$/, "");
|
|
278
|
+
const countSql = `SELECT COUNT(*) as total FROM (${cleanSql}) as _count_subquery`;
|
|
279
|
+
|
|
280
|
+
const countResult = await this.query(countSql, params, { return_row: true });
|
|
281
|
+
const total = Number(countResult?.total || countResult?.count || 0);
|
|
282
|
+
const totalPages = Math.ceil(total / limit);
|
|
283
|
+
|
|
284
|
+
const paginatedSql = `${cleanSql} LIMIT ${limit} OFFSET ${offset}`;
|
|
285
|
+
const rows = await this.query(paginatedSql, params, options);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
data: Array.isArray(rows) ? rows : [],
|
|
289
|
+
total,
|
|
290
|
+
page,
|
|
291
|
+
limit,
|
|
292
|
+
totalPages,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Executes a database transaction with automatic commit and rollback.
|
|
298
|
+
* @param {Function} callback - Async function receiving transaction client: async (tx) => { ... }
|
|
299
|
+
* @returns {Promise<*>} Value returned from callback.
|
|
300
|
+
* @example
|
|
301
|
+
* const userId = await connection.transaction(async (tx) => {
|
|
302
|
+
* const id = await tx.query("INSERT INTO users (name) VALUES (?)", ["Alice"]);
|
|
303
|
+
* await tx.query("INSERT INTO profiles (user_id) VALUES (?)", [id]);
|
|
304
|
+
* return id;
|
|
305
|
+
* });
|
|
306
|
+
*/
|
|
307
|
+
async transaction(callback) {
|
|
308
|
+
if (!mysqlPromise && !pgPromise) throw new Error("[DATABASE ERROR] No database driver available.");
|
|
309
|
+
if (!this.pool) {
|
|
310
|
+
const initialized = await this.init();
|
|
311
|
+
if (!initialized) throw new Error("[DATABASE ERROR] Failed to initialize database pool.");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (this.type === "postgres" && pgPromise) {
|
|
315
|
+
const client = await this.pool.connect();
|
|
316
|
+
try {
|
|
317
|
+
await client.query("BEGIN");
|
|
318
|
+
|
|
319
|
+
const tx = {
|
|
320
|
+
query: async (sql, params = [], options = {}) => {
|
|
321
|
+
const queryOptions = typeof options === "string" ? { [options]: true } : options;
|
|
322
|
+
const cleanSql = sql.trim();
|
|
323
|
+
const isSelect = cleanSql.toLowerCase().startsWith("select");
|
|
324
|
+
const isInsert = cleanSql.toLowerCase().startsWith("insert");
|
|
325
|
+
|
|
326
|
+
let paramIndex = 1;
|
|
327
|
+
const pgSql = cleanSql.replace(/\?/g, () => `$${paramIndex++}`);
|
|
328
|
+
const res = await client.query(pgSql, params);
|
|
329
|
+
|
|
330
|
+
if (isSelect) {
|
|
331
|
+
const rows = res.rows || [];
|
|
332
|
+
return queryOptions.return_row ? (rows[0] || null) : rows;
|
|
333
|
+
}
|
|
334
|
+
if (isInsert) {
|
|
335
|
+
if (res.rows && res.rows.length > 0) return res.rows[0].id || res.rows[0];
|
|
336
|
+
return res.rowCount;
|
|
337
|
+
}
|
|
338
|
+
return res.rowCount;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const result = await callback(tx);
|
|
343
|
+
await client.query("COMMIT");
|
|
344
|
+
return result;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
347
|
+
console.error("[DATABASE ERROR] Transaction rolled back:", err.message);
|
|
348
|
+
throw err;
|
|
349
|
+
} finally {
|
|
350
|
+
client.release();
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
const connection = await this.pool.getConnection();
|
|
354
|
+
try {
|
|
355
|
+
await connection.beginTransaction();
|
|
356
|
+
|
|
357
|
+
const tx = {
|
|
358
|
+
query: async (sql, params = [], options = {}) => {
|
|
359
|
+
const queryOptions = typeof options === "string" ? { [options]: true } : options;
|
|
360
|
+
const cleanSql = sql.trim();
|
|
361
|
+
const isSelect = cleanSql.toLowerCase().startsWith("select");
|
|
362
|
+
const isInsert = cleanSql.toLowerCase().startsWith("insert");
|
|
363
|
+
|
|
364
|
+
const [results] = await connection.execute(cleanSql, params);
|
|
365
|
+
|
|
366
|
+
if (isSelect) {
|
|
367
|
+
const rows = Array.isArray(results) ? results : [];
|
|
368
|
+
return queryOptions.return_row ? (rows[0] || null) : rows;
|
|
369
|
+
}
|
|
370
|
+
if (isInsert) {
|
|
371
|
+
return results.insertId || results;
|
|
372
|
+
}
|
|
373
|
+
return results;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const result = await callback(tx);
|
|
378
|
+
await connection.commit();
|
|
379
|
+
return result;
|
|
380
|
+
} catch (err) {
|
|
381
|
+
await connection.rollback().catch(() => {});
|
|
382
|
+
console.error("[DATABASE ERROR] Transaction rolled back:", err.message);
|
|
383
|
+
throw err;
|
|
384
|
+
} finally {
|
|
385
|
+
connection.release();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Alias for transaction().
|
|
392
|
+
* @param {Function} callback
|
|
393
|
+
*/
|
|
394
|
+
async executeTransaction(callback) {
|
|
395
|
+
return this.transaction(callback);
|
|
396
|
+
}
|
|
261
397
|
}
|
|
262
398
|
|
|
263
399
|
export { Database, DB_TYPE };
|