@aklinker1/zeta 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +655 -0
- package/package.json +60 -0
- package/src/adapters/zod-schema-adapter.ts +46 -0
- package/src/app.ts +453 -0
- package/src/client.ts +183 -0
- package/src/custom-responses.ts +166 -0
- package/src/errors.ts +543 -0
- package/src/index.ts +5 -0
- package/src/internal/call-handler.ts +132 -0
- package/src/internal/serialization.ts +72 -0
- package/src/internal/utils.ts +131 -0
- package/src/open-api.ts +234 -0
- package/src/status.ts +143 -0
- package/src/testing.ts +62 -0
- package/src/types.ts +1111 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function smartSerialize(value: unknown):
|
|
2
|
+
| {
|
|
3
|
+
contentType: string;
|
|
4
|
+
serialized: BodyInit;
|
|
5
|
+
}
|
|
6
|
+
| undefined {
|
|
7
|
+
if (value == null) return undefined;
|
|
8
|
+
|
|
9
|
+
if (value instanceof FormData) {
|
|
10
|
+
return {
|
|
11
|
+
contentType: "multipart/form-data",
|
|
12
|
+
serialized: value,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (value instanceof Blob) {
|
|
17
|
+
return {
|
|
18
|
+
contentType: value.type,
|
|
19
|
+
serialized: value,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
switch (typeof value) {
|
|
24
|
+
case "number":
|
|
25
|
+
case "boolean":
|
|
26
|
+
case "bigint":
|
|
27
|
+
case "string":
|
|
28
|
+
return { contentType: "text/plain", serialized: String(value) };
|
|
29
|
+
case "object":
|
|
30
|
+
return {
|
|
31
|
+
contentType: "application/json",
|
|
32
|
+
serialized: JSON.stringify(value),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw Error(
|
|
37
|
+
"Could not serialize object for request: " + JSON.stringify(value),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function smartDeserialize(
|
|
42
|
+
arg: Response | Request,
|
|
43
|
+
): Promise<unknown> {
|
|
44
|
+
const contentType = arg.headers.get("content-type");
|
|
45
|
+
if (contentType == null) return;
|
|
46
|
+
|
|
47
|
+
// JSON
|
|
48
|
+
if (contentType.startsWith("application/json")) {
|
|
49
|
+
return await arg.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Forms
|
|
53
|
+
if (
|
|
54
|
+
contentType.startsWith("application/x-www-form-urlencoded") ||
|
|
55
|
+
contentType.startsWith("multipart/form-data")
|
|
56
|
+
) {
|
|
57
|
+
return await arg.formData();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Text
|
|
61
|
+
if (contentType.startsWith("text/")) {
|
|
62
|
+
return await arg.text();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Binary
|
|
66
|
+
if (contentType.startsWith("application/octet-stream")) {
|
|
67
|
+
return await arg.arrayBuffer();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Unknown
|
|
71
|
+
throw Error(`Unknown content type: "${contentType}"`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import { HttpError, ValidationGlobalError } from "../errors";
|
|
3
|
+
import { HttpStatus } from "../status";
|
|
4
|
+
import type {
|
|
5
|
+
App,
|
|
6
|
+
LifeCycleHook,
|
|
7
|
+
MaybePromise,
|
|
8
|
+
RouterData,
|
|
9
|
+
StatusResult,
|
|
10
|
+
} from "../types";
|
|
11
|
+
import type { MatchedRoute } from "rou3";
|
|
12
|
+
import type { ErrorResponse } from "../custom-responses";
|
|
13
|
+
|
|
14
|
+
export function validateSchema<T>(
|
|
15
|
+
schema: StandardSchemaV1<T, T>,
|
|
16
|
+
input: unknown,
|
|
17
|
+
): T {
|
|
18
|
+
let res = schema["~standard"].validate(input);
|
|
19
|
+
if (res instanceof Promise) throw Error("Async validation not supported");
|
|
20
|
+
|
|
21
|
+
if (res.issues) throw new ValidationGlobalError(input, res.issues);
|
|
22
|
+
|
|
23
|
+
return res.value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createHttpSchemaValidator(status: HttpStatus, message: string) {
|
|
27
|
+
return <T>(schema: StandardSchemaV1<T, T>, input: unknown): T => {
|
|
28
|
+
try {
|
|
29
|
+
return validateSchema<T>(schema, input);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err instanceof ValidationGlobalError) {
|
|
32
|
+
throw new HttpError(status, message, {
|
|
33
|
+
issues: err.issues,
|
|
34
|
+
input: err.input,
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const validateInputSchema = createHttpSchemaValidator(
|
|
44
|
+
HttpStatus.BadRequest,
|
|
45
|
+
"Input validation failed",
|
|
46
|
+
);
|
|
47
|
+
export const validateOutputSchema = createHttpSchemaValidator(
|
|
48
|
+
HttpStatus.UnprocessableEntity,
|
|
49
|
+
"Output validation failed",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export function isApp(obj: unknown): obj is App<any> {
|
|
53
|
+
return (obj as any)[Symbol.toStringTag] === "ZetaApp";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getRawQuery(url: URL): Record<string, string> {
|
|
57
|
+
const query: Record<string, string> = {};
|
|
58
|
+
const params = url.searchParams;
|
|
59
|
+
const entries = params.entries();
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) query[entry[0]] = entry[1];
|
|
62
|
+
return query;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getRawParams(
|
|
66
|
+
route: MatchedRoute<RouterData>,
|
|
67
|
+
): Record<string, string> {
|
|
68
|
+
const rawParams = route.params ?? {};
|
|
69
|
+
// Rename _ to ** for validation and consistency
|
|
70
|
+
if ("_" in rawParams) {
|
|
71
|
+
rawParams["**"] = rawParams["_"];
|
|
72
|
+
delete rawParams["_"];
|
|
73
|
+
}
|
|
74
|
+
return rawParams;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getErrorStack(err: Error): string[] | undefined {
|
|
78
|
+
if (process.env.NODE_ENV === "production") return;
|
|
79
|
+
return err.stack
|
|
80
|
+
?.split("\n")
|
|
81
|
+
.map((line) => line.trim())
|
|
82
|
+
.slice(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function serializeErrorResponse(err: unknown): ErrorResponse {
|
|
86
|
+
if (err instanceof HttpError)
|
|
87
|
+
return {
|
|
88
|
+
status: err.status,
|
|
89
|
+
name: err.name,
|
|
90
|
+
message: err.message,
|
|
91
|
+
...err.additionalInfo,
|
|
92
|
+
stack: getErrorStack(err),
|
|
93
|
+
cause: err.cause != null ? serializeErrorResponse(err.cause) : undefined,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (err instanceof Error)
|
|
97
|
+
return {
|
|
98
|
+
status: HttpStatus.InternalServerError,
|
|
99
|
+
name: err.name,
|
|
100
|
+
message: err.message,
|
|
101
|
+
stack: getErrorStack(err),
|
|
102
|
+
cause: err.cause != null ? serializeErrorResponse(err.cause) : undefined,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
name: "Unknown Error",
|
|
107
|
+
message: "An unknown error occurred",
|
|
108
|
+
status: HttpStatus.InternalServerError,
|
|
109
|
+
stack: getErrorStack(err as Error),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function callCtxModifierHooks(
|
|
114
|
+
ctx: any,
|
|
115
|
+
hooks: LifeCycleHook<
|
|
116
|
+
(ctx: any) => MaybePromise<Record<string, any> | void>
|
|
117
|
+
>[],
|
|
118
|
+
): Promise<Response | undefined> {
|
|
119
|
+
for (const hook of hooks) {
|
|
120
|
+
let res = hook.callback(ctx);
|
|
121
|
+
res = res instanceof Promise ? await res : res;
|
|
122
|
+
if (res instanceof Response) return res;
|
|
123
|
+
if (res) Object.assign(ctx, res);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const IsStatusResult = Symbol("IsStatusResult");
|
|
128
|
+
|
|
129
|
+
export function isStatusResult(result: any): result is StatusResult {
|
|
130
|
+
return IsStatusResult in result;
|
|
131
|
+
}
|
package/src/open-api.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { OpenAPI } from "openapi-types";
|
|
2
|
+
import type { App, BasePath, SchemaAdapter } from "./types";
|
|
3
|
+
import type { CreateAppOptions } from "./app";
|
|
4
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
5
|
+
import { getHttpStatusName } from "./status";
|
|
6
|
+
import { ErrorResponseJsonSchema, type ZetaSchema } from "./custom-responses";
|
|
7
|
+
|
|
8
|
+
export function buildOpenApiDocs(
|
|
9
|
+
options: CreateAppOptions<any> | undefined,
|
|
10
|
+
app: App,
|
|
11
|
+
): { type: "success"; spec: any } | { type: "error"; error: unknown } {
|
|
12
|
+
try {
|
|
13
|
+
if (!options?.schemaAdapter)
|
|
14
|
+
return { type: "error", error: "OpenAPI docs require a schema adapter" };
|
|
15
|
+
const adapter = options.schemaAdapter;
|
|
16
|
+
|
|
17
|
+
const userDoc = options.openApi ?? {};
|
|
18
|
+
const docs: OpenAPI.Document = {
|
|
19
|
+
openapi: "3.1.0",
|
|
20
|
+
...userDoc,
|
|
21
|
+
info: {
|
|
22
|
+
title: "Zeta Application",
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
...userDoc.info,
|
|
25
|
+
},
|
|
26
|
+
paths: {},
|
|
27
|
+
components: {
|
|
28
|
+
...userDoc.components,
|
|
29
|
+
schemas: {
|
|
30
|
+
...userDoc.components?.schemas,
|
|
31
|
+
ErrorResponse: ErrorResponseJsonSchema,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
for (const [method, methodEntry] of Object.entries(app["~zeta"].routes)) {
|
|
36
|
+
for (const [path, routerData] of Object.entries(methodEntry)) {
|
|
37
|
+
const openApiPath = path.replace(/\/:([^/]+)/g, "/{$1}");
|
|
38
|
+
const { headers, params, query, body, responses, ...openApiOperation } =
|
|
39
|
+
routerData.def ?? {};
|
|
40
|
+
docs.paths ??= {};
|
|
41
|
+
docs.paths[openApiPath] ??= {};
|
|
42
|
+
|
|
43
|
+
(docs.paths[openApiPath] as any)[method.toLowerCase()] = {
|
|
44
|
+
...openApiOperation,
|
|
45
|
+
requestBody: body
|
|
46
|
+
? {
|
|
47
|
+
content: {
|
|
48
|
+
[adapter.getMeta(body)?.contentType ?? "application/json"]: {
|
|
49
|
+
schema: adapter.toJsonSchema(body),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
: undefined,
|
|
54
|
+
parameters: [
|
|
55
|
+
...mapParameters(adapter, params, "path"),
|
|
56
|
+
...mapParameters(adapter, query, "query"),
|
|
57
|
+
...mapParameters(adapter, headers, "header"),
|
|
58
|
+
] as OpenAPI.Parameters,
|
|
59
|
+
responses: {
|
|
60
|
+
...(!responses
|
|
61
|
+
? {}
|
|
62
|
+
: "~standard" in responses
|
|
63
|
+
? {
|
|
64
|
+
200: buildResponse(200, responses, adapter),
|
|
65
|
+
}
|
|
66
|
+
: Object.fromEntries(
|
|
67
|
+
Object.entries(responses).map(([status, response]) => [
|
|
68
|
+
status,
|
|
69
|
+
buildResponse(Number(status), response, adapter),
|
|
70
|
+
]),
|
|
71
|
+
)),
|
|
72
|
+
...((params || query || headers || body) && {
|
|
73
|
+
400: {
|
|
74
|
+
description: "Bad Request",
|
|
75
|
+
content: {
|
|
76
|
+
"application/json": {
|
|
77
|
+
schema: {
|
|
78
|
+
$ref: "#/components/schemas/ErrorResponse",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
} as OpenAPI.Operation;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { type: "success", spec: optimizeSpec(docs) };
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return { type: "error", error };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildScalarHtml(
|
|
96
|
+
jsonRoute: BasePath,
|
|
97
|
+
options: CreateAppOptions<any> | undefined,
|
|
98
|
+
): string {
|
|
99
|
+
const scalarConfig = {
|
|
100
|
+
// Aaron's preferences
|
|
101
|
+
defaultOpenAllTags: true,
|
|
102
|
+
|
|
103
|
+
// User options
|
|
104
|
+
...options?.scalar,
|
|
105
|
+
|
|
106
|
+
// Required config
|
|
107
|
+
url: jsonRoute,
|
|
108
|
+
};
|
|
109
|
+
return `<!doctype html>
|
|
110
|
+
<html>
|
|
111
|
+
<head>
|
|
112
|
+
<title>API Reference</title>
|
|
113
|
+
<meta charset="utf-8" />
|
|
114
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<div id="app"></div>
|
|
118
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
119
|
+
<script>
|
|
120
|
+
const config = ${JSON.stringify(scalarConfig)};
|
|
121
|
+
if (${process.env.NODE_ENV !== "production"}) {
|
|
122
|
+
config.servers ??= [];
|
|
123
|
+
config.servers.unshift({ url: location.origin })
|
|
124
|
+
}
|
|
125
|
+
Scalar.createApiReference('#app', config)
|
|
126
|
+
</script>
|
|
127
|
+
</body>
|
|
128
|
+
</html>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function mapParameters(
|
|
133
|
+
adapter: SchemaAdapter,
|
|
134
|
+
schema: StandardSchemaV1 | undefined,
|
|
135
|
+
_in: "query" | "path" | "header",
|
|
136
|
+
): OpenAPI.Parameters {
|
|
137
|
+
if (!schema) return [];
|
|
138
|
+
|
|
139
|
+
return adapter
|
|
140
|
+
.parseParamsRecord(schema)
|
|
141
|
+
.map(({ schema, optional, ...param }) => ({
|
|
142
|
+
...param,
|
|
143
|
+
in: _in,
|
|
144
|
+
schema: adapter.toJsonSchema(schema),
|
|
145
|
+
required: !optional,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildResponse(
|
|
150
|
+
status: number,
|
|
151
|
+
schema: StandardSchemaV1 | ZetaSchema,
|
|
152
|
+
adapter: SchemaAdapter,
|
|
153
|
+
): NonNullable<OpenAPI.Operation["responses"]>[string] {
|
|
154
|
+
if ("~zeta" in schema) {
|
|
155
|
+
const description =
|
|
156
|
+
schema["~zeta"].meta?.responseDescription ??
|
|
157
|
+
getHttpStatusName(status) ??
|
|
158
|
+
"";
|
|
159
|
+
if (schema["~zeta"].type === "NoResponse") {
|
|
160
|
+
return {
|
|
161
|
+
description,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (schema["~zeta"].type === "ErrorResponse") {
|
|
165
|
+
return {
|
|
166
|
+
description,
|
|
167
|
+
content: {
|
|
168
|
+
"application/json": {
|
|
169
|
+
schema: {
|
|
170
|
+
$ref: "#/components/schemas/ErrorResponse",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const meta = adapter.getMeta(schema);
|
|
179
|
+
return {
|
|
180
|
+
description: meta?.responseDescription ?? getHttpStatusName(status),
|
|
181
|
+
content: {
|
|
182
|
+
[meta?.contentType ?? "application/json"]: {
|
|
183
|
+
schema: adapter.toJsonSchema(schema),
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function optimizeSpec(spec: OpenAPI.Document): OpenAPI.Document {
|
|
190
|
+
const optimized = structuredClone(spec);
|
|
191
|
+
|
|
192
|
+
// Optimizations
|
|
193
|
+
addModelRefs(optimized);
|
|
194
|
+
|
|
195
|
+
return optimized;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Look for `ref` properties from schema metadata and move those models to
|
|
200
|
+
* `components.schemas`.
|
|
201
|
+
*/
|
|
202
|
+
function addModelRefs(spec: any): void {
|
|
203
|
+
const recurse = (obj: any): void => {
|
|
204
|
+
if (obj == null) return;
|
|
205
|
+
if (typeof obj !== "object") return;
|
|
206
|
+
|
|
207
|
+
// Recursively update array items
|
|
208
|
+
if (Array.isArray(obj)) {
|
|
209
|
+
for (let i = 0, il = obj.length; i < il; i++) recurse(obj[i]);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Recursively update object properties
|
|
214
|
+
const values = Object.values(obj);
|
|
215
|
+
for (let i = 0, il = values.length; i < il; i++) recurse(values[i]);
|
|
216
|
+
|
|
217
|
+
// Move model if it includes a ref
|
|
218
|
+
if (typeof obj.ref === "string") {
|
|
219
|
+
const ref = obj.ref;
|
|
220
|
+
spec.components ??= {};
|
|
221
|
+
spec.components.schemas ??= {};
|
|
222
|
+
spec.components.schemas[ref] = {
|
|
223
|
+
...structuredClone(obj),
|
|
224
|
+
// Remove any zeta-only properties OpenAPI doesn't support
|
|
225
|
+
ref: undefined,
|
|
226
|
+
};
|
|
227
|
+
for (const key of Object.keys(obj)) delete obj[key];
|
|
228
|
+
obj.$ref = `#/components/schemas/${ref}`;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Process the "paths" object
|
|
233
|
+
recurse(spec.paths);
|
|
234
|
+
}
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enum containing all HTTP status codes.
|
|
3
|
+
*/
|
|
4
|
+
export enum HttpStatus {
|
|
5
|
+
Continue = 100,
|
|
6
|
+
SwitchingProtocols = 101,
|
|
7
|
+
ProcessingDeprecated = 102,
|
|
8
|
+
EarlyHints = 103,
|
|
9
|
+
Ok = 200,
|
|
10
|
+
Created = 201,
|
|
11
|
+
Accepted = 202,
|
|
12
|
+
NonAuthoritativeInformation = 203,
|
|
13
|
+
NoContent = 204,
|
|
14
|
+
ResetContent = 205,
|
|
15
|
+
PartialContent = 206,
|
|
16
|
+
MultiStatus = 207,
|
|
17
|
+
AlreadyReported = 208,
|
|
18
|
+
ImUsed = 226,
|
|
19
|
+
MultipleChoices = 300,
|
|
20
|
+
MovedPermanently = 301,
|
|
21
|
+
Found = 302,
|
|
22
|
+
SeeOther = 303,
|
|
23
|
+
NotModified = 304,
|
|
24
|
+
UseProxyDeprecated = 305,
|
|
25
|
+
Unused = 306,
|
|
26
|
+
TemporaryRedirect = 307,
|
|
27
|
+
PermanentRedirect = 308,
|
|
28
|
+
BadRequest = 400,
|
|
29
|
+
Unauthorized = 401,
|
|
30
|
+
PaymentRequired = 402,
|
|
31
|
+
Forbidden = 403,
|
|
32
|
+
NotFound = 404,
|
|
33
|
+
MethodNotAllowed = 405,
|
|
34
|
+
NotAcceptable = 406,
|
|
35
|
+
ProxyAuthenticationRequired = 407,
|
|
36
|
+
RequestTimeout = 408,
|
|
37
|
+
Conflict = 409,
|
|
38
|
+
Gone = 410,
|
|
39
|
+
LengthRequired = 411,
|
|
40
|
+
PreconditionFailed = 412,
|
|
41
|
+
ContentTooLarge = 413,
|
|
42
|
+
UriTooLong = 414,
|
|
43
|
+
UnsupportedMediaType = 415,
|
|
44
|
+
RangeNotSatisfiable = 416,
|
|
45
|
+
ExpectationFailed = 417,
|
|
46
|
+
ImATeapot = 418,
|
|
47
|
+
MisdirectedRequest = 421,
|
|
48
|
+
UnprocessableEntity = 422,
|
|
49
|
+
Locked = 423,
|
|
50
|
+
FailedDependency = 424,
|
|
51
|
+
TooEarly = 425,
|
|
52
|
+
UpgradeRequired = 426,
|
|
53
|
+
PreconditionRequired = 428,
|
|
54
|
+
TooManyRequests = 429,
|
|
55
|
+
RequestHeaderFieldsTooLarge = 431,
|
|
56
|
+
UnavailableForLegalReasons = 451,
|
|
57
|
+
InternalServerError = 500,
|
|
58
|
+
NotImplemented = 501,
|
|
59
|
+
BadGateway = 502,
|
|
60
|
+
ServiceUnavailable = 503,
|
|
61
|
+
GatewayTimeout = 504,
|
|
62
|
+
HttpVersionNotSupported = 505,
|
|
63
|
+
VariantAlsoNegotiates = 506,
|
|
64
|
+
InsufficientStorage = 507,
|
|
65
|
+
LoopDetected = 508,
|
|
66
|
+
NotExtended = 510,
|
|
67
|
+
NetworkAuthenticationRequired = 511,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const STATUS_NAME_MAP: Record<HttpStatus, string> = {
|
|
71
|
+
[HttpStatus.Continue]: "Continue",
|
|
72
|
+
[HttpStatus.SwitchingProtocols]: "Switching Protocols",
|
|
73
|
+
[HttpStatus.ProcessingDeprecated]: "Processing",
|
|
74
|
+
[HttpStatus.EarlyHints]: "Early Hints",
|
|
75
|
+
[HttpStatus.Ok]: "OK",
|
|
76
|
+
[HttpStatus.Created]: "Created",
|
|
77
|
+
[HttpStatus.Accepted]: "Accepted",
|
|
78
|
+
[HttpStatus.NonAuthoritativeInformation]: "Non-Authoritative Information",
|
|
79
|
+
[HttpStatus.NoContent]: "No Content",
|
|
80
|
+
[HttpStatus.ResetContent]: "Reset Content",
|
|
81
|
+
[HttpStatus.PartialContent]: "Partial Content",
|
|
82
|
+
[HttpStatus.MultiStatus]: "Multi-Status",
|
|
83
|
+
[HttpStatus.AlreadyReported]: "Already Reported",
|
|
84
|
+
[HttpStatus.ImUsed]: "IM Used",
|
|
85
|
+
[HttpStatus.MultipleChoices]: "Multiple Choices",
|
|
86
|
+
[HttpStatus.MovedPermanently]: "Moved Permanently",
|
|
87
|
+
[HttpStatus.Found]: "Found",
|
|
88
|
+
[HttpStatus.SeeOther]: "See Other",
|
|
89
|
+
[HttpStatus.NotModified]: "Not Modified",
|
|
90
|
+
[HttpStatus.UseProxyDeprecated]: "Use Proxy",
|
|
91
|
+
[HttpStatus.Unused]: "Unused",
|
|
92
|
+
[HttpStatus.TemporaryRedirect]: "Temporary Redirect",
|
|
93
|
+
[HttpStatus.PermanentRedirect]: "Permanent Redirect",
|
|
94
|
+
[HttpStatus.BadRequest]: "Bad Request",
|
|
95
|
+
[HttpStatus.Unauthorized]: "Unauthorized",
|
|
96
|
+
[HttpStatus.PaymentRequired]: "Payment Required",
|
|
97
|
+
[HttpStatus.Forbidden]: "Forbidden",
|
|
98
|
+
[HttpStatus.NotFound]: "Not Found",
|
|
99
|
+
[HttpStatus.MethodNotAllowed]: "Method Not Allowed",
|
|
100
|
+
[HttpStatus.NotAcceptable]: "Not Acceptable",
|
|
101
|
+
[HttpStatus.ProxyAuthenticationRequired]: "Proxy Authentication Required",
|
|
102
|
+
[HttpStatus.RequestTimeout]: "Request Timeout",
|
|
103
|
+
[HttpStatus.Conflict]: "Conflict",
|
|
104
|
+
[HttpStatus.Gone]: "Gone",
|
|
105
|
+
[HttpStatus.LengthRequired]: "Length Required",
|
|
106
|
+
[HttpStatus.PreconditionFailed]: "Precondition Failed",
|
|
107
|
+
[HttpStatus.ContentTooLarge]: "Content Too Large",
|
|
108
|
+
[HttpStatus.UriTooLong]: "URI Too Long",
|
|
109
|
+
[HttpStatus.UnsupportedMediaType]: "Unsupported Media Type",
|
|
110
|
+
[HttpStatus.RangeNotSatisfiable]: "Range Not Satisfiable",
|
|
111
|
+
[HttpStatus.ExpectationFailed]: "Expectation Failed",
|
|
112
|
+
[HttpStatus.ImATeapot]: "I'm a Teapot",
|
|
113
|
+
[HttpStatus.MisdirectedRequest]: "Misdirected Request",
|
|
114
|
+
[HttpStatus.UnprocessableEntity]: "Unprocessable Entity",
|
|
115
|
+
[HttpStatus.Locked]: "Locked",
|
|
116
|
+
[HttpStatus.FailedDependency]: "Failed Dependency",
|
|
117
|
+
[HttpStatus.TooEarly]: "Too Early",
|
|
118
|
+
[HttpStatus.UpgradeRequired]: "Upgrade Required",
|
|
119
|
+
[HttpStatus.PreconditionRequired]: "Precondition Required",
|
|
120
|
+
[HttpStatus.TooManyRequests]: "Too Many Requests",
|
|
121
|
+
[HttpStatus.RequestHeaderFieldsTooLarge]: "Request Header Fields Too Large",
|
|
122
|
+
[HttpStatus.UnavailableForLegalReasons]: "Unavailable For Legal Reasons",
|
|
123
|
+
[HttpStatus.InternalServerError]: "Internal Server Error",
|
|
124
|
+
[HttpStatus.NotImplemented]: "Not Implemented",
|
|
125
|
+
[HttpStatus.BadGateway]: "Bad Gateway",
|
|
126
|
+
[HttpStatus.ServiceUnavailable]: "Service Unavailable",
|
|
127
|
+
[HttpStatus.GatewayTimeout]: "Gateway Timeout",
|
|
128
|
+
[HttpStatus.HttpVersionNotSupported]: "HTTP Version Not Supported",
|
|
129
|
+
[HttpStatus.VariantAlsoNegotiates]: "Variant Also Negotiates",
|
|
130
|
+
[HttpStatus.InsufficientStorage]: "Insufficient Storage",
|
|
131
|
+
[HttpStatus.LoopDetected]: "Loop Detected",
|
|
132
|
+
[HttpStatus.NotExtended]: "Not Extended",
|
|
133
|
+
[HttpStatus.NetworkAuthenticationRequired]: "Network Authentication Required",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns the name of the HTTP status code (200 -> "OK").
|
|
138
|
+
* @param status The HTTP status code.
|
|
139
|
+
* @returns The name of the HTTP status code or `undefined` if the status code is not recognized.
|
|
140
|
+
*/
|
|
141
|
+
export function getHttpStatusName(status: number): string | undefined {
|
|
142
|
+
return STATUS_NAME_MAP[status as HttpStatus];
|
|
143
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for testing your server-side application.
|
|
3
|
+
*
|
|
4
|
+
* You don't need to use these utils, you can call the app's `fetch` function directly.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const app = createApp()
|
|
8
|
+
* .get("/example", () => "Hello, world!")
|
|
9
|
+
* const fetch = app.build();
|
|
10
|
+
*
|
|
11
|
+
* const res = await fetch("http://test/example");
|
|
12
|
+
* expect(res.status).toEqual(200);
|
|
13
|
+
* expect(await res.text()).toEqual("Hello, world!");
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* If you just care about the response body or error thrown, you can use the `createTestAppClient`.
|
|
17
|
+
*
|
|
18
|
+
* @see {@link createTestAppClient}
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
createAppClient,
|
|
23
|
+
type AppClient,
|
|
24
|
+
type CreateAppClientOptions,
|
|
25
|
+
type GetClientRoutes,
|
|
26
|
+
} from "./client";
|
|
27
|
+
import type { App } from "./types";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a client for testing your server-side application. When `fetch` is
|
|
31
|
+
* called, the app's `fetch` function is called instead of using the global
|
|
32
|
+
* `fetch`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const app = createApp()
|
|
37
|
+
* .get("/example", () => "Hello, world!")
|
|
38
|
+
*
|
|
39
|
+
* const client = createTestAppClient(app);
|
|
40
|
+
*
|
|
41
|
+
* expect(await client.get("/example")).toEqual("Hello, world!");
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @param app
|
|
45
|
+
* @param options
|
|
46
|
+
* @returns An app client used to test your server-side application.
|
|
47
|
+
*/
|
|
48
|
+
export function createTestAppClient<TApp extends App>(
|
|
49
|
+
app: TApp,
|
|
50
|
+
options?: Omit<CreateAppClientOptions, "fetch">,
|
|
51
|
+
): AppClient<GetClientRoutes<TApp>> {
|
|
52
|
+
const { baseUrl = "http://localhost" } = options ?? {};
|
|
53
|
+
|
|
54
|
+
const fetch = app.build();
|
|
55
|
+
|
|
56
|
+
return createAppClient({
|
|
57
|
+
baseUrl,
|
|
58
|
+
...options,
|
|
59
|
+
// @ts-expect-error: Fetch type varies between environments
|
|
60
|
+
fetch: (...args) => fetch(new Request(...args)),
|
|
61
|
+
});
|
|
62
|
+
}
|