Daniela Matos de Carvalho

January 18, 2019

Query by 2 or more fields on GraphQL

We at YLD are using Slack together with missions.ai, allowing our employees to get some relevant information about them or about other people in the company and removing TOIL so our operations staff have more time to do other things. It helps to answer questions such as "How much hardware budget do I still have?" or "Is the person X on holidays?", or simply to request business cards.

We are grabbing some data about the person from different data sources such as BambooHR, Slack and also some master spreadsheets with other metadata. We decided to go with a GraphQL + Apollo solution for the API and our schema is similar to the following:

type Query {
  employee(email: String!): Employee
  employees(filter: String): [Employee]
}

type Employee {
  id: String
  birthday: String
  displayName: String
  hireDate: String
  slack: Slack
  workEmail: String
}

type Slack {
  id: String
  handle: String
}

Problem

We are currently adding more missions to our list and we saw that querying employee by email is not sufficient for our requirements. What if we want to get an employee information by a field other than email (e.g. slackId)?

What we want is something such as the following:

type Query {
  employee(email: String!): Employee
  employee(slackId: String!): Employee
  employees(filter: String): [Employee]
}

Unfortunately this is not possible in GraphQL! What exactly do we want?

  • query by employee email or slackId
  • email or slackId are required

1st Solution

One possible solution is to add two different queries and resolvers:

type Query {
  getEmployeeByByEmail(email: String!): Employee
  getEmployeeBySlackId(slackId: String!): Employee
  employees(filter: String): [Employee]
}

This works and it is an explicit solution: everyone that reads this piece of code understands exactly what it does. However, if we have 10 other fields we might want to query (e.g Github handle or Twitter handle, which are both unique values) we can end up with a messy solution that is not scalable.

2nd Solution

Another solution we can think of is having both fields for the same resolver as follows:

type Query {
  employee(email: String, slackId: String): Employee
  employees(filter: String): [Employee]
}

In this case we miss the required (!) field filter in the query and that validation has to be done inside the resolver:

// resolver code
employee: async (root, { email, slackId }, { dataSources }) => {
  // at least one of the parameters is required
  if (!email && !slackId) {
    return new Error('Email or SlackId are required.');
  }
  // ...

This could also be confusing if you just look at the Query defined in the GraphQL schema. Moreover, if we have multiple parameters to filter from we would have the same issue for all of them. This solution is also confusing and not scalable.

3rd Solution

We ended up using another solution: GraphQL Input Types. With input types you can specify types of inputs ("fields") that can be used in your query. We created a new input type:

input EmployeeSearch {
  email: String
  slackId: String
}

We use EmployeeSearch in our query referring it as a required field (!). This way we are specifying that at least one of the fields should be used to perform the query.

type Query {
  employee(where: EmployeeSearch!): Employee
  employees(filter: String): [Employee]
}

GraphiQL showing up and executing a query that searches for an employee

This is a solution that is more declarative and clear when we look at the schema. Furthermore, it is widely used in projects like Gatsby (check GraphQLInputObjectType used in Gatsby for details). In comparison with the former solutions presented, using Input Types is more scalable but has the disadvantage of having to filter by field inside the resolver. Also, we should not forget that the resolver must give an Error if either email or slackId are not sent to query employee:

// resolver code
employee: async (root, { where: { email, slackId } }, { dataSources }) => {
  if (!email && !slackId) {
    return new Error('Email or SlackId are required.');
  }

  if (email) {
    return getEmployeeFromEmail(email, dataSources);
  }
  if (slackId) {
    return getEmployeeFromSlackId(slackId, dataSources);
  }
  return null;
},
// ...

We ended up using input types and making our schema clean and mean. In order to make it even prettier we followed some open-crud specification ideas, which is used by interesting projects like Prisma or Hygraph.

Enjoy input types!

Originally published at blog.yld.io on January 18, 2019 by Daniela Matos de Carvalho (@sericaia on Twitter/Github)