-
Notifications
You must be signed in to change notification settings - Fork 1
/
lib.ts
177 lines (146 loc) · 5.63 KB
/
lib.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
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
import 'reflect-metadata';
/*
* keys, constants, types
*/
const HTTP_METHOD_KEY = Symbol('method');
const PATH_KEY = Symbol('path');
const PARAMS_META_KEY = Symbol('paramsMeta');
const PROVIDERS_KEY = Symbol('providers');
const CONTROLLERS_KEY = Symbol('controllers');
const CONTROLLER_PREFIX_KEY = Symbol('controllerPrefix');
const DESIGN_PARAM_TYPES = 'design:paramtypes';
enum HandlerParamType {
ROUTE_PARAM = 'ROUTE_PARAM',
BODY = 'BODY',
}
enum HttpMethod {
GET = 'get',
POST = 'post',
}
type ClassType = new (...args: unknown[]) => unknown;
/*
* decorators to be used in program code
*/
export function Controller(prefix?: string) {
return function (target: ClassType) {
Reflect.defineMetadata(CONTROLLER_PREFIX_KEY, prefix ?? '', target);
};
}
function getRouteDecorator(httpMethod: HttpMethod, path: string) {
return function (target: any, key: string) {
Reflect.defineMetadata(HTTP_METHOD_KEY, httpMethod, target, key);
Reflect.defineMetadata(PATH_KEY, path, target, key);
};
}
export function Get(path: string) {
return getRouteDecorator(HttpMethod.GET, path);
}
export function Post(path: string) {
return getRouteDecorator(HttpMethod.POST, path);
}
function getHandlerParamDecorator(type: HandlerParamType, key: string) {
return function (target: any, methodName: string, index: number) {
const paramsMeta = Reflect.getMetadata(PARAMS_META_KEY, target, methodName) ?? {};
paramsMeta[index] = { key, type };
Reflect.defineMetadata(PARAMS_META_KEY, paramsMeta, target, methodName);
};
}
export function Param(key?: string) {
return getHandlerParamDecorator(HandlerParamType.ROUTE_PARAM, key);
}
export function Body(key?: string) {
return getHandlerParamDecorator(HandlerParamType.BODY, key);
}
export function Module({
providers,
controllers,
}: {
providers: ClassType[];
controllers: ClassType[];
}) {
return function (target: ClassType) {
Reflect.defineMetadata(PROVIDERS_KEY, providers, target);
Reflect.defineMetadata(CONTROLLERS_KEY, controllers, target);
};
}
export function Injectable() {
return function (_: ClassType) {};
}
/*
* framework code that uses the metadata injected by the decorators
* to create an express app
*/
export function createApp(module: ClassType) {
const app = express();
app.use(bodyParser.json());
// cache to store the instances of providers
const providerInstances = new Map();
function instantiateProvider(Cls: ClassType) {
if (providerInstances.has(Cls)) return providerInstances.get(Cls);
// get all the dependencies of the provider, and instantiate those first
// not handling circular dependencies
const deps = Reflect.getMetadata(DESIGN_PARAM_TYPES, Cls) ?? [];
const params = deps.map(instantiateProvider);
const instance = new Cls(...params);
// cache it to be used when it is required next time
providerInstances.set(Cls, instance);
return instance;
}
// Let's instantiate all the providers first with their dependencies
// and keep them in the cache
Reflect.getMetadata(PROVIDERS_KEY, module).forEach(instantiateProvider);
// process the controllers now
Reflect.getMetadata(CONTROLLERS_KEY, module).forEach((ControllerCls: ClassType) => {
// instantiate the controller with all their dependencies
const params = Reflect.getMetadata(DESIGN_PARAM_TYPES, ControllerCls).map(
(ProviderCls: ClassType) => {
if (!providerInstances.has(ProviderCls))
throw new Error(
`You forgot to add ${ProviderCls.name} to the providers array of the module`
);
return providerInstances.get(ProviderCls);
}
);
const controller = new ControllerCls(...params);
let prefix = Reflect.getMetadata(CONTROLLER_PREFIX_KEY, ControllerCls);
if (prefix && !prefix.startsWith('/')) prefix = `/${prefix}`;
// process each of the route handlers
Reflect.ownKeys(ControllerCls.prototype)
.filter((property: string) => {
return Reflect.hasOwnMetadata(HTTP_METHOD_KEY, ControllerCls.prototype, property);
})
.forEach((method: string) => {
// metadata of each parameter is stored in a object against the index of where it appears
// and the whole object is stored in the method's metadata against the PARAMS_META_KEY key
// let's get the whole object and keep them to be used when when we go through each parameter
const paramsMeta =
Reflect.getMetadata(PARAMS_META_KEY, ControllerCls.prototype, method) ?? {};
const httpMethod = Reflect.getMetadata(HTTP_METHOD_KEY, controller, method);
const path = Reflect.getMetadata(PATH_KEY, controller, method);
const fullPath = `${prefix}${path}`;
app[httpMethod](fullPath, async (req: Request, res: Response) => {
const params = Reflect.getMetadata(
// get all the params first
DESIGN_PARAM_TYPES,
ControllerCls.prototype,
method
).map((_: any, index: number) => {
// and then map them to the actual data to be passed
const paramMeta = paramsMeta[index];
if (!paramMeta) return undefined;
const dataToPass = {
[HandlerParamType.BODY]: req.body,
[HandlerParamType.ROUTE_PARAM]: req.params,
}[paramMeta.type];
console.log({ httpMethod, dataToPass, paramMeta, body: req.body });
if (paramMeta.key) return dataToPass[paramMeta.key];
return dataToPass;
});
res.send(await controller[method](...params));
});
});
});
return app;
}