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

Partial GraphQL types #688

Closed
Carlovan opened this issue Mar 22, 2020 · 12 comments
Closed

Partial GraphQL types #688

Carlovan opened this issue Mar 22, 2020 · 12 comments

Comments

@Carlovan
Copy link

I'm submitting a...


[ ] Regression 
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

Now it's not possible to create a "partial" type from a GraphQL type; here with "partial type" I mean a type with all fields nullable.

Expected behavior

I'd like to be able to create a partial version of an existing GraphQL type, that is a copy of that type with the only difference that all fields are nullable.

What is the motivation / use case for changing the behavior?

In my API I need to handle various complex entities with CRUD operations; specifically the input Create and Update operations are the same but for Create the fields are all mandatory and for Update they are all nullable (I may want to update only some of them).
I managed to get this in type-graphql by accessing the MetadataStorage and actually copying all the FieldMetadata with the nullable option set, but with the new LazyMetadataStorage this is not possible anymore.

Environment


Nest GraphQL version: 7.0.12

@kamilmysliwiec
Copy link
Member

I managed to get this in type-graphql by accessing the MetadataStorage and actually copying all the FieldMetadata with the nullable option set

You've basically used a non-public API to accomplish this. I suppose there's a way to achieve something similar, but using a different technique (without accessing a private API). I'll think about how we can make it more straightforward for everyone.

@Carlovan
Copy link
Author

Yes I did because I had no alternatives. Maybe this could be accomplished using directives?

@doug-martin
Copy link

@Carlovan I had the same issue, I had to do the following to get the fields loaded.

 getGraphqlFieldsForType<T>(objType: Class<T>): PropertyMetadata[] | undefined {
    LazyMetadataStorage.load([objType]);
    TypeMetadataStorage.compile();
    let graphqlObjType = this.getGraphqlObjectMetadata(objType);
    if (!graphqlObjType) {
      graphqlObjType = TypeMetadataStorage.getInputTypesMetadata().find(o => o.target === objType);
    }
    return graphqlObjType?.properties;
  }
export function PartialType<T>(TClass: Class<T>): Class<Partial<T>> {
  const graphQLFields = getMetadataStorage().getGraphqlFieldsForType(TClass);
  if (!(graphQLFields && graphQLFields.length)) {
    throw new Error(`Unable to find fields for graphql type ${TClass.name}`);
  }
  @ObjectType({ isAbstract: true })
  abstract class PartialObjectType {}
  graphQLFields.forEach(f => Field(f.typeFn, { ...f.options, nullable: true })(PartialObjectType.prototype, f.name));
  return PartialObjectType as Class<Partial<T>>;
}

@kamilmysliwiec
Copy link
Member

PartialType function has been added in 7.1.0 release.

We have also added OmitType and PickType functions which can mimic the behavior of built-in TypeScript types (respectively Omit and Pick).

Example:

@InputType()
class CreateUserInput {
  @Field()
  email: string;

  @Field()
  password: string;

  @Field()
  firstName: string
}

and to create the UpdateUserInput class based on the CreateUserInput, but with all properties marked as optional, use the following code:

@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {}

If you want to exclude, let's say, the email property (because, for instance, email has to be unique and cannot be modified), compose OmitType and PartialType functions, as follows:

@InputType()
export class UpdateUserInput extends PartialType(OmitType(CreateUserInput, ['email'])) {}

@doug-martin
Copy link

@kamilmysliwiec thanks for the quick turn around on this. Unfortunately this doesnt work for my use case :(

In my use case someone can provide a ObjectType and I need to create a partial InputType from it. For a little more detail when a user provides a base DTO for CRUD operations if they do not provide a Create or Update DTO, I just create a partial InputType from the original DTO.

Here is what I had.

import { Class } from '@nestjs-query/core';
import { Field, ObjectType, InputType } from '@nestjs/graphql';
import { getMetadataStorage } from '../metadata';

export function PartialType<T>(TClass: Class<T>): Class<Partial<T>> {
  const graphQLFields = getMetadataStorage().getGraphqlFieldsForType(TClass);
  if (!(graphQLFields && graphQLFields.length)) {
    throw new Error(`Unable to find fields for graphql type ${TClass.name}`);
  }
  @ObjectType({ isAbstract: true })
  abstract class PartialObjectType {}
  graphQLFields.forEach(f => Field(f.typeFn, { ...f.options, nullable: true })(PartialObjectType.prototype, f.name));
  return PartialObjectType as Class<Partial<T>>;
}

export function PartialInputType<T>(TClass: Class<T>): Class<Partial<T>> {
 // the getGraphqlFieldsForType uses the code snippet I provided earlier.
  const graphQLFields = getMetadataStorage().getGraphqlFieldsForType(TClass);
  if (!(graphQLFields && graphQLFields.length)) {
    throw new Error(`Unable to find fields for graphql type ${TClass.name}`);
  }
  @InputType({ isAbstract: true })
  class PartialInptType {}
  graphQLFields.forEach(f => Field(f.typeFn, { ...f.options, nullable: true })(PartialInptType.prototype, f.name));
  return PartialInptType as Class<Partial<T>>;
}

I see in your new implementation you preserve the original type decorator, and it find the fields just fine, however its annotated with @ObjectType({isAbstract: true}) and when the schema is generated in graphql-schema-factory.js my InputType is missing the fields.

Thank you again for the work you are putting into this!

@kamilmysliwiec
Copy link
Member

In my use case someone can provide a ObjectType and I need to create a partial InputType from it.

In 7.1.2, all utility functions (PartialType, PickType, OmitType) will accept an additional decorator that lets you explicitly pass the decorator factory to be applied to the class.

Example (where User is an ObjectType):

@InputType()
export class CreateUserInput extends PartialType(User, InputType) {}

@doug-martin
Copy link

@kamilmysliwiec that was the exact solution I was thinking :)

I cant thank you enough for fixing this, your awesome!

@amille14
Copy link

@kamilmysliwiec Is there any way to allow partial types to also include fields from extended abstract types (which use type-graphql's @ObjectType({ isAbstract: true }) syntax)?

For example:

@ObjectType()
export class User extends BaseType {
  @Field()
  email: string
  
  @Field()
  name: string
}
@ObjectType({ isAbstract: true })
export abstract class BaseType {
  @Field(type => ID)
  id: string

  @Field(type => GraphQLISODateTime)
  createdAt: Date

  @Field(type => GraphQLISODateTime)
  updatedAt: Date
}

When creating a partial user input type, like so:

@InputType()
export class UserPartialInput extends PartialType(User, InputType) {}

The resulting partial input type will only include fields from User and not from the base BaseType class:

input UserPartialInput {
  email: String
  name: String
}

@kamilmysliwiec
Copy link
Member

@amille14 fixed in 7.3.4 :)

@dotellie
Copy link
Contributor

This might be an issue of its own, but reading this conversation and the documentation on docs.nestjs.com gives two very different impressions regarding the decorator argument. On this thread I read it basically as "you should pass in the decorator you want the type to act as" where as the docs say pretty much the opposite, that you should provide the decorator that is being used. I might be misunderstanding something, but I thought it's worth bringing up nonetheless as this thread helped me solve an issue I couldn't quite grasp from reading the docs.

Quote from the docs that mislead me:

The PartialType() function takes an optional second argument that is a reference to the decorator factory of the type being extended. In the example above, we are extending CreateUserInput which is annotated with the @inputType() decorator. We didn't need to pass InputType as the second argument since it's the default value. If you want to extend a class decorated with @ObjectType, pass ObjectType as the second argument.

@kamilmysliwiec
Copy link
Member

@dotellie docs are wrong. We're tracking the update here nestjs/docs.nestjs.com#1167

@mark600211

This comment has been minimized.

@nestjs nestjs locked and limited conversation to collaborators Mar 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

6 participants