Mutations

So far all examples in this documentation have dealt with Query type and reading the data. What about creating, updating or deleting?

Enter the Mutation type, Query’s sibling that GraphQL servers use to implement functions that change application state.

Note

Because there is no restriction on what can be done inside resolvers, technically there’s nothing stopping somebody from making Query fields act as mutations, taking inputs and executing state-changing logic.

In practice such queries break the contract with client libraries such as Apollo-Client that do client-side caching and state management, resulting in non-responsive controls or inaccurate information being displayed in the UI as the library displays cached data before redrawing it to display an actual response from the GraphQL.

Defining mutations

Lets define the basic schema that implements a simple authentication mechanism allowing the client to see if they are authenticated, and to log in and log out:

type_def = """
    type Query {
        isAuthenticated: Boolean!
    }

    type Mutation {
        login(username: String!, password: String!): Boolean!
        logout: Boolean!
    }
"""

In this example we have the following elements:

Query type with single field: boolean for checking if we are authenticated or not. It may appear superficial for the sake of this example, but Ariadne requires that your GraphQL API always defines Query type.

Mutation type with two mutations: login mutation that requires username and password strings and returns bool with status, and logout that takes no arguments and just returns status.

For the sake of simplicity, our mutations return bools, but really there is no such restriction. You can have a resolver that returns status code, an updated object, or an error message:

type_def = """
    type Mutation {
        login(username: String!, password: String!) {
            status: String!
            error: Error
            user: User
        }
    }
"""

Writing resolvers

Mutation resolvers are no different to resolvers used by other types. They are functions that take parent and info arguments, as well as any mutation’s arguments as keyword arguments. They then return data that should be sent to client as a query result:

def resolve_login(_, info, username, password):
    request = info.context["request"]
    user = auth.authenticate(username, password)
    if user:
        auth.login(request, user)
        return True
    return False


def resolve_logout(_, info):
    request = info.context["request"]
    if request.user.is_authenticated:
        auth.logout(request)
        return True
    return False

Because Mutation is a GraphQL type like others, you can map resolvers to mutations using dict:

resolvers {
    "Mutation": {
        "login": resolve_login,
        "logout": resolve_logout,
    }
}

Inputs

Let’s consider the following type:

type_def = """
    type Discussion {
        category: Category!
        poster: User
        postedOn: Date!
        title: String!
        isAnnouncement: Boolean!
        isClosed: Boolean!
    }
"""

Imagine a mutation for creating Discussion that takes category, poster, title, announcement and closed states as inputs, and creates a new Discussion in the database. Looking at the previous example, we may want to define it like this:

type_def = """
    type Mutation {
        createDiscussion(category: ID!, title: String!, isAnnouncement: Boolean, isClosed: Boolean) {
            status: Boolean!
            error: Error
            discussion: Discussion
        }
    }
"""

Our mutation takes only four arguments, but it is already too unwieldy to work with. Imagine adding another one or two arguments to it in future - its going to explode!

GraphQL provides a better way for solving this problem: input allows us to move arguments into a dedicated type:

type_def = """
    type Mutation {
        createDiscussion(input: DiscussionInput!) {
            status: Boolean!
            error: Error
            discussion: Discussion
        }
    }

    input DiscussionInput {
        category: ID!
        title: String!,
        isAnnouncement: Boolean
        isClosed: Boolean
    }
"""

Now when client wants to create a new discussion, they need to provide an input object that matches the DiscussionInput definition. This input will then be validated and passed to the mutation’s resolver as dict available under the input keyword argument:

def resolve_create_discussion(_, info, input):
    clean_input = {
        "category": input["category"],
        "title": input["title"],
        "is_announcement": input.get("isAnnouncement"),
        "is_closed": input.get("isClosed"),
    }

    try:
        return {
            "status": True,
            "discussion": create_new_discussion(info.context, clean_input),
        }
    except ValidationError as err:
        return {
            "status": False,
            "error: err,
        }

Another advantage of input-s is that they are reusable. If we later decide to implement another mutation for updating the Discussion, we can do it like this:

type_def = """
    type Mutation {
        createDiscussion(input: DiscussionInput!) {
            status: Boolean!
            error: Error
            discussion: Discussion
        }
        updateDiscussion(discussion: ID!, input: DiscussionInput!) {
            status: Boolean!
            error: Error
            discussion: Discussion
        }
    }

    input DiscussionInput {
        category: ID!
        title: String!,
        isAnnouncement: Boolean
        isClosed: Boolean
    }
"""

Our updateDiscussion mutation will now accept two arguments: discussion and input:

def resolve_update_discussion(_, info, discussion, input):
    clean_input = {
        "category": input["category"],
        "title": input["title"],
        "is_announcement": input.get("isAnnouncement"),
        "is_closed": input.get("isClosed"),
    }

    try:
        return {
            "status": True,
            "discussion": update_discussion(info.context, discussion, clean_input),
        }
    except ValidationError as err:
        return {
            "status": False,
            "error: err,
        }

You may wonder why you would want to use input instead of reusing already defined type. This is because input types provide some guarantees that regular objects don’t: they are serializable, and they don’t implement interfaces or unions. However input fields are not limited to scalars. You can create fields that are lists, or even reference other inputs:

type_def = """
    input PollInput {
        question: String!,
        options: [PollOptionInput!]!
    }

    input PollOptionInput {
        label: String!
        color: String!
    }
"""

Lastly, take note that inputs are not specific to mutations. You can create inputs to implement complex filtering in your Query fields.