@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.
- package/README.md +734 -0
- package/__index__.ts +44 -0
- package/package.json +66 -0
- package/src/common/global.ts +30 -0
- package/src/config.ts +18 -0
- package/src/context.ts +49 -0
- package/src/decorator/common.ts +87 -0
- package/src/decorator/controller.ts +13 -0
- package/src/decorator/endpoint.ts +102 -0
- package/src/decorator/param.ts +64 -0
- package/src/interceptor/accept.ts +70 -0
- package/src/interceptor/body-parse.ts +123 -0
- package/src/interceptor/compress.ts +119 -0
- package/src/interceptor/context.ts +23 -0
- package/src/interceptor/cookies.ts +97 -0
- package/src/interceptor/cors.ts +94 -0
- package/src/interceptor/decompress.ts +91 -0
- package/src/interceptor/etag.ts +99 -0
- package/src/interceptor/logging.ts +71 -0
- package/src/interceptor/respond.ts +26 -0
- package/src/interceptor/response-cache.ts +47 -0
- package/src/interceptor/trust-proxy.ts +53 -0
- package/src/registry/controller.ts +288 -0
- package/src/registry/types.ts +229 -0
- package/src/registry/visitor.ts +52 -0
- package/src/router/base.ts +67 -0
- package/src/router/standard.ts +59 -0
- package/src/types/cookie.ts +18 -0
- package/src/types/core.ts +33 -0
- package/src/types/dispatch.ts +23 -0
- package/src/types/error.ts +10 -0
- package/src/types/filter.ts +7 -0
- package/src/types/headers.ts +108 -0
- package/src/types/interceptor.ts +54 -0
- package/src/types/message.ts +33 -0
- package/src/types/request.ts +22 -0
- package/src/types/response.ts +20 -0
- package/src/util/body.ts +220 -0
- package/src/util/common.ts +142 -0
- package/src/util/cookie.ts +145 -0
- package/src/util/endpoint.ts +277 -0
- package/src/util/mime.ts +36 -0
- package/src/util/net.ts +61 -0
- package/support/test/dispatch-util.ts +90 -0
- package/support/test/dispatcher.ts +15 -0
- package/support/test/suite/base.ts +61 -0
- package/support/test/suite/controller.ts +103 -0
- package/support/test/suite/schema.ts +275 -0
- package/support/test/suite/standard.ts +178 -0
- package/support/transformer.web.ts +207 -0
package/README.md
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/web/DOC.tsx and execute "npx trv doc" to rebuild -->
|
|
3
|
+
# Web API
|
|
4
|
+
|
|
5
|
+
## Declarative api for Web Applications with support for the dependency injection.
|
|
6
|
+
|
|
7
|
+
**Install: @travetto/web**
|
|
8
|
+
```bash
|
|
9
|
+
npm install @travetto/web
|
|
10
|
+
|
|
11
|
+
# or
|
|
12
|
+
|
|
13
|
+
yarn add @travetto/web
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The module provides a declarative API for creating and describing a Web application. Since the framework is declarative, decorators are used to configure almost everything. The general layout of an application is a collection of [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L9)s that employ some combination of [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s to help manage which functionality is executed before the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) code, within the [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L9). This module will look at:
|
|
17
|
+
* Request/Response Pattern
|
|
18
|
+
* Defining a [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L9)
|
|
19
|
+
* Defining an [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14)s
|
|
20
|
+
* Using [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s
|
|
21
|
+
* Creating a Custom [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)
|
|
22
|
+
* Cookies
|
|
23
|
+
* SSL Support
|
|
24
|
+
* Error Handling
|
|
25
|
+
|
|
26
|
+
## Request/Response Pattern
|
|
27
|
+
Unlike other frameworks (e.g. [express](https://expressjs.com), [fastify](https://www.fastify.io/)), this module takes an approach that is similar to [AWS Lambda](https://aws.amazon.com/lambda/)'s model for requests and responses. What you can see here is that [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) and [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) are very simple objects, with the focus being on the `payload` and `body`. This is intended to provide maximal compatibility with non-HTTP sources. The driving goal is to support more than just standard HTTP servers but also allow for seamless integration with tools like event queues, web sockets, etc.
|
|
28
|
+
|
|
29
|
+
**Code: Base Shape**
|
|
30
|
+
```typescript
|
|
31
|
+
export class BaseWebMessage<B = unknown, C = unknown> implements WebMessage<B, C> {
|
|
32
|
+
readonly context: C;
|
|
33
|
+
readonly headers: WebHeaders;
|
|
34
|
+
body?: B;
|
|
35
|
+
constructor(o: WebMessageInit<B, C> = {});
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Code: Request Shape**
|
|
40
|
+
```typescript
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Code: Response Shape**
|
|
45
|
+
```typescript
|
|
46
|
+
export class WebResponse<B = unknown> extends BaseWebMessage<B, WebResponseContext> {
|
|
47
|
+
/**
|
|
48
|
+
* Build the redirect
|
|
49
|
+
* @param location Location to redirect to
|
|
50
|
+
* @param statusCode Status code
|
|
51
|
+
*/
|
|
52
|
+
static redirect(location: string, statusCode = 302): WebResponse<undefined>;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
These objects do not represent the underlying sockets provided by various http servers, but in fact are simple wrappers that track the flow through the call stack of the various [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s and the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) handler. One of the biggest departures here, is that the response is not an entity that is passed around from call-site to call-site, but is is solely a return-value. This doesn't mean the return value has to be static and pre-allocated, on the contrary streams are still supported. The difference here is that the streams/asynchronous values will be consumed until the response is sent back to the user. The [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L50) is a good reference for transforming a [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) that can either be a stream or a fixed value.
|
|
57
|
+
|
|
58
|
+
## Defining a Controller
|
|
59
|
+
To start, we must define a [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L9), which is only allowed on classes. Controllers can be configured with:
|
|
60
|
+
* `path` - The required context path the controller will operate atop
|
|
61
|
+
* `title` - The definition of the controller
|
|
62
|
+
* `description` - High level description fo the controller
|
|
63
|
+
Additionally, the module is predicated upon [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support."), and so all standard injection techniques (constructor, fields) work for registering dependencies.
|
|
64
|
+
|
|
65
|
+
[JSDoc](http://usejsdoc.org/about-getting-started.html) comments can also be used to define the `title` attribute.
|
|
66
|
+
|
|
67
|
+
**Code: Basic Controller Registration**
|
|
68
|
+
```typescript
|
|
69
|
+
import { Controller } from '@travetto/web';
|
|
70
|
+
|
|
71
|
+
@Controller('/simple')
|
|
72
|
+
class SimpleController {
|
|
73
|
+
// endpoints
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Defining an Endpoint
|
|
78
|
+
Once the controller is declared, each method of the controller is a candidate for being an endpoint. By design, everything is asynchronous, and so async/await is natively supported.
|
|
79
|
+
|
|
80
|
+
The most common pattern is to register HTTP-driven endpoints. The HTTP methods that are currently supported:
|
|
81
|
+
* [@Get](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L43)
|
|
82
|
+
* [@Post](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L50)
|
|
83
|
+
* [@Put](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L57)
|
|
84
|
+
* [@Delete](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L70)
|
|
85
|
+
* [@Patch](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L64)
|
|
86
|
+
* [@Head](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L76)
|
|
87
|
+
* [@Options](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L82)
|
|
88
|
+
Similar to the Controller, each endpoint decorator handles the following config:
|
|
89
|
+
* `title` - The definition of the endpoint
|
|
90
|
+
* `description` - High level description fo the endpoint
|
|
91
|
+
[JSDoc](http://usejsdoc.org/about-getting-started.html) comments can also be used to define the `title` attribute, as well as describing the parameters using `@param` tags in the comment.
|
|
92
|
+
|
|
93
|
+
The return type of the method will also be used to describe the `responseType` if not specified manually.
|
|
94
|
+
|
|
95
|
+
**Code: Controller with Sample Endpoint**
|
|
96
|
+
```typescript
|
|
97
|
+
import { Get, Controller } from '@travetto/web';
|
|
98
|
+
|
|
99
|
+
class Data { }
|
|
100
|
+
|
|
101
|
+
@Controller('/simple')
|
|
102
|
+
class SimpleController {
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets the most basic of data
|
|
106
|
+
*/
|
|
107
|
+
@Get('/')
|
|
108
|
+
async simpleGet() {
|
|
109
|
+
let data: Data | undefined;
|
|
110
|
+
//
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Note**: In development mode the module supports hot reloading of `class`es. Endpoints can be added/modified/removed at runtime.
|
|
117
|
+
|
|
118
|
+
### Parameters
|
|
119
|
+
Endpoints can be configured to describe and enforce parameter behavior. Request parameters can be defined in five areas:
|
|
120
|
+
* [@PathParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L37) - Path params
|
|
121
|
+
* [@QueryParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L43) - Query params - can be either a single value or bind to a whole object
|
|
122
|
+
* [@Body](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L55) - Request body
|
|
123
|
+
* [@HeaderParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L49) - Header values
|
|
124
|
+
Each [@Param](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L24) can be configured to indicate:
|
|
125
|
+
* `name` - Name of param, field name, defaults to handler parameter name if necessary
|
|
126
|
+
* `description` - Description of param, pulled from [JSDoc](http://usejsdoc.org/about-getting-started.html), or defaults to name if empty
|
|
127
|
+
* `required?` - Is the field required?, defaults to whether or not the parameter itself is optional
|
|
128
|
+
* `type` - The class of the type to be enforced, pulled from parameter type
|
|
129
|
+
[JSDoc](http://usejsdoc.org/about-getting-started.html) comments can also be used to describe parameters using `@param` tags in the comment.
|
|
130
|
+
|
|
131
|
+
**Code: Full-fledged Controller with Endpoints**
|
|
132
|
+
```typescript
|
|
133
|
+
import { Get, Controller, Post, QueryParam, WebRequest, ContextParam } from '@travetto/web';
|
|
134
|
+
import { Integer, Min } from '@travetto/schema';
|
|
135
|
+
|
|
136
|
+
import { MockService } from './mock.ts';
|
|
137
|
+
|
|
138
|
+
@Controller('/simple')
|
|
139
|
+
export class Simple {
|
|
140
|
+
|
|
141
|
+
service: MockService;
|
|
142
|
+
|
|
143
|
+
@ContextParam()
|
|
144
|
+
request: WebRequest;
|
|
145
|
+
|
|
146
|
+
constructor(service: MockService) {
|
|
147
|
+
this.service = service;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get a random user by name
|
|
152
|
+
*/
|
|
153
|
+
@Get('/name')
|
|
154
|
+
async getName() {
|
|
155
|
+
const user = await this.service.fetch();
|
|
156
|
+
return `/simple/name => ${user.first.toLowerCase()}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get a user by id
|
|
161
|
+
*/
|
|
162
|
+
@Get('/:id')
|
|
163
|
+
async getById(id: number) {
|
|
164
|
+
const user = await this.service.fetch(id);
|
|
165
|
+
return `/simple/id => ${user.first.toLowerCase()}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@Post('/name')
|
|
169
|
+
async createName(person: { name: string }) {
|
|
170
|
+
await this.service.update({ name: person.name });
|
|
171
|
+
return { success: true };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Get('img/*')
|
|
175
|
+
async getImage(
|
|
176
|
+
@QueryParam('w') @Integer() @Min(100) width?: number,
|
|
177
|
+
@QueryParam('h') @Integer() @Min(100) height?: number
|
|
178
|
+
) {
|
|
179
|
+
const img = await this.service.fetchImage(this.request.context.path, { width, height });
|
|
180
|
+
return img;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### ContextParam
|
|
186
|
+
In addition to endpoint parameters (i.e. user-provided inputs), there may also be a desire to access indirect contextual information. Specifically you may need access to the entire [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11). These are able to be injected using the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) on a class-level field from the [WebAsyncContext](https://github.com/travetto/travetto/tree/main/module/web/src/context.ts#L11). These are not exposed as endpoint parameters as they cannot be provided when making RPC invocations.
|
|
187
|
+
|
|
188
|
+
**Code: Example ContextParam usage**
|
|
189
|
+
```typescript
|
|
190
|
+
import { CacheControl, ContextParam, Controller, Get, WebRequest, WebResponse } from '@travetto/web';
|
|
191
|
+
|
|
192
|
+
@Controller('/context')
|
|
193
|
+
class ContextController {
|
|
194
|
+
|
|
195
|
+
@ContextParam()
|
|
196
|
+
request: WebRequest;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Gets the ip of the user, ensure no caching
|
|
200
|
+
*/
|
|
201
|
+
@CacheControl(0)
|
|
202
|
+
@Get('/ip')
|
|
203
|
+
async getIp() {
|
|
204
|
+
return new WebResponse({
|
|
205
|
+
body: { ip: this.request.context.connection?.ip },
|
|
206
|
+
headers: {
|
|
207
|
+
'Content-Type': 'application/json+ip'
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Note**: When referencing the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) values, the contract for idempotency needs to be carefully inspected, if expected. You can see in the example above that the [CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#L55) decorator is used to ensure that the response is not cached.
|
|
215
|
+
|
|
216
|
+
### Validating Inputs
|
|
217
|
+
The module provides high level access for [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") support, via decorators, for validating and typing request inputs.
|
|
218
|
+
|
|
219
|
+
By default, all endpoint parameters are validated for type, and any additional constraints added (required, vs optional, minlength, etc). Each parameter location ([@PathParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L37), [@Body](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L55), [@QueryParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L43), [@HeaderParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L49)) primarily provides a source to bind the endpoint arguments from. Once bound, the module will validate that the provided arguments are in fact valid. All validation will occur before the endpoint is ever executed, ensuring a strong contract.
|
|
220
|
+
|
|
221
|
+
**Code: Using Body for POST requests**
|
|
222
|
+
```typescript
|
|
223
|
+
import { Schema } from '@travetto/schema';
|
|
224
|
+
import { Controller, Post, Body } from '@travetto/web';
|
|
225
|
+
|
|
226
|
+
@Schema()
|
|
227
|
+
class User {
|
|
228
|
+
name: string;
|
|
229
|
+
age: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@Controller('/user')
|
|
233
|
+
class UserController {
|
|
234
|
+
|
|
235
|
+
private service: {
|
|
236
|
+
update(user: User): Promise<User>;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
@Post('/saveUser')
|
|
240
|
+
async save(@Body() user: User) {
|
|
241
|
+
const saved = await this.service.update(user);
|
|
242
|
+
return { success: !!saved };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Code: Using Query + Schema for GET requests**
|
|
248
|
+
```typescript
|
|
249
|
+
import { Schema } from '@travetto/schema';
|
|
250
|
+
import { Controller, Get } from '@travetto/web';
|
|
251
|
+
|
|
252
|
+
@Schema()
|
|
253
|
+
class SearchParams {
|
|
254
|
+
page: number = 0;
|
|
255
|
+
pageSize: number = 100;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Controller('/user')
|
|
259
|
+
class UserController {
|
|
260
|
+
|
|
261
|
+
private service: {
|
|
262
|
+
search(query: SearchParams): Promise<number[]>;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
@Get('/search')
|
|
266
|
+
async search(query: SearchParams) {
|
|
267
|
+
return await this.service.search(query);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Additionally, schema related inputs can also be used with `interface`s and `type` literals in lieu of classes. This is best suited for simple types:
|
|
273
|
+
|
|
274
|
+
**Code: Using QuerySchema with a type literal**
|
|
275
|
+
```typescript
|
|
276
|
+
import { Controller, Get } from '@travetto/web';
|
|
277
|
+
|
|
278
|
+
type Paging = {
|
|
279
|
+
page?: number;
|
|
280
|
+
pageSize?: number;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
@Controller('/user')
|
|
284
|
+
class UserController {
|
|
285
|
+
|
|
286
|
+
private service: {
|
|
287
|
+
search(query: Paging): Promise<number>;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
@Get('/search')
|
|
291
|
+
async search(query: Paging = { page: 0, pageSize: 100 }) {
|
|
292
|
+
return await this.service.search(query);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Using Interceptors
|
|
298
|
+
[WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s are a key part of the web framework, to allow for conditional functionality to be added, across all endpoints.
|
|
299
|
+
|
|
300
|
+
### Anatomy of an Interceptor
|
|
301
|
+
|
|
302
|
+
**Code: A Simple Interceptor**
|
|
303
|
+
```typescript
|
|
304
|
+
import { WebChainedContext, WebInterceptor, WebInterceptorCategory, WebInterceptorContext } from '@travetto/web';
|
|
305
|
+
import { Injectable } from '@travetto/di';
|
|
306
|
+
|
|
307
|
+
@Injectable()
|
|
308
|
+
export class HelloWorldInterceptor implements WebInterceptor {
|
|
309
|
+
|
|
310
|
+
category: WebInterceptorCategory = 'application';
|
|
311
|
+
|
|
312
|
+
applies(context: WebInterceptorContext<unknown>): boolean {
|
|
313
|
+
return context.endpoint.httpMethod === 'HEAD';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
filter(ctx: WebChainedContext) {
|
|
317
|
+
console.log('Hello world!');
|
|
318
|
+
return ctx.next();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
In this example you can see the markers of a simple interceptor:
|
|
324
|
+
|
|
325
|
+
#### category
|
|
326
|
+
`category` - This represents the generally request lifecycle phase an interceptor will run in. It can be customized further with `dependsOn` and `runsBefore` to control exact ordering within a category. In this example `application` represents the lowest priority, and will run right before the endpoint is executed.
|
|
327
|
+
|
|
328
|
+
#### applies
|
|
329
|
+
`applies` - This represents ability for the per-endpoint configuration to determine if an interceptor is applicable. By default, all interceptors will auto-register on every endpoint. Some interceptors are opt-in, and control that by setting applies to constantly return `false`.
|
|
330
|
+
|
|
331
|
+
#### filter
|
|
332
|
+
`filter` - This is the actual logic that will be invoked around the endpoint call, represented by `ctx.next()`. The next call passes control to the next interceptor all the way down to the endpoint, and then will pop back up the stack. Code executed before `next()` is generally used for request filtering, and code afterwards is generally used for response control.
|
|
333
|
+
|
|
334
|
+
Out of the box, the web framework comes with a few interceptors, and more are contributed by other modules as needed. The default interceptor set is (in order of execution):
|
|
335
|
+
|
|
336
|
+
### Order of Execution
|
|
337
|
+
|
|
338
|
+
1. global - Intended to run outside of the request flow - [AsyncContextInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/context.ts#L13)
|
|
339
|
+
1. terminal - Handles once request and response are finished building - [LoggingInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/logging.ts#L28), [RespondInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/respond.ts#L12)
|
|
340
|
+
1. pre-request - Prepares the request for running - [TrustProxyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/trust-proxy.ts#L23)
|
|
341
|
+
1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53), [AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L34), [BodyParseInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body-parse.ts#L61), [CookiesInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookies.ts#L56)
|
|
342
|
+
1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L50), [CorsInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cors.ts#L51), [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L34), [ResponseCacheInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/response-cache.ts#L30)
|
|
343
|
+
1. application - Lives outside of the general request/response behavior, [Web Auth](https://github.com/travetto/travetto/tree/main/module/auth-web#readme "Web authentication integration support for the Travetto framework") uses this for login and logout flows.
|
|
344
|
+
|
|
345
|
+
### Packaged Interceptors
|
|
346
|
+
|
|
347
|
+
#### AsyncContextInterceptor
|
|
348
|
+
[AsyncContextInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/context.ts#L13) is responsible for sharing context across the various layers that may be touched by a request. This
|
|
349
|
+
|
|
350
|
+
#### LoggingInterceptor
|
|
351
|
+
[LoggingInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/logging.ts#L28) is used for logging the request/response, handling any error logging as needed. This interceptor can be noisy, and so can easily be disabled as needed by setting `web.log.applies: false` in your config.
|
|
352
|
+
|
|
353
|
+
**Code: Web Log Config**
|
|
354
|
+
```typescript
|
|
355
|
+
export class WebLogConfig {
|
|
356
|
+
/**
|
|
357
|
+
* Enable logging of all requests
|
|
358
|
+
*/
|
|
359
|
+
applies = true;
|
|
360
|
+
/**
|
|
361
|
+
* Should errors be dumped as full stack traces
|
|
362
|
+
*/
|
|
363
|
+
showStackTrace = true;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### RespondInterceptor
|
|
368
|
+
[RespondInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/respond.ts#L12) is a basic catch-all that forces errors and data alike into a consistent format for sending back to the user.
|
|
369
|
+
|
|
370
|
+
#### TrustProxyInterceptor
|
|
371
|
+
[TrustProxyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/trust-proxy.ts#L23) allows for overriding connection information (host, ip, protocol) using `X-Forwarded-*` headers. This allows for proxied requests to retain access to the "source" request information as necessary.
|
|
372
|
+
|
|
373
|
+
**Code: TrustProxy Config**
|
|
374
|
+
```typescript
|
|
375
|
+
export class TrustProxyConfig {
|
|
376
|
+
/**
|
|
377
|
+
* Enforces trust rules for X-Forwarded-* headers
|
|
378
|
+
*/
|
|
379
|
+
applies = true;
|
|
380
|
+
/**
|
|
381
|
+
* The accepted ips
|
|
382
|
+
*/
|
|
383
|
+
ips: string[] = [];
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### AcceptInterceptor
|
|
388
|
+
[AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L34) handles verifying the inbound request matches the allowed content-types. This acts as a standard gate-keeper for spurious input.
|
|
389
|
+
|
|
390
|
+
**Code: Accept Config**
|
|
391
|
+
```typescript
|
|
392
|
+
export class AcceptConfig {
|
|
393
|
+
/**
|
|
394
|
+
* Accepts certain request content types
|
|
395
|
+
*/
|
|
396
|
+
applies = false;
|
|
397
|
+
/**
|
|
398
|
+
* The accepted types
|
|
399
|
+
*/
|
|
400
|
+
types: string[] = [];
|
|
401
|
+
|
|
402
|
+
@Ignore()
|
|
403
|
+
matcher: (type: string) => boolean;
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### DecompressInterceptor
|
|
408
|
+
[DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53) handles decompressing the inbound request, if supported. This relies upon HTTP standards for content encoding, and negotiating the appropriate decompression scheme.
|
|
409
|
+
|
|
410
|
+
**Code: Decompress Config**
|
|
411
|
+
```typescript
|
|
412
|
+
export class DecompressConfig {
|
|
413
|
+
/**
|
|
414
|
+
* Parse request body
|
|
415
|
+
*/
|
|
416
|
+
applies: boolean = true;
|
|
417
|
+
/**
|
|
418
|
+
* Supported encodings
|
|
419
|
+
*/
|
|
420
|
+
supportedEncodings: WebDecompressEncoding[] = ['br', 'gzip', 'deflate', 'identity'];
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
#### CookiesInterceptor
|
|
425
|
+
[CookiesInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookies.ts#L56) is responsible for processing inbound cookie headers and populating the appropriate data on the request, as well as sending the appropriate response data
|
|
426
|
+
|
|
427
|
+
**Code: Cookies Config**
|
|
428
|
+
```typescript
|
|
429
|
+
export class CookieConfig implements CookieSetOptions {
|
|
430
|
+
/**
|
|
431
|
+
* Support reading/sending cookies
|
|
432
|
+
*/
|
|
433
|
+
applies = true;
|
|
434
|
+
/**
|
|
435
|
+
* Are they signed
|
|
436
|
+
*/
|
|
437
|
+
signed = true;
|
|
438
|
+
/**
|
|
439
|
+
* Supported only via http (not in JS)
|
|
440
|
+
*/
|
|
441
|
+
httpOnly = true;
|
|
442
|
+
/**
|
|
443
|
+
* Enforce same site policy
|
|
444
|
+
*/
|
|
445
|
+
sameSite: Cookie['sameSite'] = 'lax';
|
|
446
|
+
/**
|
|
447
|
+
* The signing keys
|
|
448
|
+
*/
|
|
449
|
+
@Secret()
|
|
450
|
+
keys?: string[];
|
|
451
|
+
/**
|
|
452
|
+
* Is the cookie only valid for https
|
|
453
|
+
*/
|
|
454
|
+
secure?: boolean = false;
|
|
455
|
+
/**
|
|
456
|
+
* The domain of the cookie
|
|
457
|
+
*/
|
|
458
|
+
domain?: string;
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
#### BodyParseInterceptor
|
|
463
|
+
[BodyParseInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body-parse.ts#L61) handles the inbound request, and converting the body payload into an appropriate format.
|
|
464
|
+
|
|
465
|
+
**Code: Body Parse Config**
|
|
466
|
+
```typescript
|
|
467
|
+
export class BodyParseConfig {
|
|
468
|
+
/**
|
|
469
|
+
* Parse request body
|
|
470
|
+
*/
|
|
471
|
+
applies: boolean = true;
|
|
472
|
+
/**
|
|
473
|
+
* Max body size limit
|
|
474
|
+
*/
|
|
475
|
+
limit: `${number}${'mb' | 'kb' | 'gb' | 'b' | ''}` = '1mb';
|
|
476
|
+
/**
|
|
477
|
+
* How to interpret different content types
|
|
478
|
+
*/
|
|
479
|
+
parsingTypes: Record<string, string> = {
|
|
480
|
+
text: 'text',
|
|
481
|
+
'application/json': 'json',
|
|
482
|
+
'application/x-www-form-urlencoded': 'form'
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
@Ignore()
|
|
486
|
+
_limit: number | undefined;
|
|
487
|
+
|
|
488
|
+
postConstruct(): void {
|
|
489
|
+
this._limit = WebCommonUtil.parseByteSize(this.limit);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
#### CompressInterceptor
|
|
495
|
+
[CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L50) by default, will compress all valid outbound responses over a certain size, or for streams will cache every response. This relies on Node's [Buffer](https://nodejs.org/api/zlib.html) support for compression.
|
|
496
|
+
|
|
497
|
+
**Code: Compress Config**
|
|
498
|
+
```typescript
|
|
499
|
+
export class CompressConfig {
|
|
500
|
+
/**
|
|
501
|
+
* Attempting to compressing responses
|
|
502
|
+
*/
|
|
503
|
+
applies: boolean = true;
|
|
504
|
+
/**
|
|
505
|
+
* Raw encoding options
|
|
506
|
+
*/
|
|
507
|
+
raw?: (ZlibOptions & BrotliOptions) | undefined;
|
|
508
|
+
/**
|
|
509
|
+
* Preferred encodings
|
|
510
|
+
*/
|
|
511
|
+
preferredEncodings?: WebCompressEncoding[] = ['br', 'gzip', 'identity'];
|
|
512
|
+
/**
|
|
513
|
+
* Supported encodings
|
|
514
|
+
*/
|
|
515
|
+
supportedEncodings: WebCompressEncoding[] = ['br', 'gzip', 'identity', 'deflate'];
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
#### EtagInterceptor
|
|
520
|
+
[EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L34) by default, will tag all cacheable HTTP responses, when the response value/length is known. Streams, and other async data sources do not have a pre-defined length, and so are ineligible for etagging.
|
|
521
|
+
|
|
522
|
+
**Code: ETag Config**
|
|
523
|
+
```typescript
|
|
524
|
+
export class EtagConfig {
|
|
525
|
+
/**
|
|
526
|
+
* Attempt ETag generation
|
|
527
|
+
*/
|
|
528
|
+
applies = true;
|
|
529
|
+
/**
|
|
530
|
+
* Should we generate a weak etag
|
|
531
|
+
*/
|
|
532
|
+
weak?: boolean;
|
|
533
|
+
|
|
534
|
+
@Ignore()
|
|
535
|
+
cacheable?: boolean;
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
#### CorsInterceptor
|
|
540
|
+
[CorsInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cors.ts#L51) allows cors functionality to be configured out of the box, by setting properties in your `application.yml`, specifically, the `web.cors` config space.
|
|
541
|
+
|
|
542
|
+
**Code: Cors Config**
|
|
543
|
+
```typescript
|
|
544
|
+
export class CorsConfig {
|
|
545
|
+
/**
|
|
546
|
+
* Send CORS headers on responses
|
|
547
|
+
*/
|
|
548
|
+
applies = true;
|
|
549
|
+
/**
|
|
550
|
+
* Allowed origins
|
|
551
|
+
*/
|
|
552
|
+
origins?: string[];
|
|
553
|
+
/**
|
|
554
|
+
* Allowed http methods
|
|
555
|
+
*/
|
|
556
|
+
methods?: HttpMethod[];
|
|
557
|
+
/**
|
|
558
|
+
* Allowed http headers
|
|
559
|
+
*/
|
|
560
|
+
headers?: string[];
|
|
561
|
+
/**
|
|
562
|
+
* Support credentials?
|
|
563
|
+
*/
|
|
564
|
+
credentials?: boolean;
|
|
565
|
+
|
|
566
|
+
@Ignore()
|
|
567
|
+
resolved: {
|
|
568
|
+
origins: Set<string>;
|
|
569
|
+
methods: string;
|
|
570
|
+
headers: string;
|
|
571
|
+
credentials: boolean;
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
#### ResponseCacheInterceptor
|
|
577
|
+
[ResponseCacheInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/response-cache.ts#L30) by default, disables caching for all GET requests if the response does not include caching headers. This can be managed by setting `web.getCache.applies: <boolean>` in your config. This interceptor applies by default.
|
|
578
|
+
|
|
579
|
+
### Configuring Interceptors
|
|
580
|
+
All framework-provided interceptors, follow the same patterns for general configuration. This falls into three areas:
|
|
581
|
+
|
|
582
|
+
#### Enable/disable of individual interceptors via configuration
|
|
583
|
+
This applies only to interceptors that have opted in, to exposing a config, and tying that configuration to the applies logic.
|
|
584
|
+
|
|
585
|
+
**Code: Sample interceptor disabling configuration**
|
|
586
|
+
```yaml
|
|
587
|
+
web:
|
|
588
|
+
trustProxy:
|
|
589
|
+
applies: false
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Code: Configurable Interceptor**
|
|
593
|
+
```typescript
|
|
594
|
+
export class TrustProxyConfig {
|
|
595
|
+
/**
|
|
596
|
+
* Enforces trust rules for X-Forwarded-* headers
|
|
597
|
+
*/
|
|
598
|
+
applies = true;
|
|
599
|
+
/**
|
|
600
|
+
* The accepted ips
|
|
601
|
+
*/
|
|
602
|
+
ips: string[] = [];
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
#### Endpoint-enabled control via decorators
|
|
607
|
+
|
|
608
|
+
**Code: Sample controller with endpoint-level allow/deny**
|
|
609
|
+
```typescript
|
|
610
|
+
import { Controller, Get, QueryParam, ConfigureInterceptor, CorsInterceptor, ExcludeInterceptors } from '@travetto/web';
|
|
611
|
+
|
|
612
|
+
@Controller('/allowDeny')
|
|
613
|
+
@ConfigureInterceptor(CorsInterceptor, { applies: true })
|
|
614
|
+
export class AlowDenyController {
|
|
615
|
+
|
|
616
|
+
@Get('/override')
|
|
617
|
+
@ConfigureInterceptor(CorsInterceptor, { applies: false })
|
|
618
|
+
withoutCors(@QueryParam() value: string) {
|
|
619
|
+
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@Get('/raw')
|
|
623
|
+
@ExcludeInterceptors(v => v.category === 'response')
|
|
624
|
+
withoutResponse(@QueryParam() value: string) {
|
|
625
|
+
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
The resolution logic is as follows:
|
|
631
|
+
* Check the resolved [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14)/[@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L9) overrides to see if an interceptor is explicitly allowed or disallowed
|
|
632
|
+
* Default to `applies()` logic for all available interceptors
|
|
633
|
+
|
|
634
|
+
## Creating a Custom WebInterceptor
|
|
635
|
+
Additionally it may be desirable to create a custom interceptor. Interceptors can be registered with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") by implementing the [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15) interface and adding an [@Injectable](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L29) decorator. A simple logging interceptor:
|
|
636
|
+
|
|
637
|
+
**Code: Defining a new Interceptor**
|
|
638
|
+
```typescript
|
|
639
|
+
import { WebChainedContext, WebInterceptor, WebInterceptorCategory } from '@travetto/web';
|
|
640
|
+
import { Injectable } from '@travetto/di';
|
|
641
|
+
|
|
642
|
+
class Appender {
|
|
643
|
+
write(...args: unknown[]): void { }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
@Injectable()
|
|
647
|
+
export class CustomLoggingInterceptor implements WebInterceptor {
|
|
648
|
+
|
|
649
|
+
category: WebInterceptorCategory = 'terminal';
|
|
650
|
+
|
|
651
|
+
appender: Appender;
|
|
652
|
+
|
|
653
|
+
constructor(appender: Appender) {
|
|
654
|
+
this.appender = appender;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async filter({ request, next }: WebChainedContext) {
|
|
658
|
+
try {
|
|
659
|
+
return await next();
|
|
660
|
+
} finally {
|
|
661
|
+
// Write request to database
|
|
662
|
+
this.appender.write(request.context.httpMethod, request.context.path, request.context.httpQuery);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
When running an interceptor, if you chose to skip calling `ctx.next()`, you will bypass all the downstream interceptors and return a response directly.
|
|
669
|
+
|
|
670
|
+
**Code: Defining a fully controlled Interceptor**
|
|
671
|
+
```typescript
|
|
672
|
+
import { WebInterceptor, WebInterceptorCategory, WebChainedContext, WebError } from '@travetto/web';
|
|
673
|
+
import { Injectable } from '@travetto/di';
|
|
674
|
+
|
|
675
|
+
@Injectable()
|
|
676
|
+
export class SimpleAuthInterceptor implements WebInterceptor {
|
|
677
|
+
|
|
678
|
+
category: WebInterceptorCategory = 'terminal';
|
|
679
|
+
|
|
680
|
+
async filter(ctx: WebChainedContext) {
|
|
681
|
+
if (ctx.request.headers.has('X-Auth')) {
|
|
682
|
+
return await ctx.next();
|
|
683
|
+
} else {
|
|
684
|
+
throw WebError.for('Missing auth', 401, {}, 'authentication');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
## Cookie Support
|
|
691
|
+
[express](https://expressjs.com)/[koa](https://koajs.com/)/[fastify](https://www.fastify.io/) all have their own cookie implementations that are common for each framework but are somewhat incompatible. To that end, cookies are supported for every platform, by using [cookies](https://www.npmjs.com/package/cookies). This functionality is exposed onto the [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) object following the pattern set forth by Koa (this is the library Koa uses). This choice also enables better security support as we are able to rely upon standard behavior when it comes to cookies, and signing.
|
|
692
|
+
|
|
693
|
+
**Code: Sample Cookie Usage**
|
|
694
|
+
```typescript
|
|
695
|
+
import {
|
|
696
|
+
Controller, Get, QueryParam, WebRequest, ContextParam,
|
|
697
|
+
WebResponse, CookieJar, CookieGetOptions, CookieSetOptions
|
|
698
|
+
} from '@travetto/web';
|
|
699
|
+
|
|
700
|
+
@Controller('/simple')
|
|
701
|
+
export class SimpleEndpoints {
|
|
702
|
+
|
|
703
|
+
private getOptions: CookieGetOptions;
|
|
704
|
+
private setOptions: CookieSetOptions;
|
|
705
|
+
|
|
706
|
+
@ContextParam()
|
|
707
|
+
request: WebRequest;
|
|
708
|
+
|
|
709
|
+
@ContextParam()
|
|
710
|
+
cookies: CookieJar;
|
|
711
|
+
|
|
712
|
+
@Get('/cookies')
|
|
713
|
+
getCookies(@QueryParam() value: string) {
|
|
714
|
+
this.cookies.get('name', this.getOptions);
|
|
715
|
+
|
|
716
|
+
// Set a cookie on response
|
|
717
|
+
this.cookies.set({ name: 'name', value, ...this.setOptions });
|
|
718
|
+
return new WebResponse({ body: null });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## SSL Support
|
|
724
|
+
Additionally the framework supports SSL out of the box, by allowing you to specify your public and private keys for the cert. In dev mode, the framework will also automatically generate a self-signed cert if:
|
|
725
|
+
* SSL support is configured
|
|
726
|
+
* [node-forge](https://www.npmjs.com/package/node-forge) is installed
|
|
727
|
+
* Not running in prod
|
|
728
|
+
* No keys provided
|
|
729
|
+
This is useful for local development where you implicitly trust the cert.
|
|
730
|
+
|
|
731
|
+
SSL support can be enabled by setting `web.ssl.active: true` in your config. The key/cert can be specified as string directly in the config file/environment variables. The key/cert can also be specified as a path to be picked up by [RuntimeResources](https://github.com/travetto/travetto/tree/main/module/runtime/src/resources.ts#L8).
|
|
732
|
+
|
|
733
|
+
## Full Config
|
|
734
|
+
The entire [WebConfig](https://github.com/travetto/travetto/tree/main/module/web/src/config.ts#L7) which will show the full set of valid configuration parameters for the web module.
|