Integrating Mozaik API into an existing GraphQL schema

by Peter Balazs on 12/03/2018

The main benefit of GraphQL is that you can query all of your data in one request. However, when you want to add a new service to your application you either need to update your existing GraphQL schema and implement all resolvers to map the new service or expose another endpoint. Both cases can result in a lot of extra work. Especially when the new service you want to integrate into your application already has a GraphQL API. This is the problem that you can quickly solve with Apollo schema stitching.

Schema stitching is taking multiple GraphQL schemas and merge them into one endpoint that you can use for querying data from all of them. You can read more about the idea in the Apollo docs. However, probably the easiest to see it in an example, so let's do that. You can find the full project on Github.

Our use case for this example is straightforward: we have a small online bookshop which is great for handling the e-commerce part of our business, but we want to add more editorial content, book reviews, etc. to our site. However we don't want to implement our custom CMS because it would be a lot of work, so we decide to use Mozaik for that.

Our local schema

Just to keep simple, we don't implement a full ecom website, but have a simple setup: we have categories and products. Categories can have subcategories, and a product can be part of multiple categories. So our schema looks like this:

type ProductCategory {
id: ID
name: String
subCategories: [ProductCategory]
parentCategory: ProductCategory
products: [Product]
}
type Product {
id: ID
name: String
categories: [ProductCategory]
}
type Query {
getProduct(id: ID!): Product
getProducts(categoryId: ID): [Product]
getCategory(id: ID!): ProductCategory
getCategories: [ProductCategory]
}

Mozaik content types schema

In Mozaik we need to create two content types, one for the product pages, and one for the category pages. And let's keep them simple for now:

The product page content type has two fields: a product id field which is a single line text input, and a description field which is multi-line text input.

The category page content type is similar to the product page with the following fields: a category id (single line text input), and a description (multi-line text input).

Mozaik will generate the following schema for your project (this is only the part that is important for our example):

interface DocumentInterface {
id: ID!
contentType: DocumentContentTypeEnum
displayName: String
slug: String
currentVersion: DocumentVersion
createdAt: DateTime
updatedAt: DateTime
project: Project
author: User
status: DocumentStatusEnum
content: JSON
liveVersionId: String
latestVersionId: String
lockId: String
firstPublishDate: DateTime
latestPublishDate: DateTime
}
type DocumentList {
pagination: PageInfo
items: [DocumentInterface]
}
enum DocumentContentTypeEnum {
PRODUCT_PAGE
CATEGORY_PAGE
}
type CategoryPageDocument implements DocumentInterface {
id: ID!
contentType: DocumentContentTypeEnum
displayName: String
slug: String
currentVersion: DocumentVersion
createdAt: DateTime
updatedAt: DateTime
project: Project
author: User
status: DocumentStatusEnum
content: JSON
liveVersionId: String
latestVersionId: String
lockId: String
firstPublishDate: DateTime
latestPublishDate: DateTime
categoryId: String
description: String
}
type CategoryPageDocumentList {
pagination: PageInfo
items: [CategoryPageDocument]
}
type ProductPageDocument implements DocumentInterface {
id: ID!
contentType: DocumentContentTypeEnum
displayName: String
slug: String
currentVersion: DocumentVersion
createdAt: DateTime
updatedAt: DateTime
project: Project
author: User
status: DocumentStatusEnum
content: JSON
liveVersionId: String
latestVersionId: String
lockId: String
firstPublishDate: DateTime
latestPublishDate: DateTime
productId: String
description: String
}
type ProductPageDocumentList {
pagination: PageInfo
items: [ProductPageDocument]
}
type Query {
document(id: ID, slug: String, contentType: DocumentContentTypeEnum, versionId: ID, withLatestPublishedVersion: Boolean = true, predicate: PredicateInput): DocumentInterface
documents(pageSize: Int = 20, page: Int = 1, types: [DocumentContentTypeEnum], predicates: PredicateInput, sortBy: [SortByInput], publishedVersionsOnly: Boolean = true): DocumentList
getProductPageDocument(id: ID, slug: String, versionId: ID, withLatestPublishedVersion: Boolean = true, predicate: PredicateInput): ProductPageDocument
getProductPageDocumentList(pageSize: Int = 20, page: Int = 1, predicates: PredicateInput, sortBy: [SortByInput], publishedVersionsOnly: Boolean = true): ProductPageDocumentList
getCategoryPageDocument(id: ID, slug: String, versionId: ID, withLatestPublishedVersion: Boolean = true, predicate: PredicateInput): CategoryPageDocument
getCategoryPageDocumentList(pageSize: Int = 20, page: Int = 1, predicates: PredicateInput, sortBy: [SortByInput], publishedVersionsOnly: Boolean = true): CategoryPageDocumentList
}

To merge our schemas we need four steps:

  1. Make our local schema executable
  2. Make the Mozaik schema executable locally
  3. Define links between the two schemas
  4. Merge schemas

Making our local schema executable

As we used the GraphQL Schema Definition Language to define our schema, we can make it executable by using the makeExecutableSchema function from Apollo's graphql-tools. This function takes the type definitions and resolver functions as parameters and generates a Graphql.js GraphqlSchema instance.

import { makeExecutableSchema } from 'graphql-tools';
import products from '../data/products';
import categories from '../data/categories';
const typeDefs = `
type ProductCategory {
	id: ID
	name: String
	subCategories: [ProductCategory]
	parentCategory: ProductCategory
	products: [Product]
}
type Product {
	id: ID
	name: String
	categories: [ProductCategory]
}
type Query {
	getProduct(id: ID!): Product
	getProducts(categoryId: ID): [Product]
  getCategory(id: ID!): ProductCategory
	getCategories: [ProductCategory]
}
`;
const resolvers = {
Query: {
  getProduct(_, { id }) {
    return products.find(p => p.id === id);
  },
  getProducts(_, { categoryId }) {
    if (!categoryId) {
      return products;
    }
    return products.filter(p => p.categoryId === categoryId);
  },
  getCategory(_, { id }) {
    return categories.find(c => c.id === id);
  },
  getCategories() {
    return categories;
  }
},
ProductCategory: {
  subCategories(category) {
    return categories.filter(c => c.parentCategory === category.id);
  },
  parentCategory(category) {
    if (!category.parentCategory) {
      return null;
    }
    return categories.find(c => c.id === category.parentCategory);
  },
  products(category) {
    return products.filter(p => p.categories.includes(category.id));
  }
},
Product: {
  categories(parent) {
    const product = products.find(p => p.id === parent.id);
    console.log(product);
    const result = product.categories.map(categoryId =>
      categories.find(c => c.id === categoryId)
    );
    console.log(result);
    return result;
  }
}
};
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
export default schema;

Making the Mozaik schema executable locally

For this we need four steps:

  1. Create an API access token in Mozaik so we can authorize the API request. As this access token will be used for querying the API, it must have at least a document read permission. (Of course if later we want to execute mutations as well the token must have document write permission too).
  2. Create a link which is a function that can retrieve GraphQL results. We can use the HttpLink from the apollo-link-http package.
  3. Use the introspectSchema function from graphql-tools package to get the schema from Mozaik.
  4. Use the makeExecutableRemoteSchema function to create a schema that uses the link to delegate requests to the Mozaik API.
import { HttpLink } from 'apollo-link-http';
import { introspectSchema, makeRemoteExecutableSchema } from 'graphql-tools';
import fetch from 'node-fetch';
const link = new HttpLink({
uri: 'https://api.mozaik.io/graphql/mozaik-api-integration',
fetch,
headers: {
  'Content-Type': 'application/json',
  Authorization: 'Bearer 9e096fe527db3f3d4c2adbdc9c7dfbb2ef557abc'
}
});
export default async function createMozaikSchema() {
const schema = await introspectSchema(link);
const executableSchema = makeRemoteExecutableSchema({
  schema,
  link
});
return executableSchema;
}

At this point, we would be ready to merge our two schemas:

import { mergeSchemas } from 'graphql-tools';
import localSchema from './local-schema';
import createMozaikSchema from './mozaik-schema';
export default async function createMergeSchemas() {
const mozaikSchema = await createMozaikSchema();
const schema = mergeSchemas({
  schemas: [localSchema, mozaikSchema]
});
return schema;
}

As a result, we could query any data that are either local or served from Mozaik. In some cases, this can be enough, but we need to get a product page based on a product id. For this, we can add resolvers (links) between schemas.

Defining links between schemas

To enable links between types, we need to extend existing types with fields that can take you from one schema to the other. We can do this the same way we define our schema:

const linkTypeDefs = `
extend type Product {
  page: ProductPageDocument
}
extend type ProductCategory {
  page: CategoryPageDocument
}
`;

Of course, just by extending existing types we won't be able to query the new fields because the merged schema doesn't know how to resolve these fields. We have to define our implementation for this.

Resolvers added as part of the mergeSchemas have access a delegate function that allows you to delegate to root fields. Let's see the full implementation first:

function resolvers(mergeInfo) {
return {
  Product: {
    page: {
      fragment: `fragment ProductFragment on Product { id }`,
      resolve(parent, args, context, info) {
        return mergeInfo.delegate(
          'query',
          'getProductPageDocument',
          {
            predicate: {
              type: 'IS',
              key: 'productId',
              values: parent.sku
            }
          },
          context,
          info
        );
      }
    }
  },
  ProductCategory: {
    page: {
      fragment: `fragment CategoryFragment on Category { id }`,
      resolve(parent, args, context, info) {
        return mergeInfo.delegate(
          'query',
          'getCategoryPageDocument',
          {
            predicate: {
              type: 'IS',
              key: 'categoryId',
              values: parent.id
            }
          },
          context,
          info
        );
      }
    }
  }
};
}

So we define a resolver for the page field that we added to the Product type. We need the id of the product to be able to get the relevant document from Mozaik. We can define a fragment for this resolver where we can specify all required fields for this function. In the resolver function, we delegate this resolver to a root query: the operation is "query", the rootFieldName is "getProductPageDocument", and the arguments is a predicate to find the document where the product id is the id of the parent product.

We have to update our createMergeSchamas function:

export default async function createMergeSchemas() {
const mozaikSchema = await createMozaikSchema();
const schema = mergeSchemas({
  schemas: [localSchema, mozaikSchema, linkTypeDefs],
  resolvers
});
return schema;
}

As a result, we have a full schema that can query both the local and Mozaik API and can easily navigate between the two schemas based on the links we defined. Let's see an example query:

Conclusion

Merging different GraphQL schemas are simple thanks to Apollo, and opens up a lot of opportunities. This small example above is just for showcasing how easy to integrate Mozaik with your existing schema. But you can quickly create a webshop, where your product catalogue and other content are in Mozaik, and you just need to implement the business logic for managing users' baskets, orders and shipments.