Files
3dverse-testtask/vendor/deno.land/x/oak@v12.5.0/context.ts
T
didier amyot 0d627c9051 Added a very complicated AsyncIterableReader class.
The class was already implemented in
"https://deno.land/std@0.144.0/streams/conversion.ts".  So, I litterally
lost 1 day of work!  I hate myself.
2023-06-12 12:02:15 -04:00

377 lines
11 KiB
TypeScript

// 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)
}`;
}
}