@noxfly/noxus 2.5.0 → 3.0.0-dev.1
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 +405 -340
- package/dist/app-injector-Bz3Upc0y.d.mts +125 -0
- package/dist/app-injector-Bz3Upc0y.d.ts +125 -0
- package/dist/child.d.mts +157 -23
- package/dist/child.d.ts +157 -23
- package/dist/child.js +1111 -1341
- package/dist/child.mjs +1086 -1294
- package/dist/main.d.mts +720 -284
- package/dist/main.d.ts +720 -284
- package/dist/main.js +1471 -1650
- package/dist/main.mjs +1409 -1559
- package/dist/preload.d.mts +28 -0
- package/dist/preload.d.ts +28 -0
- package/dist/preload.js +95 -0
- package/dist/preload.mjs +70 -0
- package/dist/renderer.d.mts +159 -22
- package/dist/renderer.d.ts +159 -22
- package/dist/renderer.js +104 -177
- package/dist/renderer.mjs +100 -172
- package/dist/request-BlTtiHbi.d.ts +112 -0
- package/dist/request-qJ9EiDZc.d.mts +112 -0
- package/package.json +24 -19
- 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/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 +4 -5
- package/src/internal/app.ts +217 -0
- package/src/internal/bootstrap.ts +108 -0
- package/src/{preload-bridge.ts → internal/preload-bridge.ts} +1 -1
- package/src/{renderer-client.ts → internal/renderer-client.ts} +2 -2
- package/src/{renderer-events.ts → internal/renderer-events.ts} +1 -1
- package/src/{request.ts → internal/request.ts} +3 -3
- package/src/internal/router.ts +353 -0
- package/src/internal/routes.ts +78 -0
- package/src/{socket.ts → internal/socket.ts} +4 -4
- package/src/main.ts +10 -14
- package/src/non-electron-process.ts +1 -2
- package/src/preload.ts +10 -0
- package/src/renderer.ts +13 -0
- package/src/window/window-manager.ts +255 -0
- package/tsconfig.json +5 -10
- package/tsup.config.ts +29 -13
- 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/app.ts +0 -244
- package/src/bootstrap.ts +0 -84
- 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 +0 -594
- /package/src/{exceptions.ts → internal/exceptions.ts} +0 -0
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Lifetime } from "src/DI/app-injector";
|
|
2
|
-
|
|
3
|
-
export const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_METADATA_KEY");
|
|
4
|
-
|
|
5
|
-
export function defineInjectableMetadata(target: Function, lifetime: Lifetime): void {
|
|
6
|
-
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, lifetime, target);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getInjectableMetadata(target: Function): Lifetime | undefined {
|
|
10
|
-
return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) as Lifetime | undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function hasInjectableMetadata(target: Function): boolean {
|
|
14
|
-
return Reflect.hasMetadata(INJECTABLE_METADATA_KEY, target);
|
|
15
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2025 NoxFly
|
|
3
|
-
* @license MIT
|
|
4
|
-
* @author NoxFly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { CONTROLLER_METADATA_KEY } from "src/decorators/controller.decorator";
|
|
8
|
-
import { Injectable, INJECTABLE_METADATA_KEY } from "src/decorators/injectable.decorator";
|
|
9
|
-
import { Type } from "src/utils/types";
|
|
10
|
-
|
|
11
|
-
export interface IModuleMetadata {
|
|
12
|
-
imports?: Type<unknown>[];
|
|
13
|
-
exports?: Type<unknown>[];
|
|
14
|
-
providers?: Type<unknown>[];
|
|
15
|
-
controllers?: Type<unknown>[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Module decorator is used to define a module in the application.
|
|
20
|
-
* It is a kind of node in the routing tree, that can contains controllers, services, and other modules.
|
|
21
|
-
*
|
|
22
|
-
* @param metadata - The metadata for the module.
|
|
23
|
-
*/
|
|
24
|
-
export function Module(metadata: IModuleMetadata): ClassDecorator {
|
|
25
|
-
return (target: Function) => {
|
|
26
|
-
// Validate imports and exports: must be decorated with @Module
|
|
27
|
-
const checkModule = (arr?: Type<unknown>[], arrName?: string): void => {
|
|
28
|
-
if(!arr)
|
|
29
|
-
return;
|
|
30
|
-
|
|
31
|
-
for(const clazz of arr) {
|
|
32
|
-
if(!Reflect.getMetadata(MODULE_METADATA_KEY, clazz)) {
|
|
33
|
-
throw new Error(`Class ${clazz.name} in ${arrName} must be decorated with @Module`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Validate providers: must be decorated with @Injectable
|
|
39
|
-
const checkInjectable = (arr?: Type<unknown>[]): void => {
|
|
40
|
-
if(!arr)
|
|
41
|
-
return;
|
|
42
|
-
|
|
43
|
-
for(const clazz of arr) {
|
|
44
|
-
if(!Reflect.getMetadata(INJECTABLE_METADATA_KEY, clazz)) {
|
|
45
|
-
throw new Error(`Class ${clazz.name} in providers must be decorated with @Injectable`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Validate controllers: must be decorated with @Controller
|
|
51
|
-
const checkController = (arr?: Type<unknown>[]): void => {
|
|
52
|
-
if(!arr) return;
|
|
53
|
-
for(const clazz of arr) {
|
|
54
|
-
if(!Reflect.getMetadata(CONTROLLER_METADATA_KEY, clazz)) {
|
|
55
|
-
throw new Error(`Class ${clazz.name} in controllers must be decorated with @Controller`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
checkModule(metadata.imports, 'imports');
|
|
61
|
-
checkModule(metadata.exports, 'exports');
|
|
62
|
-
checkInjectable(metadata.providers);
|
|
63
|
-
checkController(metadata.controllers);
|
|
64
|
-
|
|
65
|
-
Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, target);
|
|
66
|
-
|
|
67
|
-
Injectable('singleton')(target);
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function getModuleMetadata(target: Function): IModuleMetadata | undefined {
|
|
72
|
-
return Reflect.getMetadata(MODULE_METADATA_KEY, target);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const MODULE_METADATA_KEY = Symbol('MODULE_METADATA_KEY');
|
package/src/router.ts
DELETED
|
@@ -1,594 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright 2025 NoxFly
|
|
3
|
-
* @license MIT
|
|
4
|
-
* @author NoxFly
|
|
5
|
-
*/
|
|
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 { InjectorExplorer } from 'src/DI/injector-explorer';
|
|
16
|
-
import { Logger } from 'src/utils/logger';
|
|
17
|
-
import { RadixTree } from 'src/utils/radix-tree';
|
|
18
|
-
import { Type } from 'src/utils/types';
|
|
19
|
-
|
|
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
|
-
export interface ILazyRoute {
|
|
25
|
-
/** Path prefix (e.g. "auth", "printing"). Matched against the first segment(s) of the request path. */
|
|
26
|
-
path: string;
|
|
27
|
-
/** Dynamic import function returning the module file. */
|
|
28
|
-
loadModule: () => Promise<unknown>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface LazyRouteEntry {
|
|
32
|
-
loadModule: () => Promise<unknown>;
|
|
33
|
-
loading: Promise<void> | null;
|
|
34
|
-
loaded: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
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
|
-
export interface IRouteDefinition {
|
|
49
|
-
method: string;
|
|
50
|
-
path: string;
|
|
51
|
-
controller: Type<any>;
|
|
52
|
-
handler: string;
|
|
53
|
-
guards: Type<IGuard>[];
|
|
54
|
-
middlewares: Type<IMiddleware>[];
|
|
55
|
-
}
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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')
|
|
69
|
-
export class Router {
|
|
70
|
-
private readonly routes = new RadixTree<IRouteDefinition>();
|
|
71
|
-
private readonly rootMiddlewares: Type<IMiddleware>[] = [];
|
|
72
|
-
private readonly lazyRoutes = new Map<string, LazyRouteEntry>();
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Registers a controller class with the router.
|
|
76
|
-
* This method extracts the route metadata from the controller class and registers it in the routing tree.
|
|
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);
|
|
85
|
-
|
|
86
|
-
if(!controllerMeta)
|
|
87
|
-
throw new Error(`Missing @Controller decorator on ${controllerClass.name}`);
|
|
88
|
-
|
|
89
|
-
const routeMetadata = getRouteMetadata(controllerClass);
|
|
90
|
-
|
|
91
|
-
for(const def of routeMetadata) {
|
|
92
|
-
const fullPath = `${controllerMeta.path}/${def.path}`.replace(/\/+/g, '/');
|
|
93
|
-
|
|
94
|
-
const routeGuards = getGuardForControllerAction(controllerClass.name, def.handler);
|
|
95
|
-
const routeMiddlewares = getMiddlewaresForControllerAction(controllerClass.name, def.handler);
|
|
96
|
-
|
|
97
|
-
const guards = new Set([...controllerGuards, ...routeGuards]);
|
|
98
|
-
const middlewares = new Set([...controllerMiddlewares, ...routeMiddlewares]);
|
|
99
|
-
|
|
100
|
-
const routeDef: IRouteDefinition = {
|
|
101
|
-
method: def.method,
|
|
102
|
-
path: fullPath,
|
|
103
|
-
controller: controllerClass,
|
|
104
|
-
handler: def.handler,
|
|
105
|
-
guards: [...guards],
|
|
106
|
-
middlewares: [...middlewares],
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
this.routes.insert(fullPath + '/' + def.method, routeDef);
|
|
110
|
-
|
|
111
|
-
const hasActionGuards = routeDef.guards.length > 0;
|
|
112
|
-
|
|
113
|
-
const actionGuardsInfo = hasActionGuards
|
|
114
|
-
? '<' + routeDef.guards.map(g => g.name).join('|') + '>'
|
|
115
|
-
: '';
|
|
116
|
-
|
|
117
|
-
Logger.log(`Mapped {${routeDef.method} /${fullPath}}${actionGuardsInfo} route`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const hasCtrlGuards = controllerMeta.guards.length > 0;
|
|
121
|
-
|
|
122
|
-
const controllerGuardsInfo = hasCtrlGuards
|
|
123
|
-
? '<' + controllerMeta.guards.map(g => g.name).join('|') + '>'
|
|
124
|
-
: '';
|
|
125
|
-
|
|
126
|
-
Logger.log(`Mapped ${controllerClass.name}${controllerGuardsInfo} controller's routes`);
|
|
127
|
-
|
|
128
|
-
return this;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Registers a lazy route. The module behind this route prefix will only
|
|
133
|
-
* be imported (and its controllers/services registered in DI) the first
|
|
134
|
-
* time a request targets this prefix.
|
|
135
|
-
*
|
|
136
|
-
* @param pathPrefix - Route prefix (e.g. "auth"). Matched against the first segment of the request path.
|
|
137
|
-
* @param loadModule - A function that returns a dynamic import promise.
|
|
138
|
-
*/
|
|
139
|
-
public registerLazyRoute(pathPrefix: string, loadModule: () => Promise<unknown>): Router {
|
|
140
|
-
const normalized = pathPrefix.replace(/^\/+|\/+$/g, '');
|
|
141
|
-
this.lazyRoutes.set(normalized, { loadModule, loading: null, loaded: false });
|
|
142
|
-
Logger.log(`Registered lazy route prefix {${normalized}}`);
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
|
|
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 {
|
|
153
|
-
this.rootMiddlewares.push(middleware);
|
|
154
|
-
return this;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Shuts down the message channel for a specific sender ID.
|
|
159
|
-
* This method closes the IPC channel for the specified sender ID and
|
|
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
|
-
}
|
|
167
|
-
|
|
168
|
-
return this.handleAtomic(request);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
private async handleAtomic(request: Request): Promise<IResponse> {
|
|
172
|
-
Logger.comment(`> ${request.method} /${request.path}`);
|
|
173
|
-
|
|
174
|
-
const t0 = performance.now();
|
|
175
|
-
|
|
176
|
-
const response: IResponse = {
|
|
177
|
-
requestId: request.id,
|
|
178
|
-
status: 200,
|
|
179
|
-
body: null,
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
let isCritical: boolean = false;
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
const routeDef = await this.findRoute(request);
|
|
186
|
-
await this.resolveController(request, response, routeDef);
|
|
187
|
-
|
|
188
|
-
if(response.status > 400) {
|
|
189
|
-
throw new ResponseException(response.status, response.error);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
catch(error: unknown) {
|
|
193
|
-
response.body = undefined;
|
|
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
|
-
}
|
|
212
|
-
}
|
|
213
|
-
finally {
|
|
214
|
-
const t1 = performance.now();
|
|
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
|
-
|
|
246
|
-
return response;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private async handleBatch(request: Request): Promise<IResponse> {
|
|
251
|
-
Logger.comment(`> ${request.method} /${request.path}`);
|
|
252
|
-
|
|
253
|
-
const t0 = performance.now();
|
|
254
|
-
|
|
255
|
-
const response: IResponse<IBatchResponsePayload> = {
|
|
256
|
-
requestId: request.id,
|
|
257
|
-
status: 200,
|
|
258
|
-
body: { responses: [] },
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
let isCritical: boolean = false;
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const payload = this.normalizeBatchPayload(request.body);
|
|
265
|
-
|
|
266
|
-
const batchPromises = payload.requests.map((item, index) => {
|
|
267
|
-
const subRequestId = item.requestId ?? `${request.id}:${index}`;
|
|
268
|
-
const atomicRequest = new Request(request.event, request.senderId, subRequestId, item.method, item.path, item.body);
|
|
269
|
-
return this.handleAtomic(atomicRequest);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
response.body!.responses = await Promise.all(batchPromises);
|
|
273
|
-
}
|
|
274
|
-
catch(error: unknown) {
|
|
275
|
-
response.body = undefined;
|
|
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
|
-
}
|
|
294
|
-
}
|
|
295
|
-
finally {
|
|
296
|
-
const t1 = performance.now();
|
|
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
|
-
|
|
328
|
-
return response;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private normalizeBatchPayload(body: unknown): IBatchRequestPayload {
|
|
333
|
-
if(body === null || typeof body !== 'object') {
|
|
334
|
-
throw new BadRequestException('Batch payload must be an object containing a requests array.');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const possiblePayload = body as Partial<IBatchRequestPayload>;
|
|
338
|
-
const { requests } = possiblePayload;
|
|
339
|
-
|
|
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
|
-
private tryFindRoute(request: Request): IRouteDefinition | undefined {
|
|
395
|
-
const matchedRoutes = this.routes.search(request.path);
|
|
396
|
-
|
|
397
|
-
if(matchedRoutes?.node === undefined || matchedRoutes.node.children.length === 0) {
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const routeDef = matchedRoutes.node.findExactChild(request.method);
|
|
402
|
-
return routeDef?.value;
|
|
403
|
-
}
|
|
404
|
-
|
|
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
|
-
private async findRoute(request: Request): Promise<IRouteDefinition> {
|
|
411
|
-
// Fast path: route already registered
|
|
412
|
-
const direct = this.tryFindRoute(request);
|
|
413
|
-
if(direct) return direct;
|
|
414
|
-
|
|
415
|
-
// Try lazy route loading
|
|
416
|
-
await this.tryLoadLazyRoute(request.path);
|
|
417
|
-
|
|
418
|
-
// Retry after lazy load
|
|
419
|
-
const afterLazy = this.tryFindRoute(request);
|
|
420
|
-
if(afterLazy) return afterLazy;
|
|
421
|
-
|
|
422
|
-
throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
|
|
423
|
-
}
|
|
424
|
-
|
|
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
|
-
private async tryLoadLazyRoute(requestPath: string): Promise<void> {
|
|
430
|
-
const firstSegment = requestPath.replace(/^\/+/, '').split('/')[0] ?? '';
|
|
431
|
-
|
|
432
|
-
// Check exact first segment, then try multi-segment prefixes
|
|
433
|
-
for(const [prefix, entry] of this.lazyRoutes) {
|
|
434
|
-
if(entry.loaded) continue;
|
|
435
|
-
|
|
436
|
-
const normalizedPath = requestPath.replace(/^\/+/, '');
|
|
437
|
-
if(normalizedPath === prefix || normalizedPath.startsWith(prefix + '/') || firstSegment === prefix) {
|
|
438
|
-
if(!entry.loading) {
|
|
439
|
-
entry.loading = this.loadLazyModule(prefix, entry);
|
|
440
|
-
}
|
|
441
|
-
await entry.loading;
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
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
|
-
private async loadLazyModule(prefix: string, entry: LazyRouteEntry): Promise<void> {
|
|
452
|
-
const t0 = performance.now();
|
|
453
|
-
|
|
454
|
-
InjectorExplorer.beginAccumulate();
|
|
455
|
-
await entry.loadModule();
|
|
456
|
-
InjectorExplorer.flushAccumulated();
|
|
457
|
-
|
|
458
|
-
entry.loaded = true;
|
|
459
|
-
|
|
460
|
-
const t1 = performance.now();
|
|
461
|
-
Logger.info(`Lazy-loaded module for prefix {${prefix}} in ${Math.round(t1 - t0)}ms`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Resolves the controller for a given route definition.
|
|
466
|
-
* This method creates an instance of the controller class and prepares the request parameters.
|
|
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);
|
|
476
|
-
|
|
477
|
-
Object.assign(request.params, this.extractParams(request.path, routeDef.path));
|
|
478
|
-
|
|
479
|
-
await this.runRequestPipeline(request, response, routeDef, controllerInstance);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Runs the request pipeline for a given request.
|
|
484
|
-
* This method executes the middlewares and guards associated with the route,
|
|
485
|
-
* and finally calls the controller action.
|
|
486
|
-
* @param request - The Request object containing the request data.
|
|
487
|
-
* @param response - The IResponse object to populate with the response data.
|
|
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> {
|
|
494
|
-
const middlewares = [...new Set([...this.rootMiddlewares, ...routeDef.middlewares])];
|
|
495
|
-
|
|
496
|
-
const middlewareMaxIndex = middlewares.length - 1;
|
|
497
|
-
const guardsMaxIndex = middlewareMaxIndex + routeDef.guards.length;
|
|
498
|
-
|
|
499
|
-
let index = -1;
|
|
500
|
-
|
|
501
|
-
const dispatch = async (i: number): Promise<void> => {
|
|
502
|
-
if(i <= index)
|
|
503
|
-
throw new Error("next() called multiple times");
|
|
504
|
-
|
|
505
|
-
index = i;
|
|
506
|
-
|
|
507
|
-
// middlewares
|
|
508
|
-
if(i <= middlewareMaxIndex) {
|
|
509
|
-
const nextFn = dispatch.bind(null, i + 1);
|
|
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
|
-
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// guards
|
|
520
|
-
if(i <= guardsMaxIndex) {
|
|
521
|
-
const guardIndex = i - middlewares.length;
|
|
522
|
-
const guardType = routeDef.guards[guardIndex]!;
|
|
523
|
-
await this.runGuard(request, guardType);
|
|
524
|
-
await dispatch(i + 1);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// endpoint action
|
|
529
|
-
const action = controllerInstance[routeDef.handler] as ControllerAction;
|
|
530
|
-
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
|
-
}
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
await dispatch(0);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Runs a middleware function in the request pipeline.
|
|
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);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Runs a guard to check if the request is authorized.
|
|
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)
|
|
570
|
-
throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Extracts parameters from the actual request path based on the template path.
|
|
575
|
-
* This method splits the actual path and the template path into segments,
|
|
576
|
-
* then maps the segments to parameters based on the template.
|
|
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
|
-
*/
|
|
581
|
-
private extractParams(actual: string, template: string): Record<string, string> {
|
|
582
|
-
const aParts = actual.split('/');
|
|
583
|
-
const tParts = template.split('/');
|
|
584
|
-
const params: Record<string, string> = {};
|
|
585
|
-
|
|
586
|
-
tParts.forEach((part, i) => {
|
|
587
|
-
if(part.startsWith(':')) {
|
|
588
|
-
params[part.slice(1)] = aParts[i] ?? '';
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
return params;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
File without changes
|