-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhandler.ts
130 lines (122 loc) · 3.56 KB
/
handler.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
import { STATUS_CODE } from "@std/http/status";
import { ZodError, type ZodType } from "zod";
import { type Awaitable, settled } from "./async.ts";
export * from "@std/http/status";
type Merge<T, U> = Omit<T, keyof U> & U;
export type Handler<C = unknown> = (
req: Request,
ctx: C,
) => Awaitable<Response>;
export function toFetch(handler: Handler): (req: Request) => Promise<Response> {
return async (req) => await handler(req, null);
}
export function logTime<C>(handler: Handler<C>): Handler<C> {
return async (req, ctx) => {
const start = performance.now();
const result = await settled(handler(req, ctx));
const end = performance.now();
const rt = (end - start).toFixed(1);
if (result.status === "rejected") {
const error = result.reason;
console.warn(`${req.method} ${req.url} - ${rt} ms - ${error}`);
throw error;
}
const res = result.value;
res.headers.append("server-timing", `rt;dur=${rt}`);
console.log(`${req.method} ${req.url} - ${rt} ms - ${res.status}`);
return res;
};
}
export function supportedMethods<C>(
methods: object & Iterable<string>,
handler: Handler<C>,
): Handler<C> {
const set = new Set(methods);
if (set.has("GET")) {
set.add("HEAD");
}
return async (req, ctx) => {
if (!set.has(req.method)) {
return Response.json(
{ error: `Method ${req.method} is not implemented` },
{
status: STATUS_CODE.NotImplemented,
headers: [
["connection", "close"],
],
},
);
}
return await handler(req, ctx);
};
}
export function route<C>(
routes: {
readonly [pathname: string]: Handler<
Merge<C, { readonly params: Record<string, string | undefined> }>
>;
},
fallback: Handler<C>,
): Handler<C> {
const entries = Object.entries(routes).map(([pathname, handler]) => ({
pattern: new URLPattern({ pathname }),
handler,
}));
return async (req, ctx) => {
const url = new URL(req.url);
const path = (ctx as { params?: { 0?: string } } | null)?.params?.[0];
if (path !== undefined) {
url.pathname = path;
}
for (const { pattern, handler } of entries) {
const match = pattern.exec(url);
if (match) {
return await handler(req, { ...ctx, params: match.pathname.groups });
}
}
return await fallback(req, ctx);
};
}
export function methods<C>(methods: Record<string, Handler<C>>): Handler<C> {
const map = new Map(Object.entries(methods));
const get = map.get("GET");
if (get) {
map.set("HEAD", get);
}
const allow = Array.from(map.keys()).sort().join(", ");
return async (req, ctx) => {
const handler = map.get(req.method);
const res = handler ? await handler(req, ctx) : Response.json(
{ error: `Method ${req.method} is not allowed for the URL` },
{ status: STATUS_CODE.MethodNotAllowed },
);
res.headers.append("allow", allow);
return res;
};
}
export function parseBodyAsJson<T, C>(
T: ZodType<T>,
handler: Handler<Merge<C, { readonly body: T }>>,
): Handler<C> {
return async (req, ctx) => {
let body: T;
try {
body = T.parse(await req.json());
} catch (e) {
if (e instanceof SyntaxError) {
return Response.json(
{ error: e.message },
{ status: STATUS_CODE.BadRequest },
);
}
if (e instanceof ZodError) {
return Response.json(
{ error: "Cannot parse body", issues: e.issues },
{ status: STATUS_CODE.BadRequest },
);
}
throw e;
}
return await handler(req, { ...ctx, body });
};
}