@seip/blue-bird 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -20
- package/core/app.js +96 -15
- package/core/cli/docker.js +343 -25
- package/core/database.js +136 -0
- package/core/index.d.ts +132 -0
- package/core/router.js +33 -8
- package/core/ws.js +210 -0
- package/docker/nginx.conf +1 -0
- package/package.json +3 -1
package/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
|
|
|
@@ -237,11 +293,81 @@ const stats = await connection.query("SELECT COUNT(*) as count FROM access_logs"
|
|
|
237
293
|
|
|
238
294
|
// 3. INSERT query (returns insertId for MySQL, or inserted row ID / rowCount for PostgreSQL)
|
|
239
295
|
const newId = await connection.query("INSERT INTO users (name) VALUES (?)", ["John"]);
|
|
296
|
+
|
|
297
|
+
// 4. Automatic SQL Query Pagination (Runs count query + LIMIT/OFFSET calculation)
|
|
298
|
+
const paginated = await connection.paginate(
|
|
299
|
+
"SELECT * FROM users WHERE status = ?",
|
|
300
|
+
["active"],
|
|
301
|
+
{ page: 1, limit: 10, cache: 60 }
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// 5. Atomic Database Transactions with Automatic Commit & Rollback
|
|
305
|
+
const txUserId = await connection.transaction(async (tx) => {
|
|
306
|
+
const userId = await tx.query("INSERT INTO users (name, email) VALUES (?, ?)", ["Alice", "alice@example.com"]);
|
|
307
|
+
await tx.query("INSERT INTO profiles (user_id) VALUES (?)", [userId]);
|
|
308
|
+
return userId;
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
### 8. Real-Time WebSockets Engine (`WebSocketManager`)
|
|
315
|
+
|
|
316
|
+
Built-in high-performance vanilla WebSocket server (`ws`) sharing the **exact same HTTP server and port as Express** (port 3000), supporting room subscriptions, JWT authentication, 30s heartbeat ping/pong, and Redis Pub/Sub cluster synchronization:
|
|
317
|
+
|
|
318
|
+
```javascript
|
|
319
|
+
import App from "@seip/blue-bird/core/app.js";
|
|
320
|
+
|
|
321
|
+
const app = new App({ ... });
|
|
322
|
+
|
|
323
|
+
// 1. Initialize WebSocket server on route /ws with optional JWT Auth check
|
|
324
|
+
const ws = app.websocket({ path: "/ws", auth: true });
|
|
325
|
+
|
|
326
|
+
ws.onConnection((socket, req) => {
|
|
327
|
+
socket.join("lobby");
|
|
328
|
+
socket.sendJSON({ status: "connected", user: socket.user });
|
|
329
|
+
|
|
330
|
+
socket.on("message", (raw) => {
|
|
331
|
+
// Broadcast to room (synced across PM2 cluster via Redis Pub/Sub)
|
|
332
|
+
ws.broadcast({ room: "lobby", text: raw.toString() }, "lobby");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
app.run();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Broadcasting from Express API Routes:
|
|
340
|
+
```javascript
|
|
341
|
+
router.post("/api/comments", async (req, res) => {
|
|
342
|
+
// 1. Save comment into database...
|
|
343
|
+
const comment = { id: 1, text: req.body.text };
|
|
344
|
+
|
|
345
|
+
// 2. Broadcast in real time to all WebSocket clients in the "lobby" room
|
|
346
|
+
app.wsManager.broadcast({ type: "NEW_COMMENT", data: comment }, "lobby");
|
|
347
|
+
|
|
348
|
+
return res.success(comment, "Comment created");
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### Native Client Connection (Browser & Node.js v22+):
|
|
353
|
+
```javascript
|
|
354
|
+
// Native W3C Standard WebSocket Client (Browser & Node.js v22+)
|
|
355
|
+
const socket = new WebSocket("ws://localhost:3000/ws");
|
|
356
|
+
|
|
357
|
+
socket.addEventListener("open", () => {
|
|
358
|
+
console.log("Connected to WebSocket server!");
|
|
359
|
+
socket.send("Hello server from native client!");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
socket.addEventListener("message", (event) => {
|
|
363
|
+
const data = JSON.parse(event.data);
|
|
364
|
+
console.log("Message received from server:", data);
|
|
365
|
+
});
|
|
240
366
|
```
|
|
241
367
|
|
|
242
368
|
---
|
|
243
369
|
|
|
244
|
-
###
|
|
370
|
+
### 9. Nginx Static Asset Caching
|
|
245
371
|
|
|
246
372
|
Nginx is configured to explicitly cache static assets (`.js`, `.css`, `.jpg`, `.png`, etc.) in the user's browser with the `Cache-Control` header (valid for 1 month). HTML and API endpoints (`/api/*`) are not cached by Nginx to ensure they serve dynamic and up-to-date content, relying instead on the Node.js application and Redis for data-layer caching.
|
|
247
373
|
|
|
@@ -269,8 +395,17 @@ npx blue-bird docker <command> [options]
|
|
|
269
395
|
- **`npx blue-bird docker ps`**: Lists running project containers and ports.
|
|
270
396
|
- **`npx blue-bird docker logs [app|db|postgres|mysql]`**: Tails logs for the specified container.
|
|
271
397
|
- **`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
|
-
-
|
|
398
|
+
- **`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:
|
|
399
|
+
- `npx blue-bird docker mysql users` -> executes `SELECT * FROM users;` formatted as ASCII table.
|
|
400
|
+
- `npx blue-bird docker mysql users --limit=10 --where="id > 5"` -> executes filtered query.
|
|
401
|
+
- `npx blue-bird docker mysql tables` -> lists database tables (`SHOW TABLES`).
|
|
402
|
+
- `npx blue-bird docker mysql columns users` -> describes table schema (`SHOW COLUMNS`).
|
|
403
|
+
- **`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).
|
|
404
|
+
- **`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).
|
|
405
|
+
- **`npx blue-bird docker redis`**: Connects into the container's interactive Redis CLI terminal. Supports smart subcommands:
|
|
406
|
+
- `npx blue-bird docker redis monitor` -> live stream of all incoming Redis commands.
|
|
407
|
+
- `npx blue-bird docker redis keys [pattern]` -> lists all matching Redis keys (defaults to `*`).
|
|
408
|
+
- `npx blue-bird docker redis key <keyname>` -> gets value for specific Redis key.
|
|
274
409
|
- **`npx blue-bird docker prune`**: Safely clears orphaned volumes, dangling build caches, and images.
|
|
275
410
|
|
|
276
411
|
---
|
package/core/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import http from "node:http";
|
|
1
2
|
import express from "express";
|
|
2
3
|
import cors from "cors";
|
|
3
4
|
import path from "path";
|
|
@@ -10,6 +11,7 @@ import compression from "compression";
|
|
|
10
11
|
import Config from "./config.js";
|
|
11
12
|
import Logger from "./logger.js";
|
|
12
13
|
import Debug from "./debug.js";
|
|
14
|
+
import WebSocketManager from "./ws.js";
|
|
13
15
|
|
|
14
16
|
const __dirname = Config.dirname();
|
|
15
17
|
const props = Config.props();
|
|
@@ -58,6 +60,8 @@ class App {
|
|
|
58
60
|
*/
|
|
59
61
|
constructor(options = {}) {
|
|
60
62
|
this.app = express();
|
|
63
|
+
this.server = http.createServer(this.app);
|
|
64
|
+
this.wsManager = null;
|
|
61
65
|
this.routes = options.routes || [];
|
|
62
66
|
this.cors = options.cors || {};
|
|
63
67
|
this.middlewares = options.middlewares || [];
|
|
@@ -114,6 +118,42 @@ class App {
|
|
|
114
118
|
this.app.use((req, res, next) => {
|
|
115
119
|
req.lang = req.query?.lang || req.body?.lang || req.cookies?.lang || "en";
|
|
116
120
|
res.locals.lang = req.lang;
|
|
121
|
+
|
|
122
|
+
res.success = (data = null, message = "Success", statusCode = 200) => {
|
|
123
|
+
return res.status(statusCode).json({
|
|
124
|
+
status: "success",
|
|
125
|
+
message,
|
|
126
|
+
data,
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
res.error = (message = "Error", statusCode = 400, errors = []) => {
|
|
131
|
+
return res.status(statusCode).json({
|
|
132
|
+
status: "error",
|
|
133
|
+
message,
|
|
134
|
+
errors,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
res.paginate = (data = [], pagination = {}, message = "Success") => {
|
|
139
|
+
const page = Number(pagination.page) || 1;
|
|
140
|
+
const limit = Number(pagination.limit) || data.length;
|
|
141
|
+
const total = Number(pagination.total) || data.length;
|
|
142
|
+
const totalPages = limit > 0 ? Math.ceil(total / limit) : 1;
|
|
143
|
+
|
|
144
|
+
return res.status(200).json({
|
|
145
|
+
status: "success",
|
|
146
|
+
message,
|
|
147
|
+
data,
|
|
148
|
+
pagination: {
|
|
149
|
+
page,
|
|
150
|
+
limit,
|
|
151
|
+
total,
|
|
152
|
+
totalPages,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
117
157
|
next();
|
|
118
158
|
});
|
|
119
159
|
|
|
@@ -241,25 +281,23 @@ class App {
|
|
|
241
281
|
*/
|
|
242
282
|
_errorHandler() {
|
|
243
283
|
this.app.use((err, req, res, next) => {
|
|
244
|
-
const
|
|
284
|
+
const statusCode = err.statusCode || err.status || 500;
|
|
245
285
|
const message = err.message || "Internal Server Error";
|
|
286
|
+
const errors = err.errors || [];
|
|
246
287
|
|
|
247
|
-
this.loggerInstance.error(`[${
|
|
288
|
+
this.loggerInstance.error(`[${statusCode}] ${message} - ${err.stack}`);
|
|
248
289
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
290
|
+
const responsePayload = {
|
|
291
|
+
status: "error",
|
|
292
|
+
message: statusCode === 500 && !props.debug ? "Internal Server Error" : message,
|
|
293
|
+
errors,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
if (props.debug && err.stack) {
|
|
297
|
+
responsePayload.stack = err.stack;
|
|
256
298
|
}
|
|
257
299
|
|
|
258
|
-
return res.status(
|
|
259
|
-
success: false,
|
|
260
|
-
error: true,
|
|
261
|
-
message: status === 500 ? "Internal Server Error" : message,
|
|
262
|
-
});
|
|
300
|
+
return res.status(statusCode).json(responsePayload);
|
|
263
301
|
});
|
|
264
302
|
}
|
|
265
303
|
|
|
@@ -295,7 +333,7 @@ class App {
|
|
|
295
333
|
run() {
|
|
296
334
|
this._ready
|
|
297
335
|
.then(() => {
|
|
298
|
-
this.
|
|
336
|
+
this.server.listen(this.port, () => {
|
|
299
337
|
console.log(
|
|
300
338
|
chalk.bold.blue("Blue Bird Server Online\n") +
|
|
301
339
|
chalk.bold.cyan("App URL: ") +
|
|
@@ -318,6 +356,29 @@ class App {
|
|
|
318
356
|
});
|
|
319
357
|
}
|
|
320
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Initializes and returns the WebSocket manager attached to the Express HTTP server instance.
|
|
361
|
+
* @param {Function|Object} [options] - Connection callback handler: (ws, req) => {} or options object.
|
|
362
|
+
* @returns {WebSocketManager}
|
|
363
|
+
* @example
|
|
364
|
+
* app.websocket((ws, req) => {
|
|
365
|
+
* ws.join("chat");
|
|
366
|
+
* ws.sendJSON({ message: "Welcome to Blue Bird WebSockets" });
|
|
367
|
+
* });
|
|
368
|
+
*/
|
|
369
|
+
websocket(options = {}) {
|
|
370
|
+
const handler = typeof options === "function" ? options : null;
|
|
371
|
+
const wsOptions = typeof options === "object" && options !== null ? options : {};
|
|
372
|
+
|
|
373
|
+
if (!this.wsManager) {
|
|
374
|
+
this.wsManager = new WebSocketManager(this.server, wsOptions);
|
|
375
|
+
}
|
|
376
|
+
if (handler) {
|
|
377
|
+
this.wsManager.onConnection(handler);
|
|
378
|
+
}
|
|
379
|
+
return this.wsManager;
|
|
380
|
+
}
|
|
381
|
+
|
|
321
382
|
/**
|
|
322
383
|
* Returns a pre-configured Helmet middleware for use on specific routers.
|
|
323
384
|
* @param {Object} [options={}] - Helmet options to override defaults.
|
|
@@ -335,4 +396,24 @@ class App {
|
|
|
335
396
|
}
|
|
336
397
|
}
|
|
337
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Operational application error class for standardizing custom API errors.
|
|
401
|
+
*/
|
|
402
|
+
export class AppError extends Error {
|
|
403
|
+
/**
|
|
404
|
+
* Creates an AppError instance.
|
|
405
|
+
* @param {string} message - Error message description.
|
|
406
|
+
* @param {number} [statusCode=500] - HTTP status code.
|
|
407
|
+
* @param {Array|Object} [errors=[]] - Array or object of detailed errors.
|
|
408
|
+
*/
|
|
409
|
+
constructor(message, statusCode = 500, errors = []) {
|
|
410
|
+
super(message);
|
|
411
|
+
this.name = "AppError";
|
|
412
|
+
this.statusCode = statusCode;
|
|
413
|
+
this.errors = errors;
|
|
414
|
+
this.isOperational = true;
|
|
415
|
+
Error.captureStackTrace(this, this.constructor);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
338
419
|
export default App;
|