@noxfly/noxus 2.5.0 → 3.0.0-dev.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 +403 -341
- package/dist/app-injector-Bz3Upc0y.d.mts +125 -0
- package/dist/app-injector-Bz3Upc0y.d.ts +125 -0
- package/dist/child.d.mts +48 -22
- package/dist/child.d.ts +48 -22
- package/dist/child.js +1111 -1341
- package/dist/child.mjs +1087 -1295
- package/dist/main.d.mts +301 -309
- package/dist/main.d.ts +301 -309
- package/dist/main.js +1471 -1650
- package/dist/main.mjs +1420 -1570
- package/dist/renderer.d.mts +3 -3
- package/dist/renderer.d.ts +3 -3
- package/dist/renderer.js +109 -135
- package/dist/renderer.mjs +109 -135
- package/dist/request-BlTtiHbi.d.ts +112 -0
- package/dist/request-qJ9EiDZc.d.mts +112 -0
- package/package.json +7 -7
- package/src/DI/app-injector.ts +95 -106
- package/src/DI/injector-explorer.ts +93 -119
- package/src/DI/token.ts +53 -0
- package/src/app.ts +141 -168
- package/src/bootstrap.ts +78 -54
- package/src/decorators/controller.decorator.ts +38 -27
- package/src/decorators/guards.decorator.ts +5 -64
- package/src/decorators/injectable.decorator.ts +68 -15
- package/src/decorators/method.decorator.ts +40 -81
- package/src/decorators/middleware.decorator.ts +5 -72
- package/src/index.ts +2 -0
- package/src/main.ts +4 -8
- package/src/non-electron-process.ts +0 -1
- package/src/preload-bridge.ts +1 -1
- package/src/renderer-client.ts +2 -2
- package/src/renderer-events.ts +1 -1
- package/src/request.ts +3 -3
- package/src/router.ts +190 -431
- package/src/routes.ts +78 -0
- package/src/socket.ts +4 -4
- package/src/window/window-manager.ts +255 -0
- package/tsconfig.json +5 -10
- package/tsup.config.ts +2 -2
- package/dist/app-injector-B3MvgV3k.d.mts +0 -95
- package/dist/app-injector-B3MvgV3k.d.ts +0 -95
- package/dist/request-CdpZ9qZL.d.ts +0 -167
- package/dist/request-Dx_5Prte.d.mts +0 -167
- package/src/decorators/inject.decorator.ts +0 -24
- package/src/decorators/injectable.metadata.ts +0 -15
- package/src/decorators/module.decorator.ts +0 -75
package/src/router.ts
CHANGED
|
@@ -4,252 +4,155 @@
|
|
|
4
4
|
* @author NoxFly
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import '
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
import { getControllerMetadata } from './decorators/controller.decorator';
|
|
8
|
+
import { Guard } from './decorators/guards.decorator';
|
|
9
|
+
import { Injectable } from './decorators/injectable.decorator';
|
|
10
|
+
import { getRouteMetadata, isAtomicHttpMethod } from './decorators/method.decorator';
|
|
11
|
+
import { Middleware, NextFunction } from './decorators/middleware.decorator';
|
|
12
|
+
import { InjectorExplorer } from './DI/injector-explorer';
|
|
13
|
+
import {
|
|
14
|
+
BadRequestException,
|
|
15
|
+
NotFoundException,
|
|
16
|
+
ResponseException,
|
|
17
|
+
UnauthorizedException
|
|
18
|
+
} from './exceptions';
|
|
19
|
+
import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from './request';
|
|
20
|
+
import { Logger } from './utils/logger';
|
|
21
|
+
import { RadixTree } from './utils/radix-tree';
|
|
22
|
+
import { Type } from './utils/types';
|
|
19
23
|
|
|
20
|
-
/**
|
|
21
|
-
* A lazy route entry maps a path prefix to a dynamic import function.
|
|
22
|
-
* The module is loaded on the first request matching the prefix.
|
|
23
|
-
*/
|
|
24
24
|
export interface ILazyRoute {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
load: () => Promise<unknown>;
|
|
26
|
+
guards: Guard[];
|
|
27
|
+
middlewares: Middleware[];
|
|
28
|
+
loading: Promise<void> | null;
|
|
29
|
+
loaded: boolean;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
interface LazyRouteEntry {
|
|
32
|
-
|
|
33
|
+
load: (() => Promise<unknown>) | null;
|
|
34
|
+
guards: Guard[];
|
|
35
|
+
middlewares: Middleware[];
|
|
33
36
|
loading: Promise<void> | null;
|
|
34
37
|
loaded: boolean;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
const ATOMIC_HTTP_METHODS: ReadonlySet<AtomicHttpMethod> = new Set<AtomicHttpMethod>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
38
|
-
|
|
39
|
-
function isAtomicHttpMethod(method: unknown): method is AtomicHttpMethod {
|
|
40
|
-
return typeof method === 'string' && ATOMIC_HTTP_METHODS.has(method as AtomicHttpMethod);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* IRouteDefinition interface defines the structure of a route in the application.
|
|
45
|
-
* It includes the HTTP method, path, controller class, handler method name,
|
|
46
|
-
* guards, and middlewares associated with the route.
|
|
47
|
-
*/
|
|
48
40
|
export interface IRouteDefinition {
|
|
49
41
|
method: string;
|
|
50
42
|
path: string;
|
|
51
|
-
controller: Type<
|
|
43
|
+
controller: Type<unknown>;
|
|
52
44
|
handler: string;
|
|
53
|
-
guards:
|
|
54
|
-
middlewares:
|
|
45
|
+
guards: Guard[];
|
|
46
|
+
middlewares: Middleware[];
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
* This type defines a function that represents an action in a controller.
|
|
59
|
-
* It takes a Request and an IResponse as parameters and returns a value or a Promise.
|
|
60
|
-
*/
|
|
61
|
-
export type ControllerAction = (request: Request, response: IResponse) => any;
|
|
62
|
-
|
|
49
|
+
export type ControllerAction = (request: Request, response: IResponse) => unknown;
|
|
63
50
|
|
|
64
|
-
|
|
65
|
-
* Router class is responsible for managing the application's routing.
|
|
66
|
-
* It registers controllers, handles requests, and manages middlewares and guards.
|
|
67
|
-
*/
|
|
68
|
-
@Injectable('singleton')
|
|
51
|
+
@Injectable({ lifetime: 'singleton' })
|
|
69
52
|
export class Router {
|
|
70
53
|
private readonly routes = new RadixTree<IRouteDefinition>();
|
|
71
|
-
private readonly rootMiddlewares:
|
|
54
|
+
private readonly rootMiddlewares: Middleware[] = [];
|
|
72
55
|
private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
|
|
73
56
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* It also handles the guards and middlewares associated with the controller.
|
|
78
|
-
* @param controllerClass - The controller class to register.
|
|
79
|
-
*/
|
|
80
|
-
public registerController(controllerClass: Type<unknown>): Router {
|
|
81
|
-
const controllerMeta = getControllerMetadata(controllerClass);
|
|
82
|
-
|
|
83
|
-
const controllerGuards = getGuardForController(controllerClass.name);
|
|
84
|
-
const controllerMiddlewares = getMiddlewaresForController(controllerClass.name);
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// Registration
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
85
60
|
|
|
86
|
-
|
|
87
|
-
|
|
61
|
+
public registerController(
|
|
62
|
+
controllerClass: Type<unknown>,
|
|
63
|
+
pathPrefix: string,
|
|
64
|
+
routeGuards: Guard[] = [],
|
|
65
|
+
routeMiddlewares: Middleware[] = [],
|
|
66
|
+
): this {
|
|
67
|
+
const meta = getControllerMetadata(controllerClass);
|
|
88
68
|
|
|
89
|
-
|
|
69
|
+
if (!meta) {
|
|
70
|
+
throw new Error(`[Noxus] Missing @Controller decorator on ${controllerClass.name}`);
|
|
71
|
+
}
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
const fullPath = `${controllerMeta.path}/${def.path}`.replace(/\/+/g, '/');
|
|
73
|
+
const routeMeta = getRouteMetadata(controllerClass);
|
|
93
74
|
|
|
94
|
-
|
|
95
|
-
const
|
|
75
|
+
for (const def of routeMeta) {
|
|
76
|
+
const fullPath = `${pathPrefix}/${def.path}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
96
77
|
|
|
97
|
-
|
|
98
|
-
const
|
|
78
|
+
// Route-level guards/middlewares from defineRoutes() + action-level ones
|
|
79
|
+
const guards = [...new Set([...routeGuards, ...def.guards])];
|
|
80
|
+
const middlewares = [...new Set([...routeMiddlewares, ...def.middlewares])];
|
|
99
81
|
|
|
100
82
|
const routeDef: IRouteDefinition = {
|
|
101
83
|
method: def.method,
|
|
102
84
|
path: fullPath,
|
|
103
85
|
controller: controllerClass,
|
|
104
86
|
handler: def.handler,
|
|
105
|
-
guards
|
|
106
|
-
middlewares
|
|
87
|
+
guards,
|
|
88
|
+
middlewares,
|
|
107
89
|
};
|
|
108
90
|
|
|
109
91
|
this.routes.insert(fullPath + '/' + def.method, routeDef);
|
|
110
92
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
const actionGuardsInfo = hasActionGuards
|
|
114
|
-
? '<' + routeDef.guards.map(g => g.name).join('|') + '>'
|
|
115
|
-
: '';
|
|
116
|
-
|
|
117
|
-
Logger.log(`Mapped {${routeDef.method} /${fullPath}}${actionGuardsInfo} route`);
|
|
93
|
+
const guardInfo = guards.length ? `<${guards.map(g => g.name).join('|')}>` : '';
|
|
94
|
+
Logger.log(`Mapped {${def.method} /${fullPath}}${guardInfo} route`);
|
|
118
95
|
}
|
|
119
96
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const controllerGuardsInfo = hasCtrlGuards
|
|
123
|
-
? '<' + controllerMeta.guards.map(g => g.name).join('|') + '>'
|
|
97
|
+
const ctrlGuardInfo = routeGuards.length
|
|
98
|
+
? `<${routeGuards.map(g => g.name).join('|')}>`
|
|
124
99
|
: '';
|
|
125
|
-
|
|
126
|
-
Logger.log(`Mapped ${controllerClass.name}${controllerGuardsInfo} controller's routes`);
|
|
100
|
+
Logger.log(`Mapped ${controllerClass.name}${ctrlGuardInfo} controller's routes`);
|
|
127
101
|
|
|
128
102
|
return this;
|
|
129
103
|
}
|
|
130
104
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* @param loadModule - A function that returns a dynamic import promise.
|
|
138
|
-
*/
|
|
139
|
-
public registerLazyRoute(pathPrefix: string, loadModule: () => Promise<unknown>): Router {
|
|
105
|
+
public registerLazyRoute(
|
|
106
|
+
pathPrefix: string,
|
|
107
|
+
load: () => Promise<unknown>,
|
|
108
|
+
guards: Guard[] = [],
|
|
109
|
+
middlewares: Middleware[] = [],
|
|
110
|
+
): this {
|
|
140
111
|
const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
|
|
141
|
-
this.lazyRoutes.set(normalized, {
|
|
112
|
+
this.lazyRoutes.set(normalized, { load, guards, middlewares, loading: null, loaded: false });
|
|
142
113
|
Logger.log(`Registered lazy route prefix {${normalized}}`);
|
|
143
114
|
return this;
|
|
144
115
|
}
|
|
145
116
|
|
|
146
|
-
|
|
147
|
-
* Defines a middleware for the root of the application.
|
|
148
|
-
* This method allows you to register a middleware that will be applied to all requests
|
|
149
|
-
* to the application, regardless of the controller or action.
|
|
150
|
-
* @param middleware - The middleware class to register.
|
|
151
|
-
*/
|
|
152
|
-
public defineRootMiddleware(middleware: Type<IMiddleware>): Router {
|
|
117
|
+
public defineRootMiddleware(middleware: Middleware): this {
|
|
153
118
|
this.rootMiddlewares.push(middleware);
|
|
154
119
|
return this;
|
|
155
120
|
}
|
|
156
121
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
* removes it from the messagePorts map.
|
|
161
|
-
* @param channelSenderId - The ID of the sender channel to shut down.
|
|
162
|
-
*/
|
|
163
|
-
public async handle(request: Request): Promise<IResponse> {
|
|
164
|
-
if(request.method === 'BATCH') {
|
|
165
|
-
return this.handleBatch(request);
|
|
166
|
-
}
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// Request handling
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
167
125
|
|
|
168
|
-
|
|
126
|
+
public async handle(request: Request): Promise<IResponse> {
|
|
127
|
+
return request.method === 'BATCH'
|
|
128
|
+
? this.handleBatch(request)
|
|
129
|
+
: this.handleAtomic(request);
|
|
169
130
|
}
|
|
170
131
|
|
|
171
132
|
private async handleAtomic(request: Request): Promise<IResponse> {
|
|
172
133
|
Logger.comment(`> ${request.method} /${request.path}`);
|
|
173
|
-
|
|
174
134
|
const t0 = performance.now();
|
|
175
135
|
|
|
176
|
-
const response: IResponse = {
|
|
177
|
-
|
|
178
|
-
status: 200,
|
|
179
|
-
body: null,
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
let isCritical: boolean = false;
|
|
136
|
+
const response: IResponse = { requestId: request.id, status: 200, body: null };
|
|
137
|
+
let isCritical = false;
|
|
183
138
|
|
|
184
139
|
try {
|
|
185
140
|
const routeDef = await this.findRoute(request);
|
|
186
141
|
await this.resolveController(request, response, routeDef);
|
|
187
142
|
|
|
188
|
-
if(response.status
|
|
189
|
-
throw new ResponseException(response.status, response.error);
|
|
190
|
-
}
|
|
143
|
+
if (response.status >= 400) throw new ResponseException(response.status, response.error);
|
|
191
144
|
}
|
|
192
|
-
catch(error
|
|
193
|
-
response
|
|
194
|
-
|
|
195
|
-
if(error instanceof ResponseException) {
|
|
196
|
-
response.status = error.status;
|
|
197
|
-
response.error = error.message;
|
|
198
|
-
response.stack = error.stack;
|
|
199
|
-
}
|
|
200
|
-
else if(error instanceof Error) {
|
|
201
|
-
isCritical = true;
|
|
202
|
-
response.status = 500;
|
|
203
|
-
response.error = error.message || 'Internal Server Error';
|
|
204
|
-
response.stack = error.stack || 'No stack trace available';
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
isCritical = true;
|
|
208
|
-
response.status = 500;
|
|
209
|
-
response.error = 'Unknown error occurred';
|
|
210
|
-
response.stack = 'No stack trace available';
|
|
211
|
-
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
this.fillErrorResponse(response, error, (c) => { isCritical = c; });
|
|
212
147
|
}
|
|
213
148
|
finally {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
|
|
217
|
-
|
|
218
|
-
if(response.status < 400) {
|
|
219
|
-
Logger.log(message);
|
|
220
|
-
}
|
|
221
|
-
else if(response.status < 500) {
|
|
222
|
-
Logger.warn(message);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
if(isCritical) {
|
|
226
|
-
Logger.critical(message);
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
Logger.error(message);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if(response.error !== undefined) {
|
|
234
|
-
if(isCritical) {
|
|
235
|
-
Logger.critical(response.error);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
Logger.error(response.error);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if(response.stack !== undefined) {
|
|
242
|
-
Logger.errorStack(response.stack);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
149
|
+
this.logResponse(request, response, performance.now() - t0, isCritical);
|
|
246
150
|
return response;
|
|
247
151
|
}
|
|
248
152
|
}
|
|
249
153
|
|
|
250
154
|
private async handleBatch(request: Request): Promise<IResponse> {
|
|
251
155
|
Logger.comment(`> ${request.method} /${request.path}`);
|
|
252
|
-
|
|
253
156
|
const t0 = performance.now();
|
|
254
157
|
|
|
255
158
|
const response: IResponse<IBatchResponsePayload> = {
|
|
@@ -257,338 +160,194 @@ export class Router {
|
|
|
257
160
|
status: 200,
|
|
258
161
|
body: { responses: [] },
|
|
259
162
|
};
|
|
260
|
-
|
|
261
|
-
let isCritical: boolean = false;
|
|
163
|
+
let isCritical = false;
|
|
262
164
|
|
|
263
165
|
try {
|
|
264
166
|
const payload = this.normalizeBatchPayload(request.body);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
response.body!.responses = await Promise.all(batchPromises);
|
|
167
|
+
response.body!.responses = await Promise.all(
|
|
168
|
+
payload.requests.map((item, i) => {
|
|
169
|
+
const id = item.requestId ?? `${request.id}:${i}`;
|
|
170
|
+
return this.handleAtomic(new Request(request.event, request.senderId, id, item.method, item.path, item.body));
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
273
173
|
}
|
|
274
|
-
catch(error
|
|
275
|
-
response
|
|
276
|
-
|
|
277
|
-
if(error instanceof ResponseException) {
|
|
278
|
-
response.status = error.status;
|
|
279
|
-
response.error = error.message;
|
|
280
|
-
response.stack = error.stack;
|
|
281
|
-
}
|
|
282
|
-
else if(error instanceof Error) {
|
|
283
|
-
isCritical = true;
|
|
284
|
-
response.status = 500;
|
|
285
|
-
response.error = error.message || 'Internal Server Error';
|
|
286
|
-
response.stack = error.stack || 'No stack trace available';
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
isCritical = true;
|
|
290
|
-
response.status = 500;
|
|
291
|
-
response.error = 'Unknown error occurred';
|
|
292
|
-
response.stack = 'No stack trace available';
|
|
293
|
-
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.fillErrorResponse(response, error, (c) => { isCritical = c; });
|
|
294
176
|
}
|
|
295
177
|
finally {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
|
|
299
|
-
|
|
300
|
-
if(response.status < 400) {
|
|
301
|
-
Logger.log(message);
|
|
302
|
-
}
|
|
303
|
-
else if(response.status < 500) {
|
|
304
|
-
Logger.warn(message);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
if(isCritical) {
|
|
308
|
-
Logger.critical(message);
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
Logger.error(message);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if(response.error !== undefined) {
|
|
316
|
-
if(isCritical) {
|
|
317
|
-
Logger.critical(response.error);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
Logger.error(response.error);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if(response.stack !== undefined) {
|
|
324
|
-
Logger.errorStack(response.stack);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
178
|
+
this.logResponse(request, response, performance.now() - t0, isCritical);
|
|
328
179
|
return response;
|
|
329
180
|
}
|
|
330
181
|
}
|
|
331
182
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const possiblePayload = body as Partial<IBatchRequestPayload>;
|
|
338
|
-
const { requests } = possiblePayload;
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
// Route resolution
|
|
185
|
+
// -------------------------------------------------------------------------
|
|
339
186
|
|
|
340
|
-
if(!Array.isArray(requests)) {
|
|
341
|
-
throw new BadRequestException('Batch payload must define a requests array.');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const normalizedRequests = requests.map((entry, index) => this.normalizeBatchItem(entry, index));
|
|
345
|
-
|
|
346
|
-
return { requests: normalizedRequests };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
|
|
350
|
-
if(entry === null || typeof entry !== 'object') {
|
|
351
|
-
throw new BadRequestException(`Batch request at index ${index} must be an object.`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
|
|
355
|
-
|
|
356
|
-
if(requestId !== undefined && typeof requestId !== 'string') {
|
|
357
|
-
throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if(typeof path !== 'string' || path.length === 0) {
|
|
361
|
-
throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if(typeof method !== 'string') {
|
|
365
|
-
throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const normalizedMethod = method.toUpperCase();
|
|
369
|
-
|
|
370
|
-
if(!isAtomicHttpMethod(normalizedMethod)) {
|
|
371
|
-
throw new BadRequestException(`Batch request at index ${index} uses the unsupported method ${method}.`);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
requestId,
|
|
376
|
-
path,
|
|
377
|
-
method: normalizedMethod as AtomicHttpMethod,
|
|
378
|
-
body,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Finds the route definition for a given request.
|
|
384
|
-
* This method searches the routing tree for a matching route based on the request's path and method.
|
|
385
|
-
* If no matching route is found, it throws a NotFoundException.
|
|
386
|
-
* @param request - The Request object containing the method and path to search for.
|
|
387
|
-
* @returns The IRouteDefinition for the matched route.
|
|
388
|
-
*/
|
|
389
|
-
/**
|
|
390
|
-
* Attempts to find a route definition for the given request.
|
|
391
|
-
* Returns undefined instead of throwing when the route is not found,
|
|
392
|
-
* so the caller can try lazy-loading first.
|
|
393
|
-
*/
|
|
394
187
|
private tryFindRoute(request: Request): IRouteDefinition | undefined {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const routeDef = matchedRoutes.node.findExactChild(request.method);
|
|
402
|
-
return routeDef?.value;
|
|
188
|
+
const matched = this.routes.search(request.path);
|
|
189
|
+
if (!matched?.node || matched.node.children.length === 0) return undefined;
|
|
190
|
+
return matched.node.findExactChild(request.method)?.value;
|
|
403
191
|
}
|
|
404
192
|
|
|
405
|
-
/**
|
|
406
|
-
* Finds the route definition for a given request.
|
|
407
|
-
* If no eagerly-registered route matches, attempts to load a lazy module
|
|
408
|
-
* whose prefix matches the request path, then retries.
|
|
409
|
-
*/
|
|
410
193
|
private async findRoute(request: Request): Promise<IRouteDefinition> {
|
|
411
|
-
// Fast path: route already registered
|
|
412
194
|
const direct = this.tryFindRoute(request);
|
|
413
|
-
if(direct) return direct;
|
|
195
|
+
if (direct) return direct;
|
|
414
196
|
|
|
415
|
-
// Try lazy route loading
|
|
416
197
|
await this.tryLoadLazyRoute(request.path);
|
|
417
198
|
|
|
418
|
-
// Retry after lazy load
|
|
419
199
|
const afterLazy = this.tryFindRoute(request);
|
|
420
|
-
if(afterLazy) return afterLazy;
|
|
200
|
+
if (afterLazy) return afterLazy;
|
|
421
201
|
|
|
422
202
|
throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
|
|
423
203
|
}
|
|
424
204
|
|
|
425
|
-
/**
|
|
426
|
-
* Given a request path, checks whether a lazy route prefix matches
|
|
427
|
-
* and triggers the dynamic import if it hasn't been loaded yet.
|
|
428
|
-
*/
|
|
429
205
|
private async tryLoadLazyRoute(requestPath: string): Promise<void> {
|
|
430
206
|
const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
|
|
431
207
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if(normalizedPath === prefix || normalizedPath.startsWith(prefix + '/') || firstSegment === prefix) {
|
|
438
|
-
if(!entry.loading) {
|
|
439
|
-
entry.loading = this.loadLazyModule(prefix, entry);
|
|
440
|
-
}
|
|
208
|
+
for (const [prefix, entry] of this.lazyRoutes) {
|
|
209
|
+
if (entry.loaded) continue;
|
|
210
|
+
const normalized = requestPath.replace(/^\/+/, '');
|
|
211
|
+
if (normalized === prefix || normalized.startsWith(prefix + '/') || firstSegment === prefix) {
|
|
212
|
+
if (!entry.loading) entry.loading = this.loadLazyModule(prefix, entry);
|
|
441
213
|
await entry.loading;
|
|
442
214
|
return;
|
|
443
215
|
}
|
|
444
216
|
}
|
|
445
217
|
}
|
|
446
218
|
|
|
447
|
-
/**
|
|
448
|
-
* Dynamically imports a lazy module and registers its decorated classes
|
|
449
|
-
* (controllers, services) in the DI container using the two-phase strategy.
|
|
450
|
-
*/
|
|
451
219
|
private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
|
|
452
220
|
const t0 = performance.now();
|
|
453
|
-
|
|
454
221
|
InjectorExplorer.beginAccumulate();
|
|
455
|
-
|
|
456
|
-
|
|
222
|
+
|
|
223
|
+
await entry.load?.();
|
|
224
|
+
|
|
225
|
+
entry.loading = null;
|
|
226
|
+
entry.load = null;
|
|
227
|
+
|
|
228
|
+
InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
|
|
457
229
|
|
|
458
230
|
entry.loaded = true;
|
|
459
231
|
|
|
460
|
-
|
|
461
|
-
Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(t1 - t0)}ms`);
|
|
232
|
+
Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(performance.now() - t0)}ms`);
|
|
462
233
|
}
|
|
463
234
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
* It also runs the request pipeline, which includes executing middlewares and guards.
|
|
468
|
-
* @param request - The Request object containing the request data.
|
|
469
|
-
* @param response - The IResponse object to populate with the response data.
|
|
470
|
-
* @param routeDef - The IRouteDefinition for the matched route.
|
|
471
|
-
* @return A Promise that resolves when the controller action has been executed.
|
|
472
|
-
* @throws UnauthorizedException if the request is not authorized by the guards.
|
|
473
|
-
*/
|
|
474
|
-
private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
|
|
475
|
-
const controllerInstance = request.context.resolve(routeDef.controller);
|
|
235
|
+
// -------------------------------------------------------------------------
|
|
236
|
+
// Pipeline
|
|
237
|
+
// -------------------------------------------------------------------------
|
|
476
238
|
|
|
239
|
+
private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
|
|
240
|
+
const instance = request.context.resolve(routeDef.controller);
|
|
477
241
|
Object.assign(request.params, this.extractParams(request.path, routeDef.path));
|
|
478
|
-
|
|
479
|
-
await this.runRequestPipeline(request, response, routeDef, controllerInstance);
|
|
242
|
+
await this.runPipeline(request, response, routeDef, instance);
|
|
480
243
|
}
|
|
481
244
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
* @param routeDef - The IRouteDefinition for the matched route.
|
|
489
|
-
* @param controllerInstance - The instance of the controller class.
|
|
490
|
-
* @return A Promise that resolves when the request pipeline has been executed.
|
|
491
|
-
* @throws ResponseException if the response status is not successful.
|
|
492
|
-
*/
|
|
493
|
-
private async runRequestPipeline(request: Request, response: IResponse, routeDef: IRouteDefinition, controllerInstance: any): Promise<void> {
|
|
245
|
+
private async runPipeline(
|
|
246
|
+
request: Request,
|
|
247
|
+
response: IResponse,
|
|
248
|
+
routeDef: IRouteDefinition,
|
|
249
|
+
controllerInstance: unknown,
|
|
250
|
+
): Promise<void> {
|
|
494
251
|
const middlewares = [...new Set([...this.rootMiddlewares, ...routeDef.middlewares])];
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
const guardsMaxIndex = middlewareMaxIndex + routeDef.guards.length;
|
|
498
|
-
|
|
252
|
+
const mwMax = middlewares.length - 1;
|
|
253
|
+
const guardMax = mwMax + routeDef.guards.length;
|
|
499
254
|
let index = -1;
|
|
500
255
|
|
|
501
256
|
const dispatch = async (i: number): Promise<void> => {
|
|
502
|
-
if(i <= index)
|
|
503
|
-
throw new Error("next() called multiple times");
|
|
504
|
-
|
|
257
|
+
if (i <= index) throw new Error('next() called multiple times');
|
|
505
258
|
index = i;
|
|
506
259
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
await this.runMiddleware(request, response, nextFn, middlewares[i]!);
|
|
511
|
-
|
|
512
|
-
if(response.status >= 400) {
|
|
513
|
-
throw new ResponseException(response.status, response.error);
|
|
514
|
-
}
|
|
515
|
-
|
|
260
|
+
if (i <= mwMax) {
|
|
261
|
+
await this.runMiddleware(request, response, dispatch.bind(null, i + 1), middlewares[i]!);
|
|
262
|
+
if (response.status >= 400) throw new ResponseException(response.status, response.error);
|
|
516
263
|
return;
|
|
517
264
|
}
|
|
518
265
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const guardIndex = i - middlewares.length;
|
|
522
|
-
const guardType = routeDef.guards[guardIndex]!;
|
|
523
|
-
await this.runGuard(request, guardType);
|
|
266
|
+
if (i <= guardMax) {
|
|
267
|
+
await this.runGuard(request, routeDef.guards[i - middlewares.length]!);
|
|
524
268
|
await dispatch(i + 1);
|
|
525
269
|
return;
|
|
526
270
|
}
|
|
527
271
|
|
|
528
|
-
|
|
529
|
-
const action = controllerInstance[routeDef.handler] as ControllerAction;
|
|
272
|
+
const action = (controllerInstance as Record<string, ControllerAction>)[routeDef.handler]!;
|
|
530
273
|
response.body = await action.call(controllerInstance, request, response);
|
|
531
|
-
|
|
532
|
-
// avoid parsing error on the renderer if the action just does treatment without returning anything
|
|
533
|
-
if(response.body === undefined) {
|
|
534
|
-
response.body = {};
|
|
535
|
-
}
|
|
274
|
+
if (response.body === undefined) response.body = {};
|
|
536
275
|
};
|
|
537
276
|
|
|
538
277
|
await dispatch(0);
|
|
539
278
|
}
|
|
540
279
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
* This method creates an instance of the middleware and invokes its `invoke` method,
|
|
544
|
-
* passing the request, response, and next function.
|
|
545
|
-
* @param request - The Request object containing the request data.
|
|
546
|
-
* @param response - The IResponse object to populate with the response data.
|
|
547
|
-
* @param next - The NextFunction to call to continue the middleware chain.
|
|
548
|
-
* @param middlewareType - The type of the middleware to run.
|
|
549
|
-
* @return A Promise that resolves when the middleware has been executed.
|
|
550
|
-
*/
|
|
551
|
-
private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middlewareType: Type<IMiddleware>): Promise<void> {
|
|
552
|
-
const middleware = request.context.resolve(middlewareType);
|
|
553
|
-
await middleware.invoke(request, response, next);
|
|
280
|
+
private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middleware: Middleware): Promise<void> {
|
|
281
|
+
await middleware(request, response, next);
|
|
554
282
|
}
|
|
555
283
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
* This method creates an instance of the guard and calls its `canActivate` method.
|
|
559
|
-
* If the guard returns false, it throws an UnauthorizedException.
|
|
560
|
-
* @param request - The Request object containing the request data.
|
|
561
|
-
* @param guardType - The type of the guard to run.
|
|
562
|
-
* @return A Promise that resolves if the guard allows the request, or throws an UnauthorizedException if not.
|
|
563
|
-
* @throws UnauthorizedException if the guard denies access to the request.
|
|
564
|
-
*/
|
|
565
|
-
private async runGuard(request: Request, guardType: Type<IGuard>): Promise<void> {
|
|
566
|
-
const guard = request.context.resolve(guardType);
|
|
567
|
-
const allowed = await guard.canActivate(request);
|
|
568
|
-
|
|
569
|
-
if(!allowed)
|
|
284
|
+
private async runGuard(request: Request, guard: Guard): Promise<void> {
|
|
285
|
+
if (!await guard(request)) {
|
|
570
286
|
throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
|
|
287
|
+
}
|
|
571
288
|
}
|
|
572
289
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
* @param actual - The actual request path.
|
|
578
|
-
* @param template - The template path to extract parameters from.
|
|
579
|
-
* @returns An object containing the extracted parameters.
|
|
580
|
-
*/
|
|
290
|
+
// -------------------------------------------------------------------------
|
|
291
|
+
// Utilities
|
|
292
|
+
// -------------------------------------------------------------------------
|
|
293
|
+
|
|
581
294
|
private extractParams(actual: string, template: string): Record<string, string> {
|
|
582
295
|
const aParts = actual.split('/');
|
|
583
296
|
const tParts = template.split('/');
|
|
584
297
|
const params: Record<string, string> = {};
|
|
585
|
-
|
|
586
298
|
tParts.forEach((part, i) => {
|
|
587
|
-
if(part.startsWith(':'))
|
|
588
|
-
params[part.slice(1)] = aParts[i] ?? '';
|
|
589
|
-
}
|
|
299
|
+
if (part.startsWith(':')) params[part.slice(1)] = aParts[i] ?? '';
|
|
590
300
|
});
|
|
591
|
-
|
|
592
301
|
return params;
|
|
593
302
|
}
|
|
303
|
+
|
|
304
|
+
private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
|
|
305
|
+
if (body === null || typeof body !== 'object') {
|
|
306
|
+
throw new BadRequestException('Batch payload must be an object containing a requests array.');
|
|
307
|
+
}
|
|
308
|
+
const { requests } = body as Partial<IBatchRequestPayload>;
|
|
309
|
+
if (!Array.isArray(requests)) throw new BadRequestException('Batch payload must define a requests array.');
|
|
310
|
+
return { requests: requests.map((e, i) => this.normalizeBatchItem(e, i)) };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
|
|
314
|
+
if (entry === null || typeof entry !== 'object') throw new BadRequestException(`Batch request at index ${index} must be an object.`);
|
|
315
|
+
const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
|
|
316
|
+
if (requestId !== undefined && typeof requestId !== 'string') throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
|
|
317
|
+
if (typeof path !== 'string' || !path.length) throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
|
|
318
|
+
if (typeof method !== 'string') throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
|
|
319
|
+
const normalized = method.toUpperCase();
|
|
320
|
+
if (!isAtomicHttpMethod(normalized)) throw new BadRequestException(`Batch request at index ${index} uses unsupported method ${method}.`);
|
|
321
|
+
return { requestId, path, method: normalized, body };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private fillErrorResponse(response: IResponse, error: unknown, setCritical: (v: boolean) => void): void {
|
|
325
|
+
response.body = undefined;
|
|
326
|
+
if (error instanceof ResponseException) {
|
|
327
|
+
response.status = error.status;
|
|
328
|
+
response.error = error.message;
|
|
329
|
+
response.stack = error.stack;
|
|
330
|
+
} else if (error instanceof Error) {
|
|
331
|
+
setCritical(true);
|
|
332
|
+
response.status = 500;
|
|
333
|
+
response.error = error.message || 'Internal Server Error';
|
|
334
|
+
response.stack = error.stack;
|
|
335
|
+
} else {
|
|
336
|
+
setCritical(true);
|
|
337
|
+
response.status = 500;
|
|
338
|
+
response.error = 'Unknown error occurred';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private logResponse(request: Request, response: IResponse, ms: number, isCritical: boolean): void {
|
|
343
|
+
const msg = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(ms)}ms${Logger.colors.initial}`;
|
|
344
|
+
if (response.status < 400) Logger.log(msg);
|
|
345
|
+
else if (response.status < 500) Logger.warn(msg);
|
|
346
|
+
else isCritical ? Logger.critical(msg) : Logger.error(msg);
|
|
347
|
+
|
|
348
|
+
if (response.error) {
|
|
349
|
+
isCritical ? Logger.critical(response.error) : Logger.error(response.error);
|
|
350
|
+
if (response.stack) Logger.errorStack(response.stack);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
594
353
|
}
|