@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/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
+ });