bro-framework 2.4.2 → 2.4.4
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 +29 -1
- package/bin/bro.js +15 -7
- package/package.json +9 -1
- package/src/locale.js +1 -1
- package/src/next.d.ts +70 -0
- package/src/next.js +291 -0
- package/src/router.js +0 -1
- package/src/server.js +7 -2
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
- [Architecture & Request Lifecycle](#architecture--request-lifecycle)
|
|
36
36
|
- [Tech Stack Breakdown](#tech-stack-breakdown)
|
|
37
37
|
- [CLI Reference](#cli-reference)
|
|
38
|
+
- [Next.js App Router Integration](#nextjs-app-router-integration)
|
|
38
39
|
- [Author & License](#author--license)
|
|
39
40
|
|
|
40
41
|
---
|
|
@@ -309,9 +310,36 @@ For Redis-backed integration tests without an external Redis server, install `io
|
|
|
309
310
|
| `bro init` | Automated workspace scaffolder. Generates configuration files and forcefully ensures your `package.json` respects `"type": "module"`. |
|
|
310
311
|
| `bro sdk` | Route parser and browser client compiler. Generates your typed `bro-sdk.js` frontend SDK in one hit. |
|
|
311
312
|
|
|
313
|
+
## Next.js App Router Integration
|
|
314
|
+
|
|
315
|
+
You can natively use `bro.js` syntax, Zod validation, and Authentication inside your Next.js API routes (`app/api/.../route.ts`)!
|
|
316
|
+
|
|
317
|
+
First, create your factory instance (e.g., `lib/bro.ts`):
|
|
318
|
+
```typescript
|
|
319
|
+
import { createBro } from 'bro-framework/next';
|
|
320
|
+
|
|
321
|
+
export const { defineRoute, z } = createBro({
|
|
322
|
+
auth: { apiKey: process.env.API_KEY }
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Then use it seamlessly in your route files:
|
|
327
|
+
```typescript
|
|
328
|
+
import { defineRoute, z } from '@/lib/bro';
|
|
329
|
+
|
|
330
|
+
export const POST = defineRoute({
|
|
331
|
+
body: z.object({ name: z.string() }),
|
|
332
|
+
handler: async ({ body }) => {
|
|
333
|
+
return { success: true, hello: body.name };
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
> **Capabilities & Limitations**: Because Next.js API routes are "Serverless" (meaning they sleep when not actively processing a request), features that require a constantly running server such as **WebSockets**, **Background Tasks (Cron)**, **Rate Limiting**, and **Auto-generated Docs** are strictly limited to the standalone `bro.js` framework and are not available in the Next.js adapter.
|
|
339
|
+
|
|
312
340
|
---
|
|
313
341
|
|
|
314
342
|
## Author & License
|
|
315
343
|
|
|
316
|
-
- **Author**:
|
|
344
|
+
- **Author**: Yass1n (@medyass1ne)
|
|
317
345
|
- **License**: MIT
|
package/bin/bro.js
CHANGED
|
@@ -207,7 +207,8 @@ async function bootstrap() {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
const localeDir = globalConfig.locale?.directory || path.join(cwd, 'locale');
|
|
210
|
-
const
|
|
210
|
+
const tasksDir = path.join(cwd, 'tasks');
|
|
211
|
+
const { app, server, routes: initialRoutes, reload, reloadLocale, reloadTasks, io, shutdown } = await createServer(globalConfig, routesDir, db);
|
|
211
212
|
const port = globalConfig.port;
|
|
212
213
|
|
|
213
214
|
let currentRoutes = initialRoutes;
|
|
@@ -234,28 +235,35 @@ async function bootstrap() {
|
|
|
234
235
|
|
|
235
236
|
printCurrentRoutes(currentRoutes);
|
|
236
237
|
|
|
237
|
-
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts}';
|
|
238
|
-
const watcher = chokidar.watch([routesDir, localeGlob], { ignoreInitial: true });
|
|
238
|
+
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts,json}';
|
|
239
|
+
const watcher = chokidar.watch([routesDir, localeGlob, tasksDir], { ignoreInitial: true });
|
|
239
240
|
|
|
240
241
|
watcher.on('all', async (event, filepath) => {
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
242
|
+
const isValidFile = filepath.match(/\.(js|ts|mjs|json)$/);
|
|
243
|
+
if (!isValidFile) return;
|
|
243
244
|
const relLocale = path.relative(path.resolve(localeDir), filepath);
|
|
244
245
|
const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
|
|
246
|
+
|
|
247
|
+
const relTask = path.relative(path.resolve(tasksDir), filepath);
|
|
248
|
+
const isTaskFile = !relTask.startsWith('..') && !path.isAbsolute(relTask);
|
|
245
249
|
|
|
246
250
|
try {
|
|
247
251
|
const reloadStartTime = performance.now();
|
|
248
252
|
if (isLocaleFile) {
|
|
249
253
|
await reloadLocale();
|
|
254
|
+
} else if (isTaskFile) {
|
|
255
|
+
await reloadTasks();
|
|
250
256
|
} else {
|
|
251
257
|
currentRoutes = await reload();
|
|
252
258
|
}
|
|
253
259
|
const reloadTimeMs = performance.now() - reloadStartTime;
|
|
254
260
|
|
|
255
|
-
|
|
261
|
+
const fileType = isTaskFile ? 'Task' : (isLocaleFile ? 'Locale' : 'Route');
|
|
262
|
+
printHotReload(path.basename(filepath), event, reloadTimeMs, fileType);
|
|
256
263
|
printCurrentRoutes(currentRoutes);
|
|
257
264
|
} catch (err) {
|
|
258
|
-
|
|
265
|
+
const fileType = isTaskFile ? 'tasks' : (isLocaleFile ? 'locale' : 'routes');
|
|
266
|
+
console.error(`\n ✗ Error hot-reloading ${fileType}:`, err);
|
|
259
267
|
}
|
|
260
268
|
});
|
|
261
269
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bro-framework",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.4",
|
|
4
4
|
"description": "The No-BS Backend Framework for Node.js",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
"exports": {
|
|
16
16
|
".": {
|
|
17
17
|
"types": "./src/index.d.ts",
|
|
18
|
+
"import": "./src/index.js",
|
|
19
|
+
"require": "./src/index.js",
|
|
18
20
|
"default": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./next": {
|
|
23
|
+
"types": "./src/next.d.ts",
|
|
24
|
+
"import": "./src/next.js",
|
|
25
|
+
"require": "./src/next.js",
|
|
26
|
+
"default": "./src/next.js"
|
|
19
27
|
}
|
|
20
28
|
},
|
|
21
29
|
"bin": {
|
package/src/locale.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
|
|
6
|
-
const LOCALE_EXTENSIONS = new Set(['.js', '.mjs', '.ts']);
|
|
6
|
+
const LOCALE_EXTENSIONS = new Set(['.js', '.mjs', '.ts', '.json']);
|
|
7
7
|
|
|
8
8
|
function localeFromFilename(fileName) {
|
|
9
9
|
return path.basename(fileName, path.extname(fileName));
|
package/src/next.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z, ZodTypeAny } from 'zod';
|
|
2
|
+
|
|
3
|
+
export { z };
|
|
4
|
+
|
|
5
|
+
export interface UploadedFile {
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
size: number;
|
|
9
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
10
|
+
stream: () => ReadableStream;
|
|
11
|
+
text: () => Promise<string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NextBroGlobalConfig<TDb = any> {
|
|
15
|
+
env?: ZodTypeAny;
|
|
16
|
+
locales?: Record<string, any>;
|
|
17
|
+
defaultLocale?: string;
|
|
18
|
+
redisUrl?: string;
|
|
19
|
+
rateLimit?: { windowMs: number; max: number; };
|
|
20
|
+
auth?: {
|
|
21
|
+
jwtSecret?: string;
|
|
22
|
+
apiKey?: string | string[];
|
|
23
|
+
expiresIn?: string | number;
|
|
24
|
+
};
|
|
25
|
+
db?: TDb | Promise<TDb> | (() => TDb | Promise<TDb>) | { init: () => TDb | Promise<TDb> };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TDb = any> {
|
|
29
|
+
req: Request;
|
|
30
|
+
env: Record<string, string | undefined>;
|
|
31
|
+
db: TDb;
|
|
32
|
+
redis: any;
|
|
33
|
+
io: { emit: (event: string, data: any) => void };
|
|
34
|
+
body: TBody extends ZodTypeAny ? z.infer<TBody> : any;
|
|
35
|
+
query: TQuery extends ZodTypeAny ? z.infer<TQuery> : any;
|
|
36
|
+
params: TParams extends ZodTypeAny ? z.infer<TParams> : any;
|
|
37
|
+
file?: UploadedFile;
|
|
38
|
+
files?: Record<string, UploadedFile[]>;
|
|
39
|
+
locale: string;
|
|
40
|
+
t: (key: string, values?: any) => string;
|
|
41
|
+
user?: any;
|
|
42
|
+
jwt: { sign: (payload: any, opts?: any) => string };
|
|
43
|
+
error: (status: number, message: string) => never;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TDb = any> {
|
|
47
|
+
auth?: boolean | string[] | 'api-key' | string;
|
|
48
|
+
body?: TBody;
|
|
49
|
+
query?: TQuery;
|
|
50
|
+
params?: TParams;
|
|
51
|
+
cache?: number;
|
|
52
|
+
rateLimit?: { windowMs: number; max: number; } | false;
|
|
53
|
+
response?: ZodTypeAny;
|
|
54
|
+
summary?: string;
|
|
55
|
+
upload?: any;
|
|
56
|
+
handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TDb>) => Promise<any> | any;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BroNextInstance<TDb = any> {
|
|
60
|
+
z: typeof z;
|
|
61
|
+
defineRoute: <
|
|
62
|
+
TBody extends ZodTypeAny = any,
|
|
63
|
+
TQuery extends ZodTypeAny = any,
|
|
64
|
+
TParams extends ZodTypeAny = any
|
|
65
|
+
>(
|
|
66
|
+
config: NextRouteConfig<TBody, TQuery, TParams, TDb>
|
|
67
|
+
) => (req: Request | any, context: any) => Promise<any>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export declare function createBro<TDb = any>(config?: NextBroGlobalConfig<TDb>): BroNextInstance<TDb>;
|
package/src/next.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { verifyJwt, signJwt } from './auth.js';
|
|
4
|
+
import { createClient } from 'redis';
|
|
5
|
+
|
|
6
|
+
export { z };
|
|
7
|
+
|
|
8
|
+
export function createBro(globalConfig = {}) {
|
|
9
|
+
let isInitialized = false;
|
|
10
|
+
let initPromise = null;
|
|
11
|
+
|
|
12
|
+
let globalDb = null;
|
|
13
|
+
let globalRedis = null;
|
|
14
|
+
let globalLocale = null;
|
|
15
|
+
|
|
16
|
+
async function ensureInitialized() {
|
|
17
|
+
if (isInitialized) return;
|
|
18
|
+
if (initPromise) return initPromise;
|
|
19
|
+
|
|
20
|
+
initPromise = (async () => {
|
|
21
|
+
try {
|
|
22
|
+
if (globalConfig.env) {
|
|
23
|
+
try {
|
|
24
|
+
globalConfig.env.parse(process.env);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('[bro.js/next] Environment Validation Error:', err);
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof globalConfig.db === 'function') {
|
|
32
|
+
globalDb = await globalConfig.db();
|
|
33
|
+
} else if (globalConfig.db && typeof globalConfig.db.init === 'function') {
|
|
34
|
+
globalDb = await globalConfig.db.init();
|
|
35
|
+
} else if (globalConfig.db) {
|
|
36
|
+
globalDb = globalConfig.db;
|
|
37
|
+
if (globalDb instanceof Promise) globalDb = await globalDb;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (globalConfig.redisUrl) {
|
|
41
|
+
globalRedis = createClient({ url: globalConfig.redisUrl });
|
|
42
|
+
globalRedis.on('error', (err) => console.error('[bro.js/next] Redis Error:', err));
|
|
43
|
+
if (globalRedis.status === 'wait' || !globalRedis.status) {
|
|
44
|
+
await globalRedis.connect().catch(err => {
|
|
45
|
+
if (!err.message.includes('already connecting') && !err.message.includes('already connected')) {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isInitialized = true;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error('[bro.js/next] Initialization Error:', err);
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
|
|
59
|
+
return initPromise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const resolveLocale = (headers) => {
|
|
63
|
+
const acceptLanguage = headers['accept-language'] || '';
|
|
64
|
+
const preferredLanguages = acceptLanguage
|
|
65
|
+
.split(',')
|
|
66
|
+
.map(lang => lang.split(';')[0].trim().toLowerCase())
|
|
67
|
+
.filter(lang => lang);
|
|
68
|
+
|
|
69
|
+
const configuredLocales = Object.keys(globalConfig.locales || {});
|
|
70
|
+
|
|
71
|
+
for (const lang of preferredLanguages) {
|
|
72
|
+
if (configuredLocales.includes(lang)) {
|
|
73
|
+
return lang;
|
|
74
|
+
}
|
|
75
|
+
const baseLang = lang.split('-')[0];
|
|
76
|
+
if (configuredLocales.includes(baseLang)) {
|
|
77
|
+
return baseLang;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return globalConfig.defaultLocale || 'en';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const translate = (locale, key, values = {}) => {
|
|
85
|
+
let messages = globalConfig.locales?.[locale] || globalConfig.locales?.[globalConfig.defaultLocale || 'en'];
|
|
86
|
+
if (!messages) return key;
|
|
87
|
+
|
|
88
|
+
// Handle Webpack / ES module JSON interop where the object is under .default
|
|
89
|
+
if (messages.default && typeof messages.default === 'object') {
|
|
90
|
+
messages = messages.default;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const message = key.split('.').reduce((acc, part) => acc && acc[part], messages);
|
|
94
|
+
if (!message || typeof message !== 'string') return key;
|
|
95
|
+
|
|
96
|
+
return message.replace(/\{(\w+)\}/g, (_, name) => {
|
|
97
|
+
return values[name] !== undefined ? String(values[name]) : `{${name}}`;
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const errorHelper = (status, message) => {
|
|
102
|
+
const err = new Error(message);
|
|
103
|
+
err.status = status;
|
|
104
|
+
throw err;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function defineRoute(config) {
|
|
108
|
+
return async function (req, context) {
|
|
109
|
+
try {
|
|
110
|
+
await ensureInitialized();
|
|
111
|
+
|
|
112
|
+
const resolvedLocale = resolveLocale(Object.fromEntries(req.headers.entries()));
|
|
113
|
+
|
|
114
|
+
// Auth extraction early for Identity caching
|
|
115
|
+
let user = null;
|
|
116
|
+
let apiKeyUsed = null;
|
|
117
|
+
if (config.auth) {
|
|
118
|
+
if (config.auth === 'api-key') {
|
|
119
|
+
const apiKey = req.headers.get('x-api-key') || req.headers.get('authorization')?.replace('Bearer ', '');
|
|
120
|
+
const configuredKey = globalConfig?.auth?.apiKey || process.env.API_KEY;
|
|
121
|
+
|
|
122
|
+
let isValid = false;
|
|
123
|
+
if (configuredKey) {
|
|
124
|
+
const keys = (Array.isArray(configuredKey) ? configuredKey : configuredKey.split(',')).map(k => String(k).trim());
|
|
125
|
+
isValid = keys.includes(apiKey);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!isValid) {
|
|
129
|
+
return NextResponse.json({ error: 'Unauthorized', details: 'Missing or invalid API key' }, { status: 401 });
|
|
130
|
+
}
|
|
131
|
+
apiKeyUsed = apiKey;
|
|
132
|
+
} else {
|
|
133
|
+
const authHeader = req.headers.get('authorization');
|
|
134
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
135
|
+
return NextResponse.json({ error: 'Unauthorized', details: 'Missing Bearer token' }, { status: 401 });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const token = authHeader.split(' ')[1];
|
|
139
|
+
const secret = globalConfig?.auth?.jwtSecret || process.env.JWT_SECRET;
|
|
140
|
+
|
|
141
|
+
if (!secret) {
|
|
142
|
+
return NextResponse.json({ error: 'Internal Server Error', details: 'JWT_SECRET is not configured' }, { status: 500 });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const decoded = verifyJwt(token, secret);
|
|
146
|
+
if (!decoded.valid) {
|
|
147
|
+
return NextResponse.json({ error: 'Unauthorized', details: decoded.error }, { status: 401 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
user = decoded.payload;
|
|
151
|
+
|
|
152
|
+
if (Array.isArray(config.auth) && config.auth.length > 0) {
|
|
153
|
+
if (!user.role || !config.auth.includes(user.role)) {
|
|
154
|
+
return NextResponse.json({ error: 'Forbidden', details: 'Insufficient permissions' }, { status: 403 });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Rate Limiting
|
|
161
|
+
const activeRateLimit = config.rateLimit === false ? null : (config.rateLimit || globalConfig.rateLimit);
|
|
162
|
+
if (activeRateLimit && globalRedis) {
|
|
163
|
+
const ip = req.headers.get('x-forwarded-for') || 'ip';
|
|
164
|
+
const urlObj = new URL(req.url);
|
|
165
|
+
const rlKey = `rate-limit:${urlObj.pathname}:${ip}`;
|
|
166
|
+
const currentCount = await globalRedis.incr(rlKey);
|
|
167
|
+
if (currentCount === 1) {
|
|
168
|
+
await globalRedis.expire(rlKey, Math.ceil(activeRateLimit.windowMs / 1000));
|
|
169
|
+
}
|
|
170
|
+
if (currentCount > activeRateLimit.max) {
|
|
171
|
+
return NextResponse.json({ error: 'Too Many Requests' }, { status: 429 });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Caching
|
|
176
|
+
let cacheKey = null;
|
|
177
|
+
if (config.cache && globalRedis && req.method === 'GET') {
|
|
178
|
+
const urlObj = new URL(req.url);
|
|
179
|
+
const identity = user ? (user.id || user.role || 'user') : (apiKeyUsed || 'anon');
|
|
180
|
+
cacheKey = `cache:${urlObj.pathname}${urlObj.search}:${resolvedLocale}:${identity}`;
|
|
181
|
+
|
|
182
|
+
const cachedData = await globalRedis.get(cacheKey);
|
|
183
|
+
if (cachedData) {
|
|
184
|
+
return NextResponse.json(JSON.parse(cachedData), { status: 200 });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rawParams = context?.params ? await context.params : {};
|
|
189
|
+
const url = new URL(req.url);
|
|
190
|
+
const rawQuery = Object.fromEntries(url.searchParams.entries());
|
|
191
|
+
|
|
192
|
+
let rawBody = {};
|
|
193
|
+
const parsedFiles = {};
|
|
194
|
+
let totalFiles = 0;
|
|
195
|
+
|
|
196
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
197
|
+
const contentType = req.headers.get('content-type') || '';
|
|
198
|
+
|
|
199
|
+
if (contentType.includes('multipart/form-data')) {
|
|
200
|
+
try {
|
|
201
|
+
const formData = await req.formData();
|
|
202
|
+
for (const [key, value] of formData.entries()) {
|
|
203
|
+
if (value instanceof File || value instanceof Blob) {
|
|
204
|
+
if (!parsedFiles[key]) parsedFiles[key] = [];
|
|
205
|
+
parsedFiles[key].push(value);
|
|
206
|
+
totalFiles++;
|
|
207
|
+
} else {
|
|
208
|
+
rawBody[key] = value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {}
|
|
212
|
+
} else {
|
|
213
|
+
try {
|
|
214
|
+
rawBody = await req.json();
|
|
215
|
+
} catch (err) {}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let body, params, query;
|
|
220
|
+
try {
|
|
221
|
+
if (config.body) body = await config.body.parseAsync(rawBody);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (err instanceof z.ZodError) return NextResponse.json({ error: 'Invalid Request Body', details: err.issues }, { status: 400 });
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
if (config.params) params = await config.params.parseAsync(rawParams);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (err instanceof z.ZodError) return NextResponse.json({ error: 'Invalid URL Parameters', details: err.issues }, { status: 400 });
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
if (config.query) query = await config.query.parseAsync(rawQuery);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err instanceof z.ZodError) return NextResponse.json({ error: 'Invalid Query Parameters', details: err.issues }, { status: 400 });
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let ctxFile = undefined;
|
|
240
|
+
let ctxFiles = undefined;
|
|
241
|
+
if (totalFiles === 1) {
|
|
242
|
+
const keys = Object.keys(parsedFiles);
|
|
243
|
+
ctxFile = parsedFiles[keys[0]][0];
|
|
244
|
+
ctxFiles = parsedFiles;
|
|
245
|
+
} else if (totalFiles > 1) {
|
|
246
|
+
ctxFiles = parsedFiles;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const ctx = {
|
|
250
|
+
req,
|
|
251
|
+
env: process.env,
|
|
252
|
+
db: globalDb,
|
|
253
|
+
redis: globalRedis,
|
|
254
|
+
io: { emit: () => console.warn('[bro.js/next] WebSockets require standard bro.js server.') },
|
|
255
|
+
body,
|
|
256
|
+
params,
|
|
257
|
+
query,
|
|
258
|
+
file: ctxFile,
|
|
259
|
+
files: ctxFiles,
|
|
260
|
+
locale: resolvedLocale,
|
|
261
|
+
t: (key, values) => translate(resolvedLocale, key, values),
|
|
262
|
+
user,
|
|
263
|
+
jwt: {
|
|
264
|
+
sign: (payload, opts) => signJwt(payload, globalConfig?.auth?.jwtSecret || process.env.JWT_SECRET, Object.assign({ expiresIn: globalConfig?.auth?.expiresIn || '1d' }, opts || {}))
|
|
265
|
+
},
|
|
266
|
+
error: errorHelper
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await config.handler(ctx);
|
|
270
|
+
|
|
271
|
+
if (cacheKey && globalRedis) {
|
|
272
|
+
await globalRedis.set(cacheKey, JSON.stringify(result), { EX: config.cache });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return NextResponse.json(result, { status: 200 });
|
|
276
|
+
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error.status) {
|
|
279
|
+
return NextResponse.json({ error: error.message }, { status: error.status });
|
|
280
|
+
}
|
|
281
|
+
console.error('[bro.js/next] Unhandled Error:', error);
|
|
282
|
+
return NextResponse.json(
|
|
283
|
+
{ error: 'Internal Server Error', message: error.message },
|
|
284
|
+
{ status: 500 }
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { defineRoute, z };
|
|
291
|
+
}
|
package/src/router.js
CHANGED
|
@@ -157,7 +157,6 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// Auto-inject security definition if auth is true
|
|
161
160
|
if (config.auth === 'api-key') {
|
|
162
161
|
operation.security = [{ apiKeyAuth: [] }];
|
|
163
162
|
} else if (config.auth) {
|
package/src/server.js
CHANGED
|
@@ -407,7 +407,12 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
407
407
|
|
|
408
408
|
const initialRoutes = await reload();
|
|
409
409
|
|
|
410
|
-
|
|
410
|
+
let taskManager = await scanTasks({ db, io });
|
|
411
|
+
|
|
412
|
+
const reloadTasks = async () => {
|
|
413
|
+
if (taskManager) taskManager.stopAll();
|
|
414
|
+
taskManager = await scanTasks({ db, io });
|
|
415
|
+
};
|
|
411
416
|
|
|
412
417
|
let isShuttingDown = false;
|
|
413
418
|
const shutdown = async () => {
|
|
@@ -434,5 +439,5 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
434
439
|
});
|
|
435
440
|
};
|
|
436
441
|
|
|
437
|
-
return { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown };
|
|
442
|
+
return { app, server, routes: initialRoutes, reload, reloadLocale, reloadTasks, io, shutdown };
|
|
438
443
|
}
|