import { z } from "@hono/zod-openapi"; import { type Assume, type Column, type DrizzleTypeError, type Equal, getTableColumns, is, type Simplify, type Table, } from "drizzle-orm"; import { MySqlChar, MySqlVarBinary, MySqlVarChar, } from "drizzle-orm/mysql-core"; import { type PgArray, PgChar, PgUUID, PgVarchar } from "drizzle-orm/pg-core"; import { SQLiteText } from "drizzle-orm/sqlite-core"; const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); type Literal = z.infer; type Json = Literal | { [key: string]: Json } | Json[]; export const jsonSchema: z.ZodType = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) ); type MapInsertColumnToZod< TColumn extends Column, TType extends z.ZodTypeAny > = TColumn["_"]["notNull"] extends false ? z.ZodOptional> : TColumn["_"]["hasDefault"] extends true ? z.ZodOptional : TType; type MapSelectColumnToZod< TColumn extends Column, TType extends z.ZodTypeAny > = TColumn["_"]["notNull"] extends false ? z.ZodNullable : TType; type MapColumnToZod< TColumn extends Column, TType extends z.ZodTypeAny, TMode extends "insert" | "select" > = TMode extends "insert" ? MapInsertColumnToZod : MapSelectColumnToZod; type MaybeOptional< TColumn extends Column, TType extends z.ZodTypeAny, TMode extends "insert" | "select", TNoOptional extends boolean > = TNoOptional extends true ? TType : MapColumnToZod; type GetZodType = TColumn["_"]["dataType"] extends infer TDataType ? TDataType extends "custom" ? z.ZodAny : TDataType extends "json" ? z.ZodType : TColumn extends { enumValues: [string, ...string[]] } ? Equal extends true ? z.ZodString : z.ZodEnum : TDataType extends "array" ? z.ZodArray< GetZodType["baseColumn"]> > : TDataType extends "bigint" ? z.ZodBigInt : TDataType extends "number" ? z.ZodNumber : TDataType extends "string" ? z.ZodString : TDataType extends "boolean" ? z.ZodBoolean : TDataType extends "date" ? // ? z.ZodDate z.ZodString : z.ZodAny : never; type ValueOrUpdater = T | ((arg: TUpdaterArg) => T); type UnwrapValueOrUpdater = T extends ValueOrUpdater ? U : never; export type Refine = { [K in keyof TTable["_"]["columns"]]?: ValueOrUpdater< z.ZodTypeAny, TMode extends "select" ? BuildSelectSchema : BuildInsertSchema >; }; export type BuildInsertSchema< TTable extends Table, TRefine extends Refine | {}, TNoOptional extends boolean = false > = TTable["_"]["columns"] extends infer TColumns extends Record< string, Column > ? { [K in keyof TColumns & string]: MaybeOptional< TColumns[K], K extends keyof TRefine ? Assume, z.ZodTypeAny> : GetZodType, "insert", TNoOptional >; } : never; export type BuildSelectSchema< TTable extends Table, TRefine extends Refine, TNoOptional extends boolean = false > = Simplify<{ [K in keyof TTable["_"]["columns"]]: MaybeOptional< TTable["_"]["columns"][K], K extends keyof TRefine ? Assume, z.ZodTypeAny> : GetZodType, "select", TNoOptional >; }>; export function createInsertSchema< TTable extends Table, TRefine extends Refine = Refine >( table: TTable, /** * @param refine Refine schema fields */ refine?: { [K in keyof TRefine]: K extends keyof TTable["_"]["columns"] ? TRefine[K] : DrizzleTypeError<`Column '${K & string}' does not exist in table '${TTable["_"]["name"]}'`>; } ): z.ZodObject< BuildInsertSchema< TTable, Equal> extends true ? {} : TRefine > > { const columns = getTableColumns(table); const columnEntries = Object.entries(columns); let schemaEntries = Object.fromEntries( columnEntries.map(([name, column]) => { return [name, mapColumnToSchema(column)]; }) ); if (refine) { schemaEntries = Object.assign( schemaEntries, Object.fromEntries( Object.entries(refine).map(([name, refineColumn]) => { return [ name, typeof refineColumn === "function" ? refineColumn( schemaEntries as BuildInsertSchema ) : refineColumn, ]; }) ) ); } for (const [name, column] of columnEntries) { if (!column.notNull) { schemaEntries[name] = schemaEntries[name]!.nullable().optional(); } else if (column.hasDefault) { schemaEntries[name] = schemaEntries[name]!.optional(); } } return z.object(schemaEntries) as any; } export function createSelectSchema< TTable extends Table, TRefine extends Refine = Refine >( table: TTable, /** * @param refine Refine schema fields */ refine?: { [K in keyof TRefine]: K extends keyof TTable["_"]["columns"] ? TRefine[K] : DrizzleTypeError<`Column '${K & string}' does not exist in table '${TTable["_"]["name"]}'`>; } ): z.ZodObject< BuildSelectSchema< TTable, Equal> extends true ? {} : TRefine > > { const columns = getTableColumns(table); const columnEntries = Object.entries(columns); let schemaEntries = Object.fromEntries( columnEntries.map(([name, column]) => { return [name, mapColumnToSchema(column)]; }) ); if (refine) { schemaEntries = Object.assign( schemaEntries, Object.fromEntries( Object.entries(refine).map(([name, refineColumn]) => { return [ name, typeof refineColumn === "function" ? refineColumn( schemaEntries as BuildSelectSchema ) : refineColumn, ]; }) ) ); } for (const [name, column] of columnEntries) { if (!column.notNull) { schemaEntries[name] = schemaEntries[name]!.nullable(); } } return z.object(schemaEntries) as any; } function isWithEnum( column: Column ): column is typeof column & { enumValues: [string, ...string[]] } { return ( "enumValues" in column && Array.isArray(column.enumValues) && column.enumValues.length > 0 ); } function mapColumnToSchema(column: Column): z.ZodTypeAny { let type: z.ZodTypeAny | undefined; if (isWithEnum(column)) { type = column.enumValues.length ? z.enum(column.enumValues) : z.string(); } if (!type) { if (is(column, PgUUID)) { type = z.string().uuid(); } else if (column.dataType === "custom") { type = z.any(); } else if (column.dataType === "json") { type = jsonSchema; } else if (column.dataType === "array") { type = z.array( mapColumnToSchema((column as PgArray).baseColumn) ); } else if (column.dataType === "number") { type = z.number(); } else if (column.dataType === "bigint") { type = z.bigint(); } else if (column.dataType === "boolean") { type = z.boolean(); } else if (column.dataType === "date") { // type = z.date(); type = z.string(); } else if (column.dataType === "string") { let sType = z.string(); if ( (is(column, PgChar) || is(column, PgVarchar) || is(column, MySqlVarChar) || is(column, MySqlVarBinary) || is(column, MySqlChar) || is(column, SQLiteText)) && typeof column.length === "number" ) { sType = sType.max(column.length); } type = sType; } } if (!type) { type = z.any(); } return type; }