A Guide to Common Resolver Patterns
145 votes
40519 views

Introduction

This tutorial gives an overview about common scenarios you might encounter when implementing your GraphQL server with graphql-yoga and Prisma.

Scenario: Add a custom/computed field to a Prisma model via the application schema (Prisma bindings)

Assume you have the following Prisma datamodel, commonly called datamodel.prisma:

type User {
  id: ID! @id
  firstName: String!
  lastName: String!
}

In the GraphQL API of your application layer, you now want to expose a computed field, e.g. the fullName that's composed of the firstName and lastName.

In that case, you need to redefine the User type in your application schema, commonly called schema.graphql:

type Query {
  users: [User!]!
}

type User {
  id: ID!
  firstName: String!
  lastName: String!
  fullName: String!
}

Now you need to implement the resolvers, here's a naive way to implement it:

const resolvers = {
  Query: {
    users: (parent, args, ctx, info) => {
      return ctx.db.query.users({}, `{id firstName lastName}`)
    },
  },
  User: {
    fullName: parent => `${parent.firstName} ${parent.lastName}`,
  },
}

This will work but it's very error-prone and especially doesn't scale for more complex use cases. Instead, you should be using fragment replacements which will ensure that firstName and lastName will always be fetched, no matter what the info object looks like:

const { addFragmentToInfo } = require('graphql-binding')

const resolvers = {
  Query: {
    users: (parent, args, ctx, info) => {
      const fragment = `fragment EnsureFullName on User { firstName lastName }`
      return ctx.db.query.users({}, addFragmentToInfo(info, fragment))
    },
  },
  User: {
    fullName: parent => `${parent.firstName} ${parent.lastName}`,
  },
}

For more info about what's going on, check out this article on the info object.

Scenario: Implementing relations with Prisma client

Assume you want to implement the following application schema using Prisma client:

type User {
  id: ID!
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  published: Boolean!
  author: User!
}

type Query {
  posts: [Post!]!
}

type Mutation {
  createDraft(title: String!, authorId: ID!): Post!
  publish(id: ID!): Post
}

Assume your Prisma datamodel is defined as follows:

type User {
  id: ID! @id
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID! @id
  title: String!
  published: Boolean! @default(value: false)
  author: User!
}

The implementation of the root resolvers posts, createDraft and publish can be done as follows (assuming a Prisma client instance is available as prisma):

const resolvers = {
  Query: {
    posts() {
      return prisma.posts()
    },
  },
  Mutation: {
    createDraft(_, args) {
      return prisma.createPost({
        title: args.title,
        author: {
          connect: {
            id: args.authorId,
          },
        },
      })
    },
    publish(_, args) {
      return prisma.updatePost({
        data: {
          published: true,
        },
        where: {
          id: args.id,
        },
      })
    },
  },
}

This implementation seems fairly straightforward, but it has a subtle bug that's related to the relation between User and Post. For example, assume you send the following query to the API:

query {
  posts {
    title
    author {
      name
    }
  }
}

With the current resolver implementation, the author relation of a Post object can not be resolved! This is because the posts resolver returns a list of Post objects where each Post object only contains scalar values - no relations! The author fields are not fetched by the client when invoking the posts method as is done in the posts resolver.

The way to resolve this situation is to introduce "type resolvers" for the Post and User types with explicit resolvers for the relation fields.

Here is how you need to adjut the resolvers object from before to add the type resolvers:

const resolvers = {
  Query: {
    // ... like before
  },
  Mutation: {
    // ... like before
  },
  User: {
    posts(parent) {
      return prisma.user({ id: parent.id }).posts()
    },
  },
  Post: {
    author(parent) {
      return prisma.post({ id: parent.id }).author()
    },
  },
}

When the GraphQL server now receives the nested query from before, it will not only invoke the posts resolver, but now it can also invoke the author resolver to fetch the author for each Post object. Note that the parent argument that's passed into each resolver is the return value of the previous resolver execution level. You can learn more about the query resolution process and the resolver arguments in this article.

Scenario: Add a new field to the data model and expose it in the GraphQL API via Prisma bindings

This scenario is based on the typescript-basic GraphQL boilerplate project.

Adding a new address field to the User type in the database, with the purpose of exposing it in the application API as well.

Instructions

1. Adding the field to the data model

in database/datamodel.graphql:

type User {
  id: ID! @id
  email: String! @unique
  password: String!
  name: String!
  posts: [Post!]!
+ address: String
}

2. Deploying the updated data model

prisma deploy

This will...

  • ... deploy the new database structure to the local service
  • ... download the new GraphQL schema for the database to database/schema.graphql

3. Adding the field to the application schema

In src/schema.graphql:

type User {
  id: ID!
  email: String!
  name: String!
  posts: [Post!]!
+ address: String
}

Scenario: Adding a new resolver

Suppose we want to add a custom resolver to delete a Post.

Instructions

Add a new delete field to the Mutation type in src/schema.graphql

type Mutation {
  createDraft(title: String!, text: String): Post
  publish(id: ID!): Post
+ delete(id: ID!): Post
}

Add a delete resolver to Mutation part of src/index.js

delete(parent, { id }, ctx, info) {
  return ctx.db.mutation.deletePost(
  {
    where: { id }
  },
    info
  );
}

Start the server with yarn start.

Then you can run the following mutation to delete a post:

mutation {
  delete(id: "__POST_ID__") {
    id
  }
}