-
Notifications
You must be signed in to change notification settings - Fork 235
/
context.ts
372 lines (349 loc) · 10.8 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
// Copyright 2018-2024 the oak authors. All rights reserved. MIT license.
/**
* Contains the {@linkcode Context} class which is the context that is provided
* to middleware.
*
* Typically this is not used directly by end users except when creating
* re-usable middleware.
*
* @module
*/
import type { Application, State } from "./application.ts";
import {
createHttpError,
type ErrorStatus,
type HttpErrorOptions,
type KeyStack,
SecureCookieMap,
type ServerSentEventTarget,
type ServerSentEventTargetOptions,
} from "./deps.ts";
import { Request } from "./request.ts";
import { Response } from "./response.ts";
import { send, type SendOptions } from "./send.ts";
import type { ServerRequest, UpgradeWebSocketOptions } from "./types.ts";
/** Options that can be supplied when creating a {@linkcode Context} */
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;
}
/** Options that can be supplied when using the `.send()` method. */
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 "jsr:@oak/oak/";
*
* 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 "jsr:@oak/oak/";
*
* 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(
condition: unknown,
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 ("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, resolving with 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.
*
* Invoking this will cause the a response to be sent to the client
* immediately to initialize the stream of events, and therefore any further
* changes to the response, like headers will not reach the client.
*/
async sendEvents(
options?: ServerSentEventTargetOptions,
): Promise<ServerSentEventTarget> {
if (!this.#sse) {
const sse = this.#sse = await this.request.sendEvents(options, {
headers: this.response.headers,
});
this.app.addEventListener("close", () => sse.close());
this.respond = false;
}
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) {
const socket = this.#socket = this.request.upgrade(options);
this.app.addEventListener("close", () => socket.close());
this.respond = false;
}
return this.#socket;
}
[Symbol.for("Deno.customInspect")](
inspect: (value: unknown) => string,
): 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,
// deno-lint-ignore no-explicit-any
): any {
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)
}`;
}
}