@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 +84 -19
- package/core/cli/docker.js +343 -25
- package/docker/nginx.conf +1 -0
- package/package.json +1 -1
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
|
-

|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@seip/blue-bird)
|
|
8
8
|
[](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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
273
|
-
-
|
|
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
|
---
|
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/docker/nginx.conf
CHANGED