GraphQL Federation Tutorial

The following sections provide examples of implementing GraphQL Federation using Ontotext Platform. To start the federation service follow the Getting Started guide.

The examples in this section 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 Object Service 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 SOaaS 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 SOaaS 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 Service 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 SOaaS 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 SOaaS 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 SOaaS 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, SOaaS that extends 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 SOaaS combined in the response.