chrishoermann / zod-prisma-types

Generator creates zod types for your prisma models with advanced validation
Other
578 stars 43 forks source link

[Feature Request] Update `@prisma/client` to use the generated types #223

Open Stadly opened 6 months ago

Stadly commented 6 months ago

Have you considered updating @prisma/client with the generated types? Then we would automatically get correctly typed models out of the database and get type safety when inserting in the database. We would not get automatic checks that inserted objects adhere to the generated Zod schema, but we would get automatic checks that they adhere to the TypeScript type corresponding to the Zod schema.

For the normal validators, I don't think it would make any difference, but for custom validators I think it would be really useful.

Example

Consider the following schema.prisma, schema generated by zod-prisma-types, and generated @prisma/client:

// schema.prisma
model App {
  id       String @id @default(cuid())
  i18nName Json   /// @zod.custom.use(z.record(z.string(), z.string()))
  type     String /// @zod.custom.use(z.union([z.literal("foo"), z.literal("bar")]))
  details  Json?  /// @zod.custom.use(z.object({description: z.string().optional(), names: z.string().array()}))
}
// prisma/generated/zod/index.ts
export const AppSchema = z.object({
  id: z.string().cuid(),
  i18nName: z.record(z.string(), z.string()),
  type: z.union([z.literal("foo"), z.literal("bar")]),
  details: z.object({description: z.string().optional(), names: z.string().array()}).nullable(),
})

export type App = z.infer<typeof AppSchema>
// node_modules/.prisma/client/index.d.ts
export namespace Prisma {
  export type $AppPayload<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
    name: "App"
    objects: {}
    scalars: $Extensions.GetPayloadResult<{
      id: string
      /**
       * @zod.custom.use(z.record(z.string(), z.string()))
       */
      i18nName: Prisma.JsonValue
      /**
       * @zod.custom.use(z.union([z.literal("foo"), z.literal("bar")]))
       */
      type: string
      /**
       * @zod.custom.use(z.object({description: z.string().optional(), names: z.string().array()}))
       */
      details: Prisma.JsonValue | null
    }, ExtArgs["result"]["app"]>
    composites: {}
  }
}

I envision that node_modules/.prisma/client/index.d.ts was changed to something like this:

// node_modules/.prisma/client/index.d.ts
import { App } from "../../../prisma/generated/zod"
export namespace Prisma {
  export type $AppPayload<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
    name: "App"
    objects: {}
    scalars: $Extensions.GetPayloadResult<{
      id: App["id"]
      /**
       * @zod.custom.use(z.record(z.string(), z.string()))
       */
      i18nName: App["i18nName"]
      /**
       * @zod.custom.use(z.union([z.literal("foo"), z.literal("bar")]))
       */
      type: App["type"]
      /**
       * @zod.custom.use(z.object({description: z.string().optional(), names: z.string().array()}))
       */
      details: App["details"]
    }, ExtArgs["result"]["app"]>
    composites: {}
  }
}

This would give us type-safety out of the box:

// TypeScript checks that `newApp` is of type `App` (from `prisma/generated/zod/index.ts`)
await prisma.app.create({data: newApp})

// `myApp` is of type `App` (from `prisma/generated/zod/index.ts`)
const myApp = await prisma.app.findUnique({where: {id: myUuid}})

Prisma Json Types Generator uses this approach for typing JSON fields, so that might be a good source for implementation inspiration.

coindegen commented 6 months ago

I agree this would be useful.

The discrepancy between the Prisma types and the zod-prisma-types is proving to be annoying. I am also using custom validators, specifically to handle date serialization like this:

model MintData {
  id                  String      @id @default(cuid())
  ...
  date_last_sale      DateTime? ///@zod.custom.use(z.coerce.string().nullish())

  created_at DateTime @default(now()) ///@zod.custom.use(z.coerce.string())
  updated_at DateTime @updatedAt ///@zod.custom.use(z.coerce.string())
}

This raises all kinds of "cannot assign Date to string" errors everywhere I run prisma queries, because Prisma is still expecting Date objects.

As a result, I end up having to override the inferred types everywhere by using as unknown as ActualType:

import {prisma} from "#app/utils/db.server.ts"
import type { Collection, MintData } from "#types/generated/index.ts";

export type IFetchedCollection = Collection & { mint_data: MintData };

export async function fetchCollection(address: string) {
  const collection = (await prisma.collection.findFirst({
    where: { address: address },
    include: { mint_data: true },
  })) as unknown as IFetchedCollection;

  if (collectionBySlug) {
    return collectionBySlug;
  }

  throw new Error("Collection not found");
}

I also have a question in this regard. Do I have to create this custom include intersection type (IFetchedCollection), or is that already exported somewhere in my generated types?

I know zod-prisma-types exports my "Collection" type like this:

export type Collection = z.infer<typeof CollectionSchema>

However, is there also a "Collection" type with all possible "joins"? In other words, is there a type consisting of my model intersecting all its potential includes?

chrishoermann commented 6 months ago

@Stadly thanks for the suggestion, this seems to be a nice extension of the client. I need to look into it and see how much time this would take to implement and how complex it would get.

@coindegen you can try to create additional model types with the options createOptionalDefaultValuesTypes, createRelationValuesTypes, createPartialTypes and see if these generated schemas/types will suit your needs.

coindegen commented 6 months ago

@coindegen you can try to create additional model types with the options createOptionalDefaultValuesTypes, createRelationValuesTypes, createPartialTypes and see if these generated schemas/types will suit your needs.

that's really helpful, thanks. Just what I needed, there's so many options it's hard to keep track :)