@noxfly/noxus 2.4.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 +1114 -1239
- package/dist/child.mjs +1090 -1193
- package/dist/main.d.mts +304 -261
- package/dist/main.d.ts +304 -261
- package/dist/main.js +1473 -1873
- package/dist/main.mjs +1423 -1791
- package/dist/renderer.d.mts +113 -2
- package/dist/renderer.d.ts +113 -2
- package/dist/renderer.js +144 -132
- package/dist/renderer.mjs +143 -132
- 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 +100 -81
- package/src/DI/token.ts +53 -0
- package/src/app.ts +141 -131
- package/src/bootstrap.ts +79 -40
- 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 +3 -0
- package/src/main.ts +4 -11
- 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 +221 -369
- 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/index-BxWQVi6C.d.ts +0 -253
- package/dist/index-DQBQQfMw.d.mts +0 -253
- 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,218 +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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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';
|
|
23
|
+
|
|
24
|
+
export interface ILazyRoute {
|
|
25
|
+
load: () => Promise<unknown>;
|
|
26
|
+
guards: Guard[];
|
|
27
|
+
middlewares: Middleware[];
|
|
28
|
+
loading: Promise<void> | null;
|
|
29
|
+
loaded: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface LazyRouteEntry {
|
|
33
|
+
load: (() => Promise<unknown>) | null;
|
|
34
|
+
guards: Guard[];
|
|
35
|
+
middlewares: Middleware[];
|
|
36
|
+
loading: Promise<void> | null;
|
|
37
|
+
loaded: boolean;
|
|
23
38
|
}
|
|
24
39
|
|
|
25
|
-
/**
|
|
26
|
-
* IRouteDefinition interface defines the structure of a route in the application.
|
|
27
|
-
* It includes the HTTP method, path, controller class, handler method name,
|
|
28
|
-
* guards, and middlewares associated with the route.
|
|
29
|
-
*/
|
|
30
40
|
export interface IRouteDefinition {
|
|
31
41
|
method: string;
|
|
32
42
|
path: string;
|
|
33
|
-
controller: Type<
|
|
43
|
+
controller: Type<unknown>;
|
|
34
44
|
handler: string;
|
|
35
|
-
guards:
|
|
36
|
-
middlewares:
|
|
45
|
+
guards: Guard[];
|
|
46
|
+
middlewares: Middleware[];
|
|
37
47
|
}
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
* This type defines a function that represents an action in a controller.
|
|
41
|
-
* It takes a Request and an IResponse as parameters and returns a value or a Promise.
|
|
42
|
-
*/
|
|
43
|
-
export type ControllerAction = (request: Request, response: IResponse) => any;
|
|
44
|
-
|
|
49
|
+
export type ControllerAction = (request: Request, response: IResponse) => unknown;
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
* Router class is responsible for managing the application's routing.
|
|
48
|
-
* It registers controllers, handles requests, and manages middlewares and guards.
|
|
49
|
-
*/
|
|
50
|
-
@Injectable('singleton')
|
|
51
|
+
@Injectable({ lifetime: 'singleton' })
|
|
51
52
|
export class Router {
|
|
52
53
|
private readonly routes = new RadixTree<IRouteDefinition>();
|
|
53
|
-
private readonly rootMiddlewares:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
54
|
+
private readonly rootMiddlewares: Middleware[] = [];
|
|
55
|
+
private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
|
|
56
|
+
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// Registration
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
public registerController(
|
|
62
|
+
controllerClass: Type<unknown>,
|
|
63
|
+
pathPrefix: string,
|
|
64
|
+
routeGuards: Guard[] = [],
|
|
65
|
+
routeMiddlewares: Middleware[] = [],
|
|
66
|
+
): this {
|
|
67
|
+
const meta = getControllerMetadata(controllerClass);
|
|
68
|
+
|
|
69
|
+
if (!meta) {
|
|
70
|
+
throw new Error(`[Noxus] Missing @Controller decorator on ${controllerClass.name}`);
|
|
71
|
+
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
const fullPath = `${controllerMeta.path}/${def.path}`.replace(/\/+/g, '/');
|
|
73
|
+
const routeMeta = getRouteMetadata(controllerClass);
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
const
|
|
75
|
+
for (const def of routeMeta) {
|
|
76
|
+
const fullPath = `${pathPrefix}/${def.path}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
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])];
|
|
80
81
|
|
|
81
82
|
const routeDef: IRouteDefinition = {
|
|
82
83
|
method: def.method,
|
|
83
84
|
path: fullPath,
|
|
84
85
|
controller: controllerClass,
|
|
85
86
|
handler: def.handler,
|
|
86
|
-
guards
|
|
87
|
-
middlewares
|
|
87
|
+
guards,
|
|
88
|
+
middlewares,
|
|
88
89
|
};
|
|
89
90
|
|
|
90
91
|
this.routes.insert(fullPath + '/' + def.method, routeDef);
|
|
91
92
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const actionGuardsInfo = hasActionGuards
|
|
95
|
-
? '<' + routeDef.guards.map(g => g.name).join('|') + '>'
|
|
96
|
-
: '';
|
|
97
|
-
|
|
98
|
-
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`);
|
|
99
95
|
}
|
|
100
96
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
const controllerGuardsInfo = hasCtrlGuards
|
|
104
|
-
? '<' + controllerMeta.guards.map(g => g.name).join('|') + '>'
|
|
97
|
+
const ctrlGuardInfo = routeGuards.length
|
|
98
|
+
? `<${routeGuards.map(g => g.name).join('|')}>`
|
|
105
99
|
: '';
|
|
100
|
+
Logger.log(`Mapped ${controllerClass.name}${ctrlGuardInfo} controller's routes`);
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
108
104
|
|
|
105
|
+
public registerLazyRoute(
|
|
106
|
+
pathPrefix: string,
|
|
107
|
+
load: () => Promise<unknown>,
|
|
108
|
+
guards: Guard[] = [],
|
|
109
|
+
middlewares: Middleware[] = [],
|
|
110
|
+
): this {
|
|
111
|
+
const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
|
|
112
|
+
this.lazyRoutes.set(normalized, { load, guards, middlewares, loading: null, loaded: false });
|
|
113
|
+
Logger.log(`Registered lazy route prefix {${normalized}}`);
|
|
109
114
|
return this;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
* Defines a middleware for the root of the application.
|
|
114
|
-
* This method allows you to register a middleware that will be applied to all requests
|
|
115
|
-
* to the application, regardless of the controller or action.
|
|
116
|
-
* @param middleware - The middleware class to register.
|
|
117
|
-
*/
|
|
118
|
-
public defineRootMiddleware(middleware: Type<IMiddleware>): Router {
|
|
117
|
+
public defineRootMiddleware(middleware: Middleware): this {
|
|
119
118
|
this.rootMiddlewares.push(middleware);
|
|
120
119
|
return this;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* removes it from the messagePorts map.
|
|
127
|
-
* @param channelSenderId - The ID of the sender channel to shut down.
|
|
128
|
-
*/
|
|
129
|
-
public async handle(request: Request): Promise<IResponse> {
|
|
130
|
-
if(request.method === 'BATCH') {
|
|
131
|
-
return this.handleBatch(request);
|
|
132
|
-
}
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// Request handling
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
133
125
|
|
|
134
|
-
|
|
126
|
+
public async handle(request: Request): Promise<IResponse> {
|
|
127
|
+
return request.method === 'BATCH'
|
|
128
|
+
? this.handleBatch(request)
|
|
129
|
+
: this.handleAtomic(request);
|
|
135
130
|
}
|
|
136
131
|
|
|
137
132
|
private async handleAtomic(request: Request): Promise<IResponse> {
|
|
138
133
|
Logger.comment(`> ${request.method} /${request.path}`);
|
|
139
|
-
|
|
140
134
|
const t0 = performance.now();
|
|
141
135
|
|
|
142
|
-
const response: IResponse = {
|
|
143
|
-
|
|
144
|
-
status: 200,
|
|
145
|
-
body: null,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
let isCritical: boolean = false;
|
|
136
|
+
const response: IResponse = { requestId: request.id, status: 200, body: null };
|
|
137
|
+
let isCritical = false;
|
|
149
138
|
|
|
150
139
|
try {
|
|
151
|
-
const routeDef = this.findRoute(request);
|
|
140
|
+
const routeDef = await this.findRoute(request);
|
|
152
141
|
await this.resolveController(request, response, routeDef);
|
|
153
142
|
|
|
154
|
-
if(response.status
|
|
155
|
-
throw new ResponseException(response.status, response.error);
|
|
156
|
-
}
|
|
143
|
+
if (response.status >= 400) throw new ResponseException(response.status, response.error);
|
|
157
144
|
}
|
|
158
|
-
catch(error
|
|
159
|
-
response
|
|
160
|
-
|
|
161
|
-
if(error instanceof ResponseException) {
|
|
162
|
-
response.status = error.status;
|
|
163
|
-
response.error = error.message;
|
|
164
|
-
response.stack = error.stack;
|
|
165
|
-
}
|
|
166
|
-
else if(error instanceof Error) {
|
|
167
|
-
isCritical = true;
|
|
168
|
-
response.status = 500;
|
|
169
|
-
response.error = error.message || 'Internal Server Error';
|
|
170
|
-
response.stack = error.stack || 'No stack trace available';
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
isCritical = true;
|
|
174
|
-
response.status = 500;
|
|
175
|
-
response.error = 'Unknown error occurred';
|
|
176
|
-
response.stack = 'No stack trace available';
|
|
177
|
-
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
this.fillErrorResponse(response, error, (c) => { isCritical = c; });
|
|
178
147
|
}
|
|
179
148
|
finally {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
|
|
183
|
-
|
|
184
|
-
if(response.status < 400) {
|
|
185
|
-
Logger.log(message);
|
|
186
|
-
}
|
|
187
|
-
else if(response.status < 500) {
|
|
188
|
-
Logger.warn(message);
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
if(isCritical) {
|
|
192
|
-
Logger.critical(message);
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
Logger.error(message);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if(response.error !== undefined) {
|
|
200
|
-
if(isCritical) {
|
|
201
|
-
Logger.critical(response.error);
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
Logger.error(response.error);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if(response.stack !== undefined) {
|
|
208
|
-
Logger.errorStack(response.stack);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
149
|
+
this.logResponse(request, response, performance.now() - t0, isCritical);
|
|
212
150
|
return response;
|
|
213
151
|
}
|
|
214
152
|
}
|
|
215
153
|
|
|
216
154
|
private async handleBatch(request: Request): Promise<IResponse> {
|
|
217
155
|
Logger.comment(`> ${request.method} /${request.path}`);
|
|
218
|
-
|
|
219
156
|
const t0 = performance.now();
|
|
220
157
|
|
|
221
158
|
const response: IResponse<IBatchResponsePayload> = {
|
|
@@ -223,279 +160,194 @@ export class Router {
|
|
|
223
160
|
status: 200,
|
|
224
161
|
body: { responses: [] },
|
|
225
162
|
};
|
|
226
|
-
|
|
227
|
-
let isCritical: boolean = false;
|
|
163
|
+
let isCritical = false;
|
|
228
164
|
|
|
229
165
|
try {
|
|
230
166
|
const payload = this.normalizeBatchPayload(request.body);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
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
|
+
);
|
|
239
173
|
}
|
|
240
|
-
catch(error
|
|
241
|
-
response
|
|
242
|
-
|
|
243
|
-
if(error instanceof ResponseException) {
|
|
244
|
-
response.status = error.status;
|
|
245
|
-
response.error = error.message;
|
|
246
|
-
response.stack = error.stack;
|
|
247
|
-
}
|
|
248
|
-
else if(error instanceof Error) {
|
|
249
|
-
isCritical = true;
|
|
250
|
-
response.status = 500;
|
|
251
|
-
response.error = error.message || 'Internal Server Error';
|
|
252
|
-
response.stack = error.stack || 'No stack trace available';
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
isCritical = true;
|
|
256
|
-
response.status = 500;
|
|
257
|
-
response.error = 'Unknown error occurred';
|
|
258
|
-
response.stack = 'No stack trace available';
|
|
259
|
-
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.fillErrorResponse(response, error, (c) => { isCritical = c; });
|
|
260
176
|
}
|
|
261
177
|
finally {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
|
|
265
|
-
|
|
266
|
-
if(response.status < 400) {
|
|
267
|
-
Logger.log(message);
|
|
268
|
-
}
|
|
269
|
-
else if(response.status < 500) {
|
|
270
|
-
Logger.warn(message);
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
if(isCritical) {
|
|
274
|
-
Logger.critical(message);
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
Logger.error(message);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if(response.error !== undefined) {
|
|
282
|
-
if(isCritical) {
|
|
283
|
-
Logger.critical(response.error);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
Logger.error(response.error);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if(response.stack !== undefined) {
|
|
290
|
-
Logger.errorStack(response.stack);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
178
|
+
this.logResponse(request, response, performance.now() - t0, isCritical);
|
|
294
179
|
return response;
|
|
295
180
|
}
|
|
296
181
|
}
|
|
297
182
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const possiblePayload = body as Partial<IBatchRequestPayload>;
|
|
304
|
-
const { requests } = possiblePayload;
|
|
305
|
-
|
|
306
|
-
if(!Array.isArray(requests)) {
|
|
307
|
-
throw new BadRequestException('Batch payload must define a requests array.');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const normalizedRequests = requests.map((entry, index) => this.normalizeBatchItem(entry, index));
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
// Route resolution
|
|
185
|
+
// -------------------------------------------------------------------------
|
|
311
186
|
|
|
312
|
-
|
|
187
|
+
private tryFindRoute(request: Request): IRouteDefinition | undefined {
|
|
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;
|
|
313
191
|
}
|
|
314
192
|
|
|
315
|
-
private
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
193
|
+
private async findRoute(request: Request): Promise<IRouteDefinition> {
|
|
194
|
+
const direct = this.tryFindRoute(request);
|
|
195
|
+
if (direct) return direct;
|
|
319
196
|
|
|
320
|
-
|
|
197
|
+
await this.tryLoadLazyRoute(request.path);
|
|
321
198
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
199
|
+
const afterLazy = this.tryFindRoute(request);
|
|
200
|
+
if (afterLazy) return afterLazy;
|
|
325
201
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if(typeof method !== 'string') {
|
|
331
|
-
throw new BadRequestException(`Batch request at index ${index} must define an HTTP method.`);
|
|
332
|
-
}
|
|
202
|
+
throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
|
|
203
|
+
}
|
|
333
204
|
|
|
334
|
-
|
|
205
|
+
private async tryLoadLazyRoute(requestPath: string): Promise<void> {
|
|
206
|
+
const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
|
|
335
207
|
|
|
336
|
-
|
|
337
|
-
|
|
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);
|
|
213
|
+
await entry.loading;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
338
216
|
}
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
requestId,
|
|
342
|
-
path,
|
|
343
|
-
method: normalizedMethod as AtomicHttpMethod,
|
|
344
|
-
body,
|
|
345
|
-
};
|
|
346
217
|
}
|
|
347
218
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
* If no matching route is found, it throws a NotFoundException.
|
|
352
|
-
* @param request - The Request object containing the method and path to search for.
|
|
353
|
-
* @returns The IRouteDefinition for the matched route.
|
|
354
|
-
*/
|
|
355
|
-
private findRoute(request: Request): IRouteDefinition {
|
|
356
|
-
const matchedRoutes = this.routes.search(request.path);
|
|
357
|
-
|
|
358
|
-
if(matchedRoutes?.node === undefined || matchedRoutes.node.children.length === 0) {
|
|
359
|
-
throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
|
|
360
|
-
}
|
|
219
|
+
private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
|
|
220
|
+
const t0 = performance.now();
|
|
221
|
+
InjectorExplorer.beginAccumulate();
|
|
361
222
|
|
|
362
|
-
|
|
223
|
+
await entry.load?.();
|
|
363
224
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
225
|
+
entry.loading = null;
|
|
226
|
+
entry.load = null;
|
|
227
|
+
|
|
228
|
+
InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
|
|
367
229
|
|
|
368
|
-
|
|
230
|
+
entry.loaded = true;
|
|
231
|
+
|
|
232
|
+
Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(performance.now() - t0)}ms`);
|
|
369
233
|
}
|
|
370
234
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
* It also runs the request pipeline, which includes executing middlewares and guards.
|
|
375
|
-
* @param request - The Request object containing the request data.
|
|
376
|
-
* @param response - The IResponse object to populate with the response data.
|
|
377
|
-
* @param routeDef - The IRouteDefinition for the matched route.
|
|
378
|
-
* @return A Promise that resolves when the controller action has been executed.
|
|
379
|
-
* @throws UnauthorizedException if the request is not authorized by the guards.
|
|
380
|
-
*/
|
|
381
|
-
private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
|
|
382
|
-
const controllerInstance = request.context.resolve(routeDef.controller);
|
|
235
|
+
// -------------------------------------------------------------------------
|
|
236
|
+
// Pipeline
|
|
237
|
+
// -------------------------------------------------------------------------
|
|
383
238
|
|
|
239
|
+
private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
|
|
240
|
+
const instance = request.context.resolve(routeDef.controller);
|
|
384
241
|
Object.assign(request.params, this.extractParams(request.path, routeDef.path));
|
|
385
|
-
|
|
386
|
-
await this.runRequestPipeline(request, response, routeDef, controllerInstance);
|
|
242
|
+
await this.runPipeline(request, response, routeDef, instance);
|
|
387
243
|
}
|
|
388
244
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
* @param routeDef - The IRouteDefinition for the matched route.
|
|
396
|
-
* @param controllerInstance - The instance of the controller class.
|
|
397
|
-
* @return A Promise that resolves when the request pipeline has been executed.
|
|
398
|
-
* @throws ResponseException if the response status is not successful.
|
|
399
|
-
*/
|
|
400
|
-
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> {
|
|
401
251
|
const middlewares = [...new Set([...this.rootMiddlewares, ...routeDef.middlewares])];
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
const guardsMaxIndex = middlewareMaxIndex + routeDef.guards.length;
|
|
405
|
-
|
|
252
|
+
const mwMax = middlewares.length - 1;
|
|
253
|
+
const guardMax = mwMax + routeDef.guards.length;
|
|
406
254
|
let index = -1;
|
|
407
255
|
|
|
408
256
|
const dispatch = async (i: number): Promise<void> => {
|
|
409
|
-
if(i <= index)
|
|
410
|
-
throw new Error("next() called multiple times");
|
|
411
|
-
|
|
257
|
+
if (i <= index) throw new Error('next() called multiple times');
|
|
412
258
|
index = i;
|
|
413
259
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
await this.runMiddleware(request, response, nextFn, middlewares[i]!);
|
|
418
|
-
|
|
419
|
-
if(response.status >= 400) {
|
|
420
|
-
throw new ResponseException(response.status, response.error);
|
|
421
|
-
}
|
|
422
|
-
|
|
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);
|
|
423
263
|
return;
|
|
424
264
|
}
|
|
425
265
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const guardIndex = i - middlewares.length;
|
|
429
|
-
const guardType = routeDef.guards[guardIndex]!;
|
|
430
|
-
await this.runGuard(request, guardType);
|
|
266
|
+
if (i <= guardMax) {
|
|
267
|
+
await this.runGuard(request, routeDef.guards[i - middlewares.length]!);
|
|
431
268
|
await dispatch(i + 1);
|
|
432
269
|
return;
|
|
433
270
|
}
|
|
434
271
|
|
|
435
|
-
|
|
436
|
-
const action = controllerInstance[routeDef.handler] as ControllerAction;
|
|
272
|
+
const action = (controllerInstance as Record<string, ControllerAction>)[routeDef.handler]!;
|
|
437
273
|
response.body = await action.call(controllerInstance, request, response);
|
|
438
|
-
|
|
439
|
-
// avoid parsing error on the renderer if the action just does treatment without returning anything
|
|
440
|
-
if(response.body === undefined) {
|
|
441
|
-
response.body = {};
|
|
442
|
-
}
|
|
274
|
+
if (response.body === undefined) response.body = {};
|
|
443
275
|
};
|
|
444
276
|
|
|
445
277
|
await dispatch(0);
|
|
446
278
|
}
|
|
447
279
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
* This method creates an instance of the middleware and invokes its `invoke` method,
|
|
451
|
-
* passing the request, response, and next function.
|
|
452
|
-
* @param request - The Request object containing the request data.
|
|
453
|
-
* @param response - The IResponse object to populate with the response data.
|
|
454
|
-
* @param next - The NextFunction to call to continue the middleware chain.
|
|
455
|
-
* @param middlewareType - The type of the middleware to run.
|
|
456
|
-
* @return A Promise that resolves when the middleware has been executed.
|
|
457
|
-
*/
|
|
458
|
-
private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middlewareType: Type<IMiddleware>): Promise<void> {
|
|
459
|
-
const middleware = request.context.resolve(middlewareType);
|
|
460
|
-
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);
|
|
461
282
|
}
|
|
462
283
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
* This method creates an instance of the guard and calls its `canActivate` method.
|
|
466
|
-
* If the guard returns false, it throws an UnauthorizedException.
|
|
467
|
-
* @param request - The Request object containing the request data.
|
|
468
|
-
* @param guardType - The type of the guard to run.
|
|
469
|
-
* @return A Promise that resolves if the guard allows the request, or throws an UnauthorizedException if not.
|
|
470
|
-
* @throws UnauthorizedException if the guard denies access to the request.
|
|
471
|
-
*/
|
|
472
|
-
private async runGuard(request: Request, guardType: Type<IGuard>): Promise<void> {
|
|
473
|
-
const guard = request.context.resolve(guardType);
|
|
474
|
-
const allowed = await guard.canActivate(request);
|
|
475
|
-
|
|
476
|
-
if(!allowed)
|
|
284
|
+
private async runGuard(request: Request, guard: Guard): Promise<void> {
|
|
285
|
+
if (!await guard(request)) {
|
|
477
286
|
throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
|
|
287
|
+
}
|
|
478
288
|
}
|
|
479
289
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
* @param actual - The actual request path.
|
|
485
|
-
* @param template - The template path to extract parameters from.
|
|
486
|
-
* @returns An object containing the extracted parameters.
|
|
487
|
-
*/
|
|
290
|
+
// -------------------------------------------------------------------------
|
|
291
|
+
// Utilities
|
|
292
|
+
// -------------------------------------------------------------------------
|
|
293
|
+
|
|
488
294
|
private extractParams(actual: string, template: string): Record<string, string> {
|
|
489
295
|
const aParts = actual.split('/');
|
|
490
296
|
const tParts = template.split('/');
|
|
491
297
|
const params: Record<string, string> = {};
|
|
492
|
-
|
|
493
298
|
tParts.forEach((part, i) => {
|
|
494
|
-
if(part.startsWith(':'))
|
|
495
|
-
params[part.slice(1)] = aParts[i] ?? '';
|
|
496
|
-
}
|
|
299
|
+
if (part.startsWith(':')) params[part.slice(1)] = aParts[i] ?? '';
|
|
497
300
|
});
|
|
498
|
-
|
|
499
301
|
return params;
|
|
500
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
|
+
}
|
|
501
353
|
}
|