@deepkit/http 1.0.18 → 1.1.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cjs/index.d.ts +2 -0
  3. package/dist/cjs/index.js +2 -0
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/src/decorator.js +11 -7
  6. package/dist/cjs/src/decorator.js.map +1 -1
  7. package/dist/cjs/src/filter.js +1 -1
  8. package/dist/cjs/src/filter.js.map +1 -1
  9. package/dist/cjs/src/http.d.ts +5 -2
  10. package/dist/cjs/src/http.js +93 -67
  11. package/dist/cjs/src/http.js.map +1 -1
  12. package/dist/cjs/src/kernel.d.ts +2 -2
  13. package/dist/cjs/src/kernel.js +7 -14
  14. package/dist/cjs/src/kernel.js.map +1 -1
  15. package/dist/cjs/src/logger.js +0 -7
  16. package/dist/cjs/src/logger.js.map +1 -1
  17. package/dist/cjs/src/middleware.d.ts +2 -2
  18. package/dist/cjs/src/middleware.js +2 -2
  19. package/dist/cjs/src/mime.d.ts +19 -0
  20. package/dist/cjs/src/mime.js +147 -0
  21. package/dist/cjs/src/mime.js.map +1 -0
  22. package/dist/cjs/src/model.d.ts +7 -0
  23. package/dist/cjs/src/model.js +16 -7
  24. package/dist/cjs/src/model.js.map +1 -1
  25. package/dist/cjs/src/module.js +9 -5
  26. package/dist/cjs/src/module.js.map +1 -1
  27. package/dist/cjs/src/query-string.d.ts +6 -0
  28. package/dist/cjs/src/query-string.js +229 -0
  29. package/dist/cjs/src/query-string.js.map +1 -0
  30. package/dist/cjs/src/request-parser.js +12 -10
  31. package/dist/cjs/src/request-parser.js.map +1 -1
  32. package/dist/cjs/src/router.js +14 -15
  33. package/dist/cjs/src/router.js.map +1 -1
  34. package/dist/cjs/src/static-serving.d.ts +6 -0
  35. package/dist/cjs/src/static-serving.js +232 -28
  36. package/dist/cjs/src/static-serving.js.map +1 -1
  37. package/dist/cjs/src/utils.js +1 -1
  38. package/dist/cjs/src/utils.js.map +1 -1
  39. package/dist/esm/index.d.ts +2 -0
  40. package/dist/esm/index.js +2 -0
  41. package/dist/esm/index.js.map +1 -1
  42. package/dist/esm/src/decorator.js +11 -7
  43. package/dist/esm/src/decorator.js.map +1 -1
  44. package/dist/esm/src/filter.js +1 -1
  45. package/dist/esm/src/filter.js.map +1 -1
  46. package/dist/esm/src/http.d.ts +5 -2
  47. package/dist/esm/src/http.js +58 -66
  48. package/dist/esm/src/http.js.map +1 -1
  49. package/dist/esm/src/kernel.d.ts +2 -2
  50. package/dist/esm/src/kernel.js +8 -15
  51. package/dist/esm/src/kernel.js.map +1 -1
  52. package/dist/esm/src/logger.js +0 -7
  53. package/dist/esm/src/logger.js.map +1 -1
  54. package/dist/esm/src/middleware.d.ts +2 -2
  55. package/dist/esm/src/middleware.js +2 -2
  56. package/dist/esm/src/mime.d.ts +19 -0
  57. package/dist/esm/src/mime.js +142 -0
  58. package/dist/esm/src/mime.js.map +1 -0
  59. package/dist/esm/src/model.d.ts +7 -0
  60. package/dist/esm/src/model.js +16 -7
  61. package/dist/esm/src/model.js.map +1 -1
  62. package/dist/esm/src/module.js +9 -5
  63. package/dist/esm/src/module.js.map +1 -1
  64. package/dist/esm/src/query-string.d.ts +6 -0
  65. package/dist/esm/src/query-string.js +226 -0
  66. package/dist/esm/src/query-string.js.map +1 -0
  67. package/dist/esm/src/request-parser.js +12 -10
  68. package/dist/esm/src/request-parser.js.map +1 -1
  69. package/dist/esm/src/router.js +14 -12
  70. package/dist/esm/src/router.js.map +1 -1
  71. package/dist/esm/src/static-serving.d.ts +6 -0
  72. package/dist/esm/src/static-serving.js +234 -27
  73. package/dist/esm/src/static-serving.js.map +1 -1
  74. package/dist/esm/src/utils.js +1 -1
  75. package/dist/esm/src/utils.js.map +1 -1
  76. package/index.ts +2 -0
  77. package/package.json +22 -25
  78. package/src/http.ts +43 -10
  79. package/src/kernel.ts +8 -21
  80. package/src/middleware.ts +2 -2
  81. package/src/mime.ts +141 -0
  82. package/src/model.ts +10 -2
  83. package/src/module.ts +4 -2
  84. package/src/query-string.ts +228 -0
  85. package/src/request-parser.ts +6 -4
  86. package/src/router.ts +3 -3
  87. package/src/static-serving.ts +221 -8
  88. package/tsconfig.json +3 -17
  89. package/tsconfig.spec.json +4 -1
  90. package/LICENSE +0 -21
  91. package/dist/cjs/tsconfig.tsbuildinfo +0 -1
  92. package/dist/esm/tsconfig.esm.tsbuildinfo +0 -1
package/src/http.ts CHANGED
@@ -27,9 +27,10 @@ export function isElementStruct(v: any): v is ElementStruct {
27
27
 
28
28
  let templateRender: typeof render;
29
29
 
30
- function getTemplateRender(): typeof render {
30
+ // @deepkit/template is optional, so it's only loaded when a controller returns a JSX element.
31
+ async function getTemplateRender(): Promise<typeof render> {
31
32
  if (!templateRender) {
32
- const template = require('@deepkit/template');
33
+ const template = await import('@deepkit/template');
33
34
  templateRender = template.render;
34
35
  }
35
36
 
@@ -110,6 +111,9 @@ export class HttpInternalServerError extends createHttpError(500, 'Internal serv
110
111
  export class HttpNotImplementedError extends createHttpError(501, 'Not implemented') {
111
112
  }
112
113
 
114
+ export class HttpGatewayTimeoutError extends createHttpError(504, 'Gateway timeout') {
115
+ }
116
+
113
117
  export class HttpWorkflowEvent extends BaseEvent {
114
118
  public nextState?: any;
115
119
  public nextStateEvent?: any;
@@ -318,7 +322,7 @@ export const httpWorkflow = createWorkflow('http', {
318
322
  }, {
319
323
  start: 'request',
320
324
  request: 'route',
321
- route: ['auth', 'routeNotFound'],
325
+ route: ['auth', 'routeNotFound', 'response'],
322
326
  auth: ['resolveParameters', 'accessDenied'],
323
327
  resolveParameters: ['controller', 'parametersFailed'],
324
328
  accessDenied: 'response',
@@ -541,9 +545,9 @@ export class HttpListener {
541
545
  protected logger: LoggerInterface,
542
546
  protected resultFormatter: HttpResultFormatter,
543
547
  protected injector: Injector,
544
- protected stopwatch?: Stopwatch,
548
+ protected stopwatch: Stopwatch,
545
549
  ) {
546
- this.setRouteConfig = injector.createSetter(RouteConfig, {name: 'http'});
550
+ this.setRouteConfig = injector.createSetter(RouteConfig, 'http');
547
551
  }
548
552
 
549
553
  @eventDispatcher.listen(httpWorkflow.onRequest, 100)
@@ -559,6 +563,7 @@ export class HttpListener {
559
563
  if (event.sent) return;
560
564
  if (event.hasNext()) return;
561
565
  const logger = this.logger;
566
+ let inMiddleware = false;
562
567
 
563
568
  try {
564
569
  const resolved = this.router.resolveRequest(event.request);
@@ -570,6 +575,7 @@ export class HttpListener {
570
575
  if (resolved.middlewares) {
571
576
  const middlewares = resolved.middlewares(event.injectorContext);
572
577
  if (middlewares.length) {
578
+ inMiddleware = true;
573
579
  await asyncOperation(async (resolve, reject) => {
574
580
  let lastTimer: any = undefined;
575
581
 
@@ -581,6 +587,7 @@ export class HttpListener {
581
587
 
582
588
  event.response.once('finish', finish);
583
589
  let i = -1;
590
+ let timedOut = false;
584
591
 
585
592
  async function next() {
586
593
  i++;
@@ -594,14 +601,18 @@ export class HttpListener {
594
601
  const timeout = middlewares[i].timeout;
595
602
  if (timeout !== undefined && timeout > 0) {
596
603
  lastTimer = setTimeout(() => {
604
+ //a middleware that neither calls next() nor ends the response in time fails the request
605
+ timedOut = true;
597
606
  logger.warn(`Middleware timed out. Increase the timeout or fix the middleware. (${middlewares[i].fn})`);
598
- next();
607
+ event.response.off('finish', finish);
608
+ reject(new HttpGatewayTimeoutError('Middleware timed out'));
599
609
  }, timeout);
600
610
  }
601
611
 
602
612
  try {
603
613
  await middlewares[i].fn(event.request, event.response, (error?: any) => {
604
614
  if (lastTimer) clearTimeout(lastTimer);
615
+ if (timedOut) return;
605
616
  if (error) {
606
617
  event.response.off('finish', finish);
607
618
  reject(error);
@@ -610,19 +621,42 @@ export class HttpListener {
610
621
  }
611
622
  });
612
623
  } catch (error) {
624
+ event.response.off('finish', finish);
613
625
  reject(error);
614
626
  }
615
627
  }
616
628
 
617
629
  await next();
618
630
  });
631
+ inMiddleware = false;
619
632
  }
620
633
  }
621
634
 
635
+ if (event.sent) {
636
+ //a middleware ended the request, so we skip the controller but still trigger onResponse (e.g. for logging)
637
+ event.next('response', new HttpResponseEvent(event.injectorContext, event.request, event.response, undefined, resolved.routeConfig));
638
+ return;
639
+ }
640
+
622
641
  this.setRouteConfig(resolved.routeConfig, event.injectorContext.scope);
623
642
  event.routeFound(resolved.routeConfig, resolved.parameters);
624
643
  }
625
644
  } catch (error) {
645
+ if (error instanceof HttpError) {
646
+ if (event.request.throwErrorOnNotFound && error.httpCode === 404) {
647
+ throw error;
648
+ }
649
+ event.send(new JSONResponse({
650
+ message: error.message
651
+ }, error.httpCode).disableAutoSerializing());
652
+ return;
653
+ }
654
+ if (inMiddleware) {
655
+ //like a failing controller, an error of a middleware is a server error (#439)
656
+ this.logger.error('Middleware error', error);
657
+ event.send(new HtmlResponse('Internal error', 500));
658
+ return;
659
+ }
626
660
  this.logger.error('Could not resolve request', error);
627
661
  event.notFound();
628
662
  }
@@ -690,7 +724,7 @@ export class HttpListener {
690
724
  ? getClassName(event.route.action.controller) + '.' + event.route.action.methodName
691
725
  : event.route.action.fn.name;
692
726
 
693
- const frame = this.stopwatch ? this.stopwatch.start(stopWatchLabel, FrameCategory.httpController) : undefined;
727
+ const frame = this.stopwatch.start(stopWatchLabel, FrameCategory.httpController);
694
728
  try {
695
729
  let result: any;
696
730
  if (event.route.action.type === 'controller') {
@@ -702,7 +736,7 @@ export class HttpListener {
702
736
  }
703
737
 
704
738
  if (isElementStruct(result)) {
705
- const html = await getTemplateRender()(event.injectorContext.getRootInjector(), result, this.stopwatch ? this.stopwatch : undefined);
739
+ const html = await (await getTemplateRender())(event.injectorContext.getRootInjector(), result, this.stopwatch ? this.stopwatch : undefined);
706
740
  result = new HtmlResponse(html, 200).header('Content-Type', 'text/html; charset=utf-8');
707
741
  }
708
742
  if (result instanceof stream.Readable) {
@@ -716,7 +750,6 @@ export class HttpListener {
716
750
  responseEvent.controllerActionTime = Date.now() - start;
717
751
  event.next('response', responseEvent);
718
752
  } catch (error: any) {
719
- if (frame) frame.end();
720
753
  if (error instanceof HttpAccessDeniedError) {
721
754
  event.next('accessDenied', new HttpAccessDeniedEvent(event.injectorContext, event.request, event.response, event.route, error));
722
755
  } else {
@@ -725,7 +758,7 @@ export class HttpListener {
725
758
  event.next('controllerError', errorEvent);
726
759
  }
727
760
  } finally {
728
- if (frame) frame.end();
761
+ frame.end();
729
762
  }
730
763
  }
731
764
 
package/src/kernel.ts CHANGED
@@ -2,14 +2,7 @@ import { InjectorContext, Setter } from '@deepkit/injector';
2
2
  import { HttpRouter } from './router.js';
3
3
  import { EventDispatcher } from '@deepkit/event';
4
4
  import { LoggerInterface } from '@deepkit/logger';
5
- import {
6
- HttpRequest,
7
- HttpResponse,
8
- incomingMessageToHttpRequest,
9
- MemoryHttpResponse,
10
- RequestBuilder,
11
- serverResponseToHttpResponse,
12
- } from './model.js';
5
+ import { HttpRequest, HttpResponse, incomingMessageToHttpRequest, MemoryHttpResponse, RequestBuilder, serverResponseToHttpResponse } from './model.js';
13
6
  import { HttpError, HttpRequestEvent, HttpResultFormatter, httpWorkflow, JSONResponse } from './http.js';
14
7
  import { FrameCategory, Stopwatch } from '@deepkit/stopwatch';
15
8
  import { unlink } from 'fs';
@@ -43,7 +36,7 @@ export class HttpKernel {
43
36
  protected eventDispatcher: EventDispatcher,
44
37
  protected injectorContext: InjectorContext,
45
38
  protected logger: LoggerInterface,
46
- protected stopwatch?: Stopwatch,
39
+ protected stopwatch: Stopwatch,
47
40
  ) {
48
41
  this.setHttpRequest = injectorContext.setter(undefined, HttpRequest);
49
42
  this.setHttpResponse = injectorContext.setter(undefined, HttpResponse);
@@ -112,16 +105,12 @@ export class HttpKernel {
112
105
 
113
106
  req.throwErrorOnNotFound = options.throwOnNotFound || false;
114
107
 
115
- const frame = this.stopwatch ? this.stopwatch.start(req.method + ' ' + req.getUrl(), FrameCategory.http, true) : undefined;
116
- const workflow = httpWorkflow.create('start', this.eventDispatcher, httpInjectorContext, this.stopwatch);
108
+ const frame = this.stopwatch.start(req.method + ' ' + req.getUrl(), FrameCategory.http, true);
109
+ const workflow = httpWorkflow.create('start', this.eventDispatcher, this.stopwatch, httpInjectorContext);
117
110
 
118
111
  try {
119
- if (frame) {
120
- frame.data({ url: req.getUrl(), method: req.getMethod(), clientIp: req.getRemoteAddress() });
121
- await frame.run(() => workflow.apply('request', new HttpRequestEvent(httpInjectorContext, req, res)));
122
- } else {
123
- await workflow.apply('request', new HttpRequestEvent(httpInjectorContext, req, res));
124
- }
112
+ frame.data({ url: req.getUrl(), method: req.getMethod(), clientIp: req.getRemoteAddress() });
113
+ await frame.run(() => workflow.apply('request', new HttpRequestEvent(httpInjectorContext, req, res)));
125
114
  } catch (error: any) {
126
115
  if (!res.headersSent) {
127
116
  const resultFormatter = httpInjectorContext.get(HttpResultFormatter);
@@ -151,10 +140,8 @@ export class HttpKernel {
151
140
  });
152
141
  }
153
142
 
154
- if (frame) {
155
- frame.data({ responseStatus: res.statusCode });
156
- frame.end();
157
- }
143
+ frame.data({ responseStatus: res.statusCode });
144
+ frame.end();
158
145
  }
159
146
  }
160
147
  }
package/src/middleware.ts CHANGED
@@ -79,8 +79,8 @@ export class HttpMiddlewareApi {
79
79
  }
80
80
 
81
81
  /**
82
- * When the middleware does not respond (either calling next() or sending headers) withing <timeout> milliseconds,
83
- * automatically the next is executed and warning printed.
82
+ * When the middleware neither calls next() nor ends the response within <timeout> milliseconds,
83
+ * the request fails with HttpGatewayTimeoutError (504) and a warning is logged.
84
84
  */
85
85
  timeout(timeout: number) {
86
86
  this.t.timeout = timeout;
package/src/mime.ts ADDED
@@ -0,0 +1,141 @@
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 { extname } from 'path';
11
+
12
+ /**
13
+ * File extension to MIME type map of common web types.
14
+ * Add more types at runtime by setting `mimeTypes[extension] = type`.
15
+ */
16
+ export const mimeTypes: { [extension: string]: string } = {
17
+ // text
18
+ html: 'text/html',
19
+ htm: 'text/html',
20
+ css: 'text/css',
21
+ js: 'text/javascript',
22
+ mjs: 'text/javascript',
23
+ cjs: 'application/node',
24
+ txt: 'text/plain',
25
+ text: 'text/plain',
26
+ csv: 'text/csv',
27
+ tsv: 'text/tab-separated-values',
28
+ md: 'text/markdown',
29
+ markdown: 'text/markdown',
30
+ yaml: 'text/yaml',
31
+ yml: 'text/yaml',
32
+ ics: 'text/calendar',
33
+ vtt: 'text/vtt',
34
+ appcache: 'text/cache-manifest',
35
+ // application
36
+ json: 'application/json',
37
+ map: 'application/json',
38
+ jsonld: 'application/ld+json',
39
+ webmanifest: 'application/manifest+json',
40
+ xml: 'application/xml',
41
+ xsl: 'application/xml',
42
+ rtf: 'application/rtf',
43
+ wasm: 'application/wasm',
44
+ pdf: 'application/pdf',
45
+ zip: 'application/zip',
46
+ gz: 'application/gzip',
47
+ tar: 'application/x-tar',
48
+ bz2: 'application/x-bzip2',
49
+ '7z': 'application/x-7z-compressed',
50
+ rar: 'application/vnd.rar',
51
+ xz: 'application/x-xz',
52
+ bin: 'application/octet-stream',
53
+ epub: 'application/epub+zip',
54
+ doc: 'application/msword',
55
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
56
+ xls: 'application/vnd.ms-excel',
57
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
58
+ ppt: 'application/vnd.ms-powerpoint',
59
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
60
+ odt: 'application/vnd.oasis.opendocument.text',
61
+ ods: 'application/vnd.oasis.opendocument.spreadsheet',
62
+ odp: 'application/vnd.oasis.opendocument.presentation',
63
+ m3u8: 'application/vnd.apple.mpegurl',
64
+ mpd: 'application/dash+xml',
65
+ // images
66
+ png: 'image/png',
67
+ apng: 'image/apng',
68
+ jpg: 'image/jpeg',
69
+ jpeg: 'image/jpeg',
70
+ jpe: 'image/jpeg',
71
+ gif: 'image/gif',
72
+ webp: 'image/webp',
73
+ avif: 'image/avif',
74
+ svg: 'image/svg+xml',
75
+ svgz: 'image/svg+xml',
76
+ ico: 'image/vnd.microsoft.icon',
77
+ bmp: 'image/bmp',
78
+ tif: 'image/tiff',
79
+ tiff: 'image/tiff',
80
+ heic: 'image/heic',
81
+ heif: 'image/heif',
82
+ jxl: 'image/jxl',
83
+ psd: 'image/vnd.adobe.photoshop',
84
+ // fonts
85
+ woff: 'font/woff',
86
+ woff2: 'font/woff2',
87
+ ttf: 'font/ttf',
88
+ otf: 'font/otf',
89
+ eot: 'application/vnd.ms-fontobject',
90
+ // audio
91
+ mp3: 'audio/mpeg',
92
+ m4a: 'audio/mp4',
93
+ aac: 'audio/aac',
94
+ oga: 'audio/ogg',
95
+ ogg: 'audio/ogg',
96
+ opus: 'audio/ogg',
97
+ wav: 'audio/wav',
98
+ flac: 'audio/x-flac',
99
+ // video
100
+ mp4: 'video/mp4',
101
+ m4v: 'video/x-m4v',
102
+ webm: 'video/webm',
103
+ ogv: 'video/ogg',
104
+ mov: 'video/quicktime',
105
+ avi: 'video/x-msvideo',
106
+ mkv: 'video/x-matroska',
107
+ mpeg: 'video/mpeg',
108
+ mpg: 'video/mpeg',
109
+ '3gp': 'video/3gpp',
110
+ ts: 'video/mp2t',
111
+ // 3d models
112
+ glb: 'model/gltf-binary',
113
+ gltf: 'model/gltf+json',
114
+ obj: 'model/obj',
115
+ stl: 'model/stl',
116
+ usdz: 'model/vnd.usdz+zip',
117
+ };
118
+
119
+ /**
120
+ * Returns the MIME type for a file path, file name, or extension (with or without leading dot).
121
+ * Returns undefined for unknown extensions.
122
+ *
123
+ * `getMimeType('/assets/logo.svg')` → `image/svg+xml`
124
+ */
125
+ export function getMimeType(path: string): string | undefined {
126
+ const extension = extname('x.' + path)
127
+ .slice(1)
128
+ .toLowerCase();
129
+ return Object.prototype.hasOwnProperty.call(mimeTypes, extension) ? mimeTypes[extension] : undefined;
130
+ }
131
+
132
+ /**
133
+ * Returns the value for a Content-Type header, including `charset=utf-8` for text types.
134
+ * Returns undefined for unknown extensions.
135
+ */
136
+ export function getContentType(path: string): string | undefined {
137
+ const type = getMimeType(path);
138
+ if (!type) return;
139
+ const isText = type.startsWith('text/') || type === 'application/json' || type === 'application/manifest+json';
140
+ return isText ? type + '; charset=utf-8' : type;
141
+ }
package/src/model.ts CHANGED
@@ -312,13 +312,21 @@ export class RequestBuilder {
312
312
  }, this.contentBuffer);
313
313
  }
314
314
 
315
+ /**
316
+ * Replaces all headers. Names are lowercased like Node does for incoming requests.
317
+ */
315
318
  headers(headers: { [name: string]: string }): this {
316
- this._headers = headers;
319
+ this._headers = {};
320
+ for (const [name, value] of Object.entries(headers)) this.header(name, value);
317
321
  return this;
318
322
  }
319
323
 
324
+ /**
325
+ * Sets a header. The name is lowercased like Node does for incoming requests,
326
+ * so `header('Authorization', ...)` is found by `authorization: HttpHeader<string>`.
327
+ */
320
328
  header(name: string, value: string | number): this {
321
- this._headers[name] = String(value);
329
+ this._headers[name.toLowerCase()] = String(value);
322
330
  return this;
323
331
  }
324
332
 
package/src/module.ts CHANGED
@@ -32,8 +32,9 @@ export class HttpModule extends createModuleClass({
32
32
  HttpResultFormatter,
33
33
  HttpRouterRegistry,
34
34
  HttpRouterFilterResolver,
35
- { provide: HttpResponse, scope: 'http' },
36
- { provide: HttpRequest, scope: 'http' },
35
+ { provide: InjectorContext, useValue: undefined, scope: 'http' },
36
+ { provide: HttpResponse, useValue: undefined, scope: 'http' },
37
+ { provide: HttpRequest, useValue: undefined, scope: 'http' },
37
38
  { provide: RouteConfig, useValue: undefined, scope: 'http' },
38
39
  { provide: Logger, useValue: new Logger([new ConsoleTransport()]) },
39
40
  ],
@@ -49,6 +50,7 @@ export class HttpModule extends createModuleClass({
49
50
  HttpKernel,
50
51
  HttpResultFormatter,
51
52
  HttpRouterFilterResolver,
53
+ InjectorContext,
52
54
  HttpResponse,
53
55
  HttpRequest,
54
56
  HttpControllers,
@@ -0,0 +1,228 @@
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
+
11
+ /**
12
+ * Query string parser compatible with the default behavior of `qs.parse`.
13
+ *
14
+ * Supports nested objects (`a[b]=1`), arrays (`a[]=1`, `a[0]=1`) and duplicate keys (`a=1&a=2` becomes an array).
15
+ *
16
+ * Limits:
17
+ * - at most 1000 parameters are parsed, the rest is ignored
18
+ * - at most 5 levels of nesting, the remainder is used as literal key (`a[b][c][d][e][f][g]` → `a.b.c.d.e.f['[g]']`)
19
+ * - array indices above 20 create object keys instead of sparse arrays (`a[100]=1` → `{a: {100: '1'}}`)
20
+ *
21
+ * Keys that exist on Object.prototype (`__proto__`, `constructor`, `toString`, `hasOwnProperty`, ...) are dropped.
22
+ */
23
+ const parameterLimit = 1000;
24
+ const depthLimit = 5;
25
+ const arrayLimit = 20;
26
+
27
+ const has = Object.prototype.hasOwnProperty;
28
+ const isArray = Array.isArray;
29
+
30
+ function decode(str: string): string {
31
+ if (str.indexOf('+') !== -1) str = str.replace(/\+/g, ' ');
32
+ if (str.indexOf('%') === -1) return str;
33
+ try {
34
+ return decodeURIComponent(str);
35
+ } catch {
36
+ return str;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Query strings without brackets, the common case: `a=1&b=2&a=3` → `{a: ['1', '3'], b: '2'}`.
42
+ * Same result as the general parser, without its nesting and compaction steps.
43
+ */
44
+ function parseFlat(str: string): Record<string, any> {
45
+ const obj: Record<string, any> = {};
46
+ for (const part of str.split('&', parameterLimit)) {
47
+ const pos = part.indexOf('=');
48
+ const key = decode(pos === -1 ? part : part.slice(0, pos));
49
+ if (!key || has.call(Object.prototype, key)) continue;
50
+ const val = pos === -1 ? '' : decode(part.slice(pos + 1));
51
+ obj[key] = has.call(obj, key) ? ([] as any[]).concat(obj[key], val) : val;
52
+ }
53
+ return obj;
54
+ }
55
+
56
+ const nesting = /[[\]]|%5[bd]/i;
57
+
58
+ function parseValues(str: string): Record<string, any> {
59
+ const obj: Record<string, any> = Object.create(null);
60
+ const parts = str.replace(/%5B/gi, '[').replace(/%5D/gi, ']').split('&', parameterLimit);
61
+
62
+ for (const part of parts) {
63
+ const bracketEqualsPos = part.indexOf(']=');
64
+ const pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1;
65
+
66
+ const key = decode(pos === -1 ? part : part.slice(0, pos));
67
+ const val = pos === -1 ? '' : decode(part.slice(pos + 1));
68
+
69
+ obj[key] = has.call(obj, key) ? ([] as any[]).concat(obj[key], val) : val;
70
+ }
71
+
72
+ return obj;
73
+ }
74
+
75
+ function parseObject(chain: string[], val: any): any {
76
+ let leaf = val;
77
+
78
+ for (let i = chain.length - 1; i >= 0; --i) {
79
+ let obj: any;
80
+ const root = chain[i];
81
+
82
+ if (root === '[]') {
83
+ obj = ([] as any[]).concat(leaf);
84
+ } else {
85
+ obj = {};
86
+ const cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root;
87
+ const index = parseInt(cleanRoot, 10);
88
+ if (
89
+ !isNaN(index) &&
90
+ root !== cleanRoot &&
91
+ String(index) === cleanRoot &&
92
+ index >= 0 &&
93
+ index <= arrayLimit
94
+ ) {
95
+ obj = [];
96
+ obj[index] = leaf;
97
+ } else if (cleanRoot !== '__proto__') {
98
+ obj[cleanRoot] = leaf;
99
+ }
100
+ }
101
+
102
+ leaf = obj;
103
+ }
104
+
105
+ return leaf;
106
+ }
107
+
108
+ function parseKeys(key: string, val: any): any {
109
+ if (!key) return;
110
+
111
+ const brackets = /(\[[^[\]]*])/;
112
+ const child = /(\[[^[\]]*])/g;
113
+
114
+ let segment = brackets.exec(key);
115
+ const parent = segment ? key.slice(0, segment.index) : key;
116
+
117
+ const keys: string[] = [];
118
+ if (parent) {
119
+ if (has.call(Object.prototype, parent)) return;
120
+ keys.push(parent);
121
+ }
122
+
123
+ let i = 0;
124
+ while ((segment = child.exec(key)) !== null && i < depthLimit) {
125
+ i += 1;
126
+ if (has.call(Object.prototype, segment[1].slice(1, -1))) return;
127
+ keys.push(segment[1]);
128
+ }
129
+
130
+ // depth limit reached, the remainder is used as literal key
131
+ if (segment) keys.push('[' + key.slice(segment.index) + ']');
132
+
133
+ return parseObject(keys, val);
134
+ }
135
+
136
+ function arrayToObject(source: any[]): Record<string, any> {
137
+ const obj: Record<string, any> = {};
138
+ for (let i = 0; i < source.length; ++i) {
139
+ if (source[i] !== undefined) obj[i] = source[i];
140
+ }
141
+ return obj;
142
+ }
143
+
144
+ function merge(target: any, source: any): any {
145
+ if (!source) return target;
146
+
147
+ if (typeof source !== 'object') {
148
+ if (isArray(target)) {
149
+ target.push(source);
150
+ } else if (target && typeof target === 'object') {
151
+ if (!has.call(Object.prototype, source)) target[source] = true;
152
+ } else {
153
+ return [target, source];
154
+ }
155
+ return target;
156
+ }
157
+
158
+ if (!target || typeof target !== 'object') {
159
+ return [target].concat(source);
160
+ }
161
+
162
+ if (isArray(target) && isArray(source)) {
163
+ source.forEach((item, i) => {
164
+ if (has.call(target, i)) {
165
+ const targetItem = target[i];
166
+ if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') {
167
+ target[i] = merge(targetItem, item);
168
+ } else {
169
+ target.push(item);
170
+ }
171
+ } else {
172
+ target[i] = item;
173
+ }
174
+ });
175
+ return target;
176
+ }
177
+
178
+ const mergeTarget = isArray(target) && !isArray(source) ? arrayToObject(target) : target;
179
+ for (const key of Object.keys(source)) {
180
+ mergeTarget[key] = has.call(mergeTarget, key) ? merge(mergeTarget[key], source[key]) : source[key];
181
+ }
182
+ return mergeTarget;
183
+ }
184
+
185
+ /**
186
+ * Removes holes from (sparse) arrays, e.g. `a[1]=x` results in `['x']`, not `[undefined, 'x']`.
187
+ */
188
+ function compact(value: Record<string, any>): Record<string, any> {
189
+ const queue: { obj: any; prop: string }[] = [{ obj: { o: value }, prop: 'o' }];
190
+ const refs: any[] = [];
191
+
192
+ for (let i = 0; i < queue.length; ++i) {
193
+ const obj = queue[i].obj[queue[i].prop];
194
+ for (const key of Object.keys(obj)) {
195
+ const val = obj[key];
196
+ if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) {
197
+ queue.push({ obj, prop: key });
198
+ refs.push(val);
199
+ }
200
+ }
201
+ }
202
+
203
+ while (queue.length > 1) {
204
+ const item = queue.pop()!;
205
+ const obj = item.obj[item.prop];
206
+ if (isArray(obj)) item.obj[item.prop] = obj.filter(v => v !== undefined);
207
+ }
208
+
209
+ return value;
210
+ }
211
+
212
+ /**
213
+ * Parses a query string (without leading `?`) into an object.
214
+ *
215
+ * `a=1&b[c]=2&d[]=3&d[]=4` → `{a: '1', b: {c: '2'}, d: ['3', '4']}`
216
+ */
217
+ export function parseQueryString(str: string): Record<string, any> {
218
+ if (!str) return {};
219
+ if (!nesting.test(str)) return parseFlat(str);
220
+
221
+ const values = parseValues(str);
222
+ let obj: Record<string, any> = {};
223
+ for (const key of Object.keys(values)) {
224
+ obj = merge(obj, parseKeys(key, values[key]));
225
+ }
226
+
227
+ return compact(obj);
228
+ }
@@ -19,7 +19,7 @@ import {
19
19
  } from '@deepkit/type';
20
20
  import { BodyValidationError, createRequestWithCachedBody, getRegExp, HttpRequest, ValidatedBody } from './model.js';
21
21
  import { getRouteActionLabel, RouteConfig, UploadedFile, UploadedFileSymbol } from './router.js';
22
- import qs from 'qs';
22
+ import { parseQueryString } from './query-string.js';
23
23
  import formidable, { Fields, Files, Options } from 'formidable';
24
24
  import { HttpParserOptions } from './module.config.js';
25
25
  import type IncomingForm from 'formidable/Formidable.js';
@@ -231,10 +231,10 @@ export function buildRequestParser(parseOptions: HttpParserOptions, parameters:
231
231
  routeConfig
232
232
  });
233
233
  compiler.context.set('ValidationError', ValidationError);
234
- compiler.context.set('qs', qs);
234
+ compiler.context.set('parseQueryString', parseQueryString);
235
235
 
236
236
  let needsQueryString = !!params.find(v => v.query || v.queries || v.requestParser);
237
- const query = needsQueryString ? '_qPosition === -1 ? {} : qs.parse(_url.substr(_qPosition + 1))' : '{}';
237
+ const query = needsQueryString ? '_qPosition === -1 ? {} : parseQueryString(_url.substr(_qPosition + 1))' : '{}';
238
238
 
239
239
  const regexVar = compiler.reserveVariable('regex', new RegExp('^' + pathRegex + '$'));
240
240
 
@@ -329,7 +329,9 @@ export function getRequestParserCodeForParameters(
329
329
 
330
330
  const queryPath = parameter.typePath === undefined && !parameter.queries ? parameter.parameter.name : parameter.typePath;
331
331
  const accessor = queryPath ? `['` + (queryPath.replace(/\./g, `']['`)) + `']` : '';
332
- const queryAccessor = parameter.header ? `_headers${accessor}` : queryPath ? `_query${accessor}` : '_query';
332
+ // header names are case-insensitive, Node lowercases them in request.headers
333
+ const headerAccessor = queryPath ? `_headers[${JSON.stringify(queryPath.toLowerCase())}]` : '_headers';
334
+ const queryAccessor = parameter.header ? headerAccessor : queryPath ? `_query${accessor}` : '_query';
333
335
 
334
336
  if (isOptional(parameter.parameter.parameter) || hasDefaultValue(parameter.parameter.parameter)) {
335
337
  setParameters.push(`parameters.${parameter.parameter.name} = ${queryAccessor} === undefined ? undefined : ${converterVar}(${queryAccessor}, {loosely: true});`);