@seip/blue-bird 1.1.3 → 1.1.5

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.
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import chalk from "chalk";
6
+ import { Database, DB_TYPE } from "../database.js";
7
+
8
+ /**
9
+ * Generates current timestamp string in YYYYMMDD_HHMMSS format.
10
+ * @returns {string}
11
+ */
12
+ function getTimestamp() {
13
+ const now = new Date();
14
+ const pad = (n) => String(n).padStart(2, "0");
15
+ const year = now.getFullYear();
16
+ const month = pad(now.getMonth() + 1);
17
+ const day = pad(now.getDate());
18
+ const hours = pad(now.getHours());
19
+ const mins = pad(now.getMinutes());
20
+ const secs = pad(now.getSeconds());
21
+ return `${year}${month}${day}_${hours}${mins}${secs}`;
22
+ }
23
+
24
+ /**
25
+ * Creates a new migration file in database/migrations/.
26
+ * @param {string} name
27
+ */
28
+ function makeMigration(name) {
29
+ if (!name) {
30
+ console.log(chalk.red("[ERROR] Missing migration name."));
31
+ console.log("Usage: npx blue-bird make:migration <name>");
32
+ process.exit(1);
33
+ }
34
+
35
+ const cleanName = name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
36
+ const filename = `${getTimestamp()}_${cleanName}.sql`;
37
+ const migrationsDir = path.resolve(process.cwd(), "database/migrations");
38
+
39
+ if (!fs.existsSync(migrationsDir)) {
40
+ fs.mkdirSync(migrationsDir, { recursive: true });
41
+ }
42
+
43
+ const filePath = path.join(migrationsDir, filename);
44
+
45
+ const template = `-- =============================================================
46
+ -- Migration: ${cleanName}
47
+ -- Created At: ${new Date().toISOString()}
48
+ -- Driver Compatibility: SQLite / MySQL / PostgreSQL
49
+ -- =============================================================
50
+
51
+ CREATE TABLE IF NOT EXISTS ${cleanName} (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ name VARCHAR(255) NOT NULL,
54
+ description TEXT,
55
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
56
+ );
57
+ `;
58
+
59
+ fs.writeFileSync(filePath, template, "utf-8");
60
+ console.log(chalk.green(`[OK] Migration created: database/migrations/${filename}`));
61
+ }
62
+
63
+ /**
64
+ * Creates a new seed file in database/seeds/.
65
+ * @param {string} name
66
+ */
67
+ function makeSeed(name) {
68
+ if (!name) {
69
+ console.log(chalk.red("[ERROR] Missing seed name."));
70
+ console.log("Usage: npx blue-bird make:seed <name>");
71
+ process.exit(1);
72
+ }
73
+
74
+ const cleanName = name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
75
+ const filename = `${cleanName}.sql`;
76
+ const seedsDir = path.resolve(process.cwd(), "database/seeds");
77
+
78
+ if (!fs.existsSync(seedsDir)) {
79
+ fs.mkdirSync(seedsDir, { recursive: true });
80
+ }
81
+
82
+ const filePath = path.join(seedsDir, filename);
83
+
84
+ const template = `-- =============================================================
85
+ -- Seed: ${cleanName}
86
+ -- Created At: ${new Date().toISOString()}
87
+ -- =============================================================
88
+
89
+ -- INSERT INTO table_name (name) VALUES ('Sample Item 1');
90
+ `;
91
+
92
+ fs.writeFileSync(filePath, template, "utf-8");
93
+ console.log(chalk.green(`[OK] Seed file created: database/seeds/${filename}`));
94
+ }
95
+
96
+ /**
97
+ * Ensures migrations tracking table exists.
98
+ * @param {Database} db
99
+ */
100
+ async function ensureMigrationsTable(db) {
101
+ let ddl = "";
102
+ if (DB_TYPE === "postgres") {
103
+ ddl = `CREATE TABLE IF NOT EXISTS _bluebird_migrations (
104
+ id SERIAL PRIMARY KEY,
105
+ name VARCHAR(255) NOT NULL UNIQUE,
106
+ batch INT NOT NULL,
107
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
108
+ );`;
109
+ } else if (DB_TYPE === "mysql") {
110
+ ddl = `CREATE TABLE IF NOT EXISTS _bluebird_migrations (
111
+ id INT AUTO_INCREMENT PRIMARY KEY,
112
+ name VARCHAR(255) NOT NULL UNIQUE,
113
+ batch INT NOT NULL,
114
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
115
+ );`;
116
+ } else {
117
+ // sqlite default
118
+ ddl = `CREATE TABLE IF NOT EXISTS _bluebird_migrations (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ name TEXT NOT NULL UNIQUE,
121
+ batch INTEGER NOT NULL,
122
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
123
+ );`;
124
+ }
125
+
126
+ await db.query(ddl);
127
+ }
128
+
129
+ /**
130
+ * Executes pending database migrations.
131
+ */
132
+ async function runMigrations() {
133
+ const migrationsDir = path.resolve(process.cwd(), "database/migrations");
134
+ if (!fs.existsSync(migrationsDir)) {
135
+ console.log(chalk.yellow("[INFO] No 'database/migrations' directory found. Nothing to migrate."));
136
+ return;
137
+ }
138
+
139
+ const files = fs
140
+ .readdirSync(migrationsDir)
141
+ .filter((f) => f.endsWith(".sql") || f.endsWith(".js"))
142
+ .sort();
143
+
144
+ if (files.length === 0) {
145
+ console.log(chalk.yellow("[INFO] No migration files found in 'database/migrations'."));
146
+ return;
147
+ }
148
+
149
+ let db;
150
+ try {
151
+ db = new Database(5);
152
+ await ensureMigrationsTable(db);
153
+ } catch (err) {
154
+ console.error(chalk.red("[ERROR] Could not connect to database to run migrations:"), err.message);
155
+ process.exit(1);
156
+ }
157
+
158
+ const appliedRows = (await db.query("SELECT name, batch FROM _bluebird_migrations ORDER BY id ASC")) || [];
159
+ const appliedSet = new Set(appliedRows.map((r) => r.name));
160
+
161
+ const maxBatchRow = await db.query("SELECT MAX(batch) as max_batch FROM _bluebird_migrations", [], "return_row");
162
+ const currentBatch = ((maxBatchRow && maxBatchRow.max_batch) || 0) + 1;
163
+
164
+ const pending = files.filter((f) => !appliedSet.has(f));
165
+
166
+ if (pending.length === 0) {
167
+ console.log(chalk.green("[INFO] Database is up to date. No pending migrations."));
168
+ process.exit(0);
169
+ }
170
+
171
+ console.log(chalk.cyan(`[INFO] Running ${pending.length} pending migration(s) (Batch #${currentBatch})...\n`));
172
+
173
+ for (const file of pending) {
174
+ const filePath = path.join(migrationsDir, file);
175
+ try {
176
+ if (file.endsWith(".sql")) {
177
+ const sql = fs.readFileSync(filePath, "utf-8");
178
+ // Split statements by semicolon where appropriate
179
+ const statements = sql
180
+ .split(/;\s*$/m)
181
+ .map((s) => s.trim())
182
+ .filter((s) => s.length > 0);
183
+
184
+ await db.transaction(async (tx) => {
185
+ for (const stmt of statements) {
186
+ await tx.query(stmt);
187
+ }
188
+ await tx.query(
189
+ "INSERT INTO _bluebird_migrations (name, batch) VALUES (?, ?)",
190
+ [file, currentBatch]
191
+ );
192
+ });
193
+ } else if (file.endsWith(".js")) {
194
+ const modulePath = `file://${filePath}`;
195
+ const migrationModule = await import(modulePath);
196
+ if (typeof migrationModule.up === "function") {
197
+ await db.transaction(async (tx) => {
198
+ await migrationModule.up(tx);
199
+ await tx.query(
200
+ "INSERT INTO _bluebird_migrations (name, batch) VALUES (?, ?)",
201
+ [file, currentBatch]
202
+ );
203
+ });
204
+ }
205
+ }
206
+
207
+ console.log(chalk.green(` [MIGRATED] ${file}`));
208
+ } catch (err) {
209
+ console.error(chalk.red(` [FAILED] ${file}: ${err.message}`));
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ console.log(chalk.bold.green("\n[OK] All pending migrations executed successfully."));
215
+ process.exit(0);
216
+ }
217
+
218
+ /**
219
+ * Shows migration status list.
220
+ */
221
+ async function showMigrationStatus() {
222
+ const migrationsDir = path.resolve(process.cwd(), "database/migrations");
223
+ if (!fs.existsSync(migrationsDir)) {
224
+ console.log(chalk.yellow("[INFO] No 'database/migrations' directory found."));
225
+ return;
226
+ }
227
+
228
+ const files = fs
229
+ .readdirSync(migrationsDir)
230
+ .filter((f) => f.endsWith(".sql") || f.endsWith(".js"))
231
+ .sort();
232
+
233
+ let db;
234
+ try {
235
+ db = new Database(5);
236
+ await ensureMigrationsTable(db);
237
+ } catch (err) {
238
+ console.error(chalk.red("[ERROR] Could not connect to database:"), err.message);
239
+ process.exit(1);
240
+ }
241
+
242
+ const appliedRows = (await db.query("SELECT name, batch, executed_at FROM _bluebird_migrations ORDER BY id ASC")) || [];
243
+ const appliedMap = new Map(appliedRows.map((r) => [r.name, r]));
244
+
245
+ console.log(chalk.bold.cyan("\n============================================================="));
246
+ console.log(chalk.bold.cyan(" Database Migrations Status"));
247
+ console.log(chalk.bold.cyan("=============================================================\n"));
248
+
249
+ for (const file of files) {
250
+ if (appliedMap.has(file)) {
251
+ const record = appliedMap.get(file);
252
+ console.log(` ${chalk.green("[APPLIED]")} ${file.padEnd(40)} (Batch: ${record.batch}, At: ${record.executed_at})`);
253
+ } else {
254
+ console.log(` ${chalk.yellow("[PENDING]")} ${file}`);
255
+ }
256
+ }
257
+
258
+ console.log("");
259
+ process.exit(0);
260
+ }
261
+
262
+ /**
263
+ * Runs seed scripts from database/seeds/.
264
+ */
265
+ async function runSeeds() {
266
+ const seedsDir = path.resolve(process.cwd(), "database/seeds");
267
+ if (!fs.existsSync(seedsDir)) {
268
+ console.log(chalk.yellow("[INFO] No 'database/seeds' directory found. Nothing to seed."));
269
+ return;
270
+ }
271
+
272
+ const files = fs
273
+ .readdirSync(seedsDir)
274
+ .filter((f) => f.endsWith(".sql") || f.endsWith(".js"))
275
+ .sort();
276
+
277
+ if (files.length === 0) {
278
+ console.log(chalk.yellow("[INFO] No seed files found in 'database/seeds'."));
279
+ return;
280
+ }
281
+
282
+ let db;
283
+ try {
284
+ db = new Database(5);
285
+ } catch (err) {
286
+ console.error(chalk.red("[ERROR] Could not connect to database to run seeds:"), err.message);
287
+ process.exit(1);
288
+ }
289
+
290
+ console.log(chalk.cyan(`[INFO] Running ${files.length} seed file(s)...\n`));
291
+
292
+ for (const file of files) {
293
+ const filePath = path.join(seedsDir, file);
294
+ try {
295
+ if (file.endsWith(".sql")) {
296
+ const sql = fs.readFileSync(filePath, "utf-8");
297
+ const statements = sql
298
+ .split(/;\s*$/m)
299
+ .map((s) => s.trim())
300
+ .filter((s) => s.length > 0);
301
+
302
+ await db.transaction(async (tx) => {
303
+ for (const stmt of statements) {
304
+ await tx.query(stmt);
305
+ }
306
+ });
307
+ } else if (file.endsWith(".js")) {
308
+ const modulePath = `file://${filePath}`;
309
+ const seedModule = await import(modulePath);
310
+ if (typeof seedModule.seed === "function") {
311
+ await db.transaction(async (tx) => {
312
+ await seedModule.seed(tx);
313
+ });
314
+ }
315
+ }
316
+
317
+ console.log(chalk.green(` [SEEDED] ${file}`));
318
+ } catch (err) {
319
+ console.error(chalk.red(` [FAILED] ${file}: ${err.message}`));
320
+ process.exit(1);
321
+ }
322
+ }
323
+
324
+ console.log(chalk.bold.green("\n[OK] Database seeding completed."));
325
+ process.exit(0);
326
+ }
327
+
328
+ // CLI Dispatcher
329
+ const rawArgs = process.argv.slice(2);
330
+ const cmd = rawArgs[0];
331
+
332
+ if (cmd === "make:migration") {
333
+ makeMigration(rawArgs[1]);
334
+ } else if (cmd === "make:seed") {
335
+ makeSeed(rawArgs[1]);
336
+ } else if (cmd === "migrate:status") {
337
+ showMigrationStatus();
338
+ } else if (cmd === "seed") {
339
+ runSeeds();
340
+ } else {
341
+ runMigrations();
342
+ }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import chalk from "chalk";
6
+
7
+ /**
8
+ * Parses .env file to extract APP_URL and PORT.
9
+ * @returns {{ domain: string, port: number, appUrl: string }}
10
+ */
11
+ function getEnvConfig() {
12
+ const env = { ...process.env };
13
+ const envPath = path.resolve(process.cwd(), ".env");
14
+ if (fs.existsSync(envPath)) {
15
+ const content = fs.readFileSync(envPath, "utf-8");
16
+ content.split(/\r?\n/).forEach((line) => {
17
+ line = line.trim();
18
+ if (line && !line.startsWith("#") && line.includes("=")) {
19
+ const idx = line.indexOf("=");
20
+ const key = line.substring(0, idx).trim();
21
+ const value = line.substring(idx + 1).trim().replace(/^['"]|['"]$/g, "");
22
+ env[key] = value;
23
+ }
24
+ });
25
+ }
26
+
27
+ let domain = "";
28
+ if (env.APP_URL) {
29
+ try {
30
+ const parsedUrl = new URL(env.APP_URL.includes("://") ? env.APP_URL : `http://${env.APP_URL}`);
31
+ domain = parsedUrl.hostname;
32
+ } catch {
33
+ domain = env.APP_URL.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "").trim();
34
+ }
35
+ }
36
+
37
+ const port = parseInt(env.PORT || "3000", 10);
38
+ return { domain, port, appUrl: env.APP_URL || "" };
39
+ }
40
+
41
+ /**
42
+ * Generates Host Nginx reverse proxy configuration snippet.
43
+ */
44
+ function generateNginxConfig() {
45
+ const rawArgs = process.argv.slice(2);
46
+ const filteredArgs = rawArgs.filter(
47
+ (a) => a !== "nginx:conf" && a !== "nginx:host" && a !== "nginx" && !a.endsWith("nginx.js") && !a.endsWith("init.js")
48
+ );
49
+
50
+ const envConfig = getEnvConfig();
51
+
52
+ let domainArg = filteredArgs.find((a) => !a.startsWith("-") && isNaN(Number(a)));
53
+ let portArg = filteredArgs.find((a) => !isNaN(Number(a)));
54
+
55
+ let domain = domainArg || envConfig.domain;
56
+ let port = portArg ? parseInt(portArg, 10) : envConfig.port;
57
+
58
+ if (!domain) {
59
+ domain = "example.com";
60
+ console.log(chalk.yellow("[INFO] No domain provided and APP_URL is not configured in .env. Defaulting to 'example.com'."));
61
+ } else if (!domainArg && envConfig.domain) {
62
+ console.log(chalk.cyan(`[INFO] Using domain '${domain}' and port ${port} resolved from .env (APP_URL / PORT).`));
63
+ console.log(chalk.gray(` (You can override via: npx blue-bird nginx:conf <domain> [port])\n`));
64
+ }
65
+
66
+ // Clean domain name (strip protocol, path, port)
67
+ domain = domain.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/:\d+$/, "").trim();
68
+
69
+ const nginxSnippet = `# =============================================================
70
+ # Blue Bird Host Nginx Reverse Proxy Configuration
71
+ # File: /etc/nginx/sites-available/${domain}
72
+ # =============================================================
73
+
74
+ server {
75
+ listen 80;
76
+ listen [::]:80;
77
+ server_name ${domain};
78
+
79
+ # Maximum request body size for file uploads
80
+ client_max_body_size 50M;
81
+
82
+ # Security: Block hidden files and scan attempts
83
+ location ~* /\\.(env|git) {
84
+ deny all;
85
+ return 404;
86
+ }
87
+
88
+ # Reverse proxy to Blue Bird container stack
89
+ location / {
90
+ proxy_pass http://127.0.0.1:${port};
91
+ proxy_http_version 1.1;
92
+
93
+ # WebSocket upgrade support
94
+ proxy_set_header Upgrade $http_upgrade;
95
+ proxy_set_header Connection "upgrade";
96
+
97
+ # Standard proxy headers
98
+ proxy_set_header Host $host;
99
+ proxy_set_header X-Real-IP $remote_addr;
100
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
101
+ proxy_set_header X-Forwarded-Proto $scheme;
102
+ proxy_set_header X-Forwarded-Host $host;
103
+ proxy_set_header X-Forwarded-Port $server_port;
104
+
105
+ # Proxy timeouts
106
+ proxy_connect_timeout 60s;
107
+ proxy_send_timeout 60s;
108
+ proxy_read_timeout 60s;
109
+ }
110
+ }`;
111
+
112
+ console.log(chalk.bold.cyan("============================================================="));
113
+ console.log(chalk.bold.cyan(` Host Nginx Configuration & Let's Encrypt Setup for: ${domain}`));
114
+ console.log(chalk.bold.cyan("============================================================="));
115
+ console.log("");
116
+ console.log(chalk.yellow("1. Create the site configuration file on your VPS:"));
117
+ console.log(chalk.green(` sudo nano /etc/nginx/sites-available/${domain}`));
118
+ console.log("");
119
+ console.log(chalk.yellow("2. Paste the following configuration block:"));
120
+ console.log("");
121
+ console.log(chalk.white(nginxSnippet));
122
+ console.log("");
123
+ console.log(chalk.yellow("3. Enable the site configuration:"));
124
+ console.log(chalk.green(` sudo ln -s /etc/nginx/sites-available/${domain} /etc/nginx/sites-enabled/`));
125
+ console.log("");
126
+ console.log(chalk.yellow("4. Test Nginx syntax:"));
127
+ console.log(chalk.green(" sudo nginx -t"));
128
+ console.log("");
129
+ console.log(chalk.yellow("5. Reload Nginx to apply changes:"));
130
+ console.log(chalk.green(" sudo systemctl reload nginx"));
131
+ console.log("");
132
+ console.log(chalk.yellow("6. Provision free SSL certificate with Certbot (Let's Encrypt):"));
133
+ console.log(chalk.green(` sudo certbot --nginx -d ${domain}`));
134
+ console.log("");
135
+ console.log(chalk.bold.cyan("============================================================="));
136
+ }
137
+
138
+ generateNginxConfig();
package/core/cli/route.js CHANGED
@@ -1,43 +1,144 @@
1
- import path from 'path';
2
- import fs from 'fs';
3
- import Config from "../config.js";
1
+ #!/usr/bin/env node
4
2
 
5
- const __dirname = Config.dirname();
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import chalk from "chalk";
6
6
 
7
7
  class RouteCLI {
8
8
  /**
9
- * Create route
9
+ * Creates a new RESTful route file with Validation, Cache, and optional Auth.
10
10
  */
11
- create() {
12
- let nameRoute = process.argv[2];
13
- if (!nameRoute) {
14
- console.log("Please provide a route name. Usage: npm run route <route-name>");
15
- return;
16
- }
17
- nameRoute =nameRoute.charAt(0).toUpperCase() + nameRoute.slice(1);
18
- const folder= path.join(__dirname, 'backend/routes');
19
- if (!fs.existsSync(folder)){
20
- fs.mkdirSync(folder, { recursive: true });
21
- }
22
- const filePath = path.join(folder, `${nameRoute}.js`);
23
- if (fs.existsSync(filePath)) {
24
- console.log(`Route ${nameRoute} already exists.`);
25
- return;
26
- }
27
- const content =`import Router from "@seip/blue-bird/core/router.js"
28
-
29
- const router${nameRoute} = new Router("/${nameRoute.toLowerCase()}");
30
-
31
- router${nameRoute}.get("/", (req, res) => {
32
- res.json({ message: "Hello from ${nameRoute} route!" });
33
- });
11
+ create() {
12
+ const rawArgs = process.argv.slice(2);
13
+ // Filter out command name if invoked as 'route' or 'make:route'
14
+ const args = rawArgs.filter(
15
+ (a) => a !== "route" && a !== "make:route" && !a.endsWith("route.js")
16
+ );
17
+
18
+ let routeName = args.find((a) => !a.startsWith("-"));
19
+ const withAuth = args.some((a) => a === "--auth" || a === "-a");
20
+
21
+ if (!routeName) {
22
+ console.log(chalk.red("[ERROR] Missing route name."));
23
+ console.log("");
24
+ console.log("Usage:");
25
+ console.log(" npx blue-bird make:route <name> [--auth]");
26
+ console.log("");
27
+ console.log("Examples:");
28
+ console.log(" npx blue-bird make:route products");
29
+ console.log(" npx blue-bird make:route articles --auth");
30
+ process.exit(1);
31
+ }
34
32
 
35
- export default router${nameRoute};
36
- `;
37
- fs.writeFileSync(filePath, content);
38
- console.log(`Route ${nameRoute} created successfully at ${filePath}`);
33
+ // Normalize casing
34
+ routeName = routeName.toLowerCase().replace(/[^a-z0-9_-]/g, "");
35
+ const singularName = routeName.endsWith("s") ? routeName.slice(0, -1) : routeName;
36
+ const pascalName = routeName.charAt(0).toUpperCase() + routeName.slice(1);
37
+ const routerVarName = `router${pascalName}`;
38
+ const basePath = `/${routeName}`;
39
+
40
+ const routesFolder = path.resolve(process.cwd(), "backend/routes");
41
+ if (!fs.existsSync(routesFolder)) {
42
+ fs.mkdirSync(routesFolder, { recursive: true });
43
+ }
44
+
45
+ const filePath = path.join(routesFolder, `${routeName}.js`);
46
+ if (fs.existsSync(filePath)) {
47
+ console.log(chalk.yellow(`[WARN] Route file '${routeName}.js' already exists at backend/routes/${routeName}.js.`));
48
+ return;
39
49
  }
50
+
51
+ const authImport = withAuth
52
+ ? `import Auth from "@seip/blue-bird/core/auth.js";\n`
53
+ : "";
54
+
55
+ const authProtect = withAuth ? `Auth.protect(), ` : "";
56
+
57
+ const content = `import Router from "@seip/blue-bird/core/router.js";
58
+ import Validator from "@seip/blue-bird/core/validate.js";
59
+ import Cache from "@seip/blue-bird/core/cache.js";
60
+ ${authImport}
61
+ const ${routerVarName} = new Router("${basePath}");
62
+
63
+ // Validation schema for incoming requests
64
+ const ${singularName}Schema = {
65
+ name: { required: true, min: 2, max: 255 },
66
+ description: { required: false },
67
+ price: { required: false }
68
+ };
69
+
70
+ const validate${pascalName} = new Validator(${singularName}Schema, "en");
71
+
72
+ /**
73
+ * GET ${basePath}
74
+ * List all items with in-memory / Redis route caching (60 seconds)
75
+ */
76
+ ${routerVarName}.get("/", Cache.middleware(60), (req, res) => {
77
+ res.ok({ ${routeName}: [] }, "${pascalName} list retrieved successfully");
78
+ });
79
+
80
+ /**
81
+ * GET ${basePath}/:id
82
+ * Retrieve a single item by ID
83
+ */
84
+ ${routerVarName}.get("/:id", Cache.middleware(60), (req, res) => {
85
+ const { id } = req.params;
86
+ res.ok({ ${singularName}: { id } }, "${pascalName} retrieved successfully");
87
+ });
88
+
89
+ /**
90
+ * POST ${basePath}
91
+ * Create a new item (with validation and automatic cache invalidation)
92
+ */
93
+ ${routerVarName}.post("/", ${authProtect}validate${pascalName}.middleware(), async (req, res) => {
94
+ const data = req.body;
95
+
96
+ // Invalidate cached route list
97
+ await Cache.delete("${basePath}");
98
+
99
+ res.created({ ${singularName}: data }, "${pascalName} created successfully");
100
+ });
101
+
102
+ /**
103
+ * PUT ${basePath}/:id
104
+ * Update an existing item
105
+ */
106
+ ${routerVarName}.put("/:id", ${authProtect}validate${pascalName}.middleware(), async (req, res) => {
107
+ const { id } = req.params;
108
+ const data = req.body;
109
+
110
+ // Invalidate cached item and list
111
+ await Cache.delete("${basePath}");
112
+ await Cache.delete(\`${basePath}/\${id}\`);
113
+
114
+ res.ok({ ${singularName}: { id, ...data } }, "${pascalName} updated successfully");
115
+ });
116
+
117
+ /**
118
+ * DELETE ${basePath}/:id
119
+ * Delete an existing item
120
+ */
121
+ ${routerVarName}.delete("/:id", ${authProtect}async (req, res) => {
122
+ const { id } = req.params;
123
+
124
+ // Invalidate cache
125
+ await Cache.delete("${basePath}");
126
+ await Cache.delete(\`${basePath}/\${id}\`);
127
+
128
+ res.ok({ id }, "${pascalName} deleted successfully");
129
+ });
130
+
131
+ export default ${routerVarName};
132
+ `;
133
+
134
+ fs.writeFileSync(filePath, content, "utf-8");
135
+ console.log(chalk.green(`[OK] Route '${routeName}' created successfully at backend/routes/${routeName}.js`));
136
+ console.log("");
137
+ console.log(chalk.cyan("To register this route, import it in backend/index.js:"));
138
+ console.log(chalk.gray(` import ${routerVarName} from "./routes/${routeName}.js";`));
139
+ console.log(chalk.gray(` // Pass ${routerVarName} into App({ routes: [...] })`));
140
+ }
40
141
  }
41
142
 
42
143
  const routeCLI = new RouteCLI();
43
- routeCLI.create()
144
+ routeCLI.create();
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  import { execSync } from "node:child_process";
2
4
 
3
5
  class SwaggerCli {