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 +1 -0
- package/bin/bro.js +15 -8
- package/package.json +1 -1
- package/src/index.d.ts +12 -1
- package/src/router.js +16 -0
- package/src/sdk.js +6 -2
- package/src/server.js +34 -11
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-
|
|
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
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}
|
|
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}
|
|
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'
|
|
56
|
-
|
|
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 (
|
|
97
|
-
const result =
|
|
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 (
|
|
105
|
-
const result =
|
|
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 (
|
|
113
|
-
const result =
|
|
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
|
-
|
|
212
|
-
const routes = await loadRoutes(newRouter, routesDir, createHandler,
|
|
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
|
};
|