@seip/blue-bird 0.8.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/blue-bird.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)
@@ -15,6 +15,25 @@ Blue Bird is a powerful, performance-first API framework built on Express. It fe
15
15
 
16
16
  ---
17
17
 
18
+ ## 🕊️ The Blue Bird Philosophy
19
+
20
+ Stop wasting time configuring CORS, security headers, database connections, and authentication flows. Blue Bird provides an opinionated, highly efficient core structure allowing you to focus on writing clean business logic. Nginx handles the static frontend directly from the filesystem, Express handles the backend APIs. Includes a preconfigured Docker stack with Nginx reverse proxy, Redis cache, and PM2 cluster scaling.
21
+
22
+ ### 🧩 Decoupled Architecture
23
+ Nginx natively serves static frontend assets and extensionless HTML pages from `frontend/`. Express owns the API layer, keeping your backend entirely focused on performance and logic.
24
+
25
+ - **Cross-Platform Versatility**: Because the Express backend API is fully decoupled from the HTML/JS/CSS frontend layer, developers can easily build and maintain multiple application targets pointing to the same core API:
26
+ - **Web Applications**: Static HTML, CSS, and client-side JS served natively by Nginx.
27
+ - **Mobile Applications**: Powered by **Capacitor**, Cordova, or React Native.
28
+ - **Desktop Applications**: Built with **Electron** or **Tauri**.
29
+ - **Strong Business Logic**: Enforces a strict separation of concerns — Nginx excels at ultra-fast static file delivery and public assets, while Express handles API routing, business rules, validation, and data operations without UI rendering overhead.
30
+ - **Lightweight Footprint**: Offloading static assets to Nginx optimizes Node.js event loop performance, resulting in extremely minimal RAM, CPU, and disk consumption under high concurrent workloads.
31
+
32
+ ### 🐳 Built-in Orchestration
33
+ Includes a comprehensive Docker Compose CLI wrapper to bootstrap, build, stop, and clean dev and production environments with zero manual scripting.
34
+
35
+ ---
36
+
18
37
  ## 🚀 Key Features / Características Clave
19
38
 
20
39
  - All-In-One: Pre-configured Express API server with JSON, URL encoding, Cookies, and CORS.
@@ -76,7 +95,34 @@ project/
76
95
 
77
96
  ## 📖 Core Modules Documentation / Documentación de Módulos
78
97
 
79
- ### 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`)
80
126
 
81
127
  Do not use Express' native router. Always use Blue Bird's wrapper class:
82
128
 
@@ -116,57 +162,86 @@ routerApi.post("/users", validateUser.middleware(), (req, res) => {
116
162
 
117
163
  ---
118
164
 
119
- ### 4. JWT Authentication (`Auth`)
165
+ ### 4. JWT Authentication & Redis Sessions (`Auth`)
120
166
 
121
- 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.
122
168
 
123
169
  #### Protecting Routes
124
170
 
125
171
  ```javascript
126
172
  import Auth from "@seip/blue-bird/core/auth.js";
127
173
 
128
- // Secure API endpoint (returns 401 on failure)
174
+ // 1. Secure API endpoint (returns 401 JSON on failure)
129
175
  router.get("/profile", Auth.protect(), (req, res) => {
130
176
  res.json({ user: req.user });
131
177
  });
132
178
 
133
- // Secure web page (redirects to /login on failure)
134
- router.get("/dashboard", Auth.protect({ redirect: "/login" }), (req, res) => {
135
- 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>`);
136
182
  });
137
183
  ```
138
184
 
139
- #### Authentication Sessions
185
+ #### Authentication Sessions & Utilities
140
186
 
141
187
  ```javascript
188
+ // Login & Sync Session state in Redis (if active)
142
189
  router.post("/login", async (req, res) => {
143
- const user = { id: 1, name: "John Doe" };
144
- await Auth.login(res, user);
190
+ const user = { id: 1, name: "John Doe", role: "admin" };
191
+ await Auth.login(res, user, "auth", { expiresIn: "7d" });
145
192
  res.json({ message: "Logged in successfully" });
146
193
  });
147
194
 
195
+ // Logout & Delete Session from Redis
148
196
  router.post("/logout", async (req, res) => {
149
- await Auth.logout(res);
197
+ await Auth.logout(res, "auth", {}, req);
150
198
  res.json({ message: "Logged out" });
151
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);
152
206
  ```
153
207
 
154
208
  ---
155
209
 
156
- ### 5. Performance Cache Middleware (`Cache`)
210
+ ### 5. Performance Cache & Redis Client (`Cache`)
157
211
 
158
- Applies caching at the route handler level. Automatically caches JSON payloads (`res.json`) and rendered outputs (`res.send`).
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.
213
+
214
+ #### Route Caching Middleware
159
215
 
160
216
  ```javascript
161
- import Cache from "@seip/blue-bird/core/cache.js";
217
+ import Cache, { getRedisClient } from "@seip/blue-bird/core/cache.js";
162
218
 
163
- // Cache endpoint for 60 seconds
219
+ // Cache endpoint for 60 seconds (sets X-Blue-Bird-Cache: HIT/MISS headers)
164
220
  router.get("/stats", Cache.middleware(60), (req, res) => {
165
221
  res.json({ usersOnline: 42 });
166
222
  });
167
223
  ```
168
224
 
169
- 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
+ ```
170
245
 
171
246
  ---
172
247
 
@@ -250,8 +325,17 @@ npx blue-bird docker <command> [options]
250
325
  - **`npx blue-bird docker ps`**: Lists running project containers and ports.
251
326
  - **`npx blue-bird docker logs [app|db|postgres|mysql]`**: Tails logs for the specified container.
252
327
  - **`npx blue-bird docker pm2 [args]`**: Runs PM2 commands inside the Node.js application container (e.g. `status`, `monit`, `reload all`).
253
- - **`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`.
254
- - **`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.
255
339
  - **`npx blue-bird docker prune`**: Safely clears orphaned volumes, dangling build caches, and images.
256
340
 
257
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/core/cli/init.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
@@ -1,6 +1,6 @@
1
- services:
2
- app:
3
- command: ["npm", "run", "dev"]
4
- environment:
5
- - NODE_ENV=development
6
- - DEBUG=true
1
+ services:
2
+ app:
3
+ command: ["npm", "run", "dev"]
4
+ environment:
5
+ - NODE_ENV=development
6
+ - DEBUG=true
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
 
@@ -10,7 +10,7 @@
10
10
  visual elegance and performance with raw HTML and CSS.">
11
11
  <meta name="keywords" content="">
12
12
  <meta name="author" content="Seip25">
13
- <link rel="icon" href="/favicon.ico" />
13
+ <link rel="icon" href="/images/favicon.ico" />
14
14
  </head>
15
15
 
16
16
  <body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
@@ -56,10 +56,13 @@
56
56
  <article class="p-8 rounded-2xl bg-slate-900/40 border border-white/5 shadow-xl">
57
57
  <h2 class="text-2xl font-bold text-white mb-4">Our Philosophy</h2>
58
58
  <p class="text-slate-400 leading-relaxed mb-4">
59
- Blue Bird was born out of a desire for a clean, performance-first approach to web development. We believe in harnessing the raw power of Express.js and Nginx, stripping away unnecessary bloat, and providing developers with a robust foundation that just works.
59
+ Blue Bird was born out of a desire for a clean, performance-first approach to web development. We
60
+ believe in harnessing the raw power of Express.js and Nginx, stripping away unnecessary bloat, and
61
+ providing developers with a robust foundation that just works.
60
62
  </p>
61
63
  <p class="text-slate-400 leading-relaxed">
62
- By separating the static frontend delivery (handled blazingly fast by Nginx) from the dynamic API layer (powered by Express and Redis), Blue Bird achieves unparalleled performance out of the box.
64
+ By separating the static frontend delivery (handled blazingly fast by Nginx) from the dynamic API
65
+ layer (powered by Express and Redis), Blue Bird achieves unparalleled performance out of the box.
63
66
  </p>
64
67
  </article>
65
68
 
@@ -76,11 +79,13 @@
76
79
  </li>
77
80
  <li class="flex items-start">
78
81
  <span class="text-blue-500 mr-2">✓</span>
79
- <span><strong>High Performance:</strong> Redis caching for the data layer and Nginx static delivery.</span>
82
+ <span><strong>High Performance:</strong> Redis caching for the data layer and Nginx static
83
+ delivery.</span>
80
84
  </li>
81
85
  <li class="flex items-start">
82
86
  <span class="text-blue-500 mr-2">✓</span>
83
- <span><strong>Pure HTML/CSS:</strong> No bloated frontend frameworks. Write code close to the metal.</span>
87
+ <span><strong>Pure HTML/CSS:</strong> No bloated frontend frameworks. Write code close to the
88
+ metal.</span>
84
89
  </li>
85
90
  </ul>
86
91
  </article>
@@ -92,7 +97,7 @@
92
97
  <p>Powered by Blue Bird Framework. All rights reserved.</p>
93
98
  </div>
94
99
  </footer>
95
- <script src="/js/bundle.js"></script>
100
+ <script src="/js/tailwind.js"></script>
96
101
  </body>
97
102
 
98
103
  </html>
@@ -10,7 +10,7 @@
10
10
  visual elegance and performance with raw HTML and CSS.">
11
11
  <meta name="keywords" content="">
12
12
  <meta name="author" content="Seip25">
13
- <link rel="icon" href="/favicon.ico" />
13
+ <link rel="icon" href="/images/favicon.ico" />
14
14
  </head>
15
15
 
16
16
  <body class="min-h-screen bg-slate-950 text-white font-sans antialiased selection:bg-blue-500 selection:text-white">
@@ -135,7 +135,7 @@
135
135
  <p>Powered by Blue Bird Framework. All rights reserved.</p>
136
136
  </div>
137
137
  </footer>
138
- <script src="/js/bundle.js"></script>
138
+ <script src="/js/tailwind.js"></script>
139
139
  </body>
140
140
 
141
141
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.8.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": {
File without changes
File without changes
File without changes