@travetto/web 7.0.0-rc.2 → 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 +4 -4
- package/package.json +10 -10
- package/src/decorator/common.ts +4 -4
- package/src/decorator/endpoint.ts +5 -5
- package/src/decorator/param.ts +3 -3
- 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 +14 -14
- package/src/registry/types.ts +6 -6
- 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 +51 -52
- 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/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
|
|
|
@@ -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
|
}
|
|
@@ -742,8 +742,8 @@ Cookies are a unique element, within the framework, as they sit on the request a
|
|
|
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
|
|
@@ -14,7 +14,7 @@ type ParamDecorator = (instance: ClassInstance, property: string | symbol, idx:
|
|
|
14
14
|
* @kind decorator
|
|
15
15
|
*/
|
|
16
16
|
export function Param(location: EndpointParamLocation, aliasOrConfig: string | Partial<EndpointParameterConfig>): ParamDecorator {
|
|
17
|
-
return (instance: ClassInstance, property: string
|
|
17
|
+
return (instance: ClassInstance, property: string, idx: number): void => {
|
|
18
18
|
const config = typeof aliasOrConfig === 'string' ? {} : aliasOrConfig;
|
|
19
19
|
const cls = getClass(instance);
|
|
20
20
|
if (typeof aliasOrConfig === 'string') {
|
|
@@ -62,7 +62,7 @@ export function Body(input: Partial<EndpointParameterConfig> = {}): ParamDecorat
|
|
|
62
62
|
* @kind decorator
|
|
63
63
|
*/
|
|
64
64
|
export function ContextParam() {
|
|
65
|
-
return (instance: ClassInstance, property: string
|
|
65
|
+
return (instance: ClassInstance, property: string): void => {
|
|
66
66
|
ControllerRegistryIndex.getForRegister(getClass(instance)).register({ contextParams: { [property]: true } });
|
|
67
67
|
ControllerRegistryIndex.bindContextParamsOnPostConstruct(getClass(instance));
|
|
68
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.get(this.#cls).getMethod(
|
|
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
|
}
|
|
@@ -85,8 +85,8 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
85
85
|
return this.store.get(cls).get();
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
getEndpoint(cls: Class, method: string
|
|
89
|
-
return this.getController(cls).endpoints.find(
|
|
88
|
+
getEndpoint(cls: Class, method: string): EndpointConfig {
|
|
89
|
+
return this.getController(cls).endpoints.find(endpoint => endpoint.methodName === method)!;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
getEndpointById(id: string): EndpointConfig | undefined {
|
|
@@ -94,16 +94,16 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
process(events: ChangeEvent<Class>[]): void {
|
|
97
|
-
for (const
|
|
98
|
-
if ('
|
|
99
|
-
for (const
|
|
100
|
-
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);
|
|
101
101
|
}
|
|
102
102
|
} else {
|
|
103
103
|
// Match by name
|
|
104
|
-
const toDelete = [...this.#endpointsById.values()].filter(
|
|
105
|
-
for (const
|
|
106
|
-
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);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
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
|
/**
|
package/src/router/base.ts
CHANGED
|
@@ -20,18 +20,18 @@ export abstract class BaseWebRouter implements WebRouter {
|
|
|
20
20
|
#cleanup = new Map<string, Function>();
|
|
21
21
|
#interceptors: WebInterceptor[];
|
|
22
22
|
|
|
23
|
-
async #register(
|
|
24
|
-
const config = ControllerRegistryIndex.getConfig(
|
|
23
|
+
async #register(cls: Class): Promise<void> {
|
|
24
|
+
const config = ControllerRegistryIndex.getConfig(cls);
|
|
25
25
|
|
|
26
|
-
let endpoints = await EndpointUtil.getBoundEndpoints(
|
|
26
|
+
let endpoints = await EndpointUtil.getBoundEndpoints(cls);
|
|
27
27
|
endpoints = EndpointUtil.orderEndpoints(endpoints);
|
|
28
28
|
|
|
29
|
-
for (const
|
|
30
|
-
|
|
29
|
+
for (const endpoint of endpoints) {
|
|
30
|
+
endpoint.filter = EndpointUtil.createEndpointHandler(this.#interceptors, endpoint, config);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const fn = await this.register(endpoints, config);
|
|
34
|
-
this.#cleanup.set(
|
|
34
|
+
this.#cleanup.set(cls.Ⲑid, fn);
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -41,25 +41,25 @@ export abstract class BaseWebRouter implements WebRouter {
|
|
|
41
41
|
|
|
42
42
|
this.#interceptors = await DependencyRegistryIndex.getInstances(toConcrete<WebInterceptor>());
|
|
43
43
|
this.#interceptors = EndpointUtil.orderInterceptors(this.#interceptors);
|
|
44
|
-
const names = this.#interceptors.map(
|
|
44
|
+
const names = this.#interceptors.map(interceptor => interceptor.constructor.name);
|
|
45
45
|
console.debug('Sorting interceptors', { count: names.length, names });
|
|
46
46
|
|
|
47
47
|
// Register all active
|
|
48
|
-
for (const
|
|
49
|
-
await this.#register(
|
|
48
|
+
for (const cls of ControllerRegistryIndex.getClasses()) {
|
|
49
|
+
await this.#register(cls);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Listen for updates
|
|
53
|
-
Registry.onClassChange(async
|
|
54
|
-
const targetCls = '
|
|
55
|
-
console.debug('Registry event', { type:
|
|
53
|
+
Registry.onClassChange(async event => {
|
|
54
|
+
const targetCls = 'current' in event ? event.current : event.previous;
|
|
55
|
+
console.debug('Registry event', { type: event.type, target: targetCls.Ⲑid });
|
|
56
56
|
|
|
57
|
-
if ('
|
|
58
|
-
this.#cleanup.get(
|
|
59
|
-
this.#cleanup.delete(
|
|
57
|
+
if ('previous' in event) {
|
|
58
|
+
this.#cleanup.get(event.previous.Ⲑid)?.();
|
|
59
|
+
this.#cleanup.delete(event.previous.Ⲑid);
|
|
60
60
|
}
|
|
61
|
-
if ('
|
|
62
|
-
await this.#register(
|
|
61
|
+
if ('current' in event) {
|
|
62
|
+
await this.#register(event.current);
|
|
63
63
|
}
|
|
64
64
|
}, ControllerRegistryIndex);
|
|
65
65
|
}
|
package/src/router/standard.ts
CHANGED
|
@@ -27,16 +27,16 @@ export class StandardWebRouter extends BaseWebRouter {
|
|
|
27
27
|
raw = router();
|
|
28
28
|
|
|
29
29
|
async register(endpoints: EndpointConfig[]): Promise<() => void> {
|
|
30
|
-
for (const
|
|
31
|
-
const fullPath =
|
|
30
|
+
for (const endpoint of endpoints) {
|
|
31
|
+
const fullPath = endpoint.fullPath.replace(/[*][^*]+/g, '*'); // Flatten wildcards
|
|
32
32
|
const handler = (): void => { };
|
|
33
|
-
this.#cache.set(handler,
|
|
34
|
-
this.raw[HTTP_METHODS[
|
|
33
|
+
this.#cache.set(handler, endpoint);
|
|
34
|
+
this.raw[HTTP_METHODS[endpoint.httpMethod ?? DEFAULT_HTTP_METHOD].lower](fullPath, handler);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
return (): void => {
|
|
38
|
-
for (const
|
|
39
|
-
this.raw.off(
|
|
38
|
+
for (const endpoint of endpoints ?? []) {
|
|
39
|
+
this.raw.off(endpoint.httpMethod ?? DEFAULT_HTTP_METHOD, endpoint.fullPath);
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
}
|
package/src/types/core.ts
CHANGED
|
@@ -3,8 +3,8 @@ function verb<
|
|
|
3
3
|
M extends string,
|
|
4
4
|
L extends string,
|
|
5
5
|
C extends Partial<MethodConfig>
|
|
6
|
-
>(method: M, lower: L,
|
|
7
|
-
return { body: false, cacheable: false, emptyStatusCode: 204, ...
|
|
6
|
+
>(method: M, lower: L, config: C): { method: M, lower: L } & C & MethodConfig {
|
|
7
|
+
return { body: false, cacheable: false, emptyStatusCode: 204, ...config, method, lower, };
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export const HTTP_METHODS = {
|