@deepkit/http 1.0.1-alpha.16 → 1.0.1-alpha.160

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 (124) hide show
  1. package/CHANGELOG.md +236 -0
  2. package/dist/cjs/index.d.ts +14 -10
  3. package/dist/cjs/index.js +19 -11
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/src/controllers.d.ts +10 -3
  6. package/dist/cjs/src/controllers.js +7 -3
  7. package/dist/cjs/src/controllers.js.map +1 -1
  8. package/dist/cjs/src/decorator.d.ts +144 -70
  9. package/dist/cjs/src/decorator.js +277 -102
  10. package/dist/cjs/src/decorator.js.map +1 -1
  11. package/dist/cjs/src/filter.d.ts +49 -0
  12. package/dist/cjs/src/filter.js +215 -0
  13. package/dist/cjs/src/filter.js.map +1 -0
  14. package/dist/cjs/src/http-app.d.ts +8 -0
  15. package/dist/cjs/src/http-app.js +16 -0
  16. package/dist/cjs/src/http-app.js.map +1 -0
  17. package/dist/cjs/src/http.d.ts +131 -32
  18. package/dist/cjs/src/http.js +523 -122
  19. package/dist/cjs/src/http.js.map +1 -1
  20. package/dist/cjs/src/kernel.d.ts +50 -9
  21. package/dist/cjs/src/kernel.js +109 -98
  22. package/dist/cjs/src/kernel.js.map +1 -1
  23. package/dist/cjs/src/logger.d.ts +3 -3
  24. package/dist/cjs/src/logger.js +9 -12
  25. package/dist/cjs/src/logger.js.map +1 -1
  26. package/dist/cjs/src/middleware.d.ts +61 -0
  27. package/dist/cjs/src/middleware.js +95 -0
  28. package/dist/cjs/src/middleware.js.map +1 -0
  29. package/dist/cjs/src/model.d.ts +270 -4
  30. package/dist/cjs/src/model.js +401 -3
  31. package/dist/cjs/src/model.js.map +1 -1
  32. package/dist/cjs/src/module.config.d.ts +149 -4
  33. package/dist/cjs/src/module.config.js +11 -6
  34. package/dist/cjs/src/module.config.js.map +1 -1
  35. package/dist/cjs/src/module.d.ts +14 -32
  36. package/dist/cjs/src/module.js +122 -7
  37. package/dist/cjs/src/module.js.map +1 -1
  38. package/dist/cjs/src/request-parser.d.ts +37 -0
  39. package/dist/cjs/src/request-parser.js +382 -0
  40. package/dist/cjs/src/request-parser.js.map +1 -0
  41. package/dist/cjs/src/router.d.ts +174 -75
  42. package/dist/cjs/src/router.js +469 -288
  43. package/dist/cjs/src/router.js.map +1 -1
  44. package/dist/cjs/src/static-serving.d.ts +32 -4
  45. package/dist/cjs/src/static-serving.js +115 -71
  46. package/dist/cjs/src/static-serving.js.map +1 -1
  47. package/dist/cjs/src/utils.js +2 -2
  48. package/dist/cjs/src/utils.js.map +1 -1
  49. package/dist/cjs/tsconfig.tsbuildinfo +1 -2916
  50. package/dist/esm/index.d.ts +14 -10
  51. package/dist/esm/index.js +14 -10
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/src/controllers.d.ts +10 -3
  54. package/dist/esm/src/controllers.js +7 -3
  55. package/dist/esm/src/controllers.js.map +1 -1
  56. package/dist/esm/src/decorator.d.ts +144 -70
  57. package/dist/esm/src/decorator.js +272 -99
  58. package/dist/esm/src/decorator.js.map +1 -1
  59. package/dist/esm/src/filter.d.ts +49 -0
  60. package/dist/esm/src/filter.js +209 -0
  61. package/dist/esm/src/filter.js.map +1 -0
  62. package/dist/esm/src/http-app.d.ts +8 -0
  63. package/dist/esm/src/http-app.js +12 -0
  64. package/dist/esm/src/http-app.js.map +1 -0
  65. package/dist/esm/src/http.d.ts +131 -32
  66. package/dist/esm/src/http.js +504 -121
  67. package/dist/esm/src/http.js.map +1 -1
  68. package/dist/esm/src/kernel.d.ts +50 -9
  69. package/dist/esm/src/kernel.js +110 -100
  70. package/dist/esm/src/kernel.js.map +1 -1
  71. package/dist/esm/src/logger.d.ts +3 -3
  72. package/dist/esm/src/logger.js +7 -11
  73. package/dist/esm/src/logger.js.map +1 -1
  74. package/dist/esm/src/middleware.d.ts +61 -0
  75. package/dist/esm/src/middleware.js +90 -0
  76. package/dist/esm/src/middleware.js.map +1 -0
  77. package/dist/esm/src/model.d.ts +270 -4
  78. package/dist/esm/src/model.js +357 -1
  79. package/dist/esm/src/model.js.map +1 -1
  80. package/dist/esm/src/module.config.d.ts +149 -4
  81. package/dist/esm/src/module.config.js +9 -5
  82. package/dist/esm/src/module.config.js.map +1 -1
  83. package/dist/esm/src/module.d.ts +14 -32
  84. package/dist/esm/src/module.js +121 -7
  85. package/dist/esm/src/module.js.map +1 -1
  86. package/dist/esm/src/request-parser.d.ts +37 -0
  87. package/dist/esm/src/request-parser.js +372 -0
  88. package/dist/esm/src/request-parser.js.map +1 -0
  89. package/dist/esm/src/router.d.ts +174 -75
  90. package/dist/esm/src/router.js +463 -285
  91. package/dist/esm/src/router.js.map +1 -1
  92. package/dist/esm/src/static-serving.d.ts +32 -4
  93. package/dist/esm/src/static-serving.js +107 -65
  94. package/dist/esm/src/static-serving.js.map +1 -1
  95. package/dist/esm/src/utils.js +1 -0
  96. package/dist/esm/src/utils.js.map +1 -1
  97. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -2916
  98. package/index.ts +14 -10
  99. package/package.json +30 -19
  100. package/src/controllers.ts +4 -3
  101. package/src/decorator.ts +242 -104
  102. package/src/filter.ts +224 -0
  103. package/src/http-app.ts +11 -0
  104. package/src/http.ts +501 -100
  105. package/src/kernel.ts +128 -80
  106. package/src/logger.ts +4 -6
  107. package/src/middleware.ts +116 -0
  108. package/src/model.ts +516 -5
  109. package/src/module.config.ts +168 -5
  110. package/src/module.ts +120 -7
  111. package/src/request-parser.ts +430 -0
  112. package/src/router.ts +546 -317
  113. package/src/static-serving.ts +120 -50
  114. package/src/utils.ts +0 -2
  115. package/tsconfig.esm.json +7 -7
  116. package/tsconfig.json +18 -11
  117. package/tsconfig.spec.json +8 -0
  118. package/dist/cjs/tests/router.spec.d.ts +0 -1
  119. package/dist/cjs/tests/router.spec.js +0 -481
  120. package/dist/cjs/tests/router.spec.js.map +0 -1
  121. package/dist/esm/tests/router.spec.d.ts +0 -1
  122. package/dist/esm/tests/router.spec.js +0 -479
  123. package/dist/esm/tests/router.spec.js.map +0 -1
  124. package/tests/router.spec.ts +0 -347
package/src/http.ts CHANGED
@@ -8,16 +8,54 @@
8
8
  * You should have received a copy of the MIT License along with this program.
9
9
  */
10
10
 
11
- import { CustomError } from '@deepkit/core';
12
- import { getClassTypeFromInstance, isClassInstance, isRegisteredEntity, jsonSerializer, ValidationFailed } from '@deepkit/type';
13
- import { ServerResponse } from 'http';
11
+ import {
12
+ asyncOperation,
13
+ ClassType,
14
+ CustomError,
15
+ getClassName,
16
+ getClassTypeFromInstance,
17
+ isArray,
18
+ isClassInstance,
19
+ isObject,
20
+ } from '@deepkit/core';
21
+ import { OutgoingHttpHeaders, ServerResponse } from 'http';
14
22
  import { eventDispatcher } from '@deepkit/event';
15
- import { HttpRequest, HttpResponse } from './model';
16
- import { injectable, InjectorContext } from '@deepkit/injector';
17
- import { Logger } from '@deepkit/logger';
18
- import { RouteConfig, RouteParameterResolverForInjector, Router } from './router';
23
+ import { HttpRequest, HttpRequestPositionedParameters, HttpResponse } from './model.js';
24
+ import { InjectorContext } from '@deepkit/injector';
25
+ import { LoggerInterface } from '@deepkit/logger';
26
+ import { HttpRouter, RouteConfig, RouteParameterResolverForInjector } from './router.js';
19
27
  import { createWorkflow, WorkflowEvent } from '@deepkit/workflow';
20
- import { isElementStruct, render } from '@deepkit/template';
28
+ import type { ElementStruct, render } from '@deepkit/template';
29
+ import { FrameCategory, Stopwatch } from '@deepkit/stopwatch';
30
+ import {
31
+ getSerializeFunction,
32
+ hasTypeInformation,
33
+ ReflectionKind,
34
+ resolveReceiveType,
35
+ SerializationError,
36
+ serialize,
37
+ serializer,
38
+ Type,
39
+ typeSettings,
40
+ UnpopulatedCheck,
41
+ ValidationError,
42
+ } from '@deepkit/type';
43
+ import stream from 'stream';
44
+
45
+ export function isElementStruct(v: any): v is ElementStruct {
46
+ return isObject(v) && 'hasOwnProperty' in v && v.hasOwnProperty('render') && v.hasOwnProperty('attributes');
47
+ }
48
+
49
+ let templateRender: typeof render;
50
+
51
+ function getTemplateRender(): typeof render {
52
+ if (!templateRender) {
53
+ const template = require('@deepkit/template');
54
+ templateRender = template.render;
55
+ }
56
+
57
+ return templateRender;
58
+ }
21
59
 
22
60
  export class Redirect {
23
61
  public routeName?: string;
@@ -43,42 +81,65 @@ export class Redirect {
43
81
  }
44
82
  }
45
83
 
46
- export interface HttpError<T> {
47
- new(...args: any[]): Error;
48
-
49
- getHttpCode(): T;
84
+ export class HttpError<T extends number> extends CustomError {
85
+ constructor(public message: string, public httpCode: T) {
86
+ super(message);
87
+ }
50
88
  }
51
89
 
52
- export function HttpError<T extends number>(code: T, defaultMessage: string = ''): HttpError<T> {
53
- return class extends CustomError {
54
- constructor(message: string = defaultMessage) {
55
- super(message);
90
+ export function createHttpError<T extends number>(code: T, defaultMessage: string = ''): ClassType<HttpError<T>> {
91
+ return class extends HttpError<T> {
92
+ constructor(public message: string = defaultMessage) {
93
+ super(message, code);
56
94
  }
95
+ } as any;
96
+ }
57
97
 
58
- static getHttpCode() {
59
- return code;
60
- }
61
- };
98
+ export class HttpBadRequestError extends createHttpError(400, 'Bad request') {
99
+ }
100
+
101
+ export class HttpUnauthorizedError extends createHttpError(401, 'Unauthorized') {
102
+ }
103
+
104
+ export class HttpAccessDeniedError extends createHttpError(403, 'Access denied') {
105
+ }
106
+
107
+ export class HttpNotFoundError extends createHttpError(404, 'Not found') {
108
+ }
109
+
110
+ export class HttpMethodNotAllowedError extends createHttpError(405, 'Method not allowed') {
111
+ }
112
+
113
+ export class HttpNotAcceptableError extends createHttpError(406, 'Not acceptable') {
62
114
  }
63
115
 
64
- export class HttpNotFoundError extends HttpError(404, 'Not found') {
116
+ export class HttpTimeoutError extends createHttpError(408, 'Request timeout') {
65
117
  }
66
118
 
67
- export class HttpBadRequestError extends HttpError(400, 'Bad request') {
119
+ export class HttpConflictError extends createHttpError(409, 'Conflict') {
68
120
  }
69
121
 
70
- export class HttpAccessDeniedError extends HttpError(403, 'Access denied') {
122
+ export class HttpGoneError extends createHttpError(410, 'Gone') {
123
+ }
124
+
125
+ export class HttpTooManyRequestsError extends createHttpError(429, 'Too many requests') {
126
+ }
127
+
128
+ export class HttpInternalServerError extends createHttpError(500, 'Internal server error') {
129
+ }
130
+
131
+ export class HttpNotImplementedError extends createHttpError(501, 'Not implemented') {
71
132
  }
72
133
 
73
134
  export class HttpWorkflowEvent {
74
- stopped = false;
135
+ propagationStopped = false;
75
136
 
76
137
  stopPropagation() {
77
- this.stopped = true;
138
+ this.propagationStopped = true;
78
139
  }
79
140
 
80
- isStopped() {
81
- return this.stopped;
141
+ isPropagationStopped() {
142
+ return this.propagationStopped;
82
143
  }
83
144
 
84
145
  public nextState?: any;
@@ -97,6 +158,9 @@ export class HttpWorkflowEvent {
97
158
  this.nextStateEvent = event;
98
159
  }
99
160
 
161
+ /**
162
+ * Whether already a next workflow state has been scheduled.
163
+ */
100
164
  hasNext(): boolean {
101
165
  return this.nextState !== undefined;
102
166
  }
@@ -140,6 +204,10 @@ export class HttpWorkflowEventWithRoute extends HttpWorkflowEvent {
140
204
  send(response: any) {
141
205
  this.next('response', new HttpResponseEvent(this.injectorContext, this.request, this.response, response, this.route));
142
206
  }
207
+
208
+ accessDenied(error?: Error) {
209
+ this.next('accessDenied', new HttpAccessDeniedEvent(this.injectorContext, this.request, this.response, this.route, error));
210
+ }
143
211
  }
144
212
 
145
213
  export class HttpRouteEvent extends HttpWorkflowEvent {
@@ -177,10 +245,6 @@ export class HttpAuthEvent extends HttpWorkflowEventWithRoute {
177
245
  success() {
178
246
  this.next('resolveParameters', new HttpResolveParametersEvent(this.injectorContext, this.request, this.response, this.parameterResolver, this.route));
179
247
  }
180
-
181
- accessDenied() {
182
- this.next('accessDenied', new HttpAccessDeniedEvent(this.injectorContext, this.request, this.response, this.route));
183
- }
184
248
  }
185
249
 
186
250
  export class HttpAccessDeniedEvent extends HttpWorkflowEvent {
@@ -189,13 +253,14 @@ export class HttpAccessDeniedEvent extends HttpWorkflowEvent {
189
253
  public request: HttpRequest,
190
254
  public response: HttpResponse,
191
255
  public route: RouteConfig,
256
+ public error?: Error,
192
257
  ) {
193
258
  super(injectorContext, request, response);
194
259
  }
195
260
  }
196
261
 
197
- export class HttpResolveParametersEvent extends HttpWorkflowEvent {
198
- public parameters: any[] = [];
262
+ export class HttpResolveParametersEvent extends HttpWorkflowEventWithRoute {
263
+ public parameters: HttpRequestPositionedParameters = { arguments: [], parameters: {} };
199
264
 
200
265
  constructor(
201
266
  public injectorContext: InjectorContext,
@@ -204,23 +269,32 @@ export class HttpResolveParametersEvent extends HttpWorkflowEvent {
204
269
  public parameterResolver: RouteParameterResolverForInjector,
205
270
  public route: RouteConfig,
206
271
  ) {
207
- super(injectorContext, request, response);
272
+ super(injectorContext, request, response, route);
273
+ }
274
+
275
+ accessDenied() {
276
+ this.next('accessDenied', new HttpAccessDeniedEvent(this.injectorContext, this.request, this.response, this.route));
208
277
  }
209
278
  }
210
279
 
211
- export class HttpControllerEvent extends HttpWorkflowEvent {
280
+ export class HttpControllerEvent extends HttpWorkflowEventWithRoute {
212
281
  constructor(
213
282
  public injectorContext: InjectorContext,
214
283
  public request: HttpRequest,
215
284
  public response: HttpResponse,
216
- public parameters: any[] = [],
285
+ public parameters: HttpRequestPositionedParameters = { arguments: [], parameters: {} },
217
286
  public route: RouteConfig,
218
287
  ) {
219
- super(injectorContext, request, response);
288
+ super(injectorContext, request, response, route);
220
289
  }
221
290
  }
222
291
 
223
292
  export class HttpResponseEvent extends WorkflowEvent {
293
+ /**
294
+ * The time it took to call the controller action in milliseconds.
295
+ */
296
+ public controllerActionTime: number = 0;
297
+
224
298
  constructor(
225
299
  public injectorContext: InjectorContext,
226
300
  public request: HttpRequest,
@@ -232,7 +306,12 @@ export class HttpResponseEvent extends WorkflowEvent {
232
306
  }
233
307
  }
234
308
 
235
- export class HttpControllerErrorEvent extends HttpWorkflowEvent {
309
+ export class HttpControllerErrorEvent extends HttpWorkflowEventWithRoute {
310
+ /**
311
+ * The time it took to call the controller action in milliseconds.
312
+ */
313
+ public controllerActionTime: number = 0;
314
+
236
315
  constructor(
237
316
  public injectorContext: InjectorContext,
238
317
  public request: HttpRequest,
@@ -240,7 +319,7 @@ export class HttpControllerErrorEvent extends HttpWorkflowEvent {
240
319
  public route: RouteConfig,
241
320
  public error: Error,
242
321
  ) {
243
- super(injectorContext, request, response);
322
+ super(injectorContext, request, response, route);
244
323
  }
245
324
  }
246
325
 
@@ -269,22 +348,217 @@ export const httpWorkflow = createWorkflow('http', {
269
348
  routeNotFound: 'response',
270
349
  });
271
350
 
272
- export class HtmlResponse {
273
- constructor(public html: string, public statusCode?: number) {
351
+ export class BaseResponse {
352
+ autoSerializing: boolean = true;
353
+
354
+ constructor(public _statusCode?: number, public _headers: OutgoingHttpHeaders = {}) {
355
+ }
356
+
357
+ status(code: number): this {
358
+ this._statusCode = code;
359
+ return this;
360
+ }
361
+
362
+ /**
363
+ * Per default a JSONResponse is serialized using the return type specified at the route.
364
+ * This disables that behaviour so that JSON.stringify is run on the result directly.
365
+ */
366
+ disableAutoSerializing() {
367
+ this.autoSerializing = false;
368
+ return this;
369
+ }
370
+
371
+ header(name: string, value: string | number): this {
372
+ this._headers[name] = value;
373
+ return this;
374
+ }
375
+
376
+ headers(headers: OutgoingHttpHeaders): this {
377
+ this._headers = headers;
378
+ return this;
379
+ }
380
+
381
+ contentType(type: string): this {
382
+ this._headers['content-type'] = type;
383
+ return this;
274
384
  }
275
385
  }
276
386
 
277
- export class JSONResponse {
278
- constructor(public json: any, public statusCode?: number) {
387
+ export class Response extends BaseResponse {
388
+ constructor(public content: string | Uint8Array, contentType: string, statusCode?: number) {
389
+ super(statusCode);
390
+ this.contentType(contentType);
279
391
  }
280
392
  }
281
393
 
394
+ export class HtmlResponse extends BaseResponse {
395
+ constructor(public html: string, statusCode?: number) {
396
+ super(statusCode);
397
+ }
398
+ }
399
+
400
+ export class JSONResponse extends BaseResponse {
401
+ constructor(public json: any, statusCode?: number) {
402
+ super(statusCode);
403
+ }
404
+ }
405
+
406
+ export type SupportedHttpResult =
407
+ undefined
408
+ | null
409
+ | number
410
+ | string
411
+ | Response
412
+ | JSONResponse
413
+ | HtmlResponse
414
+ | HttpResponse
415
+ | ServerResponse
416
+ | stream.Readable
417
+ | Redirect
418
+ | Uint8Array
419
+ | Error;
420
+
421
+ export interface HttpResultFormatterContext {
422
+ request: HttpRequest;
423
+ response: HttpResponse;
424
+ route?: RouteConfig;
425
+ }
426
+
427
+ export class HttpResultFormatter {
428
+ protected jsonContentType: string = 'application/json; charset=utf-8';
429
+ protected htmlContentType: string = 'text/html; charset=utf-8';
430
+
431
+ constructor(protected router: HttpRouter) {
432
+ }
433
+
434
+ protected setContentTypeIfNotSetAlready(response: HttpResponse, contentType: string): void {
435
+ if (response.hasHeader('Content-Type')) return;
436
+
437
+ response.setHeader('Content-Type', contentType);
438
+ }
439
+
440
+ handleError(error: Error, context: HttpResultFormatterContext): void {
441
+
442
+ }
443
+
444
+ handleUndefined(result: undefined | null, context: HttpResultFormatterContext): void {
445
+ context.response.end(result);
446
+ }
447
+
448
+ handleRedirect(result: Redirect, context: HttpResultFormatterContext): void {
449
+ if (result.routeName) {
450
+ context.response.writeHead(result.statusCode, {
451
+ Location: this.router.resolveUrl(result.routeName, result.routeParameters)
452
+ });
453
+ } else {
454
+ context.response.writeHead(result.statusCode, {
455
+ Location: result.url
456
+ });
457
+ }
458
+ context.response.end();
459
+ }
460
+
461
+ handleUnknown(result: any, context: HttpResultFormatterContext): void {
462
+ const oldCheck = typeSettings.unpopulatedCheck;
463
+ try {
464
+ typeSettings.unpopulatedCheck = UnpopulatedCheck.None;
465
+ this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType);
466
+ context.response.end(JSON.stringify(result));
467
+ } finally {
468
+ typeSettings.unpopulatedCheck = oldCheck;
469
+ }
470
+ }
471
+
472
+ handleHtmlResponse(result: HtmlResponse, context: HttpResultFormatterContext): void {
473
+ this.setContentTypeIfNotSetAlready(context.response, this.htmlContentType);
474
+ context.response.writeHead(result._statusCode || 200, result._headers);
475
+ context.response.end(result.html);
476
+ }
477
+
478
+ handleGenericResponse(result: Response, context: HttpResultFormatterContext): void {
479
+ context.response.writeHead(result._statusCode || 200, result._headers);
480
+ context.response.end(result.content);
481
+ }
482
+
483
+ handleJSONResponse(result: JSONResponse, context: HttpResultFormatterContext): void {
484
+ this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType);
485
+ context.response.writeHead(result._statusCode || 200, result._headers);
486
+ context.response.end(JSON.stringify(result.json));
487
+ }
488
+
489
+ handleTypeEntity<T>(classType: ClassType<T>, instance: T, context: HttpResultFormatterContext, route?: RouteConfig): void {
490
+ this.handleType(resolveReceiveType(classType), instance, context, route);
491
+ }
492
+
493
+ handleType<T>(type: Type, instance: T, context: HttpResultFormatterContext, route?: RouteConfig): void {
494
+ this.setContentTypeIfNotSetAlready(context.response, this.jsonContentType);
495
+ const serializerToUse = route && route?.serializer ? route.serializer : serializer;
496
+ context.response.end(JSON.stringify(serialize(instance, route ? route.serializationOptions : undefined, serializerToUse, undefined, type)));
497
+ }
498
+
499
+ handleStream(stream: stream.Readable, context: HttpResultFormatterContext): void {
500
+ stream.pipe(context.response);
501
+ }
502
+
503
+ handleBinary(result: Uint8Array, context: HttpResultFormatterContext): void {
504
+ context.response.end(result);
505
+ }
506
+
507
+ handleResponse(context: HttpResultFormatterContext) {
508
+ }
509
+
510
+ handle(result: SupportedHttpResult, context: HttpResultFormatterContext): void {
511
+ if (result === null || result === undefined) {
512
+ this.handleUndefined(result, context);
513
+ } else if (result instanceof Error) {
514
+ this.handleError(result, context);
515
+ } else if (result instanceof Redirect) {
516
+ this.handleRedirect(result, context);
517
+ } else if (result instanceof ServerResponse) {
518
+ this.handleResponse(context);
519
+ } else if (result instanceof HtmlResponse) {
520
+ this.handleHtmlResponse(result, context);
521
+ } else if (result instanceof stream.Readable) {
522
+ this.handleStream(result, context);
523
+ } else if (result instanceof Uint8Array) {
524
+ this.handleBinary(result, context);
525
+ } else if (result instanceof JSONResponse) {
526
+ this.handleJSONResponse(result, context);
527
+ } else if (result instanceof Response) {
528
+ this.handleGenericResponse(result, context);
529
+ } else {
530
+ if (isClassInstance(result)) {
531
+ const classType = getClassTypeFromInstance(result);
532
+ if (hasTypeInformation(classType)) {
533
+ this.handleTypeEntity(classType, result, context);
534
+ return;
535
+ }
536
+ } else if (isArray(result) && result.length > 0 && isClassInstance(result[0])) {
537
+ const firstClassType = getClassTypeFromInstance(result[0]);
538
+ let allSameType = true;
539
+ for (const item of result) {
540
+ if (!isClassInstance(item) || getClassTypeFromInstance(item) !== firstClassType) {
541
+ allSameType = false;
542
+ break;
543
+ }
544
+ }
545
+ if (allSameType && hasTypeInformation(firstClassType)) {
546
+ this.handleType({ kind: ReflectionKind.array, type: resolveReceiveType(firstClassType) }, result, context);
547
+ return;
548
+ }
549
+ }
550
+
551
+ this.handleUnknown(result, context);
552
+ }
553
+ }
554
+ }
282
555
 
283
- @injectable()
284
556
  export class HttpListener {
285
557
  constructor(
286
- protected router: Router,
287
- protected logger: Logger,
558
+ protected router: HttpRouter,
559
+ protected logger: LoggerInterface,
560
+ protected resultFormatter: HttpResultFormatter,
561
+ protected stopwatch?: Stopwatch,
288
562
  ) {
289
563
  }
290
564
 
@@ -297,13 +571,76 @@ export class HttpListener {
297
571
  }
298
572
 
299
573
  @eventDispatcher.listen(httpWorkflow.onRoute, 100)
300
- onRoute(event: typeof httpWorkflow.onRoute.event) {
574
+ async onRoute(event: typeof httpWorkflow.onRoute.event) {
301
575
  if (event.sent) return;
302
576
  if (event.hasNext()) return;
577
+ const logger = this.logger;
303
578
 
304
- const resolved = this.router.resolveRequest(event.request);
305
- if (resolved) {
306
- event.routeFound(resolved.routeConfig, resolved.parameters);
579
+ try {
580
+ const resolved = this.router.resolveRequest(event.request);
581
+
582
+ if (resolved) {
583
+ event.request.uploadedFiles = resolved.uploadedFiles;
584
+
585
+ //todo: embed that into the generated router code.
586
+ if (resolved.middlewares) {
587
+ const middlewares = resolved.middlewares(event.injectorContext);
588
+ if (middlewares.length) {
589
+ await asyncOperation(async (resolve, reject) => {
590
+ let lastTimer: any = undefined;
591
+
592
+ function finish() {
593
+ clearTimeout(lastTimer);
594
+ resolve(undefined);
595
+ //middleware finished the request. We end the workflow transition
596
+ }
597
+
598
+ event.response.once('finish', finish);
599
+ let i = -1;
600
+
601
+ async function next() {
602
+ i++;
603
+
604
+ if (i >= middlewares.length) {
605
+ event.response.off('finish', finish);
606
+ resolve(undefined);
607
+ return;
608
+ }
609
+
610
+ const timeout = middlewares[i].timeout;
611
+ if (timeout !== undefined && timeout > 0) {
612
+ lastTimer = setTimeout(() => {
613
+ logger.warn(`Middleware timed out. Increase the timeout or fix the middleware. (${middlewares[i].fn})`);
614
+ next();
615
+ }, timeout);
616
+ }
617
+
618
+ try {
619
+ await middlewares[i].fn(event.request, event.response, (error?: any) => {
620
+ if (lastTimer) clearTimeout(lastTimer);
621
+ if (error) {
622
+ event.response.off('finish', finish);
623
+ reject(error);
624
+ } else {
625
+ next();
626
+ }
627
+ });
628
+ } catch (error) {
629
+ reject(error);
630
+ }
631
+ }
632
+
633
+ await next();
634
+ });
635
+ }
636
+ }
637
+
638
+ event.injectorContext.set(RouteConfig, resolved.routeConfig);
639
+ event.routeFound(resolved.routeConfig, resolved.parameters);
640
+ }
641
+ } catch (error) {
642
+ this.logger.error('Could not resolve request', error);
643
+ event.notFound();
307
644
  }
308
645
  }
309
646
 
@@ -322,7 +659,11 @@ export class HttpListener {
322
659
  if (event.sent) return;
323
660
  if (event.hasNext()) return;
324
661
 
325
- event.send(new HtmlResponse('Not found', 404));
662
+ if (event.request.throwErrorOnNotFound) {
663
+ throw new HttpNotFoundError;
664
+ } else {
665
+ event.send(new HtmlResponse('Not found', 404));
666
+ }
326
667
  }
327
668
 
328
669
  @eventDispatcher.listen(httpWorkflow.onAuth, 100)
@@ -341,7 +682,7 @@ export class HttpListener {
341
682
  try {
342
683
  event.parameters = await event.parameterResolver(event.injectorContext);
343
684
  event.next('controller', new HttpControllerEvent(event.injectorContext, event.request, event.response, event.parameters, event.route));
344
- } catch (error) {
685
+ } catch (error: any) {
345
686
  event.next('parametersFailed', new HttpControllerErrorEvent(event.injectorContext, event.request, event.response, event.route, error));
346
687
  }
347
688
  }
@@ -359,16 +700,48 @@ export class HttpListener {
359
700
  if (event.sent) return;
360
701
  if (event.hasNext()) return;
361
702
 
362
- const controllerInstance = event.injectorContext.get(event.route.action.controller);
703
+
704
+ const start = Date.now();
705
+ const stopWatchLabel = event.route.action.type === 'controller'
706
+ ? getClassName(event.route.action.controller) + '.' + event.route.action.methodName
707
+ : event.route.action.fn.name;
708
+
709
+ const frame = this.stopwatch ? this.stopwatch.start(stopWatchLabel, FrameCategory.httpController) : undefined;
363
710
  try {
364
- const method = controllerInstance[event.route.action.methodName];
365
- event.next('response', new HttpResponseEvent(event.injectorContext, event.request, event.response, await method.apply(controllerInstance, event.parameters), event.route));
366
- } catch (error) {
711
+ let result: any;
712
+ if (event.route.action.type === 'controller') {
713
+ const controllerInstance = event.injectorContext.get(event.route.action.controller, event.route.action.module);
714
+ const method = controllerInstance[event.route.action.methodName];
715
+ result = await method.apply(controllerInstance, event.parameters.arguments);
716
+ } else {
717
+ result = await event.route.action.fn(...event.parameters.arguments);
718
+ }
719
+
720
+ if (isElementStruct(result)) {
721
+ const html = await getTemplateRender()(event.injectorContext.getRootInjector(), result, this.stopwatch ? this.stopwatch : undefined);
722
+ result = new HtmlResponse(html, 200).header('Content-Type', 'text/html; charset=utf-8');
723
+ }
724
+ if (result instanceof stream.Readable) {
725
+ const stream = result as stream.Readable;
726
+ await new Promise((resolve, reject) => {
727
+ stream.once('readable', resolve);
728
+ stream.once('error', reject);
729
+ });
730
+ }
731
+ const responseEvent = new HttpResponseEvent(event.injectorContext, event.request, event.response, result, event.route);
732
+ responseEvent.controllerActionTime = Date.now() - start;
733
+ event.next('response', responseEvent);
734
+ } catch (error: any) {
735
+ if (frame) frame.end();
367
736
  if (error instanceof HttpAccessDeniedError) {
368
- event.next('accessDenied', new HttpAccessDeniedEvent(event.injectorContext, event.request, event.response, event.route));
737
+ event.next('accessDenied', new HttpAccessDeniedEvent(event.injectorContext, event.request, event.response, event.route, error));
369
738
  } else {
370
- event.next('controllerError', new HttpControllerErrorEvent(event.injectorContext, event.request, event.response, event.route, error));
739
+ const errorEvent = new HttpControllerErrorEvent(event.injectorContext, event.request, event.response, event.route, error);
740
+ errorEvent.controllerActionTime = Date.now() - start;
741
+ event.next('controllerError', errorEvent);
371
742
  }
743
+ } finally {
744
+ if (frame) frame.end();
372
745
  }
373
746
  }
374
747
 
@@ -377,14 +750,28 @@ export class HttpListener {
377
750
  if (event.response.finished) return;
378
751
  if (event.sent) return;
379
752
 
380
- this.logger.error('Controller parameter resolving error:', event.error);
381
-
382
- if (event.error instanceof ValidationFailed) {
753
+ if (event.error instanceof SerializationError) {
754
+ event.send(new JSONResponse({
755
+ message: event.error.message,
756
+ errors: [{
757
+ path: event.error.path,
758
+ code: 'serialization',
759
+ message: event.error.originalMessage,
760
+ }]
761
+ }, 400).disableAutoSerializing());
762
+ } else if (event.error instanceof ValidationError) {
383
763
  event.send(new JSONResponse({
384
764
  message: event.error.message,
385
765
  errors: event.error.errors
386
- }, 500));
766
+ }, 400).disableAutoSerializing());
767
+ } else if (event.error instanceof HttpError) {
768
+ event.send(new JSONResponse({
769
+ message: event.error.message
770
+ }, event.error.httpCode).disableAutoSerializing());
771
+ return;
387
772
  } else {
773
+ this.logger.error(`Controller for route ${event.route.getFullPath()} parameter resolving error:`, event.error);
774
+
388
775
  event.send(new HtmlResponse('Internal error', 500));
389
776
  }
390
777
  }
@@ -394,50 +781,64 @@ export class HttpListener {
394
781
  if (event.response.finished) return;
395
782
  if (event.sent) return;
396
783
 
784
+ if (event.error instanceof ValidationError) {
785
+ event.send(new JSONResponse({
786
+ message: event.error.message,
787
+ errors: event.error.errors
788
+ }, 400).disableAutoSerializing());
789
+ return;
790
+ } else if (event.error instanceof HttpError) {
791
+ if (event.request.throwErrorOnNotFound && event.error.httpCode === 404) {
792
+ throw event.error;
793
+ }
794
+ event.send(new JSONResponse({
795
+ message: event.error.message
796
+ }, event.error.httpCode).disableAutoSerializing());
797
+ return;
798
+ }
799
+
397
800
  this.logger.error('Controller error', event.error);
398
801
 
399
802
  event.send(new HtmlResponse('Internal error', 500));
400
803
  }
401
804
 
402
- @eventDispatcher.listen(httpWorkflow.onResponse, 100)
403
- async onResponse(event: typeof httpWorkflow.onResponse.event) {
404
- const response = event.result;
405
-
406
- if (response === null || response === undefined) {
407
- event.response.end(response);
408
- } else if ('string' === typeof response) {
409
- event.response.end(response);
410
- } else if (response instanceof Redirect) {
411
- if (response.routeName) {
412
- event.response.writeHead(response.statusCode, {
413
- Location: this.router.resolveUrl(response.routeName, response.routeParameters)
414
- });
415
- } else {
416
- event.response.writeHead(response.statusCode, {
417
- Location: response.url
418
- });
419
- }
420
- event.response.end();
421
- } else if (response instanceof ServerResponse || response instanceof HttpResponse) {
422
- } else if (response instanceof HtmlResponse) {
423
- event.response.setHeader('Content-Type', 'text/html; charset=utf-8');
424
- if (response.statusCode) event.response.writeHead(response.statusCode);
425
- event.response.end(response.html);
426
- } else if (isElementStruct(response)) {
427
- event.response.setHeader('Content-Type', 'text/html; charset=utf-8');
428
- event.response.end(await render(event.injectorContext, response));
429
- } else if (isClassInstance(response) && isRegisteredEntity(getClassTypeFromInstance(response))) {
430
- event.response.setHeader('Content-Type', 'application/json; charset=utf-8');
431
- event.response.end(JSON.stringify(jsonSerializer.for(getClassTypeFromInstance(response)).serialize(response)));
432
- } else if (response instanceof Uint8Array) {
433
- event.response.end(response);
434
- } else if (response instanceof JSONResponse) {
435
- event.response.setHeader('Content-Type', 'application/json; charset=utf-8');
436
- if (response.statusCode) event.response.writeHead(response.statusCode);
437
- event.response.end(JSON.stringify(response.json));
805
+ /**
806
+ * This happens before the result is sent.
807
+ */
808
+ @eventDispatcher.listen(httpWorkflow.onResponse, -100)
809
+ async onResultSerialization(event: typeof httpWorkflow.onResponse.event) {
810
+ if (!event.route) return;
811
+ if (event.response.headersSent) return;
812
+ if (event.result === undefined || event.result === null) return;
813
+
814
+ if (event.result instanceof HtmlResponse || event.result instanceof Response || event.result instanceof ServerResponse || event.result instanceof Redirect || event.result instanceof stream.Readable) {
815
+ // don't do anything
816
+ } else if (event.result instanceof JSONResponse) {
817
+ const schema = (event.result._statusCode && event.route.getSchemaForResponse(event.result._statusCode)) || event.route.returnType;
818
+
819
+ if (!schema || !event.result.autoSerializing) return;
820
+
821
+ const serializerToUse = event.route && event.route.serializer ? event.route.serializer : serializer;
822
+ const serialize = getSerializeFunction(schema, serializerToUse.serializeRegistry);
823
+ event.result.json = serialize(event.result.json, event.route.serializationOptions);
824
+ } else if (event.route.returnType && event.route.returnType.kind !== ReflectionKind.any) {
825
+ const serializerToUse = event.route && event.route.serializer ? event.route.serializer : serializer;
826
+ const serialize = getSerializeFunction(event.route.returnType, serializerToUse.serializeRegistry);
827
+ event.result = serialize(event.result, event.route.serializationOptions);
438
828
  } else {
439
- event.response.setHeader('Content-Type', 'application/json; charset=utf-8');
440
- event.response.end(JSON.stringify(response));
829
+ const schema = event.route.getSchemaForResponse(200);
830
+ if (!schema) return;
831
+
832
+ const serializerToUse = event.route && event.route.serializer ? event.route.serializer : serializer;
833
+ const serialize = getSerializeFunction(schema, serializerToUse.serializeRegistry);
834
+ event.result = serialize(event.result, event.route.serializationOptions);
441
835
  }
442
836
  }
837
+
838
+ @eventDispatcher.listen(httpWorkflow.onResponse, 100)
839
+ async onResponse(event: typeof httpWorkflow.onResponse.event) {
840
+ if (event.response.headersSent) return;
841
+
842
+ this.resultFormatter.handle(event.result, event);
843
+ }
443
844
  }