@aklinker1/zeta 1.0.2 → 1.1.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/README.md +5 -21
- package/package.json +8 -2
- package/src/adapters/zod-schema-adapter.ts +1 -1
- package/src/app.ts +5 -1
- package/src/internal/call-handler.ts +13 -7
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Zeta
|
|
2
2
|
|
|
3
|
-
[](https://jsr.io/@aklinker1/zeta) [](https://jsr.io/@aklinker1/zeta) [](https://jsr.io/@aklinker1/zeta/doc) [](https://github.com/aklinker1/zeta/blob/main/LICENSE) [](https://github.com/aklinker1/zeta/blob/main/CHANGELOG.md)
|
|
3
|
+
[](https://jsr.io/@aklinker1/zeta) [](https://www.npmjs.com/package/@aklinker1/zeta) [](https://jsr.io/@aklinker1/zeta) [](https://jsr.io/@aklinker1/zeta/doc) [](https://github.com/aklinker1/zeta/blob/main/LICENSE) [](https://github.com/aklinker1/zeta/blob/main/CHANGELOG.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Composable, testable, OpenAPI-first backend framework with validation built-in.
|
|
6
6
|
|
|
7
7
|
**Features**
|
|
8
8
|
|
|
@@ -268,7 +268,9 @@ When a response schema(s) are defined, the return value from the function is typ
|
|
|
268
268
|
|
|
269
269
|
#### Custom Content Types
|
|
270
270
|
|
|
271
|
-
By default, Zeta will use `application/json` as the content type in the OpenAPI docs
|
|
271
|
+
By default, Zeta will use `application/json` as the content type in the OpenAPI docs and infer the content type of the response at runtime based on the response type.
|
|
272
|
+
|
|
273
|
+
You can override both the docs and the response content type by setting the `contentType` metadata on your schema:
|
|
272
274
|
|
|
273
275
|
```ts
|
|
274
276
|
app.get(
|
|
@@ -282,24 +284,6 @@ app.get(
|
|
|
282
284
|
);
|
|
283
285
|
```
|
|
284
286
|
|
|
285
|
-
> [!WARNING]
|
|
286
|
-
>
|
|
287
|
-
> Zeta ignores this metadata when building the response. Make sure to set the `Content-Type` header in your handler:
|
|
288
|
-
>
|
|
289
|
-
> ```ts
|
|
290
|
-
> app.get(
|
|
291
|
-
> "/csv",
|
|
292
|
-
> {
|
|
293
|
-
> responses: z.string().meta({ contentType: "text/csv" }),
|
|
294
|
-
> },
|
|
295
|
-
> ({ set }) => {
|
|
296
|
-
> // ...
|
|
297
|
-
> set.headers["Content-Type"] = "text/csv";
|
|
298
|
-
> return "...";
|
|
299
|
-
> },
|
|
300
|
-
> );
|
|
301
|
-
> ```
|
|
302
|
-
|
|
303
287
|
## Life Cycle Hooks
|
|
304
288
|
|
|
305
289
|
## `App#decorate`
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aklinker1/zeta",
|
|
3
|
-
"
|
|
3
|
+
"description": "Composable, testable, OpenAPI-first backend framework with validation built-in",
|
|
4
|
+
"version": "1.1.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"packageManager": "bun@1.2.17",
|
|
@@ -24,13 +25,18 @@
|
|
|
24
25
|
"repository": {
|
|
25
26
|
"url": "https://github.com/aklinker1/zeta"
|
|
26
27
|
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"backend",
|
|
30
|
+
"server",
|
|
31
|
+
"validation",
|
|
32
|
+
"openapi"
|
|
33
|
+
],
|
|
27
34
|
"scripts": {
|
|
28
35
|
"dev": "bun test --watch",
|
|
29
36
|
"build": "echo 'Noop - nothing to build.'",
|
|
30
37
|
"bench": "bun run src/__tests__/bench.ts",
|
|
31
38
|
"example": "bun --watch run example.ts",
|
|
32
39
|
"example:prod": "NODE_ENV=production bun run example",
|
|
33
|
-
"publish": "bun run scripts/publish.ts",
|
|
34
40
|
"docs:dev": "vitepress dev docs",
|
|
35
41
|
"docs:build": "vitepress build docs",
|
|
36
42
|
"docs:preview": "vitepress preview docs"
|
|
@@ -21,7 +21,7 @@ const { z } = await import(zod);
|
|
|
21
21
|
export const zodSchemaAdapter: SchemaAdapter = {
|
|
22
22
|
toJsonSchema: (schema) => {
|
|
23
23
|
if (!("_zod" in schema)) throw Error("input schema is not a Zod schema");
|
|
24
|
-
const res = z.toJSONSchema(schema, { target: "
|
|
24
|
+
const res = z.toJSONSchema(schema, { target: "openapi-3.0" });
|
|
25
25
|
delete res.$schema;
|
|
26
26
|
return res;
|
|
27
27
|
},
|
package/src/app.ts
CHANGED
|
@@ -168,7 +168,11 @@ export function createApp<TPrefix extends BasePrefix = "">(
|
|
|
168
168
|
return onGlobalRequestResponse;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
const response = await callHandler(
|
|
171
|
+
const response = await callHandler(
|
|
172
|
+
ctx,
|
|
173
|
+
getRoute,
|
|
174
|
+
options?.schemaAdapter,
|
|
175
|
+
);
|
|
172
176
|
ctx.response = response;
|
|
173
177
|
|
|
174
178
|
return response;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MatchedRoute } from "rou3";
|
|
2
|
-
import type { RouterData } from "../types";
|
|
2
|
+
import type { RouterData, SchemaAdapter } from "../types";
|
|
3
3
|
import { NotFoundHttpError } from "../errors";
|
|
4
4
|
import {
|
|
5
5
|
callCtxModifierHooks,
|
|
@@ -18,6 +18,7 @@ export async function callHandler(
|
|
|
18
18
|
method: string,
|
|
19
19
|
path: string,
|
|
20
20
|
) => MatchedRoute<RouterData> | undefined,
|
|
21
|
+
schemaAdapter: SchemaAdapter | undefined,
|
|
21
22
|
): Promise<Response> {
|
|
22
23
|
const route = getRoute(ctx.method, ctx.path);
|
|
23
24
|
if (route == null) {
|
|
@@ -69,10 +70,12 @@ export async function callHandler(
|
|
|
69
70
|
body,
|
|
70
71
|
});
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
{
|
|
74
|
+
let response: any = route.data.handler(ctx);
|
|
75
|
+
if (response instanceof Promise) response = await response;
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
ctx.response = response;
|
|
78
|
+
}
|
|
76
79
|
|
|
77
80
|
for (const hook of route.data.hooks.onAfterHandle) {
|
|
78
81
|
let res = hook.callback(ctx);
|
|
@@ -81,6 +84,7 @@ export async function callHandler(
|
|
|
81
84
|
ctx.response = res;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
let responseMeta: Record<string, any> | undefined;
|
|
84
88
|
if (!(ctx.response instanceof Response)) {
|
|
85
89
|
if (route.data.def?.responses) {
|
|
86
90
|
if ("~standard" in route.data.def.responses) {
|
|
@@ -88,10 +92,11 @@ export async function callHandler(
|
|
|
88
92
|
route.data.def.responses,
|
|
89
93
|
ctx.response,
|
|
90
94
|
);
|
|
95
|
+
responseMeta = schemaAdapter?.getMeta(route.data.def.responses);
|
|
91
96
|
} else {
|
|
92
97
|
if (!ctx.response || !isStatusResult(ctx.response)) {
|
|
93
98
|
throw new Error(
|
|
94
|
-
"When `responses` is a record of schemas, you must return a value from `ctx.status()`.",
|
|
99
|
+
"When `responses` is a record of schemas, you must return a value from `ctx.status(...)`.",
|
|
95
100
|
);
|
|
96
101
|
}
|
|
97
102
|
const { status, body } = ctx.response;
|
|
@@ -102,11 +107,12 @@ export async function callHandler(
|
|
|
102
107
|
}
|
|
103
108
|
ctx.set.status = status;
|
|
104
109
|
ctx.response = validateOutputSchema(schema, body);
|
|
110
|
+
responseMeta = schemaAdapter?.getMeta(schema);
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
if (response instanceof Response) return response;
|
|
115
|
+
if (ctx.response instanceof Response) return ctx.response;
|
|
110
116
|
|
|
111
117
|
for (const hook of route.data.hooks.onMapResponse) {
|
|
112
118
|
let res = hook.callback(ctx);
|
|
@@ -125,7 +131,7 @@ export async function callHandler(
|
|
|
125
131
|
return new Response(resBody.serialized, {
|
|
126
132
|
status: ctx.set.status,
|
|
127
133
|
headers: {
|
|
128
|
-
"Content-Type": resBody.contentType,
|
|
134
|
+
"Content-Type": responseMeta?.contentType ?? resBody.contentType,
|
|
129
135
|
...ctx.set.headers,
|
|
130
136
|
},
|
|
131
137
|
});
|