bro-framework 2.2.2 → 2.3.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 +19 -4
- package/bin/bro.js +23 -8
- package/package.json +1 -1
- package/src/index.d.ts +15 -1
- package/src/index.js +1 -0
- package/src/locale.js +118 -0
- package/src/logger.js +2 -2
- package/src/router.js +9 -4
- package/src/sdk.js +10 -1
- package/src/server.js +32 -6
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ export default defineRoute({
|
|
|
97
97
|
Create a `.js` file in the `routes/` directory, and it automatically becomes an endpoint. We use Next.js-style bracket syntax for dynamic parameters. A file named `routes/users/[id].get.js` translates natively to a `GET /users/:id` Express route under the hood.
|
|
98
98
|
|
|
99
99
|
### Bouncer-Grade Validation
|
|
100
|
-
Powered by Zod. Attach a schema to `body`, `query`, or `params` in your route definition. If the client sends malformed data, `bro.js` automatically rejects the request with a structured `400 Bad Request` JSON payload *before* your handler ever executes. You never have to manually validate inputs again.
|
|
100
|
+
Powered by Zod. Attach a schema to `body`, `query`, or `params` directly in your route definition. If the client sends malformed data, `bro.js` automatically rejects the request with a structured `400 Bad Request` JSON payload *before* your handler ever executes. You never have to manually validate inputs again. You can also define a `response` schema to strongly type your OpenAPI documentation (strictly opt-in; arbitrary 200s work out of the box).
|
|
101
101
|
|
|
102
102
|
### Zero-Config JWTs
|
|
103
103
|
Add `auth: true` to your route config. `bro.js` will intercept the request, extract the `Authorization: Bearer <token>` header, verify the signature using your `jwtSecret`, and inject the decoded payload directly into `ctx.user`.
|
|
@@ -115,7 +115,22 @@ Tired of writing frontend `fetch` wrappers? Run `bro sdk`. The CLI will parse yo
|
|
|
115
115
|
Don't spin up a separate worker server. Drop a JavaScript file anywhere in the `tasks/` folder, export a cron string (e.g., `"0 0 * * *"`), and an async handler. `bro.js` natively schedules it as a background worker with full access to your injected database and WebSocket contexts.
|
|
116
116
|
|
|
117
117
|
### Zero-Boilerplate File Uploads
|
|
118
|
-
Add `upload: true` to a route. `bro.js` automatically hooks into `multer`, parses the `multipart/form-data` payload in memory, and injects the files directly into `ctx.files`.
|
|
118
|
+
Add `upload: true` to a route. `bro.js` automatically hooks into `multer`, parses the `multipart/form-data` payload in memory, and injects the files directly into `ctx.files`. It also natively supports granular file limits, restricting max sizes, parts, and fielding counts instantly to protect your RAM. (Note: Use the `storage` configuration for heavy production disk writing to prevent memory exhaustion).
|
|
119
|
+
|
|
120
|
+
### File-Based Locale
|
|
121
|
+
Create a `locale/` folder with one translation file per locale, such as `locale/en.js` and `locale/fr.js`. Export a plain object from each file, then use `t()` in any route:
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
// locale/fr.js
|
|
125
|
+
export default { welcome: 'Bienvenue, {name} !' };
|
|
126
|
+
|
|
127
|
+
// routes/welcome.get.js
|
|
128
|
+
export default defineRoute({
|
|
129
|
+
handler: async ({ t }) => ({ message: t('welcome', { name: 'Sam' }) })
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The locale is negotiated dynamically using RFC 9110 `Accept-Language` headers, supporting full region fallback and custom defaults, and the generated SDK can securely set it via `setLocale('fr')`.
|
|
119
134
|
|
|
120
135
|
---
|
|
121
136
|
|
|
@@ -178,9 +193,9 @@ Add `upload: true` to a route. `bro.js` automatically hooks into `multer`, parse
|
|
|
178
193
|
| Command | Description |
|
|
179
194
|
| :--- | :--- |
|
|
180
195
|
| `bro dev` | Development server featuring instant boot, visual CLI banner, and `chokidar`-powered hot module remapping. |
|
|
181
|
-
| `bro start` | Production runner locked down for security.
|
|
196
|
+
| `bro start` | Production runner locked down for security. Features Graceful Shutdown APIs (with `onShutdown` DB teardown), suppressed internal logs, and isolated API docs. |
|
|
182
197
|
| `bro init` | Automated workspace scaffolder. Generates configuration files and forcefully ensures your `package.json` respects `"type": "module"`. |
|
|
183
|
-
| `bro sdk` | Route parser and browser client compiler. Generates your frontend SDK in one hit. |
|
|
198
|
+
| `bro sdk` | Route parser and browser client compiler. Generates your typed `bro-sdk.js` frontend SDK in one hit. |
|
|
184
199
|
|
|
185
200
|
---
|
|
186
201
|
|
package/bin/bro.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs';
|
|
@@ -37,6 +37,12 @@ export default defineConfig({
|
|
|
37
37
|
jwtSecret: 'dev_secret_please_change',
|
|
38
38
|
expiresIn: '7d'
|
|
39
39
|
},
|
|
40
|
+
|
|
41
|
+
// Optional file-based API translations
|
|
42
|
+
// Add locale/en.js, locale/fr.js, etc.
|
|
43
|
+
locale: {
|
|
44
|
+
defaultLocale: 'en'
|
|
45
|
+
},
|
|
40
46
|
|
|
41
47
|
// API Documentation (Scalar UI)
|
|
42
48
|
docs: process.env.NODE_ENV !== 'production', // Set to false to disable completely, or true to force in prod
|
|
@@ -190,7 +196,8 @@ async function bootstrap() {
|
|
|
190
196
|
process.exit(1);
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
const
|
|
199
|
+
const localeDir = globalConfig.locale?.directory || path.join(cwd, 'locale');
|
|
200
|
+
const { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown } = await createServer(globalConfig, routesDir, db);
|
|
194
201
|
const port = globalConfig.port;
|
|
195
202
|
|
|
196
203
|
let currentRoutes = initialRoutes;
|
|
@@ -217,20 +224,28 @@ async function bootstrap() {
|
|
|
217
224
|
|
|
218
225
|
printCurrentRoutes(currentRoutes);
|
|
219
226
|
|
|
220
|
-
const
|
|
227
|
+
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts}';
|
|
228
|
+
const watcher = chokidar.watch([routesDir, localeGlob], { ignoreInitial: true });
|
|
221
229
|
|
|
222
230
|
watcher.on('all', async (event, filepath) => {
|
|
223
|
-
|
|
224
|
-
|
|
231
|
+
const isJavaScriptFile = filepath.endsWith('.js') || filepath.endsWith('.ts') || filepath.endsWith('.mjs');
|
|
232
|
+
if (!isJavaScriptFile) return;
|
|
233
|
+
const relLocale = path.relative(path.resolve(localeDir), filepath);
|
|
234
|
+
const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
|
|
235
|
+
|
|
225
236
|
try {
|
|
226
237
|
const reloadStartTime = performance.now();
|
|
227
|
-
|
|
238
|
+
if (isLocaleFile) {
|
|
239
|
+
await reloadLocale();
|
|
240
|
+
} else {
|
|
241
|
+
currentRoutes = await reload();
|
|
242
|
+
}
|
|
228
243
|
const reloadTimeMs = performance.now() - reloadStartTime;
|
|
229
244
|
|
|
230
|
-
printHotReload(path.basename(filepath), event, reloadTimeMs);
|
|
245
|
+
printHotReload(path.basename(filepath), event, reloadTimeMs, isLocaleFile ? 'Locale' : 'Route');
|
|
231
246
|
printCurrentRoutes(currentRoutes);
|
|
232
247
|
} catch (err) {
|
|
233
|
-
console.error(`\n ✗ Error hot-reloading routes:`, err);
|
|
248
|
+
console.error(`\n ✗ Error hot-reloading ${isLocaleFile ? 'locale' : 'routes'}:`, err);
|
|
234
249
|
}
|
|
235
250
|
});
|
|
236
251
|
}
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -12,16 +12,18 @@ export interface BroContext<Body = any, Params = any, Query = any> {
|
|
|
12
12
|
db?: any;
|
|
13
13
|
io?: any;
|
|
14
14
|
files?: any[];
|
|
15
|
+
locale: string;
|
|
16
|
+
t: (key: string, values?: Record<string, unknown>) => string;
|
|
15
17
|
error?: any;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface RouteConfig<Body = any, Params = any, Query = any> {
|
|
19
21
|
auth?: boolean;
|
|
20
22
|
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; };
|
|
22
23
|
body?: Body;
|
|
23
24
|
params?: Params;
|
|
24
25
|
query?: Query;
|
|
26
|
+
response?: ZodTypeAny;
|
|
25
27
|
rateLimit?: {
|
|
26
28
|
windowMs: number;
|
|
27
29
|
max: number;
|
|
@@ -34,12 +36,23 @@ export function defineRoute<Body = any, Params = any, Query = any>(
|
|
|
34
36
|
config: RouteConfig<Body, Params, Query>
|
|
35
37
|
): RouteConfig<Body, Params, Query>;
|
|
36
38
|
|
|
39
|
+
export function loadLocale(directory: string, options?: { defaultLocale?: string }): Promise<{
|
|
40
|
+
locales: string[];
|
|
41
|
+
defaultLocale: string;
|
|
42
|
+
resolveLocale: (request: any) => string;
|
|
43
|
+
translate: (locale: string, key: string, values?: Record<string, unknown>) => string;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
37
46
|
export interface BroConfig {
|
|
38
47
|
env?: ZodTypeAny;
|
|
39
48
|
server?: {
|
|
40
49
|
port?: number;
|
|
41
50
|
cors?: boolean | object;
|
|
42
51
|
};
|
|
52
|
+
locale?: {
|
|
53
|
+
directory?: string;
|
|
54
|
+
defaultLocale?: string;
|
|
55
|
+
};
|
|
43
56
|
auth?: {
|
|
44
57
|
jwtSecret?: string;
|
|
45
58
|
expiresIn?: string | number;
|
|
@@ -59,6 +72,7 @@ export interface BroConfig {
|
|
|
59
72
|
};
|
|
60
73
|
db?: () => Promise<any> | any;
|
|
61
74
|
sockets?: (io: any, db: any) => Promise<void> | void;
|
|
75
|
+
onShutdown?: (db: any) => Promise<void> | void;
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
export function defineConfig(config: BroConfig): BroConfig;
|
package/src/index.js
CHANGED
package/src/locale.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const LOCALE_EXTENSIONS = new Set(['.js', '.mjs', '.ts']);
|
|
7
|
+
|
|
8
|
+
function localeFromFilename(fileName) {
|
|
9
|
+
return path.basename(fileName, path.extname(fileName));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeLocale(locale) {
|
|
13
|
+
const normalized = String(locale || '').trim().replaceAll('_', '-').toLowerCase();
|
|
14
|
+
const aliases = {
|
|
15
|
+
english: 'en',
|
|
16
|
+
arabic: 'ar',
|
|
17
|
+
french: 'fr',
|
|
18
|
+
spanish: 'es',
|
|
19
|
+
german: 'de',
|
|
20
|
+
portuguese: 'pt',
|
|
21
|
+
italian: 'it',
|
|
22
|
+
japanese: 'ja',
|
|
23
|
+
korean: 'ko',
|
|
24
|
+
chinese: 'zh'
|
|
25
|
+
};
|
|
26
|
+
return aliases[normalized] || normalized;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function findLocale(locales, requestedLocale, fallbackLocale) {
|
|
30
|
+
const requested = normalizeLocale(requestedLocale);
|
|
31
|
+
const exact = locales.find(locale => normalizeLocale(locale) === requested);
|
|
32
|
+
if (exact) return exact;
|
|
33
|
+
|
|
34
|
+
const language = requested.split('-')[0];
|
|
35
|
+
const languageMatch = locales.find(locale => normalizeLocale(locale).split('-')[0] === language);
|
|
36
|
+
if (languageMatch) return languageMatch;
|
|
37
|
+
|
|
38
|
+
return fallbackLocale;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readMessage(messages, key) {
|
|
42
|
+
return key.split('.').reduce((value, part) => value?.[part], messages);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function interpolate(message, values) {
|
|
46
|
+
return message.replace(/\{(\w+)\}/g, (_, name) => {
|
|
47
|
+
return values[name] === undefined ? `{${name}}` : String(values[name]);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseAcceptLanguage(header) {
|
|
52
|
+
return String(header || '')
|
|
53
|
+
.split(',')
|
|
54
|
+
.map(value => {
|
|
55
|
+
const [tag, qVal] = value.split(';').map(s => s.trim());
|
|
56
|
+
let q = 1;
|
|
57
|
+
if (qVal && qVal.startsWith('q=')) {
|
|
58
|
+
const parsedQ = parseFloat(qVal.slice(2));
|
|
59
|
+
if (!isNaN(parsedQ)) q = parsedQ;
|
|
60
|
+
}
|
|
61
|
+
return { tag, q };
|
|
62
|
+
})
|
|
63
|
+
.filter(item => item.tag && item.q > 0)
|
|
64
|
+
.sort((a, b) => b.q - a.q)
|
|
65
|
+
.map(item => item.tag);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Loads translation files from the application's locale directory.
|
|
70
|
+
* Each file name becomes a locale, for example locale/en-US.js.
|
|
71
|
+
* @param {string} directory
|
|
72
|
+
* @param {{ defaultLocale?: string }} [options]
|
|
73
|
+
*/
|
|
74
|
+
export async function loadLocale(directory, options = {}) {
|
|
75
|
+
const files = fs.existsSync(directory)
|
|
76
|
+
? fs.readdirSync(directory).filter(file => {
|
|
77
|
+
const fullPath = path.join(directory, file);
|
|
78
|
+
return fs.statSync(fullPath).isFile() && LOCALE_EXTENSIONS.has(path.extname(file));
|
|
79
|
+
})
|
|
80
|
+
: [];
|
|
81
|
+
|
|
82
|
+
const messages = {};
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const module = await import(`${pathToFileURL(path.join(directory, file)).href}?update=${crypto.randomUUID()}`);
|
|
85
|
+
const catalog = module.default || module.messages || module;
|
|
86
|
+
if (catalog && typeof catalog === 'object') {
|
|
87
|
+
messages[localeFromFilename(file)] = catalog;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const locales = Object.keys(messages);
|
|
92
|
+
const configuredDefault = options.defaultLocale;
|
|
93
|
+
const defaultLocale = findLocale(locales, configuredDefault, null)
|
|
94
|
+
|| locales.find(locale => ['en', 'en-us'].includes(normalizeLocale(locale)))
|
|
95
|
+
|| locales[0]
|
|
96
|
+
|| 'en';
|
|
97
|
+
|
|
98
|
+
function resolveLocale(request) {
|
|
99
|
+
const requestedLocales = parseAcceptLanguage(request?.headers?.['accept-language']);
|
|
100
|
+
for (const requested of requestedLocales) {
|
|
101
|
+
const match = findLocale(locales, requested, null);
|
|
102
|
+
if (match) return match;
|
|
103
|
+
}
|
|
104
|
+
return defaultLocale;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function translate(locale, key, values = {}) {
|
|
108
|
+
const selectedLocale = findLocale(locales, locale, defaultLocale);
|
|
109
|
+
const message = readMessage(messages[selectedLocale], key)
|
|
110
|
+
?? readMessage(messages[defaultLocale], key);
|
|
111
|
+
|
|
112
|
+
if (message === undefined) return key;
|
|
113
|
+
if (typeof message !== 'string') return message;
|
|
114
|
+
return interpolate(message, values);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { locales, defaultLocale, resolveLocale, translate };
|
|
118
|
+
}
|
package/src/logger.js
CHANGED
|
@@ -72,8 +72,8 @@ export function printRoute(method, routePath, hasAuth, isLast = false) {
|
|
|
72
72
|
console.log(` ${colors.dim}${branch}${colors.reset} ${coloredMethod} ${routePath}${authIcon}`);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export function printHotReload(fileName, event, reloadTimeMs) {
|
|
75
|
+
export function printHotReload(fileName, event, reloadTimeMs, resourceType = 'Route') {
|
|
76
76
|
const time = typeof reloadTimeMs === 'number' ? reloadTimeMs.toFixed(0) : reloadTimeMs;
|
|
77
|
-
console.log(`\n ${colors.cyan}
|
|
77
|
+
console.log(`\n ${colors.cyan}${resourceType} updated:${colors.reset} ${colors.bold}${fileName}${colors.reset} ${colors.dim}(${event})${colors.reset}`);
|
|
78
78
|
console.log(` ${colors.dim}Remapped in ${time}ms${colors.reset}\n`);
|
|
79
79
|
}
|
package/src/router.js
CHANGED
|
@@ -116,10 +116,15 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
|
116
116
|
summary: config.summary || `${method.toUpperCase()} ${routePath}`,
|
|
117
117
|
responses: { '200': { description: 'Successful response' } }
|
|
118
118
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
|
|
120
|
+
if (config.response) {
|
|
121
|
+
operation.responses['200'].content = {
|
|
122
|
+
'application/json': { schema: zodToJsonSchema(config.response) }
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const bodySchema = config.body;
|
|
126
|
+
const paramsSchema = config.params;
|
|
127
|
+
const querySchema = config.query;
|
|
123
128
|
|
|
124
129
|
if (bodySchema) {
|
|
125
130
|
operation.requestBody = {
|
package/src/sdk.js
CHANGED
|
@@ -21,11 +21,16 @@ export async function generateSDK() {
|
|
|
21
21
|
const code = `// Auto-generated by bro.js
|
|
22
22
|
const CONFIG = {
|
|
23
23
|
baseURL: 'http://localhost:5000',
|
|
24
|
-
tokenKey: 'bro_token'
|
|
24
|
+
tokenKey: 'bro_token',
|
|
25
|
+
locale: undefined
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
async function request(method, path, data) {
|
|
28
29
|
const headers = {};
|
|
30
|
+
|
|
31
|
+
if (CONFIG.locale) {
|
|
32
|
+
headers['Accept-Language'] = CONFIG.locale;
|
|
33
|
+
}
|
|
29
34
|
|
|
30
35
|
if (typeof localStorage !== 'undefined') {
|
|
31
36
|
const token = localStorage.getItem(CONFIG.tokenKey);
|
|
@@ -76,6 +81,10 @@ export function setBaseURL(url) {
|
|
|
76
81
|
export function setTokenKey(key) {
|
|
77
82
|
CONFIG.tokenKey = key;
|
|
78
83
|
}
|
|
84
|
+
|
|
85
|
+
export function setLocale(locale) {
|
|
86
|
+
CONFIG.locale = locale;
|
|
87
|
+
}
|
|
79
88
|
`;
|
|
80
89
|
|
|
81
90
|
fs.writeFileSync(path.join(process.cwd(), 'bro-sdk.js'), code, 'utf-8');
|
package/src/server.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import cors from 'cors';
|
|
3
3
|
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
4
5
|
import { Server } from 'socket.io';
|
|
5
6
|
import rateLimit from 'express-rate-limit';
|
|
6
7
|
import multer from 'multer';
|
|
7
8
|
import { apiReference } from '@scalar/express-api-reference';
|
|
8
9
|
import { verifyJwt, signJwt } from './auth.js';
|
|
10
|
+
import { loadLocale } from './locale.js';
|
|
9
11
|
import { loadRoutes } from './router.js';
|
|
10
12
|
import { scanTasks } from './tasks.js';
|
|
11
13
|
|
|
@@ -14,7 +16,7 @@ import { scanTasks } from './tasks.js';
|
|
|
14
16
|
* @param {Object} globalConfig - User's bro.config.js configurations.
|
|
15
17
|
* @param {string} routesDir - Path to the target routes directory.
|
|
16
18
|
* @param {any} db - Initialized database instance.
|
|
17
|
-
* @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, io: import('socket.io').Server }>}
|
|
19
|
+
* @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, reloadLocale: Function, io: import('socket.io').Server, shutdown: Function }>}
|
|
18
20
|
*/
|
|
19
21
|
export async function createServer(globalConfig, routesDir, db) {
|
|
20
22
|
if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
|
|
@@ -23,6 +25,8 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
23
25
|
|
|
24
26
|
const app = express();
|
|
25
27
|
const server = http.createServer(app);
|
|
28
|
+
const localeDirectory = globalConfig.locale?.directory || path.join(process.cwd(), 'locale');
|
|
29
|
+
let locale = await loadLocale(localeDirectory, globalConfig.locale);
|
|
26
30
|
|
|
27
31
|
const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
|
|
28
32
|
|
|
@@ -41,12 +45,14 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
41
45
|
if (globalConfig.sockets) {
|
|
42
46
|
await globalConfig.sockets(io, db);
|
|
43
47
|
}
|
|
44
|
-
|
|
45
48
|
const createHandler = (routeConfig) => {
|
|
46
49
|
const middlewares = [];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
if (routeConfig.schema) {
|
|
51
|
+
throw new Error("Nested 'schema' object is no longer supported in bro.js v2.3.0+. Please use flat, top-level properties (body, query, params) instead.");
|
|
52
|
+
}
|
|
53
|
+
const bodySchema = routeConfig.body;
|
|
54
|
+
const paramsSchema = routeConfig.params;
|
|
55
|
+
const querySchema = routeConfig.query;
|
|
50
56
|
|
|
51
57
|
if (routeConfig.rateLimit) {
|
|
52
58
|
middlewares.push(rateLimit(routeConfig.rateLimit));
|
|
@@ -80,6 +86,7 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
80
86
|
|
|
81
87
|
middlewares.push(async (req, res) => {
|
|
82
88
|
try {
|
|
89
|
+
const requestLocale = locale.resolveLocale(req);
|
|
83
90
|
const ctx = {
|
|
84
91
|
env: globalConfig.envData || process.env,
|
|
85
92
|
db,
|
|
@@ -88,6 +95,8 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
88
95
|
params: req.params,
|
|
89
96
|
query: req.query,
|
|
90
97
|
files: req.files || req.file,
|
|
98
|
+
locale: requestLocale,
|
|
99
|
+
t: (key, values) => locale.translate(requestLocale, key, values),
|
|
91
100
|
user: null,
|
|
92
101
|
jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
|
|
93
102
|
error: (status, message) => {
|
|
@@ -235,17 +244,34 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
235
244
|
return routes;
|
|
236
245
|
};
|
|
237
246
|
|
|
247
|
+
const reloadLocale = async () => {
|
|
248
|
+
locale = await loadLocale(localeDirectory, globalConfig.locale);
|
|
249
|
+
return locale;
|
|
250
|
+
};
|
|
251
|
+
|
|
238
252
|
const initialRoutes = await reload();
|
|
239
253
|
|
|
240
254
|
const taskManager = await scanTasks({ db, io });
|
|
241
255
|
|
|
256
|
+
let isShuttingDown = false;
|
|
242
257
|
const shutdown = async () => {
|
|
258
|
+
if (isShuttingDown) return;
|
|
259
|
+
isShuttingDown = true;
|
|
243
260
|
if (taskManager) taskManager.stopAll();
|
|
244
261
|
if (io) io.close();
|
|
262
|
+
|
|
263
|
+
if (typeof globalConfig.onShutdown === 'function') {
|
|
264
|
+
try {
|
|
265
|
+
await globalConfig.onShutdown(db);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error('[bro.js] Error during database teardown hook:', err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
245
271
|
return new Promise((resolve) => {
|
|
246
272
|
server.close(() => resolve());
|
|
247
273
|
});
|
|
248
274
|
};
|
|
249
275
|
|
|
250
|
-
return { app, server, routes: initialRoutes, reload, io, shutdown };
|
|
276
|
+
return { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown };
|
|
251
277
|
}
|