@aklinker1/zeta 1.2.5 → 1.3.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/package.json CHANGED
@@ -1,7 +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.2.5",
4
+ "version": "1.3.1",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "packageManager": "bun@1.3.2",
@@ -12,6 +12,7 @@
12
12
  "./types": "./src/types.ts",
13
13
  "./client": "./src/client.ts",
14
14
  "./testing": "./src/testing.ts",
15
+ "./schema": "./src/schema.ts",
15
16
  "./adapters/zod-schema-adapter": "./src/adapters/zod-schema-adapter.ts",
16
17
  "./transports/bun-transport": "./src/transports/bun-transport.ts",
17
18
  "./transports/deno-transport": "./src/transports/deno-transport.ts"
package/src/client.ts CHANGED
@@ -8,7 +8,7 @@ import type {
8
8
  GetRequestParamsInput,
9
9
  GetResponseOutput,
10
10
  } from "./types";
11
- import type { ErrorResponse } from "./custom-responses";
11
+ import type { ErrorResponse } from "./schema";
12
12
  import { smartDeserialize, smartSerialize } from "./internal/serialization";
13
13
  import type {
14
14
  GetAppRoutes,
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from "./app";
2
2
  export type { App } from "./types";
3
- export { ErrorResponse, NoResponse, type ZetaSchema } from "./custom-responses";
3
+ export { ErrorResponse, NoResponse, type ZetaSchema } from "./schema";
4
4
  export * from "./status";
5
5
  export * from "./errors";
@@ -13,6 +13,26 @@ export function smartSerialize(value: unknown):
13
13
  };
14
14
  }
15
15
 
16
+ if (value instanceof File) {
17
+ const serialized = new FormData();
18
+ serialized.append("file", value);
19
+ return {
20
+ contentType: undefined,
21
+ serialized,
22
+ };
23
+ }
24
+
25
+ if (typeof FileList !== "undefined" && value instanceof FileList) {
26
+ const serialized = new FormData();
27
+ for (let i = 0; i < value.length; i++) {
28
+ serialized.append("files", value.item(i)!);
29
+ }
30
+ return {
31
+ contentType: undefined,
32
+ serialized,
33
+ };
34
+ }
35
+
16
36
  if (value instanceof Blob) {
17
37
  return {
18
38
  contentType: value.type,
@@ -10,7 +10,7 @@ import type {
10
10
  Transport,
11
11
  } from "../types";
12
12
  import type { MatchedRoute } from "rou3";
13
- import type { ErrorResponse } from "../custom-responses";
13
+ import type { ErrorResponse } from "../schema";
14
14
  import { createBunTransport } from "../transports/bun-transport";
15
15
  import { createDenoTransport } from "../transports/deno-transport";
16
16
 
package/src/meta.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
2
  import type { SchemaAdapter } from "./types";
3
- import type { ZetaSchema } from "./custom-responses";
3
+ import { isZetaSchema, type ZetaSchema } from "./schema";
4
4
 
5
5
  /** Get metadata for either a ZetaSchema or a StandardSchemaV1. */
6
6
  export function getMeta(
7
7
  adapter: SchemaAdapter | undefined,
8
8
  schema: StandardSchemaV1 | ZetaSchema,
9
9
  ): Record<string, any> {
10
- if ("~zeta" in schema) return schema.meta;
10
+ if (isZetaSchema(schema)) return schema["~zeta"].meta;
11
11
 
12
12
  if (!adapter) return {};
13
13
  return adapter.getMeta(schema) ?? {};
package/src/open-api.ts CHANGED
@@ -3,7 +3,11 @@ import type { App, BasePath, SchemaAdapter } from "./types";
3
3
  import type { CreateAppOptions } from "./app";
4
4
  import type { StandardSchemaV1 } from "@standard-schema/spec";
5
5
  import { getHttpStatusName } from "./status";
6
- import { ErrorResponseJsonSchema, type ZetaSchema } from "./custom-responses";
6
+ import {
7
+ ErrorResponseJsonSchema,
8
+ isZetaSchema,
9
+ type ZetaSchema,
10
+ } from "./schema";
7
11
  import { getMeta } from "./meta";
8
12
 
9
13
  export function buildOpenApiDocs(
@@ -51,7 +55,9 @@ export function buildOpenApiDocs(
51
55
  ? {
52
56
  content: {
53
57
  [getMeta(adapter, body)?.contentType ?? "application/json"]: {
54
- schema: adapter.toJsonSchema(body),
58
+ schema: isZetaSchema(body)
59
+ ? body.toJsonSchema?.()
60
+ : adapter.toJsonSchema(body),
55
61
  },
56
62
  },
57
63
  }
@@ -159,7 +165,7 @@ function buildResponse(
159
165
  ): NonNullable<OpenAPI.Operation["responses"]>[string] {
160
166
  const meta = getMeta(adapter, schema);
161
167
 
162
- if ("~zeta" in schema) {
168
+ if (isZetaSchema(schema)) {
163
169
  const description =
164
170
  meta?.responseDescription ?? getHttpStatusName(status) ?? "";
165
171
 
@@ -9,12 +9,14 @@ export type ZetaSchema<Input = unknown, Output = Input> = StandardSchemaV1<
9
9
  type: string;
10
10
  meta: Record<string, any>;
11
11
  };
12
+ toJsonSchema?(): any;
12
13
  meta(meta?: Record<string, any>): ZetaSchema<Input, Output>;
13
14
  };
14
15
 
15
16
  function createZetaSchema<Input = unknown, Output = Input>(
16
17
  name: string,
17
18
  validate: (value: unknown) => StandardSchemaV1.Result<Output>,
19
+ toJsonSchema?: () => any,
18
20
  meta: Record<string, string> = {},
19
21
  ): ZetaSchema<Input, Output> {
20
22
  const parentMeta = meta;
@@ -29,11 +31,12 @@ function createZetaSchema<Input = unknown, Output = Input>(
29
31
  validate,
30
32
  },
31
33
  meta(meta) {
32
- return createZetaSchema(name, validate, {
34
+ return createZetaSchema(name, validate, undefined, {
33
35
  ...parentMeta,
34
36
  ...meta,
35
37
  });
36
38
  },
39
+ toJsonSchema,
37
40
  };
38
41
  }
39
42
 
@@ -97,6 +100,10 @@ export const ErrorResponse: ZetaSchema<unknown, ErrorResponse> =
97
100
  },
98
101
  );
99
102
 
103
+ export function isZetaSchema(schema: any): schema is ZetaSchema {
104
+ return schema?.["~standard"]?.vendor === "@aklinker/zeta";
105
+ }
106
+
100
107
  /**
101
108
  * The actual type an error response conforms to.
102
109
  */
@@ -164,3 +171,101 @@ export const NoResponse: ZetaSchema<undefined | null | void, void> =
164
171
  : { value: undefined };
165
172
  },
166
173
  );
174
+
175
+ export const FormDataBody: ZetaSchema<FormData> = createZetaSchema<FormData>(
176
+ "FormDataBody",
177
+ (value: unknown): StandardSchemaV1.Result<FormData> => {
178
+ return value instanceof FormData
179
+ ? { value }
180
+ : {
181
+ issues: [{ message: `Expected FormData, got ${typeof value}` }],
182
+ };
183
+ },
184
+ () => ({
185
+ type: "object",
186
+ additionalProperties: true,
187
+ }),
188
+ {
189
+ contentType: "multipart/form-data",
190
+ },
191
+ );
192
+
193
+ export const UploadFileBody: ZetaSchema<File> = createZetaSchema<File>(
194
+ "UploadFileBody",
195
+ (value: unknown): StandardSchemaV1.Result<File> => {
196
+ if (!(value instanceof FormData)) {
197
+ return {
198
+ issues: [{ message: `Expected FormData, got ${typeof value}` }],
199
+ };
200
+ }
201
+
202
+ const file = value.get("file");
203
+ if (!(file instanceof File)) {
204
+ return {
205
+ issues: [{ message: `Expected File, got ${typeof file}` }],
206
+ };
207
+ }
208
+
209
+ return { value: file };
210
+ },
211
+ () => ({
212
+ type: "object",
213
+ properties: {
214
+ file: {
215
+ type: "string",
216
+ format: "binary",
217
+ },
218
+ },
219
+ }),
220
+ {
221
+ contentType: "multipart/form-data",
222
+ },
223
+ );
224
+
225
+ export const UploadFilesBody: ZetaSchema<FormData, File[]> = createZetaSchema<
226
+ FormData,
227
+ File[]
228
+ >(
229
+ "UploadFilesBody",
230
+ (value): StandardSchemaV1.Result<File[]> => {
231
+ if (!(value instanceof FormData)) {
232
+ return {
233
+ issues: [{ message: `Expected FormData, got ${typeof value}` }],
234
+ };
235
+ }
236
+
237
+ const files = value.getAll("file");
238
+ if (!Array.isArray(files)) {
239
+ return {
240
+ issues: [{ message: `Expected array of Files, got ${typeof files}` }],
241
+ };
242
+ }
243
+
244
+ const issues: string[] = [];
245
+ for (const file of files) {
246
+ if (!(file instanceof File)) {
247
+ issues.push(`Expected File, got ${typeof file}`);
248
+ }
249
+ }
250
+ if (issues.length > 0) {
251
+ return { issues: issues.map((message) => ({ message })) };
252
+ }
253
+
254
+ return { value: files as File[] };
255
+ },
256
+ () => ({
257
+ type: "object",
258
+ properties: {
259
+ files: {
260
+ type: "array",
261
+ items: {
262
+ type: "string",
263
+ format: "binary",
264
+ },
265
+ },
266
+ },
267
+ }),
268
+ {
269
+ contentType: "multipart/form-data",
270
+ },
271
+ );