Proposal: Alternative API for mutations and subscriptions
See original GitHub issueThe problem
Currently the mutation api is quite clunky and not very intuitive: https://docs.graphene-python.org/en/latest/types/mutations/
Often users get confused about the mutate
function being a static method and that you need to define a separate Mutation
class that extends ObjectType
with each mutation in it. Also the Output
field is not a very intuitive way of defining the return value of the Mutation
.
See: https://github.com/graphql-python/graphene/issues/1163 https://github.com/graphql-python/graphene/issues/810 https://github.com/graphql-python/graphene-django/issues/759 and others
Subscriptions in Graphene are currently a very new and experimental feature and the current API has some downsides as outlined here: https://github.com/graphql-python/graphene/pull/1107#issuecomment-657194024
The solution
Implement a new decorator based API for both mutations and subscriptions and expose a new mutations
and subscriptions
argument to Schema
that accepts a list of mutation and subscription functions. This has the benefit of not only being simpler but also making it super clear that the mutation/subscribe function is a static method.
Examples:
Mutations:
from graphene import mutation, String, Schema
from .definitions import User, Query
@mutation(User, required=True, arguments={"name": String(required=True)})
def update_user(root, info, name):
# Update the user...
return User(name=name)
schema = Schema(query=Query, mutations=[update_user])
Equivalent functionality with the current API
from graphene import Mutation, String, Schema, ObjectType, Field
class UpdateUser(Mutation):
class Arguments:
name = String(required=True)
Output = User
def mutate(root, info, name):
# Update the user...
return User(name=name)
class MyMutation(ObjectType):
update_user = UpdateUser.Field(required=True)
schema = Schema(query=Query, mutation=MyMutation)
Subscriptions:
from graphene import subscription, Int, Schema
from .definitions import Query
@subscription(Int, required=True)
async def count_to_ten(root, info):
count = 0
while count < 10:
count += 1
yield count
schema = Schema(query=Query, subscriptions=[count_to_ten])
Drawbacks
Using decorators like this is quite different to the current API so it would require a lot of documentation and education to get users to migrate to it (though the 2 APIs could coexist quite easily). Since there should only be 1 way of defining mutations we should try and make it as easy as possible for people to migrate.
Alternatives
We could double down on the current Mutation API and improve the documentation and add more validation to help users use it correctly. The Subscription API could also follow the Mutation API so that they are consistent.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:8
- Comments:13 (12 by maintainers)
Top GitHub Comments
Thanks for the feedback @syrusakbary
I don’t think that is an issue because the first argument to the decorator is the return type so it could be replaced with a
MutateUser
type. For example:I agree that this API does start diverging from the underlying root types but I would argue that Graphene is an opinionated way of writing GraphQL servers and so I don’t see an issue with providing some abstractions that make it easier to build a schema. In my opinion it maps quite cleanly to the underlying root types and the alternative (of having to define a
Mutation
type that subclassesObjectType
) is potentially quite confusing for developers who don’t have good understanding of the GraphQL type system.Btw to avoid confusion the argument
mutations=[update_user]
will be converted directly to:The
arguments
field is probably the part of this proposal I am most unsure about. I would be happy with just using the current way of passing extra kwargs to the decorator and they would be mapped to the field arguments (like how it’s done ingraphene.Field
). As an implementation detail this decorator would just return aField
anyway so both ways would work. IMO it’s clearer to use thearguments
kwarg then having to try and explain to people that the extra kwargs which aren’tname
,description
etc. get mapped to field arguments but I don’t have a strong opinion on that.As I mentioned above I agree this syntax is moving away from how Graphene types are created but I see that as a good thing because I think Graphene should be an opinionated way of writing a GraphQL schema and so we should optimise for developer ergonomics rather than just sticking to how the underlying types work. Btw in your example there is a typo in
resolve_upate_user
which wouldn’t happen in the proposed API and more crucially it wouldn’t even raise an error, it would just silently not work.I didn’t want to include it as part of this proposal (to keep things focused and because I think the mutation api needs the most change) but this pattern could be extended to include normal query resolvers by providing a decorator for fields like so:
This API keeps the resolver and the field together in the code which I think is much cleaner. I’ve been using this API at my current work and it works nicely. (credit to @dvndrsn for coming up with this API here: https://github.com/graphql-python/graphene/issues/898#issuecomment-493805109)
@ekampf we could use type annotations to define the return type of a mutation (and you could even use it for the arguments as well) however the main issue I have with it is that it wouldn’t work if you returned something other than an object type from the resolver. For example you couldn’t return a Django model instance because it would break the typechecker:
I’m not sure how to get around this issue other than maybe implementing something with protocols but I don’t know the typing module well enough to do that.