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…
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!:
- API Testing with Mocha
- NodeJS Layered Architecture
- Programming Language: The General Parts
- HTML for Beginners
- HTML Tag Attributes
- A Morning in a Front End Developer’s Mac Terminal
- HTML Tag Nesting
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 Vaughn, Ali Spittel, and Emma Bostian.