@seip/blue-bird 0.9.0 → 0.9.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **High-Performance Express Framework — Built for Speed, Caching, and Visual Excellence**
4
4
 
5
- ![Blue Bird Logo](https://seip25.github.io/Blue-bird/favicon.png)
5
+ ![Blue Bird Logo](https://seip25.github.io/Blue-bird/favicon.ico)
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@seip/blue-bird.svg)](https://www.npmjs.com/package/@seip/blue-bird)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -95,7 +95,34 @@ project/
95
95
 
96
96
  ## 📖 Core Modules Documentation / Documentación de Módulos
97
97
 
98
- ### 1. Routing (`Router`)
98
+ ### 1. Application Class (`App`)
99
+
100
+ Initializes the Express server. If Docker/Nginx is not used (or for lightweight setups like Express + SQLite), you can configure Express to serve static frontend files directly via the `static` parameter:
101
+
102
+ ```javascript
103
+ import App from "@seip/blue-bird/core/app.js";
104
+ import routerApi from "./backend/routes/api.js";
105
+
106
+ const app = new App({
107
+ port: process.env.PORT || 3000,
108
+ host: "http://localhost",
109
+ routes: [routerApi],
110
+ cors: [],
111
+ middlewares: [],
112
+ logger: false,
113
+ // Standalone Express Static Asset Serving (No Nginx/Docker required)
114
+ static: {
115
+ path: "../frontend", // relative directory path to frontend files
116
+ options: {} // express.static options
117
+ }
118
+ });
119
+
120
+ app.run();
121
+ ```
122
+
123
+ ---
124
+
125
+ ### 2. Routing (`Router`)
99
126
 
100
127
  Do not use Express' native router. Always use Blue Bird's wrapper class:
101
128
 
@@ -135,57 +162,86 @@ routerApi.post("/users", validateUser.middleware(), (req, res) => {
135
162
 
136
163
  ---
137
164
 
138
- ### 4. JWT Authentication (`Auth`)
165
+ ### 4. JWT Authentication & Redis Sessions (`Auth`)
139
166
 
140
- Secure user sessions using stateless AES-256-GCM encrypted JWTs stored in secure HTTP-Only cookies.
167
+ Secure user authentication with AES-256-GCM encrypted tokens. Transmitted via HTTP-Only cookies or `Authorization` headers, with optional Redis session storage and invalidation.
141
168
 
142
169
  #### Protecting Routes
143
170
 
144
171
  ```javascript
145
172
  import Auth from "@seip/blue-bird/core/auth.js";
146
173
 
147
- // Secure API endpoint (returns 401 on failure)
174
+ // 1. Secure API endpoint (returns 401 JSON on failure)
148
175
  router.get("/profile", Auth.protect(), (req, res) => {
149
176
  res.json({ user: req.user });
150
177
  });
151
178
 
152
- // Secure web page (redirects to /login on failure)
153
- router.get("/dashboard", Auth.protect({ redirect: "/login" }), (req, res) => {
154
- Template.render(res, "dashboard");
179
+ // 2. Secure web page (redirects to /login on failure)
180
+ router.get("/dashboard", Auth.protect({ redirect: "/login", key: "user", cookieKey: "auth" }), (req, res) => {
181
+ res.send(`<h1>Welcome ${req.user.name}</h1>`);
155
182
  });
156
183
  ```
157
184
 
158
- #### Authentication Sessions
185
+ #### Authentication Sessions & Utilities
159
186
 
160
187
  ```javascript
188
+ // Login & Sync Session state in Redis (if active)
161
189
  router.post("/login", async (req, res) => {
162
- const user = { id: 1, name: "John Doe" };
163
- await Auth.login(res, user);
190
+ const user = { id: 1, name: "John Doe", role: "admin" };
191
+ await Auth.login(res, user, "auth", { expiresIn: "7d" });
164
192
  res.json({ message: "Logged in successfully" });
165
193
  });
166
194
 
195
+ // Logout & Delete Session from Redis
167
196
  router.post("/logout", async (req, res) => {
168
- await Auth.logout(res);
197
+ await Auth.logout(res, "auth", {}, req);
169
198
  res.json({ message: "Logged out" });
170
199
  });
200
+
201
+ // Manual Encrypted JWT Tokens & AES-256-GCM Encryption
202
+ const token = Auth.generateToken({ id: 1 }, process.env.JWT_SECRET, "2h");
203
+ const decoded = Auth.verifyToken(token, process.env.JWT_SECRET);
204
+ const encrypted = Auth.encrypt({ secret: "1234" }, process.env.JWT_SECRET);
205
+ const decrypted = Auth.decrypt(encrypted, process.env.JWT_SECRET);
171
206
  ```
172
207
 
173
208
  ---
174
209
 
175
- ### 5. Performance Cache Middleware (`Cache`)
210
+ ### 5. Performance Cache & Redis Client (`Cache`)
211
+
212
+ Applies route-level response caching for JSON payloads (`res.json`) and HTML output (`res.send`). Automatically uses Redis when `REDIS_HOST` is configured, and transparently degrades to an in-memory cache if Redis is unavailable or offline.
176
213
 
177
- Applies caching at the route handler level. Automatically caches JSON payloads (`res.json`) and rendered outputs (`res.send`).
214
+ #### Route Caching Middleware
178
215
 
179
216
  ```javascript
180
- import Cache from "@seip/blue-bird/core/cache.js";
217
+ import Cache, { getRedisClient } from "@seip/blue-bird/core/cache.js";
181
218
 
182
- // Cache endpoint for 60 seconds
219
+ // Cache endpoint for 60 seconds (sets X-Blue-Bird-Cache: HIT/MISS headers)
183
220
  router.get("/stats", Cache.middleware(60), (req, res) => {
184
221
  res.json({ usersOnline: 42 });
185
222
  });
186
223
  ```
187
224
 
188
- Integrates with Redis if `REDIS_HOST` is defined in the environment. Falls back to an in-memory cache automatically if Redis is not configured or not running.
225
+ #### Custom Database & Data Caching with `getRedisClient()`
226
+
227
+ ```javascript
228
+ // Direct access to the active Redis client for database query or custom key caching
229
+ router.get("/custom-cache", async (req, res) => {
230
+ const redis = getRedisClient();
231
+ if (redis) {
232
+ const cached = await redis.get("my_custom_key");
233
+ if (cached) return res.json(JSON.parse(cached));
234
+
235
+ const dbData = await fetchHeavyDataFromDB();
236
+ await redis.set("my_custom_key", JSON.stringify(dbData), { EX: 120 }); // Expiry 120s
237
+ return res.json(dbData);
238
+ }
239
+
240
+ // Fallback if Redis is disabled
241
+ const dbData = await fetchHeavyDataFromDB();
242
+ res.json(dbData);
243
+ });
244
+ ```
189
245
 
190
246
  ---
191
247
 
@@ -269,8 +325,17 @@ npx blue-bird docker <command> [options]
269
325
  - **`npx blue-bird docker ps`**: Lists running project containers and ports.
270
326
  - **`npx blue-bird docker logs [app|db|postgres|mysql]`**: Tails logs for the specified container.
271
327
  - **`npx blue-bird docker pm2 [args]`**: Runs PM2 commands inside the Node.js application container (e.g. `status`, `monit`, `reload all`).
272
- - **`npx blue-bird docker db`** (or `psql` / `mysql`): Connects into the container's interactive database shell (`psql` for PostgreSQL, `mysql` for MySQL) using credentials from `.env`.
273
- - **`npx blue-bird docker redis`**: Connects into the container's interactive Redis CLI terminal.
328
+ - **`npx blue-bird docker db`** (or `psql` / `mysql`): Connects into the container's interactive database shell (`psql` for PostgreSQL, `mysql` for MySQL). Supports smart table queries, schema inspection, and backups:
329
+ - `npx blue-bird docker mysql users` -> executes `SELECT * FROM users;` formatted as ASCII table.
330
+ - `npx blue-bird docker mysql users --limit=10 --where="id > 5"` -> executes filtered query.
331
+ - `npx blue-bird docker mysql tables` -> lists database tables (`SHOW TABLES`).
332
+ - `npx blue-bird docker mysql columns users` -> describes table schema (`SHOW COLUMNS`).
333
+ - **`npx blue-bird docker export [filename.sql]`** (or `npx blue-bird docker mysql export`): Dumps database schema and data into `backups/backup_YYYY-MM-DD.sql` (creates `backups/` folder automatically).
334
+ - **`npx blue-bird docker import [filename.sql]`** (or `npx blue-bird docker mysql import`): Restores database from a `.sql` file in `backups/` (uses the most recent `.sql` backup if no filename is specified).
335
+ - **`npx blue-bird docker redis`**: Connects into the container's interactive Redis CLI terminal. Supports smart subcommands:
336
+ - `npx blue-bird docker redis monitor` -> live stream of all incoming Redis commands.
337
+ - `npx blue-bird docker redis keys [pattern]` -> lists all matching Redis keys (defaults to `*`).
338
+ - `npx blue-bird docker redis key <keyname>` -> gets value for specific Redis key.
274
339
  - **`npx blue-bird docker prune`**: Safely clears orphaned volumes, dangling build caches, and images.
275
340
 
276
341
  ---
@@ -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
- async function dbClientCommand(userOpt, passOpt, dbOpt, rootOpt, explicitService) {
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
- if (dbType === "postgres") {
295
- if (rootOpt) {
296
- dbUser = "postgres";
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
- dbUser = dbUser || env.DB_USER || "postgres";
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
- dbName = dbName || env.DB_NAME || "blue_bird";
380
+ }
301
381
 
302
- const targetDb = dbName ? ` (database: ${dbName})` : "";
303
- console.log(chalk.cyan(`Connecting to PostgreSQL shell (psql) in container as '${dbUser}'${targetDb}...`));
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
- const targetDb = dbName ? ` (database: ${dbName})` : "";
333
- console.log(chalk.cyan(`Connecting to MySQL shell in container as '${dbUser}'${targetDb}...`));
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
- await runCmd("docker", cmdArgs);
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
- let user, password, db, root = false;
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/docker/nginx.conf CHANGED
@@ -66,6 +66,7 @@ http {
66
66
  }
67
67
 
68
68
  location / {
69
+ add_header Cache-Control "no-cache";
69
70
  try_files $uri $uri.html $uri/ @node_app;
70
71
  }
71
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Express opinionated API framework with built-in JWT auth, validation, and caching",
5
5
  "type": "module",
6
6
  "exports": {