@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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # Zeta
2
2
 
3
- [![JSR](https://jsr.io/badges/@aklinker1/zeta)](https://jsr.io/@aklinker1/zeta) [![Docs](https://img.shields.io/badge/Docs-blue?logo=readme&logoColor=white)](https://jsr.io/@aklinker1/zeta) [![API Reference](https://img.shields.io/badge/API%20Reference-blue?logo=readme&logoColor=white)](https://jsr.io/@aklinker1/zeta/doc) [![License](https://img.shields.io/github/license/aklinker1/zeta)](https://github.com/aklinker1/zeta/blob/main/LICENSE) [![Changelog](https://img.shields.io/badge/Changelog-blue?logo=github&logoColor=white)](https://github.com/aklinker1/zeta/blob/main/CHANGELOG.md)
3
+ [![JSR](https://jsr.io/badges/@aklinker1/zeta)](https://jsr.io/@aklinker1/zeta) [![NPM Version](https://img.shields.io/npm/v/%40aklinker1%2Fzeta?logo=npm&labelColor=red&color=white)](https://www.npmjs.com/package/@aklinker1/zeta) [![Docs](https://img.shields.io/badge/Docs-blue?logo=readme&logoColor=white)](https://jsr.io/@aklinker1/zeta) [![API Reference](https://img.shields.io/badge/API%20Reference-blue?logo=readme&logoColor=white)](https://jsr.io/@aklinker1/zeta/doc) [![License](https://img.shields.io/github/license/aklinker1/zeta)](https://github.com/aklinker1/zeta/blob/main/LICENSE) [![Changelog](https://img.shields.io/badge/Changelog-blue?logo=github&logoColor=white)](https://github.com/aklinker1/zeta/blob/main/CHANGELOG.md)
4
4
 
5
- Personal alternative to [Elysia](https://elysiajs.com/) with better validation support.
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. you can override this by setting the `contentType` metadata on your schema:
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
- "version": "1.0.2",
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: "draft-7" });
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(ctx, getRoute);
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
- let response: any = route.data.handler(ctx);
73
- if (response instanceof Promise) response = await response;
73
+ {
74
+ let response: any = route.data.handler(ctx);
75
+ if (response instanceof Promise) response = await response;
74
76
 
75
- ctx.response = response;
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
  });