strawberry icon indicating copy to clipboard operation
strawberry copied to clipboard

Proposal: Automatic creation of Input Classes

Open paulo-raca opened this issue 2 years ago • 4 comments

Let me start with an example, a very standard CRUD API:

@strawberry.type
class User
  name: str
  birthday: date
  ...
  @strawberry.field
  async def something(self) -> Something

@strawberry.input
class UserInput
  name: str = UNSET
  birthday: date = UNSET
  ...

@strawberry.type
class Query:
  @strawberry.field
  get_user(id: ID) -> User:
    ...

@strawberry.type
class Mutation:
  @strawberry.mutation
  create_user(user: UserInput) -> User:
    ...
  @strawberry.mutation
  update_user(id: ID, user: UserInput) -> User:
    ..

The types User and UserInput are essentially the same thing all over again -- Except the inputs don't have resolvers Keeping the fields and docs in sync is just boring repetitive work.

Suggestion 1

Would be great if we could reuse the same type declaration in our arguments, and strawberry would just create an input type in the schema under the hood containing only the dataclass fields

@strawberry.type
class Mutation:
  @strawberry.mutation
  create_user(user: User) -> User:
    ...
---
type Mutation {
  createUser(user: UserInput!): User!
}

GraphQL-Kotlin does that

IMHO this is a simple and reasonable approach

Suggestion 2

Another suggestion is to keep the Main/Input types distinct, but generate the Type.Input automatically:

  • The python class for the input would be added as a nested class User.Input
  • dataclass-style fields would be kept by default
  • resolver fields are discarded by default
  • default values would be set to UNSET or removed entirely
  • If the field type if a graphQL SomeTime, it gets replaced by SomeTypeInput
  • These option could be overriden using strawberry.field()
strawberry.field(
  input_options=InputFieldOptions(
    keep=True,  # Keep/discard this field in the Input type,
    type=DataType,  # Modify the field type. e.g., a NestedObject-type field might be replaced by an ID in an input
    default=DataType()  # Uses this as the default value
    default_factory=lambda: DataType()  # Uses this as the factory
    description="Same field, now in an input"
  )
)

This would look like this:

@strawberry.type
class Mutation:
  @strawberry.mutation
  create_user(user: User.Input) -> User:
    ...

paulo-raca avatar Mar 09 '22 03:03 paulo-raca

I also ran into this problem, it's too repetitive, is there any plan to modify this ?

mailgyc avatar Mar 28 '22 13:03 mailgyc

I have a draft implementation of suggestion 2 by adding a @gen_strawberry_input decorator. Hopefully this can be useful to you https://gist.github.com/paulo-raca/b1896a29c5029455b7290d03c4a106af

This is still a draft:

  • I currently plan to store the InputFieldOptions using "typing.Annotated", but I need #1730 to be merged first
  • I expect to get more feedback on this before making a PR

paulo-raca avatar Mar 29 '22 15:03 paulo-raca

In a way this is actually solved with the pydantic plugin, so strange that core library cannot do it

invokermain avatar May 06 '22 21:05 invokermain

I think we might want to do this at some point, but we are still working on some internal refactoring 😊

Also we'd need to make a decision on this would work exactly, especially with regards to UNSET, making fields options and dealing with customisation of the type

patrick91 avatar May 06 '22 22:05 patrick91

https://gist.github.com/paulo-raca/b1896a29c5029455b7290d03c4a106af

Hello! Made several improvements to this code, added support for:

  • Optional
  • List
  • Union
  • Generics and "bad" Generics
  • LazyType

The code is uploaded in the comments.

ArtyomZemlyak avatar Sep 24 '22 07:09 ArtyomZemlyak

I think something that is worth thinking about doing this is that GraphQL APIs aren't really meant to be CRUD APIs, so adding something like this directly to the core of Strawberry might look like promoting creating CRUD APIs and all the problems that it brings.

Code duplication is not necessarily a bad thing if that allows you to customise your API better to your clients use-case

marcoacierno avatar Sep 24 '22 08:09 marcoacierno

Yes, it's true.

But, the cases are different, it is not uncommon that such functionality would be useful to save time. If the developer understands that even in the idealized case, the output and input classes will be the same, then it would be cool to give him the opportunity to generate such an input class.

But, of course, there are big questions to ensure that this is added to the core as the main part of the API. Maybe a single function would be enough (with not full support, certain limitations).

ArtyomZemlyak avatar Sep 24 '22 09:09 ArtyomZemlyak

I've added some extra implementation details between input/outputs here #2369 I basically want the same functionality mentioned in this issue.

XChikuX avatar Jan 18 '23 09:01 XChikuX