2
votes

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?

4
Federated services need to implement the federation spec. In Apollo, this is done by using the buildFederatedSchema function. I'm not sure if graphene supports anything like that. - Daniel Rearden
As far as I understand, and after successfully implementing Ariadne, it is that for federated services to work, in the schema there needs to be a _service field, of type _Service, which has a field sdl; whcih returns the whole schema as a string. This is very weird though as this is just repetition, essentially having a field in a schema, which returns said schema. You are correct in that graphene doesnt support this natively, but neither does almost every single backend trying to utilize graphql, like Ariadne we just define what their documentation says there needs to be. - jupiar

4 Answers

2
votes

This pip library can help https://pypi.org/project/graphene-federation/

Just use build_schema, and it'll add _service{sdl} for you:

import graphene
from graphene_federation import build_schema


class Query(graphene.ObjectType):
    ...
    pass

schema = build_schema(Query)  # add _service{sdl} field in Query
1
votes

You are on the good path on the other answer, but it looks like you are going to need to strip out some stuff from the printed version.

here is the way I have used in a github issue

i sum up my code here:

schema = ""
class ServiceField(graphene.ObjectType):
    sdl = String()

    def resolve_sdl(parent, _):
        string_schema = str(schema)
        string_schema = string_schema.replace("\n", " ")
        string_schema = string_schema.replace("type Query", "extend type Query")
        string_schema = string_schema.replace("schema {   query: Query   mutation: MutationQuery }", "")
        return string_schema


class Service:
    _service = graphene.Field(ServiceField, name="_service", resolver=lambda x, _: {})

class Query(
    # ...
    Service,
    graphene.ObjectType,
):
    pass

schema = graphene.Schema(query=Query, types=CUSTOM_ATTRIBUTES_TYPES)
1
votes

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)

1
votes

In case anyone is wondering, this is because graphene v2 uses commas instead of ampersands in interfaces

interface x implements y, z {
   ...
}

and this syntax no longer works, a workaround is to monkey-patch get_sdl

import re

from myproject import Query, Mutation
from graphene_federation import service, build_schema


# monkey patch old get_sdl
old_get_sdl = service.get_sdl

def get_sdl(schema, custom_entities):
    string_schema = old_get_sdl(schema, custom_entities)
    string_schema = string_schema.replace('\n', ' ')

    pattern_types_interfaces = r'type [A-Za-z]* implements ([A-Za-z]+\s*,?\s*)+'
    pattern = re.compile(pattern_types_interfaces)

    string_schema = pattern.sub(lambda matchObj: matchObj.group().replace(',', ' &'), string_schema)
    return string_schema

service.get_sdl = get_sdl
schema = build_schema(Query, mutation=Mutation)

and it works.