Prisma select
Prisma Select takes the info: GraphQLResolveInfo
object in general graphql arguments (parent, args, context, info) to select object accepted by prisma client
. The approach allows a better performance since you will only be using one resolver to retrieve all your request. By doing so, it also eliminates the N + 1
issue.
CONTENT
Example query
query {
findUniqueUser(where: { id: 1 }) {
id
email
name
posts(where: { title: { contains: "a" } }, orderBy: { createdAt: asc }, first: 10, skip: 5) {
id
title
comments(where: { contain: { contains: "a" } }) {
id
contain
}
}
}
}
convert to
const result = {
select: {
id: true,
email: true,
name: true,
posts: {
select: {
id: true,
title: true,
comments: {
select: { id: true, contain: true },
where: { contain: { contains: 'a' } },
},
},
where: { title: { contains: 'a' } },
orderBy: { createdAt: 'asc' },
first: 10,
skip: 5,
},
},
};
API
constructor
Take two args:
info
:GraphQLResolveInfo
options
: accept object
// example
const defaultFields = {
User: { id: true, name: true },
Type: { id: true, descriptionRaw: true },
Post: { id: true, body: true },
// as function you can check if client select some fields to add another to default fields
Account: (select) => select.name ? {firstname: true, lastname: true} : {}
};
// the type of options
type options = {
// you can pass object with your models and what the fields you need to include for every model even if user not requested in GraphQL query
defaultFields?: {
[key: string]: { [key: string]: boolean } | ((select: any) => { [key: string]: boolean });
};
// array of dmmf object import from generated prisma client default "import {dmmf} from '@prisma/client'"
dmmf?: DMMF.Document[];
};
Methods
value
Return your converted object.
Example
const select = new PrismaSelect(info).value;
valueWithFilter
function take 1 arg:
modelName
: Your schema model name to filter returned object and remove any field, not in your schema model
Example
const select = new PrismaSelect(info).valueWithFilter('User');
valueOf
function take 3 args:
field
: path to field you want inside type. You can deeb inside nested relation with thisuser.posts.comments
filterBy
: take schema Model name to filter returned object by his schema typemergeObject
: like constructor you can pass here any object to merge with returned data.
Example of use
If We have a mutation called "login", which returns a non schema model type called AuthPayload
that has a schema model type in it,
like the following example,
type AuthPayload {
token: String
user: User
}
type Mutation {
login(email: String!, password: String!): AuthPayload
}
Here's how the nested type, filter, and to merge custom object would look like.
const resolver = {
Mutation: {
login: (_parent, { email, password }, { prisma }: Context, info) => {
const select = new PrismaSelect(info).valueOf('user', 'User', { select: { id: true } });
return {
token: 'token',
user: prisma.user.findUnique({
where: { email },
...select,
}),
};
},
},
};
mergeDeep
This is a static method that you can use to merge our converted object with your custom object.
Also, you can use it to merge any object with another object.
You can use it if you pass select objects inside the context.
const resolvers = {
Query: {
user(_parent, { where }, { prisma, select }, info) {
const mergedObject = PrismaSelect.mergeDeep(select, { select: { id: true } }); return prisma.user.findUnique({
where,
...mergedObject,
});
},
},
};
filter
Prisma Select can also be used as a private method to filter your computed fields not included originally in your prisma schema. This feature gives you the ability to customize additional fields in the schema.
Example
// prisma.schema
model User {
id Int @default(autoincrement()) @id
firstName String
lastName String
}
# graphql type
type User {
id: Int
firstName: String
lastName: String
fullName: String
}
By adding firstName
and lastName
to PrismaSelect in the user field of Query, and fullName
in User, the client can request fullName directly.
import { PrismaSelect } from '@paljs/plugins';
const defaultFields = {
User: { firstName: true, lastName: true },
};
const resolvers = {
Query: {
user(_parent, { where }, { prisma }, info) {
const select = new PrismaSelect(info, { defaultFields }).value; return prisma.user.findUnique({
where,
// this object must not have `fullName` because will throw error it's not in our db
// So we have built in filter to remove any field not in our schema model
...select,
});
},
},
User: {
fullName: (parent, args, { prisma }: Context) => {
return parent.firstName + parent.lastName; },
},
};
Map models
If You need to customize your graphql type and use any name instead of using a prisma model name, you can do this by adding a comment in your schema.prisma file before the model name like this example:
/// @PrismaSelect.map([Account, Profile])
model User {
id Int @default(autoincrement()) @id
firstName String
lastName String
}
Now you can create your GrqphQL types with name you provided and you will get filter by the model User
type User {
id: Int
firstName: String
lastName: String
}
type Account {
id: Int
firstName: String
lastName: String
fullName: String
}
type Profile {
id: Int
firstName: String
lastName: String
anyfield: String
}
Performance Example
If we have a Prisma Schema
with the models below.
model User {
id Int @default(autoincrement()) @id
email String @unique
password String
posts Post[]
}
model Post {
id Int @default(autoincrement()) @id
published Boolean @default(false)
title String
author User? @relation(fields: [authorId], references: [id])
authorId Int?
comments Comment[]
}
model Comment {
id Int @default(autoincrement()) @id
contain String
post Post @relation(fields: [postId], references: [id])
postId Int
}
The normal GraphQL Resolvers
to get one User will be like this:
const resolvers = {
Query: {
findUniqueUser: (_parent, args, { prisma }) => {
return prisma.user.findUnique(args);
},
},
User: {
posts: (parent, args, { prisma }) => {
return prisma.user.findUnique({ where: { id: parent.id } }).posts(args);
},
},
Post: {
comments: (parent, args, { prisma }) => {
return prisma.post.findUnique({ where: { id: parent.id } }).comments(args);
},
},
};
Let me do GraphQL query to get one user with his posts and comments inside posts and see what is the result:
{
findUniqueUser(where: { id: 1 }) {
id
posts {
id
comments {
id
}
}
}
}
Even though we are only requesting ids in the query, the backend is doing 5 queries to select all the table fields as the log shows.
prisma:query SELECT `dev`.`User`.`id`, `dev`.`User`.`createdAt`, `dev`.`User`.`email`, `dev`.`User`.`name`, `dev`.`User`.`password`, `dev`.`User`.`groupId` FROM `dev`.`User` WHERE `dev`.`User`.`id` = ? LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`User`.`id` FROM `dev`.`User` WHERE `dev`.`User`.`id` = ? LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`published`, `dev`.`Post`.`title`, `dev`.`Post`.`authorId`, `dev`.`Post`.`createdAt`, `dev`.`Post`.`updatedAt`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE `dev`.`Post`.`authorId` IN (?) LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Post`.`id` FROM `dev`.`Post` WHERE `dev`.`Post`.`id` IN (?,?,?) LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Comment`.`id`, `dev`.`Comment`.`contain`, `dev`.`Comment`.`postId`, `dev`.`Comment`.`authorId`, `dev`.`Comment`.`createdAt`, `dev`.`Comment`.`updatedAt`, `dev`.`Comment`.`postId` FROM `dev`.`Comment` WHERE `dev`.`Comment`.`postId` IN (?,?,?) LIMIT ? OFFSET ?
With PalJs's Tool GraphQL Resolvers
:
import { PrismaSelect } from '@paljs/plugins';
{
Query: {
findUniqueUser: (_parent, args, { prisma }, info) => {
const select = new PrismaSelect(info).value;
return prisma.user.findUnique({
...args,
...select,
});
},
},
}
When we do the same query:
{
findUniqueUser(where: { id: 1 }) {
id
posts {
id
comments {
id
}
}
}
}
According to this log, We only get 3 queries using our tool. By using Paljs,
we first query for all the relationship between models, then we select the id
from db
prisma:query SELECT `dev`.`User`.`id` FROM `dev`.`User` WHERE `dev`.`User`.`id` = ? LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE `dev`.`Post`.`authorId` IN (?) LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Comment`.`id`, `dev`.`Comment`.`postId` FROM `dev`.`Comment` WHERE `dev`.`Comment`.`postId` IN (?,?,?) LIMIT ? OFFSET ?