@deepkit/http 1.0.1-alpha.100

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.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/dist/.gitkeep +0 -0
  4. package/dist/cjs/index.d.ts +14 -0
  5. package/dist/cjs/index.js +31 -0
  6. package/dist/cjs/index.js.map +1 -0
  7. package/dist/cjs/src/controllers.d.ts +13 -0
  8. package/dist/cjs/src/controllers.js +17 -0
  9. package/dist/cjs/src/controllers.js.map +1 -0
  10. package/dist/cjs/src/decorator.d.ts +241 -0
  11. package/dist/cjs/src/decorator.js +422 -0
  12. package/dist/cjs/src/decorator.js.map +1 -0
  13. package/dist/cjs/src/filter.d.ts +49 -0
  14. package/dist/cjs/src/filter.js +215 -0
  15. package/dist/cjs/src/filter.js.map +1 -0
  16. package/dist/cjs/src/http-app.d.ts +8 -0
  17. package/dist/cjs/src/http-app.js +16 -0
  18. package/dist/cjs/src/http-app.js.map +1 -0
  19. package/dist/cjs/src/http.d.ts +278 -0
  20. package/dist/cjs/src/http.js +878 -0
  21. package/dist/cjs/src/http.js.map +1 -0
  22. package/dist/cjs/src/kernel.d.ts +16 -0
  23. package/dist/cjs/src/kernel.js +79 -0
  24. package/dist/cjs/src/kernel.js.map +1 -0
  25. package/dist/cjs/src/logger.d.ts +7 -0
  26. package/dist/cjs/src/logger.js +34 -0
  27. package/dist/cjs/src/logger.js.map +1 -0
  28. package/dist/cjs/src/middleware.d.ts +61 -0
  29. package/dist/cjs/src/middleware.js +95 -0
  30. package/dist/cjs/src/middleware.js.map +1 -0
  31. package/dist/cjs/src/model.d.ts +165 -0
  32. package/dist/cjs/src/model.js +398 -0
  33. package/dist/cjs/src/model.js.map +1 -0
  34. package/dist/cjs/src/module.config.d.ts +84 -0
  35. package/dist/cjs/src/module.config.js +14 -0
  36. package/dist/cjs/src/module.config.js.map +1 -0
  37. package/dist/cjs/src/module.d.ts +14 -0
  38. package/dist/cjs/src/module.js +124 -0
  39. package/dist/cjs/src/module.js.map +1 -0
  40. package/dist/cjs/src/request-parser.d.ts +36 -0
  41. package/dist/cjs/src/request-parser.js +305 -0
  42. package/dist/cjs/src/request-parser.js.map +1 -0
  43. package/dist/cjs/src/router.d.ts +230 -0
  44. package/dist/cjs/src/router.js +617 -0
  45. package/dist/cjs/src/router.js.map +1 -0
  46. package/dist/cjs/src/static-serving.d.ts +32 -0
  47. package/dist/cjs/src/static-serving.js +157 -0
  48. package/dist/cjs/src/static-serving.js.map +1 -0
  49. package/dist/cjs/src/utils.d.ts +1 -0
  50. package/dist/cjs/src/utils.js +13 -0
  51. package/dist/cjs/src/utils.js.map +1 -0
  52. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  53. package/dist/esm/index.d.ts +14 -0
  54. package/dist/esm/index.js +15 -0
  55. package/dist/esm/index.js.map +1 -0
  56. package/dist/esm/package.json +1 -0
  57. package/dist/esm/src/controllers.d.ts +13 -0
  58. package/dist/esm/src/controllers.js +13 -0
  59. package/dist/esm/src/controllers.js.map +1 -0
  60. package/dist/esm/src/decorator.d.ts +241 -0
  61. package/dist/esm/src/decorator.js +414 -0
  62. package/dist/esm/src/decorator.js.map +1 -0
  63. package/dist/esm/src/filter.d.ts +49 -0
  64. package/dist/esm/src/filter.js +209 -0
  65. package/dist/esm/src/filter.js.map +1 -0
  66. package/dist/esm/src/http-app.d.ts +8 -0
  67. package/dist/esm/src/http-app.js +12 -0
  68. package/dist/esm/src/http-app.js.map +1 -0
  69. package/dist/esm/src/http.d.ts +278 -0
  70. package/dist/esm/src/http.js +841 -0
  71. package/dist/esm/src/http.js.map +1 -0
  72. package/dist/esm/src/kernel.d.ts +16 -0
  73. package/dist/esm/src/kernel.js +75 -0
  74. package/dist/esm/src/kernel.js.map +1 -0
  75. package/dist/esm/src/logger.d.ts +7 -0
  76. package/dist/esm/src/logger.js +30 -0
  77. package/dist/esm/src/logger.js.map +1 -0
  78. package/dist/esm/src/middleware.d.ts +61 -0
  79. package/dist/esm/src/middleware.js +90 -0
  80. package/dist/esm/src/middleware.js.map +1 -0
  81. package/dist/esm/src/model.d.ts +165 -0
  82. package/dist/esm/src/model.js +363 -0
  83. package/dist/esm/src/model.js.map +1 -0
  84. package/dist/esm/src/module.config.d.ts +84 -0
  85. package/dist/esm/src/module.config.js +10 -0
  86. package/dist/esm/src/module.config.js.map +1 -0
  87. package/dist/esm/src/module.d.ts +14 -0
  88. package/dist/esm/src/module.js +120 -0
  89. package/dist/esm/src/module.js.map +1 -0
  90. package/dist/esm/src/request-parser.d.ts +36 -0
  91. package/dist/esm/src/request-parser.js +295 -0
  92. package/dist/esm/src/request-parser.js.map +1 -0
  93. package/dist/esm/src/router.d.ts +230 -0
  94. package/dist/esm/src/router.js +605 -0
  95. package/dist/esm/src/router.js.map +1 -0
  96. package/dist/esm/src/static-serving.d.ts +32 -0
  97. package/dist/esm/src/static-serving.js +149 -0
  98. package/dist/esm/src/static-serving.js.map +1 -0
  99. package/dist/esm/src/utils.d.ts +1 -0
  100. package/dist/esm/src/utils.js +9 -0
  101. package/dist/esm/src/utils.js.map +1 -0
  102. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  103. package/index.ts +14 -0
  104. package/package.json +67 -0
  105. package/src/controllers.ts +11 -0
  106. package/src/decorator.ts +411 -0
  107. package/src/filter.ts +224 -0
  108. package/src/http-app.ts +11 -0
  109. package/src/http.ts +828 -0
  110. package/src/kernel.ts +79 -0
  111. package/src/logger.ts +20 -0
  112. package/src/middleware.ts +116 -0
  113. package/src/model.ts +392 -0
  114. package/src/module.config.ts +95 -0
  115. package/src/module.ts +116 -0
  116. package/src/request-parser.ts +346 -0
  117. package/src/router.ts +747 -0
  118. package/src/static-serving.ts +153 -0
  119. package/src/utils.ts +5 -0
  120. package/tsconfig.esm.json +36 -0
  121. package/tsconfig.json +53 -0
package/src/router.ts ADDED
@@ -0,0 +1,747 @@
1
+ /*
2
+ * Deepkit Framework
3
+ * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify
6
+ * it under the terms of the MIT License.
7
+ *
8
+ * You should have received a copy of the MIT License along with this program.
9
+ */
10
+ import { ClassType, CompilerContext, getClassName, isArray, isClass, urlJoin } from '@deepkit/core';
11
+ import { entity, ReflectionClass, ReflectionFunction, ReflectionKind, ReflectionParameter, SerializationOptions, serializer, Serializer, Type, ValidationError } from '@deepkit/type';
12
+ import { HttpAction, httpClass, HttpController, HttpDecorator } from './decorator.js';
13
+ import { HttpRequest, HttpRequestPositionedParameters, HttpRequestQuery, HttpRequestResolvedParameters } from './model.js';
14
+ import { InjectorContext, InjectorModule, TagRegistry } from '@deepkit/injector';
15
+ import { Logger, LoggerInterface } from '@deepkit/logger';
16
+ import { HttpControllers } from './controllers.js';
17
+ import { MiddlewareRegistry, MiddlewareRegistryEntry } from '@deepkit/app';
18
+ import { HttpMiddlewareConfig, HttpMiddlewareFn } from './middleware.js';
19
+
20
+ //@ts-ignore
21
+ import qs from 'qs';
22
+ import { HtmlResponse, JSONResponse, Response } from './http.js';
23
+ import { getRequestParserCodeForParameters, ParameterForRequestParser, parseRoutePathToRegex } from './request-parser.js';
24
+ import { HttpConfig, HttpParserOptions } from './module.config.js';
25
+
26
+ export type RouteParameterResolverForInjector = ((injector: InjectorContext) => HttpRequestPositionedParameters | Promise<HttpRequestPositionedParameters>);
27
+
28
+ interface ResolvedController {
29
+ parameters: RouteParameterResolverForInjector;
30
+ routeConfig: RouteConfig;
31
+ uploadedFiles: { [name: string]: UploadedFile };
32
+ middlewares?: (injector: InjectorContext) => { fn: HttpMiddlewareFn, timeout?: number }[];
33
+ }
34
+
35
+ export const UploadedFileSymbol = Symbol('UploadedFile');
36
+
37
+ @entity.name('@deepkit/UploadedFile')
38
+ export class UploadedFile {
39
+ /**
40
+ * Validator to ensure the file was provided by the framework, and not the user spoofing it.
41
+ */
42
+ validator!: typeof UploadedFileSymbol | null;
43
+
44
+ /**
45
+ * The size of the uploaded file in bytes.
46
+ */
47
+ size!: number;
48
+
49
+ /**
50
+ * The local path this file is being written to. Will be deleted when request is handled.
51
+ */
52
+ path!: string;
53
+
54
+ /**
55
+ * The name this file had according to the uploading client.
56
+ */
57
+ name!: string | null;
58
+
59
+ /**
60
+ * The mime type of this file, according to the uploading client.
61
+ */
62
+ type!: string | null;
63
+
64
+ /**
65
+ * A Date object (or `null`) containing the time this file was last written to.
66
+ * Mostly here for compatibility with the [W3C File API Draft](http://dev.w3.org/2006/webapi/FileAPI/).
67
+ */
68
+ lastModifiedDate!: Date | null;
69
+
70
+ // /**
71
+ // * If `options.hash` calculation was set, you can read the hex digest out of this var.
72
+ // */
73
+ // hash!: string | 'sha1' | 'md5' | 'sha256' | null;
74
+ }
75
+
76
+ serializer.typeGuards.getRegistry(1).registerClass(UploadedFile, (type, state) => {
77
+ state.setContext({ UploadedFileSymbol });
78
+ state.addSetterAndReportErrorIfInvalid('uploadSecurity', 'Not an uploaded file', `typeof ${state.accessor} === 'object' && ${state.accessor} !== null && ${state.accessor}.validator === UploadedFileSymbol`);
79
+ });
80
+
81
+ export interface RouteFunctionControllerAction {
82
+ type: 'function';
83
+ //if not set, the root module is used
84
+ module?: InjectorModule<any>;
85
+ fn: (...args: any[]) => any;
86
+ }
87
+
88
+ export interface RouteClassControllerAction {
89
+ type: 'controller';
90
+ //if not set, the root module is used
91
+ module?: InjectorModule<any>;
92
+ controller: ClassType;
93
+ methodName: string;
94
+ }
95
+
96
+ export class RouteConfig {
97
+ public baseUrl: string = '';
98
+
99
+ public responses: { statusCode: number, description: string, type?: Type }[] = [];
100
+
101
+ public description: string = '';
102
+ public groups: string[] = [];
103
+ public category: string = '';
104
+
105
+ public returnType?: Type;
106
+
107
+ public serializationOptions?: SerializationOptions;
108
+ public serializer?: Serializer;
109
+
110
+ /**
111
+ * When assigned defines where this route came from.
112
+ */
113
+ public module?: InjectorModule<any>;
114
+
115
+ resolverForToken: Map<any, ClassType> = new Map();
116
+
117
+ middlewares: { config: HttpMiddlewareConfig, module?: InjectorModule<any> }[] = [];
118
+
119
+ resolverForParameterName: Map<string, ClassType> = new Map();
120
+
121
+ /**
122
+ * An arbitrary data container the user can use to store app specific settings/values.
123
+ */
124
+ data = new Map<any, any>();
125
+
126
+ constructor(
127
+ public readonly name: string,
128
+ public readonly httpMethods: string[],
129
+ public readonly path: string,
130
+ public readonly action: RouteClassControllerAction | RouteFunctionControllerAction,
131
+ public internal: boolean = false,
132
+ ) {
133
+ }
134
+
135
+ getReflectionFunction(): ReflectionFunction {
136
+ return this.action.type === 'controller' ?
137
+ ReflectionClass.from(this.action.controller).getMethod(this.action.methodName)
138
+ : ReflectionFunction.from(this.action.fn);
139
+ }
140
+
141
+ getSchemaForResponse(statusCode: number): Type | undefined {
142
+ if (!this.responses.length) return;
143
+ for (const response of this.responses) {
144
+ if (response.statusCode === statusCode) return response.type;
145
+ }
146
+ return;
147
+ }
148
+
149
+ getFullPath(): string {
150
+ let path = this.baseUrl ? urlJoin(this.baseUrl, this.path) : this.path;
151
+ if (!path.startsWith('/')) path = '/' + path;
152
+ return path;
153
+ }
154
+ }
155
+
156
+ class ParsedRoute {
157
+ public regex?: string;
158
+
159
+ public pathParameterNames: { [name: string]: number } = {};
160
+
161
+ protected parameters: ParameterForRequestParser[] = [];
162
+
163
+ constructor(public routeConfig: RouteConfig) {
164
+ }
165
+
166
+ addParameter(property: ReflectionParameter): ParameterForRequestParser {
167
+ const parameter = new ParameterForRequestParser(property);
168
+ this.parameters.push(parameter);
169
+ return parameter;
170
+ }
171
+
172
+ getParameters(): ParameterForRequestParser[] {
173
+ return this.parameters;
174
+ }
175
+
176
+ getParameter(name: string): ParameterForRequestParser {
177
+ for (const parameter of this.parameters) {
178
+ if (parameter.getName() === name) return parameter;
179
+ }
180
+ throw new Error(`No route parameter with name ${name} defined.`);
181
+ }
182
+ }
183
+
184
+ export function parseRouteControllerAction(routeConfig: RouteConfig): ParsedRoute {
185
+ const parsedRoute = new ParsedRoute(routeConfig);
186
+
187
+ const parameters = routeConfig.getReflectionFunction().getParameters();
188
+ const parsedPath = parseRoutePathToRegex(routeConfig.getFullPath(), parameters);
189
+ parsedRoute.regex = parsedPath.regex;
190
+ parsedRoute.pathParameterNames = parsedPath.parameterNames;
191
+
192
+ for (const property of parameters) {
193
+ const parsedParameter = parsedRoute.addParameter(property);
194
+ parsedParameter.regexPosition = parsedPath.parameterNames[property.name];
195
+ }
196
+
197
+ return parsedRoute;
198
+ }
199
+
200
+ export function dotToUrlPath(dotPath: string): string {
201
+ if (-1 === dotPath.indexOf('.')) return dotPath;
202
+
203
+ return dotPath.replace(/\./g, '][').replace('][', '[') + ']';
204
+ }
205
+
206
+ export interface RouteParameterResolver {
207
+ resolve(context: RouteParameterResolverContext): any | Promise<any>;
208
+ }
209
+
210
+ export interface RouteParameterResolverContext {
211
+ token: ClassType | string | symbol | any;
212
+ route: RouteConfig;
213
+ request: HttpRequest;
214
+
215
+ /**
216
+ * The parameter name (variable name).
217
+ */
218
+ name: any;
219
+
220
+ /**
221
+ * The raw parameter value from the path, if the parameter is defined in the path (e.g. /user/:name).
222
+ * If not in the path, you have to use `parameters.<name>` instead.
223
+ */
224
+ value: any;
225
+
226
+ query: HttpRequestQuery;
227
+ parameters: HttpRequestResolvedParameters;
228
+ type: ReflectionParameter;
229
+ }
230
+
231
+ function filterMiddlewaresForRoute(middlewareRawConfigs: MiddlewareRegistryEntry[], routeConfig: RouteConfig, fullPath: string): {
232
+ config: HttpMiddlewareConfig,
233
+ module: InjectorModule<any>
234
+ }[] {
235
+ const middlewares = middlewareRawConfigs.slice(0);
236
+ middlewares.push(...routeConfig.middlewares as any[]);
237
+
238
+ const middlewareConfigs = middlewares.filter((v) => {
239
+ if (!(v.config instanceof HttpMiddlewareConfig)) return false;
240
+
241
+ if (v.config.controllers.length && routeConfig.action.type === 'controller' && !v.config.controllers.includes(routeConfig.action.controller)) {
242
+ return false;
243
+ }
244
+
245
+ if (v.config.excludeControllers.length && routeConfig.action.type === 'controller' && v.config.excludeControllers.includes(routeConfig.action.controller)) {
246
+ return false;
247
+ }
248
+
249
+ if (v.config.modules.length && (!routeConfig.module || !v.config.modules.includes(routeConfig.module))) {
250
+ if (!routeConfig.module) return false;
251
+ for (const module of v.config.modules) {
252
+ if (routeConfig.module.id !== module.id) return false;
253
+ }
254
+ }
255
+
256
+ if (v.config.selfModule && v.module.id !== routeConfig.module?.id) return false;
257
+
258
+ if (v.config.routeNames.length) {
259
+ let found: boolean = false;
260
+ for (const name of v.config.routeNames) {
261
+ if (name.includes('*')) {
262
+ const regex = new RegExp('^' + name.replace(/\*/g, '.*') + '$');
263
+ if (regex.test(routeConfig.name)) {
264
+ found = true;
265
+ break;
266
+ }
267
+ } else if (name === routeConfig.name) {
268
+ found = true;
269
+ break;
270
+ }
271
+ }
272
+ if (!found) return false;
273
+ }
274
+
275
+ if (v.config.excludeRouteNames.length) {
276
+ for (const name of v.config.excludeRouteNames) {
277
+ if (name.includes('*')) {
278
+ const regex = new RegExp('^' + name.replace(/\*/g, '.*') + '$');
279
+ if (regex.test(routeConfig.name)) return false;
280
+ } else if (name == routeConfig.name) {
281
+ return false;
282
+ }
283
+ }
284
+ }
285
+
286
+ for (const route of v.config.routes) {
287
+ if (route.httpMethod && !routeConfig.httpMethods.includes(route.httpMethod)) return false;
288
+
289
+ if (route.category && route.category !== routeConfig.category) return false;
290
+ if (route.excludeCategory && route.excludeCategory === routeConfig.category) return false;
291
+
292
+ if (route.group && !routeConfig.groups.includes(route.group)) return false;
293
+ if (route.excludeGroup && routeConfig.groups.includes(route.excludeGroup)) return false;
294
+
295
+ if (route.path || route.pathRegExp) {
296
+ if (!route.pathRegExp && route.path) route.pathRegExp = new RegExp('^' + route.path.replace(/\*/g, '.*') + '$');
297
+ if (route.pathRegExp && !route.pathRegExp.test(fullPath)) return false;
298
+ }
299
+ }
300
+
301
+ return true;
302
+ }) as { config: HttpMiddlewareConfig, module: any }[];
303
+
304
+ middlewareConfigs.sort((a, b) => {
305
+ return a.config.order - b.config.order;
306
+ });
307
+
308
+ return middlewareConfigs;
309
+ }
310
+
311
+ export interface HttpRouterFunctionOptions {
312
+ path: string;
313
+ name?: string;
314
+ methods?: string[];
315
+ description?: string;
316
+ category?: string;
317
+ groups?: string[];
318
+
319
+ /**
320
+ * An arbitrary data container the user can use to store app specific settings/values.
321
+ */
322
+ data?: Record<any, any>;
323
+
324
+ baseUrl?: string;
325
+ middlewares?: (() => HttpMiddlewareConfig)[];
326
+
327
+ serializer?: Serializer;
328
+ serializationOptions?: SerializationOptions;
329
+
330
+ resolverForToken?: Map<any, ClassType>;
331
+ resolverForParameterName?: Map<string, ClassType>;
332
+
333
+ responses?: { statusCode: number, description: string, type?: Type }[];
334
+ }
335
+
336
+ function convertOptions(methods: string[], pathOrOptions: string | HttpRouterFunctionOptions, defaultOptions: Partial<HttpRouterFunctionOptions>): HttpRouterFunctionOptions {
337
+ const options = 'string' === typeof pathOrOptions ? { path: pathOrOptions } : pathOrOptions;
338
+ if (options.methods) return options;
339
+ return { ...options, methods };
340
+ }
341
+
342
+ /**
343
+ * Annotated types like HTMLResponse/JSONResponse are not used for serialization.
344
+ */
345
+ function filterValidReturnType(type: Type): Type | undefined {
346
+ if (type.kind === ReflectionKind.class && (type.classType === HtmlResponse || type.classType === JSONResponse || type.classType === Response)) return;
347
+ return type;
348
+ }
349
+
350
+ export abstract class HttpRouterRegistryFunctionRegistrar {
351
+ protected defaultOptions: Partial<HttpRouterFunctionOptions> = {};
352
+
353
+ abstract addRoute(routeConfig: RouteConfig): void;
354
+
355
+ /**
356
+ * Returns a new registrar object with default options that apply to each registered route through this registrar.
357
+ *
358
+ * ```typescript
359
+ * const registry: HttpRouterRegistry = ...;
360
+ *
361
+ * const secretRegistry = registry.forOptions({groups: ['secret']});
362
+ *
363
+ * secretRegistry.get('/admin/groups', () => {
364
+ * });
365
+ *
366
+ * secretRegistry.get('/admin/users', () => {
367
+ * });
368
+ * ```
369
+ */
370
+ forOptions(options: Partial<HttpRouterFunctionOptions>): HttpRouterRegistryFunctionRegistrar {
371
+ const that = this;
372
+ return new class extends HttpRouterRegistryFunctionRegistrar {
373
+ defaultOptions = options;
374
+
375
+ addRoute(routeConfig: RouteConfig) {
376
+ that.addRoute(routeConfig);
377
+ }
378
+ };
379
+ }
380
+
381
+ public any(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
382
+ this.register(convertOptions([], pathOrOptions, this.defaultOptions), callback);
383
+ }
384
+
385
+ public add(decorator: HttpDecorator, callback: (...args: any[]) => any) {
386
+ const data = decorator(Object, '_');
387
+ const action = isArray(data) ? data.find(v => v instanceof HttpAction) : undefined;
388
+ if (!action) throw new Error('No HttpAction available');
389
+
390
+ const fn = ReflectionFunction.from(callback);
391
+ const routeConfig = createRouteConfigFromHttpAction({
392
+ type: 'function',
393
+ fn: callback,
394
+ }, action);
395
+ routeConfig.returnType = filterValidReturnType(fn.getReturnType());
396
+ this.addRoute(routeConfig);
397
+ }
398
+
399
+ public get(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
400
+ this.register(convertOptions(['GET'], pathOrOptions, this.defaultOptions), callback);
401
+ }
402
+
403
+ public post(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
404
+ this.register(convertOptions(['POST'], pathOrOptions, this.defaultOptions), callback);
405
+ }
406
+
407
+ public put(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
408
+ this.register(convertOptions(['PUT'], pathOrOptions, this.defaultOptions), callback);
409
+ }
410
+
411
+ public patch(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
412
+ this.register(convertOptions(['PATCH'], pathOrOptions, this.defaultOptions), callback);
413
+ }
414
+
415
+ public delete(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
416
+ this.register(convertOptions(['DELETE'], pathOrOptions, this.defaultOptions), callback);
417
+ }
418
+
419
+ public options(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
420
+ this.register(convertOptions(['OPTIONS'], pathOrOptions, this.defaultOptions), callback);
421
+ }
422
+
423
+ public trace(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
424
+ this.register(convertOptions(['TRACE'], pathOrOptions, this.defaultOptions), callback);
425
+ }
426
+
427
+ public head(pathOrOptions: string | HttpRouterFunctionOptions, callback: (...args: any[]) => any) {
428
+ this.register(convertOptions(['HEAD'], pathOrOptions, this.defaultOptions), callback);
429
+ }
430
+
431
+ private register(options: HttpRouterFunctionOptions, callback: (...args: any[]) => any, module?: InjectorModule<any>) {
432
+ const fn = ReflectionFunction.from(callback);
433
+
434
+ const routeConfig = new RouteConfig(options.name || '', options.methods || [], options.path, {
435
+ type: 'function',
436
+ fn: callback,
437
+ });
438
+ routeConfig.module = module;
439
+ if (options.responses) routeConfig.responses = options.responses;
440
+ if (options.description) routeConfig.description = options.description;
441
+ if (options.category) routeConfig.category = options.category;
442
+ if (options.groups) routeConfig.groups = options.groups;
443
+ if (options.data) routeConfig.data = new Map(Object.entries(options.data));
444
+ if (options.baseUrl) routeConfig.baseUrl = options.baseUrl;
445
+ if (options.middlewares) {
446
+ routeConfig.middlewares = options.middlewares.map(v => {
447
+ return { config: v(), module };
448
+ });
449
+ }
450
+
451
+ if (options.resolverForToken) {
452
+ for (const item of options.resolverForToken) routeConfig.resolverForToken.set(...item);
453
+ }
454
+
455
+ if (options.resolverForToken) {
456
+ for (const item of options.resolverForToken) routeConfig.resolverForToken.set(...item);
457
+ }
458
+
459
+ if (options.resolverForParameterName) {
460
+ for (const item of options.resolverForParameterName) routeConfig.resolverForParameterName.set(...item);
461
+ }
462
+
463
+ routeConfig.serializer = options.serializer;
464
+ routeConfig.serializationOptions = options.serializationOptions;
465
+
466
+ routeConfig.returnType = filterValidReturnType(fn.getReturnType());
467
+ this.addRoute(routeConfig);
468
+ }
469
+ }
470
+
471
+ function createRouteConfigFromHttpAction(routeAction: RouteClassControllerAction | RouteFunctionControllerAction, action: HttpAction, module?: InjectorModule<any>, controller?: HttpController) {
472
+ const routeConfig = new RouteConfig(action.name, action.httpMethods, action.path, routeAction);
473
+ routeConfig.responses = action.responses;
474
+ routeConfig.description = action.description;
475
+ routeConfig.category = action.category;
476
+ routeConfig.groups = action.groups;
477
+ routeConfig.data = new Map(action.data);
478
+ if (controller) {
479
+ routeConfig.baseUrl = controller.baseUrl;
480
+
481
+ routeConfig.middlewares = controller.middlewares.map(v => {
482
+ return { config: v(), module };
483
+ });
484
+ routeConfig.resolverForToken = new Map(controller.resolverForToken);
485
+ routeConfig.resolverForParameterName = new Map(controller.resolverForParameterName);
486
+ }
487
+
488
+ routeConfig.middlewares.push(...action.middlewares.map(v => {
489
+ return { config: v(), module };
490
+ }));
491
+
492
+ for (const item of action.resolverForToken) routeConfig.resolverForToken.set(...item);
493
+
494
+ for (const item of action.resolverForParameterName) routeConfig.resolverForParameterName.set(...item);
495
+
496
+ routeConfig.serializer = action.serializer;
497
+ routeConfig.serializationOptions = action.serializationOptions;
498
+ return routeConfig;
499
+ }
500
+
501
+ export class HttpRouterRegistry extends HttpRouterRegistryFunctionRegistrar {
502
+ protected routes: RouteConfig[] = [];
503
+ private buildId: number = 1;
504
+
505
+ public getBuildId(): number {
506
+ return this.buildId;
507
+ }
508
+
509
+ public getRoutes(): RouteConfig[] {
510
+ return this.routes;
511
+ }
512
+
513
+ public addRouteForController(controller: ClassType, module: InjectorModule<any>) {
514
+ const controllerData = httpClass._fetch(controller);
515
+ if (!controllerData) throw new Error(`Http controller class ${getClassName(controller)} has no @http.controller decorator.`);
516
+ const schema = ReflectionClass.from(controller);
517
+
518
+ for (const action of controllerData.getActions()) {
519
+ const routeAction: RouteClassControllerAction = {
520
+ type: 'controller',
521
+ controller,
522
+ module,
523
+ methodName: action.methodName
524
+ };
525
+ const routeConfig = createRouteConfigFromHttpAction(routeAction, action, module, controllerData);
526
+
527
+ routeConfig.module = module;
528
+
529
+ if (schema.hasMethod(action.methodName)) routeConfig.returnType = filterValidReturnType(schema.getMethod(action.methodName).getReturnType());
530
+ this.addRoute(routeConfig);
531
+ }
532
+ }
533
+
534
+ public addRoute(routeConfig: RouteConfig) {
535
+ this.routes.push(routeConfig);
536
+ this.buildId++;
537
+ }
538
+ }
539
+
540
+ export class HttpRouter {
541
+ protected fn?: (request: HttpRequest) => ResolvedController | undefined;
542
+ protected buildId: number = 0;
543
+ protected resolveFn?: (name: string, parameters: { [name: string]: any }) => string;
544
+
545
+ constructor(
546
+ controllers: HttpControllers,
547
+ private logger: LoggerInterface,
548
+ private config: HttpConfig,
549
+ private middlewareRegistry: MiddlewareRegistry = new MiddlewareRegistry,
550
+ private registry: HttpRouterRegistry = new HttpRouterRegistry,
551
+ ) {
552
+ for (const controller of controllers.controllers) {
553
+ this.addRouteForController(controller.controller, controller.module);
554
+ }
555
+ }
556
+
557
+ getRoutes(): RouteConfig[] {
558
+ return this.registry.getRoutes();
559
+ }
560
+
561
+ static forControllers(
562
+ controllers: (ClassType | { module: InjectorModule<any>, controller: ClassType })[],
563
+ middlewareRegistry: MiddlewareRegistry = new MiddlewareRegistry(),
564
+ module: InjectorModule<any> = new InjectorModule(),
565
+ config: HttpConfig = new HttpConfig()
566
+ ): HttpRouter {
567
+ return new this(new HttpControllers(controllers.map(v => {
568
+ return isClass(v) ? { controller: v, module } : v;
569
+ })), new Logger([], []), config, middlewareRegistry);
570
+ }
571
+
572
+ protected getRouteCode(compiler: CompilerContext, routeConfig: RouteConfig): string {
573
+ const parsedRoute = parseRouteControllerAction(routeConfig);
574
+ const path = routeConfig.getFullPath();
575
+ const pathParamStarter = path.indexOf(':');
576
+ const prefix = path.substr(0, pathParamStarter);
577
+
578
+ const regexVar = compiler.reserveVariable('regex', new RegExp('^' + parsedRoute.regex + '$'));
579
+
580
+ const hasParameters = pathParamStarter !== -1 || parsedRoute.getParameters().length > 0;
581
+
582
+ const fullPath = routeConfig.getFullPath();
583
+ const middlewareConfigs = filterMiddlewaresForRoute(this.middlewareRegistry.configs, routeConfig, fullPath);
584
+
585
+ const parameters = [...parsedRoute.getParameters()];
586
+
587
+ //look through all http listeners and check if they need any http parameters.
588
+ //the router then parses
589
+
590
+ let matcher = `_path.startsWith(${JSON.stringify(prefix)}) && (_match = _path.match(${regexVar}))`;
591
+ if (!hasParameters) {
592
+ matcher = `_path === ${JSON.stringify(path)}`;
593
+ }
594
+
595
+ let middlewares = 'undefined';
596
+ if (middlewareConfigs.length) {
597
+ const middlewareItems: string[] = [];
598
+ for (const middlewareConfig of middlewareConfigs) {
599
+ const moduleVar = middlewareConfig.module ? ', ' + compiler.reserveVariable('module', middlewareConfig.module) : '';
600
+
601
+ for (const middleware of middlewareConfig.config.middlewares) {
602
+ if (isClass(middleware)) {
603
+ const classVar = compiler.reserveVariable('middlewareClassType', middleware);
604
+ middlewareItems.push(`{fn: function() {return _injector.get(${classVar}${moduleVar}).execute(...arguments) }, timeout: ${middlewareConfig.config.timeout}}`);
605
+ } else {
606
+ middlewareItems.push(`{fn: ${compiler.reserveVariable('middlewareFn', middleware)}, timeout: ${middlewareConfig.config.timeout}}`);
607
+ }
608
+ }
609
+ }
610
+
611
+ middlewares = `
612
+ function(_injector) {
613
+ return [${middlewareItems.join(', ')}];
614
+ }
615
+ `;
616
+ }
617
+
618
+ const parametersLoader = getRequestParserCodeForParameters(compiler, this.config.parser, parameters, {
619
+ module: routeConfig.module,
620
+ resolverForToken: routeConfig.resolverForToken,
621
+ resolverForParameterName: routeConfig.resolverForParameterName,
622
+ pathParameterNames: parsedRoute.pathParameterNames,
623
+ routeConfig
624
+ });
625
+
626
+ let methodCheck = '';
627
+ if (routeConfig.httpMethods.length) {
628
+ methodCheck = '(' + routeConfig.httpMethods.map(v => {
629
+ return `_method === '${v.toUpperCase()}'`;
630
+ }).join(' || ') + ') && ';
631
+ }
632
+
633
+ const routeConfigVar = compiler.reserveVariable('routeConfigVar', routeConfig);
634
+ return `
635
+ //=> ${routeConfig.httpMethods.join(',')} ${path}
636
+ if (${methodCheck}${matcher}) {
637
+ return {routeConfig: ${routeConfigVar}, parameters: ${parametersLoader}, uploadedFiles: uploadedFiles, middlewares: ${middlewares}};
638
+ }
639
+ `;
640
+ }
641
+
642
+ protected getRouteUrlResolveCode(compiler: CompilerContext, routeConfig: RouteConfig): string {
643
+ const parsedRoute = parseRouteControllerAction(routeConfig);
644
+
645
+ let url = routeConfig.getFullPath();
646
+ url = url.replace(/:(\w+)/g, (a, name) => {
647
+ return `\${parameters.${name}}`;
648
+ });
649
+
650
+ const modify: string[] = [];
651
+ for (const parameter of parsedRoute.getParameters()) {
652
+ if (parameter.query || parameter.queries) {
653
+ const queryPath = parameter.typePath === undefined && parameter.query ? parameter.parameter.name : parameter.typePath || '';
654
+
655
+ if (parameter.parameter.type.kind === ReflectionKind.class || parameter.parameter.type.kind === ReflectionKind.objectLiteral) {
656
+ for (const property of ReflectionClass.from(parameter.parameter.type).getProperties()) {
657
+ const accessor = `parameters.${parameter.getName()}?.${property.name}`;
658
+ const thisPath = queryPath ? queryPath + '.' + property.name : property.name;
659
+ modify.push(`${accessor} !== undefined && query.push(${JSON.stringify(dotToUrlPath(thisPath))} + '=' + encodeURIComponent(${accessor}))`);
660
+ }
661
+ } else {
662
+ modify.push(`parameters.${parameter.getName()} !== undefined && query.push(${JSON.stringify(dotToUrlPath(queryPath))} + '=' + encodeURIComponent(parameters.${parameter.getName()}))`);
663
+
664
+ }
665
+ }
666
+ }
667
+
668
+ return `
669
+ case ${JSON.stringify(routeConfig.name)}: {
670
+ let url = \`${url}\`;
671
+ let query = [];
672
+ ${modify.join('\n')}
673
+ return url + (query.length ? '?'+query.join('&') : '');
674
+ }
675
+ `;
676
+ }
677
+
678
+ public addRoute(routeConfig: RouteConfig) {
679
+ this.registry.addRoute(routeConfig);
680
+ }
681
+
682
+ public addRouteForController(controller: ClassType, module: InjectorModule<any>) {
683
+ this.registry.addRouteForController(controller, module);
684
+ }
685
+
686
+ protected build(): (request: HttpRequest) => ResolvedController | undefined {
687
+ this.buildId = this.registry.getBuildId();
688
+ const compiler = new CompilerContext;
689
+ compiler.context.set('ValidationError', ValidationError);
690
+ compiler.context.set('qs', qs);
691
+
692
+ const code: string[] = [];
693
+
694
+ for (const route of this.getRoutes()) {
695
+ code.push(this.getRouteCode(compiler, route));
696
+ }
697
+
698
+ return compiler.build(`
699
+ let _match;
700
+ const _method = request.method || 'GET';
701
+ const _url = request.url || '/';
702
+ const _headers = request.headers || {};
703
+ const _qPosition = _url.indexOf('?');
704
+ let uploadedFiles = {};
705
+ const _path = _qPosition === -1 ? _url : _url.substr(0, _qPosition);
706
+ const _query = _qPosition === -1 ? {} : qs.parse(_url.substr(_qPosition + 1));
707
+ ${code.join('\n')}
708
+ `, 'request') as any;
709
+ }
710
+
711
+ protected buildUrlResolver(): any {
712
+ const compiler = new CompilerContext;
713
+ const code: string[] = [];
714
+
715
+ for (const route of this.getRoutes()) {
716
+ code.push(this.getRouteUrlResolveCode(compiler, route));
717
+ }
718
+
719
+ return compiler.build(`
720
+ switch (name) {
721
+ ${code.join('\n')}
722
+ }
723
+ throw new Error('No route for name ' + name + ' found');
724
+ `, 'name', 'parameters') as any;
725
+ }
726
+
727
+ public resolveUrl(routeName: string, parameters: { [name: string]: any } = {}): string {
728
+ if (!this.resolveFn) {
729
+ this.resolveFn = this.buildUrlResolver();
730
+ }
731
+
732
+ return this.resolveFn!(routeName, parameters);
733
+ }
734
+
735
+ public resolveRequest(request: HttpRequest): ResolvedController | undefined {
736
+ if (!this.fn || this.buildId !== this.registry.getBuildId()) {
737
+ this.fn = this.build();
738
+ }
739
+
740
+ return this.fn(request);
741
+ }
742
+
743
+ public resolve(method: string, url: string): ResolvedController | undefined {
744
+ method = method.toUpperCase();
745
+ return this.resolveRequest({ url, method } as any);
746
+ }
747
+ }