drizzle-team / drizzle-orm

Headless TypeScript ORM with a head. Runs on Node, Bun and Deno. Lives on the Edge and yes, it's a JavaScript ORM too 😅
https://orm.drizzle.team
Apache License 2.0
23.61k stars 581 forks source link

[FEATURE]: Type Coercion in `drizzle-zod` #776

Open rawnly opened 1 year ago

rawnly commented 1 year ago

Describe what you want

It would be nice to being able to support zod types coercion when using drizzle-zod. A real world example below:

Consider the following table:

const users = pgTable('users', {
  id: serial('id').primaryKey(),
  created_at: timestamp('created_at').defaultNow()
})

const insertSchema = createInsertSchema(users)

export type InsertUser = z.infer<typeof insertSchema>
export const InsertUser = insertSchema

Later on my endpoint:

export async function GET(request: NextRequest) {
   const body  = await request.json();
   const payload = InsertUser.parse(body) // throws error since Date is serialised as string / number in http requests
}

Something like the following could help writing less boilerplate code and avoid code duplication:

const users = pgTable('users', {
  id: serial('id').primaryKey(),
  created_at: timestamp('created_at').defaultNow()
})

// in order to avoid breaking the current api it can be overloaded and support both function signs (old and new one)
const insertSchema = createInsertSchema(users, true, { /* refinements */ })

export type InsertUser = z.infer<typeof insertSchema>
export const InsertUser = insertSchema
rawnly commented 1 year ago

If this is something you might consider, I can work on a PR. It shouldn't be too difficult to implement as what I saw

krishna-404 commented 10 months ago

Will be happy to contribute to this if somebody can guide me how...

The issue I am facing is that RHF outputs all values as string.. there is a work around to use valueAsNumber but thats difficult to work with MUI.

Right now I am making it work as below... would be good to have auto coercion setup....

export const insertLicSchema = createInsertSchema(lic_table, {
    mobile_number: z.coerce.number()
})
wesbos commented 7 months ago

Would love to see this too - though this refinement API seems to be pretty nice for this type of thing:

export const insertListingSchema = createInsertSchema(listings, {
  price: ({ price }) => z.coerce.number().min(500).pipe(price)
});
danielsharvey commented 3 months ago

In order to handle date coercion, I transform the Zod schemas. This may be useful for someone:

/**
 * Make the following schema changes:
 * - Add coercion to ZodDate's (handling optional/nullable wrappers)
 *
 * @param schema
 * @returns
 */
export function fixType<TSchema extends z.ZodTypeAny>(schema: TSchema): TSchema {
  if(schema._def.typeName === z.ZodFirstPartyTypeKind.ZodNullable) {
    const s = schema as unknown as z.ZodNullable<any>;
    const u = s.unwrap();
    return fixType(u).nullable();
  } else if(schema._def.typeName === z.ZodFirstPartyTypeKind.ZodOptional) {
    const s = schema as unknown as z.ZodOptional<any>;
    const u = s.unwrap();
    return fixType(u).optional();
  } else if(schema._def.typeName === z.ZodFirstPartyTypeKind.ZodDate) {
    return z.coerce.date() as unknown as TSchema;
    // return z.string().datetime({ offset:true });
  } else {
    return schema;
  }
}

/**
 * Transform Zod schema to convert Date's to use coercion to handle/parse
 * string inputs.
 *
 * @param schema
 * @returns
 */
export function transformSchema<TSchema extends z.AnyZodObject>(schema: TSchema) {
  // also see https://github.com/colinhacks/zod/discussions/2050#discussioncomment-5018870
  const entries = Object.entries( schema.shape ) as
        [ keyof TSchema[ 'shape' ], z.ZodTypeAny ][]
  const ret = schema.merge(z.object(
    Object.fromEntries(
      entries.map(([k,v]) => ([k,fixType(v)]))
    ),
  ));

  return ret as TSchema;
}

This is used as follows:

const zodTable = transformSchema(createSelectSchema(table));