@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.
Files changed (48) hide show
  1. package/README.md +403 -341
  2. package/dist/app-injector-Bz3Upc0y.d.mts +125 -0
  3. package/dist/app-injector-Bz3Upc0y.d.ts +125 -0
  4. package/dist/child.d.mts +48 -22
  5. package/dist/child.d.ts +48 -22
  6. package/dist/child.js +1114 -1239
  7. package/dist/child.mjs +1090 -1193
  8. package/dist/main.d.mts +304 -261
  9. package/dist/main.d.ts +304 -261
  10. package/dist/main.js +1473 -1873
  11. package/dist/main.mjs +1423 -1791
  12. package/dist/renderer.d.mts +113 -2
  13. package/dist/renderer.d.ts +113 -2
  14. package/dist/renderer.js +144 -132
  15. package/dist/renderer.mjs +143 -132
  16. package/dist/request-BlTtiHbi.d.ts +112 -0
  17. package/dist/request-qJ9EiDZc.d.mts +112 -0
  18. package/package.json +7 -7
  19. package/src/DI/app-injector.ts +95 -106
  20. package/src/DI/injector-explorer.ts +100 -81
  21. package/src/DI/token.ts +53 -0
  22. package/src/app.ts +141 -131
  23. package/src/bootstrap.ts +79 -40
  24. package/src/decorators/controller.decorator.ts +38 -27
  25. package/src/decorators/guards.decorator.ts +5 -64
  26. package/src/decorators/injectable.decorator.ts +68 -15
  27. package/src/decorators/method.decorator.ts +40 -81
  28. package/src/decorators/middleware.decorator.ts +5 -72
  29. package/src/index.ts +3 -0
  30. package/src/main.ts +4 -11
  31. package/src/non-electron-process.ts +0 -1
  32. package/src/preload-bridge.ts +1 -1
  33. package/src/renderer-client.ts +2 -2
  34. package/src/renderer-events.ts +1 -1
  35. package/src/request.ts +3 -3
  36. package/src/router.ts +221 -369
  37. package/src/routes.ts +78 -0
  38. package/src/socket.ts +4 -4
  39. package/src/window/window-manager.ts +255 -0
  40. package/tsconfig.json +5 -10
  41. package/tsup.config.ts +2 -2
  42. package/dist/app-injector-B3MvgV3k.d.mts +0 -95
  43. package/dist/app-injector-B3MvgV3k.d.ts +0 -95
  44. package/dist/index-BxWQVi6C.d.ts +0 -253
  45. package/dist/index-DQBQQfMw.d.mts +0 -253
  46. package/src/decorators/inject.decorator.ts +0 -24
  47. package/src/decorators/injectable.metadata.ts +0 -15
  48. 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 'reflect-metadata';
8
- import { getControllerMetadata } from 'src/decorators/controller.decorator';
9
- import { getGuardForController, getGuardForControllerAction, IGuard } from 'src/decorators/guards.decorator';
10
- import { Injectable } from 'src/decorators/injectable.decorator';
11
- import { AtomicHttpMethod, getRouteMetadata } from 'src/decorators/method.decorator';
12
- import { getMiddlewaresForController, getMiddlewaresForControllerAction, IMiddleware, NextFunction } from 'src/decorators/middleware.decorator';
13
- import { BadRequestException, MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
14
- import { IBatchRequestItem, IBatchRequestPayload, IBatchResponsePayload, IResponse, Request } from 'src/request';
15
- import { Logger } from 'src/utils/logger';
16
- import { RadixTree } from 'src/utils/radix-tree';
17
- import { Type } from 'src/utils/types';
18
-
19
- const ATOMIC_HTTP_METHODS: ReadonlySet<AtomicHttpMethod> = new Set<AtomicHttpMethod>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
20
-
21
- function isAtomicHttpMethod(method: unknown): method is AtomicHttpMethod {
22
- return typeof method === 'string' && ATOMIC_HTTP_METHODS.has(method as AtomicHttpMethod);
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<any>;
43
+ controller: Type<unknown>;
34
44
  handler: string;
35
- guards: Type<IGuard>[];
36
- middlewares: Type<IMiddleware>[];
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: Type<IMiddleware>[] = [];
54
-
55
- /**
56
- * Registers a controller class with the router.
57
- * This method extracts the route metadata from the controller class and registers it in the routing tree.
58
- * It also handles the guards and middlewares associated with the controller.
59
- * @param controllerClass - The controller class to register.
60
- */
61
- public registerController(controllerClass: Type<unknown>): Router {
62
- const controllerMeta = getControllerMetadata(controllerClass);
63
-
64
- const controllerGuards = getGuardForController(controllerClass.name);
65
- const controllerMiddlewares = getMiddlewaresForController(controllerClass.name);
66
-
67
- if(!controllerMeta)
68
- throw new Error(`Missing @Controller decorator on ${controllerClass.name}`);
69
-
70
- const routeMetadata = getRouteMetadata(controllerClass);
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
- for(const def of routeMetadata) {
73
- const fullPath = `${controllerMeta.path}/${def.path}`.replace(/\/+/g, '/');
73
+ const routeMeta = getRouteMetadata(controllerClass);
74
74
 
75
- const routeGuards = getGuardForControllerAction(controllerClass.name, def.handler);
76
- const routeMiddlewares = getMiddlewaresForControllerAction(controllerClass.name, def.handler);
75
+ for (const def of routeMeta) {
76
+ const fullPath = `${pathPrefix}/${def.path}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
77
77
 
78
- const guards = new Set([...controllerGuards, ...routeGuards]);
79
- const middlewares = new Set([...controllerMiddlewares, ...routeMiddlewares]);
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: [...guards],
87
- middlewares: [...middlewares],
87
+ guards,
88
+ middlewares,
88
89
  };
89
90
 
90
91
  this.routes.insert(fullPath + '/' + def.method, routeDef);
91
92
 
92
- const hasActionGuards = routeDef.guards.length > 0;
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 hasCtrlGuards = controllerMeta.guards.length > 0;
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
- Logger.log(`Mapped ${controllerClass.name}${controllerGuardsInfo} controller's routes`);
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
- * Shuts down the message channel for a specific sender ID.
125
- * This method closes the IPC channel for the specified sender ID and
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
- return this.handleAtomic(request);
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
- requestId: request.id,
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 > 400) {
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: unknown) {
159
- response.body = undefined;
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
- const t1 = performance.now();
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
- const batchPromises = payload.requests.map((item, index) => {
233
- const subRequestId = item.requestId ?? `${request.id}:${index}`;
234
- const atomicRequest = new Request(request.event, request.senderId, subRequestId, item.method, item.path, item.body);
235
- return this.handleAtomic(atomicRequest);
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: unknown) {
241
- response.body = undefined;
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
- const t1 = performance.now();
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
- private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
299
- if(body === null || typeof body !== 'object') {
300
- throw new BadRequestException('Batch payload must be an object containing a requests array.');
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
- return { requests: normalizedRequests };
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 normalizeBatchItem(entry: unknown, index: number): IBatchRequestItem {
316
- if(entry === null || typeof entry !== 'object') {
317
- throw new BadRequestException(`Batch request at index ${index} must be an object.`);
318
- }
193
+ private async findRoute(request: Request): Promise<IRouteDefinition> {
194
+ const direct = this.tryFindRoute(request);
195
+ if (direct) return direct;
319
196
 
320
- const { requestId, path, method, body } = entry as Partial<IBatchRequestItem> & { method?: unknown };
197
+ await this.tryLoadLazyRoute(request.path);
321
198
 
322
- if(requestId !== undefined && typeof requestId !== 'string') {
323
- throw new BadRequestException(`Batch request at index ${index} has an invalid requestId.`);
324
- }
199
+ const afterLazy = this.tryFindRoute(request);
200
+ if (afterLazy) return afterLazy;
325
201
 
326
- if(typeof path !== 'string' || path.length === 0) {
327
- throw new BadRequestException(`Batch request at index ${index} must define a non-empty path.`);
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
- const normalizedMethod = method.toUpperCase();
205
+ private async tryLoadLazyRoute(requestPath: string): Promise<void> {
206
+ const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
335
207
 
336
- if(!isAtomicHttpMethod(normalizedMethod)) {
337
- throw new BadRequestException(`Batch request at index ${index} uses the unsupported method ${method}.`);
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
- * Finds the route definition for a given request.
350
- * This method searches the routing tree for a matching route based on the request's path and method.
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
- const routeDef = matchedRoutes.node.findExactChild(request.method);
223
+ await entry.load?.();
363
224
 
364
- if(routeDef?.value === undefined) {
365
- throw new MethodNotAllowedException(`Method Not Allowed for ${request.method} ${request.path}`);
366
- }
225
+ entry.loading = null;
226
+ entry.load = null;
227
+
228
+ InjectorExplorer.flushAccumulated(entry.guards, entry.middlewares, prefix);
367
229
 
368
- return routeDef.value;
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
- * Resolves the controller for a given route definition.
373
- * This method creates an instance of the controller class and prepares the request parameters.
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
- * Runs the request pipeline for a given request.
391
- * This method executes the middlewares and guards associated with the route,
392
- * and finally calls the controller action.
393
- * @param request - The Request object containing the request data.
394
- * @param response - The IResponse object to populate with the response data.
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 middlewareMaxIndex = middlewares.length - 1;
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
- // middlewares
415
- if(i <= middlewareMaxIndex) {
416
- const nextFn = dispatch.bind(null, i + 1);
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
- // guards
427
- if(i <= guardsMaxIndex) {
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
- // endpoint action
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
- * Runs a middleware function in the request pipeline.
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
- * Runs a guard to check if the request is authorized.
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
- * Extracts parameters from the actual request path based on the template path.
482
- * This method splits the actual path and the template path into segments,
483
- * then maps the segments to parameters based on the template.
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
  }