@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.
- package/README.md +734 -0
- package/__index__.ts +44 -0
- package/package.json +66 -0
- package/src/common/global.ts +30 -0
- package/src/config.ts +18 -0
- package/src/context.ts +49 -0
- package/src/decorator/common.ts +87 -0
- package/src/decorator/controller.ts +13 -0
- package/src/decorator/endpoint.ts +102 -0
- package/src/decorator/param.ts +64 -0
- package/src/interceptor/accept.ts +70 -0
- package/src/interceptor/body-parse.ts +123 -0
- package/src/interceptor/compress.ts +119 -0
- package/src/interceptor/context.ts +23 -0
- package/src/interceptor/cookies.ts +97 -0
- package/src/interceptor/cors.ts +94 -0
- package/src/interceptor/decompress.ts +91 -0
- package/src/interceptor/etag.ts +99 -0
- package/src/interceptor/logging.ts +71 -0
- package/src/interceptor/respond.ts +26 -0
- package/src/interceptor/response-cache.ts +47 -0
- package/src/interceptor/trust-proxy.ts +53 -0
- package/src/registry/controller.ts +288 -0
- package/src/registry/types.ts +229 -0
- package/src/registry/visitor.ts +52 -0
- package/src/router/base.ts +67 -0
- package/src/router/standard.ts +59 -0
- package/src/types/cookie.ts +18 -0
- package/src/types/core.ts +33 -0
- package/src/types/dispatch.ts +23 -0
- package/src/types/error.ts +10 -0
- package/src/types/filter.ts +7 -0
- package/src/types/headers.ts +108 -0
- package/src/types/interceptor.ts +54 -0
- package/src/types/message.ts +33 -0
- package/src/types/request.ts +22 -0
- package/src/types/response.ts +20 -0
- package/src/util/body.ts +220 -0
- package/src/util/common.ts +142 -0
- package/src/util/cookie.ts +145 -0
- package/src/util/endpoint.ts +277 -0
- package/src/util/mime.ts +36 -0
- package/src/util/net.ts +61 -0
- package/support/test/dispatch-util.ts +90 -0
- package/support/test/dispatcher.ts +15 -0
- package/support/test/suite/base.ts +61 -0
- package/support/test/suite/controller.ts +103 -0
- package/support/test/suite/schema.ts +275 -0
- package/support/test/suite/standard.ts +178 -0
- 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
|
+
}
|