cloudflare / chanfana

OpenAPI 3 and 3.1 schema generator and validator for Hono, itty-router and more!
https://chanfana.pages.dev
MIT License
288 stars 38 forks source link

Is there any way to convert Schema into typescript interface #47

Closed rhutikcodes closed 1 year ago

rhutikcodes commented 1 year ago

I have this Schema

const AddUserToWaitlistSchema = {
  email: new Str({ required: true }),
  isVerfied: new Bool({ required: true }),
};

I want to convert it into an interface (IAddUserToWaitlist). Any easy way? without writing it manually

export async function addUserToWaitlist(payload: IAddUserToWaitlist) {
  try {
    console.log(payload.email);
    return {
      message: "success",
    };
  } catch (error) {
    logger.error("Error while addUserToWaitlist()", error);
    if (error instanceof DBConnectionError) {
      throw error;
    } else {
      throw new AddUserToWaitlistError(
        ADD_USER_TO_WAITLIST_ERROR.message,
        ADD_USER_TO_WAITLIST_ERROR.errorCode,
        500
      );
    }
  }
}

@G4brym @ericlewis @gimenete ?

gimenete commented 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.

G4brym commented 1 year ago

@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

rhutikcodes commented 1 year ago

@gimenete Interesting! Please share some example code once you are back :)

gimenete commented 1 year ago

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.

rhutikcodes commented 1 year ago

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?

kjolibois commented 6 months ago

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')

G4brym commented 6 months ago

@kjolibois try the official cors example in the docs here: https://cloudflare.github.io/itty-router-openapi/user-guide/cors/

kjolibois commented 6 months ago

@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.