bro-framework 2.1.0 → 2.2.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/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  <a href="https://brojs.yessindevs.me">Documentation Website</a>
18
18
  </p>
19
19
  </p>
20
+
20
21
  ---
21
22
 
22
23
  > "NestJS wants four decorators, three modules, and an existential crisis just to handle a GET request. Express makes you write the same 40 lines of CORS, JSON parsing, and auth middleware for every project. bro.js gives you file routing, auto-validation, JWT auth, WebSockets, and live docs out of the box. Be honest: you just want to return an object."
package/bin/bro.js CHANGED
@@ -9,7 +9,7 @@ register();
9
9
 
10
10
  import { createServer } from '../src/server.js';
11
11
  import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
12
- import { scanTasks } from '../src/tasks.js';
12
+ import { scanTasks, stopTasks } from '../src/tasks.js';
13
13
  import { generateSDK } from '../src/sdk.js';
14
14
  import dotenv from 'dotenv';
15
15
  import chokidar from 'chokidar';
@@ -122,7 +122,7 @@ if (command === 'init') {
122
122
 
123
123
  if (['sdk', 'generate-client', 'client'].includes(command)) {
124
124
  generateSDK().then(() => {
125
- console.log(`\n ${colors.green} bro-client.js generated successfully!${colors.reset}\n`);
125
+ console.log(`\n ${colors.green} bro-sdk.js generated successfully!${colors.reset}\n`);
126
126
  process.exit(0);
127
127
  }).catch(err => {
128
128
  console.error(`\n ${colors.red} Error generating SDK:${colors.reset}`, err.message);
@@ -185,12 +185,6 @@ async function bootstrap() {
185
185
  globalConfig.envData = envResult.data;
186
186
  }
187
187
 
188
- if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
189
- console.error(`\n ✗ CRITICAL SECURITY ERROR: You are running in production with a default JWT secret!`);
190
- console.error(` Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.`);
191
- process.exit(1);
192
- }
193
-
194
188
  if (!fs.existsSync(routesDir)) {
195
189
  console.error(`✗ Error: 'routes' directory not found in ${cwd}`);
196
190
  console.error(` Please create a 'routes/' folder and add your first route.`);
@@ -243,6 +237,19 @@ async function bootstrap() {
243
237
  }
244
238
  });
245
239
  }
240
+
241
+ const handleShutdown = async (signal) => {
242
+ console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
243
+ stopTasks();
244
+ if (io) io.close();
245
+ server.close(() => {
246
+ console.log('[bro.js] HTTP server closed.');
247
+ process.exit(0);
248
+ });
249
+ };
250
+
251
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
252
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
246
253
  });
247
254
  }
248
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bro-framework",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "The No-BS Backend Framework for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.d.ts CHANGED
@@ -3,6 +3,8 @@ import { z, ZodTypeAny } from 'zod';
3
3
  type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
4
4
 
5
5
  export interface BroContext<Body = any, Params = any, Query = any> {
6
+ env?: any;
7
+ jwt?: { sign: (payload: any, options?: any) => string };
6
8
  body: InferZod<Body>;
7
9
  params: InferZod<Params>;
8
10
  query: InferZod<Query>;
@@ -15,7 +17,8 @@ export interface BroContext<Body = any, Params = any, Query = any> {
15
17
 
16
18
  export interface RouteConfig<Body = any, Params = any, Query = any> {
17
19
  auth?: boolean;
18
- upload?: boolean;
20
+ upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
21
+ schema?: { body?: Body; params?: Params; query?: Query; };
19
22
  body?: Body;
20
23
  params?: Params;
21
24
  query?: Query;
@@ -46,6 +49,14 @@ export interface BroConfig {
46
49
  windowMs: number;
47
50
  max: number;
48
51
  };
52
+ upload?: {
53
+ limits?: {
54
+ fileSize?: number;
55
+ files?: number;
56
+ fields?: number;
57
+ [key: string]: any;
58
+ };
59
+ };
49
60
  db?: () => Promise<any> | any;
50
61
  sockets?: (io: any, db: any) => Promise<void> | void;
51
62
  }
package/src/router.js CHANGED
@@ -142,6 +142,22 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
142
142
  operation.security = [{ bearerAuth: [] }];
143
143
  }
144
144
 
145
+ if (config.upload) {
146
+ operation.requestBody = operation.requestBody || { content: {} };
147
+ operation.requestBody.content['multipart/form-data'] = {
148
+ schema: { type: 'object' }
149
+ };
150
+ }
151
+
152
+ if (bodySchema || paramsSchema || querySchema) {
153
+ operation.responses['400'] = { $ref: '#/components/responses/BadRequest' };
154
+ }
155
+ if (config.auth) {
156
+ operation.responses['401'] = { $ref: '#/components/responses/Unauthorized' };
157
+ }
158
+ operation.responses['404'] = { $ref: '#/components/responses/NotFound' };
159
+ operation.responses['500'] = { $ref: '#/components/responses/ServerError' };
160
+
145
161
  openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
146
162
  }
147
163
 
package/src/sdk.js CHANGED
@@ -116,9 +116,13 @@ function generateApiObject(endpoints) {
116
116
  function renderTree(node, indent = ' ') {
117
117
  let result = '';
118
118
 
119
+ const isValidIdentifier = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key);
120
+
119
121
  for (const [key, val] of Object.entries(node)) {
122
+ const formattedKey = isValidIdentifier(key) ? key : `"${key}"`;
123
+
120
124
  if (val._isParam) {
121
- result += `${indent}"${key}": (${key}) => ({\n`;
125
+ result += `${indent}${formattedKey}: (${key}) => ({\n`;
122
126
 
123
127
  for (const [m, p] of Object.entries(val._methods)) {
124
128
  const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${encodeURIComponent($1)}');
@@ -132,7 +136,7 @@ function generateApiObject(endpoints) {
132
136
 
133
137
  result += `${indent}}),\n`;
134
138
  } else {
135
- result += `${indent}"${key}": {\n`;
139
+ result += `${indent}${formattedKey}: {\n`;
136
140
  for (const [m, p] of Object.entries(val._methods)) {
137
141
  result += `${indent} ${m}: (data) => request('${m}', '${p}', data),\n`;
138
142
  }
package/src/server.js CHANGED
@@ -16,6 +16,10 @@ import { loadRoutes } from './router.js';
16
16
  * @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, io: import('socket.io').Server }>}
17
17
  */
18
18
  export async function createServer(globalConfig, routesDir, db) {
19
+ if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
20
+ throw new Error('CRITICAL SECURITY ERROR: You are running in production with a default JWT secret! Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.');
21
+ }
22
+
19
23
  const app = express();
20
24
  const server = http.createServer(app);
21
25
 
@@ -39,6 +43,9 @@ export async function createServer(globalConfig, routesDir, db) {
39
43
 
40
44
  const createHandler = (routeConfig) => {
41
45
  const middlewares = [];
46
+ const bodySchema = routeConfig.schema?.body || routeConfig.body;
47
+ const paramsSchema = routeConfig.schema?.params || routeConfig.params;
48
+ const querySchema = routeConfig.schema?.query || routeConfig.query;
42
49
 
43
50
  if (routeConfig.rateLimit) {
44
51
  middlewares.push(rateLimit(routeConfig.rateLimit));
@@ -52,10 +59,25 @@ export async function createServer(globalConfig, routesDir, db) {
52
59
  fields: 20
53
60
  }
54
61
  };
55
- if (typeof routeConfig.upload === 'object' && routeConfig.upload.limits) {
56
- routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
62
+ if (typeof routeConfig.upload === 'object') {
63
+ if (routeConfig.upload.limits) {
64
+ routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
65
+ }
66
+ if (routeConfig.upload.fileFilter) routeMulterConfig.fileFilter = routeConfig.upload.fileFilter;
67
+ if (routeConfig.upload.storage) routeMulterConfig.storage = routeConfig.upload.storage;
68
+ }
69
+
70
+ const uploadParser = multer(routeMulterConfig);
71
+
72
+ if (typeof routeConfig.upload === 'object' && routeConfig.upload.fields) {
73
+ middlewares.push(uploadParser.fields(routeConfig.upload.fields));
74
+ } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.single) {
75
+ middlewares.push(uploadParser.single(routeConfig.upload.single));
76
+ } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.array) {
77
+ middlewares.push(uploadParser.array(routeConfig.upload.array));
78
+ } else {
79
+ middlewares.push(uploadParser.any());
57
80
  }
58
- middlewares.push(multer(routeMulterConfig).any());
59
81
  }
60
82
 
61
83
  middlewares.push(async (req, res) => {
@@ -93,24 +115,24 @@ export async function createServer(globalConfig, routesDir, db) {
93
115
  ctx.user = authResult.payload;
94
116
  }
95
117
 
96
- if (routeConfig.params) {
97
- const result = routeConfig.params.safeParse(req.params);
118
+ if (paramsSchema) {
119
+ const result = paramsSchema.safeParse(req.params);
98
120
  if (!result.success) {
99
121
  return res.status(400).json({ error: 'Invalid URL Parameters', details: result.error.flatten() });
100
122
  }
101
123
  ctx.params = result.data;
102
124
  }
103
125
 
104
- if (routeConfig.body) {
105
- const result = routeConfig.body.safeParse(req.body);
126
+ if (bodySchema) {
127
+ const result = bodySchema.safeParse(req.body);
106
128
  if (!result.success) {
107
129
  return res.status(400).json({ error: 'Invalid Request Body', details: result.error.flatten() });
108
130
  }
109
131
  ctx.body = result.data;
110
132
  }
111
133
 
112
- if (routeConfig.query) {
113
- const result = routeConfig.query.safeParse(req.query);
134
+ if (querySchema) {
135
+ const result = querySchema.safeParse(req.query);
114
136
  if (!result.success) {
115
137
  return res.status(400).json({ error: 'Invalid Query Parameters', details: result.error.flatten() });
116
138
  }
@@ -208,8 +230,9 @@ export async function createServer(globalConfig, routesDir, db) {
208
230
 
209
231
  const reload = async () => {
210
232
  const newRouter = express.Router();
211
- openApiSpec.paths = {};
212
- const routes = await loadRoutes(newRouter, routesDir, createHandler, openApiSpec);
233
+ const tempSpec = { paths: {} };
234
+ const routes = await loadRoutes(newRouter, routesDir, createHandler, tempSpec);
235
+ openApiSpec.paths = tempSpec.paths;
213
236
  routeStack = newRouter;
214
237
  return routes;
215
238
  };