henk
henk

Reputation: 2838

Which is the correct resolver function approach?

I would like to clarify which approach I should use for my resolver functions in Apollo + GraphQL

Let's assume the following schema:

type Post {
  id: Int
  text: String
  upVotes: Int
}

type Author{
  name: String
  posts: [Post]
}

schema {
  query: Author
}

The ApoloGraphql tutorial suggests a resolver map like this:

{Query:{
    author(_, args) {
      return author.findAll()
          }
    }
},
Author {
   posts: (author) => author.getPosts(),
}

As far as I know, every logic regarding posts e.g. get author with posts where the count of post upVotes > args.upVotes, must be handled in the author method. That gives us the following resolver map:

{Query:{
    author(_, args) {
      return author.findAll({
              include:[model: Post]
              where: {//post upVotes > args.upVotes}
                })
             }
},
Author {
   posts: (author) => author.getPosts(),
}

Calling author, will first select the author with posts in one joined query, where posts are greater than args.upVotes. Then it will select the posts for that author again, in an additional query because of Author ... getPosts()

Technically, I can reach the same result by removing Author, since posts are already included in the small author method.

I have the following questions:

  1. Do I need this statement? In which cases?

    Author { posts: (author) => author.getPosts(), }

  2. If no, then how can I find out if the posts field was requested so that I can make the posts include conditionally, depending not only on the arguments, but also on the requested fields?

  3. If yes, which posts will contain the final result? Posts from the include statement, or the getPosts()?

Upvotes: 1

Views: 420

Answers (1)

Daniel Rearden
Daniel Rearden

Reputation: 84677

The resolver map you included in your question isn't valid. I'm going to assume you meant something like this for the Author type:

Author {
  posts: (author) => author.getPosts(),
}

As long as your author query always resolves to an array of objects that include a posts property, then you're right in thinking it doesn't make sense to include a customer resolver for the posts field on the Author type. In this case, your query's resolver is already populating all the necessary fields, and we don't have to do anything else.

GraphQL utilizes a default resolver that looks for properties on the parent (or root) object passed down to the resolver and uses those if they match the name of the field being resolved. So if GraphQL is resolving the posts field, and there is no resolver for posts, by default it looks at the Author object it's dealing with, and if there is a property on it by the name of posts, it resolves the field to its value.

When we provide a custom resolver, that resolver overrides the default behavior. So if your resolver was, for example:

posts: () => []

then GraphQL would always return an empty set of posts, even if the objects returned by author.findAll() included posts.

So when would you need to include the resolver for posts?

If your author resolver didn't "include" the posts, but the client requested that field. Like you said, the problem is that we're potentially making an unnecessary additional call in some cases, depending on whether your author resolver "includes" the posts or not. You can get around that by doing something like this:

posts: (author) => {
  if (author.posts) return author.posts
  return author.getPosts()
}
// or more succinctly
posts: author => author.posts ? author.posts : author.getPosts()

This way, we only call getPosts if we actually need to get the posts. Alternatively, you can omit the posts resolver and handle this inside your author resolver. We can look at the forth argument passed to the resolver for information about the request, including which fields were requested. For example, your resolver could look something like this:

author: (root, args, context, info) => {
  const include = []
  const requestedPosts = info.fieldNodes[0].selectionSet.selections.includes(s => s.name.value === 'posts'
  if (requestedPosts) include.push(Post)
  return Author.findAll({include})
}

Now your resolver will only include the posts for each author if the client specifically requested it. The AST tree object provided to the resolver is messy to parse, but there are libraries out there (like this one) to help with that.

Upvotes: 1

Related Questions