7
votes

There are couple of places in GraphQL where resolving a Type is required and not just a field in a Type.

The Backend API -

  • /users - list of users - minimal info - name, id
  • /users/:id - detailed user info
  • /foo - returns a field owner which is a UserID

Query and Schema

Building a schema to execute the following query

query a {
  users {
    age # some detail info
  }
  foo {
    owner {
      location # some detail info
    }
  }
}

and the schema can be as follows -

type Query {
  users: [User]
  foo: Foo
}
type Foo {
  owner: User
}
type User {
  id: ID
  age: Int
  location: String
}

Problem

The resolvers in the above schema would need to contain / handle the user details info fetching call in 2 different places. 1. List of users - Query.users and 2. Query.foo.owner. And one has to remember to handle this type where you only have a user ID to convert it to actual User.

Possible Solution

As of this writing, GraphQL supports resolveType on Interface and Union. It is not possible to specify a resolver for an entire Type - only a field in a Type can have a resolver. So if it's possible to resolve a type in GraphQL, this would make it simpler to implement.

Alternate Solution

Since only fields in a Type can be resolved, it is possible to create an extra type and maintain the handling of this field in the Type in the resolvers and is in one location. But now, the query is deeper than before by 1 level.

query b {
  users {
    details {
      age
    }
  }
  foo {
    owner {
      details {
        location
      }
    }
  }
}

Other similar scenarios

Since a Type cannot be resolved in GraphQL, enums face the same issue. When you have a special character in an API response and the field is an ENUM, you either remember to handle it in all the places where this enum is used or you create an extra type to represent this enum.

I have created a minimal repro for all these cases with ApolloGraphQL - https://github.com/boopathi/graphql-test-1

Questions

  1. Shouldn't the schema/language support in specifying how to handle a particular type / specifying a resolver for a Type instead of just a field in a Type? If not, why?
  2. How do you handle these things in your schema. Is there another way to do these things ?
1

1 Answers

3
votes

You're right - this is definitely something in GraphQL that is a bit odd. Essentially, because of the way resolvers work, it's the type that you came from and not the type that you're going to that is responsible for fetching the right data.

There are pros and cons to this approach. You could definitely imagine an implementation of GraphQL where the parent object simply returns an ID, and then you have one resolver for each type that knows how to fetch the details. I think that would definitely be better for some cases.

Here's how we currently suggest structuring the code to avoid this kind of coupling:

  1. Define model classes or repository objects for the different backend data sources and object types you have
  2. Use those in your resolvers instead of accessing the database directly

To achieve a poor man's dependency injection we put these onto the context of the server. When you put it all together, it looks like this:

Schema:

# Information about a GitHub repository submitted to GitHunt
type Entry {
  # Information about the repository from GitHub
  repository: Repository!
  # The GitHub user who submitted this entry
  postedBy: User!
  ...

Resolvers:

export const resolvers = {
  Entry: {
    repository({ repository_name }, _, context) {
      return context.Repositories.getByFullName(repository_name);
    },
    postedBy({ posted_by }, _, context) {
      return context.Users.getByLogin(posted_by);
    },
    ...

You can see this in the context of a whole server in the GitHunt-API example app.

Basically this approach uses the resolvers as a thin wrapper that calls the underlying business logic, almost like a router. This is consistent with the current literature about servers at Facebook and otherwise.