@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/guards.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { Logger } from 'src/logger';
2
+ import { Type } from 'src/metadata';
3
+ import { MaybeAsync } from 'src/misc';
4
+ import { Request } from 'src/request';
5
+
6
+ export interface IGuard {
7
+ canActivate(request: Request): MaybeAsync<boolean>;
8
+ }
9
+
10
+ const authorizations = new Map<string, Type<IGuard>[]>();
11
+
12
+ /**
13
+ * Peut être utilisé pour protéger les routes d'un contrôleur.
14
+ * Peut être utilisé sur une classe controleur, ou sur une méthode de contrôleur.
15
+ */
16
+ export function Authorize(...guardClasses: Type<IGuard>[]): MethodDecorator & ClassDecorator {
17
+ return (target: any, propertyKey?: string | symbol) => {
18
+ let key: string;
19
+
20
+ // Method decorator
21
+ if(propertyKey) {
22
+ const ctrlName = target.constructor.name;
23
+ const actionName = propertyKey as string;
24
+ key = `${ctrlName}.${actionName}`;
25
+ }
26
+ // Class decorator
27
+ else {
28
+ const ctrlName = (target as Type<unknown>).name;
29
+ key = `${ctrlName}`;
30
+ }
31
+
32
+ if(authorizations.has(key)) {
33
+ throw new Error(`Guard(s) already registered for ${key}`);
34
+ }
35
+
36
+ Logger.debug(`Registering guards for ${key}: ${guardClasses.map(c => c.name).join(', ')}`);
37
+
38
+ authorizations.set(key, guardClasses);
39
+ };
40
+ }
41
+
42
+
43
+ export function getGuardForController(controllerName: string): Type<IGuard>[] {
44
+ const key = `${controllerName}`;
45
+ return authorizations.get(key) ?? [];
46
+ }
47
+
48
+ export function getGuardForControllerAction(controllerName: string, actionName: string): Type<IGuard>[] {
49
+ const key = `${controllerName}.${actionName}`;
50
+ return authorizations.get(key) ?? [];
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from './app-injector';
2
+ export * from './router';
3
+ export * from './app';
4
+ export * from './bootstrap';
5
+ export * from './exceptions';
6
+ export * from './guards';
7
+ export * from './logger';
8
+ export * from './metadata';
9
+ export * from './misc';
10
+ export * from './request';
@@ -0,0 +1,53 @@
1
+ import { Lifetime, RootInjector } from "src/app-injector";
2
+ import { Logger } from "src/logger";
3
+ import { Type, getModuleMetadata, getControllerMetadata, getRouteMetadata, getInjectableMetadata } from "src/metadata";
4
+ import { Router } from "src/router";
5
+
6
+ export class InjectorExplorer {
7
+ /**
8
+ * Enregistre la classe comme étant injectable.
9
+ * Lorsqu'une classe sera instanciée, si elle a des dépendances, et que celles-ci
10
+ * figurent dans la liste grâce à cette méthode, elles seront injectées dans le
11
+ * constructeur de la classe.
12
+ */
13
+ public static register(target: Type<unknown>, lifetime: Lifetime): typeof RootInjector {
14
+ Logger.debug(`Registering ${target.name} as ${lifetime}`);
15
+ if(RootInjector.bindings.has(target)) // already registered
16
+ return RootInjector;
17
+
18
+ RootInjector.bindings.set(target, {
19
+ implementation: target,
20
+ lifetime
21
+ });
22
+
23
+ if(lifetime === 'singleton') {
24
+ RootInjector.resolve(target);
25
+ }
26
+
27
+ if(getModuleMetadata(target)) {
28
+ Logger.log(`${target.name} dependencies initialized`);
29
+ return RootInjector;
30
+ }
31
+
32
+ const controllerMeta = getControllerMetadata(target);
33
+
34
+ if(controllerMeta) {
35
+ const router = RootInjector.resolve(Router);
36
+ router?.registerController(target);
37
+ return RootInjector;
38
+ }
39
+
40
+ const routeMeta = getRouteMetadata(target);
41
+
42
+ if(routeMeta) {
43
+ return RootInjector;
44
+ }
45
+
46
+ if(getInjectableMetadata(target)) {
47
+ Logger.log(`Registered ${target.name} as ${lifetime}`);
48
+ return RootInjector;
49
+ }
50
+
51
+ return RootInjector;
52
+ }
53
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,136 @@
1
+ function getPrettyTimestamp(): string {
2
+ const now = new Date();
3
+ return `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`
4
+ + ` ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
5
+ }
6
+
7
+ function getLogPrefix(callee: string, messageType: string, color: string): string {
8
+ const timestamp = getPrettyTimestamp();
9
+
10
+ const spaces = ' '.repeat(10 - messageType.length);
11
+
12
+ return `${color}[APP] ${process.pid} - ${Logger.colors.initial}`
13
+ + `${timestamp}${spaces}`
14
+ + `${color}${messageType.toUpperCase()}${Logger.colors.initial} `
15
+ + `${Logger.colors.yellow}[${callee}]${Logger.colors.initial}`;
16
+ }
17
+
18
+ function formatObject(prefix: string, arg: object): string {
19
+ const json = JSON.stringify(arg, null, 2);
20
+
21
+ const prefixedJson = json
22
+ .split('\n')
23
+ .map((line, idx) => idx === 0 ? `${Logger.colors.darkGrey}${line}` : `${prefix} ${Logger.colors.grey}${line}`)
24
+ .join('\n') + Logger.colors.initial;
25
+
26
+ return prefixedJson;
27
+ }
28
+
29
+ function formattedArgs(prefix: string, args: any[], color: string): any[] {
30
+ return args.map(arg => {
31
+ if(typeof arg === 'string') {
32
+ return `${color}${arg}${Logger.colors.initial}`;
33
+ }
34
+
35
+ else if(typeof arg === 'object') {
36
+ return formatObject(prefix, arg);
37
+ }
38
+
39
+ return arg;
40
+ });
41
+ }
42
+
43
+ function getCallee(): string {
44
+ const stack = new Error().stack?.split('\n') ?? [];
45
+ const caller = stack[3]?.trim().match(/at (.+?)(?:\..+)? .+$/)?.[1]?.replace('Object', '') || "App";
46
+ return caller;
47
+ }
48
+
49
+ export type LogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug';
50
+
51
+ const logLevelRank: Record<LogLevel, number> = {
52
+ debug: 0,
53
+ log: 1,
54
+ info: 2,
55
+ warn: 3,
56
+ error: 4,
57
+ };
58
+
59
+ function canLog(level: LogLevel): boolean {
60
+ return logLevelRank[level] >= logLevelRank[logLevel];
61
+ }
62
+
63
+ let logLevel: LogLevel = 'log';
64
+
65
+ export namespace Logger {
66
+
67
+ export function setLogLevel(level: LogLevel): void {
68
+ logLevel = level;
69
+ }
70
+
71
+ export const colors = {
72
+ black: '\x1b[0;30m',
73
+ grey: '\x1b[0;37m',
74
+ red: '\x1b[0;31m',
75
+ green: '\x1b[0;32m',
76
+ brown: '\x1b[0;33m',
77
+ blue: '\x1b[0;34m',
78
+ purple: '\x1b[0;35m',
79
+
80
+ darkGrey: '\x1b[1;30m',
81
+ lightRed: '\x1b[1;31m',
82
+ lightGreen: '\x1b[1;32m',
83
+ yellow: '\x1b[1;33m',
84
+ lightBlue: '\x1b[1;34m',
85
+ magenta: '\x1b[1;35m',
86
+ cyan: '\x1b[1;36m',
87
+ white: '\x1b[1;37m',
88
+
89
+ initial: '\x1b[0m'
90
+ };
91
+
92
+ export function log(...args: any[]): void {
93
+ if(!canLog('log'))
94
+ return;
95
+
96
+ const callee = getCallee();
97
+ const prefix = getLogPrefix(callee, "log", colors.green);
98
+ console.log(prefix, ...formattedArgs(prefix, args, colors.green));
99
+ }
100
+
101
+ export function info(...args: any[]): void {
102
+ if(!canLog('info'))
103
+ return;
104
+
105
+ const callee = getCallee();
106
+ const prefix = getLogPrefix(callee, "info", colors.blue);
107
+ console.info(prefix, ...formattedArgs(prefix, args, colors.blue));
108
+ }
109
+
110
+ export function warn(...args: any[]): void {
111
+ if(!canLog('warn'))
112
+ return;
113
+
114
+ const callee = getCallee();
115
+ const prefix = getLogPrefix(callee, "warn", colors.brown);
116
+ console.warn(prefix, ...formattedArgs(prefix, args, colors.brown));
117
+ }
118
+
119
+ export function error(...args: any[]): void {
120
+ if(!canLog('error'))
121
+ return;
122
+
123
+ const callee = getCallee();
124
+ const prefix = getLogPrefix(callee, "error", colors.red);
125
+ console.error(prefix, ...formattedArgs(prefix, args, colors.red));
126
+ }
127
+
128
+ export function debug(...args: any[]): void {
129
+ if(!canLog('debug'))
130
+ return;
131
+
132
+ const callee = getCallee();
133
+ const prefix = getLogPrefix(callee, "debug", colors.purple);
134
+ console.debug(prefix, ...formattedArgs(prefix, args, colors.purple));
135
+ }
136
+ }
@@ -0,0 +1,52 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-function-type */
2
+
3
+ import { Lifetime } from "src/app-injector";
4
+ import { IGuard } from "src/guards";
5
+ import { HttpMethod } from "src/router";
6
+
7
+ declare const Type: FunctionConstructor;
8
+ export interface Type<T> extends Function {
9
+ // eslint-disable-next-line @typescript-eslint/prefer-function-type
10
+ new (...args: any[]): T;
11
+ }
12
+
13
+ export const MODULE_METADATA_KEY = Symbol('MODULE_METADATA_KEY');
14
+ export const INJECTABLE_METADATA_KEY = Symbol('INJECTABLE_METADATA_KEY');
15
+ export const CONTROLLER_METADATA_KEY = Symbol('CONTROLLER_METADATA_KEY');
16
+ export const ROUTE_METADATA_KEY = Symbol('ROUTE_METADATA_KEY');
17
+
18
+ export interface IModuleMetadata {
19
+ imports?: Type<unknown>[];
20
+ providers?: Type<unknown>[];
21
+ controllers?: Type<unknown>[];
22
+ exports?: Type<unknown>[];
23
+ }
24
+
25
+ export interface IRouteMetadata {
26
+ method: HttpMethod;
27
+ path: string;
28
+ handler: string;
29
+ guards: Type<IGuard>[];
30
+ }
31
+
32
+ export interface IControllerMetadata {
33
+ path: string;
34
+ guards: Type<IGuard>[];
35
+ }
36
+
37
+
38
+ export function getControllerMetadata(target: Type<unknown>): IControllerMetadata | undefined {
39
+ return Reflect.getMetadata(CONTROLLER_METADATA_KEY, target);
40
+ }
41
+
42
+ export function getRouteMetadata(target: Type<unknown>): IRouteMetadata[] {
43
+ return Reflect.getMetadata(ROUTE_METADATA_KEY, target) || [];
44
+ }
45
+
46
+ export function getModuleMetadata(target: Function): IModuleMetadata | undefined {
47
+ return Reflect.getMetadata(MODULE_METADATA_KEY, target);
48
+ }
49
+
50
+ export function getInjectableMetadata(target: Type<unknown>): Lifetime | undefined {
51
+ return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
52
+ }
package/src/misc.ts ADDED
@@ -0,0 +1 @@
1
+ export type MaybeAsync<T> = T | Promise<T>;
@@ -0,0 +1,137 @@
1
+ type Params = Record<string, string>;
2
+
3
+ interface ISearchResult<T> {
4
+ node: RadixNode<T>;
5
+ params: Params;
6
+ }
7
+
8
+ class RadixNode<T> {
9
+ public segment: string;
10
+ public children: RadixNode<T>[] = [];
11
+ public value?: T;
12
+ public isParam: boolean;
13
+ public paramName?: string;
14
+
15
+ constructor(segment: string) {
16
+ this.segment = segment;
17
+ this.isParam = segment.startsWith(":");
18
+
19
+ if(this.isParam) {
20
+ this.paramName = segment.slice(1);
21
+ }
22
+ }
23
+
24
+ public matchChild(segment: string): RadixNode<T> | undefined {
25
+ for(const child of this.children) {
26
+ if(child.isParam || segment.startsWith(child.segment))
27
+ return child; // param match
28
+ }
29
+
30
+ return undefined;
31
+ }
32
+
33
+ public findExactChild(segment: string): RadixNode<T> | undefined {
34
+ return this.children.find(c => c.segment === segment);
35
+ }
36
+
37
+ public addChild(node: RadixNode<T>): void {
38
+ this.children.push(node);
39
+ }
40
+ }
41
+
42
+ export class RadixTree<T> {
43
+ private readonly root = new RadixNode<T>("");
44
+
45
+ public insert(path: string, value: T): void {
46
+ const segments = this.normalize(path);
47
+ this.insertRecursive(this.root, segments, value);
48
+ }
49
+
50
+ private insertRecursive(node: RadixNode<T>, segments: string[], value: T): void {
51
+ if(segments.length === 0) {
52
+ node.value = value;
53
+ return;
54
+ }
55
+
56
+ const segment = segments[0] ?? "";
57
+
58
+ let child = node.children.find(c =>
59
+ c.isParam === segment.startsWith(":") &&
60
+ (c.isParam || c.segment === segment)
61
+ );
62
+
63
+ if(!child) {
64
+ child = new RadixNode<T>(segment);
65
+ node.addChild(child);
66
+ }
67
+
68
+ this.insertRecursive(child, segments.slice(1), value);
69
+ }
70
+
71
+ public search(path: string): ISearchResult<T> | undefined {
72
+ const segments = this.normalize(path);
73
+ return this.searchRecursive(this.root, segments, {});
74
+ }
75
+
76
+ private searchRecursive(node: RadixNode<T>, segments: string[], params: Params): ISearchResult<T> | undefined {
77
+ if(segments.length === 0) {
78
+ if(node.value !== undefined) {
79
+ return {
80
+ node: node,
81
+ params
82
+ };
83
+ }
84
+
85
+ return undefined;
86
+ }
87
+
88
+ const [segment, ...rest] = segments;
89
+
90
+ for(const child of node.children) {
91
+ if(child.isParam) {
92
+ const paramName = child.paramName!;
93
+
94
+ const childParams: Params = {
95
+ ...params,
96
+ [paramName]: segment ?? "",
97
+ };
98
+
99
+ if(rest.length === 0) {
100
+ return {
101
+ node: child,
102
+ params: childParams
103
+ };
104
+ }
105
+
106
+ const result = this.searchRecursive(child, rest, childParams);
107
+
108
+ if(result)
109
+ return result;
110
+ }
111
+ else if(segment === child.segment) {
112
+ if(rest.length === 0) {
113
+ return {
114
+ node: child,
115
+ params
116
+ };
117
+ }
118
+
119
+ const result = this.searchRecursive(child, rest, params);
120
+
121
+ if(result)
122
+ return result;
123
+ }
124
+ }
125
+
126
+ return undefined;
127
+ }
128
+
129
+ private normalize(path: string): string[] {
130
+ const segments = path
131
+ .replace(/^\/+|\/+$/g, "")
132
+ .split("/")
133
+ .filter(Boolean);
134
+
135
+ return ['', ...segments];
136
+ }
137
+ }
package/src/request.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { IApp } from 'src/app';
2
+ import { RootInjector } from 'src/app-injector';
3
+ import { HttpMethod } from 'src/router';
4
+ import 'reflect-metadata';
5
+
6
+
7
+ //
8
+
9
+ export class Request {
10
+ public readonly context: any = RootInjector.createScope();
11
+
12
+ public readonly params: Record<string, string> = {};
13
+
14
+ constructor(
15
+ public readonly app: IApp,
16
+ public readonly event: Electron.MessageEvent,
17
+ public readonly id: string,
18
+ public readonly method: HttpMethod,
19
+ public readonly path: string,
20
+ public readonly body: any,
21
+ ) {
22
+ this.path = path.replace(/^\/|\/$/g, '');
23
+ }
24
+ }
25
+
26
+ export interface IRequest<T = any> {
27
+ requestId: string;
28
+ path: string;
29
+ method: HttpMethod;
30
+ body?: T;
31
+ }
32
+
33
+ export interface IResponse<T = any> {
34
+ requestId: string;
35
+ status: number;
36
+ body?: T;
37
+ error?: string;
38
+ }