bro-framework 2.4.4 → 3.0.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 +44 -0
- package/bin/bro.js +194 -292
- package/package.json +138 -103
- package/src/dashboard.js +80 -0
- package/src/database.js +105 -0
- package/src/edge.js +244 -0
- package/src/engine.js +245 -0
- package/src/index.d.ts +31 -11
- package/src/index.js +10 -0
- package/src/logger.js +47 -0
- package/src/next.d.ts +9 -9
- package/src/next.js +91 -129
- package/src/observability.js +81 -0
- package/src/plugins.js +135 -0
- package/src/policy-auth.js +88 -0
- package/src/router.js +1 -1
- package/src/sdk.js +229 -169
- package/src/server.js +181 -170
- package/src/studio.js +162 -0
- package/src/task-engine.js +88 -0
- package/src/testing.js +142 -0
- package/src/uploads.js +129 -0
package/src/engine.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
|
|
2
|
+
export function resolveIdentity(req, config = {}) {
|
|
3
|
+
const headers = req.headers || {};
|
|
4
|
+
let ip = req.ip || (req.socket && req.socket.remoteAddress) || 'anonymous';
|
|
5
|
+
|
|
6
|
+
const getHeader = (k) => {
|
|
7
|
+
if (typeof headers.get === 'function') return headers.get(k);
|
|
8
|
+
if (typeof headers[k] === 'string') return headers[k];
|
|
9
|
+
if (Array.isArray(headers[k])) return headers[k][0];
|
|
10
|
+
return null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (config.trustProxy) {
|
|
14
|
+
const xff = getHeader('x-forwarded-for');
|
|
15
|
+
if (xff) {
|
|
16
|
+
const ips = xff.split(',').map(s => s.trim()).filter(Boolean);
|
|
17
|
+
if (ips.length > 0) {
|
|
18
|
+
ip = ips[0];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const auth = getHeader('authorization');
|
|
24
|
+
return getHeader('x-api-key') || (auth && auth.startsWith('Bearer ') ? auth.split(' ')[1] : null) || ip;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export class RouteRegistry {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.routes = [];
|
|
34
|
+
}
|
|
35
|
+
register(routeInfo) {
|
|
36
|
+
this.routes.push(routeInfo);
|
|
37
|
+
}
|
|
38
|
+
getRoutes() {
|
|
39
|
+
return this.routes;
|
|
40
|
+
}
|
|
41
|
+
reset() {
|
|
42
|
+
this.routes = [];
|
|
43
|
+
}
|
|
44
|
+
clear() {
|
|
45
|
+
this.routes = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validates Zod errors into RFC 7807 problem details
|
|
51
|
+
*/
|
|
52
|
+
export function formatZodError(zodError) {
|
|
53
|
+
const details = {};
|
|
54
|
+
for (const err of zodError.errors) {
|
|
55
|
+
details[err.path.join('.')] = err.message;
|
|
56
|
+
}
|
|
57
|
+
return details;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatErrorEnvelope(status, title, details, reqId, instanceUrl = 'about:blank') {
|
|
61
|
+
const codeMap = { 400: 'BAD_REQUEST', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 429: 'TOO_MANY_REQUESTS' };
|
|
62
|
+
const code = codeMap[status] || (status >= 500 ? 'INTERNAL_ERROR' : 'ERROR');
|
|
63
|
+
|
|
64
|
+
const payload = {
|
|
65
|
+
type: `https://brojs.dev/errors/${code.toLowerCase()}`,
|
|
66
|
+
title,
|
|
67
|
+
status,
|
|
68
|
+
instance: instanceUrl,
|
|
69
|
+
requestId: reqId
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (details) {
|
|
73
|
+
if (typeof details === 'string') payload.detail = details;
|
|
74
|
+
else payload.errors = details;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return payload;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The transport-agnostic execution pipeline.
|
|
82
|
+
* @param {Object} routeConfig - The exported configuration from defineRoute.
|
|
83
|
+
* @param {Object} requestData - Standardized request object: { method, originalUrl, headers, body, query, params, ip, files }
|
|
84
|
+
* @param {Object} globalConfig - The global framework configuration.
|
|
85
|
+
* @param {Object} ctxExtras - Additional context properties (e.g. db, redis, io, pluginManager).
|
|
86
|
+
* @returns {Promise<{ status: number, body: any, headers: Record<string, string>, error?: Error }>}
|
|
87
|
+
*/
|
|
88
|
+
export async function executeRequest(routeConfig, requestData, globalConfig, ctxExtras) {
|
|
89
|
+
const reqId = requestData.headers['x-request-id'] || (ctxExtras.generateId ? ctxExtras.generateId() : Date.now().toString());
|
|
90
|
+
let responseHeaders = { 'X-Request-Id': reqId };
|
|
91
|
+
let errorHeaders = { 'X-Request-Id': reqId, 'Content-Type': 'application/problem+json' };
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const ctx = {
|
|
95
|
+
...ctxExtras,
|
|
96
|
+
env: globalConfig.envData || process.env,
|
|
97
|
+
body: requestData.body,
|
|
98
|
+
query: requestData.query,
|
|
99
|
+
params: requestData.params,
|
|
100
|
+
files: requestData.files,
|
|
101
|
+
user: null
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (ctxExtras.pluginManager) {
|
|
105
|
+
await ctxExtras.pluginManager.runOnContext(ctx);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Rate Limiting Evaluation
|
|
109
|
+
const activeRateLimit = routeConfig.rateLimit === false ? null : (routeConfig.rateLimit || globalConfig.rateLimit);
|
|
110
|
+
const redisClient = ctxExtras.redis;
|
|
111
|
+
|
|
112
|
+
if (activeRateLimit && redisClient) {
|
|
113
|
+
const rlKey = generateRateLimitKey ? await generateRateLimitKey(requestData, globalConfig) : `bro:rl:${requestData.originalUrl}:${requestData.ip}`;
|
|
114
|
+
try {
|
|
115
|
+
const current = await redisClient.incr(rlKey);
|
|
116
|
+
const windowSeconds = Math.floor(activeRateLimit.windowMs / 1000);
|
|
117
|
+
if (current === 1) {
|
|
118
|
+
await redisClient.expire(rlKey, windowSeconds);
|
|
119
|
+
}
|
|
120
|
+
if (current > activeRateLimit.max) {
|
|
121
|
+
responseHeaders['Retry-After'] = windowSeconds.toString();
|
|
122
|
+
return { status: 429, headers: responseHeaders, body: formatErrorEnvelope(429, 'Too Many Requests', null, reqId) };
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error('[bro.js] Rate Limit Error:', err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Auth verification
|
|
130
|
+
if (routeConfig.auth === 'api-key') {
|
|
131
|
+
const apiKey = requestData.headers['x-api-key'] || (requestData.headers['authorization'] || '').replace('Bearer ', '');
|
|
132
|
+
const validKey = globalConfig.auth?.apiKey || process.env.API_KEY;
|
|
133
|
+
|
|
134
|
+
let isValid = false;
|
|
135
|
+
if (Array.isArray(validKey)) {
|
|
136
|
+
isValid = validKey.includes(apiKey);
|
|
137
|
+
} else {
|
|
138
|
+
isValid = apiKey && apiKey === validKey;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!isValid) {
|
|
142
|
+
return { status: 401, headers: errorHeaders, body: formatErrorEnvelope(401, 'Unauthorized', 'Missing or invalid API key', reqId, requestData.originalUrl) };
|
|
143
|
+
}
|
|
144
|
+
} else if (routeConfig.auth) {
|
|
145
|
+
const authHeader = requestData.headers['authorization'];
|
|
146
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
147
|
+
return { status: 401, headers: errorHeaders, body: formatErrorEnvelope(401, 'Unauthorized', 'Missing or invalid Bearer token', reqId, requestData.originalUrl) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const token = authHeader.split(' ')[1] ?? '';
|
|
151
|
+
const authResult = verifyJwt(token, globalConfig.jwtSecret);
|
|
152
|
+
|
|
153
|
+
if (!authResult.valid) {
|
|
154
|
+
return { status: 401, headers: errorHeaders, body: formatErrorEnvelope(401, 'Unauthorized', authResult.error, reqId, requestData.originalUrl) };
|
|
155
|
+
}
|
|
156
|
+
ctx.user = authResult.payload ?? null;
|
|
157
|
+
|
|
158
|
+
if (Array.isArray(routeConfig.auth) && routeConfig.auth.length > 0) {
|
|
159
|
+
if (!ctx.user || !ctx.user.role || !routeConfig.auth.includes(ctx.user.role)) {
|
|
160
|
+
return { status: 403, headers: errorHeaders, body: formatErrorEnvelope(403, 'Forbidden', 'Insufficient role permissions', reqId, requestData.originalUrl) };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Zod validation
|
|
166
|
+
if (routeConfig.params) {
|
|
167
|
+
const result = await routeConfig.params.safeParseAsync(requestData.params || {});
|
|
168
|
+
if (!result.success) {
|
|
169
|
+
return { status: 400, headers: errorHeaders, body: formatErrorEnvelope(400, 'Invalid URL Parameters', formatZodError(result.error), reqId, requestData.originalUrl) };
|
|
170
|
+
}
|
|
171
|
+
ctx.params = result.data;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (routeConfig.body) {
|
|
175
|
+
const result = await routeConfig.body.safeParseAsync(requestData.body || {});
|
|
176
|
+
if (!result.success) {
|
|
177
|
+
return { status: 400, headers: errorHeaders, body: formatErrorEnvelope(400, 'Invalid Request Body', formatZodError(result.error), reqId, requestData.originalUrl) };
|
|
178
|
+
}
|
|
179
|
+
ctx.body = result.data;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (routeConfig.query) {
|
|
183
|
+
const result = await routeConfig.query.safeParseAsync(requestData.query || {});
|
|
184
|
+
if (!result.success) {
|
|
185
|
+
return { status: 400, headers: errorHeaders, body: formatErrorEnvelope(400, 'Invalid Query Parameters', formatZodError(result.error), reqId, requestData.originalUrl) };
|
|
186
|
+
}
|
|
187
|
+
ctx.query = result.data;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof routeConfig.handler !== 'function') {
|
|
191
|
+
throw new Error('Route "handler" is missing or is not a function');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Cache evaluation
|
|
195
|
+
let cacheKey = null;
|
|
196
|
+
|
|
197
|
+
if (routeConfig.cache && redisClient && requestData.method === 'GET') {
|
|
198
|
+
const authIdentity = crypto.createHash('sha256').update(String(requestData.headers['authorization'] || requestData.headers['x-api-key'] || requestData.ip || 'anonymous')).digest('hex');
|
|
199
|
+
const locale = requestData.locale || 'en';
|
|
200
|
+
// URL has query parameters, so use originalUrl completely
|
|
201
|
+
cacheKey = `bro:cache:${requestData.method}:${requestData.originalUrl}:${locale}:${authIdentity}`;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const cached = await redisClient.get(cacheKey);
|
|
205
|
+
if (cached) {
|
|
206
|
+
return { status: 200, headers: responseHeaders, body: JSON.parse(cached) };
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error('[bro.js] Cache parsing failed, deleting key:', cacheKey);
|
|
210
|
+
await redisClient.del(cacheKey).catch(() => {});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Execute User Handler
|
|
215
|
+
let responseData = await routeConfig.handler(ctx);
|
|
216
|
+
|
|
217
|
+
// Runtime Response Validation
|
|
218
|
+
if (routeConfig.response && globalConfig.validateResponse !== false) {
|
|
219
|
+
const result = await routeConfig.response.safeParseAsync(responseData);
|
|
220
|
+
if (!result.success) {
|
|
221
|
+
console.error('[bro.js] ⚠️ Response Validation Failed:', formatZodError(result.error));
|
|
222
|
+
if (process.env.NODE_ENV === 'production' && globalConfig.validateResponse === 'strict') {
|
|
223
|
+
throw new Error('Response Validation Failed');
|
|
224
|
+
} else if (globalConfig.validateResponse !== 'warn') {
|
|
225
|
+
// Strip invalid data, throw 500
|
|
226
|
+
throw new Error('Response Validation Failed');
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
responseData = result.data;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (cacheKey && routeConfig.cache) {
|
|
234
|
+
await redisClient.setEx(cacheKey, routeConfig.cache, JSON.stringify(responseData));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { status: 200, headers: responseHeaders, body: responseData };
|
|
238
|
+
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const status = err.status || 500;
|
|
241
|
+
const title = status === 500 ? 'Internal Server Error' : err.message;
|
|
242
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
243
|
+
return { status, headers: errorHeaders, body: formatErrorEnvelope(status, isProd && status >= 500 ? 'Internal Server Error' : title, isProd && status >= 500 ? null : err.details, reqId, requestData.originalUrl), error: status >= 500 ? err : undefined };
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { z, ZodTypeAny } from 'zod';
|
|
2
3
|
|
|
3
4
|
type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
|
|
@@ -14,14 +15,20 @@ export interface UploadedFile {
|
|
|
14
15
|
buffer?: Buffer;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export interface
|
|
18
|
-
env
|
|
18
|
+
export interface AppContext<Env = any, Db = any, User = any> {
|
|
19
|
+
env: Env;
|
|
20
|
+
db: Db;
|
|
21
|
+
user: User;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BroContext<Body = any, Params = any, Query = any, App extends AppContext = AppContext> {
|
|
25
|
+
env?: App['env'];
|
|
19
26
|
jwt?: { sign: (payload: any, options?: any) => string };
|
|
20
27
|
body: InferZod<Body>;
|
|
21
28
|
params: InferZod<Params>;
|
|
22
29
|
query: InferZod<Query>;
|
|
23
|
-
user?:
|
|
24
|
-
db?:
|
|
30
|
+
user?: App['user'];
|
|
31
|
+
db?: App['db'];
|
|
25
32
|
io?: any;
|
|
26
33
|
file?: UploadedFile;
|
|
27
34
|
files?: UploadedFile[] | Record<string, UploadedFile[]>;
|
|
@@ -31,8 +38,9 @@ export interface BroContext<Body = any, Params = any, Query = any> {
|
|
|
31
38
|
redis?: any;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
export interface RouteConfig<Body = any, Params = any, Query = any> {
|
|
35
|
-
auth?: boolean | string[] | 'api-key';
|
|
41
|
+
export interface RouteConfig<Body = any, Params = any, Query = any, Response = any, App extends AppContext = AppContext> {
|
|
42
|
+
auth?: boolean | string[] | 'api-key';
|
|
43
|
+
operationId?: string;
|
|
36
44
|
upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
|
|
37
45
|
body?: Body;
|
|
38
46
|
params?: Params;
|
|
@@ -44,12 +52,12 @@ export interface RouteConfig<Body = any, Params = any, Query = any> {
|
|
|
44
52
|
max: number;
|
|
45
53
|
};
|
|
46
54
|
summary?: string;
|
|
47
|
-
handler: (ctx: BroContext<Body, Params, Query>) => Promise<any> | any;
|
|
55
|
+
handler: (ctx: BroContext<Body, Params, Query, App>) => Promise<Response extends import('zod').ZodTypeAny ? import('zod').infer<Response> : any> | (Response extends import('zod').ZodTypeAny ? import('zod').infer<Response> : any);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
export function defineRoute<Body = any, Params = any, Query = any>(
|
|
51
|
-
config: RouteConfig<Body, Params, Query>
|
|
52
|
-
): RouteConfig<Body, Params, Query>;
|
|
58
|
+
export function defineRoute<Body = any, Params = any, Query = any, Response = any, App extends AppContext = AppContext>(
|
|
59
|
+
config: RouteConfig<Body, Params, Query, Response, App>
|
|
60
|
+
): RouteConfig<Body, Params, Query, Response, App>;
|
|
53
61
|
|
|
54
62
|
export function loadLocale(directory: string, options?: { defaultLocale?: string }): Promise<{
|
|
55
63
|
locales: string[];
|
|
@@ -58,7 +66,19 @@ export function loadLocale(directory: string, options?: { defaultLocale?: string
|
|
|
58
66
|
translate: (locale: string, key: string, values?: Record<string, unknown>) => string;
|
|
59
67
|
}>;
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
|
|
70
|
+
export interface BroPlugin {
|
|
71
|
+
name: string;
|
|
72
|
+
version: string;
|
|
73
|
+
order?: number;
|
|
74
|
+
onInit?: (globalConfig: BroConfig, app: any) => void | Promise<void>;
|
|
75
|
+
onContext?: (ctx: BroContext) => any | Promise<any>;
|
|
76
|
+
onRequest?: (req: any, res: any) => void | Promise<void>;
|
|
77
|
+
onError?: (err: any, req: any, res: any) => void | Promise<void>;
|
|
78
|
+
onShutdown?: () => void | Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
export interface BroConfig {
|
|
81
|
+
validateResponse?: boolean | 'strict' | 'warn';
|
|
62
82
|
env?: ZodTypeAny;
|
|
63
83
|
server?: {
|
|
64
84
|
port?: number;
|
package/src/index.js
CHANGED
|
@@ -27,3 +27,13 @@ export function defineConfig(config) {
|
|
|
27
27
|
|
|
28
28
|
export { z };
|
|
29
29
|
export { loadLocale } from './locale.js';
|
|
30
|
+
export { createServer } from './server.js';
|
|
31
|
+
export { createTestHarness as test } from './testing.js';
|
|
32
|
+
export { PluginManager, ObservabilityPlugin, SecurityPlugin } from './plugins.js';
|
|
33
|
+
export { createPinoAdapter, setupOpenTelemetry, createDashboardEventBus } from './observability.js';
|
|
34
|
+
export { BaseDatabaseAdapter, PostgresAdapter } from './database.js';
|
|
35
|
+
export { SecureUploadPipeline, LocalStorageAdapter, S3StorageAdapter } from './uploads.js';
|
|
36
|
+
export { TaskManager } from './task-engine.js';
|
|
37
|
+
export { OidcProvider, PolicyEvaluator, ApiKeyManager, tenantContextPlugin } from './policy-auth.js';
|
|
38
|
+
|
|
39
|
+
export { RouteRegistry } from './engine.js';
|
package/src/logger.js
CHANGED
|
@@ -77,3 +77,50 @@ export function printHotReload(fileName, event, reloadTimeMs, resourceType = 'Ro
|
|
|
77
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
|
}
|
|
80
|
+
|
|
81
|
+
export function createLogger(config = {}) {
|
|
82
|
+
const isJson = config.format === 'json';
|
|
83
|
+
const levelPriority = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
|
|
84
|
+
const currentLevel = levelPriority[config.level] ?? levelPriority.info;
|
|
85
|
+
|
|
86
|
+
const redactKeys = config.redact || ['password', 'token', 'secret', 'authorization'];
|
|
87
|
+
|
|
88
|
+
const redact = (obj) => {
|
|
89
|
+
if (typeof obj !== 'object' || obj === null) return obj;
|
|
90
|
+
if (Array.isArray(obj)) return obj.map(redact);
|
|
91
|
+
const newObj = { ...obj };
|
|
92
|
+
for (const key of Object.keys(newObj)) {
|
|
93
|
+
if (redactKeys.some(r => key.toLowerCase().includes(r))) {
|
|
94
|
+
newObj[key] = '[REDACTED]';
|
|
95
|
+
} else if (typeof newObj[key] === 'object') {
|
|
96
|
+
newObj[key] = redact(newObj[key]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return newObj;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const log = (level, message, meta = {}) => {
|
|
103
|
+
if (levelPriority[level] < currentLevel) return;
|
|
104
|
+
const timestamp = new Date().toISOString();
|
|
105
|
+
|
|
106
|
+
if (isJson) {
|
|
107
|
+
console[level === 'debug' ? 'log' : level](JSON.stringify({ level, timestamp, message, ...redact(meta) }));
|
|
108
|
+
} else {
|
|
109
|
+
const colorMap = { debug: colors.dim, info: colors.cyan, warn: colors.yellow, error: colors.red };
|
|
110
|
+
const c = colorMap[level] || colors.reset;
|
|
111
|
+
let metaStr = Object.keys(meta).length ? ` ${colors.dim}${JSON.stringify(redact(meta))}${colors.reset}` : '';
|
|
112
|
+
console[level === 'debug' ? 'log' : level](`${colors.dim}[${timestamp}]${colors.reset} ${c}[${level.toUpperCase()}]${colors.reset} ${message}${metaStr}`);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
debug: (msg, meta) => log('debug', msg, meta),
|
|
118
|
+
info: (msg, meta) => log('info', msg, meta),
|
|
119
|
+
warn: (msg, meta) => log('warn', msg, meta),
|
|
120
|
+
error: (msg, meta) => log('error', msg, meta),
|
|
121
|
+
time: (label) => {
|
|
122
|
+
const start = performance.now();
|
|
123
|
+
return (msg, meta) => log('info', msg || `${label} completed`, { ...meta, durationMs: performance.now() - start });
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
package/src/next.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface UploadedFile {
|
|
|
11
11
|
text: () => Promise<string>;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface NextBroGlobalConfig<TDb = any> {
|
|
14
|
+
export interface NextBroGlobalConfig<TEnv = any, TDb = any, TUser = any> {
|
|
15
15
|
env?: ZodTypeAny;
|
|
16
16
|
locales?: Record<string, any>;
|
|
17
17
|
defaultLocale?: string;
|
|
@@ -25,9 +25,9 @@ export interface NextBroGlobalConfig<TDb = any> {
|
|
|
25
25
|
db?: TDb | Promise<TDb> | (() => TDb | Promise<TDb>) | { init: () => TDb | Promise<TDb> };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TDb = any> {
|
|
28
|
+
export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TEnv = any, TDb = any, TUser = any> {
|
|
29
29
|
req: Request;
|
|
30
|
-
env:
|
|
30
|
+
env: TEnv;
|
|
31
31
|
db: TDb;
|
|
32
32
|
redis: any;
|
|
33
33
|
io: { emit: (event: string, data: any) => void };
|
|
@@ -38,12 +38,12 @@ export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TDb
|
|
|
38
38
|
files?: Record<string, UploadedFile[]>;
|
|
39
39
|
locale: string;
|
|
40
40
|
t: (key: string, values?: any) => string;
|
|
41
|
-
user?:
|
|
41
|
+
user?: TUser;
|
|
42
42
|
jwt: { sign: (payload: any, opts?: any) => string };
|
|
43
43
|
error: (status: number, message: string) => never;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TDb = any> {
|
|
46
|
+
export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TEnv = any, TDb = any, TUser = any> {
|
|
47
47
|
auth?: boolean | string[] | 'api-key' | string;
|
|
48
48
|
body?: TBody;
|
|
49
49
|
query?: TQuery;
|
|
@@ -53,18 +53,18 @@ export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TDb =
|
|
|
53
53
|
response?: ZodTypeAny;
|
|
54
54
|
summary?: string;
|
|
55
55
|
upload?: any;
|
|
56
|
-
handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TDb>) => Promise<any> | any;
|
|
56
|
+
handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TEnv, TDb, TUser>) => Promise<any> | any;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export interface BroNextInstance<TDb = any> {
|
|
59
|
+
export interface BroNextInstance<TEnv = any, TDb = any, TUser = any> {
|
|
60
60
|
z: typeof z;
|
|
61
61
|
defineRoute: <
|
|
62
62
|
TBody extends ZodTypeAny = any,
|
|
63
63
|
TQuery extends ZodTypeAny = any,
|
|
64
64
|
TParams extends ZodTypeAny = any
|
|
65
65
|
>(
|
|
66
|
-
config: NextRouteConfig<TBody, TQuery, TParams, TDb>
|
|
66
|
+
config: NextRouteConfig<TBody, TQuery, TParams, TEnv, TDb, TUser>
|
|
67
67
|
) => (req: Request | any, context: any) => Promise<any>;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export declare function createBro<TDb = any>(config?: NextBroGlobalConfig<TDb>): BroNextInstance<TDb>;
|
|
70
|
+
export declare function createBro<TEnv = any, TDb = any, TUser = any>(config?: NextBroGlobalConfig<TEnv, TDb, TUser>): BroNextInstance<TEnv, TDb, TUser>;
|