@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 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.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: "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,
@@ -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
- let response: any = route.data.handler(ctx);
73
- if (response instanceof Promise) response = await response;
74
+ {
75
+ let response: any = route.data.handler(ctx);
76
+ if (response instanceof Promise) response = await response;
74
77
 
75
- ctx.response = response;
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
- [adapter.getMeta(body)?.contentType ?? "application/json"]: {
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
- schema["~zeta"].meta?.responseDescription ??
157
- getHttpStatusName(status) ??
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: {