chrishoermann / zod-prisma-types

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

SchemaWithRelation bad return type #102

Closed gregg-cbs closed 1 year ago

gregg-cbs commented 1 year ago

When working with relations it returns a z.ZodType - this does not give us access to other functionality such a .pick and .merge which do not exist on z.ZodType. The easiest would be to not return a type or somehow extend so we have functionality like pick and merge .

Currently: export const productsWithRelationsSchema: z.ZodType<productsWithRelations> =

Preferable: export const productsWithRelationsSchema =

Relates and solves type export on this too: https://github.com/chrishoermann/zod-prisma-types/issues/101

chrishoermann commented 1 year ago

@gregg-cbs an explizit type annotation with z.ZodType<...> is needed to allow recursive schemas as stated in the zod docs.

I tried using the satisfied operator but this does not satisfy the need of a type annotation on recursive types. One can argue that recursive types, especially on the model schemas, are kind of rare but nontheless we need to allow them.

Some time ago I thougt about using some logic to generally use the satisfied operator and only use a type annotation on recursive types but I never looked into this option in depth. Although this can be implemented relatively easy on the model types, I think it can lead to confusion why the .pick, .omit, etc- methods are available on some schemas but not on the others (especially for people who are not that experienced with zod).

To conclude: for normal types this can be implemented but for recursive types the .pick, .omit, etc. methods will never be possible 😞

cbs-l commented 1 year ago

Hi @chrishoermann, on this note what would be the best workaround for being able to then go ahead and do things such as merging and picking? In relation to what @gregg-cbs was getting at we basically wanted a property inside of the relation schema, however because it was part of a recursive type and we can't use pick, we essentially cannot get access to this now.

Would be good to get your insight on that end. I am not 100% sure on what makes the relation's recursive. However when we constructed the type ourselves we are able to get the outcome we need. In the below example instead of doing the original

Original Generated

export const productsWithRelationsSchema: z.ZodType<productsWithRelations> = productsSchema.merge(z.object({
  brands: z.lazy(() => BrandSchema).array(),
  attributes: z.lazy(() => AttributeSchema).array(),
  categories: z.lazy(() => CategorySchema).array(),
  images: z.lazy(() => ImageSchema).array(),
}))

Custom note I have just put the words generated here to make it easy to read

export const ProductSchema = GeneratedProductSchema
  .merge(ObjectIdSchema)
  .merge(z.object({
    brands: BrandSchema.array(),
    attributes: GeneratedAttributeSchema.array(),
    categories: GeneratedCategorySchema.array(),
    images: GeneratedImageSchema.array()
  }));
chrishoermann commented 1 year ago

@factordog In the case of picking relation fields I would go for somthing like this:

export const UserSchema = z.object({
  id: z.number().int(),
  email: z.string(),
  name: z.string(),
});

// ignore this schema for now
export const UserWithRelationsSchema: z.ZodType<UserWithRelations> =
  UserSchema.merge(
    z.object({
      Post: z.lazy(() => PostWithRelationsSchema).array(),
      Profile: z.lazy(() => ProfileWithRelationsSchema).nullable(),
    }),
  );

// Pick some keys from UserSchema
// and add the needed relation keys via merge manually

export const PickedUserSchema = UserSchema.pick({
  id: true,
  email: true,
}).merge(
  z.object({
    // should also work without lazy
    Post: z.lazy(() => PostWithRelationsSchema).array(),
  }),
);

export type PickedUser = z.infer<typeof PickedUserSchema>;

// picked user type would then look like this:
type PickedUser = {
    id: number;
    email: string;
    Post: PostWithRelations[];
}

I think this method is quite flexible and this was also my original intention how to solve such a problem.

Regarding recursice types: In case of the model types they are only necessary when a model references itself (in inputTypes they are literally everywhere):

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String
  Post    Post[]
  Profile Profile?

  // recursive user
  parent   User? @relation("UserToUser", fields: [parentId], references: [id])
  parentId Int?

  children User[] @relation("UserToUser")
}

this model would generate a relation type like this, that needs the type annotation:

export const UserWithRelationsSchema: z.ZodType<UserWithRelations> = UserSchema.merge(z.object({
  Post: z.lazy(() => PostWithRelationsSchema).array(),
  Profile: z.lazy(() => ProfileWithRelationsSchema).nullable(),
  parent: z.lazy(() => UserWithRelationsSchema).nullable(),
  children: z.lazy(() => UserWithRelationsSchema).array(),
}))

Otherwise typescript would throw the following error:

UserWithRelationsSchema' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

So to be sure that in case of recursion no type errors are thrown, I chose to always use a type annotation .

cbs-l commented 1 year ago

Right so essentially build it up our end as I have instead of using the generated?

And cool I understand now the choice for the recursive type. Thanks for the clarification

chrishoermann commented 1 year ago

@factordog it may be a bit more work to build the schemas yourself but in the end it is more flexible because you could also use your own (picked) types as relations. And to implement a satisfying way via genrator config options and/or rich-comments could easily get out of hand timewise. 😃

cbs-l commented 1 year ago

That's fine, just wanted to ensure we were not missing something critical. Appreciate the clarification.

chrishoermann commented 1 year ago

@chrishoermann you're welcome. I'll close the issue since it has been adressed.