mirumee / ariadne-graphql-modules

Ariadne package for implementing Ariadne GraphQL schemas using modular approach.

Home Page:https://ariadnegraphql.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Interoperability with other Python GraphQL libraries

miracle2k opened this issue · comments

I have a fairly large GraphQL API written in graphene. I am interested in moving to ariadne, but I'd rather not migrate the whole thing in one go. So, since both are based on graphql-core (more or less, the @2 vs @3 thing is a bit of a pain, but solvable), and both ultimately generate a graphql-core schema, I wondered, why not write some schema types in ariadne, while keeping the rest in graphene for now? This allows a step-by-step migration.

It turns out this is pretty easy from the Graphene side:

class External():
    def __init__(self, type):
        super(External, self).__init__()
        self._type = type

class BetterGraphene(GrapheneGraphQLSchema):

    def __init__(
        self,        
        external_types=None,
        **kwargs
    ):
        self.external_types = external_types
        GrapheneGraphQLSchema.__init__(self, **kwargs)

    def type_map_reducer(self, map_, type_):
        if isinstance(type_, External):
            type_ = self.external_types[type_._type]
        return super().type_map_reducer(map_, type_)

    def get_field_type(self, map_, type_):
        if isinstance(type_, External):
            return self.external_types[type_._type]
        return super().get_field_type(map_, type_)

Now I can do:

class Foo(graphene.ObjectType):
    bar = Field(External("Bar"))

And I can create the Bar type however I want, and pass it to the graphene Schema() constructor. For example, I can create a schema with ariadne with make_executable_schema, and take the Bar type from the result.

This actually works!

The problem is that I can't link back. That is, if Bar (no defined with Ariadne) wants to refer to a type that is still in Graphene. Or, simple custom scalars like a DateTime - I can't have them defined in Graphene, and use them from an Ariadne type, and of course, they can't be defined twice.

Ordinarily I would say that I maybe I am just asking for too much: Making those two libraries interact. But as you can see by how little code is required on the Graphene side, this is actually pretty straightforward and without much complexity. Again, this is because the way graphql-core functions: It does the hard lifting, and we are just syntactic sugar on top.

And Graphene makes it super easy to change the mechanism of how the graphql-core schema is generated, by allowing a subclass to override methods.

In Ariadne, make_executable_schema is a bit of a blackbox. What would be needed to make this work is just a little room in make_executable_schema to allow to inject custom types; maybe allowing a custom resolve_type function to be passed through to the ASTDefinitionBuilder.

What are your thoughts?

I was thinking about allowing GraphQLTypes to be passed as type defs to make_executable_schema. This would allow you to extract a type defined in one framework and use it in another, it would also allow code-first approach to be mixed with schema-first (which may be useful if parts of your schema come from an introspection protocol rather than from a file on disk).

Playing with this some more. Another option might be allowing the user to pass in a custom
graphql_core.GraphQLSchema subclass. This would allow this kind of customization in a very similar way to I was doing for graphene above, and, I am sure, would come in handy as an escape hatch in all kinds of cases.

This might be more difficult than I thought. Not looking closely enough before, I failed to realize that most of the code, such as build_ast_schema is actually part of graphql-core, not Ariadne. I don't think there is the option of adding features to graphql-core, since they are a 1:1 part of graphql.js. And graphql.js apparently intends the buildASTSchema function to be a way to parse a fully complete schema.

So given these limitations, I can't see how make_executable_schema() in Ariadne can ever parse an incomplete schema definition, as long as it builds on top of build_ast_schema. Curious of @patrys had a particular solution in mind to support passing custom GraphQLTypes.

I am not a fan of shoving this logic into the make_executable_schema. Introducing separate utils providing inter-op would be much preferable here.

I think that simplest (even if not top-performing) approach to your problem would be to use some utility that renders SDL for Graphene type, and use that SDL as intermediate for combining your Graphene types with Ariadne ones. One could pair this with util that converts Graphene's types to Ariadne's bindables:

from ariadne import gql, make_executable_schema
from ariadne_graphene import make_graphene_schema_bindable
from graphene import print_schema
from mygraphene_api import graphene_schema


type_defs = gql("""
type Bar {
    name: String
}
""")

schema = make_executable_schema(
    [print_schema(graphene_schema), type_defs], 
    make_graphene_schema_bindable(graphene_schema)
)

However, migrating any larger API from Graphene to Ariadne appears to me as something that should be a process with list of steps one should take and a set of code utilities enabling developers to complete those steps before moving on to next one. Something like this propably:

  • Change query execution/server layer to Ariadne and use those utils to make it work with your graphene schema.
  • Incrementally port your Graphene object types to Ariadne, writing SDL for them and moving their resolvers to Ariadne bindables. Use extending feature to add new fields to not-yet ported types.
  • Port scalars over to Ariadne.
  • Drop Graphene from project.

@rafalp Thanks for that idea. I can see that this is a way to do it, though I'm not quite sold on it's simplicity. In the meantime I did figure out a way to patch the schema returned by Ariadne to refer to the Graphene object types where appropriate, and using directives:

type_defs = gql("""
scalar External;
directive name on FIELD_DEFINITION;
type Bar {
    foo: External @name("GrapheneFoo")
}
""")

But implementing this turned out to be quite a bit of effort as well, so I'm not sure if the SDL-generating approach would have been more straightforward.

It still seems to me that there is a potential here - since all GraphQL libraries in Python just generate graphql-core objects, they should be able to interoperate nicely. But at this point I feel I am in over my head, and having solved my own problem, I'll let others decide if there is something actionable here.

make_graphene_schema_bindable

hi @rafalp! Could you provide approximate realization of make_graphene_schema_bindable from your response?

@Sergei-Rudenkov this is not possible currently. Ariadne is using graphql-core v3 while Graphene is still on v2, and both of those versions share same namespace.

@rafalp I use graphene==3.0.0b6 that use graphql-core v3 under the hood. So I basically have the same question as initial. How to start migration having some api using graphene and some api start using ariadne?

@Sergei-Rudenkov Sorry but no. I'm not able to put time now to dive into Graphene internals to develop thing like that.

I have an idea for changes to make_executable_schema that Ariadne GraphQL Modules implements that would make Graphene's v3 types work. There would be either a mixin or pass-through function that would make Graphene's type Ariadne-compatible.

Next Ariadne GraphQL Modules is designed to support multiple approaches (APIs? frontends?) to type definition to solve this issue. But we would still need a Graphene-like API for types definitions to take care of this.