Filtering with GraphQL and Prisma: What NOT to Do

 

In the past months, I have been working with the magic package of React, Apollo, GraphQL, and Prisma. I say the magic package because 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). They say that building this makes you a Full Stack developer.

I want to focus this article on GraphQL and Prisma because even thou 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 I started working with the search function for the digital product I am helping to develop.

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 with you my incorrect approach when working on this task. It took me a while to figure out why this approach was wrong, and what is a better approach when searching for data in different queries.

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

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

ReactJs

It is a Javascript framework (that is what the Js is for) that handles the interface functions between the user and the software. When we work with a feature such as a website search, the data entry function, and search submission are handled by ReactJs.

Apollo

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

GraphQL

It is a query language for APIs that handles the website’s data. It is where the data schemas and resolver functions are defined.

GraphQL is a language. Therefore, we need to instruct which data will it filter and receive. These functions written in NodeJs (that is why usually resolver function files are Js and not Graphql) are called resolvers.

It is web developer-friendly because it gives the ability to specify the data that needs to be received by the website instead of collecting everything available on the endpoint.

Prisma

It is an interface between GraphQL and the database.

It is where the different queries from the database are defined and implemented.

Define queries in Prisma. After a couple of commandos, Prisma autogenerates a file with the GraphQL data schema in Prisma. The data is filtered from the database using this schema.

In summary…

Code Context Before Implementing the Search Function

Let’s make some assumptions before going the wrong path for the search implementation.

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

GraphQL’s and Prisma’s schemas are defined. An example of a data schema on 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 (! = “it is required so it can’t be null”) 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) The query resolvers are defined

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 will return a data array of all the records of the requested query.

3) Queries requested by front-end have nested data

An example of an equipment query requested by Apollo:

import gql from 'graphql-tag';

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

An example of a company query requested by Apollo:

import gql from 'graphql-tag';

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

An example of a users query requested by Apollo:

import gql from 'graphql-tag';

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

Filter Data Using 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 data in 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 the data is nested?

My initial erroneous approach of filtering multiple queries

My instinct told me, after reading documentation, that I should filter queries 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 requires that the front-end triggers a network request for every filtered query at the moment the search is submitted.

I admit that because the implementation of the search feature was after other elements in the app had been validated, I inhibited myself from editing the resolvers that already worked.

Also, trying to consider that code repetition is a waste, fueled my reasoning that creating a query from the queries I needed to filter was the go-to approach for handling the server.

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 as a result of 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 the value was null.

Why this is not an adequate approach?

Looking through the documentation and frequently asked questions about this topic, the primary reason why invalid data is received when you should be able to access it is that because the data structure you are passing is incorrect.

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

Queries have records attached to them in the database. An assigned id identifies these records. To assign 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. It means that you need to create and save on the database a record of type Feed every time you perform a search.

Imagine how loaded would be the database if, for every search you make, you save the query combination you defined as Feed!

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

Then, what should be an appropriate approach to do searches on multiple queries?

It’s obvious now, but to filter 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 define 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 result in receiving all the data requested, including the nested data.

Ufff, we finished!

Thanks for reading all of this! 🙌🎉

It took me a while to come to this conclusion. If I can help you save a couple of hours or days by reading this article, that would be a mega mission accomplished!

Side Note

I wrote this article while listening to the Ladybug Podcast. It is rare for me to listen to a podcast while writing, but the topics the girls talked about motivated me to write this article.

Ladybug Podcast is a program focused on web development topics. Hosted by Kelly VaughnAli Spittel, and Emma Bostian.

Even thou I binge listened to the podcast, I am sharing the episode that is about GraphQL, which I also listened to while writing this article.

Ladybug Podcast is highly recommended 🙌

Leave a Reply

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