Extend the brX GraphQL Service using Apollo Federation - Bloomreach Experience - Open Source CMS

Extend the brX GraphQL Service using Apollo Federation

This Bloomreach Experience Manager feature requires a standard or premium license. Please contact Bloomreach for more information.

Introduction

The brX GraphQL Service provides a single data graph modeling online shopping. Considering the complexity of eCommerce nowadays, it is challenging to represent several facets with a single graph. The brX GraphQL Service is ready to be extended according to the Apollo Federation principles.

As example, retailers may be interested in combinining product data - powered by the brX GraphQL Service - with additional content data: in particular, marketeers may want to provide extensive articles explaining how to get the most out of that product.

The next paragraphs describe a very basic example of graph federation in action, mostly related to the use-case provided above. The example is based on the Apollo platform.

Prerequisites

Make sure that the brX GraphQL Service is up and running: you may want to read the How to install the GraphQL Service documentation page first.

Setup

Create a new separate folder (e.g. graphql-service-federation), open a shell/terminal session on that folder and initialize an empty NPM project:

npm init -y 

A new package.json file is created with the default configurations. Let's install the required dependencies:

npm install @apollo/gateway apollo-server apollo-server-express express graphql

The implementing services

This basic example is composed of two implementing services:

  1. The brX GraphQL Service, exposing commerce functionalities. It should be already up and running (e.g. http://localhost:4000/graphql);
  2. The Article Service, providing extensive information (e.g. how-to, guides) regarding products.

For the sake of simplicity, let's assume that the Article Service is a GraphQL Server responsible for resolving very basic articles, containing just one description field. You may want to increase the complexity of this service later on, maybe connecting to an external REST API. 

Create a new index.js file and paste the following snippet:

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

const typeDefs = gql`
  type Article {
    description: String!
  }

  type ItemId {
    id: String!
    code: String
  }

  extend type Item @key(fields: "itemId") {
    "The ItemId of a product item"
    itemId: ItemId! @external
    "Related article"
    article: Article
  }
`;

const resolvers = {
    Article: {
        description() {
            return 'Hey, this is an article description';
        }
    },
    Item: {
        article() { 
            return {};
        }
    }
}

const articleService = new ApolloServer({
    schema: buildFederatedSchema([{ typeDefs, resolvers }]),
});

articleService.listen({ port: 4001}, () => { 
   console.log(`Article Service ready at http://localhost:4001`);
});

The Article Service graph definition extends the Item type, already defined in the brX GraphQL Service. This new definition links the Item to the related Article: it would be possible to "navigate" from a product to the related article description, using the same GraphQL request.

The gateway

Once the implementing services are ready to be federated, you can setup the gateway. This layer is responsible for composing the individual schema in a single - federated - graph. Once the gateway is up and running, client applications can execute GraphQL operations against this new instance: it would be possible to execute GraphQL requests on the composed schema.

For simplicity, you can - properly - append the following snippet to the existing index.js file: 

const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');

class CommerceConnectorDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    if (context.connector) {
        request.http.headers.set('connector', context.connector);
    }
    if (context.authorization) {
        request.http.headers.set('authorization', context.authorization);
    }
  }
}

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'graphql-service', url: 'http://localhost:4000/graphql' },
    { name: 'article-service', url: 'http://localhost:4001' },
  ],
  buildService({ name, url }) {
    return new CommerceConnectorDataSource({ url });
  },
});

The gateway instantiation is straightforward, except for the buildService option. By default, an Apollo Gateway instance does not forward incoming HTTP headers to the implementing services. This behaviour could be problematic for the brX GraphQL Service, since it may require the authorization and connector headers.

The Apollo Gateway provides the buildService function: if specified, it is invoked once for each implementing service. In the example above, the CommerceConnectorDataSource class forwards both the authorization and connector headers: it reads the values from the context and, if those exist, set them accordingly as part of the outgoing HTTP request. 

Additional changes

The brX GraphQL Service is a full-fledged application: next to the Apollo Server, the service provides an Access Management layer enabling client authentication/authorization. Considering that the underlying service only accepts authorized requests, it may be an option to re-expose the sign-in and sign-out endpoints at gateway level. You can - properly - append the following snippet to the existing index.js file: 

const express = require('express');
const { ApolloServer: ApolloServerExpress } = require('apollo-server-express');

const server = new ApolloServerExpress({
  gateway,
  // Disable subscriptions (not currently supported with ApolloGateway)
  subscriptions: false,
  context: ({ req }) => {
    return { 
      connector: req.headers.connector,
      authorization: req.headers.authorization,
    };
  },
});

const app = express();

app.post('/signin', function(req, res) {
  res.redirect(307, 'http://localhost:4000/signin');
});
app.post('/signout', function(req, res) {
  res.redirect(307, 'http://localhost:4000/signout');
});

server.applyMiddleware({ app });

app.listen({ port: 4999 }, () => { 
  console.log(`🚀 Gateway ready at http://localhost:4999`);
});

If a client triggers a sign-in request against the gateway, it is simply redirected to the related brX GraphQL Service endpoint.
Additionally, as discussed in the previous paragraph, the server application is responsible for initializing the context with the expected properties: in the example above both the authorizion and connector headers are added.

Run

If everything goes well, you should have an index.js file like the following:

const { ApolloServer, gql } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');
const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
const express = require('express');
const { ApolloServer: ApolloServerExpress } = require('apollo-server-express');

const typeDefs = gql`
  type Article {
    description: String!
  }

  type ItemId {
    id: String!
    code: String
  }

  extend type Item @key(fields: "itemId") {
    "The ItemId of a product item"
    itemId: ItemId! @external
    "Related article"
    article: Article
  }
`;

const resolvers = {
    Article: {
        description() {
            return 'Hey, this is an article description';
        }
    },
    Item: {
        article() {
            return {};
        }
    }
}

const articleService = new ApolloServer({
    schema: buildFederatedSchema([{ typeDefs, resolvers }]),
});

articleService.listen({ port: 4001}, () => {
   console.log(`Article Service ready at http://localhost:4001`);
});

class CommerceConnectorDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    if (context.connector) {
        request.http.headers.set('connector', context.connector);
    }
    if (context.authorization) {
        request.http.headers.set('authorization', context.authorization);
    }
  }
}

const gateway = new ApolloGateway({
  serviceList: [
    { name: 'graphql-service', url: 'http://localhost:4000/graphql' },
    { name: 'article-service', url: 'http://localhost:4001' },
  ],
  buildService({ name, url }) {
    return new CommerceConnectorDataSource({ url });
  },
});

const server = new ApolloServerExpress({
  gateway,
  // Disable subscriptions (not currently supported with ApolloGateway)
  subscriptions: false,
  context: ({ req }) => {
    return {
      connector: req.headers.connector,
      authorization: req.headers.authorization,
    };
  },
});

const app = express();

app.post('/signin', function(req, res) {
  res.redirect(307, 'http://localhost:4000/signin');
});
app.post('/signout', function(req, res) {
  res.redirect(307, 'http://localhost:4000/signout');
});

server.applyMiddleware({ app });

app.listen({ port: 4999 }, () => {
  console.log(`🚀 Gateway ready at http://localhost:4999`);
});

Execute the application above with the following command:

node index.js 

Once the application has started, you should see the following logs:

Article Service ready at http://localhost:4001
🚀 Gateway ready at http://localhost:4999 

The GraphQL playground should be available at http://localhost:4999/graphql.

Query example

Now you are able to execute a GraphQL request against the federated schema:

query findItemsByKeyword {
  findItemsByKeyword(text: "", offset: 0, limit: 5) {
    items {
      itemId {
        id
        code
      }
      displayName
      article {
        description
      }
    }
    total
  }
}

Do not forget to include the following HTTP headers:

​{"connector": "<CONNECTOR_ID>", "authorization": "Bearer <AUTHORIZATION_TOKEN>"}
Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?