@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.
@@ -1,6 +1,6 @@
1
1
  import { asConstructable, castTo, Class, Runtime, TypedObject } from '@travetto/runtime';
2
- import { BindUtil, FieldConfig, SchemaRegistry, SchemaValidator, ValidationResultError } from '@travetto/schema';
3
- import { DependencyRegistry } from '@travetto/di';
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, EndpointParamConfig } from '../registry/types.ts';
12
- import { ControllerRegistry } from '../registry/controller.ts';
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 extractParameter(request: WebRequest, param: EndpointParamConfig, field: FieldConfig, value?: unknown): unknown {
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 param.prefix ? q[param.prefix] : (field.type.Ⲑid ? q : q[name]);
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 fields = SchemaRegistry.getMethodSchema(cls, method);
120
- const extracted = endpoint.params.map((c, i) => this.extractParameter(request, c, fields[i], vals?.[i]));
121
- const params = BindUtil.coerceMethodParams(cls, method, extracted);
122
- await SchemaValidator.validateMethod(cls, method, params, endpoint.params.map(x => x.prefix));
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 = endpoint.params.find(x => x.name === el.path);
163
+ const config = combined.find(x => x.schema.name === el.path);
129
164
  if (config) {
130
- el.message = `Missing ${config.location.replace(/s$/, '')}: ${config.name}`;
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.params.filter(cfg => cfg.resolve).map(fn => fn.resolve!))
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 = ControllerRegistry.get(c);
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 DependencyRegistry.getInstance(config.class);
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 async toFetchRequestInit(request: WebRequest): Promise<{ init: RequestInit, path: string }> {
68
- const { context: { httpQuery: query, httpMethod: method, path }, headers } = request;
69
-
70
- let q = '';
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 { RootRegistry } from '@travetto/registry';
1
+ import { Registry } from '@travetto/registry';
2
2
  import { castTo, Class } from '@travetto/runtime';
3
3
  import { AfterAll, BeforeAll } from '@travetto/test';
4
- import { DependencyRegistry, Injectable } from '@travetto/di';
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
- ssl: { active: false },
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 RootRegistry.init();
50
+ await Registry.init();
49
51
  this.#cleanup = await this.serve?.();
50
- this.#dispatcher = await DependencyRegistry.getInstance(this.dispatcherType);
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, SchemaRegistry, ValidationResultError, Validator } from '@travetto/schema';
5
- import { Controller, Post, Get, ControllerRegistry, WebResponse, PathParam, QueryParam, HttpMethod } from '@travetto/web';
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 ControllerRegistry.get(SchemaAPI)
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 ep = getEndpoint('/void', 'GET');
230
- assert(ep.responseType === undefined);
235
+ const responseType = getEndpointResponse('/void', 'GET');
236
+ assert(responseType === undefined);
231
237
  }
232
238
 
233
239
  @Test()
234
240
  async verifyVoidAll() {
235
- const ep = getEndpoint('/voidAll', 'GET');
236
- assert(ep.responseType === undefined);
241
+ const responseType = getEndpointResponse('/voidAll', 'GET');
242
+ assert(responseType === undefined);
237
243
  }
238
244
 
239
245
  @Test()
240
246
  async verifyList() {
241
- const ep = getEndpoint('/users', 'GET');
242
- assert(ep.responseType?.type === User);
247
+ const responseType = getEndpointResponse('/users', 'GET');
248
+ assert(responseType === User);
243
249
  }
244
250
 
245
251
  @Test()
246
252
  async verifyShapeAll() {
247
- const ep = getEndpoint('/allShapes', 'GET');
248
- console.log(`${ep.responseType}`);
253
+ const responseType = getEndpointResponse('/allShapes', 'GET');
254
+ console.log(`${responseType}`);
249
255
  }
250
256
 
251
257
  @Test()
252
258
  async verifyShapeClass() {
253
- const ep = getEndpoint('/classShape/:shape', 'GET');
254
- assert(ep.responseType);
255
- assert(SchemaRegistry.has(ep.responseType!.type));
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 ep = getEndpoint('/renderable/:age', 'GET');
261
- assert(ep.responseType?.type === undefined);
266
+ const responseType = getEndpointResponse('/renderable/:age', 'GET');
267
+ assert(responseType === undefined);
262
268
  }
263
269
 
264
270
  @Test()
265
271
  async verifyCustomSerializeable() {
266
- const ep = getEndpoint('/customSerialize', 'GET');
267
- assert(ep.responseType?.type === User);
272
+ const responseType = getEndpointResponse('/customSerialize', 'GET');
273
+ assert(responseType === User);
268
274
  }
269
275
 
270
276
  @Test()
271
277
  async verifyCustomSerializeable2() {
272
- const ep = getEndpoint('/customSerialize2', 'GET');
273
- assert(ep.responseType?.type === User);
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
- ControllerRegistry.register(TestController);
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();