Closed rhutikcodes closed 1 year ago
I'm doing the opposite. I'm using zod to define my params, request bodies and responses. And then with a utility function I generate the open api types from the zod definition.
This helps me type the "data" argument and also the returned types of my endpoints.
I can share with you the utility function but I'm away from my computer til Tuesday.
@gimenete that's really cool, can you post some sample codes in this issue? I think this library would benefit a lot by having that integration with zod natively
@gimenete Interesting! Please share some example code once you are back :)
Ok. This is what I have. It doesn't support everything itty-router-openapi
supports in terms of types and viceversa, but here it is:
// Shared functionality
import {
Arr,
Enumeration,
Int,
Path,
Query,
Str,
} from "@cloudflare/itty-router-openapi";
import { z } from "zod";
type SchemaOptions = {
example?: string;
// to be extended in the future with more options
};
export function path<T extends z.ZodTypeAny>(s: T, options?: SchemaOptions): T {
Object.assign(s, { location: "path", ...options });
return s;
}
export function query<T extends z.ZodTypeAny>(
s: T,
options?: SchemaOptions
): T {
Object.assign(s, { location: "query", ...options });
return s;
}
export function buildSchema<T extends z.ZodRawShape>(shape: T) {
const object = z.object(shape);
const schema: Record<string, any> = {};
for (const [key, type] of Object.entries(shape)) {
const required = !type.isOptional() && !type.isNullable(); // TODO: distinguish between optional and nullable
const description = type.description;
const location =
"location" in type && typeof type.location === "string"
? type.location
: null;
const Func =
location === "path" ? Path : location === "query" ? Query : null;
const options = { required, description };
const value =
type instanceof z.ZodOptional || type instanceof z.ZodNullable
? type.unwrap()
: type;
if (value instanceof z.ZodString) {
// TODO: regex
schema[key] = Func ? Func(Str, options) : new Str(options);
} else if (value instanceof z.ZodNumber) {
schema[key] = Func ? Func(Int, options) : new Int(options);
} else if (value instanceof z.ZodBoolean) {
schema[key] = Func ? Func(Boolean, options) : new Boolean(options);
} else if (value instanceof z.ZodObject) {
schema[key] = buildSchema(value.shape).schema;
} else if (value instanceof z.ZodArray) {
schema[key] = Func
? Func(new Arr(new Str(), options))
: new Arr(new Str(), options);
} else if (value instanceof z.ZodEnum) {
const values = Object.fromEntries(
value._def.values.map((v: any) => [String(v), String(v)])
);
schema[key] = Func
? Func(Enumeration, options)
: new Enumeration({ ...options, values });
} else {
console.log("Unsupported type:", value);
}
}
return { object, schema };
}
Then you can use it like this to create a zod and openapi schema:
// An example of an endpoint that has params, request body and a non-void response
const parametersSchema = buildSchema({
// you can use the `path` or `query` utility functions to build parameter types
userId: path(z.string().describe("The user id"), { example: "user1234" }),
});
const bodySchema = buildSchema({
firstName: z.string(),
lastName: z.string(),
});
const responseSchema = buildSchema({
id: z.string(),
firstName: z.string(),
lastName: z.string(),
email: z.string(),
imageURL: z.string().nullable(),
});
export class UpdateUser extends OpenAPIRoute {
static schema: OpenAPISchema = {
summary: "Update a user",
parameters: parametersSchema.schema,
requestBody: bodySchema.schema,
responses: {
"200": {
schema: responseSchema.schema,
},
},
};
async handle(
request: Request,
env: Bindings,
context: ExecutionContext,
data: z.infer<typeof parametersSchema.object> & {
body: z.infer<typeof bodySchema.object>;
}
): z.infer<typeof responseSchema.object> {
// "data" is fully typed here, and the returned type of the function is typed as well
}
}
The usage is simple. buildSchema()
is similar to the way you create schemas with zod and it returns an object with two props: object
which is the zod object definition and schema
which is the openapi schema definition.
Probaby this could be simplified because creating the class that extends OpenAPIRoute
has a bit of boilerplate.
Thanks 😊 This is really nice. Also how do i configure cors?
I am using itty-cors, but its not working.
import { createCors } from 'itty-cors';
const { preflight, corsify } = createCors({ origins: ['*'] });
router
.all('/auth/*', authRouter)
.all('/resume/*', resumeRouter)
.all('*', () => missing('Are you sure about that?')); // 404 for all else
export default {
async fetch(request: Request, env: EnvironmentVariable, ctx: ExecutionContext): Promise<Response> {
ctx.waitUntil.bind(ctx);
return router
.handle(request, ctx.waitUntil.bind(ctx))
.catch((err) => error(500, err.stack))
.then(corsify); // cors should be applied to error responses as well;
},
};
where do i put preflight?
Thanks 😊 This is really nice. Also how do i configure cors?
I am using itty-cors, but its not working.
import { createCors } from 'itty-cors'; const { preflight, corsify } = createCors({ origins: ['*'] }); router .all('/auth/*', authRouter) .all('/resume/*', resumeRouter) .all('*', () => missing('Are you sure about that?')); // 404 for all else export default { async fetch(request: Request, env: EnvironmentVariable, ctx: ExecutionContext): Promise<Response> { ctx.waitUntil.bind(ctx); return router .handle(request, ctx.waitUntil.bind(ctx)) .catch((err) => error(500, err.stack)) .then(corsify); // cors should be applied to error responses as well; }, };
where do i put preflight?
what did you do to get cors to work? when i try your code it says Uncaught (in response) TypeError: Cannot read properties of undefined (reading 'bind')
@kjolibois try the official cors example in the docs here: https://cloudflare.github.io/itty-router-openapi/user-guide/cors/
@kjolibois try the official cors example in the docs here: https://cloudflare.github.io/itty-router-openapi/user-guide/cors/
Thank you, I used that and got it to work.
I have this Schema
I want to convert it into an interface (IAddUserToWaitlist). Any easy way? without writing it manually
@G4brym @ericlewis @gimenete ?