@travetto/web 7.0.0-rc.1 → 7.0.0-rc.3
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 +12 -12
- package/package.json +10 -10
- package/src/decorator/common.ts +4 -4
- package/src/decorator/endpoint.ts +5 -5
- package/src/decorator/param.ts +19 -21
- package/src/interceptor/accept.ts +5 -5
- package/src/interceptor/body.ts +2 -2
- package/src/interceptor/compress.ts +2 -2
- package/src/interceptor/cookie.ts +1 -1
- package/src/interceptor/cors.ts +4 -4
- package/src/interceptor/logging.ts +4 -4
- package/src/interceptor/respond.ts +5 -5
- package/src/registry/registry-adapter.ts +26 -26
- package/src/registry/registry-index.ts +16 -15
- package/src/registry/types.ts +6 -6
- package/src/registry/visitor.ts +2 -2
- package/src/router/base.ts +17 -17
- package/src/router/standard.ts +6 -6
- package/src/types/core.ts +2 -2
- package/src/types/headers.ts +16 -16
- package/src/types/message.ts +4 -4
- package/src/util/body.ts +19 -19
- package/src/util/common.ts +20 -20
- package/src/util/cookie.ts +9 -9
- package/src/util/endpoint.ts +52 -58
- package/src/util/header.ts +40 -37
- package/src/util/keygrip.ts +1 -1
- package/src/util/net.ts +15 -15
- package/support/test/suite/base.ts +2 -2
- package/support/test/suite/schema.ts +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ export class BaseWebMessage<B = unknown, C = unknown> implements WebMessage<B, C
|
|
|
30
30
|
readonly context: C;
|
|
31
31
|
readonly headers: WebHeaders;
|
|
32
32
|
body?: B;
|
|
33
|
-
constructor(
|
|
33
|
+
constructor(input: WebMessageInit<B, C> = {});
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -155,10 +155,10 @@ class SimpleController {
|
|
|
155
155
|
|
|
156
156
|
### Parameters
|
|
157
157
|
Endpoints can be configured to describe and enforce parameter behavior. Request parameters can be defined in five areas:
|
|
158
|
-
* [@PathParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
159
|
-
* [@QueryParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
160
|
-
* [@Body](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
161
|
-
* [@HeaderParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
158
|
+
* [@PathParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L36) - Path params
|
|
159
|
+
* [@QueryParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L43) - Query params - can be either a single value or bind to a whole object
|
|
160
|
+
* [@Body](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L57) - Request body
|
|
161
|
+
* [@HeaderParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L50) - Header values
|
|
162
162
|
|
|
163
163
|
Each [@Param](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L16) can be configured to indicate:
|
|
164
164
|
* `name` - Name of param, field name, defaults to handler parameter name if necessary
|
|
@@ -223,7 +223,7 @@ export class Simple {
|
|
|
223
223
|
```
|
|
224
224
|
|
|
225
225
|
### ContextParam
|
|
226
|
-
In addition to endpoint parameters (i.e. user-provided inputs), there may also be a desire to access indirect contextual information. Specifically you may need access to the entire [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11). These are able to be injected using the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
226
|
+
In addition to endpoint parameters (i.e. user-provided inputs), there may also be a desire to access indirect contextual information. Specifically you may need access to the entire [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11). These are able to be injected using the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L64) on a class-level field from the [WebAsyncContext](https://github.com/travetto/travetto/tree/main/module/web/src/context.ts#L11). These are not exposed as endpoint parameters as they cannot be provided when making RPC invocations.
|
|
227
227
|
|
|
228
228
|
**Code: Example ContextParam usage**
|
|
229
229
|
```typescript
|
|
@@ -251,12 +251,12 @@ class ContextController {
|
|
|
251
251
|
}
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
**Note**: When referencing the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
254
|
+
**Note**: When referencing the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L64) values, the contract for idempotency needs to be carefully inspected, if expected. You can see in the example above that the [@CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#L45) decorator is used to ensure that the response is not cached.
|
|
255
255
|
|
|
256
256
|
### Validating Inputs
|
|
257
257
|
The module provides high level access for [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") support, via decorators, for validating and typing request inputs.
|
|
258
258
|
|
|
259
|
-
By default, all endpoint parameters are validated for type, and any additional constraints added (required, vs optional, minlength, etc). Each parameter location ([@PathParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
259
|
+
By default, all endpoint parameters are validated for type, and any additional constraints added (required, vs optional, minlength, etc). Each parameter location ([@PathParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L36), [@Body](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L57), [@QueryParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L43), [@HeaderParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L50)) primarily provides a source to bind the endpoint arguments from. Once bound, the module will validate that the provided arguments are in fact valid. All validation will occur before the endpoint is ever executed, ensuring a strong contract.
|
|
260
260
|
|
|
261
261
|
**Code: Using Body for POST requests**
|
|
262
262
|
```typescript
|
|
@@ -667,7 +667,7 @@ export class AlowDenyController {
|
|
|
667
667
|
}
|
|
668
668
|
|
|
669
669
|
@Get('/raw')
|
|
670
|
-
@ExcludeInterceptors(
|
|
670
|
+
@ExcludeInterceptors(({ category }) => category === 'response')
|
|
671
671
|
withoutResponse(@QueryParam() value: string) {
|
|
672
672
|
|
|
673
673
|
}
|
|
@@ -735,15 +735,15 @@ export class SimpleAuthInterceptor implements WebInterceptor {
|
|
|
735
735
|
```
|
|
736
736
|
|
|
737
737
|
## Cookie Support
|
|
738
|
-
Cookies are a unique element, within the framework, as they sit on the request and response flows. Ideally we would separate these out, but given the support for key rotation, there is a scenario in which reading a cookie on the request, will result in a cookie needing to be written on the response. Because of this, cookies are treated as being outside the normal [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) activity, and is exposed as the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#
|
|
738
|
+
Cookies are a unique element, within the framework, as they sit on the request and response flows. Ideally we would separate these out, but given the support for key rotation, there is a scenario in which reading a cookie on the request, will result in a cookie needing to be written on the response. Because of this, cookies are treated as being outside the normal [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) activity, and is exposed as the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L64) [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#L12). The [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#L12) has a fairly basic contract:
|
|
739
739
|
|
|
740
740
|
**Code: CookieJar contract**
|
|
741
741
|
```typescript
|
|
742
742
|
export class CookieJar {
|
|
743
743
|
constructor({ keys, ...options }: CookieJarOptions = {});
|
|
744
744
|
import(cookies: Cookie[]): this;
|
|
745
|
-
has(name: string,
|
|
746
|
-
get(name: string,
|
|
745
|
+
has(name: string, options: CookieGetOptions = {}): boolean;
|
|
746
|
+
get(name: string, options: CookieGetOptions = {}): string | undefined;
|
|
747
747
|
set(cookie: Cookie): void;
|
|
748
748
|
getAll(): Cookie[];
|
|
749
749
|
importCookieHeader(header: string | null | undefined): this;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.3",
|
|
4
4
|
"description": "Declarative support for creating Web Applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"directory": "module/web"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@travetto/config": "^7.0.0-rc.
|
|
29
|
-
"@travetto/context": "^7.0.0-rc.
|
|
30
|
-
"@travetto/di": "^7.0.0-rc.
|
|
31
|
-
"@travetto/registry": "^7.0.0-rc.
|
|
32
|
-
"@travetto/runtime": "^7.0.0-rc.
|
|
33
|
-
"@travetto/schema": "^7.0.0-rc.
|
|
28
|
+
"@travetto/config": "^7.0.0-rc.2",
|
|
29
|
+
"@travetto/context": "^7.0.0-rc.2",
|
|
30
|
+
"@travetto/di": "^7.0.0-rc.2",
|
|
31
|
+
"@travetto/registry": "^7.0.0-rc.2",
|
|
32
|
+
"@travetto/runtime": "^7.0.0-rc.2",
|
|
33
|
+
"@travetto/schema": "^7.0.0-rc.2",
|
|
34
34
|
"find-my-way": "^9.3.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
38
|
-
"@travetto/test": "^7.0.0-rc.
|
|
39
|
-
"@travetto/transformer": "^7.0.0-rc.
|
|
37
|
+
"@travetto/cli": "^7.0.0-rc.2",
|
|
38
|
+
"@travetto/test": "^7.0.0-rc.2",
|
|
39
|
+
"@travetto/transformer": "^7.0.0-rc.2"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@travetto/transformer": {
|
package/src/decorator/common.ts
CHANGED
|
@@ -10,7 +10,7 @@ function isClass(target: unknown, property: unknown,): target is Class<unknown>
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function register(config: Partial<EndpointConfig | ControllerConfig>): EndpointDecorator {
|
|
13
|
-
return function <T>(instanceOrCls: ClassInstance | Class<T>, property?: string
|
|
13
|
+
return function <T>(instanceOrCls: ClassInstance | Class<T>, property?: string, _?: EndpointFunctionDescriptor) {
|
|
14
14
|
const adapter = ControllerRegistryIndex.getForRegister(getClass(instanceOrCls));
|
|
15
15
|
if (isClass(instanceOrCls, property)) {
|
|
16
16
|
adapter.register(config);
|
|
@@ -74,10 +74,10 @@ export function Accepts(types: [string, ...string[]]): EndpointDecorator {
|
|
|
74
74
|
*/
|
|
75
75
|
export const ConfigureInterceptor = <T extends WebInterceptor>(
|
|
76
76
|
cls: Class<T>,
|
|
77
|
-
|
|
77
|
+
config: Partial<RetainPrimitiveFields<T['config']>>,
|
|
78
78
|
extra?: Partial<EndpointConfig & ControllerConfig>
|
|
79
79
|
): EndpointDecorator =>
|
|
80
|
-
ControllerRegistryIndex.createInterceptorConfigDecorator(cls,
|
|
80
|
+
ControllerRegistryIndex.createInterceptorConfigDecorator(cls, config, extra);
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Specifies if endpoint should be conditional
|
|
@@ -91,6 +91,6 @@ export function ConditionalRegister(handler: () => (boolean | Promise<boolean>))
|
|
|
91
91
|
* Registers an interceptor exclusion filter
|
|
92
92
|
* @kind decorator
|
|
93
93
|
*/
|
|
94
|
-
export function ExcludeInterceptors(interceptorExclude: (
|
|
94
|
+
export function ExcludeInterceptors(interceptorExclude: (value: WebInterceptor) => boolean): EndpointDecorator {
|
|
95
95
|
return register({ interceptorExclude });
|
|
96
96
|
};
|
|
@@ -4,7 +4,7 @@ import { EndpointConfig, EndpointFunctionDescriptor } from '../registry/types.ts
|
|
|
4
4
|
import { HTTP_METHODS, HttpMethod } from '../types/core.ts';
|
|
5
5
|
import { ControllerRegistryIndex } from '../registry/registry-index.ts';
|
|
6
6
|
|
|
7
|
-
type EndpointFunctionDecorator = <T>(instance: T, property:
|
|
7
|
+
type EndpointFunctionDecorator = <T>(instance: T, property: string, descriptor: EndpointFunctionDescriptor) => EndpointFunctionDescriptor;
|
|
8
8
|
|
|
9
9
|
type EndpointDecConfig = Partial<EndpointConfig> & { path: string };
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ type EndpointDecConfig = Partial<EndpointConfig> & { path: string };
|
|
|
12
12
|
* Generic Endpoint Decorator
|
|
13
13
|
*/
|
|
14
14
|
export function Endpoint(config: EndpointDecConfig): EndpointFunctionDecorator {
|
|
15
|
-
return function (instance: ClassInstance, property:
|
|
15
|
+
return function (instance: ClassInstance, property: string, descriptor: EndpointFunctionDescriptor): EndpointFunctionDescriptor {
|
|
16
16
|
ControllerRegistryIndex.getForRegister(getClass(instance)).registerEndpoint(property, { methodName: property }, config);
|
|
17
17
|
return descriptor;
|
|
18
18
|
};
|
|
@@ -25,9 +25,9 @@ function HttpEndpoint(method: HttpMethod, path: string): EndpointFunctionDecorat
|
|
|
25
25
|
allowsBody,
|
|
26
26
|
cacheable,
|
|
27
27
|
httpMethod: method,
|
|
28
|
-
responseFinalizer:
|
|
29
|
-
|
|
30
|
-
return
|
|
28
|
+
responseFinalizer: value => {
|
|
29
|
+
value.context.httpStatusCode ??= (value.body === null || value.body === undefined || value.body === '') ? emptyStatusCode : 200;
|
|
30
|
+
return value;
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
}
|
package/src/decorator/param.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { SchemaRegistryIndex } from '@travetto/schema';
|
|
|
4
4
|
import { ControllerRegistryIndex } from '../registry/registry-index.ts';
|
|
5
5
|
import { EndpointParameterConfig, EndpointParamLocation } from '../registry/types.ts';
|
|
6
6
|
|
|
7
|
-
type ParamDecorator = (instance: ClassInstance, property: string
|
|
7
|
+
type ParamDecorator = (instance: ClassInstance, property: string, idx: number) => void;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Define a parameter
|
|
@@ -13,50 +13,48 @@ type ParamDecorator = (instance: ClassInstance, property: string | symbol, idx:
|
|
|
13
13
|
* @augments `@travetto/schema:Input`
|
|
14
14
|
* @kind decorator
|
|
15
15
|
*/
|
|
16
|
-
export function Param(location: EndpointParamLocation,
|
|
17
|
-
return (instance: ClassInstance, property: string
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
export function Param(location: EndpointParamLocation, aliasOrConfig: string | Partial<EndpointParameterConfig>): ParamDecorator {
|
|
17
|
+
return (instance: ClassInstance, property: string, idx: number): void => {
|
|
18
|
+
const config = typeof aliasOrConfig === 'string' ? {} : aliasOrConfig;
|
|
19
|
+
const cls = getClass(instance);
|
|
20
|
+
if (typeof aliasOrConfig === 'string') {
|
|
21
|
+
SchemaRegistryIndex.getForRegister(cls).registerParameter(property, idx, {
|
|
22
|
+
aliases: [aliasOrConfig] // Register extra input string as an alias
|
|
23
|
+
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
ControllerRegistryIndex.getForRegister(
|
|
27
|
-
index: idx, location, ...config
|
|
28
|
-
});
|
|
26
|
+
ControllerRegistryIndex.getForRegister(cls).registerEndpointParameter(property, idx, { location, ...config });
|
|
29
27
|
};
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
/**
|
|
33
31
|
* Define a Path param
|
|
34
|
-
* @
|
|
32
|
+
* @input input The param configuration or alias
|
|
35
33
|
* @augments `@travetto/schema:Input`
|
|
36
34
|
* @kind decorator
|
|
37
35
|
*/
|
|
38
|
-
export function PathParam(
|
|
36
|
+
export function PathParam(input: string | Partial<EndpointParameterConfig> = {}): ParamDecorator { return Param('path', input); }
|
|
39
37
|
/**
|
|
40
38
|
* Define a Query param
|
|
41
|
-
* @
|
|
39
|
+
* @input input The param configuration or alias
|
|
42
40
|
* @augments `@travetto/schema:Input`
|
|
43
41
|
* @kind decorator
|
|
44
42
|
*/
|
|
45
|
-
export function QueryParam(
|
|
43
|
+
export function QueryParam(input: string | Partial<EndpointParameterConfig> = {}): ParamDecorator { return Param('query', input); }
|
|
46
44
|
/**
|
|
47
45
|
* Define a Header param
|
|
48
|
-
* @
|
|
46
|
+
* @input input The param configuration or alias
|
|
49
47
|
* @augments `@travetto/schema:Input`
|
|
50
48
|
* @kind decorator
|
|
51
49
|
*/
|
|
52
|
-
export function HeaderParam(
|
|
50
|
+
export function HeaderParam(input: string | Partial<EndpointParameterConfig> = {}): ParamDecorator { return Param('header', input); }
|
|
53
51
|
/**
|
|
54
52
|
* Define a body param as an input
|
|
55
|
-
* @
|
|
53
|
+
* @input input The param configuration
|
|
56
54
|
* @augments `@travetto/schema:Input`
|
|
57
55
|
* @kind decorator
|
|
58
56
|
*/
|
|
59
|
-
export function Body(
|
|
57
|
+
export function Body(input: Partial<EndpointParameterConfig> = {}): ParamDecorator { return Param('body', input); }
|
|
60
58
|
|
|
61
59
|
/**
|
|
62
60
|
* A contextual field as provided by the WebAsyncContext
|
|
@@ -64,7 +62,7 @@ export function Body(param: Partial<EndpointParameterConfig> = {}): ParamDecorat
|
|
|
64
62
|
* @kind decorator
|
|
65
63
|
*/
|
|
66
64
|
export function ContextParam() {
|
|
67
|
-
return (instance: ClassInstance, property: string
|
|
65
|
+
return (instance: ClassInstance, property: string): void => {
|
|
68
66
|
ControllerRegistryIndex.getForRegister(getClass(instance)).register({ contextParams: { [property]: true } });
|
|
69
67
|
ControllerRegistryIndex.bindContextParamsOnPostConstruct(getClass(instance));
|
|
70
68
|
};
|
|
@@ -56,14 +56,14 @@ export class AcceptInterceptor implements WebInterceptor<AcceptConfig> {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
async filter({ request, config, next }: WebChainedContext<AcceptConfig>): Promise<WebResponse> {
|
|
59
|
-
let
|
|
59
|
+
let response: WebResponse | undefined;
|
|
60
60
|
try {
|
|
61
61
|
this.validate(request, config);
|
|
62
|
-
return
|
|
63
|
-
} catch (
|
|
64
|
-
throw
|
|
62
|
+
return response = await next();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw response = await WebCommonUtil.catchResponse(error);
|
|
65
65
|
} finally {
|
|
66
|
-
|
|
66
|
+
response?.headers.setIfAbsent('Accept', config.types.join(','));
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
package/src/interceptor/body.ts
CHANGED
|
@@ -114,8 +114,8 @@ export class BodyInterceptor implements WebInterceptor<WebBodyConfig> {
|
|
|
114
114
|
WebBodyUtil.parseBody(parserType, text);
|
|
115
115
|
|
|
116
116
|
return next();
|
|
117
|
-
} catch (
|
|
118
|
-
throw WebError.for('Malformed input', 400, { cause:
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw WebError.for('Malformed input', 400, { cause: error });
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
@@ -81,8 +81,8 @@ export class CompressInterceptor implements WebInterceptor {
|
|
|
81
81
|
return binaryResponse;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const
|
|
85
|
-
const stream = COMPRESSORS[type](
|
|
84
|
+
const options = type === 'br' ? { params: { [constants.BROTLI_PARAM_QUALITY]: 4, ...raw.params }, ...raw } : { ...raw };
|
|
85
|
+
const stream = COMPRESSORS[type](options);
|
|
86
86
|
|
|
87
87
|
// If we are compressing
|
|
88
88
|
binaryResponse.headers.set('Content-Encoding', type);
|
|
@@ -96,7 +96,7 @@ export class CookieInterceptor implements WebInterceptor<CookieConfig> {
|
|
|
96
96
|
this.#cookieJar.set(jar);
|
|
97
97
|
|
|
98
98
|
const response = await next();
|
|
99
|
-
for (const
|
|
99
|
+
for (const cookie of jar.exportSetCookieHeader()) { response.headers.append('Set-Cookie', cookie); }
|
|
100
100
|
return response;
|
|
101
101
|
}
|
|
102
102
|
}
|
package/src/interceptor/cors.ts
CHANGED
|
@@ -72,13 +72,13 @@ export class CorsInterceptor implements WebInterceptor<CorsConfig> {
|
|
|
72
72
|
decorate(request: WebRequest, resolved: CorsConfig['resolved'], response: WebResponse,): WebResponse {
|
|
73
73
|
const origin = request.headers.get('Origin');
|
|
74
74
|
if (resolved.origins.size === 0 || resolved.origins.has(origin!)) {
|
|
75
|
-
for (const [
|
|
75
|
+
for (const [header, value] of [
|
|
76
76
|
['Access-Control-Allow-Origin', origin || '*'],
|
|
77
77
|
['Access-Control-Allow-Credentials', `${resolved.credentials}`],
|
|
78
78
|
['Access-Control-Allow-Methods', resolved.methods],
|
|
79
79
|
['Access-Control-Allow-Headers', resolved.headers || request.headers.get('Access-Control-Request-Headers') || '*'],
|
|
80
80
|
]) {
|
|
81
|
-
response.headers.setIfAbsent(
|
|
81
|
+
response.headers.setIfAbsent(header, value);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
return response;
|
|
@@ -87,8 +87,8 @@ export class CorsInterceptor implements WebInterceptor<CorsConfig> {
|
|
|
87
87
|
async filter({ request, config: { resolved }, next }: WebChainedContext<CorsConfig>): Promise<WebResponse> {
|
|
88
88
|
try {
|
|
89
89
|
return this.decorate(request, resolved, await next());
|
|
90
|
-
} catch (
|
|
91
|
-
throw this.decorate(request, resolved, WebCommonUtil.catchResponse(
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw this.decorate(request, resolved, WebCommonUtil.catchResponse(error));
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -41,8 +41,8 @@ export class LoggingInterceptor implements WebInterceptor {
|
|
|
41
41
|
const response = await next();
|
|
42
42
|
const duration = Date.now() - createdDate;
|
|
43
43
|
|
|
44
|
-
const
|
|
45
|
-
const code = response.context.httpStatusCode ??= (!!
|
|
44
|
+
const error = response.body instanceof Error ? response.body : undefined;
|
|
45
|
+
const code = response.context.httpStatusCode ??= (!!error ? 500 : 200);
|
|
46
46
|
|
|
47
47
|
const logMessage = {
|
|
48
48
|
method: request.context.httpMethod,
|
|
@@ -61,8 +61,8 @@ export class LoggingInterceptor implements WebInterceptor {
|
|
|
61
61
|
console.error('Request', logMessage);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
if (this.config.showStackTrace &&
|
|
65
|
-
console.error(
|
|
64
|
+
if (this.config.showStackTrace && error) {
|
|
65
|
+
console.error(error.message, { error });
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return response;
|
|
@@ -15,12 +15,12 @@ export class RespondInterceptor implements WebInterceptor {
|
|
|
15
15
|
dependsOn = [LoggingInterceptor];
|
|
16
16
|
|
|
17
17
|
async filter(ctx: WebChainedContext): Promise<WebResponse> {
|
|
18
|
-
let
|
|
18
|
+
let response;
|
|
19
19
|
try {
|
|
20
|
-
|
|
21
|
-
} catch (
|
|
22
|
-
|
|
20
|
+
response = await ctx.next();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
response = WebCommonUtil.catchResponse(error);
|
|
23
23
|
}
|
|
24
|
-
return
|
|
24
|
+
return response;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -30,7 +30,7 @@ function combineClassConfigs(base: ControllerConfig, ...overrides: Partial<Contr
|
|
|
30
30
|
return base;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function combineEndpointConfigs(
|
|
33
|
+
function combineEndpointConfigs(controller: ControllerConfig, base: EndpointConfig, ...overrides: Partial<EndpointConfig>[]): EndpointConfig {
|
|
34
34
|
for (const override of overrides) {
|
|
35
35
|
combineCommon(base, override);
|
|
36
36
|
Object.assign(
|
|
@@ -40,7 +40,7 @@ function combineEndpointConfigs(ctrl: ControllerConfig, base: EndpointConfig, ..
|
|
|
40
40
|
httpMethod: override.httpMethod ?? base.httpMethod,
|
|
41
41
|
allowsBody: override.allowsBody ?? base.allowsBody,
|
|
42
42
|
path: override.path || base.path,
|
|
43
|
-
parameters: (override.parameters ?? base.parameters).map(
|
|
43
|
+
parameters: (override.parameters ?? base.parameters).map(endpoint => ({ ...endpoint })),
|
|
44
44
|
responseFinalizer: override.responseFinalizer ?? base.responseFinalizer,
|
|
45
45
|
}
|
|
46
46
|
);
|
|
@@ -55,17 +55,17 @@ function combineEndpointConfigs(ctrl: ControllerConfig, base: EndpointConfig, ..
|
|
|
55
55
|
/**
|
|
56
56
|
* Compute the location of a parameter within an endpoint
|
|
57
57
|
*/
|
|
58
|
-
function computeParameterLocation(
|
|
59
|
-
const name =
|
|
60
|
-
if (!SchemaRegistryIndex.has(
|
|
61
|
-
if ((
|
|
58
|
+
function computeParameterLocation(endpoint: EndpointConfig, param: SchemaParameterConfig): EndpointParamLocation {
|
|
59
|
+
const name = param?.name;
|
|
60
|
+
if (!SchemaRegistryIndex.has(param.type)) {
|
|
61
|
+
if ((param.type === String || param.type === Number) && name && endpoint.path.includes(`:${name}`)) {
|
|
62
62
|
return 'path';
|
|
63
|
-
} else if (
|
|
63
|
+
} else if (param.type === Blob || param.type === File || param.type === ArrayBuffer || param.type === Uint8Array) {
|
|
64
64
|
return 'body';
|
|
65
65
|
}
|
|
66
66
|
return 'query';
|
|
67
67
|
} else {
|
|
68
|
-
return
|
|
68
|
+
return endpoint.allowsBody ? 'body' : 'query';
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -74,7 +74,7 @@ function computeParameterLocation(ep: EndpointConfig, schema: SchemaParameterCon
|
|
|
74
74
|
*/
|
|
75
75
|
export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConfig> {
|
|
76
76
|
#config: ControllerConfig;
|
|
77
|
-
#endpoints: Map<string
|
|
77
|
+
#endpoints: Map<string, EndpointConfig> = new Map();
|
|
78
78
|
#cls: Class;
|
|
79
79
|
#finalizeHandlers: Function[] = [];
|
|
80
80
|
|
|
@@ -97,7 +97,7 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
97
97
|
return this.#config;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
registerEndpoint(method: string
|
|
100
|
+
registerEndpoint(method: string, ...data: Partial<EndpointConfig>[]): EndpointConfig {
|
|
101
101
|
this.register();
|
|
102
102
|
|
|
103
103
|
if (!this.#endpoints.has(method)) {
|
|
@@ -108,8 +108,8 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
108
108
|
allowsBody: false,
|
|
109
109
|
class: this.#cls,
|
|
110
110
|
filters: [],
|
|
111
|
-
methodName: method
|
|
112
|
-
id: `${this.#cls.name}#${method
|
|
111
|
+
methodName: method,
|
|
112
|
+
id: `${this.#cls.name}#${method}`,
|
|
113
113
|
parameters: [],
|
|
114
114
|
interceptorConfigs: [],
|
|
115
115
|
responseHeaders: {},
|
|
@@ -124,23 +124,23 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
124
124
|
return this.#endpoints.get(method)!;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
registerEndpointParameter(method: string
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
safeAssign(
|
|
131
|
-
return
|
|
127
|
+
registerEndpointParameter(method: string, idx: number, ...config: Partial<EndpointParameterConfig>[]): EndpointParameterConfig {
|
|
128
|
+
const endpoint = this.registerEndpoint(method);
|
|
129
|
+
endpoint.parameters[idx] ??= { index: idx, location: 'query' };
|
|
130
|
+
safeAssign(endpoint.parameters[idx], ...config);
|
|
131
|
+
return endpoint.parameters[idx];
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
finalize(): void {
|
|
135
135
|
// Merge into controller
|
|
136
|
-
for (const
|
|
136
|
+
for (const endpoint of this.#config.endpoints) {
|
|
137
137
|
// Store full path from base for use in other contexts
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (const schema of SchemaRegistryIndex.
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
endpoint.fullPath = `/${this.#config.basePath}/${endpoint.path}`.replace(/[/]{1,4}/g, '/').replace(/(.)[/]$/, (_, a) => a);
|
|
139
|
+
endpoint.finalizedResponseHeaders = new WebHeaders({ ...this.#config.responseHeaders, ...endpoint.responseHeaders });
|
|
140
|
+
endpoint.responseContext = { ...this.#config.responseContext, ...endpoint.responseContext };
|
|
141
|
+
for (const schema of SchemaRegistryIndex.get(this.#cls).getMethod(endpoint.methodName).parameters) {
|
|
142
|
+
endpoint.parameters[schema.index!] ??= { index: schema.index!, location: undefined! };
|
|
143
|
+
endpoint.parameters[schema.index!].location ??= computeParameterLocation(endpoint, schema);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
for (const item of this.#finalizeHandlers) {
|
|
@@ -153,7 +153,7 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
153
153
|
return this.#config;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
getEndpointConfig(method: string
|
|
156
|
+
getEndpointConfig(method: string): EndpointConfig {
|
|
157
157
|
const endpoint = this.#endpoints.get(method);
|
|
158
158
|
if (!endpoint) {
|
|
159
159
|
throw new AppError(`Endpoint not registered: ${String(method)} on ${this.#cls.name}`);
|
|
@@ -170,7 +170,7 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
registerEndpointInterceptorConfig<T extends WebInterceptor>(
|
|
173
|
-
property: string
|
|
173
|
+
property: string,
|
|
174
174
|
cls: Class<T>,
|
|
175
175
|
config: Partial<RetainPrimitiveFields<T['config']>>,
|
|
176
176
|
extra?: Partial<EndpointConfig>
|
|
@@ -37,19 +37,19 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
37
37
|
/**
|
|
38
38
|
* Register a controller/endpoint with specific config for an interceptor
|
|
39
39
|
* @param cls The interceptor to register data for
|
|
40
|
-
* @param
|
|
40
|
+
* @param config The partial config override
|
|
41
41
|
*/
|
|
42
42
|
static createInterceptorConfigDecorator<T extends WebInterceptor>(
|
|
43
43
|
cls: Class<T>,
|
|
44
|
-
|
|
44
|
+
config: Partial<RetainPrimitiveFields<T['config']>>,
|
|
45
45
|
extra?: Partial<EndpointConfig & ControllerConfig>
|
|
46
46
|
): EndpointDecorator {
|
|
47
|
-
return (instanceOrCls: Class | ClassInstance, property?:
|
|
47
|
+
return (instanceOrCls: Class | ClassInstance, property?: string): void => {
|
|
48
48
|
const adapter = ControllerRegistryIndex.getForRegister(getClass(instanceOrCls));
|
|
49
49
|
if (isClass(property, instanceOrCls)) {
|
|
50
|
-
adapter.registerInterceptorConfig(cls,
|
|
50
|
+
adapter.registerInterceptorConfig(cls, config, extra);
|
|
51
51
|
} else {
|
|
52
|
-
adapter.registerEndpointInterceptorConfig(property!, cls,
|
|
52
|
+
adapter.registerEndpointInterceptorConfig(property!, cls, config, extra);
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -62,8 +62,9 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
62
62
|
const ctx = await DependencyRegistryIndex.getInstance(WebAsyncContext);
|
|
63
63
|
const cls = getClass(instance);
|
|
64
64
|
const map = this.getController(cls).contextParams;
|
|
65
|
+
const fieldMap = SchemaRegistryIndex.get(cls).getFields();
|
|
65
66
|
for (const field of Object.keys(map)) {
|
|
66
|
-
const { type } =
|
|
67
|
+
const { type } = fieldMap[field];
|
|
67
68
|
Object.defineProperty(instance, field, { get: ctx.getSource(type) });
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -84,8 +85,8 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
84
85
|
return this.store.get(cls).get();
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
getEndpoint(cls: Class, method: string
|
|
88
|
-
return this.getController(cls).endpoints.find(
|
|
88
|
+
getEndpoint(cls: Class, method: string): EndpointConfig {
|
|
89
|
+
return this.getController(cls).endpoints.find(endpoint => endpoint.methodName === method)!;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
getEndpointById(id: string): EndpointConfig | undefined {
|
|
@@ -93,16 +94,16 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
process(events: ChangeEvent<Class>[]): void {
|
|
96
|
-
for (const
|
|
97
|
-
if ('
|
|
98
|
-
for (const
|
|
99
|
-
this.#endpointsById.set(`${
|
|
97
|
+
for (const event of events) {
|
|
98
|
+
if ('current' in event) {
|
|
99
|
+
for (const endpoint of this.getController(event.current).endpoints) {
|
|
100
|
+
this.#endpointsById.set(`${event.current.name}#${endpoint.methodName}`, endpoint);
|
|
100
101
|
}
|
|
101
102
|
} else {
|
|
102
103
|
// Match by name
|
|
103
|
-
const toDelete = [...this.#endpointsById.values()].filter(
|
|
104
|
-
for (const
|
|
105
|
-
this.#endpointsById.delete(
|
|
104
|
+
const toDelete = [...this.#endpointsById.values()].filter(endpoint => endpoint.class.name === event.previous.name);
|
|
105
|
+
for (const endpoint of toDelete) {
|
|
106
|
+
this.#endpointsById.delete(endpoint.id);
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
}
|
package/src/registry/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type EndpointFunctionDescriptor = TypedPropertyDescriptor<EndpointFunctio
|
|
|
16
16
|
*/
|
|
17
17
|
export type EndpointDecorator = (
|
|
18
18
|
(<T extends Class>(target: T) => void) &
|
|
19
|
-
(<U>(target: U,
|
|
19
|
+
(<U>(target: U, property: string, descriptor?: EndpointFunctionDescriptor) => void)
|
|
20
20
|
);
|
|
21
21
|
|
|
22
22
|
export type EndpointParamLocation = 'path' | 'query' | 'body' | 'header';
|
|
@@ -48,7 +48,7 @@ interface CoreConfig {
|
|
|
48
48
|
/**
|
|
49
49
|
* Control which interceptors are excluded
|
|
50
50
|
*/
|
|
51
|
-
interceptorExclude?: (
|
|
51
|
+
interceptorExclude?: (value: WebInterceptor) => boolean;
|
|
52
52
|
/**
|
|
53
53
|
* Response headers
|
|
54
54
|
*/
|
|
@@ -72,7 +72,7 @@ export interface EndpointParameterConfig {
|
|
|
72
72
|
*/
|
|
73
73
|
location: EndpointParamLocation;
|
|
74
74
|
/**
|
|
75
|
-
* Resolves the value by executing with
|
|
75
|
+
* Resolves the value by executing with request/response as input
|
|
76
76
|
*/
|
|
77
77
|
resolve?: WebFilter;
|
|
78
78
|
/**
|
|
@@ -97,7 +97,7 @@ export interface EndpointConfig extends CoreConfig {
|
|
|
97
97
|
/**
|
|
98
98
|
* Name of the endpoint (method name)
|
|
99
99
|
*/
|
|
100
|
-
methodName: string
|
|
100
|
+
methodName: string;
|
|
101
101
|
/**
|
|
102
102
|
* Instance the endpoint is for
|
|
103
103
|
*/
|
|
@@ -133,7 +133,7 @@ export interface EndpointConfig extends CoreConfig {
|
|
|
133
133
|
/**
|
|
134
134
|
* Response finalizer
|
|
135
135
|
*/
|
|
136
|
-
responseFinalizer?: (
|
|
136
|
+
responseFinalizer?: (response: WebResponse) => WebResponse;
|
|
137
137
|
/**
|
|
138
138
|
* Response headers finalized
|
|
139
139
|
*/
|
|
@@ -159,7 +159,7 @@ export interface ControllerConfig extends CoreConfig {
|
|
|
159
159
|
/**
|
|
160
160
|
* Context parameters to bind at create
|
|
161
161
|
*/
|
|
162
|
-
contextParams: Record<string
|
|
162
|
+
contextParams: Record<string, boolean>;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|