@travetto/web 6.0.0-rc.2

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 (50) hide show
  1. package/README.md +734 -0
  2. package/__index__.ts +44 -0
  3. package/package.json +66 -0
  4. package/src/common/global.ts +30 -0
  5. package/src/config.ts +18 -0
  6. package/src/context.ts +49 -0
  7. package/src/decorator/common.ts +87 -0
  8. package/src/decorator/controller.ts +13 -0
  9. package/src/decorator/endpoint.ts +102 -0
  10. package/src/decorator/param.ts +64 -0
  11. package/src/interceptor/accept.ts +70 -0
  12. package/src/interceptor/body-parse.ts +123 -0
  13. package/src/interceptor/compress.ts +119 -0
  14. package/src/interceptor/context.ts +23 -0
  15. package/src/interceptor/cookies.ts +97 -0
  16. package/src/interceptor/cors.ts +94 -0
  17. package/src/interceptor/decompress.ts +91 -0
  18. package/src/interceptor/etag.ts +99 -0
  19. package/src/interceptor/logging.ts +71 -0
  20. package/src/interceptor/respond.ts +26 -0
  21. package/src/interceptor/response-cache.ts +47 -0
  22. package/src/interceptor/trust-proxy.ts +53 -0
  23. package/src/registry/controller.ts +288 -0
  24. package/src/registry/types.ts +229 -0
  25. package/src/registry/visitor.ts +52 -0
  26. package/src/router/base.ts +67 -0
  27. package/src/router/standard.ts +59 -0
  28. package/src/types/cookie.ts +18 -0
  29. package/src/types/core.ts +33 -0
  30. package/src/types/dispatch.ts +23 -0
  31. package/src/types/error.ts +10 -0
  32. package/src/types/filter.ts +7 -0
  33. package/src/types/headers.ts +108 -0
  34. package/src/types/interceptor.ts +54 -0
  35. package/src/types/message.ts +33 -0
  36. package/src/types/request.ts +22 -0
  37. package/src/types/response.ts +20 -0
  38. package/src/util/body.ts +220 -0
  39. package/src/util/common.ts +142 -0
  40. package/src/util/cookie.ts +145 -0
  41. package/src/util/endpoint.ts +277 -0
  42. package/src/util/mime.ts +36 -0
  43. package/src/util/net.ts +61 -0
  44. package/support/test/dispatch-util.ts +90 -0
  45. package/support/test/dispatcher.ts +15 -0
  46. package/support/test/suite/base.ts +61 -0
  47. package/support/test/suite/controller.ts +103 -0
  48. package/support/test/suite/schema.ts +275 -0
  49. package/support/test/suite/standard.ts +178 -0
  50. package/support/transformer.web.ts +207 -0
package/__index__.ts ADDED
@@ -0,0 +1,44 @@
1
+ export * from './src/types/filter.ts';
2
+ export * from './src/types/request.ts';
3
+ export * from './src/types/response.ts';
4
+ export * from './src/types/core.ts';
5
+ export * from './src/types/dispatch.ts';
6
+ export * from './src/types/error.ts';
7
+ export * from './src/types/cookie.ts';
8
+ export * from './src/types/interceptor.ts';
9
+ export * from './src/types/headers.ts';
10
+
11
+ export * from './src/context.ts';
12
+ export * from './src/config.ts';
13
+
14
+ export * from './src/router/standard.ts';
15
+ export * from './src/router/base.ts';
16
+
17
+ export * from './src/decorator/common.ts';
18
+ export * from './src/decorator/controller.ts';
19
+ export * from './src/decorator/param.ts';
20
+ export * from './src/decorator/endpoint.ts';
21
+
22
+ export * from './src/registry/controller.ts';
23
+ export * from './src/registry/visitor.ts';
24
+ export * from './src/registry/types.ts';
25
+
26
+ export * from './src/interceptor/accept.ts';
27
+ export * from './src/interceptor/body-parse.ts';
28
+ export * from './src/interceptor/cors.ts';
29
+ export * from './src/interceptor/cookies.ts';
30
+ export * from './src/interceptor/compress.ts';
31
+ export * from './src/interceptor/context.ts';
32
+ export * from './src/interceptor/decompress.ts';
33
+ export * from './src/interceptor/etag.ts';
34
+ export * from './src/interceptor/response-cache.ts';
35
+ export * from './src/interceptor/logging.ts';
36
+ export * from './src/interceptor/respond.ts';
37
+ export * from './src/interceptor/trust-proxy.ts';
38
+
39
+ export * from './src/util/body.ts';
40
+ export * from './src/util/endpoint.ts';
41
+ export * from './src/util/mime.ts';
42
+ export * from './src/util/cookie.ts';
43
+ export * from './src/util/common.ts';
44
+ export * from './src/util/net.ts';
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@travetto/web",
3
+ "version": "6.0.0-rc.2",
4
+ "description": "Declarative api for Web Applications with support for the dependency injection.",
5
+ "keywords": [
6
+ "web",
7
+ "dependency-injection",
8
+ "decorators",
9
+ "travetto",
10
+ "typescript"
11
+ ],
12
+ "homepage": "https://travetto.io",
13
+ "license": "MIT",
14
+ "author": {
15
+ "email": "travetto.framework@gmail.com",
16
+ "name": "Travetto Framework"
17
+ },
18
+ "files": [
19
+ "__index__.ts",
20
+ "src",
21
+ "support"
22
+ ],
23
+ "main": "__index__.ts",
24
+ "repository": {
25
+ "url": "git+https://github.com/travetto/travetto.git",
26
+ "directory": "module/web"
27
+ },
28
+ "dependencies": {
29
+ "@travetto/config": "^6.0.0-rc.2",
30
+ "@travetto/context": "^6.0.0-rc.2",
31
+ "@travetto/di": "^6.0.0-rc.2",
32
+ "@travetto/registry": "^6.0.0-rc.2",
33
+ "@travetto/runtime": "^6.0.0-rc.2",
34
+ "@travetto/schema": "^6.0.0-rc.2",
35
+ "@types/fresh": "^0.5.2",
36
+ "@types/keygrip": "^1.0.6",
37
+ "@types/negotiator": "^0.6.3",
38
+ "find-my-way": "^9.3.0",
39
+ "fresh": "^0.5.2",
40
+ "iconv-lite": "^0.6.3",
41
+ "keygrip": "^1.1.0",
42
+ "negotiator": "^1.0.0"
43
+ },
44
+ "peerDependencies": {
45
+ "@travetto/cli": "^6.0.0-rc.2",
46
+ "@travetto/test": "^6.0.0-rc.2",
47
+ "@travetto/transformer": "^6.0.0-rc.3"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@travetto/transformer": {
51
+ "optional": true
52
+ },
53
+ "@travetto/cli": {
54
+ "optional": true
55
+ },
56
+ "@travetto/test": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "travetto": {
61
+ "displayName": "Web API"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ }
66
+ }
@@ -0,0 +1,30 @@
1
+ import { DependencyRegistry } from '@travetto/di';
2
+ import { Runtime } from '@travetto/runtime';
3
+
4
+ import { Controller } from '../decorator/controller.ts';
5
+ import { ConditionalRegister, ConfigureInterceptor, Undocumented } from '../decorator/common.ts';
6
+ import { Get, Options } from '../decorator/endpoint.ts';
7
+ import { WebConfig } from '../config.ts';
8
+ import { LoggingInterceptor, } from '../interceptor/logging.ts';
9
+
10
+ @Undocumented()
11
+ @Controller('/')
12
+ @ConfigureInterceptor(LoggingInterceptor, { applies: false })
13
+ export class GlobalHandler {
14
+
15
+ @Get('')
16
+ @ConditionalRegister(async () => {
17
+ const config = await DependencyRegistry.getInstance(WebConfig);
18
+ return config.defaultMessage;
19
+ })
20
+ message(): { module: string, version: string, env?: string } {
21
+ return {
22
+ module: Runtime.main.name,
23
+ version: Runtime.main.version,
24
+ env: Runtime.env
25
+ };
26
+ }
27
+
28
+ @Options('*all')
29
+ options(): void { }
30
+ }
package/src/config.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Config, EnvVar } from '@travetto/config';
2
+
3
+ /**
4
+ * Web configuration
5
+ */
6
+ @Config('web')
7
+ export class WebConfig {
8
+ /**
9
+ * Should the app provide the global endpoint for app info
10
+ */
11
+ @EnvVar('WEB_DEFAULT_MESSAGE')
12
+ defaultMessage = true;
13
+ /**
14
+ * Base Url
15
+ */
16
+ @EnvVar('WEB_BASE_URL')
17
+ baseUrl?: string;
18
+ }
package/src/context.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { AsyncContextValue, AsyncContext } from '@travetto/context';
2
+ import { Inject, Injectable } from '@travetto/di';
3
+ import { AppError, castTo, Class, toConcrete } from '@travetto/runtime';
4
+
5
+ import { WebRequest } from './types/request.ts';
6
+
7
+ /**
8
+ * Shared Async Context, powering the @ContextParams
9
+ */
10
+ @Injectable()
11
+ export class WebAsyncContext {
12
+
13
+ #request = new AsyncContextValue<WebRequest>(this);
14
+ #byType = new Map<string, () => unknown>();
15
+
16
+ @Inject()
17
+ context: AsyncContext;
18
+
19
+ get request(): WebRequest {
20
+ return this.#request.get()!;
21
+ }
22
+
23
+ postConstruct(): void {
24
+ this.registerSource(toConcrete<WebRequest>(), () => this.#request.get());
25
+ }
26
+
27
+ withContext<T>(request: WebRequest, next: () => Promise<T>): Promise<T> {
28
+ return this.context.run(() => {
29
+ this.#request.set(request);
30
+ return next();
31
+ });
32
+ }
33
+
34
+ registerSource<T>(cls: Class<T>, provider: () => T): void {
35
+ this.#byType.set(cls.Ⲑid, provider);
36
+ }
37
+
38
+ getSource<T>(cls: Class<T>): () => T {
39
+ const item = this.#byType.get(cls.Ⲑid);
40
+ if (!item) {
41
+ throw new AppError('Unknown type for web context');
42
+ }
43
+ return castTo(item);
44
+ }
45
+
46
+ getValue<T>(cls: Class<T>): T {
47
+ return this.getSource(cls)();
48
+ }
49
+ }
@@ -0,0 +1,87 @@
1
+ import { asConstructable, castTo, Class, TimeSpan } from '@travetto/runtime';
2
+
3
+ import { ControllerRegistry } from '../registry/controller.ts';
4
+ import { EndpointConfig, ControllerConfig, DescribableConfig, EndpointDecorator, EndpointFunctionDescriptor } from '../registry/types.ts';
5
+ import { AcceptInterceptor } from '../interceptor/accept.ts';
6
+ import { WebInterceptor } from '../types/interceptor.ts';
7
+ import { WebCommonUtil, CacheControlFlag } from '../util/common.ts';
8
+
9
+ function register(config: Partial<EndpointConfig | ControllerConfig>): EndpointDecorator {
10
+ return function <T>(target: T | Class<T>, property?: string, descriptor?: EndpointFunctionDescriptor) {
11
+ if (descriptor) {
12
+ return ControllerRegistry.registerPendingEndpoint(asConstructable(target).constructor, descriptor, config);
13
+ } else {
14
+ return ControllerRegistry.registerPending(castTo(target), config);
15
+ }
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Decorator used to add description metadata to a class or method
21
+ * @param desc The describe config
22
+ */
23
+ export function Describe(desc: DescribableConfig): EndpointDecorator { return register(desc); }
24
+
25
+ /**
26
+ * Marks a class/endpoint as being undocumented
27
+ */
28
+ export function Undocumented(): EndpointDecorator { return register({ documented: false }); }
29
+
30
+ /**
31
+ * Set response headers on success
32
+ * @param headers The response headers to set
33
+ */
34
+ export function SetHeaders(headers: EndpointConfig['responseHeaders']): EndpointDecorator {
35
+ return register({ responseHeaders: headers });
36
+ }
37
+
38
+ /**
39
+ * Specifies content type for response
40
+ */
41
+ export function Produces(mime: string): EndpointDecorator { return SetHeaders({ 'Content-Type': mime }); }
42
+
43
+ /**
44
+ * Specifies if endpoint should be conditional
45
+ */
46
+ export function ConditionalRegister(handler: () => (boolean | Promise<boolean>)): EndpointDecorator {
47
+ return register({ conditional: handler });
48
+ }
49
+
50
+ /**
51
+ * Set the max-age of a response based on the config
52
+ * @param value The value for the duration
53
+ * @param unit The unit of measurement
54
+ */
55
+ export function CacheControl(value: number | TimeSpan, flags: CacheControlFlag[] = []): EndpointDecorator {
56
+ return SetHeaders({ 'Cache-Control': WebCommonUtil.getCacheControlValue(value, flags) });
57
+ }
58
+
59
+ /**
60
+ * Disable cache control, ensuring endpoint will not cache
61
+ */
62
+ export const DisableCacheControl = (): EndpointDecorator => CacheControl(0);
63
+
64
+ /**
65
+ * Define an endpoint to support specific input types
66
+ * @param types The list of mime types to allow/deny
67
+ */
68
+ export function Accepts(types: [string, ...string[]]): EndpointDecorator {
69
+ return ControllerRegistry.createInterceptorConfigDecorator(
70
+ AcceptInterceptor,
71
+ { types, applies: true },
72
+ { responseHeaders: { accepts: types.join(', ') } }
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Allows for configuring interceptor-level support at an endpoint or controller level
78
+ */
79
+ export const ConfigureInterceptor =
80
+ ControllerRegistry.createInterceptorConfigDecorator.bind(ControllerRegistry);
81
+
82
+ /**
83
+ * Registers an interceptor exclusion filter
84
+ */
85
+ export function ExcludeInterceptors(interceptorExclude: (val: WebInterceptor) => boolean): EndpointDecorator {
86
+ return register({ interceptorExclude });
87
+ };
@@ -0,0 +1,13 @@
1
+ import { Class } from '@travetto/runtime';
2
+ import { ControllerRegistry } from '../registry/controller.ts';
3
+
4
+ /**
5
+ * Decorator to register a new web controller
6
+ * @augments `@travetto/di:Injectable`
7
+ * @augments `@travetto/web:Controller`
8
+ */
9
+ export function Controller(path: string) {
10
+ return function <T>(target: Class<T>): void {
11
+ ControllerRegistry.registerPending(target, { basePath: path, class: target, });
12
+ };
13
+ }
@@ -0,0 +1,102 @@
1
+ import { asConstructable } from '@travetto/runtime';
2
+
3
+ import { ControllerRegistry } from '../registry/controller.ts';
4
+ import { EndpointConfig, EndpointFunctionDescriptor, EndpointIOType } from '../registry/types.ts';
5
+ import { HTTP_METHODS, HttpMethod } from '../types/core.ts';
6
+
7
+ type EndpointFunctionDecorator = <T>(target: T, prop: symbol | string, descriptor: EndpointFunctionDescriptor) => EndpointFunctionDescriptor;
8
+
9
+ type EndpointDecConfig = Partial<EndpointConfig> & { path: string };
10
+
11
+ /**
12
+ * Generic Endpoint Decorator
13
+ */
14
+ export function Endpoint(config: EndpointDecConfig): EndpointFunctionDecorator {
15
+ return function <T>(target: T, prop: symbol | string, descriptor: EndpointFunctionDescriptor): EndpointFunctionDescriptor {
16
+ const result = ControllerRegistry.registerPendingEndpoint(
17
+ asConstructable(target).constructor, descriptor, config
18
+ );
19
+ return result;
20
+ };
21
+ }
22
+
23
+
24
+ function HttpEndpoint(method: HttpMethod, path: string): EndpointFunctionDecorator {
25
+ const { body: allowsBody, cacheable, emptyStatusCode } = HTTP_METHODS[method];
26
+ return Endpoint({
27
+ path,
28
+ allowsBody,
29
+ cacheable,
30
+ httpMethod: method,
31
+ responseFinalizer: v => {
32
+ v.context.httpStatusCode ??= (v.body === null || v.body === undefined || v.body === '') ? emptyStatusCode : 200;
33
+ return v;
34
+ }
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Registers GET requests
40
+ * @param path The endpoint path for the request
41
+ * @augments `@travetto/web:Endpoint`
42
+ */
43
+ export function Get(path = '/'): EndpointFunctionDecorator { return HttpEndpoint('GET', path); }
44
+ /**
45
+ * Registers POST requests
46
+ * @param path The endpoint path for the request
47
+ * @augments `@travetto/web:HttpRequestBody`
48
+ * @augments `@travetto/web:Endpoint`
49
+ */
50
+ export function Post(path = '/'): EndpointFunctionDecorator { return HttpEndpoint('POST', path); }
51
+ /**
52
+ * Registers PUT requests
53
+ * @param path The endpoint path for the request
54
+ * @augments `@travetto/web:HttpRequestBody`
55
+ * @augments `@travetto/web:Endpoint`
56
+ */
57
+ export function Put(path = '/'): EndpointFunctionDecorator { return HttpEndpoint('PUT', path); }
58
+ /**
59
+ * Registers PATCH requests
60
+ * @param path The endpoint path for the request
61
+ * @augments `@travetto/web:HttpRequestBody`
62
+ * @augments `@travetto/web:Endpoint`
63
+ */
64
+ export function Patch(path = '/'): EndpointFunctionDecorator { return HttpEndpoint('PATCH', path); }
65
+ /**
66
+ * Registers DELETE requests
67
+ * @param path The endpoint path for the request
68
+ * @augments `@travetto/web:Endpoint`
69
+ */
70
+ export function Delete(path = '/'): EndpointFunctionDecorator { return HttpEndpoint('DELETE', path); }
71
+ /**
72
+ * Registers HEAD requests
73
+ * @param path The endpoint path for the request
74
+ * @augments `@travetto/web:Endpoint`
75
+ */
76
+ export function Head(path = '/'): EndpointFunctionDecorator { return HttpEndpoint('HEAD', path); }
77
+ /**
78
+ * Registers OPTIONS requests
79
+ * @param path The endpoint path for the request
80
+ * @augments `@travetto/web:Endpoint`
81
+ */
82
+ export function Options(path = '/'): EndpointFunctionDecorator { return HttpEndpoint('OPTIONS', path); }
83
+
84
+ /**
85
+ * Defines the response type of the endpoint
86
+ * @param responseType The desired response mime type
87
+ */
88
+ export function ResponseType(responseType: EndpointIOType): EndpointFunctionDecorator {
89
+ return function <T>(target: T, property: string | symbol, descriptor: EndpointFunctionDescriptor) {
90
+ return ControllerRegistry.registerPendingEndpoint(asConstructable(target).constructor, descriptor, { responseType });
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Defines the supported request body type
96
+ * @param requestType The type of the request body
97
+ */
98
+ export function RequestType(requestType: EndpointIOType): EndpointFunctionDecorator {
99
+ return function <T>(target: T, property: string | symbol, descriptor: EndpointFunctionDescriptor) {
100
+ return ControllerRegistry.registerPendingEndpoint(asConstructable(target).constructor, descriptor, { requestType });
101
+ };
102
+ }
@@ -0,0 +1,64 @@
1
+ import { asConstructable, Class, ClassInstance } from '@travetto/runtime';
2
+
3
+ import { ControllerRegistry } from '../registry/controller.ts';
4
+ import { EndpointParamConfig } from '../registry/types.ts';
5
+
6
+ type ParamDecorator = (target: ClassInstance, propertyKey: string | symbol, idx: number) => void;
7
+
8
+ /**
9
+ * Get the param configuration
10
+ * @param location The location of the parameter
11
+ * @param extra Any additional configuration for the param config
12
+ */
13
+ export const paramConfig = (location: EndpointParamConfig['location'], extra: string | Partial<EndpointParamConfig>): EndpointParamConfig => ({
14
+ location,
15
+ ...((typeof extra === 'string' ? { name: extra } : extra))
16
+ });
17
+
18
+ /**
19
+ * Define a parameter
20
+ * @param location The location of the parameter
21
+ * @param extra Any extra configuration for the param
22
+ * @augments `@travetto/web:Param`
23
+ */
24
+ export function Param(location: EndpointParamConfig['location'], extra: string | Partial<EndpointParamConfig>): ParamDecorator {
25
+ const param = paramConfig(location, extra);
26
+ return (target: ClassInstance, propertyKey: string | symbol, idx: number): void => {
27
+ const handler = target.constructor.prototype[propertyKey];
28
+ ControllerRegistry.registerEndpointParameter(target.constructor, handler, param, idx);
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Define a Path param
34
+ * @param param The param configuration or name
35
+ * @augments `@travetto/web:Param`
36
+ */
37
+ export function PathParam(param: string | Partial<EndpointParamConfig> = {}): ParamDecorator { return Param('path', param); }
38
+ /**
39
+ * Define a Query param
40
+ * @param param The param configuration or name
41
+ * @augments `@travetto/web:Param`
42
+ */
43
+ export function QueryParam(param: string | Partial<EndpointParamConfig> = {}): ParamDecorator { return Param('query', param); }
44
+ /**
45
+ * Define a Header param
46
+ * @param param The param configuration or name
47
+ * @augments `@travetto/web:Param`
48
+ */
49
+ export function HeaderParam(param: string | Partial<EndpointParamConfig> = {}): ParamDecorator { return Param('header', param); }
50
+ /**
51
+ * Define a body param as an input
52
+ * @param param The param configuration
53
+ * @augments `@travetto/web:Param`
54
+ */
55
+ export function Body(param: Partial<EndpointParamConfig> = {}): ParamDecorator { return Param('body', param); }
56
+
57
+ /**
58
+ * A contextual field as provided by the WebAsyncContext
59
+ * @augments `@travetto/web:ContextParam`
60
+ */
61
+ export function ContextParam(config?: { target: Class }) {
62
+ return (inst: unknown, field: string): void =>
63
+ ControllerRegistry.registerControllerContextParam(asConstructable(inst).constructor, field, config!.target);
64
+ }
@@ -0,0 +1,70 @@
1
+ import { Injectable, Inject } from '@travetto/di';
2
+ import { Config } from '@travetto/config';
3
+ import { Ignore } from '@travetto/schema';
4
+
5
+ import { MimeUtil } from '../util/mime.ts';
6
+ import { WebCommonUtil } from '../util/common.ts';
7
+
8
+ import { WebChainedContext } from '../types/filter.ts';
9
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
10
+ import { WebInterceptorCategory } from '../types/core.ts';
11
+ import { WebResponse } from '../types/response.ts';
12
+ import { WebRequest } from '../types/request.ts';
13
+ import { WebError } from '../types/error.ts';
14
+
15
+ @Config('web.accept')
16
+ export class AcceptConfig {
17
+ /**
18
+ * Accepts certain request content types
19
+ */
20
+ applies = false;
21
+ /**
22
+ * The accepted types
23
+ */
24
+ types: string[] = [];
25
+
26
+ @Ignore()
27
+ matcher: (type: string) => boolean;
28
+ }
29
+
30
+ /**
31
+ * Enables access to contextual data when running in a web application
32
+ */
33
+ @Injectable()
34
+ export class AcceptInterceptor implements WebInterceptor<AcceptConfig> {
35
+
36
+ category: WebInterceptorCategory = 'request';
37
+
38
+ @Inject()
39
+ config: AcceptConfig;
40
+
41
+ finalizeConfig({ config }: WebInterceptorContext<AcceptConfig>): AcceptConfig {
42
+ config.matcher = MimeUtil.matcher(config.types ?? []);
43
+ return config;
44
+ }
45
+
46
+ applies({ config }: WebInterceptorContext<AcceptConfig>): boolean {
47
+ return config.applies && !!config.types.length;
48
+ }
49
+
50
+ validate(request: WebRequest, config: AcceptConfig): void {
51
+ const contentType = request.headers.get('Content-Type');
52
+ if (!contentType) {
53
+ throw WebError.for('Content type was not specified', 416);
54
+ } else if (!config.matcher(contentType)) {
55
+ throw WebError.for(`Content type ${contentType} violated ${config.types.join(', ')}`, 406);
56
+ }
57
+ }
58
+
59
+ async filter({ request, config, next }: WebChainedContext<AcceptConfig>): Promise<WebResponse> {
60
+ let res: WebResponse | undefined;
61
+ try {
62
+ this.validate(request, config);
63
+ return res = await next();
64
+ } catch (err) {
65
+ throw res = await WebCommonUtil.catchResponse(err);
66
+ } finally {
67
+ res?.headers.setIfAbsent('Accept', config.types.join(','));
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,123 @@
1
+ import { Injectable, Inject, DependencyRegistry } from '@travetto/di';
2
+ import { Config } from '@travetto/config';
3
+ import { toConcrete } from '@travetto/runtime';
4
+ import { Ignore } from '@travetto/schema';
5
+
6
+ import { WebChainedContext } from '../types/filter.ts';
7
+ import { WebResponse } from '../types/response.ts';
8
+ import { WebInterceptorCategory } from '../types/core.ts';
9
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
10
+
11
+ import { WebBodyUtil } from '../util/body.ts';
12
+ import { WebCommonUtil } from '../util/common.ts';
13
+
14
+ import { AcceptInterceptor } from './accept.ts';
15
+ import { DecompressInterceptor } from './decompress.ts';
16
+ import { WebError } from '../types/error.ts';
17
+
18
+ /**
19
+ * @concrete
20
+ */
21
+ export interface BodyContentParser {
22
+ type: string;
23
+ parse: (source: string) => unknown;
24
+ };
25
+
26
+ /**
27
+ * Web body parse configuration
28
+ */
29
+ @Config('web.bodyParse')
30
+ export class BodyParseConfig {
31
+ /**
32
+ * Parse request body
33
+ */
34
+ applies: boolean = true;
35
+ /**
36
+ * Max body size limit
37
+ */
38
+ limit: `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}` = '1mb';
39
+ /**
40
+ * How to interpret different content types
41
+ */
42
+ parsingTypes: Record<string, string> = {
43
+ text: 'text',
44
+ 'application/json': 'json',
45
+ 'application/x-www-form-urlencoded': 'form'
46
+ };
47
+
48
+ @Ignore()
49
+ _limit: number | undefined;
50
+
51
+ postConstruct(): void {
52
+ this._limit = WebCommonUtil.parseByteSize(this.limit);
53
+ }
54
+ }
55
+
56
+
57
+ /**
58
+ * Parses the body input content
59
+ */
60
+ @Injectable()
61
+ export class BodyParseInterceptor implements WebInterceptor<BodyParseConfig> {
62
+
63
+ dependsOn = [AcceptInterceptor, DecompressInterceptor];
64
+ category: WebInterceptorCategory = 'request';
65
+ parsers: Record<string, BodyContentParser> = {};
66
+
67
+ @Inject()
68
+ config: BodyParseConfig;
69
+
70
+ async postConstruct(): Promise<void> {
71
+ // Load all the parser types
72
+ const instances = await DependencyRegistry.getCandidateInstances(toConcrete<BodyContentParser>());
73
+ for (const instance of instances) {
74
+ this.parsers[instance.type] = instance;
75
+ }
76
+ }
77
+
78
+ applies({ endpoint, config }: WebInterceptorContext<BodyParseConfig>): boolean {
79
+ return config.applies && endpoint.allowsBody;
80
+ }
81
+
82
+ async filter({ request, config, next }: WebChainedContext<BodyParseConfig>): Promise<WebResponse> {
83
+ const input = request.body;
84
+
85
+ if (!WebBodyUtil.isRaw(input)) {
86
+ return next();
87
+ }
88
+
89
+ const lengthRead = +(request.headers.get('Content-Length') || '');
90
+ const length = Number.isNaN(lengthRead) ? undefined : lengthRead;
91
+
92
+ const limit = config._limit ?? Number.MAX_SAFE_INTEGER;
93
+ if (length && length > limit) {
94
+ throw WebError.for('Request entity too large', 413, { length, limit });
95
+ }
96
+
97
+ const contentType = request.headers.getContentType();
98
+ if (!contentType) {
99
+ return next();
100
+ }
101
+
102
+ const parserType = config.parsingTypes[contentType.full] ?? config.parsingTypes[contentType.type];
103
+ if (!parserType) {
104
+ return next();
105
+ }
106
+
107
+ const { text, read } = await WebBodyUtil.readText(input, limit, contentType.parameters.charset);
108
+
109
+ if (length && read !== length) {
110
+ throw WebError.for('Request size did not match Content-Length', 400, { length, read });
111
+ }
112
+
113
+ try {
114
+ request.body = parserType in this.parsers ?
115
+ this.parsers[parserType].parse(text) :
116
+ WebBodyUtil.parseBody(parserType, text);
117
+
118
+ return next();
119
+ } catch (err) {
120
+ throw WebError.for('Malformed input', 400, { cause: err });
121
+ }
122
+ }
123
+ }