-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
127 lines (117 loc) · 4.03 KB
/
Copy pathserver.ts
File metadata and controls
127 lines (117 loc) · 4.03 KB
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
/**
* `@mastrojs/api/server` contains functions for the API server.
* @module
*/
// deno-lint-ignore-file no-explicit-any
import { getParams, jsonResponse } from "@mastrojs/mastro";
import { Err, type Result } from "@mastrojs/result";
import type { Method } from "./client.ts";
import type { StandardSchemaV1 } from "./standard-schema.ts";
/**
* Constructs a JSON API route, handling request input validation and JSON serialization.
* The returned type contains also all route information for the `fetchApi<JsonRoute>` client function.
*
* Example usage: place the following into `routes/todo/[id].server.ts`
*
* ```ts
* import { Err, Ok } from "@mastrojs/result";
* import { jsonRoute } from "@mastrojs/api/server";
* import { boolean, object, optional, string } from "../../validate.js";
*
* export type TodoPatch = typeof PATCH;
* export const { PATCH } = jsonRoute({
* method: "PATCH",
* path: `/todo/${"id" as string}`,
* params: object({ id: string }),
* body: object({ done: optional(boolean), title: optional(string) }),
* }, async ({ body, params }) => {
* const { done, title } = body;
* const { id } = params;
* const updatedTodo = await db.updateTodo(id, { done, title });
* return updatedTodo ? Ok(updatedTodo) : Err("Not found", 404);
* });
* ```
*/
export const jsonRoute = <
M extends Method,
P extends Record<string, number | string | undefined>,
Q extends Record<string, number | string | undefined>,
R extends object,
U extends string,
B = undefined,
>(
opts: {
/** HTTP method */
method: M;
/** Path as a string literal. Its value is not used,
* but its type is used to verify the client. Example: `` path: `/users/${"id" as string}` `` */
path: U;
/** Schema for URL path parameters */
params?: StandardSchemaV1<unknown, P>;
/** Schema for URL query parameters */
query?: StandardSchemaV1<unknown, Q>;
/** Schema for JSON request body */
body?: StandardSchemaV1<unknown, B>;
},
/**
* Callback that's called with the validated request parameters and should return a `Result`.
*/
handler: (
context: { body: B; params: P; query: Q; req: Request },
) => Result<R> | Promise<Result<R>>,
): { [method in M]: JsonRoute<B, M, P, Q, R, U> } => ({
[opts.method]: async (req: Request) => {
const url = new URL(req.url);
const params = getParams(req);
const paramsRes = await opts.params?.["~standard"].validate(params);
if (paramsRes?.issues) {
return response(Err("URL path failed to validate", 401, undefined, paramsRes));
}
const query = Object.fromEntries(url.searchParams);
const queryRes = await opts.query?.["~standard"].validate(query);
if (queryRes?.issues) {
return response(Err(`queryParams failed to validate`, 401, undefined, queryRes));
}
let body;
if (opts.body) {
let data: unknown;
try {
data = await req.json();
} catch (e) {
return response(Err(e instanceof Error ? e.message : "invalid JSON", 400));
}
const result = await opts.body["~standard"].validate(data);
if (result.issues) {
return response(Err("Body failed to validate", 400, undefined, result));
} else {
body = result.value;
}
}
const res = await handler({ body, params, query, req } as any);
return response(res);
},
} as { [method in M]: JsonRoute<B, M, P, Q, R, U> });
/**
* This is the standard `(req: Request) => Response` type, along with a few phantom types
* (aka branded types), which we use for type-checking in `fetchApi`.
*/
export type JsonRoute<
B = any,
M extends Method = any,
P = any,
Q = any,
R = any,
U extends string = any,
> = ((req: Request) => Response | Promise<Response>) & {
__method: M;
__params: P;
__path: U;
__queryParams: Q;
__reqBody: B;
__resBody: R;
};
const response = (res: Result<object>): Response =>
res.ok
? jsonResponse(res.val)
// JSON.stringify({ cause: Error("no") }) gives {"cause":{}} so we're not leaking stack traces:
: jsonResponse(res, res.statusCode || 500);