Skip to main content

graphene-chain-mutation

Provide feature to allow chaining mutations using Graphene-python

I created this package after a question I asked on Stackoverflow and that I had to solve by myself in the end.

graphene-chain-mutation allow to create edge-like mutations that can reference the results of previous mutations in the same query.

Why this package ?

The requirement

Basically, when want to be able to create an object and reference it in the same query. We can use nested mutations at some extend, but it have been pointed (here and there) that nested mutation are not guaranteed to be sequential. So if we consider a query using nesting, like the following:

mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
    n1: upsertChild(data: $child1) {
        pk
        name
        siblings { pk name }
        
        parent: createParent(data: $parent) { pk name }
        
        newSibling: createSibling(data: $child2) { pk name }
        
        # we cannot set the parent of the new sibling in this query
        # because order is not guaranted for nested mutations
    }
}

We see that we cannot set the parent of the new sibling because the latter may be created before the parent.

So we want to use only root query, and maybe be able to make queries such as:

mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
    n1: upsertParent(data: $parent) {
        pk
        name
    }
    
    n2: upsertChild(data: $child1) {
        pk
        name
    }
    
    n3: upsertChild(data: $child2) {
        pk
        name
    }
    
    e1: setParent(parent: "n1", child: "n2") { ok }
    
    e2: setParent(parent: "n1", child: "n3") { ok }
    
    e3: addSibling(node1: "n2", node2: "n3") { ok }
}

Where e1, e2 and e3 reference results of previous mutations.

The problem

Unfortunately, referencing results of previous mutations is not part of the GraphQL specification.

In my opinion, this is an important feature for graph languages. We can compare this syntax with the Graphviz dot language, were could write for example:

digraph D {

  /* Nodes */
  A 
  B
  C

  /* Edges */

  A -> B
  A -> C
  A -> D

}

The feature we need is simply allowing edge-like operations using nodes from the same mutations.

A solution

This package provide:

  1. A mutation class that will automatically "share its results" using the dict injected in resolve context.
  2. A Graphene middleware that, if used, will share the results of all resolvers (even those not inheriting from the class above).
  3. 2 base edge mutation classes, to accept GraphQL alias as input argument to retrieve the results of previous mutations in the same query.

Usage

5 steps (See the test/fake.py module for an executable example).

  1. Install the package (requires graphene)
    pip install graphene-chain-mutation
    
  2. Write node-like mutations by inheriting ShareResult before graphene.Mutation:
     import graphene
     from graphene_chain_mutation import ShareResult
     from .types import ParentType, ParentInput, ChildType, ChildInput
    
     class CreateParent(ShareResult, graphene.Mutation, ParentType):
         class Arguments:
             data = ParentInput()
     
         @staticmethod
         def mutate(_: None, __: graphene.ResolveInfo,
                    data: ParentInput = None) -> 'CreateParent':
             return CreateParent(**data.__dict__)
     
     class CreateChild(ShareResult, graphene.Mutation, ChildType):
         class Arguments:
             data = ChildInput()
     
         @staticmethod
         def mutate(_: None, __: graphene.ResolveInfo,
                    data: ChildInput = None) -> 'CreateChild':
             return CreateChild(**data.__dict__)
    
  3. Create edge-like mutations by inheriting either ParentChildEdgeMutation (for FK relationships) or SiblingEdgeMutation (for m2m relationships). Specify the type of their input nodes and implement the set_link method:
     import graphene
     from graphene_chain_mutation import ParentChildEdgeMutation, SiblingEdgeMutation
     from .types import ParentType, ChildType
     from .fake_models import FakeChildDB
     
     class SetParent(ParentChildEdgeMutation):
    
         parent_type = ParentType
         child_type = ChildType
    
         @classmethod
         def set_link(cls, parent: ParentType, child: ChildType):
             FakeChildDB[child.pk].parent = parent.pk
    
     class AddSibling(SiblingEdgeMutation):
    
         node1_type = ChildType
         node2_type = ChildType
    
         @classmethod
         def set_link(cls, node1: ChildType, node2: ChildType):
             FakeChildDB[node1.pk].siblings.append(node2.pk)
             FakeChildDB[node2.pk].siblings.append(node1.pk)
    
  4. Create your schema as usual
     class Query(graphene.ObjectType):
         parent = graphene.Field(ParentType, pk=graphene.Int())
         parents = graphene.List(ParentType)
         child = graphene.Field(ChildType, pk=graphene.Int())
         children = graphene.List(ChildType)
    
     class Mutation(graphene.ObjectType):
         create_parent = CreateParent.Field()
         create_child = CreateChild.Field()
         set_parent = SetParent.Field()
         add_sibling = AddSibling.Field()
    
     schema = graphene.Schema(query=Query, mutation=Mutation)
    
  5. We need a context while executing a query:
     class NullContext: pass
    
     result = schema.execute(
         GRAPHQL_MUTATION
         ,variables = VARIABLES
         ,context=NullContext()
     )
    

Now GRAPHQL_MUTATION can be a query where edge-like mutation reference the results of node-like mutations:

GRAPHQL_MUTATION = """
mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
    n1: createParent(data: $parent) {
        pk
        name
    }
    
    n2: createChild(data: $child1) {
        pk
        name
    }
    
    n3: createChild(data: $child2) {
        pk
        name
    }
    
    e1: setParent(parent: "n1", child: "n2") { ok }
    
    e2: setParent(parent: "n1", child: "n3") { ok }
    
    e3: addSibling(node1: "n2", node2: "n3") { ok }
}
"""

VARIABLES = dict(
    parent = dict(
        name = "Emilie"
    )
    ,child1 = dict(
        name = "John"
    )
    ,child2 = dict(
        name = "Julie"
    )
)

Nesting with inline reference

We can use the referencing capability offered by the ShareResult class to reference the result of a root mutation in a nested mutation (that uses a resolver).

Example:

import graphene
from graphene import ObjectType
from graphene_chain_mutation import ShareResult
from .types import ParentType, ParentInput, ChildType, ChildInput


class CreateParent(ShareResult, graphene.Mutation, ParentType):
    class Arguments:
        data = ParentInput()

    @staticmethod
    def mutate(_: None, __: graphene.ResolveInfo,
               data: ParentInput = None) -> 'CreateParent':
        return CreateParent(**data.__dict__)


class CreateChild(ShareResult, graphene.Mutation, ChildType):
    class Arguments:
        data = types.ChildInput()

    ref_parent = graphene.Field(types.ParentType, ref=graphene.String())
    """Resolving this field sets a reference to parent from previous mutation result."""

    @staticmethod
    def mutate(_: None, __: graphene.ResolveInfo,
               data: types.ChildInput) -> 'CreateChild':
        return UpdateChild(**data.__dict__)

    @staticmethod
    def resolve_ref_parent(child: 'CreateChild', info: graphene.ResolveInfo,
                           ref: str = None):
        """
        Nesting mutation by resolving a field and setting the parent of
        this child by referencing the result of another mutation in
        the same query.

        :param child: result of the parent mutation (mutate method of this class)
        :param info: graphene resolve info.
        :param ref: name of the node of the PArent mutation in the query.
        :return: the referenced parent.
        """
        assert ref is not None
        assert info.context.shared_results is not None
        parent = info.context.shared_results.get(ref)
        assert parent is not None
        FakeChildDB[child.pk].parent = parent.pk
        return parent

We can resolve a query like:

mutation ($parent: ParentInput, $child1: ChildInput, $child2: ChildInput) {
    n1: createParent(data: $parent) {
        pk
        name
    }
    
    n2: createChild(data: $child1) {
        pk
        name
        parent: refParent(ref: "n1") {
          pk
          name
        }
    }
    
    n3: createChild(data: $child2) {
        pk
        name
        parent: refParent(ref: "n1") {
          pk
          name
        }
    }
}

Now keep in mind, as stated at the beginning of this Readme, that nested mutation may have unpredictable results due to race condition. This is because, we recall, the order of execution of nested mutation is not guaranteed.

Caveats

Decoratored resolvers

If you create a decorator that you'll use on your resolvers (for example to check permissions), be sure to use functools.wraps, so that the signature of the resolver is preserved (as returned by inspect.signature). See more about functools.wraps in the official documentation.

Release files for graphene-chain-mutation 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for graphene-chain-mutation 1.1.0
File Size Uploaded
graphene-chain-mutation-1.1.0.tar.gz 10.2 kB Details

Release files / graphene-chain-mutation-1.1.0.tar.gz

Download URL graphene-chain-mutation-1.1.0.tar.gz
Size 10.2 kB
Tags Source
SHA-256 checksum
How to use checksums
8c7aa9148fd2c676a0d388e77210a7d8bd840a6f79d99f4a41925a4e96a05784
BLAKE2b-256 checksum
How to use checksums
586ad52c1660032392696dc86b5368641a84d8672a6f67ab2f03b625492f3877
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.10.4

Release history Release notifications | RSS feed

This release

1.1.0 This release

1 release file

1.0.9

1 release file

1.0.8

1 release file

1.0.7

1 release file

1.0.6

1 release file

1.0.5

1 release file

1.0.4

1 release file

1.0.3

1 release file

1.0.2

1 release file

1.0.1

1 release file

1.0.0

1 release file

0.0.3

1 release file

0.0.2

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page