@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **High-Performance Express Framework — Built for Speed, Caching, and Visual Excellence**
4
4
 
5
- ![Blue Bird Logo](https://seip25.github.io/Blue-bird/favicon.png)
5
+ ![Blue Bird Logo](https://seip25.github.io/Blue-bird/favicon.ico)
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@seip/blue-bird.svg)](https://www.npmjs.com/package/@seip/blue-bird)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -95,7 +95,34 @@ project/
95
95
 
96
96
  ## 📖 Core Modules Documentation / Documentación de Módulos
97
97
 
98
- ### 1. Routing (`Router`)
98
+ ### 1. Application Class (`App`)
99
+
100
+ Initializes the Express server. If Docker/Nginx is not used (or for lightweight setups like Express + SQLite), you can configure Express to serve static frontend files directly via the `static` parameter:
101
+
102
+ ```javascript
103
+ import App from "@seip/blue-bird/core/app.js";
104
+ import routerApi from "./backend/routes/api.js";
105
+
106
+ const app = new App({
107
+ port: process.env.PORT || 3000,
108
+ host: "http://localhost",
109
+ routes: [routerApi],
110
+ cors: [],
111
+ middlewares: [],
112
+ logger: false,
113
+ // Standalone Express Static Asset Serving (No Nginx/Docker required)
114
+ static: {
115
+ path: "../frontend", // relative directory path to frontend files
116
+ options: {} // express.static options
117
+ }
118
+ });
119
+
120
+ app.run();
121
+ ```
122
+
123
+ ---
124
+
125
+ ### 2. Routing (`Router`)
99
126
 
100
127
  Do not use Express' native router. Always use Blue Bird's wrapper class:
101
128
 
@@ -135,57 +162,86 @@ routerApi.post("/users", validateUser.middleware(), (req, res) => {
135
162
 
136
163
  ---
137
164
 
138
- ### 4. JWT Authentication (`Auth`)
165
+ ### 4. JWT Authentication & Redis Sessions (`Auth`)
139
166
 
140
- Secure user sessions using stateless AES-256-GCM encrypted JWTs stored in secure HTTP-Only cookies.
167
+ Secure user authentication with AES-256-GCM encrypted tokens. Transmitted via HTTP-Only cookies or `Authorization` headers, with optional Redis session storage and invalidation.
141
168
 
142
169
  #### Protecting Routes
143
170
 
144
171
  ```javascript
145
172
  import Auth from "@seip/blue-bird/core/auth.js";
146
173
 
147
- // Secure API endpoint (returns 401 on failure)
174
+ // 1. Secure API endpoint (returns 401 JSON on failure)
148
175
  router.get("/profile", Auth.protect(), (req, res) => {
149
176
  res.json({ user: req.user });
150
177
  });
151
178
 
152
- // Secure web page (redirects to /login on failure)
153
- router.get("/dashboard", Auth.protect({ redirect: "/login" }), (req, res) => {
154
- Template.render(res, "dashboard");
179
+ // 2. Secure web page (redirects to /login on failure)
180
+ router.get("/dashboard", Auth.protect({ redirect: "/login", key: "user", cookieKey: "auth" }), (req, res) => {
181
+ res.send(`<h1>Welcome ${req.user.name}</h1>`);
155
182
  });
156
183
  ```
157
184
 
158
- #### Authentication Sessions
185
+ #### Authentication Sessions & Utilities
159
186
 
160
187
  ```javascript
188
+ // Login & Sync Session state in Redis (if active)
161
189
  router.post("/login", async (req, res) => {
162
- const user = { id: 1, name: "John Doe" };
163
- await Auth.login(res, user);
190
+ const user = { id: 1, name: "John Doe", role: "admin" };
191
+ await Auth.login(res, user, "auth", { expiresIn: "7d" });
164
192
  res.json({ message: "Logged in successfully" });
165
193
  });
166
194
 
195
+ // Logout & Delete Session from Redis
167
196
  router.post("/logout", async (req, res) => {
168
- await Auth.logout(res);
197
+ await Auth.logout(res, "auth", {}, req);
169
198
  res.json({ message: "Logged out" });
170
199
  });
200
+
201
+ // Manual Encrypted JWT Tokens & AES-256-GCM Encryption
202
+ const token = Auth.generateToken({ id: 1 }, process.env.JWT_SECRET, "2h");
203
+ const decoded = Auth.verifyToken(token, process.env.JWT_SECRET);
204
+ const encrypted = Auth.encrypt({ secret: "1234" }, process.env.JWT_SECRET);
205
+ const decrypted = Auth.decrypt(encrypted, process.env.JWT_SECRET);
171
206
  ```
172
207
 
173
208
  ---
174
209
 
175
- ### 5. Performance Cache Middleware (`Cache`)
210
+ ### 5. Performance Cache & Redis Client (`Cache`)
211
+
212
+ Applies route-level response caching for JSON payloads (`res.json`) and HTML output (`res.send`). Automatically uses Redis when `REDIS_HOST` is configured, and transparently degrades to an in-memory cache if Redis is unavailable or offline.
176
213
 
177
- Applies caching at the route handler level. Automatically caches JSON payloads (`res.json`) and rendered outputs (`res.send`).
214
+ #### Route Caching Middleware
178
215
 
179
216
  ```javascript
180
- import Cache from "@seip/blue-bird/core/cache.js";
217
+ import Cache, { getRedisClient } from "@seip/blue-bird/core/cache.js";
181
218
 
182
- // Cache endpoint for 60 seconds
219
+ // Cache endpoint for 60 seconds (sets X-Blue-Bird-Cache: HIT/MISS headers)
183
220
  router.get("/stats", Cache.middleware(60), (req, res) => {
184
221
  res.json({ usersOnline: 42 });
185
222
  });
186
223
  ```
187
224
 
188
- Integrates with Redis if `REDIS_HOST` is defined in the environment. Falls back to an in-memory cache automatically if Redis is not configured or not running.
225
+ #### Custom Database & Data Caching with `getRedisClient()`
226
+
227
+ ```javascript
228
+ // Direct access to the active Redis client for database query or custom key caching
229
+ router.get("/custom-cache", async (req, res) => {
230
+ const redis = getRedisClient();
231
+ if (redis) {
232
+ const cached = await redis.get("my_custom_key");
233
+ if (cached) return res.json(JSON.parse(cached));
234
+
235
+ const dbData = await fetchHeavyDataFromDB();
236
+ await redis.set("my_custom_key", JSON.stringify(dbData), { EX: 120 }); // Expiry 120s
237
+ return res.json(dbData);
238
+ }
239
+
240
+ // Fallback if Redis is disabled
241
+ const dbData = await fetchHeavyDataFromDB();
242
+ res.json(dbData);
243
+ });
244
+ ```
189
245
 
190
246
  ---
191
247
 
@@ -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
- ### 8. Nginx Static Asset Caching
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) using credentials from `.env`.
273
- - **`npx blue-bird docker redis`**: Connects into the container's interactive Redis CLI terminal.
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 status = err.status || 500;
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(`[${status}] ${message} - ${err.stack}`);
288
+ this.loggerInstance.error(`[${statusCode}] ${message} - ${err.stack}`);
248
289
 
249
- if (props.debug) {
250
- return res.status(status).json({
251
- success: false,
252
- error: true,
253
- message: message,
254
- stack: err.stack,
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(status).json({
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.app.listen(this.port, () => {
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;