node package manager
Loosely couple your services. Use Orgs to version and reuse your code. Create a free org »

relay-composite-network-layer

RelayCompositeNetworkLayer

The RelayCompositeNetworkLayer is a Relay Network Layer which can be made of many different Network Layers each with their own schema. This is accomplished by merging multiple schemas into a single schema. Relay then generates appropriate queries using this schema while the RelayCompositeNetworkLayer splits and sends the queries by schema.

The main use case for this is allowing a local and server schema.

Installation

npm install relay-composite-network-layer

It has a peer dependency on react-relay (the version is pretty strict at the moment but probably could be loosened!).

Usage

The first step is merging the schema. You can add a build step or make it part of your update-schema script.

import {createCompositeSchema} from 'relay-composite-network-layer/lib/merge';
 
const {schema,config} = createCompositeSchema({
  // name / value pairs of schemas 
  server: serverSchema,
  local: localSchema
}, {
  // names for the query and mutation type of the output schema 
  // these can be the same names as your input schemas 
  queryType: 'Query',
  mutationType: 'Mutation'
});

The outputs of schema and config need to be saved to json files for consumption. Configure the babelRelayPlugin to use the saved merged schema. With the schemas merged and Relay configured you should be able to write / parse queries which hit multiple schemas.

A full script example is at the end of the README.

The next step is to create the RelayCompositeNetworkLayer on the client.

import RelayCompositeNetworkLayer from 'relay-composite-network-layer';
 
const compositeNetworkLayer = new RelayCompositeNetworkLayer({
  // config is the output of the `createCompositeSchema` function 
  ...config,
  // key / value pairs of schema 
  // names need to match the call to `createCompositeSchema` 
  layers: {
    server: new Relay.DefaultNetworkLayer('/graphql'),
    local: new RelayLocalSchema.NetworkLayer({schema: localSchema})
  }
});

Here we are creating a composite network layer which has two schemas. The server schema uses the default network layer and makes network requests to the /graphql endpoint. The local schema uses the RelayLocalSchema library to execute graphql-js on the client.

Finally inject the network layer into Relay.

Relay.injectNetworkLayer(compositeNetworkLayer);

That should be it!

Limitations

The main limitations are around merging.

You can only merge node interface objects. This means if you define User in multiple schemas each schema User needs an id field and needs to be able to be fetched from the node root query field. Other objects which appear in multiple schemas must be equivalent (same fields) or the merge will fail.

Enum's, Scalar's, and Input Object's also must be equivalent if they are found with the same name in multiple schemas.

Union's and Interface's, with the exception of the node interface, are not allowed to be in multiple schemas.

Fields on the Query, Mutation, and Subscription field are also merged. These names must be unique. For example most schemas have a viewer type but this field must only exist in one schema. Other schemas navigate to the viewer by querying node with the viewer id.

Some of these restrictions could be lifted -- I just haven't thought around the use cases!

Example

I'm going to use the example from the github issue for relay local data: https://github.com/facebook/relay/issues/114

We have the following query:

query {
  viewer {
    name,                     # server field
    drafts(first: 10) {       # client-only field
      edges { node { title } }
    }
  }
}

This means we have the following two schemas.

Server

type User : Node {
  id: ID!
  name: String
}
 
type Query {
  viewer: User
  node: Node
}

Local

type User : Node {
  id: ID!
  drafts: DraftConnection
}
 
type Draft : Node {
  id: ID!
  title: String
}
 
type DraftConnection {
  edges: [DraftEdge]
}
 
type DraftEdge {
  node: Draft
}
 
type Query {
  node: Node
}

The type User is defined in both schemas. Both schemas also have a query root type named Query but the names do not have to match. Only the server schema has the viewer field.

When merged we get a third schema.

Composite

type User : Node {
  id: ID!
  name: String
  drafts: DraftConnection
}
 
type Draft : Node {
  id: ID!
  title: String
}
 
type DraftConnection {
  edges: [DraftEdge]
}
 
type DraftEdge {
  node: Draft
}
 
type Query {
  viewer: User
  node: Node
}

This schema presents a unified view of the two schemas which Relay can use to generate queries.

Lets follow the execution of the query.

query {
  viewer {
    name,                     # server field
    drafts(first: 10) {       # client-only field
      edges { node { title } }
    }
  }
}

The composite network layer first splits the query by schema. This results in multiple queries with some being dependent on the result of others.

q1 server

query {
  viewer {
    id
    name
  }
}

This query is not dependent on anything.

q2 local

query {
  node(id: $id) {
    id
    __typename
    ... on User {
      drafts(first: 10) {
        edges { node { title } }
      }
    }
  }
}

This query is dependent on the viewer.id result from q1. It will be run sequentially following q1.

The queries are then executed.

q1

{
  data: {
    viewer: {
      id: 1,
      name: 'Huey'
    }
  }
}

q2

{
  data: {
    node: {
      id: 1,
      __typename: 'User',
      drafts: {
        edges: [{
          node: { title: 'Taste Javascript'},
          node: { title: 'Paint a self portrait'}
        }]
      }
    }
  }
}

Finally the results are merged and passed back to Relay.

{
  data: {
    viewer: {
      id: 1,
      __typename: 'User',
      name: 'Huey',
      drafts: {
        edges: [{
          node: { title: 'Taste Javascript'},
          node: { title: 'Paint a self portrait'}
        }]
      }
    }
  }
}

Relay cares not that part of the query has been local and part of it has been remote.

You could also add an author field to Draft and query back and forth between local and server:

query {
  viewer {                   # server field
    selectedDraft {          # local field
      author {               # server field
        selectedDraft {
          author {
            selectedDraft {
              author {
                name
              }
            }
          }
        }
      }
    }
  }
}

This will generate a bunch of sequential queries so be careful!

Mutations

Mutations are treated just like queries. As far as I know Relay only allows a single field on a mutation so no extra work is needed at the network layer for coordination. The mutation field schema is looked up and the mutation is sent to the proper schema network layer. The mutation payload is then just like a query and can come from multiple schemas.

mutation {
  addDraft(input: $input) {   # client field
    author {
      name                    # server field
      draftCount
    }
    edge {
      node {
        title
      }
    }
  }
}

Merge Script

Here is the full script:

import fs from 'fs';
import path from 'path';
 
import localSchema from '../data/local/schema.json';
import serverSchema from '../data/server/schema.json';
 
import {createCompositeSchema} from 'relay-composite-network-layer/lib/merge';
 
const {schema,config} = createCompositeSchema({
  server: serverSchema,
  local: localSchema
}, {
  queryType: 'Query',
  mutationType: 'Mutation'
});
 
fs.writeFileSync(
  path.join(__dirname, '../data/', 'schema.json'),
  JSON.stringify(schema, null, 2)
);
 
fs.writeFileSync(
  path.join(__dirname, '../data/', 'config.json'),
  JSON.stringify(config, null, 2)
);

TODO

  • chained mutation

    • AddTodo / DeleteDraft
  • remove graphql as a peer