-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdecorators.ts
239 lines (222 loc) · 9.08 KB
/
decorators.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import { container } from './container';
import { WapidiError, DecoratorError, MiddlewareError } from './errors';
import { InjectionToken } from './InjectionToken';
import { getRouteFromContext, getRoutesFromContext, httpMethodDecoratorFactory } from './helpers';
import { moduleSymbol, optionsSymbol, prefixSymbol } from './symbols';
import { isModuleOptions } from './types';
import type {
BaseRoute,
ExtendedControllerDecoratorMetadata,
ExtendedModuleDecoratorMetadata,
InjectionTokenType,
Instantiable,
MiddlewareType,
ModuleOptions,
} from './types';
import { Middleware, MiddlewareFactory } from './Middleware';
// @ts-expect-error polyfill for Symbol.metadata
Symbol.metadata ??= Symbol('Symbol.metadata');
const UNINITIALIZED = Symbol('UNINITIALIZED');
export function Module(prefixOrOptions?: string | ModuleOptions, options?: ModuleOptions) {
return function (constructor: Instantiable, context: ClassDecoratorContext) {
try {
if (context.kind !== 'class') {
throw new DecoratorError('The @Module decorator factory can only be used on the class');
}
let prefix = '';
let moduleOptions: ModuleOptions;
if (prefixOrOptions != null) {
if (isModuleOptions(prefixOrOptions)) {
moduleOptions = prefixOrOptions;
} else {
prefix = prefixOrOptions;
moduleOptions = options;
}
}
(context.metadata as ExtendedModuleDecoratorMetadata)[moduleSymbol] = true;
(context.metadata as ExtendedModuleDecoratorMetadata)[prefixSymbol] = prefix;
(context.metadata as ExtendedModuleDecoratorMetadata)[optionsSymbol] = moduleOptions;
} catch (error) {
throw new WapidiError(error);
}
};
}
export function Controller(prefix: string = '') {
return function (constructor: Instantiable, context: ClassDecoratorContext): void {
try {
if (context.kind !== 'class') {
throw new DecoratorError('The @Controller decorator factory can only be used on the class');
}
(context.metadata as ExtendedControllerDecoratorMetadata)[prefixSymbol] = prefix;
container.register({
provide: context.name,
useSingleton: constructor,
});
} catch (error) {
throw new WapidiError(error);
}
};
}
export function Injectable(as?: InjectionToken) {
return function (constructor: Instantiable, context: ClassDecoratorContext) {
try {
if (context.kind !== 'class') {
throw new DecoratorError('The @Injectable decorator factory can only be used on the class');
}
container.register({
provide: as ?? constructor,
useClass: constructor,
});
} catch (error) {
throw new WapidiError(error);
}
};
}
export function Singleton(as?: InjectionToken) {
return function (constructor: Instantiable, context: ClassDecoratorContext) {
try {
if (context.kind !== 'class') {
throw new DecoratorError('The @Singleton decorator factory can only be used on the class');
}
container.register({
provide: as ?? constructor,
useSingleton: constructor,
});
} catch (error) {
throw new WapidiError(error);
}
};
}
export function Inject<T>(token: Instantiable | InjectionTokenType): any {
return function ({ get, set }, context: ClassAccessorDecoratorContext<unknown, T>) {
try {
if (context.kind !== 'accessor') {
throw new DecoratorError(
'The Inject() decorator factory must be used as a class auto-accessor decorator'
);
}
return {
init(initialValue) {
if (initialValue != null) {
console.warn(
`Accessor ${this.constructor.name}.${String(
context.name
)} was initialized with a value, however, this value will be overriden by the injected value.`
);
}
return UNINITIALIZED;
},
get(): T {
const currentValue = get.call(this);
if (currentValue === UNINITIALIZED) {
const value = container.get<T>(token);
set.call(this, value);
return value;
}
return currentValue;
},
set(newValue: T) {
try {
const oldValue = get.call(this);
if (oldValue !== UNINITIALIZED) {
throw new DecoratorError(
`Accessor ${this.constructor.name}.${String(context.name)} can only be set once`
);
}
set.call(this, newValue);
} catch (error) {
throw new WapidiError(error);
}
},
};
} catch (error) {
throw new WapidiError(error);
}
};
}
export function Get(path: string = '') {
return httpMethodDecoratorFactory(path, 'get');
}
export function Post(path: string = '') {
return httpMethodDecoratorFactory(path, 'post');
}
export function Delete(path: string = '') {
return httpMethodDecoratorFactory(path, 'delete');
}
export function Put(path: string = '') {
return httpMethodDecoratorFactory(path, 'put');
}
export function Patch(path: string = '') {
return httpMethodDecoratorFactory(path, 'patch');
}
export function Middlewares(middlewareFunctions: MiddlewareType | MiddlewareType[], ...rest: MiddlewareType[]) {
return function (originalMethodOrConstructor: any, context: ClassDecoratorContext | ClassMethodDecoratorContext) {
try {
let middlewares: MiddlewareType[] = [];
if (Array.isArray(middlewareFunctions)) {
middlewares = middlewareFunctions;
} else if (
typeof middlewareFunctions === 'function' ||
middlewareFunctions instanceof Middleware ||
middlewareFunctions instanceof MiddlewareFactory
) {
if (
rest.some(
middleware =>
typeof middleware !== 'function' &&
!(middleware instanceof Middleware) &&
!(middleware instanceof MiddlewareFactory)
)
) {
throw new MiddlewareError('Invalid middleware provided');
}
middlewares = [middlewareFunctions, ...rest];
} else {
throw new MiddlewareError('Invalid middleware provided.');
}
if (context.kind === 'class') {
const routes = getRoutesFromContext(context);
for (const route of routes) {
if (!route.middlewares) route.middlewares = [];
for (let index = middlewares.length - 1; index >= 0; index--) {
const middleware = middlewares[index];
if (middleware instanceof Middleware || middleware instanceof MiddlewareFactory) {
if (!middleware.ignoredRoutes.includes(route.actionName)) {
route.middlewares.unshift(middleware);
}
} else {
route.middlewares.unshift(middleware);
}
}
}
} else if (context.kind === 'method') {
const route = getRouteFromContext(context);
if (!route.middlewares) route.middlewares = [];
route.middlewares.push(...middlewares);
return originalMethodOrConstructor;
} else {
throw new DecoratorError(
'The @Middlewares() decorator factory can be only applied on the class or on class methods'
);
}
} catch (error) {
throw new WapidiError(error);
}
};
}
// =========================
// Util
// =========================
export function createRouteDecorator<TRoute extends BaseRoute = BaseRoute>(cb: (route: TRoute) => any) {
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
try {
if (context.kind !== 'method') {
throw new DecoratorError('A route decorator | decorator factory can only be used on a class method');
}
cb(getRouteFromContext<TRoute>(context));
return originalMethod;
} catch (error) {
throw new WapidiError(error);
}
};
}