@travetto/web 6.0.2 → 7.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -22
- package/__index__.ts +2 -1
- package/package.json +11 -11
- package/src/common/global.ts +5 -4
- package/src/decorator/common.ts +26 -21
- package/src/decorator/controller.ts +8 -5
- package/src/decorator/endpoint.ts +21 -39
- package/src/decorator/param.ts +39 -32
- package/src/interceptor/body.ts +4 -4
- package/src/registry/registry-adapter.ts +185 -0
- package/src/registry/registry-index.ts +110 -0
- package/src/registry/types.ts +19 -56
- package/src/registry/visitor.ts +14 -11
- package/src/router/base.ts +14 -12
- package/src/types/headers.ts +1 -1
- package/src/util/common.ts +7 -7
- package/src/util/endpoint.ts +61 -26
- package/src/util/net.ts +14 -0
- package/support/test/dispatch-util.ts +5 -24
- package/support/test/suite/base.ts +7 -5
- package/support/test/suite/schema.ts +26 -20
- package/support/test/suite/standard.ts +2 -3
- package/src/registry/controller.ts +0 -292
- package/support/transformer.web.ts +0 -212
package/src/util/endpoint.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { asConstructable, castTo, Class, Runtime, TypedObject } from '@travetto/runtime';
|
|
2
|
-
import { BindUtil,
|
|
3
|
-
import {
|
|
2
|
+
import { BindUtil, SchemaParameterConfig, SchemaRegistryIndex, SchemaValidator, ValidationResultError } from '@travetto/schema';
|
|
3
|
+
import { DependencyRegistryIndex } from '@travetto/di';
|
|
4
4
|
import { RetargettingProxy } from '@travetto/registry';
|
|
5
5
|
|
|
6
6
|
import { WebChainedFilter, WebChainedContext, WebFilter } from '../types/filter.ts';
|
|
@@ -8,8 +8,8 @@ import { WebResponse } from '../types/response.ts';
|
|
|
8
8
|
import { WebInterceptor } from '../types/interceptor.ts';
|
|
9
9
|
import { WebRequest } from '../types/request.ts';
|
|
10
10
|
import { WEB_INTERCEPTOR_CATEGORIES } from '../types/core.ts';
|
|
11
|
-
import { EndpointConfig, ControllerConfig,
|
|
12
|
-
import {
|
|
11
|
+
import { EndpointConfig, ControllerConfig, EndpointParameterConfig } from '../registry/types.ts';
|
|
12
|
+
import { ControllerRegistryIndex } from '../registry/registry-index.ts';
|
|
13
13
|
import { WebCommonUtil } from './common.ts';
|
|
14
14
|
|
|
15
15
|
|
|
@@ -81,27 +81,57 @@ export class EndpointUtil {
|
|
|
81
81
|
]);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
|
|
84
85
|
/**
|
|
85
|
-
* Extract parameter from request
|
|
86
|
+
* Extract parameter value from request
|
|
87
|
+
* @param request The request
|
|
88
|
+
* @param param The parameter config
|
|
89
|
+
* @param name The parameter name
|
|
90
|
+
* @param isArray Whether the parameter is an array
|
|
86
91
|
*/
|
|
87
|
-
static
|
|
88
|
-
if (value !== undefined && value !== this.MissingParamSymbol) {
|
|
89
|
-
return value;
|
|
90
|
-
} else if (param.extract) {
|
|
91
|
-
return param.extract(request, param);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const name = param.name!;
|
|
92
|
+
static extractParameterValue(request: WebRequest, param: EndpointParameterConfig, name: string, isArray?: boolean): unknown {
|
|
95
93
|
switch (param.location) {
|
|
96
|
-
case 'path': return request.context.pathParams?.[name];
|
|
97
|
-
case 'header': return field.array ? request.headers.getList(name) : request.headers.get(name);
|
|
98
94
|
case 'body': return request.body;
|
|
95
|
+
case 'path': return request.context.pathParams?.[name];
|
|
96
|
+
case 'header': return isArray ? request.headers.getList(name) : request.headers.get(name);
|
|
99
97
|
case 'query': {
|
|
100
98
|
const withQuery: typeof request & { [WebQueryExpandedSymbol]?: Record<string, unknown> } = request;
|
|
101
99
|
const q = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
|
|
102
|
-
return
|
|
100
|
+
return q[name];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extract parameter from request
|
|
107
|
+
* @param request The request
|
|
108
|
+
* @param param The parameter config
|
|
109
|
+
* @param input The schema parameter config
|
|
110
|
+
*/
|
|
111
|
+
static extractParameter(request: WebRequest, param: EndpointParameterConfig, input: SchemaParameterConfig): unknown {
|
|
112
|
+
if (param.extract) {
|
|
113
|
+
return param.extract(request, param);
|
|
114
|
+
} else if (param.location === 'query') {
|
|
115
|
+
// TODO: Revisit this logic?
|
|
116
|
+
const withQuery: typeof request & { [WebQueryExpandedSymbol]?: Record<string, unknown> } = request;
|
|
117
|
+
const q = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
|
|
118
|
+
if (param.prefix) { // Has a prefix provided
|
|
119
|
+
return q[param.prefix];
|
|
120
|
+
} else if (input.type.Ⲑid) { // Is a full type
|
|
121
|
+
return q;
|
|
103
122
|
}
|
|
104
123
|
}
|
|
124
|
+
|
|
125
|
+
let res = this.extractParameterValue(request, param, input.name!.toString(), input.array);
|
|
126
|
+
if (!res && input.aliases) {
|
|
127
|
+
for (const name of input.aliases) {
|
|
128
|
+
res = this.extractParameterValue(request, param, name, input.array);
|
|
129
|
+
if (res !== undefined) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return res;
|
|
105
135
|
}
|
|
106
136
|
|
|
107
137
|
/**
|
|
@@ -112,22 +142,27 @@ export class EndpointUtil {
|
|
|
112
142
|
*/
|
|
113
143
|
static async extractParameters(endpoint: EndpointConfig, request: WebRequest): Promise<unknown[]> {
|
|
114
144
|
const cls = endpoint.class;
|
|
115
|
-
const method = endpoint.name;
|
|
116
145
|
const vals = WebCommonUtil.getRequestParams(request);
|
|
146
|
+
const { parameters } = SchemaRegistryIndex.getMethodConfig(cls, endpoint.methodName);
|
|
147
|
+
const combined = parameters.map((cfg) =>
|
|
148
|
+
({ schema: cfg, param: endpoint.parameters[cfg.index], value: vals?.[cfg.index] }));
|
|
117
149
|
|
|
118
150
|
try {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
const extracted = combined.map(({ param, schema, value }) =>
|
|
152
|
+
(value !== undefined && value !== this.MissingParamSymbol) ?
|
|
153
|
+
value :
|
|
154
|
+
this.extractParameter(request, param, schema)
|
|
155
|
+
);
|
|
156
|
+
const params = BindUtil.coerceMethodParams(cls, endpoint.methodName, extracted);
|
|
157
|
+
await SchemaValidator.validateMethod(cls, endpoint.methodName, params, endpoint.parameters.map(x => x.prefix));
|
|
123
158
|
return params;
|
|
124
159
|
} catch (err) {
|
|
125
160
|
if (err instanceof ValidationResultError) {
|
|
126
161
|
for (const el of err.details?.errors ?? []) {
|
|
127
162
|
if (el.kind === 'required') {
|
|
128
|
-
const config =
|
|
163
|
+
const config = combined.find(x => x.schema.name === el.path);
|
|
129
164
|
if (config) {
|
|
130
|
-
el.message = `Missing ${config.location
|
|
165
|
+
el.message = `Missing ${config.param.location} value: ${config.schema.name}`;
|
|
131
166
|
}
|
|
132
167
|
}
|
|
133
168
|
}
|
|
@@ -184,7 +219,7 @@ export class EndpointUtil {
|
|
|
184
219
|
const endpointFilters = [
|
|
185
220
|
...(controller?.filters ?? []).map(fn => fn.bind(controller?.instance)),
|
|
186
221
|
...(endpoint.filters ?? []).map(fn => fn.bind(endpoint.instance)),
|
|
187
|
-
...(endpoint.
|
|
222
|
+
...(endpoint.parameters.filter(cfg => cfg.resolve).map(fn => fn.resolve!))
|
|
188
223
|
]
|
|
189
224
|
.map(fn => ({ filter: fn }));
|
|
190
225
|
|
|
@@ -202,14 +237,14 @@ export class EndpointUtil {
|
|
|
202
237
|
* Get bound endpoints, honoring the conditional status
|
|
203
238
|
*/
|
|
204
239
|
static async getBoundEndpoints(c: Class): Promise<EndpointConfig[]> {
|
|
205
|
-
const config =
|
|
240
|
+
const config = ControllerRegistryIndex.getConfig(c);
|
|
206
241
|
|
|
207
242
|
// Skip registering conditional controllers
|
|
208
243
|
if (config.conditional && !await config.conditional()) {
|
|
209
244
|
return [];
|
|
210
245
|
}
|
|
211
246
|
|
|
212
|
-
config.instance = await
|
|
247
|
+
config.instance = await DependencyRegistryIndex.getInstance(config.class);
|
|
213
248
|
|
|
214
249
|
if (Runtime.dynamic) {
|
|
215
250
|
config.instance = RetargettingProxy.unwrap(config.instance);
|
package/src/util/net.ts
CHANGED
|
@@ -58,4 +58,18 @@ export class NetUtil {
|
|
|
58
58
|
|
|
59
59
|
return useIPv4 ? '0.0.0.0' : '::';
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Free a port if it is in use, typically used to resolve port conflicts.
|
|
64
|
+
* @param err The error that may indicate a port conflict
|
|
65
|
+
* @returns Returns true if the port was freed, false if not handled
|
|
66
|
+
*/
|
|
67
|
+
static async freePortOnConflict(err: unknown): Promise<boolean> {
|
|
68
|
+
if (NetUtil.isPortUsedError(err) && typeof err.port === 'number') {
|
|
69
|
+
await NetUtil.freePort(err.port);
|
|
70
|
+
return true;
|
|
71
|
+
} else {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
61
75
|
}
|
|
@@ -64,30 +64,11 @@ export class WebTestDispatchUtil {
|
|
|
64
64
|
return response;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
static
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (query && Object.keys(query).length) {
|
|
72
|
-
const pairs = Object.fromEntries(Object.entries(query).map(([k, v]) => [k, v === null || v === undefined ? '' : `${v}`] as const));
|
|
73
|
-
q = `?${new URLSearchParams(pairs).toString()}`;
|
|
67
|
+
static buildPath(request: WebRequest): string {
|
|
68
|
+
const params = new URLSearchParams();
|
|
69
|
+
for (const [k, v] of Object.entries(request.context.httpQuery ?? {})) {
|
|
70
|
+
params.set(k, v === null || v === undefined ? '' : `${v}`);
|
|
74
71
|
}
|
|
75
|
-
|
|
76
|
-
const finalPath = `${path}${q}`;
|
|
77
|
-
|
|
78
|
-
const body: RequestInit['body'] =
|
|
79
|
-
WebBodyUtil.isRaw(request.body) ?
|
|
80
|
-
await toBuffer(request.body) :
|
|
81
|
-
castTo(request.body);
|
|
82
|
-
|
|
83
|
-
return { path: finalPath, init: { headers, method, body } };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
static async fromFetchResponse(response: Response): Promise<WebResponse> {
|
|
87
|
-
return new WebResponse({
|
|
88
|
-
body: Buffer.from(await response.arrayBuffer()),
|
|
89
|
-
context: { httpStatusCode: response.status },
|
|
90
|
-
headers: response.headers
|
|
91
|
-
});
|
|
72
|
+
return [request.context.path, params.toString()].join('?').replace(/[?]$/, '');
|
|
92
73
|
}
|
|
93
74
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Registry } from '@travetto/registry';
|
|
2
2
|
import { castTo, Class } from '@travetto/runtime';
|
|
3
3
|
import { AfterAll, BeforeAll } from '@travetto/test';
|
|
4
|
-
import {
|
|
4
|
+
import { DependencyRegistryIndex, Injectable } from '@travetto/di';
|
|
5
5
|
import { ConfigSource, ConfigSpec } from '@travetto/config';
|
|
6
|
+
import { Schema } from '@travetto/schema';
|
|
6
7
|
|
|
7
8
|
import { WebDispatcher } from '../../../src/types/dispatch.ts';
|
|
8
9
|
import { WebRequest, WebRequestContext } from '../../../src/types/request.ts';
|
|
@@ -18,7 +19,7 @@ export class WebTestConfig implements ConfigSource {
|
|
|
18
19
|
cookie: { secure: false },
|
|
19
20
|
trustProxy: { ips: ['*'] },
|
|
20
21
|
http: {
|
|
21
|
-
|
|
22
|
+
tls: false,
|
|
22
23
|
port: -1,
|
|
23
24
|
},
|
|
24
25
|
etag: {
|
|
@@ -35,6 +36,7 @@ export class WebTestConfig implements ConfigSource {
|
|
|
35
36
|
/**
|
|
36
37
|
* Base Web Suite
|
|
37
38
|
*/
|
|
39
|
+
@Schema()
|
|
38
40
|
export abstract class BaseWebSuite {
|
|
39
41
|
|
|
40
42
|
#cleanup?: () => void;
|
|
@@ -45,9 +47,9 @@ export abstract class BaseWebSuite {
|
|
|
45
47
|
|
|
46
48
|
@BeforeAll()
|
|
47
49
|
async initServer(): Promise<void> {
|
|
48
|
-
await
|
|
50
|
+
await Registry.init();
|
|
49
51
|
this.#cleanup = await this.serve?.();
|
|
50
|
-
this.#dispatcher = await
|
|
52
|
+
this.#dispatcher = await DependencyRegistryIndex.getInstance(this.dispatcherType);
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
@AfterAll()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
3
|
import { Suite, Test } from '@travetto/test';
|
|
4
|
-
import { Schema,
|
|
5
|
-
import { Controller, Post, Get,
|
|
4
|
+
import { Schema, SchemaRegistryIndex, ValidationResultError, Validator } from '@travetto/schema';
|
|
5
|
+
import { Controller, Post, Get, ControllerRegistryIndex, WebResponse, PathParam, QueryParam, HttpMethod } from '@travetto/web';
|
|
6
6
|
|
|
7
7
|
import { BaseWebSuite } from './base.ts';
|
|
8
8
|
|
|
@@ -104,10 +104,16 @@ class SchemaAPI {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function getEndpoint(path: string, method: HttpMethod) {
|
|
107
|
-
return
|
|
107
|
+
return ControllerRegistryIndex.getConfig(SchemaAPI)
|
|
108
108
|
.endpoints.find(x => x.path === path && x.httpMethod === method)!;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function getEndpointResponse(path: string, method: HttpMethod) {
|
|
112
|
+
const ep = getEndpoint(path, method);
|
|
113
|
+
const resp = SchemaRegistryIndex.getMethodConfig(SchemaAPI, ep.methodName);
|
|
114
|
+
return resp?.returnType?.type;
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
@Suite()
|
|
112
118
|
export abstract class SchemaWebServerSuite extends BaseWebSuite {
|
|
113
119
|
|
|
@@ -226,50 +232,50 @@ export abstract class SchemaWebServerSuite extends BaseWebSuite {
|
|
|
226
232
|
|
|
227
233
|
@Test()
|
|
228
234
|
async verifyVoid() {
|
|
229
|
-
const
|
|
230
|
-
assert(
|
|
235
|
+
const responseType = getEndpointResponse('/void', 'GET');
|
|
236
|
+
assert(responseType === undefined);
|
|
231
237
|
}
|
|
232
238
|
|
|
233
239
|
@Test()
|
|
234
240
|
async verifyVoidAll() {
|
|
235
|
-
const
|
|
236
|
-
assert(
|
|
241
|
+
const responseType = getEndpointResponse('/voidAll', 'GET');
|
|
242
|
+
assert(responseType === undefined);
|
|
237
243
|
}
|
|
238
244
|
|
|
239
245
|
@Test()
|
|
240
246
|
async verifyList() {
|
|
241
|
-
const
|
|
242
|
-
assert(
|
|
247
|
+
const responseType = getEndpointResponse('/users', 'GET');
|
|
248
|
+
assert(responseType === User);
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
@Test()
|
|
246
252
|
async verifyShapeAll() {
|
|
247
|
-
const
|
|
248
|
-
console.log(`${
|
|
253
|
+
const responseType = getEndpointResponse('/allShapes', 'GET');
|
|
254
|
+
console.log(`${responseType}`);
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
@Test()
|
|
252
258
|
async verifyShapeClass() {
|
|
253
|
-
const
|
|
254
|
-
assert(
|
|
255
|
-
assert(
|
|
259
|
+
const responseType = getEndpointResponse('/classShape/:shape', 'GET');
|
|
260
|
+
assert(responseType);
|
|
261
|
+
assert(SchemaRegistryIndex.has(responseType));
|
|
256
262
|
}
|
|
257
263
|
|
|
258
264
|
@Test()
|
|
259
265
|
async verifyRenderable() {
|
|
260
|
-
const
|
|
261
|
-
assert(
|
|
266
|
+
const responseType = getEndpointResponse('/renderable/:age', 'GET');
|
|
267
|
+
assert(responseType === undefined);
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
@Test()
|
|
265
271
|
async verifyCustomSerializeable() {
|
|
266
|
-
const
|
|
267
|
-
assert(
|
|
272
|
+
const responseType = getEndpointResponse('/customSerialize', 'GET');
|
|
273
|
+
assert(responseType === User);
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
@Test()
|
|
271
277
|
async verifyCustomSerializeable2() {
|
|
272
|
-
const
|
|
273
|
-
assert(
|
|
278
|
+
const responseType = getEndpointResponse('/customSerialize2', 'GET');
|
|
279
|
+
assert(responseType === User);
|
|
274
280
|
}
|
|
275
281
|
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
3
|
import { Test, Suite, BeforeAll } from '@travetto/test';
|
|
4
|
+
import { Registry } from '@travetto/registry';
|
|
4
5
|
|
|
5
6
|
import { BaseWebSuite } from './base.ts';
|
|
6
7
|
import { TestController } from './controller.ts';
|
|
7
|
-
import { ControllerRegistry } from '../../../src/registry/controller.ts';
|
|
8
8
|
|
|
9
9
|
@Suite()
|
|
10
10
|
export abstract class StandardWebServerSuite extends BaseWebSuite {
|
|
11
11
|
|
|
12
12
|
@BeforeAll()
|
|
13
13
|
async init() {
|
|
14
|
-
|
|
15
|
-
await ControllerRegistry.install(TestController, { type: 'added' });
|
|
14
|
+
Registry.process([{ type: 'added', curr: TestController }]);
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
@Test()
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { DependencyRegistry } from '@travetto/di';
|
|
2
|
-
import { type Primitive, type Class, asFull, castTo, asConstructable, ClassInstance } from '@travetto/runtime';
|
|
3
|
-
import { MetadataRegistry } from '@travetto/registry';
|
|
4
|
-
|
|
5
|
-
import { EndpointConfig, ControllerConfig, EndpointDecorator, EndpointParamConfig, EndpointFunctionDescriptor, EndpointFunction } from './types.ts';
|
|
6
|
-
import { WebChainedFilter, WebFilter } from '../types/filter.ts';
|
|
7
|
-
import { WebInterceptor } from '../types/interceptor.ts';
|
|
8
|
-
import { WebHeaders } from '../types/headers.ts';
|
|
9
|
-
|
|
10
|
-
import { WebAsyncContext } from '../context.ts';
|
|
11
|
-
|
|
12
|
-
type ValidFieldNames<T> = {
|
|
13
|
-
[K in keyof T]:
|
|
14
|
-
(T[K] extends (Primitive | undefined) ? K :
|
|
15
|
-
(T[K] extends (Function | undefined) ? never :
|
|
16
|
-
K))
|
|
17
|
-
}[keyof T];
|
|
18
|
-
|
|
19
|
-
type RetainFields<T> = Pick<T, ValidFieldNames<T>>;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Controller registry
|
|
23
|
-
*/
|
|
24
|
-
class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointConfig> {
|
|
25
|
-
|
|
26
|
-
#endpointsById = new Map<string, EndpointConfig>();
|
|
27
|
-
|
|
28
|
-
constructor() {
|
|
29
|
-
super(DependencyRegistry);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async #bindContextParams<T>(inst: ClassInstance<T>): Promise<void> {
|
|
33
|
-
const ctx = await DependencyRegistry.getInstance(WebAsyncContext);
|
|
34
|
-
const map = this.get(inst.constructor).contextParams;
|
|
35
|
-
for (const [field, type] of Object.entries(map)) {
|
|
36
|
-
Object.defineProperty(inst, field, { get: ctx.getSource(type) });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
getEndpointById(id: string): EndpointConfig | undefined {
|
|
41
|
-
return this.#endpointsById.get(id.replace(':', '#'));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
createPending(cls: Class): ControllerConfig {
|
|
45
|
-
return {
|
|
46
|
-
class: cls,
|
|
47
|
-
filters: [],
|
|
48
|
-
interceptorConfigs: [],
|
|
49
|
-
basePath: '',
|
|
50
|
-
externalName: cls.name.replace(/(Controller|Web|Service)$/, ''),
|
|
51
|
-
endpoints: [],
|
|
52
|
-
contextParams: {},
|
|
53
|
-
responseHeaders: {}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
createPendingField(cls: Class, endpoint: EndpointFunction): EndpointConfig {
|
|
58
|
-
const controllerConf = this.getOrCreatePending(cls);
|
|
59
|
-
|
|
60
|
-
const fieldConf: EndpointConfig = {
|
|
61
|
-
id: `${cls.name}#${endpoint.name}`,
|
|
62
|
-
path: '/',
|
|
63
|
-
fullPath: '/',
|
|
64
|
-
cacheable: false,
|
|
65
|
-
allowsBody: false,
|
|
66
|
-
class: cls,
|
|
67
|
-
filters: [],
|
|
68
|
-
params: [],
|
|
69
|
-
interceptorConfigs: [],
|
|
70
|
-
name: endpoint.name,
|
|
71
|
-
endpoint,
|
|
72
|
-
responseHeaders: {},
|
|
73
|
-
finalizedResponseHeaders: new WebHeaders(),
|
|
74
|
-
responseFinalizer: undefined
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
controllerConf.endpoints!.push(fieldConf);
|
|
78
|
-
|
|
79
|
-
return fieldConf;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Register the endpoint config
|
|
84
|
-
* @param cls Controller class
|
|
85
|
-
* @param endpoint Endpoint target function
|
|
86
|
-
*/
|
|
87
|
-
getOrCreateEndpointConfig<T>(cls: Class<T>, endpoint: EndpointFunction): EndpointConfig {
|
|
88
|
-
const fieldConf = this.getOrCreatePendingField(cls, endpoint);
|
|
89
|
-
return asFull(fieldConf);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Register the controller filter
|
|
94
|
-
* @param cls Controller class
|
|
95
|
-
* @param filter The filter to call
|
|
96
|
-
*/
|
|
97
|
-
registerControllerFilter(target: Class, filter: WebFilter | WebChainedFilter): void {
|
|
98
|
-
const config = this.getOrCreatePending(target);
|
|
99
|
-
config.filters!.push(filter);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Register the controller filter
|
|
104
|
-
* @param cls Controller class
|
|
105
|
-
* @param endpoint Endpoint function
|
|
106
|
-
* @param filter The filter to call
|
|
107
|
-
*/
|
|
108
|
-
registerEndpointFilter(target: Class, endpoint: EndpointFunction, filter: WebFilter | WebChainedFilter): void {
|
|
109
|
-
const config = this.getOrCreateEndpointConfig(target, endpoint);
|
|
110
|
-
config.filters!.unshift(filter);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Register the endpoint parameter
|
|
115
|
-
* @param cls Controller class
|
|
116
|
-
* @param endpoint Endpoint function
|
|
117
|
-
* @param param The param config
|
|
118
|
-
* @param index The parameter index
|
|
119
|
-
*/
|
|
120
|
-
registerEndpointParameter(target: Class, endpoint: EndpointFunction, param: EndpointParamConfig, index: number): void {
|
|
121
|
-
const config = this.getOrCreateEndpointConfig(target, endpoint);
|
|
122
|
-
if (index >= config.params.length) {
|
|
123
|
-
config.params.length = index + 1;
|
|
124
|
-
}
|
|
125
|
-
config.params[index] = param;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Register the endpoint interceptor config
|
|
130
|
-
* @param cls Controller class
|
|
131
|
-
* @param endpoint Endpoint function
|
|
132
|
-
* @param param The param config
|
|
133
|
-
* @param index The parameter index
|
|
134
|
-
*/
|
|
135
|
-
registerEndpointInterceptorConfig<T extends WebInterceptor>(target: Class, endpoint: EndpointFunction, interceptorCls: Class<T>, config: Partial<T['config']>): void {
|
|
136
|
-
const endpointConfig = this.getOrCreateEndpointConfig(target, endpoint);
|
|
137
|
-
(endpointConfig.interceptorConfigs ??= []).push([interceptorCls, { ...config }]);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Register the controller interceptor config
|
|
142
|
-
* @param cls Controller class
|
|
143
|
-
* @param param The param config
|
|
144
|
-
* @param index The parameter index
|
|
145
|
-
*/
|
|
146
|
-
registerControllerInterceptorConfig<T extends WebInterceptor>(target: Class, interceptorCls: Class<T>, config: Partial<T['config']>): void {
|
|
147
|
-
const controllerConfig = this.getOrCreatePending(target);
|
|
148
|
-
(controllerConfig.interceptorConfigs ??= []).push([interceptorCls, { ...config }]);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Register a controller context param
|
|
153
|
-
* @param target Controller class
|
|
154
|
-
* @param field Field on controller to bind context param to
|
|
155
|
-
* @param type The context type to bind to field
|
|
156
|
-
*/
|
|
157
|
-
registerControllerContextParam<T>(target: Class, field: string, type: Class<T>): void {
|
|
158
|
-
const controllerConfig = this.getOrCreatePending(target);
|
|
159
|
-
controllerConfig.contextParams![field] = type;
|
|
160
|
-
DependencyRegistry.registerPostConstructHandler(target, 'ContextParam', inst => this.#bindContextParams(inst));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Create a filter decorator
|
|
165
|
-
* @param filter The filter to call
|
|
166
|
-
*/
|
|
167
|
-
createFilterDecorator(filter: WebFilter): EndpointDecorator {
|
|
168
|
-
return (target: unknown, prop?: symbol | string, descriptor?: EndpointFunctionDescriptor): void => {
|
|
169
|
-
if (prop) {
|
|
170
|
-
this.registerEndpointFilter(asConstructable(target).constructor, descriptor!.value!, filter);
|
|
171
|
-
} else {
|
|
172
|
-
this.registerControllerFilter(castTo(target), filter);
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Register a controller/endpoint with specific config for an interceptor
|
|
179
|
-
* @param cls The interceptor to register data for
|
|
180
|
-
* @param cfg The partial config override
|
|
181
|
-
*/
|
|
182
|
-
createInterceptorConfigDecorator<T extends WebInterceptor>(
|
|
183
|
-
cls: Class<T>,
|
|
184
|
-
cfg: Partial<RetainFields<T['config']>>,
|
|
185
|
-
extra?: Partial<EndpointConfig & ControllerConfig>
|
|
186
|
-
): EndpointDecorator {
|
|
187
|
-
return (target: unknown, prop?: symbol | string, descriptor?: EndpointFunctionDescriptor): void => {
|
|
188
|
-
const outCls: Class = descriptor ? asConstructable(target).constructor : castTo(target);
|
|
189
|
-
if (prop && descriptor) {
|
|
190
|
-
this.registerEndpointInterceptorConfig(outCls, descriptor!.value!, cls, castTo(cfg));
|
|
191
|
-
extra && this.registerPendingEndpoint(outCls, descriptor, extra);
|
|
192
|
-
} else {
|
|
193
|
-
this.registerControllerInterceptorConfig(outCls, cls, castTo(cfg));
|
|
194
|
-
extra && this.registerPending(outCls, extra);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Merge describable
|
|
201
|
-
* @param src Root describable (controller, endpoint)
|
|
202
|
-
* @param dest Target (controller, endpoint)
|
|
203
|
-
*/
|
|
204
|
-
mergeCommon(src: Partial<ControllerConfig | EndpointConfig>, dest: Partial<ControllerConfig | EndpointConfig>): void {
|
|
205
|
-
dest.filters = [...(dest.filters ?? []), ...(src.filters ?? [])];
|
|
206
|
-
dest.interceptorConfigs = [...(dest.interceptorConfigs ?? []), ...(src.interceptorConfigs ?? [])];
|
|
207
|
-
dest.interceptorExclude = dest.interceptorExclude ?? src.interceptorExclude;
|
|
208
|
-
dest.title = src.title || dest.title;
|
|
209
|
-
dest.description = src.description || dest.description;
|
|
210
|
-
dest.documented = src.documented ?? dest.documented;
|
|
211
|
-
dest.responseHeaders = { ...src.responseHeaders, ...dest.responseHeaders };
|
|
212
|
-
dest.responseContext = { ...src.responseContext, ...dest.responseContext };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Register an endpoint as pending
|
|
217
|
-
* @param target Controller class
|
|
218
|
-
* @param descriptor Prop descriptor
|
|
219
|
-
* @param config The endpoint config
|
|
220
|
-
*/
|
|
221
|
-
registerPendingEndpoint(target: Class, descriptor: EndpointFunctionDescriptor, config: Partial<EndpointConfig>): EndpointFunctionDescriptor {
|
|
222
|
-
const srcConf = this.getOrCreateEndpointConfig(target, descriptor.value!);
|
|
223
|
-
srcConf.cacheable = config.cacheable ?? srcConf.cacheable;
|
|
224
|
-
srcConf.httpMethod = config.httpMethod ?? srcConf.httpMethod;
|
|
225
|
-
srcConf.allowsBody = config.allowsBody ?? srcConf.allowsBody;
|
|
226
|
-
srcConf.path = config.path || srcConf.path;
|
|
227
|
-
srcConf.responseType = config.responseType ?? srcConf.responseType;
|
|
228
|
-
srcConf.requestType = config.requestType ?? srcConf.requestType;
|
|
229
|
-
srcConf.params = (config.params ?? srcConf.params).map(x => ({ ...x }));
|
|
230
|
-
srcConf.responseFinalizer = config.responseFinalizer ?? srcConf.responseFinalizer;
|
|
231
|
-
|
|
232
|
-
// Ensure path starts with '/'
|
|
233
|
-
if (!srcConf.path.startsWith('/')) {
|
|
234
|
-
srcConf.path = `/${srcConf.path}`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
this.mergeCommon(config, srcConf);
|
|
238
|
-
|
|
239
|
-
return descriptor;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Register a pending configuration
|
|
244
|
-
* @param target The target class
|
|
245
|
-
* @param config The controller configuration
|
|
246
|
-
*/
|
|
247
|
-
registerPending(target: Class, config: Partial<ControllerConfig>): void {
|
|
248
|
-
const srcConf = this.getOrCreatePending(target);
|
|
249
|
-
srcConf.basePath = config.basePath || srcConf.basePath;
|
|
250
|
-
|
|
251
|
-
if (!srcConf.basePath!.startsWith('/')) {
|
|
252
|
-
srcConf.basePath = `/${srcConf.basePath}`;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
srcConf.contextParams = { ...srcConf.contextParams, ...config.contextParams };
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
this.mergeCommon(config, srcConf);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Finalize endpoints, removing duplicates based on ids
|
|
263
|
-
*/
|
|
264
|
-
onInstallFinalize(cls: Class): ControllerConfig {
|
|
265
|
-
const final = asFull(this.getOrCreatePending(cls));
|
|
266
|
-
|
|
267
|
-
// Store for lookup
|
|
268
|
-
for (const ep of final.endpoints) {
|
|
269
|
-
this.#endpointsById.set(ep.id, ep);
|
|
270
|
-
// Store full path from base for use in other contexts
|
|
271
|
-
ep.fullPath = `/${final.basePath}/${ep.path}`.replace(/[/]{1,4}/g, '/').replace(/(.)[/]$/, (_, a) => a);
|
|
272
|
-
ep.finalizedResponseHeaders = new WebHeaders({ ...final.responseHeaders, ...ep.responseHeaders });
|
|
273
|
-
ep.responseContext = { ...final.responseContext, ...ep.responseContext };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (this.has(final.basePath)) {
|
|
277
|
-
console.debug('Reloading controller', { name: cls.name, path: final.basePath });
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return final;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
onUninstallFinalize<T>(cls: Class<T>): void {
|
|
284
|
-
const toDelete = [...this.#endpointsById.values()].filter(x => x.class.name === cls.name);
|
|
285
|
-
for (const k of toDelete) {
|
|
286
|
-
this.#endpointsById.delete(k.id);
|
|
287
|
-
}
|
|
288
|
-
super.onUninstallFinalize(cls);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export const ControllerRegistry = new $ControllerRegistry();
|