forked from oakserver/oak
-
Notifications
You must be signed in to change notification settings - Fork 0
/
context.ts
376 lines (354 loc) · 11.2 KB
/
context.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
// Copyright 2018-2023 the oak authors. All rights reserved. MIT license.
import type { Application, State } from "./application.ts";
import {
createHttpError,
type ErrorStatus,
type HttpErrorOptions,
KeyStack,
SecureCookieMap,
ServerSentEventStreamTarget,
type ServerSentEventTarget,
type ServerSentEventTargetOptions,
} from "./deps.ts";
import { Request } from "./request.ts";
import { Response } from "./response.ts";
import { send, SendOptions } from "./send.ts";
import type { ServerRequest, UpgradeWebSocketOptions } from "./types.d.ts";
import { assert } from "./util.ts";
export interface ContextOptions<
S extends AS = State,
// deno-lint-ignore no-explicit-any
AS extends State = Record<string, any>,
> {
jsonBodyReplacer?: (
key: string,
value: unknown,
context: Context<S>,
) => unknown;
jsonBodyReviver?: (
key: string,
value: unknown,
context: Context<S>,
) => unknown;
secure?: boolean;
}
export interface ContextSendOptions extends SendOptions {
/** The filename to send, which will be resolved based on the other options.
* If this property is omitted, the current context's `.request.url.pathname`
* will be used. */
path?: string;
}
/** Provides context about the current request and response to middleware
* functions, and the current instance being processed is the first argument
* provided a {@linkcode Middleware} function.
*
* _Typically this is only used as a type annotation and shouldn't be
* constructed directly._
*
* ### Example
*
* ```ts
* import { Application, Context } from "https://deno.land/x/oak/mod.ts";
*
* const app = new Application();
*
* app.use((ctx) => {
* // information about the request is here:
* ctx.request;
* // information about the response is here:
* ctx.response;
* // the cookie store is here:
* ctx.cookies;
* });
*
* // Needs a type annotation because it cannot be inferred.
* function mw(ctx: Context) {
* // process here...
* }
*
* app.use(mw);
* ```
*
* @template S the state which extends the application state (`AS`)
* @template AS the type of the state derived from the application
*/
export class Context<
S extends AS = State,
// deno-lint-ignore no-explicit-any
AS extends State = Record<string, any>,
> {
#socket?: WebSocket;
#sse?: ServerSentEventTarget;
#wrapReviverReplacer(
reviver?: (key: string, value: unknown, context: this) => unknown,
): undefined | ((key: string, value: unknown) => unknown) {
return reviver
? (key: string, value: unknown) => reviver(key, value, this)
: undefined;
}
/** A reference to the current application. */
app: Application<AS>;
/** An object which allows access to cookies, mediating both the request and
* response. */
cookies: SecureCookieMap;
/** Is `true` if the current connection is upgradeable to a web socket.
* Otherwise the value is `false`. Use `.upgrade()` to upgrade the connection
* and return the web socket. */
get isUpgradable(): boolean {
const upgrade = this.request.headers.get("upgrade");
if (!upgrade || upgrade.toLowerCase() !== "websocket") {
return false;
}
const secKey = this.request.headers.get("sec-websocket-key");
return typeof secKey === "string" && secKey != "";
}
/** Determines if the request should be responded to. If `false` when the
* middleware completes processing, the response will not be sent back to the
* requestor. Typically this is used if the middleware will take over low
* level processing of requests and responses, for example if using web
* sockets. This automatically gets set to `false` when the context is
* upgraded to a web socket via the `.upgrade()` method.
*
* The default is `true`. */
respond: boolean;
/** An object which contains information about the current request. */
request: Request;
/** An object which contains information about the response that will be sent
* when the middleware finishes processing. */
response: Response;
/** If the the current context has been upgraded, then this will be set to
* with the current web socket, otherwise it is `undefined`. */
get socket(): WebSocket | undefined {
return this.#socket;
}
/** The object to pass state to front-end views. This can be typed by
* supplying the generic state argument when creating a new app. For
* example:
*
* ```ts
* const app = new Application<{ foo: string }>();
* ```
*
* Or can be contextually inferred based on setting an initial state object:
*
* ```ts
* const app = new Application({ state: { foo: "bar" } });
* ```
*
* On each request/response cycle, the context's state is cloned from the
* application state. This means changes to the context's `.state` will be
* dropped when the request drops, but "defaults" can be applied to the
* application's state. Changes to the application's state though won't be
* reflected until the next request in the context's state.
*/
state: S;
constructor(
app: Application<AS>,
serverRequest: ServerRequest,
state: S,
{
secure = false,
jsonBodyReplacer,
jsonBodyReviver,
}: ContextOptions<S, AS> = {},
) {
this.app = app;
this.state = state;
const { proxy } = app;
this.request = new Request(
serverRequest,
{
proxy,
secure,
jsonBodyReviver: this.#wrapReviverReplacer(jsonBodyReviver),
},
);
this.respond = true;
this.response = new Response(
this.request,
this.#wrapReviverReplacer(jsonBodyReplacer),
);
this.cookies = new SecureCookieMap(serverRequest, {
keys: this.app.keys as KeyStack | undefined,
response: this.response,
secure: this.request.secure,
});
}
/** Asserts the condition and if the condition fails, creates an HTTP error
* with the provided status (which defaults to `500`). The error status by
* default will be set on the `.response.status`.
*
* Because of limitation of TypeScript, any assertion type function requires
* specific type annotations, so the {@linkcode Context} type should be used
* even if it can be inferred from the context.
*
* ### Example
*
* ```ts
* import { Context, Status } from "https://deno.land/x/oak/mod.ts";
*
* export function mw(ctx: Context) {
* const body = ctx.request.body();
* ctx.assert(body.type === "json", Status.NotAcceptable);
* // process the body and send a response...
* }
* ```
*/
assert(
// deno-lint-ignore no-explicit-any
condition: any,
errorStatus: ErrorStatus = 500,
message?: string,
props?: Record<string, unknown> & Omit<HttpErrorOptions, "status">,
): asserts condition {
if (condition) {
return;
}
const httpErrorOptions: HttpErrorOptions = {};
if (typeof props === "object") {
if ("headers" in props) {
httpErrorOptions.headers = props.headers;
delete props.headers;
}
if ("expose" in props) {
httpErrorOptions.expose = props.expose;
delete props.expose;
}
}
const err = createHttpError(errorStatus, message, httpErrorOptions);
if (props) {
Object.assign(err, props);
}
throw err;
}
/** Asynchronously fulfill a response with a file from the local file
* system.
*
* If the `options.path` is not supplied, the file to be sent will default
* to this `.request.url.pathname`.
*
* Requires Deno read permission. */
send(options: ContextSendOptions): Promise<string | undefined> {
const { path = this.request.url.pathname, ...sendOptions } = options;
return send(this, path, sendOptions);
}
/** Convert the connection to stream events, returning an event target for
* sending server sent events. Events dispatched on the returned target will
* be sent to the client and be available in the client's `EventSource` that
* initiated the connection.
*
* **Note** the body needs to be returned to the client to be able to
* dispatch events, so dispatching events within the middleware will delay
* sending the body back to the client.
*
* This will set the response body and update response headers to support
* sending SSE events. Additional middleware should not modify the body.
*/
sendEvents(options?: ServerSentEventTargetOptions): ServerSentEventTarget {
if (!this.#sse) {
assert(this.response.writable, "The response is not writable.");
const sse = this.#sse = new ServerSentEventStreamTarget(options);
this.app.addEventListener("close", () => sse.close());
const [bodyInit, { headers }] = sse.asResponseInit({
headers: this.response.headers,
});
this.response.body = bodyInit;
if (headers instanceof Headers) {
this.response.headers = headers;
}
}
return this.#sse;
}
/** Create and throw an HTTP Error, which can be used to pass status
* information which can be caught by other middleware to send more
* meaningful error messages back to the client. The passed error status will
* be set on the `.response.status` by default as well.
*/
throw(
errorStatus: ErrorStatus,
message?: string,
props?: Record<string, unknown>,
): never {
const err = createHttpError(errorStatus, message);
if (props) {
Object.assign(err, props);
}
throw err;
}
/** Take the current request and upgrade it to a web socket, resolving with
* the a web standard `WebSocket` object. This will set `.respond` to
* `false`. If the socket cannot be upgraded, this method will throw. */
upgrade(options?: UpgradeWebSocketOptions): WebSocket {
if (this.#socket) {
return this.#socket;
}
if (!this.request.originalRequest.upgrade) {
throw new TypeError(
"Web socket upgrades not currently supported for this type of server.",
);
}
this.#socket = this.request.originalRequest.upgrade(options);
this.app.addEventListener("close", () => this.#socket?.close());
this.respond = false;
return this.#socket;
}
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) {
const {
app,
cookies,
isUpgradable,
respond,
request,
response,
socket,
state,
} = this;
return `${this.constructor.name} ${
inspect({
app,
cookies,
isUpgradable,
respond,
request,
response,
socket,
state,
})
}`;
}
[Symbol.for("nodejs.util.inspect.custom")](
depth: number,
// deno-lint-ignore no-explicit-any
options: any,
inspect: (value: unknown, options?: unknown) => string,
) {
if (depth < 0) {
return options.stylize(`[${this.constructor.name}]`, "special");
}
const newOptions = Object.assign({}, options, {
depth: options.depth === null ? null : options.depth - 1,
});
const {
app,
cookies,
isUpgradable,
respond,
request,
response,
socket,
state,
} = this;
return `${options.stylize(this.constructor.name, "special")} ${
inspect({
app,
cookies,
isUpgradable,
respond,
request,
response,
socket,
state,
}, newOptions)
}`;
}
}