DEV Community

Cover image for GraphQL Interfaces (and Union Types) with Prisma and Yoga
Thibaut Tiberghien
Thibaut Tiberghien

Posted on • Edited on

GraphQL Interfaces (and Union Types) with Prisma and Yoga

Originally posted on Medium on Apr 2, 2018.
Photo by Clint Adair on Unsplash.

What is GraphQL?

GraphQL is an API query language that came out of the Facebook team and has been taking over the internet recently. It gets its strength from being built around a strongly typed API contract which defines exhaustively the data in your API as well as its schema, how to request for it, and so on. It supports deeply nested querying with controlled hydration and lets API clients combine data from different sources or models, all into a single query. With GraphQL, you get exactly the data you want, formatted the way you want, and in a single query, solving several problems of traditional REST APIs. Moreover, the API contract concept enables a wide variety of powerful developer tools, some of which I describe below.


My GraphQL Stack

  • Prisma, by the amazing team at Graphcool, is sort of a GraphQL ORM, it takes your data schema defined in the SDL (Schema Definition Language) and generates a database and API for it. The extensiveness of the generated API for (nested) CRUD operations is just amazing. You can deploy your database service in their cloud or using docker on your infrastructure. On top of this, Prisma comes with bindings which provide a convenience layer for building GraphQL servers on top of Prisma services.
  • graphql-yoga, also by Graphcool (these guys are on šŸ”„), is the simplest way to build GraphQL servers. It is based on or compatible with most of the de facto standard libraries for building GraphQL servers in Javascript, but it takes the angle of improving developer experience by making everything easier to setup, with sensible defaults and a more declarative approach to configuration. It covers more or less the whole GraphQL spec, even up to WebSockets support for Subscriptions.
  • GraphQL Playground, also by Graphcool (wuuut? šŸ˜±), is a web-based GraphQL client / IDE which supercharges your development workflow by introspecting your API contract to provide an automatic and interactive documentation for it as well as a query interface with auto-completion and validation against your schema. It is packed with nifty little features and is a go-to tool for anything GraphQL.
  • Apollo Client, by the geniuses at Apollo, is probably the best GraphQL client available. It is compatible with every major frontend platform, and focuses on getting your data inside UI components without taking care of all the plumbing to get it. I love its declarative data fetching approach for React, and the advanced data loading features it supports. e.g. caching, loading, optimistic UI, pagination, etc. The devtools are a great addition to your developer experience as well.

Now to Interfacesā€¦

Some Context

The GraphQL schema specification supports Interfaces and Union Types. An Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface, while Union Types allow for the grouping of several types without the sharing of any structure.

For any non-trivial data structure, you will most probably need to leverage these constructs to model your data. The problem is:

  1. Prisma does not support Interfaces or Union Types yet. There are open issues for each of them ā€” see Interface and Union Type.
  2. graphql-yoga supports both of them but their usage is not yet documented, which makes it hard to actually implement anything. I opened an issue to know more a while back and this post is where it led me.

My approach

Since Prisma only supports Types and Enums at the moment, we have to find a way to model our data without using Interfaces in Prisma. We can however use Interfaces on the GraphQL server (graphql-yoga) so that the client facing API is properly structured and users get to request data across types using Inline Fragments.

This leaves us with 2 options:

  1. Storing all data with optional type-specific fields under one type (the interface) in Prisma, and then splitting the data back between the primitive types in the app server.
  2. Storing the data in each primitive type on Prisma, and stitching things for queries on the app server.

The problem with option 2 is that you loose the consistency in pagination. How do you get the last 20 items for the interface? How many of each primitive type should you request? You could do 20, sort them, and take 20, but that seems inelegant to me.

So I picked option 1, letā€™s see how to implement it. Iā€™ll give code snippets following the schema used in the docs.

Prisma Workaround

Basically, we want to merge all primitive types as a single ā€œinterfaceā€ type. Type-specific fields must be optional since they will not be available for every entry, and they are prefixed with the name of the primitive type to make sure they are unique. In the docs, we have:

# datamodel.graphql
interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

Our workaround schema is:

# datamodel.graphql
type DbCharacter {
  # interface
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  # custom fields: Human
  human_starships: [Starship]
  human_totalCredits: Int
  #custom fields: Droid
  droid_primaryFunction: String
}

Mapping interfaces in graphql-yoga

As desired, we declare in the schema for the client facing API the same interface and primitive types as in the docs. We also copy the schema of the dbCharacters query generated by Prisma as the characters query for our client facing API. This could probably be more refined. The return type is however changed to our interface, hence returned items should be mapped to a primitive type on which type-specific inline fragments can be used.

# src/schema.graphql
# import {} from "./generated/prisma.graphql"

type Query {
  characters(
    where: DbCharacterWhereInput
    orderBy: DbCharacterOrderByInput
    skip: Int
    after: String
    before: String
    first: Int
    last: Int
  ): [Character!]!
}

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

type Human implements Character {
  # interface
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  # custom fields
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  # interface
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  # custom fields
  primaryFunction: String!
}

In order to map items returned by Prisma to a primitive type, we need to provide a type resolver for our interface at the root of our resolvers object. I have separated the declaration of interface resolvers into a separate file and import it with object destructuring into the resolvers object. See the __resolveType example in the interfaces.js file. This is a simplistic example showcasing how to resolve types. You would implement yours according to the specific business logic of your data.

// src/resolvers/index.js
const { interfaces } = require('./interfaces')
const { Query } = require('./Query')

module.exports = {
  ...interfaces,
  Query
}
// src/resolvers/interfaces.js
const interfaces = {
  Character: {
    __resolveType (obj) {
      // resolve the type of the incoming interface data
      if (obj.primaryFunction) {
        return 'Droid'
      } else {
        return 'Human'
      }
    }
  }
}

module.exports = { interfaces }

The last thing to do is to implement the client API for the interface. It is backed by the corresponding API from Prisma but we need to translate I/Os between the 2 schemas. The resolver for the characters query is implemented in the Query.js file, which is pretty classic. The implementation details are as follow:

  1. We must make sure all fields selected for the primitive types in the query are requested from Prisma. To do this I have written a utility function called makeSelection into interfaces.js which takes the info object from the resolver and parses the query AST (GraphQLResolveInfo) to generate the string selection sent to Prisma. This modifies the selection to make sure all fields nested in Inline Fragments such as ...on Droid { primaryFunction } will be queried from Prisma as normal prefixed fields, e.g. droid_primaryFunction. The code for this method was pretty much trial and error while inspecting the info object and mapping it to the expected selection to send to Prisma. Disclaimer: the code covers only the queries I have have been needing and might need additions to cover all use-cases. Note also that Iā€™m not an expert with ASTs so there might be a better way to do this, please suggest in the comments if you know one.
  2. We must format the objects received from Prisma back to their expected form in the client API schema. I use another utility function called formatPrimitiveFields, also available in interfaces.js which takes a field such as droid_primaryFunction and remove the primitive type prefix.
// src/resolvers/Query.js
const { makeSelection, formatPrimitiveFields } = require('./interfaces')

const Query = {
  characters (parent, args, ctx, info) {
    return ctx.db.query
      .dbCharacters(
        args,
        makeSelection(info)
      )
      .then(formatPrimitiveFields)
  }
}

module.exports = { Query }
// src/resolvers/interfaces.js
const R = require('ramda')

const interfaces = {...}

const unpackSelectionFromAST = R.map(s => {
  switch (s.kind) {
    case 'Field':
      if (!s.selectionSet) {
        return s.name.value
      } else {
        return `${s.name.value} { ${unpackSelectionFromAST(
          s.selectionSet.selections
        )} }`
      }
    case 'InlineFragment':
      switch (s.typeCondition.kind) {
        case 'NamedType':
          return R.compose(
            R.map(field => `${R.toLower(s.typeCondition.name.value)}_${field}`),
            R.reject(R.startsWith('__')), // apollo client compatibility (__typename)
            unpackSelectionFromAST
          )(s.selectionSet.selections)
        default:
          console.error(`${s.typeCondition.kind} unknown in selections AST`)
          break
      }
      break
    default:
      console.error(`${s.kind} unknown in selections AST`)
      break
  }
})

const makeSelection = (info) =>
  R.compose(
    fields => `{ ${R.join(',')(fields)} }`,
    R.reject(R.isNil),
    R.flatten,
    unpackSelectionFromAST,
    R.prop('selections'),
    R.prop('selectionSet'),
    R.head,
    R.prop('fieldNodes')
  )(info)

const formatPrimitiveFields = R.map(
  R.compose(
    R.fromPairs,
    R.map(([k, v]) => [R.replace(/^.*_/, '', k), v]),
    R.toPairs
  )
)

module.exports = { interfaces, makeSelection, formatPrimitiveFields }

Unions Types are not directly covered in this post but they are pretty similar to the __resolveType approach for Interfaces.

Code snippets are written for node 8 and above.

If youā€™re using Apollo Client, note that interfaces and unions in inline fragments are not resolved properly out of the box. You need to setup a custom fragment matcher based on the api schema. This is explained in details in the docs.

Top comments (0)