@aklinker1/zeta 1.0.3 → 1.1.1
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 -1
- package/src/adapters/zod-schema-adapter.ts +1 -1
- package/src/app.ts +5 -1
- package/src/internal/call-handler.ts +14 -7
- package/src/meta.ts +14 -0
- package/src/open-api.ts +7 -5
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.1",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"packageManager": "bun@1.2.17",
|
|
@@ -24,6 +25,12 @@
|
|
|
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.'",
|
|
@@ -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,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
validateOutputSchema,
|
|
12
12
|
} from "./utils";
|
|
13
13
|
import { smartDeserialize, smartSerialize } from "./serialization";
|
|
14
|
+
import { getMeta } from "../meta";
|
|
14
15
|
|
|
15
16
|
export async function callHandler(
|
|
16
17
|
ctx: any,
|
|
@@ -18,6 +19,7 @@ export async function callHandler(
|
|
|
18
19
|
method: string,
|
|
19
20
|
path: string,
|
|
20
21
|
) => MatchedRoute<RouterData> | undefined,
|
|
22
|
+
schemaAdapter: SchemaAdapter | undefined,
|
|
21
23
|
): Promise<Response> {
|
|
22
24
|
const route = getRoute(ctx.method, ctx.path);
|
|
23
25
|
if (route == null) {
|
|
@@ -69,10 +71,12 @@ export async function callHandler(
|
|
|
69
71
|
body,
|
|
70
72
|
});
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
{
|
|
75
|
+
let response: any = route.data.handler(ctx);
|
|
76
|
+
if (response instanceof Promise) response = await response;
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
ctx.response = response;
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
for (const hook of route.data.hooks.onAfterHandle) {
|
|
78
82
|
let res = hook.callback(ctx);
|
|
@@ -81,6 +85,7 @@ export async function callHandler(
|
|
|
81
85
|
ctx.response = res;
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
let responseMeta: Record<string, any> | undefined;
|
|
84
89
|
if (!(ctx.response instanceof Response)) {
|
|
85
90
|
if (route.data.def?.responses) {
|
|
86
91
|
if ("~standard" in route.data.def.responses) {
|
|
@@ -88,10 +93,11 @@ export async function callHandler(
|
|
|
88
93
|
route.data.def.responses,
|
|
89
94
|
ctx.response,
|
|
90
95
|
);
|
|
96
|
+
responseMeta = getMeta(schemaAdapter, route.data.def.responses);
|
|
91
97
|
} else {
|
|
92
98
|
if (!ctx.response || !isStatusResult(ctx.response)) {
|
|
93
99
|
throw new Error(
|
|
94
|
-
"When `responses` is a record of schemas, you must return a value from `ctx.status()`.",
|
|
100
|
+
"When `responses` is a record of schemas, you must return a value from `ctx.status(...)`.",
|
|
95
101
|
);
|
|
96
102
|
}
|
|
97
103
|
const { status, body } = ctx.response;
|
|
@@ -102,11 +108,12 @@ export async function callHandler(
|
|
|
102
108
|
}
|
|
103
109
|
ctx.set.status = status;
|
|
104
110
|
ctx.response = validateOutputSchema(schema, body);
|
|
111
|
+
responseMeta = getMeta(schemaAdapter, schema);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
if (response instanceof Response) return response;
|
|
116
|
+
if (ctx.response instanceof Response) return ctx.response;
|
|
110
117
|
|
|
111
118
|
for (const hook of route.data.hooks.onMapResponse) {
|
|
112
119
|
let res = hook.callback(ctx);
|
|
@@ -125,7 +132,7 @@ export async function callHandler(
|
|
|
125
132
|
return new Response(resBody.serialized, {
|
|
126
133
|
status: ctx.set.status,
|
|
127
134
|
headers: {
|
|
128
|
-
"Content-Type": resBody.contentType,
|
|
135
|
+
"Content-Type": responseMeta?.contentType ?? resBody.contentType,
|
|
129
136
|
...ctx.set.headers,
|
|
130
137
|
},
|
|
131
138
|
});
|
package/src/meta.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { SchemaAdapter } from "./types";
|
|
3
|
+
import type { ZetaSchema } from "./custom-responses";
|
|
4
|
+
|
|
5
|
+
/** Get metadata for either a ZetaSchema or a StandardSchemaV1. */
|
|
6
|
+
export function getMeta(
|
|
7
|
+
adapter: SchemaAdapter | undefined,
|
|
8
|
+
schema: StandardSchemaV1 | ZetaSchema,
|
|
9
|
+
): Record<string, any> {
|
|
10
|
+
if ("~zeta" in schema) return schema.meta;
|
|
11
|
+
|
|
12
|
+
if (!adapter) return {};
|
|
13
|
+
return adapter.getMeta(schema) ?? {};
|
|
14
|
+
}
|
package/src/open-api.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { CreateAppOptions } from "./app";
|
|
|
4
4
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
5
5
|
import { getHttpStatusName } from "./status";
|
|
6
6
|
import { ErrorResponseJsonSchema, type ZetaSchema } from "./custom-responses";
|
|
7
|
+
import { getMeta } from "./meta";
|
|
7
8
|
|
|
8
9
|
export function buildOpenApiDocs(
|
|
9
10
|
options: CreateAppOptions<any> | undefined,
|
|
@@ -45,7 +46,7 @@ export function buildOpenApiDocs(
|
|
|
45
46
|
requestBody: body
|
|
46
47
|
? {
|
|
47
48
|
content: {
|
|
48
|
-
[
|
|
49
|
+
[getMeta(adapter, body)?.contentType ?? "application/json"]: {
|
|
49
50
|
schema: adapter.toJsonSchema(body),
|
|
50
51
|
},
|
|
51
52
|
},
|
|
@@ -151,16 +152,18 @@ function buildResponse(
|
|
|
151
152
|
schema: StandardSchemaV1 | ZetaSchema,
|
|
152
153
|
adapter: SchemaAdapter,
|
|
153
154
|
): NonNullable<OpenAPI.Operation["responses"]>[string] {
|
|
155
|
+
const meta = getMeta(adapter, schema);
|
|
156
|
+
|
|
154
157
|
if ("~zeta" in schema) {
|
|
155
158
|
const description =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"";
|
|
159
|
+
meta?.responseDescription ?? getHttpStatusName(status) ?? "";
|
|
160
|
+
|
|
159
161
|
if (schema["~zeta"].type === "NoResponse") {
|
|
160
162
|
return {
|
|
161
163
|
description,
|
|
162
164
|
};
|
|
163
165
|
}
|
|
166
|
+
|
|
164
167
|
if (schema["~zeta"].type === "ErrorResponse") {
|
|
165
168
|
return {
|
|
166
169
|
description,
|
|
@@ -175,7 +178,6 @@ function buildResponse(
|
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
const meta = adapter.getMeta(schema);
|
|
179
181
|
return {
|
|
180
182
|
description: meta?.responseDescription ?? getHttpStatusName(status),
|
|
181
183
|
content: {
|