GraphQL Federation

Introduction

Apollo Federation provides a mechanism to combine multiple GraphQL endpoints and schema into a single aggregate endpoint and composite schema. The basic principles of GraphQL federation are as follows:

  1. Collects the schemas from multiple endpoints.
  2. Combines the schemas into a single composite schema.
  3. Serves the single composite schema to the user completely hiding the inner dependencies.
  4. When a query is performed, the Federation Gateway calls each of the endpoints in a specific order and combines their responses.

Quick Start

Download this docker-compose.yaml that manages all the required services for an aggregate federated SWAPI GraphQL endpoint that joins Semantic Objects and a simple example Apollo Server:

  • Federation Gateway
  • Semantic Objects (loaded with SWAPI data and Schema)
  • Example Extended SWAPI Service
  • GraphDB (loaded with SWAPI data)
  • GraphDB (for storing and managing the schema)

Once you have downloaded the compose file, follow the Semantic Objects Quick Start guide using this file instead of the one defined in the guide. (Skip the download operation in the Docker Compose section of the guide.)

Use this environment file .env (must be named with a dot, no extension!) to configure the location of the Ontotext Semantic Objects and GraphDB licenses.

Following the Quick Start guide, create a simple bash script setup.sh for starting everything and loading the SWAPI data and schema.

Download the GraphDB repository configuration, SWAPI data, and SOML schema and place them in the same directory as the setup script. You should have the following files in the directory: data.ttl, repo.ttl, and schema.yaml.

Run the setup script. To start the services, load the data, and build a federated aggregate GraphQL endpoint, use the following shell command:

chmod +x ./setup.sh && ./setup.sh

Note

The setup script and the .env file should be in the same directory as the docker-compose.yaml file.

Try out a GraphQL query against the federated aggregate schema using this cURL command:

curl 'http://localhost:9000/graphql' \
      -H 'Accept: application/json' \
      -H 'Content-Type: application/json' \
      --data-binary '{"query":"query { planet(where: {ID: \"https://swapi.co/resource/planet/25\"}) { name diameter gravity } }"}' \
      --compressed

That returns the Planet Dantooine with the extended calculated gravity property.

{
  "data": {
    "planet": [
      {
        "name": "Dantooine",
        "diameter": 9830,
        "gravity": "1 standard"
      }
    ]
  }
}

Tip

You can also open the Workbench on http://localhost:9993. It is configured to send queries against the Apollo Gateway.

Tutorial

The following sections provide examples of implementing GraphQL Federation using the Semantic Objects.

The examples are based on the Star Wars dataset.

Extend Semantic Objects with External Services

We will first give an example on how to extend an object defined in SOML by an external GraphQL service.

SOML

For this part, no change in the SOML is required. We have already defined the following type:

Planet:
  ...
  props:
    ...
    diameter: {label: "Diameter in Km", range: integer}
    ...

This SOML will generate the following GraphQL type:

type Planet implements Object & Nameable @key(fields : "id") { ... }

The @key directive is used by the Apollo Federation to show how to link the Planet extensions from the different endpoints.

This GraphQL schema will be exposed by the Semantic Objects for federation by the gateway.

Example Apollo GraphQL Extension Service

Now let’s take a look at a small JS application which extents the Planet type:

const { ApolloServer, gql } = require("apollo-server-express");
const { buildFederatedSchema } = require("@apollo/federation");
const express = require('express');

const path = '/graphql';
const app = express();

const typeDefs = gql`

  scalar Integer

  extend type Query {
    extend_swapi_health: Health
  }

  extend type Planet @key(fields: "id") {
    id: ID! @external
    diameter: Integer @external
    mass: Int
    calculatedGravity: Integer @requires(fields: "diameter")
  }

  type Health {
    status: String
  }

  input Integer_Where {
    AND: [Integer_Where!]
    OR: [Integer_Where!]
    EQ: Integer
    NEQ: Integer
    LT: Integer
    LTE: Integer
    GT: Integer
    GTE: Integer
    IN: [Integer!]
    NIN: [Integer!]
  }

`

const resolvers = {
  Planet: {
    __resolveReference(reference) {
      return {
        ...reference,
        ...planetList.find(planet => planet.id === reference.id)
      };
    },
    calculatedGravity(reference) {
      console.log('Planet @key/id [' + reference.id + '] diameter [' + reference.diameter + ']');
      let planet = planetList.find(planet => planet.id === reference.id);
      console.log('Found planet its mass is [' + planet.mass + ']');
      // Clearly the wrong calc, its just to show calculated extension
      let gravity = reference.diameter * planet.mass * 100;
      console.log('Calculated gravity [' + this.gravity + ']');
      return gravity
    }
  },
  Query: {
    extend_swapi_health() {
      return { status: "OK"}
    }
  }
};

// Dummy Good to go always returns OK
app.get('/__gtg', function (req, res){
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ gtg: "OK", message: "Mock SOaaS operating as expected" }));
});

// Dummy Health Check always returns OK, no dependencies/healthChecks
app.get('/__health', function (req, res){
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({
        status: "OK",
        id: `http://localhost:4006${server.graphqlPath}`,
        name: 'SOaaS Extended HealthCheck',
        type: 'graphql',
        healthChecks: []
      })
  )
});

const server = new ApolloServer({
  schema: buildFederatedSchema([
    {
      typeDefs,
      resolvers
    }
  ]),
  context: params => () => {
    console.log(params.req.body.query);
    console.log(params.req.body.variables);
    console.log(params.req.headers);
  }
});

server.applyMiddleware({app: app, path: path});

app.listen({ port: 4006 }, () => {
  console.log(`Server ready at http://localhost:4006${server.graphqlPath}`);
});

const planetList = [
  {
    id: "https://swapi.co/resource/planet/25",
    mass: 120
  },
  {
    id: "https://swapi.co/resource/planet/13",
    mass: 130
  }
];

This application does the following:

  • Adds mass property, which is retrieved from an external data source.
  • Adds calculatedGravity property, which uses a diameter field from the Semantic Objects endpoint and mass in order to get calculated.
  • Adds a new type Health.
  • Adds extend_swapi_health query, which returns type Health.

Since diameter is of type Integer (which is a non-standard GraphQL type), we have to define the Integer scalar in the schema explicitly. Because of this, we need to define the Integer_Where type as well, although we are not using it in the current schema. It is directly copied from the Semantic Objects GraphQL schema, and is needed by the Apollo Gateway in order to merge the two schemas.

The extension GraphQL schema is now also available for federation.

Aggregate Schema

The federation gateway will combine both the Semantic Objects GraphQL schema and the extended service GraphQL schema to produce a composite schema that joins both the Semantic Objects and the Extended Apollo JS service.

So running the following introspection query against the Apollo gateway:

query test {
  __type(name: "Planet") {
    name
    fields {
      name
    }
  }
}

Will return:

{
  "data": {
    "__type": {
      "name": "Planet",
      "fields": [
        {
          "name": "id"
        },
        {
          "name": "mass"
        },
        {
          "name": "calculatedGravity"
        }
      ]
    }
  }
}

We can see that the new fields (mass and calculatedGravity) are included.

Federated Query

Once everything is set, we can run a sample query to the Apollo Gateway:

query planet {
  planet (ID: "https://swapi.co/resource/planet/25") {
    id
    diameter
    mass
    calculatedGravity
  }
}

Which will return the following result:

{
  "data": {
    "planet": [
      {
        "id": "https://swapi.co/resource/planet/25",
        "diameter": 9830,
        "mass": 120,
        "calculatedGravity": 117960000
      }
    ]
  }
}

Extend External Objects

For the second example, we will extend an object defined in an external GraphQL service.

External Service

This is an example of a small GraphQL application:

const { ApolloServer, gql } = require("apollo-server-express");
const { buildFederatedSchema } = require("@apollo/federation");
const express = require('express');

const path = '/graphql';
const app = express();

const typeDefs = gql`

  extend type Query {
    organization_health: Health
  }

  type Organization @key(fields: "id") {
    id: ID!
    desc: String
    dateFounded: String
  }

  type Health {
    status: String
  }
`;

const resolvers = {
  Organization: {
    __resolveReference(reference) {
      return orgList.find(org => org.id === reference.id);
    }
  },
  Query: {
    organization(_, args) {
      if (args.id) {
        return orgList.filter(org => org.id === args.id);
      } else {
        return orgList;
      }
    },
    organization_health() {
      return { status: "OK"}
    }
  }
};

// Dummy Good to go always returns OK
app.get('/__gtg', function (req, res){
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({ gtg: "OK", message: "Mock SOaaS operating as expected" }));
});

// Dummy Health Check always returns OK, no dependencies/healthChecks
app.get('/__health', function (req, res){
  res.setHeader('Content-Type', 'application/json');
  res.end(JSON.stringify({
    status: "OK",
    id: `http://localhost:4007${server.graphqlPath}`,
    name: 'Organization GraphQL HealthCheck',
    type: 'graphql',
    healthChecks: []
  }))
});;

const server = new ApolloServer({
  schema: buildFederatedSchema([
    {
      typeDefs,
      resolvers
    }
  ]),
  context: params => () => {
    console.log(params.req.body.query);
    console.log(params.req.body.variables);
    console.log(params.req.headers);
  }
});

server.applyMiddleware({app: app, path: path});

app.listen({ port: 4007 }, () => {
  console.log(`Server ready at http://localhost:4007${server.graphqlPath}`);
});

const orgList = [
  {
    id: "https://swapi.co/resource/organization/1",
    desc: "The Resistance, also known as the Rebellion, was a private military force founded by General Leia Organa during the cold war between the New Republic and the First Order",
    dateFounded: "28 ABY"
  },
  {
    id: "https://swapi.co/resource/organization/2",
    desc: "The Galactic Empire (19 BBY–5 ABY), also known as the First Galactic Empire, the New Order, or simply the Empire, was the fascist galactic government that replaced the Galactic Republic in the aftermath of the Clone Wars",
    dateFounded: "19 BBY"
  }
];

This application defines the type Organization and has two Organization instances.

SOML

As the Organization object is not present in the Star Wars SOML, we will have to add it. So add the following to the SOML:

objects:
  Organization:
    descr: "Extended organization object coming from federation service"
    type: ["voc:Organization"]
    extend: true
    props:
      film: {label: "Film in which the Organization appeared in", range: Film, max: inf}

The important part here is the extend flag. It is false by default. Setting it to true will generate an extend type Organization in the GraphQL schema.

Apart from that flag, the rest is like a normal Object. We can add properties with range other Objects in the SOML, as well as use it as range for other props.

Queries

If we leave the SOML as it is, this will lead to a conflict in the aggregated schema. This is because in the Federation, you cannot have the same query defined in multiple endpoints. It would leave us with the following query defined in both the Semantic Objects and the external service:

type Query {
  organization(...): [Organization!]
}

To avoid this, we can use the queryPfx and mutationPfx config parameters. Add the following to the SOML:

config:
  queryPfx: "soaas_"

The Semantic Objects will end up serving the following GraphQL query:

type Query {
  soaas_organization(...): [Organization!]
}

This way, the conflict will be avoided.

Mutations

Apollo Federation does not support federated mutations. However, mutations of the extend objects are still supported as in any other Object. So let’s add some data in the Semantic Objects using the following mutation:

mutation createOrganization {
  create_Organization  (
    objects: [
      {id: "https://swapi.co/resource/organization/1",
      film: [
        {
          ids: ["https://swapi.co/resource/film/1"]
        }
      ]},
      {id: "https://swapi.co/resource/organization/2",
      film: [
        {
          ids: ["https://swapi.co/resource/film/2"]
        }
      ]}
    ]
  )
  {
    organization {
      id
    }
  }
}

Federated Query

Now that we have a service that defines Organization, Semantic Objects that extend Organization with field film, and data and both services, we can start an Apollo Federation and run the following query against the Gateway:

query organization {
   organization {
    id
    desc
    dateFounded
    film {
      id
      name
    }
  }
}

The result is the following:

{
  "data": {
    "organization": [
      {
        "id": "https://swapi.co/resource/organization/1",
        "desc": "The Resistance, also known as the Rebellion, was a private military force founded by General Leia Organa during the cold war between the New Republic and the First Order",
        "dateFounded": "28 ABY",
        "film": [
          {
            "id": "https://swapi.co/resource/film/1",
            "name": "A New Hope"
          }
        ]
      },
      {
        "id": "https://swapi.co/resource/organization/2",
        "desc": "The Galactic Empire (19 BBY–5 ABY), also known as the First Galactic Empire, the New Order, or simply the Empire, was the fascist galactic government that replaced the Galactic Republic in the aftermath of the Clone Wars",
        "dateFounded": "19 BBY",
        "film": [
          {
            "id": "https://swapi.co/resource/film/2",
            "name": "The Empire Strikes Back"
          }
        ]
      }
    ]
  }
}

We can see the data from both the JS service and the Semantic Objects combined in the response.

Limitations

  • The @key directive for all types is set to their ID, although the Apollo Federation server supports each field to be used as key.
  • The current Apollo Gateway implementation (ontotext/apollo-federation-gateway) will not update the aggregated schema if an endpoint’s schema changes. So changing the schema of a federated endpoint requires a Gateway restart.

Schema Changes

When a schema change occurs in the Semantic Objects or other external services used by the Apollo gateway, they must be restarted in order to get the combined schema rebuilt. In case the Apollo gateway does not start properly, please check its logs for more information about what might be wrong with the combined schema.

Administration

Monitoring

Health Check

The Apollo Federation Gateway has an aggregate health check that checks all configured federated services for health status.

The __health endpoint can be accessed at http://{federationhost}:{federation_port}/__health.

An example response is as follows, and you can see the Semantic Objects health response aggregated with the extended service:

{
  "status": "OK",
  "id": "apollo-federation-gateway",
  "healthChecks": [
    {
      "status": "OK",
      "healthChecks": [
        {
          "status": "OK",
          "id": "1200",
          "name": "SPARQL checks",
          "type": "sparql",
          "impact": "SPARQL Endpoint operating normally, writable and populated with data.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "SPARQL Endpoint checks.",
          "message": "SPARQL Endpoint operating normally, writable and populated with data."
        },
        {
          "status": "OK",
          "id": "1300",
          "name": "SOML Checks",
          "type": "soml",
          "impact": "SOML bound, service operating normally.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "SOML checks.",
          "message": "SOML bound, service operating normally."
        },
        {
          "status": "OK",
          "id": "1350",
          "type": "soml-rbac",
          "impact": "Security is disabled. SOML RBAC healthcheck is inactive.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "SOML RBAC checks.",
          "message": "Security is disabled. SOML RBAC healthcheck is inactive."
        },
        {
          "status": "OK",
          "id": "1400",
          "name": "Query service",
          "type": "queryService",
          "impact": "Query service operating normally.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "Query service checks.",
          "message": "Query service operating normally."
        },
        {
          "status": "OK",
          "id": "1500",
          "name": "Mutations Service",
          "type": "mutationService",
          "impact": "Mutation Service operating normally.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "Mutation Service checks.",
          "message": "Mutation Service operating normally."
        }
      ]
    },
    {
      "status": "OK",
      "id": "example/1",
      "name": "Example Health Check",
      "type": "graphql",
      "healthChecks": []
    }
  ]
}

Good to Go

The Apollo Federation Gateway has an aggregate good to go check that checks all configured federated services for good to go status.

The __gtg endpoint can be accessed at http://{federationhost}:{federation_port}/__gtg.

An example response is as follows:

{
  "gtg": "OK",
  "message": "Federation Gateway operating as expected"
}

Configuration

Federation Gateway

The Federation Gateway is provided as a Docker container. The following Docker Compose YAML describes the various ENV variables that control the Gateway:

  • GRAPHQL_SERVICE_N: GraphQL endpoint URL address to include within the aggregate schema
  • GRAPHQL_SERVICE_N_GTG: GraphQL endpoint good to go endpoint
  • GRAPHQL_SERVICE_N_HEALTH: GraphQL endpoint health endpoint
  • CACHE_MAX_AGE: cache header max age
  • ENGINE_API_KEY: in case you want to register the federation services with an Apollo Graph Manager
  • SERVICE_DWELL: dwell time between federated GraphQL endpoint health checks

You can download and use this example docker-compose.yaml configuration.

Federation Gateway Authorization

If the Semantic Objects have security enabled, the Federation Gateway must authenticate and retrieve an Authorization JWT token.

This token is used by the Gateway to make authorized requests to federated services to retrieve each GraphQL _service.sdl schema.

After that, the Gateway builds the aggregate schema and discards the token.

Subsequent GraphQL query and mutation requests will use the client’s Authorization header.

The following ENV variables control the Gateway schema aggregation authorization:

  • SECURITY_ENABLED: true - schema building uses FusionAuth supplied authorization tokens, otherwise no authorization
  • FUSION_APPLICATION_ID: The FusionAuth Application UUID for the Semantic Objects, e.g., “0afceebe-cfc6-4ddf-81b9-a7fd658b30d4”
  • FUSION_PROTOCOL: The protocol to use when connecting to the FusionAuth REST API “http” | “https”
  • FUSION_HOST: The server host to use when connecting to the FusionAuth REST API, e.g., “localhost”
  • FUSION_PORT: The TCP/IP port to use when connecting to the FusionAuth REST API, e.g., “9011”
  • FUSION_USERNAME: The FusionAuth username to use when authenticating, e.g., “federation”
  • FUSION_PASSWORD: The FusionAuth password to use when authentication, e.g., “federation”
  • FORWARD_HEADERS: The HTTP headers the Gateway should forward to federated GraphQL endpoints “X-Request-ID, Authorization”
  • FUSION_LOGIN_RETRIES: The number of authentication re-tries before giving up. Default “10”
  • FUSION_LOGIN_RETRY_DELAY: The number of milliseconds between each authentication retry. Default “2000”

As mentioned above, in order for the Federation Gateway to retrieve the SDL, it needs a user with sufficient rights to do so. So first, add the following role to the SOML (it provides gives read permissions to the SDL):

1
2
3
4
5
6
7
rbac:
  roles:
    Federation:
      description: "Federation service role"
      actions: [
        "_Service/sdl/read"
      ]

The next step is to add the user in FusionAuth. The user should have role Federation and username and password matching the ones defined in FUSION_USERNAME and FUSION_PASSWORD environment variables.

If you are using the OPCTL tool to provision the security, add the following to the configuration yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
security:
  roles:
    - name: Federation
      superRole: false
      default: false
  users:
    - username: federation
      email: federation@example.com
      password: federation
      roles:
        - Federation

Once you have a configured user and role, the Federation Gateway will be able to operate properly.

Federation Correlation

The Federation Gateway will generate X-Request-ID correlation ids if none are present on requests. X-Request-ID is always logged and forwarded to federated GraphQL endpoints.

This ensures that a GraphQL query or mutation can be traced and correlated across the Gateway, Federated GraphQL services and indeed GraphDB.

By default, the Docker file should include the following headers for forwarding:

  • FORWARD_HEADERS: “X-Request-ID, Authorization”

X-Request-ID is the Semantic Objects default correlation id HTTP header.

Semantic Objects

Semantic Objects support the Apollo Federation protocol and can be a part of an Apollo Federation (with some known limitations).

To make the Semantic Objects endpoint federation-ready, you need to enable the following property:

graphql.federation.enabled=true

This will make all necessary additions to the GraphQL schema.

Dynamic Schema Introspection

The Semantic Objects define a custom _Service type that can be used with GraphQL federation. It has two fields:

  • sdl - used by the Apollo federation gateway to fetch the schema with included specific directives needed for the gateway. This field has a value only when Apollo federation is enabled with graphql.federation.enabled=true.
  • full_sdl - can be used to get a full GraphQL schema including all custom directives.

You can use the _service.full_sdl to obtain the schema and develop solutions based on the specific information available there. For example, you can get class and property labels by reading the @descr directives and render them in a web application. You can also create dynamic behavior based on RBAC roles that can be obtained from the @hasRole directives generated in the GraphQL schema.

To fetch a full GraphQL schema including all custom directives like @hasRole and @descr, execute an introspection query like this:

Loading...
https://swapi-platform.ontotext.com/graphql
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImJNODctcWV3SldXTWRMRjIzNUtXUmluTlp4TDA0ZngzIn0.eyJhdWQiOiI2NzJjYTdiMy1jMzcyLTRkZjMtODJkOC05YTFhMGQ3ZDY4YzEiLCJleHAiOjE2OTQ3MTU5MjksImlhdCI6MTY2MzE3NTkyOSwiaXNzIjoic3dhcGktcGxhdGZvcm0ub250b3RleHQuY29tIiwic3ViIjoiZTlmOWQzNGQtZThmMS00ODM1LTlkMzAtOWRjNmU5YjQ4ZmMzIiwianRpIjoiYTY4NzkwNTMtNWUyYy00YjkzLWFiZGItNzEzZGRmOGJiMTlmIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InJlYWRvbmx5dXNlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJSZWFkT25seVVzZXIiLCJhcHBsaWNhdGlvbklkIjoiNjcyY2E3YjMtYzM3Mi00ZGYzLTgyZDgtOWExYTBkN2Q2OGMxIiwicm9sZXMiOlsiUmVhZE9ubHkiXSwiZGF0YSI6e319.imG6XclIEw8A8dhbqRqdOVWv0BqBqshAuXx_DQrEJd8
true
{ _service { full_sdl } }