@noxfly/noxus 1.0.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/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/noxus.d.mts +231 -0
- package/dist/noxus.d.ts +231 -0
- package/dist/noxus.js +1011 -0
- package/dist/noxus.js.map +1 -0
- package/dist/noxus.mjs +940 -0
- package/dist/noxus.mjs.map +1 -0
- package/eslint.config.js +108 -0
- package/images/screenshot-requests.png +0 -0
- package/images/screenshot-startup.png +0 -0
- package/package.json +41 -0
- package/src/app-injector.ts +80 -0
- package/src/app.ts +72 -0
- package/src/bootstrap.ts +126 -0
- package/src/exceptions.ts +35 -0
- package/src/guards.ts +51 -0
- package/src/index.ts +10 -0
- package/src/injector-explorer.ts +53 -0
- package/src/logger.ts +136 -0
- package/src/metadata.ts +52 -0
- package/src/misc.ts +1 -0
- package/src/radix-tree.ts +137 -0
- package/src/request.ts +38 -0
- package/src/router.ts +221 -0
- package/tsconfig.json +34 -0
- package/tsup.config.ts +20 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { Injectable } from 'src/app';
|
|
3
|
+
import { MethodNotAllowedException, NotFoundException, ResponseException, UnauthorizedException } from 'src/exceptions';
|
|
4
|
+
import { getGuardForController, getGuardForControllerAction, IGuard } from 'src/guards';
|
|
5
|
+
import { Logger } from 'src/logger';
|
|
6
|
+
import { CONTROLLER_METADATA_KEY, IControllerMetadata, getControllerMetadata, getRouteMetadata, ROUTE_METADATA_KEY, IRouteMetadata, Type } from 'src/metadata';
|
|
7
|
+
import { RadixTree } from 'src/radix-tree';
|
|
8
|
+
import { Request, IResponse } from 'src/request';
|
|
9
|
+
|
|
10
|
+
// types & interfaces
|
|
11
|
+
|
|
12
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
13
|
+
|
|
14
|
+
export interface IRouteDefinition {
|
|
15
|
+
method: string;
|
|
16
|
+
path: string;
|
|
17
|
+
controller: Type<any>;
|
|
18
|
+
handler: string;
|
|
19
|
+
guards: Type<IGuard>[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ControllerAction = (request: Request, response: IResponse) => any;
|
|
23
|
+
|
|
24
|
+
export function Controller(path: string): ClassDecorator {
|
|
25
|
+
return (target) => {
|
|
26
|
+
const data: IControllerMetadata = {
|
|
27
|
+
path,
|
|
28
|
+
guards: getGuardForController(target.name)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
Reflect.defineMetadata(CONTROLLER_METADATA_KEY, data, target);
|
|
32
|
+
Injectable('scope')(target);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createRouteDecorator(verb: HttpMethod): (path: string) => MethodDecorator {
|
|
37
|
+
return (path: string): MethodDecorator => {
|
|
38
|
+
return (target, propertyKey) => {
|
|
39
|
+
const existingRoutes: IRouteMetadata[] = Reflect.getMetadata(ROUTE_METADATA_KEY, target.constructor) || [];
|
|
40
|
+
|
|
41
|
+
const metadata: IRouteMetadata = {
|
|
42
|
+
method: verb,
|
|
43
|
+
path: path.trim().replace(/^\/|\/$/g, ''),
|
|
44
|
+
handler: propertyKey as string,
|
|
45
|
+
guards: getGuardForControllerAction((target.constructor as any).__controllerName, propertyKey as string),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
existingRoutes.push(metadata);
|
|
49
|
+
|
|
50
|
+
Reflect.defineMetadata(ROUTE_METADATA_KEY, existingRoutes, target.constructor);
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Get = createRouteDecorator('GET');
|
|
56
|
+
export const Post = createRouteDecorator('POST');
|
|
57
|
+
export const Put = createRouteDecorator('PUT');
|
|
58
|
+
export const Patch = createRouteDecorator('PATCH');
|
|
59
|
+
export const Delete = createRouteDecorator('DELETE');
|
|
60
|
+
|
|
61
|
+
@Injectable('singleton')
|
|
62
|
+
export class Router {
|
|
63
|
+
private readonly routes = new RadixTree<IRouteDefinition>();
|
|
64
|
+
|
|
65
|
+
public registerController(controllerClass: Type<unknown>): Router {
|
|
66
|
+
const controllerMeta = getControllerMetadata(controllerClass);
|
|
67
|
+
|
|
68
|
+
const controllerGuards = getGuardForController(controllerClass.name);
|
|
69
|
+
|
|
70
|
+
if(!controllerMeta)
|
|
71
|
+
throw new Error(`Missing @Controller decorator on ${controllerClass.name}`);
|
|
72
|
+
|
|
73
|
+
const routeMetadata = getRouteMetadata(controllerClass);
|
|
74
|
+
|
|
75
|
+
for(const def of routeMetadata) {
|
|
76
|
+
const fullPath = `${controllerMeta.path}/${def.path}`.replace(/\/+/g, '/');
|
|
77
|
+
|
|
78
|
+
const routeGuards = getGuardForControllerAction(controllerClass.name, def.handler);
|
|
79
|
+
|
|
80
|
+
const guards = new Set([...controllerGuards, ...routeGuards]);
|
|
81
|
+
|
|
82
|
+
const routeDef: IRouteDefinition = {
|
|
83
|
+
method: def.method,
|
|
84
|
+
path: fullPath,
|
|
85
|
+
controller: controllerClass,
|
|
86
|
+
handler: def.handler,
|
|
87
|
+
guards: [...guards],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.routes.insert(fullPath + '/' + def.method, routeDef);
|
|
91
|
+
|
|
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`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hasCtrlGuards = controllerMeta.guards.length > 0;
|
|
102
|
+
|
|
103
|
+
const controllerGuardsInfo = hasCtrlGuards
|
|
104
|
+
? '<' + controllerMeta.guards.map(g => g.name).join('|') + '>'
|
|
105
|
+
: '';
|
|
106
|
+
|
|
107
|
+
Logger.log(`Mapped ${controllerClass.name}${controllerGuardsInfo} controller's routes`);
|
|
108
|
+
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public async handle(request: Request): Promise<IResponse> {
|
|
113
|
+
Logger.log(`> Received request: {${request.method} /${request.path}}`);
|
|
114
|
+
|
|
115
|
+
const t0 = performance.now();
|
|
116
|
+
|
|
117
|
+
const response: IResponse = {
|
|
118
|
+
requestId: request.id,
|
|
119
|
+
status: 200,
|
|
120
|
+
body: null,
|
|
121
|
+
error: undefined,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const routeDef = this.findRoute(request);
|
|
126
|
+
const controllerInstance = await this.resolveController(request, routeDef);
|
|
127
|
+
|
|
128
|
+
const action = controllerInstance[routeDef.handler] as ControllerAction;
|
|
129
|
+
|
|
130
|
+
this.verifyRequestBody(request, action);
|
|
131
|
+
|
|
132
|
+
response.body = await action.call(controllerInstance, request, response);
|
|
133
|
+
}
|
|
134
|
+
catch(error: unknown) {
|
|
135
|
+
if(error instanceof ResponseException) {
|
|
136
|
+
response.status = error.status;
|
|
137
|
+
response.error = error.message;
|
|
138
|
+
}
|
|
139
|
+
else if(error instanceof Error) {
|
|
140
|
+
response.status = 500;
|
|
141
|
+
response.error = error.message || 'Internal Server Error';
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
response.status = 500;
|
|
145
|
+
response.error = 'Unknown error occurred';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
const t1 = performance.now();
|
|
150
|
+
|
|
151
|
+
const message = `< ${response.status} ${request.method} /${request.path} ${Logger.colors.yellow}${Math.round(t1 - t0)}ms${Logger.colors.initial}`;
|
|
152
|
+
|
|
153
|
+
if(response.status < 400)
|
|
154
|
+
Logger.log(message);
|
|
155
|
+
else if(response.status < 500)
|
|
156
|
+
Logger.warn(message);
|
|
157
|
+
else
|
|
158
|
+
Logger.error(message);
|
|
159
|
+
|
|
160
|
+
if(response.error !== undefined) {
|
|
161
|
+
Logger.error(response.error);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private findRoute(request: Request): IRouteDefinition {
|
|
169
|
+
const matchedRoutes = this.routes.search(request.path);
|
|
170
|
+
|
|
171
|
+
if(matchedRoutes?.node === undefined || matchedRoutes.node.children.length === 0) {
|
|
172
|
+
throw new NotFoundException(`No route matches ${request.method} ${request.path}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const routeDef = matchedRoutes.node.findExactChild(request.method);
|
|
176
|
+
|
|
177
|
+
if(routeDef?.value === undefined) {
|
|
178
|
+
throw new MethodNotAllowedException(`Method Not Allowed for ${request.method} ${request.path}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return routeDef.value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private async resolveController(request: Request, routeDef: IRouteDefinition): Promise<any> {
|
|
185
|
+
const controllerInstance = request.context.resolve(routeDef.controller);
|
|
186
|
+
|
|
187
|
+
Object.assign(request.params, this.extractParams(request.path, routeDef.path));
|
|
188
|
+
|
|
189
|
+
if(routeDef.guards.length > 0) {
|
|
190
|
+
for(const guardType of routeDef.guards) {
|
|
191
|
+
const guard = request.context.resolve(guardType);
|
|
192
|
+
const allowed = await guard.canActivate(request);
|
|
193
|
+
|
|
194
|
+
if(!allowed)
|
|
195
|
+
throw new UnauthorizedException(`Unauthorized for ${request.method} ${request.path}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return controllerInstance;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private verifyRequestBody(request: Request, action: ControllerAction): void {
|
|
203
|
+
const requiredParams = Reflect.getMetadata('design:paramtypes', action) || [];
|
|
204
|
+
// peut être à faire plus tard. problème du TS, c'est qu'en JS pas de typage.
|
|
205
|
+
// donc il faudrait passer par des décorateurs mais pas sûr que ce soit bien.
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private extractParams(actual: string, template: string): Record<string, string> {
|
|
209
|
+
const aParts = actual.split('/');
|
|
210
|
+
const tParts = template.split('/');
|
|
211
|
+
const params: Record<string, string> = {};
|
|
212
|
+
|
|
213
|
+
tParts.forEach((part, i) => {
|
|
214
|
+
if(part.startsWith(':')) {
|
|
215
|
+
params[part.slice(1)] = aParts[i] ?? '';
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return params;
|
|
220
|
+
}
|
|
221
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compileOnSave": false,
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"baseUrl": "./",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"target": "ES6",
|
|
8
|
+
"module": "CommonJS",
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitOverride": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"experimentalDecorators": true,
|
|
16
|
+
"emitDecoratorMetadata": true,
|
|
17
|
+
"importHelpers": false,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"useDefineForClassFields": false,
|
|
20
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"skipLibCheck": true,
|
|
23
|
+
"types": ["reflect-metadata"],
|
|
24
|
+
},
|
|
25
|
+
"tsc-alias": {
|
|
26
|
+
"resolveFullPaths": true,
|
|
27
|
+
"verbose": false,
|
|
28
|
+
},
|
|
29
|
+
"include": [
|
|
30
|
+
"src/**/*.ts",
|
|
31
|
+
"src/**/*.d.ts",
|
|
32
|
+
"node_modules/electron/electron.d.ts",
|
|
33
|
+
],
|
|
34
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
noxus: "src/index.ts"
|
|
6
|
+
},
|
|
7
|
+
keepNames: true,
|
|
8
|
+
name: "noxus",
|
|
9
|
+
format: ["cjs", "esm"],
|
|
10
|
+
dts: true,
|
|
11
|
+
sourcemap: true,
|
|
12
|
+
clean: true,
|
|
13
|
+
outDir: "dist",
|
|
14
|
+
external: ["electron"],
|
|
15
|
+
target: "es2020",
|
|
16
|
+
minify: false,
|
|
17
|
+
splitting: false,
|
|
18
|
+
shims: false,
|
|
19
|
+
treeshake: false,
|
|
20
|
+
});
|