Package: graphene-chain-mutation
graphene-chain-mutationProvide feature to allow chaining mutations usingGraphene-pythonI created this package aftera question I asked on Stackoverflowand that I had to solve by myself in the end.graphene-chain-mutationallow to create edge-like mutations that can reference the results of previous mutations in the same query.Why this package ?The requirementBasically, 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 (hereandthere) 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){pknamesiblings{pkname}parent:createParent(data:$parent){pkname}newSibling:createSibling(data:$child2){pkname}# 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){pkname}n2:upsertChild(data:$child1){pkname}n3:upsertChild(data:$child2){pkname}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 problemUnfortunately, 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:digraphD{/* Nodes */ABC/* Edges */A->BA->CA->D}The feature we need is simply allowingedge-likeoperations usingnodesfrom the same mutations.A solutionThis package provide:A mutation class that will automatically"share its results"using thedictinjected in resolve context.AGraphene middlewarethat, if used, will share the results of all resolvers (even those not inheriting from the class above).2 base edge mutation classes, to accept GraphQL alias as input argument to retrieve the results of previous mutations in the same query.Usage5 steps (See thetest/fake.py modulefor an executable example).Install the package (requiresgraphene)pipinstallgraphene-chain-mutationWritenode-likemutations by inheritingShareResultbeforegraphene.Mutation:importgraphenefromgraphene_chain_mutationimportShareResultfrom.typesimportParentType,ParentInput,ChildType,ChildInputclassCreateParent(ShareResult,graphene.Mutation,ParentType):classArguments:data=ParentInput()@staticmethoddefmutate(_:None,__:graphene.ResolveInfo,data:ParentInput=None)->'CreateParent':returnCreateParent(**data.__dict__)classCreateChild(ShareResult,graphene.Mutation,ChildType):classArguments:data=ChildInput()@staticmethoddefmutate(_:None,__:graphene.ResolveInfo,data:ChildInput=None)->'CreateChild':returnCreateChild(**data.__dict__)Createedge-likemutations by inheriting eitherParentChildEdgeMutation(for FK relationships) orSiblingEdgeMutation(for m2m relationships). Specify the type of their input nodes and implement theset_linkmethod:importgraphenefromgraphene_chain_mutationimportParentChildEdgeMutation,SiblingEdgeMutationfrom.typesimportParentType,ChildTypefrom.fake_modelsimportFakeChildDBclassSetParent(ParentChildEdgeMutation):parent_type=ParentTypechild_type=ChildType@classmethoddefset_link(cls,parent:ParentType,child:ChildType):FakeChildDB[child.pk].parent=parent.pkclassAddSibling(SiblingEdgeMutation):node1_type=ChildTypenode2_type=ChildType@classmethoddefset_link(cls,node1:ChildType,node2:ChildType):FakeChildDB[node1.pk].siblings.append(node2.pk)FakeChildDB[node2.pk].siblings.append(node1.pk)Create your schema as usualclassQuery(graphene.ObjectType):parent=graphene.Field(ParentType,pk=graphene.Int())parents=graphene.List(ParentType)child=graphene.Field(ChildType,pk=graphene.Int())children=graphene.List(ChildType)classMutation(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)We need a context while executing a query:classNullContext:passresult=schema.execute(GRAPHQL_MUTATION,variables=VARIABLES,context=NullContext())NowGRAPHQL_MUTATIONcan 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) {pkname}n2: createChild(data: $child1) {pkname}n3: createChild(data: $child2) {pkname}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 referenceWe can use the referencing capability offered by theShareResultclass to reference the result of a root mutation in a nested mutation (that uses a resolver).Example:importgraphenefromgrapheneimportObjectTypefromgraphene_chain_mutationimportShareResultfrom.typesimportParentType,ParentInput,ChildType,ChildInputclassCreateParent(ShareResult,graphene.Mutation,ParentType):classArguments:data=ParentInput()@staticmethoddefmutate(_:None,__:graphene.ResolveInfo,data:ParentInput=None)->'CreateParent':returnCreateParent(**data.__dict__)classCreateChild(ShareResult,graphene.Mutation,ChildType):classArguments:data=types.ChildInput()ref_parent=graphene.Field(types.ParentType,ref=graphene.String())"""Resolving this field sets a reference to parent from previous mutation result."""@staticmethoddefmutate(_:None,__:graphene.ResolveInfo,data:types.ChildInput)->'CreateChild':returnUpdateChild(**data.__dict__)@staticmethoddefresolve_ref_parent(child:'CreateChild',info:graphene.ResolveInfo,ref:str=None):"""Nesting mutation by resolving a field and setting the parent ofthis child by referencing the result of another mutation inthe 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."""assertrefisnotNoneassertinfo.context.shared_resultsisnotNoneparent=info.context.shared_results.get(ref)assertparentisnotNoneFakeChildDB[child.pk].parent=parent.pkreturnparentWe can resolve a query like:mutation($parent:ParentInput,$child1:ChildInput,$child2:ChildInput){n1:createParent(data:$parent){pkname}n2:createChild(data:$child1){pknameparent:refParent(ref:"n1"){pkname}}n3:createChild(data:$child2){pknameparent:refParent(ref:"n1"){pkname}}}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.CaveatsDecoratored resolversIf you create a decorator that you'll use on your resolvers (for example to check permissions), be sure to usefunctools.wraps, so that the signature of the resolver is preserved (as returned byinspect.signature). Seemore about functools.wraps in the official documentation.