@seip/blue-bird 1.0.1 → 1.1.0

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/.env_example CHANGED
@@ -6,6 +6,7 @@ APP_URL="http://localhost"
6
6
  VERSION="1.0.0"
7
7
 
8
8
  # Docker / Swagger Config
9
+ COMPOSE_PROJECT_NAME="blue-bird"
9
10
  TITLE="Blue-Bird"
10
11
  DESCRIPTION="Description project"
11
12
 
package/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ This document serves as the primary manual for any AI Agent interacting with the
6
6
 
7
7
  Blue Bird is a performance-first API framework built on **Express**. It saves developers from repetitive configuration, validation, security, JWT authentication, and database environment configuration out of the box, delegating all static frontend rendering to Nginx.
8
8
 
9
- - **Entrypoint (`index.js`)**: Initializes the server using `App` from `core/app.js` and registers the routes.
9
+ - **Entrypoint (`backend/index.js`)**: Initializes the server using `App` from `core/app.js` and registers the routes.
10
10
  - **Backend (`backend/`)**: Application routes and logic (e.g. `backend/routes/`).
11
11
  - **Frontend (`frontend/`)**: Static assets (HTML, CSS, JS). Handled directly by Nginx in production, bypassing Express.
12
12
  - **Core (`core/`)**: The framework core. Contains wrapper classes such as `Router`, `Validator`, `Auth`, `Cache`, etc. **DO NOT MODIFY** the core unless explicitly requested, as it could break other apps.
@@ -87,9 +87,17 @@ If an Express route involves heavy processing or database queries, utilize the `
87
87
  ```javascript
88
88
  import Cache from "@seip/blue-bird/core/cache.js";
89
89
 
90
+ // Express route middleware caching
90
91
  router.get("/stats", Cache.middleware(60), (req, res) => {
91
92
  res.json({ ok: true });
92
93
  });
94
+
95
+ // Programmatic cache manipulation
96
+ await Cache.set("custom_key", { data: "value" }, 120);
97
+ const cachedData = await Cache.get("custom_key");
98
+
99
+ // Invalidate route cache manually (e.g. after updating DB)
100
+ await Cache.delete("/api/public/config");
93
101
  ```
94
102
 
95
103
  The Cache module integrates with Redis when `REDIS_HOST` is configured in the environment. If Redis is unavailable or fails, it transparently falls back to an in-memory cache system without interrupting requests.
package/README.md CHANGED
@@ -222,6 +222,20 @@ router.get("/stats", Cache.middleware(60), (req, res) => {
222
222
  });
223
223
  ```
224
224
 
225
+ #### Programmatic Cache Manipulation & Invalidation
226
+
227
+ ```javascript
228
+ // Get / Set keys programmatically
229
+ await Cache.set("custom_key", { data: "value" }, 120);
230
+ const cachedData = await Cache.get("custom_key");
231
+
232
+ // Manually invalidate route cache (e.g. after updating DB)
233
+ await Cache.delete("/api/public/config");
234
+
235
+ // Clear all cache entries
236
+ await Cache.clear();
237
+ ```
238
+
225
239
  #### Custom Database & Data Caching with `getRedisClient()`
226
240
 
227
241
  ```javascript
package/core/cache.js CHANGED
@@ -162,6 +162,137 @@ class Cache {
162
162
  next();
163
163
  };
164
164
  }
165
+
166
+ /**
167
+ * Retrieves cached value by key.
168
+ * @param {string} key - Cache key.
169
+ * @returns {Promise<any|null>} Cached payload or null.
170
+ */
171
+ static async get(key) {
172
+ key = key.trim();
173
+ if (!key) return null;
174
+
175
+ if (redisHost && !redisClient) {
176
+ await initRedis().catch(() => { });
177
+ }
178
+
179
+ if (isRedisConnected && redisClient) {
180
+ try {
181
+ const cachedData = await redisClient.get(key);
182
+ if (cachedData) {
183
+ try {
184
+ const cached = JSON.parse(cachedData);
185
+ return cached && typeof cached === "object" && "data" in cached ? cached.data : cached;
186
+ } catch {
187
+ return cachedData;
188
+ }
189
+ }
190
+ return null;
191
+ } catch (err) {
192
+ isRedisConnected = false;
193
+ }
194
+ }
195
+
196
+ if (CACHE[key]) {
197
+ if (CACHE[key].expiry > Date.now()) {
198
+ const cached = CACHE[key];
199
+ return cached.data !== undefined ? cached.data : cached;
200
+ }
201
+ delete CACHE[key];
202
+ }
203
+
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Sets data into cache with a specified TTL in seconds.
209
+ * @param {string} key - Cache key.
210
+ * @param {any} value - Data to cache.
211
+ * @param {number} [seconds=60] - Expiry time in seconds.
212
+ * @returns {Promise<boolean>} True if set successfully.
213
+ */
214
+ static async set(key, value, seconds = 60) {
215
+ key = key.trim();
216
+ if (!key) return false;
217
+
218
+ if (redisHost && !redisClient) {
219
+ await initRedis().catch(() => { });
220
+ }
221
+
222
+ const cacheObject = {
223
+ type: typeof value === "string" ? "html" : "json",
224
+ data: value,
225
+ expiry: Date.now() + seconds * 1000,
226
+ };
227
+
228
+ if (isRedisConnected && redisClient) {
229
+ try {
230
+ await redisClient.set(key, JSON.stringify(cacheObject), {
231
+ EX: seconds,
232
+ });
233
+ } catch (err) {
234
+ CACHE[key] = cacheObject;
235
+ }
236
+ } else {
237
+ CACHE[key] = cacheObject;
238
+ }
239
+
240
+ return true;
241
+ }
242
+
243
+ /**
244
+ * Deletes one or more entries from cache.
245
+ * @param {string|string[]} keys - Single key or array of keys to delete.
246
+ * @returns {Promise<boolean>} True if deleted.
247
+ */
248
+ static async delete(keys) {
249
+ if (!keys) return false;
250
+ const keyList = Array.isArray(keys) ? keys : [keys];
251
+
252
+ if (redisHost && !redisClient) {
253
+ await initRedis().catch(() => { });
254
+ }
255
+
256
+ for (const key of keyList) {
257
+ delete CACHE[key];
258
+ if (isRedisConnected && redisClient) {
259
+ try {
260
+ await redisClient.del(key);
261
+ } catch (err) {
262
+ isRedisConnected = false;
263
+ }
264
+ }
265
+ }
266
+
267
+ return true;
268
+ }
269
+
270
+ /**
271
+ * Alias for delete.
272
+ * @param {string|string[]} keys - Single key or array of keys to delete.
273
+ * @returns {Promise<boolean>} True if deleted.
274
+ */
275
+ static async del(keys) {
276
+ return this.delete(keys);
277
+ }
278
+
279
+ /**
280
+ * Flushes all cached data in memory (and Redis if connected).
281
+ * @returns {Promise<boolean>} True if flushed.
282
+ */
283
+ static async clear() {
284
+ for (const key in CACHE) {
285
+ delete CACHE[key];
286
+ }
287
+ if (isRedisConnected && redisClient) {
288
+ try {
289
+ await redisClient.flushDb();
290
+ } catch (err) {
291
+ isRedisConnected = false;
292
+ }
293
+ }
294
+ return true;
295
+ }
165
296
  }
166
297
 
167
298
  /**
@@ -145,25 +145,25 @@ async function startCommand(service) {
145
145
  /**
146
146
  * Handles the 'dev' CLI command.
147
147
  */
148
- async function devCommand() {
149
- checkComposeFile();
150
- const dbType = getDbType();
151
- console.log(chalk.cyan("Starting development environment (Database + Redis)..."));
152
-
153
- const containers = ["redis"];
154
- if (dbType !== "none") {
155
- containers.unshift(dbType);
156
- }
157
-
158
- const code = await runCmd("docker", ["compose", "up", "-d", ...containers]);
159
- if (code === 0) {
160
- console.log(chalk.green("Development containers started."));
161
- console.log(chalk.cyan("View live logs with: npx blue-bird docker logs db"));
162
- console.log(chalk.yellow("Now execute: npm run dev"));
163
- } else {
164
- console.error(chalk.red("Error starting development environment."));
165
- process.exit(1);
166
- }
148
+ async function devCommand() {
149
+ checkComposeFile();
150
+ const dbType = getDbType();
151
+ console.log(chalk.cyan("Starting development environment (Database + Redis)..."));
152
+
153
+ const containers = ["redis"];
154
+ if (dbType !== "none") {
155
+ containers.unshift(dbType);
156
+ }
157
+
158
+ const code = await runCmd("docker", ["compose", "up", "-d", ...containers]);
159
+ if (code === 0) {
160
+ console.log(chalk.green("Development containers started."));
161
+ console.log(chalk.cyan("View live logs with: npx blue-bird docker logs db"));
162
+ console.log(chalk.yellow("Now execute: npm run dev"));
163
+ } else {
164
+ console.error(chalk.red("Error starting development environment."));
165
+ process.exit(1);
166
+ }
167
167
  }
168
168
 
169
169
  /**
package/core/cli/init.js CHANGED
@@ -103,8 +103,6 @@ class ProjectInit {
103
103
  "docker",
104
104
  ".env_example",
105
105
  "AGENTS.md",
106
- "index.js",
107
- ".gitignore"
108
106
  ];
109
107
 
110
108
  try {
@@ -128,6 +126,24 @@ class ProjectInit {
128
126
  }
129
127
  });
130
128
 
129
+ const gitignoreContent = `node_modules\nlogs\n.env\npackage-lock.json\n*.db\ntest/\n\nbackups/*.sql\n`;
130
+ const gitignoreDest = path.join(this.appDir, ".gitignore");
131
+ const gitignoreSrc = path.join(this.sourceDir, ".gitignore");
132
+
133
+ if (!fs.existsSync(gitignoreDest)) {
134
+ if (fs.existsSync(gitignoreSrc)) {
135
+ fs.copyFileSync(gitignoreSrc, gitignoreDest);
136
+ console.log(chalk.green("[OK] Copied .gitignore to root."));
137
+ } else {
138
+ fs.writeFileSync(gitignoreDest, gitignoreContent, "utf-8");
139
+ console.log(chalk.green("[OK] Created .gitignore file."));
140
+ }
141
+ } else {
142
+ console.log(
143
+ chalk.yellow("[SKIP] .gitignore already exists, skipping."),
144
+ );
145
+ }
146
+
131
147
  const composeTemplateName =
132
148
  dbType === "postgres"
133
149
  ? "docker-compose.postgres.yml"
@@ -166,8 +182,15 @@ class ProjectInit {
166
182
  let envContent = fs.readFileSync(envExamplePath, "utf-8");
167
183
 
168
184
  const jwtSecret = crypto.randomBytes(32).toString("hex");
185
+ const composeProjectName =
186
+ title
187
+ .toLowerCase()
188
+ .trim()
189
+ .replace(/\s+/g, "-")
190
+ .replace(/[^a-z0-9_-]/g, "") || "blue-bird";
169
191
 
170
192
  const updates = {
193
+ COMPOSE_PROJECT_NAME: composeProjectName,
171
194
  TITLE: title,
172
195
  PORT: port,
173
196
  APP_URL: appUrl,
@@ -190,10 +213,14 @@ class ProjectInit {
190
213
  }
191
214
 
192
215
  const lines = envContent.split(/\r?\n/);
216
+ let foundComposeProjectName = false;
193
217
  const updatedLines = lines.map((line) => {
194
218
  const match = line.match(/^([A-Z_]+)=(.+)/);
195
219
  if (match) {
196
220
  const key = match[1];
221
+ if (key === "COMPOSE_PROJECT_NAME") {
222
+ foundComposeProjectName = true;
223
+ }
197
224
  if (updates[key] !== undefined) {
198
225
  const value = updates[key];
199
226
  if (typeof value === "string" && !value.startsWith('"')) {
@@ -204,6 +231,21 @@ class ProjectInit {
204
231
  }
205
232
  return line;
206
233
  });
234
+
235
+ if (!foundComposeProjectName && updates.COMPOSE_PROJECT_NAME) {
236
+ const titleIdx = updatedLines.findIndex((l) => l.startsWith("TITLE="));
237
+ if (titleIdx !== -1) {
238
+ updatedLines.splice(
239
+ titleIdx,
240
+ 0,
241
+ `COMPOSE_PROJECT_NAME="${updates.COMPOSE_PROJECT_NAME}"`,
242
+ );
243
+ } else {
244
+ updatedLines.push(
245
+ `COMPOSE_PROJECT_NAME="${updates.COMPOSE_PROJECT_NAME}"`,
246
+ );
247
+ }
248
+ }
207
249
  envContent = updatedLines.join("\n");
208
250
 
209
251
  fs.writeFileSync(envPath, envContent, "utf-8");
package/core/index.d.ts CHANGED
@@ -1,132 +1,137 @@
1
- import { Router as ExpressRouter, Request, Response, NextFunction } from "express";
2
-
3
- declare global {
4
- namespace Express {
5
- interface Response {
6
- /**
7
- * Sends a standardized JSON success response.
8
- * @param data Data payload to return.
9
- * @param message Success message.
10
- * @param statusCode HTTP status code (default: 200).
11
- */
12
- success(data?: any, message?: string, statusCode?: number): Response;
13
-
14
- /**
15
- * Sends a standardized JSON error response.
16
- * @param message Error message.
17
- * @param statusCode HTTP status code (default: 400).
18
- * @param errors Array or object of detailed errors.
19
- */
20
- error(message?: string, statusCode?: number, errors?: any): Response;
21
-
22
- /**
23
- * Sends a standardized paginated JSON response.
24
- * @param data Array of records for current page.
25
- * @param pagination Object containing page, limit, and total count.
26
- * @param message Success message.
27
- */
28
- paginate(
29
- data?: any[],
30
- pagination?: { page?: number; limit?: number; total?: number },
31
- message?: string
32
- ): Response;
33
- }
34
- }
35
- }
36
-
37
- export class AppError extends Error {
38
- statusCode: number;
39
- errors: any;
40
- isOperational: boolean;
41
-
42
- constructor(message: string, statusCode?: number, errors?: any);
43
- }
44
-
45
- export class App {
46
- constructor(options?: {
47
- routes?: any[];
48
- cors?: any;
49
- middlewares?: any[];
50
- port?: number | string;
51
- host?: string;
52
- logger?: boolean;
53
- notFound?: boolean;
54
- json?: boolean;
55
- urlencoded?: boolean;
56
- static?: { path: string; options?: any };
57
- cookieParser?: boolean;
58
- rateLimit?: boolean | any;
59
- swagger?: boolean | any;
60
- compression?: boolean;
61
- });
62
-
63
- use(record: any): void;
64
- set(key: string, value: any): void;
65
- websocket(
66
- options?:
67
- | ((ws: any, req: any) => void)
68
- | { path?: string; auth?: boolean }
69
- ): WebSocketManager;
70
- run(): void;
71
-
72
- static helmet(options?: any): any;
73
- }
74
-
75
- export class WebSocketManager {
76
- constructor(server: any, options?: { path?: string; auth?: boolean });
77
- onConnection(handler: (ws: any, req: any) => void): void;
78
- join(room: string, ws: any): void;
79
- leave(room: string, ws: any): void;
80
- broadcast(data: any, room?: string | null): void;
81
- }
82
-
83
- export class Router {
84
- constructor(path?: string, options?: { seo?: boolean; languages?: string[] });
85
-
86
- use(...middleware: any[]): void;
87
- get(path: string | RegExp, ...callback: any[]): void;
88
- post(path: string | RegExp, ...callback: any[]): void;
89
- put(path: string | RegExp, ...callback: any[]): void;
90
- delete(path: string | RegExp, ...callback: any[]): void;
91
- patch(path: string | RegExp, ...callback: any[]): void;
92
- options(path: string | RegExp, ...callback: any[]): void;
93
- getRouter(): ExpressRouter;
94
- getPath(): string;
95
- }
96
-
97
- export class Validator {
98
- constructor(schema: Record<string, any>, lang?: string);
99
- middleware(): (req: Request, res: Response, next: NextFunction) => void;
100
- validate(data: Record<string, any>): { valid: boolean; errors: any[] };
101
- }
102
-
103
- export class Auth {
104
- static encrypt(payload: any, secret: string): string;
105
- static decrypt(data: string, secret: string): any;
106
- static generateToken(payload: any, secret?: string, expiresIn?: string | number): string;
107
- static verifyToken(token: string, secret?: string): any;
108
- static protect(options?: { redirect?: string | null; key?: string; cookieKey?: string }): (req: Request, res: Response, next: NextFunction) => Promise<any>;
109
- static login(res: Response, data: any, key?: string, options?: { expiresIn?: string | number; cookie?: any }): Promise<string>;
110
- static logout(res: Response, key?: string, options?: any, req?: Request): Promise<boolean>;
111
- }
112
-
113
- export class Cache {
114
- static middleware(seconds?: number): (req: Request, res: Response, next: NextFunction) => Promise<any>;
115
- }
116
-
117
- export function getRedisClient(): any;
118
-
119
- export class Database {
120
- constructor(connectionLimit?: number, queueLimit?: number, config?: any);
121
- init(retries?: number): Promise<boolean>;
122
- query(sql: string, params?: any[], options?: any): Promise<any>;
123
- paginate(
124
- sql: string,
125
- params?: any[],
126
- options?: { page?: number; limit?: number; cache?: number }
127
- ): Promise<{ data: any[]; total: number; page: number; limit: number; totalPages: number }>;
128
- transaction<T = any>(callback: (tx: { query: (sql: string, params?: any[], options?: any) => Promise<any> }) => Promise<T>): Promise<T>;
129
- executeTransaction<T = any>(callback: (tx: { query: (sql: string, params?: any[], options?: any) => Promise<any> }) => Promise<T>): Promise<T>;
130
- }
131
-
132
- export default App;
1
+ import { Router as ExpressRouter, Request, Response, NextFunction } from "express";
2
+
3
+ declare global {
4
+ namespace Express {
5
+ interface Response {
6
+ /**
7
+ * Sends a standardized JSON success response.
8
+ * @param data Data payload to return.
9
+ * @param message Success message.
10
+ * @param statusCode HTTP status code (default: 200).
11
+ */
12
+ success(data?: any, message?: string, statusCode?: number): Response;
13
+
14
+ /**
15
+ * Sends a standardized JSON error response.
16
+ * @param message Error message.
17
+ * @param statusCode HTTP status code (default: 400).
18
+ * @param errors Array or object of detailed errors.
19
+ */
20
+ error(message?: string, statusCode?: number, errors?: any): Response;
21
+
22
+ /**
23
+ * Sends a standardized paginated JSON response.
24
+ * @param data Array of records for current page.
25
+ * @param pagination Object containing page, limit, and total count.
26
+ * @param message Success message.
27
+ */
28
+ paginate(
29
+ data?: any[],
30
+ pagination?: { page?: number; limit?: number; total?: number },
31
+ message?: string
32
+ ): Response;
33
+ }
34
+ }
35
+ }
36
+
37
+ export class AppError extends Error {
38
+ statusCode: number;
39
+ errors: any;
40
+ isOperational: boolean;
41
+
42
+ constructor(message: string, statusCode?: number, errors?: any);
43
+ }
44
+
45
+ export class App {
46
+ constructor(options?: {
47
+ routes?: any[];
48
+ cors?: any;
49
+ middlewares?: any[];
50
+ port?: number | string;
51
+ host?: string;
52
+ logger?: boolean;
53
+ notFound?: boolean;
54
+ json?: boolean;
55
+ urlencoded?: boolean;
56
+ static?: { path: string; options?: any };
57
+ cookieParser?: boolean;
58
+ rateLimit?: boolean | any;
59
+ swagger?: boolean | any;
60
+ compression?: boolean;
61
+ });
62
+
63
+ use(record: any): void;
64
+ set(key: string, value: any): void;
65
+ websocket(
66
+ options?:
67
+ | ((ws: any, req: any) => void)
68
+ | { path?: string; auth?: boolean }
69
+ ): WebSocketManager;
70
+ run(): void;
71
+
72
+ static helmet(options?: any): any;
73
+ }
74
+
75
+ export class WebSocketManager {
76
+ constructor(server: any, options?: { path?: string; auth?: boolean });
77
+ onConnection(handler: (ws: any, req: any) => void): void;
78
+ join(room: string, ws: any): void;
79
+ leave(room: string, ws: any): void;
80
+ broadcast(data: any, room?: string | null): void;
81
+ }
82
+
83
+ export class Router {
84
+ constructor(path?: string, options?: { seo?: boolean; languages?: string[] });
85
+
86
+ use(...middleware: any[]): void;
87
+ get(path: string | RegExp, ...callback: any[]): void;
88
+ post(path: string | RegExp, ...callback: any[]): void;
89
+ put(path: string | RegExp, ...callback: any[]): void;
90
+ delete(path: string | RegExp, ...callback: any[]): void;
91
+ patch(path: string | RegExp, ...callback: any[]): void;
92
+ options(path: string | RegExp, ...callback: any[]): void;
93
+ getRouter(): ExpressRouter;
94
+ getPath(): string;
95
+ }
96
+
97
+ export class Validator {
98
+ constructor(schema: Record<string, any>, lang?: string);
99
+ middleware(): (req: Request, res: Response, next: NextFunction) => void;
100
+ validate(data: Record<string, any>): { valid: boolean; errors: any[] };
101
+ }
102
+
103
+ export class Auth {
104
+ static encrypt(payload: any, secret: string): string;
105
+ static decrypt(data: string, secret: string): any;
106
+ static generateToken(payload: any, secret?: string, expiresIn?: string | number): string;
107
+ static verifyToken(token: string, secret?: string): any;
108
+ static protect(options?: { redirect?: string | null; key?: string; cookieKey?: string }): (req: Request, res: Response, next: NextFunction) => Promise<any>;
109
+ static login(res: Response, data: any, key?: string, options?: { expiresIn?: string | number; cookie?: any }): Promise<string>;
110
+ static logout(res: Response, key?: string, options?: any, req?: Request): Promise<boolean>;
111
+ }
112
+
113
+ export class Cache {
114
+ static middleware(seconds?: number): (req: Request, res: Response, next: NextFunction) => Promise<any>;
115
+ static get(key: string): Promise<any | null>;
116
+ static set(key: string, value: any, seconds?: number): Promise<boolean>;
117
+ static delete(keys: string | string[]): Promise<boolean>;
118
+ static del(keys: string | string[]): Promise<boolean>;
119
+ static clear(): Promise<boolean>;
120
+ }
121
+
122
+ export function getRedisClient(): any;
123
+
124
+ export class Database {
125
+ constructor(connectionLimit?: number, queueLimit?: number, config?: any);
126
+ init(retries?: number): Promise<boolean>;
127
+ query(sql: string, params?: any[], options?: any): Promise<any>;
128
+ paginate(
129
+ sql: string,
130
+ params?: any[],
131
+ options?: { page?: number; limit?: number; cache?: number }
132
+ ): Promise<{ data: any[]; total: number; page: number; limit: number; totalPages: number }>;
133
+ transaction<T = any>(callback: (tx: { query: (sql: string, params?: any[], options?: any) => Promise<any> }) => Promise<T>): Promise<T>;
134
+ executeTransaction<T = any>(callback: (tx: { query: (sql: string, params?: any[], options?: any) => Promise<any> }) => Promise<T>): Promise<T>;
135
+ }
136
+
137
+ export default App;