Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types transformation utils #453

Open
MichalLytek opened this issue Oct 30, 2019 · 18 comments
Open

Types transformation utils #453

MichalLytek opened this issue Oct 30, 2019 · 18 comments
Assignees
Labels
Discussion 💬 Brainstorm about the idea Enhancement 🆕 New feature or request
Milestone

Comments

@MichalLytek
Copy link
Owner

MichalLytek commented Oct 30, 2019

The upcoming Prisma 2 integration (#217) will generate TS classes with TypeGraphQL decorators based on data model definition of Prisma schema.

But to allow for further customization, TypeGraphQL needs some operators to transform the generated classes into the user one. Mainly to hide some fields from database like user password, so we need a way to omit some fields of the base class or pick only some selected fields.

Proposed API have two approaches to apply the transformation:

  • pick/omit fields from emitted GraphQL schema using decorator option
@ObjectType({ pick: ["firstname", "lastname"] })
export class User extends BaseUser {
  // ...
}

@ObjectType({ omit: ["password", "salary"] })
export class User extends BaseUser {
  // ...
}

This approach is for use cases where you still want to have access to the hidden fields in other field resolvers, like hidding array or rates but exposing average rating by the field resolver.

  • pick/omit fields both from emitted GraphQL schema and TS type using class wrapper
@ObjectType()
export class User extends Pick(BaseUser, ["firstname", "lastname"]) {
  // ...
}

@ObjectType()
export class User extends Omit(BaseUser, ["password", "salary"]) {
  // ...
}

This approach is better when you want to create a derived type, like a subset of some input type, so you won't accidentally use the not existing field.

Initial version might not support inheritance or have a prototype methods leaks, because it's not needed by the Prisma integration. In the next release cycle I will try to make it work with broader range of use cases.

Later, more types transformation utils will be implemented, like Partial(Foo) for making fields optional, Required(Foo) for making fields non-nullable.

If possible, maybe I will try to add even a mapping util that will map the keys of one type to new values in a new type. For example if you want to generate a sorting input (with field types only ASC/DESC) based on an object type which fields are representing database columns 😉

@amille14
Copy link

Later, more types transformation utils will be implemented, like Partial(Foo) for making fields optional, Required(Foo) for making fields non-nullable.

For anyone else looking for this functionality now, I was able to hack together this partial type factory function by directly modifying the metadata storage object. Not ideal as this is a private api but it seems to do the trick so far. It just copies fields of the given type and creates a new type with the same fields, only all nullable.

import { ClassType } from 'type-graphql'

// NOTE: This only works for object types, not input types

export function PartialType<E>(EntityClass: ClassType<E>): any {
  const metadata = (global as any).TypeGraphQLMetadataStorage
  class PartialClass {}
  const name = `${EntityClass.name}Partial`
  Object.defineProperty(PartialClass, 'name', { value: name })

  // Create a new object type
  const newObjectType = {
    name,
    target: PartialClass,
    description: `Partial type for ${EntityClass.name}`
  }
  metadata.objectTypes.push(newObjectType)

  // Copy relevant fields and create a nullable version on the new type
  const fields = metadata.fields.filter(
    f => f.target === EntityClass || EntityClass.prototype instanceof f.target
  )
  fields.forEach(field => {
    const newField = {
      ...field,
      typeOptions: { ...field.typeOptions, nullable: true },
      target: PartialClass
    }
    metadata.fields.push(newField)
  })

  return PartialClass
}

Use it like so:

@ObjectType()
export class User {
  @Field()
  name: string

  @Field()
  email: string

  // ...
}

export const UserPartial = PartialType(User)

@MichalLytek
Copy link
Owner Author

And for anyone else looking for pick/omit functionality now, I would recommend using mixin classes to compose bigger types, rather than picking/omitting from bigger types to create a small ones:
https://github.com/MichalLytek/type-graphql/tree/master/examples/mixin-classes

@andreialecu
Copy link

Could something like @Field({ nullable: { objType: false, inputType: true } }) be considered instead, in order to remove duplication between ObjectTypes and InputTypes and allow specifying different behavior?

Or create a separate decorator for @InputField(...) in addition of @Field(..)?

Seems like it would be more powerful and concise than a Partial(...) mixin.

@MichalLytek
Copy link
Owner Author

MichalLytek commented Dec 18, 2019

I think that nullable is not enough - you don't want to accept id field in "edit" mutation, so you need to pick/omit the fields.

Also, this will give you false positive if you want to use the same class as the type in the mutation, so you may thing that the value will exist and in runtime it will be nullable.

That's why it's better to describe it as Input = Partial(Pick(Output, field)) which will work both for type signature, as well as for the schema part.

@andreialecu
Copy link

Alright, makes sense. There should be a way to make certain fields required as well, a combination of both Partial and also Required somehow. Not sure how that would look though.

@MichalLytek
Copy link
Owner Author

In that case I think that the mixins pattern makes more sense than trying to combine Partial and Required - it would be much more maintainable I think 😉

@andreialecu
Copy link

andreialecu commented Dec 18, 2019

Here's a Partial mixin based on @amille14 's code above, one that works for both InputType and ObjectType.

import { ClassType, InputType, ObjectType } from 'type-graphql';

export default function PartialType<TClassType extends ClassType>(
  BaseClass: TClassType,
) {
  const metadata = (global as any).TypeGraphQLMetadataStorage;

  @ObjectType({ isAbstract: true })
  @InputType({ isAbstract: true })
  class PartialClass extends BaseClass {}

  // Copy relevant fields and create a nullable version on the new type
  const fields = metadata.fields.filter(
    f => f.target === BaseClass || BaseClass.prototype instanceof f.target,
  );
  fields.forEach(field => {
    const newField = {
      ...field,
      typeOptions: { ...field.typeOptions, nullable: true },
      target: PartialClass,
    };
    metadata.fields.push(newField);
  });

  return PartialClass;
}

use like:

@InputType()
export class SomethingInput extends PartialType(
  SomethingModelBase,
) {}

@MichalLytek
Copy link
Owner Author

Be aware that the internal metadata storage might be changed without any notice between releases, so it's not recommended to do that kind of hacks.

@andreialecu
Copy link

@MichalLytek I'm personally well aware. Hopefully you can include such functionality in the core soon, so we can get rid of the workaround. This is one of the main issues users would run into as soon as their API evolves into something CRUD-like.

@andreialecu
Copy link

andreialecu commented Jul 27, 2020

I don't use this any more but you should be able to change the return to return PartialClass as Partial<TClassType>

@AmrAnwar
Copy link

AmrAnwar commented Jul 27, 2020

I don't use this any more but you should be able to change the return to return PartialClass as Partial<TClassType>

@andreialecu
That won't work in the extends,
@inputType()
export class SomethingInput extends PartialType(SomethingModelBase){}
Type 'Partial<typeof SomethingModelBase>' is not a constructor function type.ts(2507)

@MichalLytek
Copy link
Owner Author

try this monster 😄
return PartialClass as ClassType<Partial<InstanceType<TClassType>>>

@AmrAnwar
Copy link

as ClassType<Partial<InstanceType>>

Oh that's worked :D thanks, I've already added ClassType<Partial<>> but I got another error when adding another types that have the same fields types but this line worked :D

@rtnolan
Copy link

rtnolan commented Oct 19, 2020

Curious if there's been any progress on this? Also curious how I'd get involved in helping out with this if not, would be super useful for something I'm doing at the moment!

@Pablo1107
Copy link

It would be very useful to avoid code duplication if this is implemented in the library itself.

@theseyi
Copy link
Contributor

theseyi commented May 29, 2021

I don't use this any more but you should be able to change the return to return PartialClass as Partial<TClassType>

@andreialecu

Curious why you no longer use this PartialClass approach, I was going to create an Input type class factory based on it, wonder if there were pitfalls that you ran into later with TypeGraphQL? The other recommended approach of mixins/traits is safer, but gets pretty tedious and probably a maintenance hassle once you have multiple fields that are nested input types

@itpropro
Copy link

Just citing @ChrisLahaye from #1295 (comment):

Hi! We published our transformation utils to the type-graphql-utils package. It exports Pick, Required, Partial, and Omit. It doesn't have any specific code for class-validator like this PR, so I am not sure whether that works as we don't use it.

@thongxuan
Copy link

What do you guys think about support Partial with "deep" options so that the partial is applied into nested classes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion 💬 Brainstorm about the idea Enhancement 🆕 New feature or request
Projects
None yet
Development

No branches or pull requests

9 participants