@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 +103 -19
- package/core/cli/docker.js +343 -25
- package/core/cli/init.js +1 -1
- package/docker/docker-compose.dev.yml +6 -6
- package/docker/nginx.conf +1 -0
- package/frontend/about.html +11 -6
- package/frontend/index.html +2 -2
- package/package.json +1 -1
- package/frontend/css/app.css +0 -0
- /package/frontend/{favicon.ico → images/favicon.ico} +0 -0
- /package/frontend/js/{bundle.js → tailwind.js} +0 -0
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)
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
|
210
|
+
### 5. Performance Cache & Redis Client (`Cache`)
|
|
157
211
|
|
|
158
|
-
Applies
|
|
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
|
-
|
|
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)
|
|
254
|
-
-
|
|
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
|
---
|
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/cli/init.js
CHANGED
|
@@ -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
package/frontend/about.html
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
100
|
+
<script src="/js/tailwind.js"></script>
|
|
96
101
|
</body>
|
|
97
102
|
|
|
98
103
|
</html>
|
package/frontend/index.html
CHANGED
|
@@ -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/
|
|
138
|
+
<script src="/js/tailwind.js"></script>
|
|
139
139
|
</body>
|
|
140
140
|
|
|
141
141
|
</html>
|
package/package.json
CHANGED
package/frontend/css/app.css
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|