@noxfly/noxus 1.0.4 → 1.1.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/.editorconfig +16 -0
- package/README.md +108 -6
- package/dist/noxus.d.mts +108 -12
- package/dist/noxus.d.ts +108 -12
- package/dist/noxus.js +287 -85
- package/dist/noxus.mjs +280 -85
- package/dist/noxus.mjs.map +1 -1
- package/eslint.config.js +1 -0
- package/package.json +3 -2
- package/scripts/postbuild.js +26 -0
- package/src/DI/app-injector.ts +15 -2
- package/src/DI/injector-explorer.ts +6 -0
- package/src/app.ts +153 -0
- package/src/bootstrap.ts +13 -112
- package/src/decorators/controller.decorator.ts +6 -0
- package/src/decorators/guards.decorator.ts +8 -2
- package/src/decorators/injectable.decorator.ts +6 -0
- package/src/decorators/method.decorator.ts +6 -0
- package/src/decorators/middleware.decorator.ts +54 -0
- package/src/decorators/module.decorator.ts +5 -1
- package/src/exceptions.ts +48 -26
- package/src/index.ts +7 -0
- package/src/request.ts +9 -7
- package/src/router.ts +103 -19
- package/src/utils/logger.ts +6 -0
- package/src/utils/radix-tree.ts +6 -0
- package/src/utils/types.ts +6 -0
- package/tsup.config.ts +11 -0
- package/dist/noxus.js.map +0 -1
- package/images/screenshot-requests.png +0 -0
- package/images/screenshot-startup.png +0 -0
package/src/bootstrap.ts
CHANGED
|
@@ -1,128 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { app } from "electron/main";
|
|
8
|
+
import { NoxApp } from "src/app";
|
|
5
9
|
import { getModuleMetadata } from "src/decorators/module.decorator";
|
|
6
|
-
import {
|
|
7
|
-
import { IRequest, IResponse, Request } from "src/request";
|
|
8
|
-
import { Router } from "src/router";
|
|
10
|
+
import { inject } from "src/DI/app-injector";
|
|
9
11
|
import { Type } from "src/utils/types";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
*
|
|
13
15
|
*/
|
|
14
|
-
export async function bootstrapApplication(
|
|
16
|
+
export async function bootstrapApplication(rootModule: Type<any>): Promise<NoxApp> {
|
|
15
17
|
if(!getModuleMetadata(rootModule)) {
|
|
16
18
|
throw new Error(`Root module must be decorated with @Module`);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
if(!getInjectableMetadata(root)) {
|
|
20
|
-
throw new Error(`Root application must be decorated with @Injectable`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
21
|
await app.whenReady();
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const noxEngine = new Nox(root, rootModule);
|
|
28
|
-
|
|
29
|
-
const application = await noxEngine.init();
|
|
30
|
-
|
|
31
|
-
return application;
|
|
32
|
-
}
|
|
23
|
+
const noxApp = inject(NoxApp);
|
|
33
24
|
|
|
25
|
+
await noxApp.init();
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
private messagePort: Electron.MessageChannelMain | undefined;
|
|
37
|
-
|
|
38
|
-
constructor(
|
|
39
|
-
public readonly root: Type<IApp>,
|
|
40
|
-
public readonly rootModule: Type<any>
|
|
41
|
-
) {}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
45
|
-
*/
|
|
46
|
-
public async init(): Promise<IApp> {
|
|
47
|
-
const application = RootInjector.resolve(this.root);
|
|
48
|
-
|
|
49
|
-
ipcMain.on('gimme-my-port', this.giveTheClientAPort.bind(this, application));
|
|
50
|
-
|
|
51
|
-
app.once('activate', this.onAppActivated.bind(this, application));
|
|
52
|
-
app.once('window-all-closed', this.onAllWindowsClosed.bind(this, application));
|
|
53
|
-
|
|
54
|
-
await application.onReady();
|
|
55
|
-
|
|
56
|
-
console.log(''); // create a new line in the console to separate setup logs from the future logs
|
|
57
|
-
|
|
58
|
-
return application;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
*/
|
|
64
|
-
private giveTheClientAPort(application: IApp, event: Electron.IpcMainInvokeEvent): void {
|
|
65
|
-
if(this.messagePort) {
|
|
66
|
-
this.messagePort.port1.close();
|
|
67
|
-
this.messagePort.port2.close();
|
|
68
|
-
this.messagePort = undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
this.messagePort = new MessageChannelMain();
|
|
72
|
-
|
|
73
|
-
this.messagePort.port1.on('message', event => this.onClientMessage(application, event));
|
|
74
|
-
this.messagePort.port1.start();
|
|
75
|
-
|
|
76
|
-
event.sender.postMessage('port', null, [this.messagePort.port2]);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Electron specific message handling.
|
|
81
|
-
* Replaces HTTP calls by using Electron's IPC mechanism.
|
|
82
|
-
*/
|
|
83
|
-
private async onClientMessage(application: IApp, event: Electron.MessageEvent): Promise<void> {
|
|
84
|
-
const { requestId, path, method, body }: IRequest = event.data;
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
|
|
88
|
-
const request = new Request(application, event, requestId, method, path, body);
|
|
89
|
-
const router = RootInjector.resolve(Router);
|
|
90
|
-
|
|
91
|
-
const response = await router.handle(request);
|
|
92
|
-
|
|
93
|
-
this.messagePort?.port1.postMessage(response);
|
|
94
|
-
}
|
|
95
|
-
catch(err: any) {
|
|
96
|
-
const response: IResponse = {
|
|
97
|
-
requestId,
|
|
98
|
-
status: 500,
|
|
99
|
-
body: null,
|
|
100
|
-
error: err.message || 'Internal Server Error',
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
this.messagePort?.port1.postMessage(response);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
*
|
|
109
|
-
*/
|
|
110
|
-
private onAppActivated(application: IApp): void {
|
|
111
|
-
if(BrowserWindow.getAllWindows().length === 0) {
|
|
112
|
-
application.onReady();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
*/
|
|
119
|
-
private async onAllWindowsClosed(application: IApp): Promise<void> {
|
|
120
|
-
this.messagePort?.port1.close();
|
|
121
|
-
await application.dispose();
|
|
122
|
-
|
|
123
|
-
if(process.platform !== 'darwin') {
|
|
124
|
-
app.quit();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
27
|
+
return noxApp;
|
|
127
28
|
}
|
|
128
29
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import { getGuardForController, IGuard } from "src/decorators/guards.decorator";
|
|
2
8
|
import { Injectable } from "src/decorators/injectable.decorator";
|
|
3
9
|
import { Type } from "src/utils/types";
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import { Request } from 'src/request';
|
|
2
8
|
import { Logger } from 'src/utils/logger';
|
|
3
9
|
import { MaybeAsync, Type } from 'src/utils/types';
|
|
@@ -13,7 +19,7 @@ const authorizations = new Map<string, Type<IGuard>[]>();
|
|
|
13
19
|
* Peut être utilisé sur une classe controleur, ou sur une méthode de contrôleur.
|
|
14
20
|
*/
|
|
15
21
|
export function Authorize(...guardClasses: Type<IGuard>[]): MethodDecorator & ClassDecorator {
|
|
16
|
-
return (target:
|
|
22
|
+
return (target: Function | object, propertyKey?: string | symbol) => {
|
|
17
23
|
let key: string;
|
|
18
24
|
|
|
19
25
|
// Method decorator
|
|
@@ -32,7 +38,7 @@ export function Authorize(...guardClasses: Type<IGuard>[]): MethodDecorator & Cl
|
|
|
32
38
|
throw new Error(`Guard(s) already registered for ${key}`);
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
Logger.debug(`Registering
|
|
41
|
+
Logger.debug(`Registering guard(s) for ${key}: ${guardClasses.map(c => c.name).join(', ')}`);
|
|
36
42
|
|
|
37
43
|
authorizations.set(key, guardClasses);
|
|
38
44
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { IResponse, Request } from "src/request";
|
|
8
|
+
import { Logger } from "src/utils/logger";
|
|
9
|
+
import { MaybeAsync, Type } from "src/utils/types";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export type NextFunction = () => Promise<void>;
|
|
13
|
+
|
|
14
|
+
export interface IMiddleware {
|
|
15
|
+
invoke(request: Request, response: IResponse, next: NextFunction): MaybeAsync<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const middlewares = new Map<string, Type<IMiddleware>[]>();
|
|
19
|
+
|
|
20
|
+
export function UseMiddlewares(mdlw: Type<IMiddleware>[]): ClassDecorator & MethodDecorator {
|
|
21
|
+
return (target: Function | object, propertyKey?: string | symbol) => {
|
|
22
|
+
let key: string;
|
|
23
|
+
|
|
24
|
+
// Method decorator
|
|
25
|
+
if(propertyKey) {
|
|
26
|
+
const ctrlName = target.constructor.name;
|
|
27
|
+
const actionName = propertyKey as string;
|
|
28
|
+
key = `${ctrlName}.${actionName}`;
|
|
29
|
+
}
|
|
30
|
+
// Class decorator
|
|
31
|
+
else {
|
|
32
|
+
const ctrlName = (target as Type<unknown>).name;
|
|
33
|
+
key = `${ctrlName}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if(middlewares.has(key)) {
|
|
37
|
+
throw new Error(`Middlewares(s) already registered for ${key}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Logger.debug(`Registering middleware(s) for ${key}: ${mdlw.map(c => c.name).join(', ')}`);
|
|
41
|
+
|
|
42
|
+
middlewares.set(key, mdlw);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getMiddlewaresForController(controllerName: string): Type<IMiddleware>[] {
|
|
47
|
+
const key = `${controllerName}`;
|
|
48
|
+
return middlewares.get(key) ?? [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getMiddlewaresForControllerAction(controllerName: string, actionName: string): Type<IMiddleware>[] {
|
|
52
|
+
const key = `${controllerName}.${actionName}`;
|
|
53
|
+
return middlewares.get(key) ?? [];
|
|
54
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
2
6
|
|
|
3
7
|
import { CONTROLLER_METADATA_KEY } from "src/decorators/controller.decorator";
|
|
4
8
|
import { Injectable, INJECTABLE_METADATA_KEY } from "src/decorators/injectable.decorator";
|
package/src/exceptions.ts
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
export class ResponseException extends Error {
|
|
8
|
+
public readonly status: number = 0;
|
|
9
|
+
|
|
10
|
+
constructor(message?: string);
|
|
11
|
+
constructor(statusCode?: number, message?: string);
|
|
12
|
+
constructor(statusOrMessage?: number | string, message?: string) {
|
|
13
|
+
let statusCode: number | undefined;
|
|
14
|
+
|
|
15
|
+
if(typeof statusOrMessage === 'number') {
|
|
16
|
+
statusCode = statusOrMessage;
|
|
17
|
+
}
|
|
18
|
+
else if(typeof statusOrMessage === 'string') {
|
|
19
|
+
message = statusOrMessage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
super(message ?? "");
|
|
23
|
+
|
|
24
|
+
if(statusCode !== undefined) {
|
|
25
|
+
this.status = statusCode;
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
this.name = this.constructor.name
|
|
8
29
|
.replace(/([A-Z])/g, ' $1');
|
|
@@ -10,26 +31,27 @@ export abstract class ResponseException extends Error {
|
|
|
10
31
|
}
|
|
11
32
|
|
|
12
33
|
// 4XX
|
|
13
|
-
export class BadRequestException extends ResponseException { public readonly status = 400; }
|
|
14
|
-
export class UnauthorizedException extends ResponseException { public readonly status = 401; }
|
|
15
|
-
export class
|
|
16
|
-
export class
|
|
17
|
-
export class
|
|
18
|
-
export class
|
|
19
|
-
export class
|
|
20
|
-
export class
|
|
21
|
-
export class
|
|
22
|
-
export class
|
|
34
|
+
export class BadRequestException extends ResponseException { public override readonly status = 400; }
|
|
35
|
+
export class UnauthorizedException extends ResponseException { public override readonly status = 401; }
|
|
36
|
+
export class PaymentRequiredException extends ResponseException { public override readonly status = 402; }
|
|
37
|
+
export class ForbiddenException extends ResponseException { public override readonly status = 403; }
|
|
38
|
+
export class NotFoundException extends ResponseException { public override readonly status = 404; }
|
|
39
|
+
export class MethodNotAllowedException extends ResponseException { public override readonly status = 405; }
|
|
40
|
+
export class NotAcceptableException extends ResponseException { public override readonly status = 406; }
|
|
41
|
+
export class RequestTimeoutException extends ResponseException { public override readonly status = 408; }
|
|
42
|
+
export class ConflictException extends ResponseException { public override readonly status = 409; }
|
|
43
|
+
export class UpgradeRequiredException extends ResponseException { public override readonly status = 426; }
|
|
44
|
+
export class TooManyRequestsException extends ResponseException { public override readonly status = 429; }
|
|
23
45
|
// 5XX
|
|
24
|
-
export class InternalServerException extends ResponseException { public readonly status = 500; }
|
|
25
|
-
export class NotImplementedException extends ResponseException { public readonly status = 501; }
|
|
26
|
-
export class BadGatewayException extends ResponseException { public readonly status = 502; }
|
|
27
|
-
export class ServiceUnavailableException extends ResponseException { public readonly status = 503; }
|
|
28
|
-
export class GatewayTimeoutException extends ResponseException { public readonly status = 504; }
|
|
29
|
-
export class HttpVersionNotSupportedException extends ResponseException { public readonly status = 505; }
|
|
30
|
-
export class VariantAlsoNegotiatesException extends ResponseException { public readonly status = 506; }
|
|
31
|
-
export class InsufficientStorageException extends ResponseException { public readonly status = 507; }
|
|
32
|
-
export class LoopDetectedException extends ResponseException { public readonly status = 508; }
|
|
33
|
-
export class NotExtendedException extends ResponseException { public readonly status = 510; }
|
|
34
|
-
export class NetworkAuthenticationRequiredException extends ResponseException { public readonly status = 511; }
|
|
35
|
-
export class NetworkConnectTimeoutException extends ResponseException { public readonly status = 599; }
|
|
46
|
+
export class InternalServerException extends ResponseException { public override readonly status = 500; }
|
|
47
|
+
export class NotImplementedException extends ResponseException { public override readonly status = 501; }
|
|
48
|
+
export class BadGatewayException extends ResponseException { public override readonly status = 502; }
|
|
49
|
+
export class ServiceUnavailableException extends ResponseException { public override readonly status = 503; }
|
|
50
|
+
export class GatewayTimeoutException extends ResponseException { public override readonly status = 504; }
|
|
51
|
+
export class HttpVersionNotSupportedException extends ResponseException { public override readonly status = 505; }
|
|
52
|
+
export class VariantAlsoNegotiatesException extends ResponseException { public override readonly status = 506; }
|
|
53
|
+
export class InsufficientStorageException extends ResponseException { public override readonly status = 507; }
|
|
54
|
+
export class LoopDetectedException extends ResponseException { public override readonly status = 508; }
|
|
55
|
+
export class NotExtendedException extends ResponseException { public override readonly status = 510; }
|
|
56
|
+
export class NetworkAuthenticationRequiredException extends ResponseException { public override readonly status = 511; }
|
|
57
|
+
export class NetworkConnectTimeoutException extends ResponseException { public override readonly status = 599; }
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
export * from './DI/app-injector';
|
|
2
8
|
export * from './router';
|
|
3
9
|
export * from './app';
|
|
4
10
|
export * from './bootstrap';
|
|
5
11
|
export * from './exceptions';
|
|
12
|
+
export * from './decorators/middleware.decorator';
|
|
6
13
|
export * from './decorators/guards.decorator';
|
|
7
14
|
export * from './decorators/controller.decorator';
|
|
8
15
|
export * from './decorators/injectable.decorator';
|
package/src/request.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import 'reflect-metadata';
|
|
2
|
-
import { IApp } from 'src/app';
|
|
3
8
|
import { HttpMethod } from 'src/decorators/method.decorator';
|
|
4
|
-
import { RootInjector } from 'src/DI/app-injector';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
9
|
+
import { AppInjector, RootInjector } from 'src/DI/app-injector';
|
|
8
10
|
|
|
9
11
|
export class Request {
|
|
10
|
-
public readonly context:
|
|
12
|
+
public readonly context: AppInjector = RootInjector.createScope();
|
|
11
13
|
|
|
12
14
|
public readonly params: Record<string, string> = {};
|
|
13
15
|
|
|
14
16
|
constructor(
|
|
15
|
-
public readonly app: IApp,
|
|
16
17
|
public readonly event: Electron.MessageEvent,
|
|
17
18
|
public readonly id: string,
|
|
18
19
|
public readonly method: HttpMethod,
|
|
@@ -24,6 +25,7 @@ export class Request {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface IRequest<T = any> {
|
|
28
|
+
senderId: number;
|
|
27
29
|
requestId: string;
|
|
28
30
|
path: string;
|
|
29
31
|
method: HttpMethod;
|
package/src/router.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import 'reflect-metadata';
|
|
2
8
|
import { getControllerMetadata } from 'src/decorators/controller.decorator';
|
|
3
9
|
import { getGuardForController, getGuardForControllerAction, IGuard } from 'src/decorators/guards.decorator';
|
|
4
10
|
import { Injectable } from 'src/decorators/injectable.decorator';
|
|
5
11
|
import { getRouteMetadata } from 'src/decorators/method.decorator';
|
|
12
|
+
import { getMiddlewaresForController, getMiddlewaresForControllerAction, IMiddleware, NextFunction } from 'src/decorators/middleware.decorator';
|
|
6
13
|
import { MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
|
|
7
14
|
import { IResponse, Request } from 'src/request';
|
|
8
15
|
import { Logger } from 'src/utils/logger';
|
|
@@ -17,6 +24,7 @@ export interface IRouteDefinition {
|
|
|
17
24
|
controller: Type<any>;
|
|
18
25
|
handler: string;
|
|
19
26
|
guards: Type<IGuard>[];
|
|
27
|
+
middlewares: Type<IMiddleware>[];
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
export type ControllerAction = (request: Request, response: IResponse) => any;
|
|
@@ -25,11 +33,16 @@ export type ControllerAction = (request: Request, response: IResponse) => any;
|
|
|
25
33
|
@Injectable('singleton')
|
|
26
34
|
export class Router {
|
|
27
35
|
private readonly routes = new RadixTree<IRouteDefinition>();
|
|
36
|
+
private readonly rootMiddlewares: Type<IMiddleware>[] = [];
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
29
41
|
public registerController(controllerClass: Type<unknown>): Router {
|
|
30
42
|
const controllerMeta = getControllerMetadata(controllerClass);
|
|
31
43
|
|
|
32
44
|
const controllerGuards = getGuardForController(controllerClass.name);
|
|
45
|
+
const controllerMiddlewares = getMiddlewaresForController(controllerClass.name);
|
|
33
46
|
|
|
34
47
|
if(!controllerMeta)
|
|
35
48
|
throw new Error(`Missing @Controller decorator on ${controllerClass.name}`);
|
|
@@ -40,8 +53,10 @@ export class Router {
|
|
|
40
53
|
const fullPath = `${controllerMeta.path}/${def.path}`.replace(/\/+/g, '/');
|
|
41
54
|
|
|
42
55
|
const routeGuards = getGuardForControllerAction(controllerClass.name, def.handler);
|
|
56
|
+
const routeMiddlewares = getMiddlewaresForControllerAction(controllerClass.name, def.handler);
|
|
43
57
|
|
|
44
58
|
const guards = new Set([...controllerGuards, ...routeGuards]);
|
|
59
|
+
const middlewares = new Set([...controllerMiddlewares, ...routeMiddlewares]);
|
|
45
60
|
|
|
46
61
|
const routeDef: IRouteDefinition = {
|
|
47
62
|
method: def.method,
|
|
@@ -49,6 +64,7 @@ export class Router {
|
|
|
49
64
|
controller: controllerClass,
|
|
50
65
|
handler: def.handler,
|
|
51
66
|
guards: [...guards],
|
|
67
|
+
middlewares: [...middlewares],
|
|
52
68
|
};
|
|
53
69
|
|
|
54
70
|
this.routes.insert(fullPath + '/' + def.method, routeDef);
|
|
@@ -73,6 +89,18 @@ export class Router {
|
|
|
73
89
|
return this;
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
/**
|
|
93
|
+
*
|
|
94
|
+
*/
|
|
95
|
+
public defineRootMiddleware(middleware: Type<IMiddleware>): Router {
|
|
96
|
+
Logger.debug(`Registering root middleware: ${middleware.name}`);
|
|
97
|
+
this.rootMiddlewares.push(middleware);
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
*
|
|
103
|
+
*/
|
|
76
104
|
public async handle(request: Request): Promise<IResponse> {
|
|
77
105
|
Logger.log(`> Received request: {${request.method} /${request.path}}`);
|
|
78
106
|
|
|
@@ -87,13 +115,11 @@ export class Router {
|
|
|
87
115
|
|
|
88
116
|
try {
|
|
89
117
|
const routeDef = this.findRoute(request);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const action = controllerInstance[routeDef.handler] as ControllerAction;
|
|
93
|
-
|
|
94
|
-
this.verifyRequestBody(request, action);
|
|
118
|
+
await this.resolveController(request, response, routeDef);
|
|
95
119
|
|
|
96
|
-
response.
|
|
120
|
+
if(response.status > 400) {
|
|
121
|
+
throw new ResponseException(response.status, response.error);
|
|
122
|
+
}
|
|
97
123
|
}
|
|
98
124
|
catch(error: unknown) {
|
|
99
125
|
if(error instanceof ResponseException) {
|
|
@@ -129,6 +155,9 @@ export class Router {
|
|
|
129
155
|
}
|
|
130
156
|
}
|
|
131
157
|
|
|
158
|
+
/**
|
|
159
|
+
*
|
|
160
|
+
*/
|
|
132
161
|
private findRoute(request: Request): IRouteDefinition {
|
|
133
162
|
const matchedRoutes = this.routes.search(request.path);
|
|
134
163
|
|
|
@@ -145,30 +174,85 @@ export class Router {
|
|
|
145
174
|
return routeDef.value;
|
|
146
175
|
}
|
|
147
176
|
|
|
148
|
-
|
|
177
|
+
/**
|
|
178
|
+
*
|
|
179
|
+
*/
|
|
180
|
+
private async resolveController(request: Request, response: IResponse, routeDef: IRouteDefinition): Promise<void> {
|
|
149
181
|
const controllerInstance = request.context.resolve(routeDef.controller);
|
|
150
182
|
|
|
151
183
|
Object.assign(request.params, this.extractParams(request.path, routeDef.path));
|
|
152
184
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
185
|
+
await this.runRequestPipeline(request, response, routeDef, controllerInstance);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
*
|
|
190
|
+
*/
|
|
191
|
+
private async runRequestPipeline(request: Request, response: IResponse, routeDef: IRouteDefinition, controllerInstance: any): Promise<void> {
|
|
192
|
+
const middlewares = [...new Set([...this.rootMiddlewares, ...routeDef.middlewares])];
|
|
157
193
|
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
const middlewareMaxIndex = middlewares.length - 1;
|
|
195
|
+
const guardsMaxIndex = middlewareMaxIndex + routeDef.guards.length;
|
|
196
|
+
|
|
197
|
+
let index = -1;
|
|
198
|
+
|
|
199
|
+
const dispatch = async (i: number): Promise<void> => {
|
|
200
|
+
if(i <= index)
|
|
201
|
+
throw new Error("next() called multiple times");
|
|
202
|
+
|
|
203
|
+
index = i;
|
|
204
|
+
|
|
205
|
+
// middlewares
|
|
206
|
+
if(i <= middlewareMaxIndex) {
|
|
207
|
+
const nextFn = dispatch.bind(null, i + 1);
|
|
208
|
+
await this.runMiddleware(request, response, nextFn, middlewares[i]!);
|
|
209
|
+
|
|
210
|
+
if(response.status >= 400) {
|
|
211
|
+
throw new ResponseException(response.status, response.error);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// guards
|
|
218
|
+
if(i <= guardsMaxIndex) {
|
|
219
|
+
const guardIndex = i - middlewares.length;
|
|
220
|
+
const guardType = routeDef.guards[guardIndex]!;
|
|
221
|
+
await this.runGuard(request, guardType);
|
|
222
|
+
dispatch(i + 1);
|
|
223
|
+
return;
|
|
160
224
|
}
|
|
161
|
-
}
|
|
162
225
|
|
|
163
|
-
|
|
226
|
+
// endpoint action
|
|
227
|
+
const action = controllerInstance[routeDef.handler] as ControllerAction;
|
|
228
|
+
response.body = await action.call(controllerInstance, request, response);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
await dispatch(0);
|
|
164
232
|
}
|
|
165
233
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
234
|
+
/**
|
|
235
|
+
*
|
|
236
|
+
*/
|
|
237
|
+
private async runMiddleware(request: Request, response: IResponse, next: NextFunction, middlewareType: Type<IMiddleware>): Promise<void> {
|
|
238
|
+
const middleware = request.context.resolve(middlewareType);
|
|
239
|
+
await middleware.invoke(request, response, next);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
*
|
|
244
|
+
*/
|
|
245
|
+
private async runGuard(request: Request, guardType: Type<IGuard>): Promise<void> {
|
|
246
|
+
const guard = request.context.resolve(guardType);
|
|
247
|
+
const allowed = await guard.canActivate(request);
|
|
248
|
+
|
|
249
|
+
if(!allowed)
|
|
250
|
+
throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
|
|
170
251
|
}
|
|
171
252
|
|
|
253
|
+
/**
|
|
254
|
+
*
|
|
255
|
+
*/
|
|
172
256
|
private extractParams(actual: string, template: string): Record<string, string> {
|
|
173
257
|
const aParts = actual.split('/');
|
|
174
258
|
const tParts = template.split('/');
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright 2025 NoxFly
|
|
3
|
+
* @license MIT
|
|
4
|
+
* @author NoxFly
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
function getPrettyTimestamp(): string {
|
|
2
8
|
const now = new Date();
|
|
3
9
|
return `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`
|
package/src/utils/radix-tree.ts
CHANGED
package/src/utils/types.ts
CHANGED
package/tsup.config.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { defineConfig } from "tsup";
|
|
2
2
|
|
|
3
|
+
const copyrights = `
|
|
4
|
+
/**
|
|
5
|
+
* @copyright 2025 NoxFly
|
|
6
|
+
* @license MIT
|
|
7
|
+
* @author NoxFly
|
|
8
|
+
*/
|
|
9
|
+
`.trim()
|
|
10
|
+
|
|
3
11
|
export default defineConfig({
|
|
4
12
|
entry: {
|
|
5
13
|
noxus: "src/index.ts"
|
|
@@ -17,4 +25,7 @@ export default defineConfig({
|
|
|
17
25
|
splitting: false,
|
|
18
26
|
shims: false,
|
|
19
27
|
treeshake: false,
|
|
28
|
+
banner: {
|
|
29
|
+
js: copyrights,
|
|
30
|
+
}
|
|
20
31
|
});
|