Warm tip: This article is reproduced from stackoverflow.com, please click
graphql python-3.x microservices apollo apollo-server

Problems integrating Python graphene with Apollo Federation

发布于 2020-03-27 10:27:25

Using python to implement GraphQL across multiple microservices, some use Ariadne, and some use graphene (and graphene-Django). Because of the microservice architecture, it's chosen that Apollo Federation will merge the schemas from the different microservices.

With Ariadne, it's very simple (being schema first), and a small example:

from ariadne import QueryType, gql, make_executable_schema, MutationType, ObjectType
from ariadne.asgi import GraphQL

query = QueryType()
mutation = MutationType()

sdl = """
type _Service {
    sdl: String
}

type Query {
    _service: _Service!
    hello: String
}
"""

@query.field("hello")
async def resolve_hello(_, info):
    return "Hello"


@query.field("_service")
def resolve__service(_, info):
    return {
        "sdl": sdl
    }

schema = make_executable_schema(gql(sdl), query)
app = GraphQL(schema, debug=True)

Now this is picked up with no problem with Apollo Federation:

const { ApolloServer } = require("apollo-server");
const { ApolloGateway } = require("@apollo/gateway");


const gateway = new ApolloGateway({
    serviceList: [
      // { name: 'msone', url: 'http://192.168.2.222:9091' },
      { name: 'mstwo', url: 'http://192.168.2.222:9092/graphql/' },
    ]
  });

  (async () => {
    const { schema, executor } = await gateway.load();
    const server = new ApolloServer({ schema, executor });
    // server.listen();
    server.listen(
      3000, "0.0.0.0"
      ).then(({ url }) => {
      console.log(`???? Server ready at ${url}`);
    });
  })();

For which I can run graphql queries against the server on 3000.

But, with using graphene, trying to implement the same functionality as Ariadne:

import graphene

class _Service(graphene.ObjectType):
    sdl = graphene.String()

class Query(graphene.ObjectType):

    service = graphene.Field(_Service, name="_service")
    hello = graphene.String()

    def resolve_hello(self, info, **kwargs):
        return "Hello world!"

    def resolve_service(self, info, **kwargs):
        from config.settings.shared import get_loaded_sdl
        res = get_loaded_sdl()  # gets the schema defined later in this file
        return _Service(sdl=res)

schema = graphene.Schema(query=Query)

# urls.py
urlpatterns = [
    url(r'^graphql/$', GraphQLView.as_view(graphiql=True)),
]

,... now results in an error from the Apollo Federation:

GraphQLSchemaValidationError: Type Query must define one or more fields.

As I checked into this matter, I found that apollo calls the microservice with a graphql query of:

query GetServiceDefinition { _service { sdl } }

Running it on the microservice via Insomnia/Postman/GraphiQL with Ariadne gives:

{
  "data": {
    "_service": {
      "sdl": "\n\ntype _Service {\n    sdl: String\n}\n\ntype Query {\n    _service: _Service!\n    hello: String\n}\n"
    }
  }
}

# Which expanding the `sdl` part:
type _Service {
    sdl: String
}

type Query {
    _service: _Service!
    hello: String
}

and on the microservice with Graphene:

{
  "data": {
    "_service": {
      "sdl": "schema {\n  query: Query\n}\n\ntype Query {\n  _service: _Service\n  hello: String\n}\n\ntype _Service {\n  sdl: String\n}\n"
    }
  }
}

# Which expanding the `sdl` part:
schema {
    query: Query
}

type Query {
    _service: _Service
    hello: String
}

type _Service {
    sdl: String
}

So, they both are the same thing for defining how to get sdl, I checked into the microservice response, and found that graphene response is sending the correct data too, with the Json response "data" being equal to:

execution_Result:  OrderedDict([('_service', OrderedDict([('sdl', 'schema {\n  query: Query\n}\n\ntype Query {\n  _service: _Service\n  hello: String\n}\n\ntype _Service {\n  sdl: String\n}\n')]))])

So what could the reason be for Apollo Federation not being able to successfully get this microservice schema?

Questioner
jupiar
Viewed
120
5,153 2019-08-15 00:04

The solution is actually a slight hack the schema that is automatically generated via graphene. I thought I had tried this already and it still worked, but I just did it again now but it broke.

So if in Ariadne, I add

schema {
    query: Query
}

into the sdl, Apollo Federation also raises Type Query must define one or more fields.. Without it, it works fine. So then I also went to graphene and in the resolve_service function I did:

def resolve_service(self, info, **kwargs):
    from config.settings.shared import get_loaded_sdl
    res = get_loaded_sdl()
    res = res.replace("schema {\n  query: Query\n}\n\n", "")
    return _Service(sdl=res)

And now graphene works too, so I guess the problem was something I overlooked, it seems that Apollo Federation cannot handle schema grammar of:

schema {
    query: Query
}

Update 1

A line I didn't notice on Apollo's website is that:

This SDL does not include the additions of the federation spec above. Given an input like this:

This is clear when combining the services together in Federation as it will raise the error:

GraphQLSchemaValidationError: Field "_Service.sdl" can only be defined once.

So, although in the full schema for the microservice with define _Service.sdl, we want that information gone for the string of the full-schema that is returned as the return String for _Service.sdl

Update 2

The Apollo Federation is now working fine, with making sure that the string returned by the sdl field does not contain federation specs.

In graphene, I think each implementation might differ, but in general you want to replace the following:

res = get_loaded_sdl()
res = res.replace("schema {\n  query: Query\n}\n\n", "")
res = res.replace("type _Service {\n  sdl: String\n}", "")
res = res.replace("\n  _service: _Service!", "")

And in Ariadne, just need to define two sdl's, one containing the federation specs (for the schema returned by the service), and one without federation specs (the one returned by the sdl field)