Filtering with GraphQL and Prisma: What NOT to Do

 

Filtering with GraphQL and Prisma was a big learning experience so I want to focus this article on GraphQL and Prisma. But first, let me walk you through the stack I have been working with in the past months. I have been working with React, Apollo, GraphQL, and Prisma. Learning these four layers of programming provides you with the skills required to create apps from user click (React and Apollo) to handle and save information in the database (Prisma).

I was already familiar with back-end languages; I only copied code. In other words, I didn’t understand how the back-end works.

I reached a massive roadblock when working with the search function for the digital product I am building with a team.

At first, it’s easy to build functions because tutorials, even with complex topics, offer basic examples of how to create them. But what happens when basic examples are not enough?

The articles I read about filtering in the database describe examples that consider the search of the terms inside one query. Sometimes these examples included nested data.

My roadblock, what happens when I need to filter more than one query?

I want to share my incorrect approach to filtering multiple queries with GraphQL and Prisma. It took me a while to figure out why my initial direction was wrong and what is a better way when searching for data in different queries. You can read the Spanish version of this article here too.

How ReactJs, Apollo, GraphQL, and Prisma Work Together?

Let’s make a quick recap of how ReactJs, Apollo, GraphQL, and Prisma work together.

ReactJs

A Javascript framework (what the Js is for) handles the interface functions between the user and the software. ReactJs runs the data entry and searches submission of a website’s search feature.

Apollo

The ReactJs submit function must manually query the Apollo Client. Apollo communicates with GraphQL resolvers to request and receive information from the server using the GraphQL query language.

GraphQL

It is a query language for APIs that handles the website’s data. Also, it defines the data schemas and resolver functions.

GraphQL is a language. Therefore, we need to instruct how to filter or receive the data. Resolvers, written in NodeJs, filter and manages data functions.

GraphQL gives the ability to specify the needed data instead of collecting everything available on the endpoint, making it web developer-friendly.

Prisma

It is an interface between GraphQL and the database. It defines and implements the different queries from the database.

Prisma defines queries. After a couple of commands, Prisma autogenerates a file with the GraphQL data schema in a Prisma file extension. The Prisma-generated schema filters the data from the database.

In summary…

Diagram of data flow between ReactJs, Apollo, GraphQL and Prisma

Code Context Before Implementing the Search Function

Before going the wrong path for the search implementation, let’s make some assumptions.

We are making multiple query searches on a database for a small business that handles farming equipment.

1) There are multiple queries available on the database

Define GraphQL and Prisma schemas. An example of a data schema on the Prisma data model (prisma/datamodel.prisma):

type Company {
  id: ID! @id
  name: String
  logo: String
  location: String
}

type Equipment {
  id: ID! @id
  contact: [User!]
  model: String
  serialNo: String
  company: Company
  dueDate: DateTime
}

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

Next, the example of the GraphQL resolver schema (server/src/schema.graphql) with Prisma’s data schema imported:

# import User from "./generated/prisma.graphql"
# import Equipment from "./generated/prisma.graphql"
# import Company from "./generated/prisma.graphql"

type Query {
  users: [User]!
  equipment: [Equipment!]!
  company(id: ID!): Company
}

Where the resolver company requires the id argument to return data type Company.

And the users and equipment resolvers do not require an argument to return a value. The resolver users should return an array of User data types, while the equipment resolver should return a data array of Equipment data types.

Also, the equipment resolver should return an array of no null values. Neither the data array can itself be null.

2) Define query resolvers

So the sever/src/resolvers/Query.js file:

const Query = {
  users: (parent, args, ctx, info) => {
    return ctx.db.query.users(null, info)
  },
  equipment: (parent, args, ctx, info) => {
    return ctx.db.query.equipment(null, info)
  },
  company: (parent, { id }, ctx, info) => {
    return ctx.db.query.company({ where: { id } }, info)
  },
}

module.exports = {
  Query,
}

The resolver for the company query must receive an id-type argument. This resolver should return the query data filtered by id.

The resolvers for the user and equipment queries do not require an argument to return filtered data. Therefore, it produces a data array of all the query records requested.

3) Queries requested by the front-end have nested data

Examples of queries requested by Apollo:

Equipment query

import gql from 'graphql-tag';

export default gql`
  query Equipment {
    equipment {
      id
      contacts {
        id
        fistName
        lastName
        email
      }
      model
      serialNo
      dueDate
      company {
        id
        name
      }
    }
  }
`;

Company query

import gql from 'graphql-tag';

export default gql`
  query Company ($id: ID!) {
    company (id: $id) {
      id
      name
      logo
    }
  }
`;

Users query

import gql from 'graphql-tag';

export default gql`
  query Users {
    users {
      id
      email
      firstName
      lastName
      company {
        id
        name
        logo
      }
    }
  }
`;

Filtering with GraphQL and Prisma

Remember the company query we defined earlier? An id argument filters the company query. That’s a basic example of how to filter using GraphQL. It’s easy to find documentation on this topic.

const Query = {
  company: (parent, { id }, ctx, info) => {
    return ctx.db.query.company({ where: { id } }, info)
  },
}

module.exports = {
  Query,
}

An example of the resolver for the company query if we want to search the id argument in each of the Company type items:

const Query = {
  company: (parent, { id }, ctx, info) => {
    where = {
       OR: [
         { id_contains: id },
         { name_contains: id },
         { logo_contains: id },
       ]
    }
    return ctx.db.query.company({ where }, info)
  },
}

module.exports = {
  Query,
}

Most of the articles about filtering on GraphQL finish up to this point, where we can only filter data on a query.

But what happens if I need to search for more than one query? And, what happens if the data is nested?

My initial erroneous approach of filtering multiple queries

After reading the documentation, my instinct told me to filter queries by editing the already defined resolvers (remember the previously stated examples).

But, the idea of performing multiple requests to the database seemed heavy. The “edit the resolvers” approach would trigger multiple requests after the submitted search. These requests would be for each of the filtered queries.

I have to admit that I inhibited myself from editing the resolvers that already worked. I didn’t want to mess with the resolvers that worked to implement the new search feature.

Another thing that fueled my decision to follow the “creating a query from queries I need to filter” approach was that code repetition is a waste.

So, I defined my new query, Feed, on the Prisma data type schema (prisma/datamodel.prisma) as:

type Company {
  id: ID! @id
  name: String
  logo: String
}

type Equipment {
  id: ID! @id
  contacts: [User!]
  model: String
  serialNo: String
  company: Company
  dueDate: DateTime
}

type User {
  id: ID! @id
  firstName: String
  lastName: String
  email: String @unique
  password: String
  company: Company
}

type Feed {
  users: [User]!
  company(id: ID!): Company
  equipment: [Equipment]!
}

Import the query Feed to the GraphQL resolver schema after auto-generating the Prisma file, (server/src/schema.graphql) as:

# import User from "./generated/prisma.graphql"
# import Equipment from "./generated/prisma.graphql"
# import Company from "./generated/prisma.graphql"
# import Feed from "./generated/prisma.graphql"

type Query {
  users: [User]!
  equipment: [Equipment!]!
  company(id: ID!): Company
  feed(id: ID!, filter: String!): Feed
}

Also, the auto-generated Prisma file provides the conditions you can use as a developer to filter data on Prisma. The feed resolver (sever/src/resolvers/Query.js) below:

const Query = {
  users: (parent, args, ctx, info) => {
    return ctx.db.query.users(null, info)
  },
  equipment: (parent, args, ctx, info) => {
    return ctx.db.query.equipment(null, info)
  },
  company: (parent, { id }, ctx, info) => {
    return ctx.db.query.company({ where: { id } }, info)
  },
  feed: async (parent, { id, filter }, ctx, info) => {
   const company = await ctx.db.query.company({
      where: {
       AND: [
         { id },
         OR: [
           { name_contains: filter },
           { logo_contains: filter },
         ],
       ],
      },
    }, info);
    const equipment = await ctx.db.query.equipment({
      where: {
        OR: [
             { model_contains: filter },
             { serialNo_contains: filter },
             { dueDate_contains: filter },
             { company_some: {
                 OR: [
                   { name_contains: filter },
                   { logo_contains: filter },
                 ]
               }
             }
          ]
        }
    }, info);
    const users = await ctx.db.query.users({
      where: {
        OR: [
          { firstName_contains: filter },
          { lastName_contains: filter },
          { email_contains: filter },
        ]
      }
    }, info);
    return { users, company, equipment } ;
  }
}

module.exports = {
  Query,
}

Finally, the query requested from the front-end:

import gql from 'graphql-tag';

export default gql`
  query Feed ($id: ID!, $filter: String) {
    feed (id: $id, filter: $filter) {
       id
       user {
         id
         email
         firstName
         lastName
         company {
           id
           name
           logo
          }
       }
        company {
          id
          name
          logo
       }
       equipment {
         id
         contacts {
          id
          fistName
          lastName
          email
         }
         model
         serialNo
         dueDate
         company {
            id
            name
            logo
         }
       }
    }
  }
`;

Some received data from the three queries due to these composite codes. But, the data was incomplete. I could access the nested data from the contacts and company inside the equipment. I could only see what type of data I was receiving, but they had null values.

Why this is not a good approach?

Passing the incorrect data structure causes the data received as invalid when you should be able to access it.

But, what does a correct schema for Feed look like if we are already passing the suitable data types?

Queries have records attached to them in the database. An assigned id identifies these records. To set these ids, one needs to create a function that allows the query to mutate (create a new id) and can save the data attached to the database record.

So, you need to create a Feed mutation and connect the data queries for User, Company, and Equipment. This means that you need to create and save a record of type Feed on the database every time you perform a search.

Imagine how many rows a database would have if you save the query combination defined as Feed every time you search!

It would be too expensive and unnecessary to keep something like that. Also, we are not taking full advantage of the power of GraphQL.

Then, what should be an appropriate approach to filtering multiple queries?

It may be obvious now, but filtering data queries is done in…. ta-da… the same data queries. So my instinct of adding the capacity to filter on the already defined resolvers was a better approach.

When we search multiple queries on the database, the front-end requests the data query individually. Apollo handles the requests to the GraphQL resolvers, so the response is as lean as the developer needs.

We do not need to define a new query to perform a search on multiple queries. So, let’s go back and redefine prisma/datamodel.prisma as:

type Company {
  id: ID! @id
  name: String
  logo: String
}

type Equipment {
  id: ID! @id
  contacts: [User!]
  model: String
  serialNo: String
  dueDate: DateTime
  company: Company
}

type User {
  id: ID! @id
  fistName: String
  lastName: String
  email: String @unique
  password: String
  company: Company
}

Also, let’s go back and edit the GraphQL resolver schema (server/src/schema.graphql), eliminating the Feed type definition and adding the filter parameter to each query. The filter parameter is the data string the user wrote as the input for the search function:

# import User from "./generated/prisma.graphql"
# import Equipment from "./generated/prisma.graphql"
# import Company from "./generated/prisma.graphql"

type Query {
  users(filter: String): [User]!
  equipment(filter: String): [Equipment!]!
  company(id: ID!, filter: String): Company
}

Note: I assigned filter as an optional parameter so I can use these resolvers on other components.

On the GraphQL resolvers (sever/src/resolvers/Query.js), where the magic happens, we eliminate the feed resolver, and edit the other resolvers, so each data query accepts the filter argument:

const Query = {
  users: (parent, { filter }, ctx, info) => {
    const where = null;
    if (filter) {
      where = {
         OR: [
           { firstName_contains: filter },
           { lastName_contains: filter },
           { email_contains: filter },
         ]
       }
     }
    return ctx.db.query.user({ where }, info)
  },
  equipment: (parent, { filter }, ctx, info) => {
    const where = null;
    if (filter) {
      where = {
        OR: [
             { model_comntains: filter },
             { serialNo_contains: filter },
             { company_some: {
                 OR: [
                   { name_contains: filter },
                   { logo_contains: filter },
                 ]
                },
             },
          ]
      },
    }
    return ctx.db.query.equipment(null, info)
  },
  company: (parent, { id, filter }, ctx, info) => {
    const where = { id };
    if (filter) {
      where = {
        AND: [
          { id },
          {
            OR: [
              { name_contains: filter },
              { logo_contains: filter },
             ]
           },
         ]
       },
    }
    return ctx.db.query.company({ where }, info)
   },
}

module.exports = {
  Query,
}

And the data queries that front-end requests:

import gql from 'graphql-tag';

export default gql`
  query Equipment($filter: String) {
    equipment(filter: $filter) {
      id
      contacts {
        id
        firstName
        lastName
        email
      }
      model
      serialNo
      dueDate
      company {
        id
        name
      }
    }
  }
`;
import gql from 'graphql-tag';

export default gql`
  query Company ($id: ID!, $filter: String) {
    company (id: $id, filter: $filter) {
      id
      name
      logo
    }
  }
`;
import gql from 'graphql-tag';

export default gql`
  query Users($filter: String) {
    users(filter: $filter){
      id
      email
      firstName
      lastName
      company {
        id
        name
        logo
      }
    }
  }
`;

This approach to filtering multiple queries should receive all the data requested, including the nested data.

Ufff, we finished filtering with Graphql and Prisma!

Thanks for reading all of this! 🙌🎉

It took me a while to come to this conclusion. It would make me very happy if I could help you save a couple of hours or days filtering with GraphQL and Prisma with this article!

You can get a new post notification directly to your email by signing up at the following link.

Related Articles

The following CTRL-Y articles are related somewhat to this post. You may want to check them out!:

By the Way – A playlist for you

I wrote this article while listening to the Ladybug Podcast. Listening to a podcast while writing is rare, but the GraphQL episode motivated me to write this article. Ladybug Podcast is a program focused on web development topics. Hosted by Kelly VaughnAli Spittel, and Emma Bostian.

Ladybug Podcast is highly recommended 🙌

Leave a Reply

Your email address will not be published. Required fields are marked *