@travetto/web 6.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +734 -0
  2. package/__index__.ts +44 -0
  3. package/package.json +66 -0
  4. package/src/common/global.ts +30 -0
  5. package/src/config.ts +18 -0
  6. package/src/context.ts +49 -0
  7. package/src/decorator/common.ts +87 -0
  8. package/src/decorator/controller.ts +13 -0
  9. package/src/decorator/endpoint.ts +102 -0
  10. package/src/decorator/param.ts +64 -0
  11. package/src/interceptor/accept.ts +70 -0
  12. package/src/interceptor/body-parse.ts +123 -0
  13. package/src/interceptor/compress.ts +119 -0
  14. package/src/interceptor/context.ts +23 -0
  15. package/src/interceptor/cookies.ts +97 -0
  16. package/src/interceptor/cors.ts +94 -0
  17. package/src/interceptor/decompress.ts +91 -0
  18. package/src/interceptor/etag.ts +99 -0
  19. package/src/interceptor/logging.ts +71 -0
  20. package/src/interceptor/respond.ts +26 -0
  21. package/src/interceptor/response-cache.ts +47 -0
  22. package/src/interceptor/trust-proxy.ts +53 -0
  23. package/src/registry/controller.ts +288 -0
  24. package/src/registry/types.ts +229 -0
  25. package/src/registry/visitor.ts +52 -0
  26. package/src/router/base.ts +67 -0
  27. package/src/router/standard.ts +59 -0
  28. package/src/types/cookie.ts +18 -0
  29. package/src/types/core.ts +33 -0
  30. package/src/types/dispatch.ts +23 -0
  31. package/src/types/error.ts +10 -0
  32. package/src/types/filter.ts +7 -0
  33. package/src/types/headers.ts +108 -0
  34. package/src/types/interceptor.ts +54 -0
  35. package/src/types/message.ts +33 -0
  36. package/src/types/request.ts +22 -0
  37. package/src/types/response.ts +20 -0
  38. package/src/util/body.ts +220 -0
  39. package/src/util/common.ts +142 -0
  40. package/src/util/cookie.ts +145 -0
  41. package/src/util/endpoint.ts +277 -0
  42. package/src/util/mime.ts +36 -0
  43. package/src/util/net.ts +61 -0
  44. package/support/test/dispatch-util.ts +90 -0
  45. package/support/test/dispatcher.ts +15 -0
  46. package/support/test/suite/base.ts +61 -0
  47. package/support/test/suite/controller.ts +103 -0
  48. package/support/test/suite/schema.ts +275 -0
  49. package/support/test/suite/standard.ts +178 -0
  50. package/support/transformer.web.ts +207 -0
@@ -0,0 +1,53 @@
1
+ import { Config } from '@travetto/config';
2
+ import { Inject, Injectable } from '@travetto/di';
3
+ import { castTo } from '@travetto/runtime';
4
+
5
+ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
6
+ import { WebInterceptorCategory } from '../types/core.ts';
7
+ import { WebResponse } from '../types/response.ts';
8
+ import { WebChainedContext } from '../types/filter.ts';
9
+
10
+ @Config('web.trustProxy')
11
+ export class TrustProxyConfig {
12
+ /**
13
+ * Enforces trust rules for X-Forwarded-* headers
14
+ */
15
+ applies = true;
16
+ /**
17
+ * The accepted ips
18
+ */
19
+ ips: string[] = [];
20
+ }
21
+
22
+ @Injectable()
23
+ export class TrustProxyInterceptor implements WebInterceptor<TrustProxyConfig> {
24
+
25
+ category: WebInterceptorCategory = 'pre-request';
26
+
27
+ @Inject()
28
+ config: TrustProxyConfig;
29
+
30
+ applies({ config }: WebInterceptorContext<TrustProxyConfig>): boolean {
31
+ return config.applies;
32
+ }
33
+
34
+ filter({ request, next, config }: WebChainedContext<TrustProxyConfig>): Promise<WebResponse> {
35
+ const forwardedFor = request.headers.get('X-Forwarded-For');
36
+
37
+ if (forwardedFor) {
38
+ const connection = request.context.connection ?? {};
39
+ if (config.ips[0] === '*' || (connection.ip && config.ips.includes(connection.ip))) {
40
+ connection.httpProtocol = castTo(request.headers.get('X-Forwarded-Proto')!) || connection.httpProtocol;
41
+ connection.host = request.headers.get('X-Forwarded-Host') || connection.host;
42
+ connection.ip = forwardedFor;
43
+ Object.assign(request.context, { connection });
44
+ }
45
+ }
46
+
47
+ request.headers.delete('X-Forwarded-For');
48
+ request.headers.delete('X-Forwarded-Proto');
49
+ request.headers.delete('X-Forwarded-Host');
50
+
51
+ return next();
52
+ }
53
+ }
@@ -0,0 +1,288 @@
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
+ };
54
+ }
55
+
56
+ createPendingField(cls: Class, endpoint: EndpointFunction): EndpointConfig {
57
+ const controllerConf = this.getOrCreatePending(cls);
58
+
59
+ const fieldConf: EndpointConfig = {
60
+ id: `${cls.name}#${endpoint.name}`,
61
+ path: '/',
62
+ fullPath: '/',
63
+ cacheable: false,
64
+ allowsBody: false,
65
+ class: cls,
66
+ filters: [],
67
+ params: [],
68
+ interceptorConfigs: [],
69
+ name: endpoint.name,
70
+ endpoint,
71
+ responseHeaderMap: new WebHeaders(),
72
+ responseFinalizer: undefined
73
+ };
74
+
75
+ controllerConf.endpoints!.push(fieldConf);
76
+
77
+ return fieldConf;
78
+ }
79
+
80
+ /**
81
+ * Register the endpoint config
82
+ * @param cls Controller class
83
+ * @param endpoint Endpoint target function
84
+ */
85
+ getOrCreateEndpointConfig<T>(cls: Class<T>, endpoint: EndpointFunction): EndpointConfig {
86
+ const fieldConf = this.getOrCreatePendingField(cls, endpoint);
87
+ return asFull(fieldConf);
88
+ }
89
+
90
+ /**
91
+ * Register the controller filter
92
+ * @param cls Controller class
93
+ * @param filter The filter to call
94
+ */
95
+ registerControllerFilter(target: Class, filter: WebFilter | WebChainedFilter): void {
96
+ const config = this.getOrCreatePending(target);
97
+ config.filters!.push(filter);
98
+ }
99
+
100
+ /**
101
+ * Register the controller filter
102
+ * @param cls Controller class
103
+ * @param endpoint Endpoint function
104
+ * @param filter The filter to call
105
+ */
106
+ registerEndpointFilter(target: Class, endpoint: EndpointFunction, filter: WebFilter | WebChainedFilter): void {
107
+ const config = this.getOrCreateEndpointConfig(target, endpoint);
108
+ config.filters!.unshift(filter);
109
+ }
110
+
111
+ /**
112
+ * Register the endpoint parameter
113
+ * @param cls Controller class
114
+ * @param endpoint Endpoint function
115
+ * @param param The param config
116
+ * @param index The parameter index
117
+ */
118
+ registerEndpointParameter(target: Class, endpoint: EndpointFunction, param: EndpointParamConfig, index: number): void {
119
+ const config = this.getOrCreateEndpointConfig(target, endpoint);
120
+ if (index >= config.params.length) {
121
+ config.params.length = index + 1;
122
+ }
123
+ config.params[index] = param;
124
+ }
125
+
126
+ /**
127
+ * Register the endpoint interceptor config
128
+ * @param cls Controller class
129
+ * @param endpoint Endpoint function
130
+ * @param param The param config
131
+ * @param index The parameter index
132
+ */
133
+ registerEndpointInterceptorConfig<T extends WebInterceptor>(target: Class, endpoint: EndpointFunction, interceptorCls: Class<T>, config: Partial<T['config']>): void {
134
+ const endpointConfig = this.getOrCreateEndpointConfig(target, endpoint);
135
+ (endpointConfig.interceptorConfigs ??= []).push([interceptorCls, { ...config }]);
136
+ }
137
+
138
+ /**
139
+ * Register the controller interceptor config
140
+ * @param cls Controller class
141
+ * @param param The param config
142
+ * @param index The parameter index
143
+ */
144
+ registerControllerInterceptorConfig<T extends WebInterceptor>(target: Class, interceptorCls: Class<T>, config: Partial<T['config']>): void {
145
+ const controllerConfig = this.getOrCreatePending(target);
146
+ (controllerConfig.interceptorConfigs ??= []).push([interceptorCls, { ...config }]);
147
+ }
148
+
149
+ /**
150
+ * Register a controller context param
151
+ * @param target Controller class
152
+ * @param field Field on controller to bind context param to
153
+ * @param type The context type to bind to field
154
+ */
155
+ registerControllerContextParam<T>(target: Class, field: string, type: Class<T>): void {
156
+ const controllerConfig = this.getOrCreatePending(target);
157
+ controllerConfig.contextParams![field] = type;
158
+ DependencyRegistry.registerPostConstructHandler(target, 'ContextParam', inst => this.#bindContextParams(inst));
159
+ }
160
+
161
+ /**
162
+ * Create a filter decorator
163
+ * @param filter The filter to call
164
+ */
165
+ createFilterDecorator(filter: WebFilter): EndpointDecorator {
166
+ return (target: unknown, prop?: symbol | string, descriptor?: EndpointFunctionDescriptor): void => {
167
+ if (prop) {
168
+ this.registerEndpointFilter(asConstructable(target).constructor, descriptor!.value!, filter);
169
+ } else {
170
+ this.registerControllerFilter(castTo(target), filter);
171
+ }
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Register a controller/endpoint with specific config for an interceptor
177
+ * @param cls The interceptor to register data for
178
+ * @param cfg The partial config override
179
+ */
180
+ createInterceptorConfigDecorator<T extends WebInterceptor>(
181
+ cls: Class<T>,
182
+ cfg: Partial<RetainFields<T['config']>>,
183
+ extra?: Partial<EndpointConfig & ControllerConfig>
184
+ ): EndpointDecorator {
185
+ return (target: unknown, prop?: symbol | string, descriptor?: EndpointFunctionDescriptor): void => {
186
+ const outCls: Class = descriptor ? asConstructable(target).constructor : castTo(target);
187
+ if (prop && descriptor) {
188
+ this.registerEndpointInterceptorConfig(outCls, descriptor!.value!, cls, castTo(cfg));
189
+ extra && this.registerPendingEndpoint(outCls, descriptor, extra);
190
+ } else {
191
+ this.registerControllerInterceptorConfig(outCls, cls, castTo(cfg));
192
+ extra && this.registerPending(outCls, extra);
193
+ }
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Merge describable
199
+ * @param src Root describable (controller, endpoint)
200
+ * @param dest Target (controller, endpoint)
201
+ */
202
+ mergeDescribable(src: Partial<ControllerConfig | EndpointConfig>, dest: Partial<ControllerConfig | EndpointConfig>): void {
203
+ dest.filters = [...(dest.filters ?? []), ...(src.filters ?? [])];
204
+ dest.interceptorConfigs = [...(dest.interceptorConfigs ?? []), ...(src.interceptorConfigs ?? [])];
205
+ dest.interceptorExclude = dest.interceptorExclude ?? src.interceptorExclude;
206
+ dest.title = src.title || dest.title;
207
+ dest.description = src.description || dest.description;
208
+ dest.documented = src.documented ?? dest.documented;
209
+ dest.responseHeaders = { ...src.responseHeaders, ...dest.responseHeaders };
210
+ }
211
+
212
+ /**
213
+ * Register an endpoint as pending
214
+ * @param target Controller class
215
+ * @param descriptor Prop descriptor
216
+ * @param config The endpoint config
217
+ */
218
+ registerPendingEndpoint(target: Class, descriptor: EndpointFunctionDescriptor, config: Partial<EndpointConfig>): EndpointFunctionDescriptor {
219
+ const srcConf = this.getOrCreateEndpointConfig(target, descriptor.value!);
220
+ srcConf.cacheable = config.cacheable ?? srcConf.cacheable;
221
+ srcConf.httpMethod = config.httpMethod ?? srcConf.httpMethod;
222
+ srcConf.allowsBody = config.allowsBody ?? srcConf.allowsBody;
223
+ srcConf.path = config.path || srcConf.path;
224
+ srcConf.responseType = config.responseType ?? srcConf.responseType;
225
+ srcConf.requestType = config.requestType ?? srcConf.requestType;
226
+ srcConf.params = (config.params ?? srcConf.params).map(x => ({ ...x }));
227
+ srcConf.responseFinalizer = config.responseFinalizer ?? srcConf.responseFinalizer;
228
+
229
+ // Ensure path starts with '/'
230
+ if (!srcConf.path.startsWith('/')) {
231
+ srcConf.path = `/${srcConf.path}`;
232
+ }
233
+
234
+ this.mergeDescribable(config, srcConf);
235
+
236
+ return descriptor;
237
+ }
238
+
239
+ /**
240
+ * Register a pending configuration
241
+ * @param target The target class
242
+ * @param config The controller configuration
243
+ */
244
+ registerPending(target: Class, config: Partial<ControllerConfig>): void {
245
+ const srcConf = this.getOrCreatePending(target);
246
+ srcConf.basePath = config.basePath || srcConf.basePath;
247
+
248
+ if (!srcConf.basePath!.startsWith('/')) {
249
+ srcConf.basePath = `/${srcConf.basePath}`;
250
+ }
251
+
252
+ srcConf.contextParams = { ...srcConf.contextParams, ...config.contextParams };
253
+
254
+
255
+ this.mergeDescribable(config, srcConf);
256
+ }
257
+
258
+ /**
259
+ * Finalize endpoints, removing duplicates based on ids
260
+ */
261
+ onInstallFinalize(cls: Class): ControllerConfig {
262
+ const final = asFull(this.getOrCreatePending(cls));
263
+
264
+ // Store for lookup
265
+ for (const ep of final.endpoints) {
266
+ this.#endpointsById.set(ep.id, ep);
267
+ // Store full path from base for use in other contexts
268
+ ep.fullPath = `/${final.basePath}/${ep.path}`.replace(/[/]{1,4}/g, '/').replace(/(.)[/]$/, (_, a) => a);
269
+ ep.responseHeaderMap = new WebHeaders({ ...final.responseHeaders ?? {}, ...ep.responseHeaders ?? {} });
270
+ }
271
+
272
+ if (this.has(final.basePath)) {
273
+ console.debug('Reloading controller', { name: cls.name, path: final.basePath });
274
+ }
275
+
276
+ return final;
277
+ }
278
+
279
+ onUninstallFinalize<T>(cls: Class<T>): void {
280
+ const toDelete = [...this.#endpointsById.values()].filter(x => x.class.name === cls.name);
281
+ for (const k of toDelete) {
282
+ this.#endpointsById.delete(k.id);
283
+ }
284
+ super.onUninstallFinalize(cls);
285
+ }
286
+ }
287
+
288
+ export const ControllerRegistry = new $ControllerRegistry();
@@ -0,0 +1,229 @@
1
+ import type { Any, Class, TypedFunction } from '@travetto/runtime';
2
+ import type { FieldConfig, ClassConfig } from '@travetto/schema';
3
+
4
+ import type { WebInterceptor } from '../types/interceptor.ts';
5
+ import type { WebChainedFilter, WebFilter } from '../types/filter.ts';
6
+ import { HttpMethod } from '../types/core.ts';
7
+ import { WebHeaders } from '../types/headers.ts';
8
+ import { WebResponse } from '../types/response.ts';
9
+ import { WebRequest } from '../types/request.ts';
10
+
11
+ export type EndpointFunction = TypedFunction<Any, unknown>;
12
+ export type EndpointFunctionDescriptor = TypedPropertyDescriptor<EndpointFunction>;
13
+
14
+ /**
15
+ * Endpoint decorator for composition of routing logic
16
+ */
17
+ export type EndpointDecorator = (
18
+ (<T extends Class>(target: T) => void) &
19
+ (<U>(target: U, prop: string, descriptor?: EndpointFunctionDescriptor) => void)
20
+ );
21
+
22
+ /**
23
+ * Endpoint type
24
+ */
25
+ export type EndpointIOType = {
26
+ type: Class;
27
+ array?: boolean;
28
+ description?: string;
29
+ };
30
+
31
+ /**
32
+ * Describable elements
33
+ */
34
+ export interface DescribableConfig {
35
+ /**
36
+ * The title
37
+ */
38
+ title?: string;
39
+ /**
40
+ * Description
41
+ */
42
+ description?: string;
43
+ /**
44
+ * Is the resource documented
45
+ */
46
+ documented?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Core configuration for endpoints and controllers
51
+ */
52
+ interface CoreConfig {
53
+ /**
54
+ * The related class
55
+ */
56
+ class: Class;
57
+ /**
58
+ * The actual class instance
59
+ */
60
+ instance?: unknown;
61
+ /**
62
+ * List of filters to run on request
63
+ */
64
+ filters: (WebFilter | WebChainedFilter)[];
65
+ /**
66
+ * Set of interceptor configs
67
+ */
68
+ interceptorConfigs?: [Class<WebInterceptor>, unknown][];
69
+ /**
70
+ * Should the resource only be used conditionally?
71
+ */
72
+ conditional?: () => (boolean | Promise<boolean>);
73
+ /**
74
+ * Control which interceptors are excluded
75
+ */
76
+ interceptorExclude?: (val: WebInterceptor) => boolean;
77
+ /**
78
+ * Response headers
79
+ */
80
+ responseHeaders?: Record<string, string>;
81
+ }
82
+
83
+ /**
84
+ * Endpoint param configuration
85
+ */
86
+ export interface EndpointParamConfig {
87
+ /**
88
+ * Name of the parameter
89
+ */
90
+ name?: string;
91
+ /**
92
+ * Raw text of parameter at source
93
+ */
94
+ sourceText?: string;
95
+ /**
96
+ * Location of the parameter
97
+ */
98
+ location: 'path' | 'query' | 'body' | 'header';
99
+ /**
100
+ * Resolves the value by executing with req/res as input
101
+ */
102
+ resolve?: WebFilter;
103
+ /**
104
+ * Extract the value from request
105
+ * @param context The http context with the endpoint param config
106
+ */
107
+ extract?: (ctx: WebRequest, config: EndpointParamConfig) => unknown;
108
+ /**
109
+ * Input prefix for parameter
110
+ */
111
+ prefix?: string;
112
+ }
113
+
114
+ /**
115
+ * Endpoint configuration
116
+ */
117
+ export interface EndpointConfig extends CoreConfig, DescribableConfig {
118
+ /**
119
+ * Unique identifier
120
+ */
121
+ id: string;
122
+ /**
123
+ * The name of the method
124
+ */
125
+ name: string;
126
+ /**
127
+ * Instance the endpoint is for
128
+ */
129
+ instance?: unknown;
130
+ /**
131
+ * Method alias for the endpoint
132
+ */
133
+ httpMethod?: HttpMethod;
134
+ /**
135
+ * Is this endpoint cacheable
136
+ */
137
+ cacheable: boolean;
138
+ /**
139
+ * Does this endpoint allow a body
140
+ */
141
+ allowsBody: boolean;
142
+ /**
143
+ * The path of the endpoint
144
+ */
145
+ path: string;
146
+ /**
147
+ * The function the endpoint will call
148
+ */
149
+ endpoint: EndpointFunction;
150
+ /**
151
+ * The compiled and finalized handler
152
+ */
153
+ filter?: WebFilter;
154
+ /**
155
+ * List of params for the endpoint
156
+ */
157
+ params: EndpointParamConfig[];
158
+ /**
159
+ * The response type
160
+ */
161
+ responseType?: EndpointIOType;
162
+ /**
163
+ * The request type
164
+ */
165
+ requestType?: EndpointIOType;
166
+ /**
167
+ * Full path including controller
168
+ */
169
+ fullPath: string;
170
+ /**
171
+ * Response header map
172
+ */
173
+ responseHeaderMap: WebHeaders;
174
+ /**
175
+ * Response finalizer
176
+ */
177
+ responseFinalizer?: (res: WebResponse) => WebResponse;
178
+ }
179
+
180
+ /**
181
+ * Controller configuration
182
+ */
183
+ export interface ControllerConfig extends CoreConfig, DescribableConfig {
184
+ /**
185
+ * The base path of the controller
186
+ */
187
+ basePath: string;
188
+ /**
189
+ * List of all endpoints
190
+ */
191
+ endpoints: EndpointConfig[];
192
+ /**
193
+ * Client name, used by consuming tools/clients
194
+ */
195
+ externalName: string;
196
+ /**
197
+ * Context parameters to bind at create
198
+ */
199
+ contextParams: Record<string, Class>;
200
+ }
201
+
202
+ /**
203
+ * Controller visitor options
204
+ */
205
+ export type ControllerVisitorOptions = { skipUndocumented?: boolean };
206
+
207
+ /**
208
+ * Controller visitor pattern
209
+ */
210
+ export interface ControllerVisitor<T = unknown> {
211
+
212
+ getOptions?: () => ControllerVisitorOptions;
213
+
214
+ onControllerStart?(controller: ControllerConfig): Promise<unknown> | unknown;
215
+ onControllerEnd?(controller: ControllerConfig): Promise<unknown> | unknown;
216
+
217
+ onEndpointStart?(endpoint: EndpointConfig, controller: ControllerConfig, methodParams: FieldConfig[]): Promise<unknown> | unknown;
218
+ onEndpointEnd?(endpoint: EndpointConfig, controller: ControllerConfig, methodParams: FieldConfig[]): Promise<unknown> | unknown;
219
+
220
+ onSchema?(schema: ClassConfig): Promise<unknown> | unknown;
221
+
222
+ onControllerAdd?(cls: Class): Promise<unknown> | unknown;
223
+ onControllerRemove?(cls: Class): Promise<unknown> | unknown;
224
+
225
+ onSchemaAdd?(cls: Class): Promise<boolean> | boolean;
226
+ onSchemaRemove?(cls: Class): Promise<boolean> | boolean;
227
+
228
+ onComplete?(): T | Promise<T>;
229
+ }
@@ -0,0 +1,52 @@
1
+ import { Class } from '@travetto/runtime';
2
+ import { SchemaRegistry } from '@travetto/schema';
3
+
4
+ import { ControllerVisitor, ControllerVisitorOptions } from './types.ts';
5
+ import { ControllerRegistry } from './controller.ts';
6
+
7
+ /**
8
+ * Supports visiting the controller structure
9
+ */
10
+ export class ControllerVisitUtil {
11
+
12
+ static #onSchemaEvent(visitor: ControllerVisitor, type?: Class): unknown | Promise<unknown> {
13
+ return type && SchemaRegistry.has(type) ? visitor.onSchema?.(SchemaRegistry.get(type)) : undefined;
14
+ }
15
+
16
+ static async visitController(visitor: ControllerVisitor, cls: Class, options: ControllerVisitorOptions = {}): Promise<void> {
17
+ if (visitor.getOptions) {
18
+ options = Object.assign(visitor.getOptions(), options);
19
+ }
20
+
21
+ options.skipUndocumented ??= true;
22
+
23
+ const controller = ControllerRegistry.get(cls);
24
+ if (!controller || controller.documented === false && options.skipUndocumented) {
25
+ return;
26
+ }
27
+
28
+ await visitor.onControllerStart?.(controller);
29
+ for (const endpoint of controller.endpoints) {
30
+ if (endpoint.documented === false && options.skipUndocumented) {
31
+ continue;
32
+ }
33
+
34
+ const params = SchemaRegistry.getMethodSchema(cls, endpoint.name);
35
+ await visitor.onEndpointStart?.(endpoint, controller, params);
36
+ await this.#onSchemaEvent(visitor, endpoint.responseType?.type);
37
+ await this.#onSchemaEvent(visitor, endpoint.requestType?.type);
38
+ for (const param of params) {
39
+ await this.#onSchemaEvent(visitor, param.type);
40
+ }
41
+ await visitor.onEndpointEnd?.(endpoint, controller, params);
42
+ }
43
+ await visitor.onControllerEnd?.(controller);
44
+ }
45
+
46
+ static async visit<T = unknown>(visitor: ControllerVisitor<T>, options: ControllerVisitorOptions = {}): Promise<T> {
47
+ for (const cls of ControllerRegistry.getClasses()) {
48
+ await this.visitController(visitor, cls, options);
49
+ }
50
+ return await visitor.onComplete?.() ?? undefined!;
51
+ }
52
+ }
@@ -0,0 +1,67 @@
1
+ import { Class, toConcrete } from '@travetto/runtime';
2
+ import { DependencyRegistry } from '@travetto/di';
3
+
4
+ import { ControllerConfig, EndpointConfig } from '../registry/types.ts';
5
+ import { ControllerRegistry } from '../registry/controller.ts';
6
+
7
+ import type { WebRouter } from '../types/dispatch.ts';
8
+ import { WebInterceptor } from '../types/interceptor.ts';
9
+ import { WebResponse } from '../types/response.ts';
10
+ import type { WebFilterContext } from '../types/filter.ts';
11
+
12
+ import { EndpointUtil } from '../util/endpoint.ts';
13
+
14
+ /**
15
+ * Supports the base pattern for the most common web router implementations
16
+ */
17
+ export abstract class BaseWebRouter implements WebRouter {
18
+
19
+ #cleanup = new Map<string, Function>();
20
+ #interceptors: WebInterceptor[];
21
+
22
+ async #register(c: Class): Promise<void> {
23
+ const config = ControllerRegistry.get(c);
24
+
25
+ let endpoints = await EndpointUtil.getBoundEndpoints(c);
26
+ endpoints = EndpointUtil.orderEndpoints(endpoints);
27
+
28
+ for (const ep of endpoints) {
29
+ ep.filter = EndpointUtil.createEndpointHandler(this.#interceptors, ep, config);
30
+ }
31
+
32
+ console.debug('Registering Controller Instance', { id: config.class.Ⲑid, path: config.basePath, endpointCount: endpoints.length });
33
+ const fn = await this.register(endpoints, config);
34
+ this.#cleanup.set(c.Ⲑid, fn);
35
+ };
36
+
37
+ /**
38
+ * Initialize router, encapsulating common patterns for standard router setup
39
+ */
40
+ async postConstruct(): Promise<void> {
41
+
42
+ this.#interceptors = await DependencyRegistry.getCandidateInstances(toConcrete<WebInterceptor>());
43
+ this.#interceptors = EndpointUtil.orderInterceptors(this.#interceptors);
44
+ const names = this.#interceptors.map(x => x.constructor.name);
45
+ console.debug('Sorting interceptors', { count: names.length, names });
46
+
47
+ // Register all active
48
+ for (const c of ControllerRegistry.getClasses()) {
49
+ await this.#register(c);
50
+ }
51
+
52
+ // Listen for updates
53
+ ControllerRegistry.on(async e => {
54
+ console.debug('Registry event', { type: e.type, target: (e.curr ?? e.prev)?.Ⲑid });
55
+ if (e.prev && ControllerRegistry.hasExpired(e.prev)) {
56
+ this.#cleanup.get(e.prev.Ⲑid)?.();
57
+ this.#cleanup.delete(e.prev.Ⲑid);
58
+ }
59
+ if (e.curr) {
60
+ await this.#register(e.curr);
61
+ }
62
+ });
63
+ }
64
+
65
+ abstract register(endpoints: EndpointConfig[], controller: ControllerConfig): Promise<() => void>;
66
+ abstract dispatch(ctx: WebFilterContext): Promise<WebResponse>;
67
+ }