question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Proposal: Alternative API for mutations and subscriptions

See original GitHub issue

The 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:closed
  • Created 3 years ago
  • Reactions:8
  • Comments:13 (12 by maintainers)

github_iconTop GitHub Comments

1reaction
jkimbocommented, Jul 13, 2020

Thanks for the feedback @syrusakbary

  1. The current proposed design would not fit very well with custom mutation types (types that are only useful for that mutation). For example, for createUser mutation you might want to return more than just the User (but a MutateUser).

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:

from graphene import mutation, ObjectType, User, Boolean

class MutateUser(ObjectType):
	success = Boolean()
	user = Field(User)

@mutate(MutateUser, required=True, arguments={"name": String(required=True)})
def update_user(root, info, name):
    # Update the user...
    return MutateUser(user=User(name=name))
  1. The schema = Schema(query=Query, mutations=[update_user]) doesn’t fit really the GraphQL internals. Similarly to Query, Mutation and Subscription are root types. That mean that there can be only one root type.

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 subclasses ObjectType) 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:

type Mutation {
  updateUser(name: String): User
}
  1. The arguments field while being valid, it goes a different way to provide arguments (see, for example, how we do it with graphene.Field).

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 in graphene.Field). As an implementation detail this decorator would just return a Field anyway so both ways would work. IMO it’s clearer to use the arguments kwarg then having to try and explain to people that the extra kwargs which aren’t name, description etc. get mapped to field arguments but I don’t have a strong opinion on that.

At the same time, the syntax is moving a bit apart from how Graphene types are being created. Which introduces another layer to learn. What was exposed could also be written as:

class Mutation(graphene.ObjectType):
    update_user = graphene.Field(User, required=True, name=graphene.String())
    
    def resolve_upate_user(root, info, name)
       # Update the user...
        return User(name=name)

I completely understand that this is “more to type” than the exposed example, but is also simpler regarding the current structure.

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.

Perhaps there is a common solution that can be found solving this problems, but I think it have to be thought as a whole (how we can simplify schema creation, field creation, …) rather than the smaller parts.

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:

class Query(ObjectType):
    @field(User, id=ID(required=True))
	def get_user_by_id(root, info, id):
        return User(id=id)

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)

1reaction
jkimbocommented, Jul 12, 2020

@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:

class UpdateUser(Mutation):
	def mutate(root, info, id: ID, name: str) -> User:
		user = DjangoUser.objects.get(pk=id)
		user.name = name
		user.save()
		return user # <-- this would break the typechecker because it's not a `User` type

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Released GraphQL API v0.7, with support for mutations, and ...
Version 0.7 of the GraphQL API for WordPress, supporting mutations, and nested mutations, has been released!. Mutations are awesome!
Read more >
GraphQL vs. REST: A GraphQL Tutorial
GraphQL is a new way to fetch APIs, an alternative to REST that is more efficient, simpler, and faster to implement.
Read more >
appSubscriptionCreate
Create a subscription for an app on both a recurring pricing plan and usage pricing plan.Create a subscription for an app on an...
Read more >
Subscriptions and Live Queries - Real Time with GraphQL
Subscriptions are the go-to solution for adding real-time capabilities to a GraphQL-powered application. At the same time the term GraphQL ...
Read more >
GraphQL queries, mutations & subscriptions
No matter if you want to learn how to build your first GraphQL API or looking for more advanced information about GraphQL Mutations,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found