@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,178 @@
1
+ import assert from 'node:assert';
2
+
3
+ import { Test, Suite, BeforeAll } from '@travetto/test';
4
+
5
+ import { BaseWebSuite } from './base.ts';
6
+ import { TestController } from './controller.ts';
7
+ import { ControllerRegistry } from '../../../src/registry/controller.ts';
8
+
9
+ @Suite()
10
+ export abstract class StandardWebServerSuite extends BaseWebSuite {
11
+
12
+ @BeforeAll()
13
+ async init() {
14
+ ControllerRegistry.register(TestController);
15
+ await ControllerRegistry.install(TestController, { type: 'added' });
16
+ }
17
+
18
+ @Test()
19
+ async getJSON() {
20
+ const response = await this.request({ context: { httpMethod: 'GET', path: '/test/json' } });
21
+ assert.deepStrictEqual(response.body, { json: true });
22
+ }
23
+
24
+ @Test()
25
+ async getParam() {
26
+ const response = await this.request({ context: { httpMethod: 'POST', path: '/test/param/bob' } });
27
+ assert.deepStrictEqual(response.body, { param: 'bob' });
28
+ }
29
+
30
+ @Test()
31
+ async putQuery() {
32
+ const response = await this.request({
33
+ context: {
34
+ httpMethod: 'PUT', path: '/test/query',
35
+ httpQuery: {
36
+ age: '20'
37
+ }
38
+ }
39
+ });
40
+ assert.deepStrictEqual(response.body, { query: 20 });
41
+
42
+ await assert.rejects(() => this.request({
43
+ context: {
44
+ httpMethod: 'PUT', path: '/test/query',
45
+ httpQuery: {
46
+ age: 'blue'
47
+ }
48
+ }
49
+ }), /Validation errors have occurred/i);
50
+ }
51
+
52
+ @Test()
53
+ async postBody() {
54
+ const response = await this.request({
55
+ context: {
56
+ httpMethod: 'PUT', path: '/test/body',
57
+ },
58
+ body: {
59
+ age: 20
60
+ }
61
+ });
62
+ assert.deepStrictEqual(response.body, { body: 20 });
63
+ }
64
+
65
+ @Test()
66
+ async testCookie() {
67
+ const response = await this.request({
68
+ context: {
69
+ httpMethod: 'DELETE', path: '/test/cookie',
70
+ },
71
+ headers: {
72
+ Cookie: 'orange=yummy'
73
+ }
74
+ });
75
+ const [cookie] = response.headers.getSetCookie();
76
+ assert(cookie !== undefined);
77
+ assert(/flavor.*oreo/.test(cookie));
78
+ assert.deepStrictEqual(response.body, { cookie: 'yummy' });
79
+ }
80
+
81
+ @Test()
82
+ async testRegex() {
83
+ const response = await this.request({ context: { httpMethod: 'PATCH', path: '/test/regexp/super-poodle-party' } });
84
+ assert.deepStrictEqual(response.body, { path: 'poodle' });
85
+ assert(response.headers.has('ETag'));
86
+ }
87
+
88
+ @Test()
89
+ async testBuffer() {
90
+ const response = await this.request({ context: { httpMethod: 'GET', path: '/test/buffer' } });
91
+ assert(response.body === 'hello');
92
+ assert(response.headers.has('ETag'));
93
+ }
94
+
95
+ @Test()
96
+ async testStream() {
97
+ try {
98
+ const response = await this.request({ context: { httpMethod: 'GET', path: '/test/stream' } });
99
+ assert(response.body === 'hello');
100
+ assert(!response.headers.has('ETag'));
101
+ } catch (err) {
102
+ console.error(err);
103
+ throw err;
104
+ }
105
+ }
106
+
107
+ @Test()
108
+ async testRenderable() {
109
+ const response = await this.request({ context: { httpMethod: 'GET', path: '/test/renderable' } });
110
+ assert(response.body === 'hello');
111
+ }
112
+
113
+ @Test()
114
+ async testFullUrl() {
115
+ const response = await this.request({ context: { httpMethod: 'GET', path: '/test/fullUrl' } });
116
+ assert.deepStrictEqual(response.body, { path: '/test/fullUrl' });
117
+ }
118
+
119
+ @Test()
120
+ async testHeaderFirst() {
121
+ const response = await this.request({
122
+ context: {
123
+ httpMethod: 'GET', path: '/test/headerFirst',
124
+ },
125
+ headers: {
126
+ age: ['1', '2', '3']
127
+ }
128
+ });
129
+ assert.deepStrictEqual(response.body, { header: '1' });
130
+ }
131
+
132
+ @Test()
133
+ async testGetIp() {
134
+ const response = await this.request<{ ip: string | undefined }>({ context: { httpMethod: 'GET', path: '/test/ip', connection: { ip: '::1' } } });
135
+ assert(response.body?.ip === '127.0.0.1' || response.body?.ip === '::1');
136
+
137
+ const { body: ret2 } = await this.request<{ ip: string | undefined }>({ context: { httpMethod: 'GET', path: '/test/ip' }, headers: { 'X-Forwarded-For': 'bob' } });
138
+ assert(ret2?.ip === 'bob');
139
+ }
140
+
141
+ @Test()
142
+ async testErrorThrow() {
143
+ const { context: { httpStatusCode: statusCode } } = await this.request<{ ip: string | undefined }>({ context: { httpMethod: 'POST', path: '/test/ip' } }, false);
144
+ assert(statusCode === 500);
145
+ }
146
+
147
+ @Test()
148
+ async compressionReturned() {
149
+ {
150
+ const response = await this.request({ context: { httpMethod: 'GET', path: '/test/json' }, headers: { 'Accept-Encoding': 'gzip;q=1' } });
151
+ assert(!response.headers.has('Content-Encoding'));
152
+ assert.deepStrictEqual(response.body, { json: true });
153
+ }
154
+ for (const encoding of ['gzip', 'br', 'deflate']) {
155
+ const response = await this.request({ context: { httpMethod: 'GET', path: '/test/json/large/20000' }, headers: { 'Accept-Encoding': `${encoding};q=1` } });
156
+ const value = response.headers.get('Content-Encoding');
157
+ assert(value === encoding);
158
+
159
+ assert(response.body);
160
+ assert(typeof response.body === 'object');
161
+ assert('json' in response.body);
162
+ assert(typeof response.body.json === 'string');
163
+ assert(response.body.json.startsWith('0123456789'));
164
+ }
165
+
166
+ {
167
+ const { headers } = await this.request({ context: { httpMethod: 'GET', path: '/test/json/large/50000' }, headers: { 'Accept-Encoding': 'orange' } }, false);
168
+ assert(!('content-encoding' in headers));
169
+ // assert(status === 406);
170
+ }
171
+ }
172
+
173
+ @Test()
174
+ async testWildcard() {
175
+ const response = await this.request<{ path: string }>({ context: { httpMethod: 'GET', path: '/test/fun/1/2/3/4' } });
176
+ assert(response.body?.path === '1/2/3/4');
177
+ }
178
+ }
@@ -0,0 +1,207 @@
1
+ import ts from 'typescript';
2
+
3
+ import {
4
+ TransformerState, OnClass, OnMethod, DocUtil, DecoratorUtil, DecoratorMeta, LiteralUtil, AnyType,
5
+ OnProperty
6
+ } from '@travetto/transformer';
7
+
8
+ import { SchemaTransformUtil } from '@travetto/schema/support/transformer/util.ts';
9
+
10
+ const PARAM_DEC_IMPORT = '@travetto/web/src/decorator/param.ts';
11
+ const COMMON_DEC_IMPORT = '@travetto/web/src/decorator/common.ts';
12
+ const ENDPOINT_DEC_IMPORT = '@travetto/web/src/decorator/endpoint.ts';
13
+
14
+ /**
15
+ * Handle @Controller, @Endpoint
16
+ */
17
+ export class WebTransformer {
18
+
19
+ /**
20
+ * Handle endpoint parameter
21
+ */
22
+ static handleEndpointParameter(state: TransformerState, node: ts.ParameterDeclaration, epDec: DecoratorMeta, idx: number): ts.ParameterDeclaration {
23
+ const pDec = state.findDecorator(this, node, 'Param');
24
+ let pDecArg = DecoratorUtil.getPrimaryArgument(pDec)!;
25
+ if (pDecArg && ts.isStringLiteral(pDecArg)) {
26
+ pDecArg = state.fromLiteral({ name: pDecArg });
27
+ }
28
+
29
+ const paramType = state.resolveType(node);
30
+ let name = node.name.getText();
31
+ if (/[{}\[\]]/.test(name)) { // Destructured
32
+ name = `param__${idx + 1}`;
33
+ }
34
+
35
+ let detectedParamType: string | undefined;
36
+
37
+ const config: { type: AnyType, name?: string } = { type: paramType };
38
+
39
+ // Detect default behavior
40
+ // If primitive
41
+ if (paramType.key !== 'managed' && paramType.key !== 'shape') {
42
+ // Get path of endpoint
43
+ const arg = DecoratorUtil.getPrimaryArgument(epDec.dec);
44
+ // If non-regex
45
+ if (arg && ts.isStringLiteral(arg)) {
46
+ const literal = LiteralUtil.toLiteral(arg);
47
+ if (typeof literal !== 'string') {
48
+ throw new Error(`Unexpected literal type: ${literal}`);
49
+ }
50
+ // If param name matches path param, default to @Path
51
+ detectedParamType = new RegExp(`:${name}\\b`).test(literal) ? 'PathParam' : 'QueryParam';
52
+ } else {
53
+ // Default to query for empty or regex endpoints
54
+ detectedParamType = 'QueryParam';
55
+ }
56
+ } else if (epDec.ident.getText() !== 'All') { // Treat all separate
57
+ // Treat as schema, and see if endpoint supports a body for default behavior on untyped
58
+ detectedParamType = epDec.targets?.includes('@travetto/web:HttpRequestBody') ? 'Body' : 'QueryParam';
59
+ config.name = '';
60
+ }
61
+
62
+ node = SchemaTransformUtil.computeField(state, node, config);
63
+
64
+ const modifiers = (node.modifiers ?? []).filter(x => x !== pDec);
65
+ const conf = state.extendObjectLiteral({ name, sourceText: node.name.getText() }, pDecArg);
66
+
67
+ if (!pDec) { // Handle default, missing
68
+ modifiers.push(state.createDecorator(PARAM_DEC_IMPORT, detectedParamType ?? 'QueryParam', conf));
69
+ } else if (ts.isCallExpression(pDec.expression)) { // if it does exist, update
70
+ modifiers.push(state.factory.createDecorator(
71
+ state.factory.createCallExpression(
72
+ pDec.expression.expression,
73
+ [],
74
+ [conf, ...pDec.expression.arguments.slice(1)]
75
+ )
76
+ ));
77
+ }
78
+
79
+ return state.factory.updateParameterDeclaration(
80
+ node,
81
+ modifiers,
82
+ node.dotDotDotToken,
83
+ node.name,
84
+ node.questionToken,
85
+ node.type,
86
+ node.initializer
87
+ );
88
+ }
89
+
90
+ /**
91
+ * On @Endpoint method
92
+ */
93
+ @OnMethod('Endpoint')
94
+ static handleEndpoint(state: TransformerState, node: ts.MethodDeclaration, dec?: DecoratorMeta): ts.MethodDeclaration {
95
+
96
+ const modifiers = (node.modifiers ?? []).slice(0);
97
+ const newDecls: ts.ModifierLike[] = [];
98
+
99
+ const comments = DocUtil.describeDocs(node);
100
+
101
+ // Handle description/title/summary w/e
102
+ if (comments.description) {
103
+ newDecls.push(state.createDecorator(COMMON_DEC_IMPORT, 'Describe', state.fromLiteral({
104
+ title: comments.description
105
+ })));
106
+ }
107
+
108
+ let nParams = node.parameters;
109
+
110
+ // Handle parameters
111
+ if (node.parameters.length) {
112
+ const params: ts.ParameterDeclaration[] = [];
113
+ // If there are parameters to process
114
+ let i = 0;
115
+ for (const p of node.parameters) {
116
+ params.push(this.handleEndpointParameter(state, p, dec!, i));
117
+ i += 1;
118
+ }
119
+
120
+ nParams = state.factory.createNodeArray(params);
121
+ }
122
+
123
+ // If we have a valid response type, declare it
124
+ const nodeType = state.resolveReturnType(node);
125
+ let targetType = nodeType;
126
+
127
+ if (nodeType.key === 'literal' && nodeType.typeArguments?.length && nodeType.name === 'Promise') {
128
+ targetType = nodeType.typeArguments[0];
129
+ }
130
+
131
+ let inner: AnyType | undefined;
132
+ if (targetType.key === 'managed' && targetType.name === 'WebResponse' && targetType.importName.startsWith('@travetto/web')) {
133
+ inner = state.getApparentTypeOfField(targetType.original!, 'body');
134
+ }
135
+
136
+ const returnType = SchemaTransformUtil.ensureType(state, inner ?? nodeType, node);
137
+ if (returnType.type) {
138
+ newDecls.push(state.createDecorator(ENDPOINT_DEC_IMPORT, 'ResponseType', state.fromLiteral({
139
+ ...returnType,
140
+ title: comments.return
141
+ })));
142
+ }
143
+
144
+ if (newDecls.length || nParams !== node.parameters) {
145
+ return state.factory.updateMethodDeclaration(
146
+ node,
147
+ [...modifiers, ...newDecls],
148
+ node.asteriskToken,
149
+ node.name,
150
+ node.questionToken,
151
+ node.typeParameters,
152
+ nParams,
153
+ node.type,
154
+ node.body
155
+ );
156
+ } else {
157
+ return node;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Handle @Controller
163
+ */
164
+ @OnClass('Controller')
165
+ static handleController(state: TransformerState, node: ts.ClassDeclaration): ts.ClassDeclaration {
166
+ // Read title/description/summary from jsdoc on class
167
+ const comments = DocUtil.describeDocs(node);
168
+
169
+ if (!comments.description) {
170
+ return node;
171
+ } else {
172
+ return state.factory.updateClassDeclaration(
173
+ node,
174
+ [
175
+ ...(node.modifiers ?? []),
176
+ state.createDecorator(COMMON_DEC_IMPORT, 'Describe', state.fromLiteral({
177
+ title: comments.description
178
+ }))
179
+ ],
180
+ node.name,
181
+ node.typeParameters,
182
+ node.heritageClauses,
183
+ node.members
184
+ );
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Handle ContextParam annotation
190
+ */
191
+ @OnProperty('ContextParam')
192
+ static registerContextParam(state: TransformerState, node: ts.PropertyDeclaration): typeof node {
193
+ const decl = state.findDecorator(this, node, 'ContextParam', PARAM_DEC_IMPORT);
194
+
195
+ // Doing decls
196
+ return state.factory.updatePropertyDeclaration(
197
+ node,
198
+ DecoratorUtil.spliceDecorators(node, decl, [
199
+ state.createDecorator(PARAM_DEC_IMPORT, 'ContextParam', state.fromLiteral({ target: state.getConcreteType(node) }))
200
+ ], 0),
201
+ node.name,
202
+ node.questionToken,
203
+ node.type,
204
+ node.initializer
205
+ );
206
+ }
207
+ }