prisma-binding to SDL-first
Overview
This upgrade guide describes how to migrate a Node.js project that's based on Prisma 1 and uses prisma-binding
to implement a GraphQL server.
The code will keep the SDL-first approach for constructing the GraphQL schema. When migrating from prisma-binding
to Prisma Client, the main difference is that the info
object can't be used to resolve relations automatically any more, instead you'll need to implement your type resolvers to ensure that relations get resolved properly.
The guide assumes that you already went through the guide for upgrading the Prisma ORM layer. This means you already:
- installed the Prisma ORM 2 CLI
- created your Prisma ORM 2 schema
- introspected your database and resolved potential schema incompatibilities
- installed and generated Prisma Client
The guide further assumes that you have a file setup that looks similar to this:
.
├── README.md
├── package.json
├── prisma
│ └── schema.prisma
├── prisma1
│ ├── datamodel.prisma
│ └── prisma.yml
└── src
├── generated
│ └── prisma.graphql
├── index.js
└── schema.graphql
The important parts are:
- A folder called with
prisma
with your Prisma ORM 2 schema - A folder called
src
with your application code and a schema calledschema.graphql
If this is not what your project structure looks like, you'll need to adjust the instructions in the guide to match your own setup.
1. Adjusting your GraphQL schema
With prisma-binding
, your approach for defining your GraphQL schema (sometimes called application schema) is based on importing GraphQL types from the generated prisma.graphql
file (in Prisma 1, this is typically called Prisma GraphQL schema). These types mirror the types from your Prisma 1 datamodel and serve as foundation for your GraphQL API.
With Prisma ORM 2, there's no prisma.graphql
file any more that you could import from. Therefore, you have to spell out all the types of your GraphQL schema directly inside your schema.graphql
file.
The easiest way to do so is by downloading the full GraphQL schema from the GraphQL Playground. To do so, open the SCHEMA tab and click the DOWNLOAD button in the top-right corner, then select SDL:
Alternatively, you can use the get-schema
command of the GraphQL CLI to download your full schema:
npx graphql get-schema --endpoint __GRAPHQL_YOGA_ENDPOINT__ --output schema.graphql --no-all
Note: With the above command, you need to replace the
__GRAPHQL_YOGA_ENDPOINT__
placeholder with the actual endpoint of your GraphQL Yoga server.
Once you obtained the schema.graphql
file, replace your current version in src/schema.graphql
with the new contents. Note that the two schemas are 100% equivalent, except that the new one doesn't use graphql-import
for importing types from a different file. Instead, it spells out all types in a single file.
Here's a comparison of these two versions of the sample GraphQL schema that we'll migrate in this guide (you can use the tabs to switch between the two versions):
- Before (with graphql-import)
- After (with Prisma 2)
# import Post from './generated/prisma.graphql'
# import User from './generated/prisma.graphql'
# import Category from './generated/prisma.graphql'
type Query {
posts(searchString: String): [Post!]!
user(userUniqueInput: UserUniqueInput!): User
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
allCategories: [Category!]!
}
input UserUniqueInput {
id: String
email: String
}
type Mutation {
createDraft(authorId: ID!, title: String!, content: String!): Post
publish(id: ID!): Post
deletePost(id: ID!): Post
signup(name: String!, email: String!): User!
updateBio(userId: String!, bio: String!): User
addPostToCategories(postId: String!, categoryIds: [String!]!): Post
}
type Query {
posts(searchString: String): [Post!]!
user(id: ID!): User
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
allCategories: [Category!]!
}
type Category implements Node {
id: ID!
name: String!
posts(where: PostWhereInput, orderBy: Enumerable<PostOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [Post!]
}
input CategoryCreateManyWithoutPostsInput {
create: [CategoryCreateWithoutPostsInput!]
connect: [CategoryWhereUniqueInput!]
}
input CategoryCreateWithoutPostsInput {
id: ID
name: String!
}
enum CategoryOrderByInput {
id_ASC
id_DESC
name_ASC
name_DESC
}
input CategoryWhereInput {
"""Logical AND on all given filters."""
AND: [CategoryWhereInput!]
"""Logical OR on all given filters."""
OR: [CategoryWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [CategoryWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
name: String
"""All values that are not equal to given value."""
name_not: String
"""All values that are contained in given list."""
name_in: [String!]
"""All values that are not contained in given list."""
name_not_in: [String!]
"""All values less than the given value."""
name_lt: String
"""All values less than or equal the given value."""
name_lte: String
"""All values greater than the given value."""
name_gt: String
"""All values greater than or equal the given value."""
name_gte: String
"""All values containing the given string."""
name_contains: String
"""All values not containing the given string."""
name_not_contains: String
"""All values starting with the given string."""
name_starts_with: String
"""All values not starting with the given string."""
name_not_starts_with: String
"""All values ending with the given string."""
name_ends_with: String
"""All values not ending with the given string."""
name_not_ends_with: String
posts_every: PostWhereInput
posts_some: PostWhereInput
posts_none: PostWhereInput
}
input CategoryWhereUniqueInput {
id: ID
}
scalar DateTime
"""Raw JSON value"""
scalar Json
"""An object with an ID"""
interface Node {
"""The id of the object."""
id: ID!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String
published: Boolean!
author: User
categories(where: CategoryWhereInput, orderBy: Enumerable<CategoryOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [Category!]
}
input PostCreateManyWithoutAuthorInput {
create: [PostCreateWithoutAuthorInput!]
connect: [PostWhereUniqueInput!]
}
input PostCreateWithoutAuthorInput {
id: ID
title: String!
content: String
published: Boolean
categories: CategoryCreateManyWithoutPostsInput
}
enum PostOrderByInput {
id_ASC
id_DESC
createdAt_ASC
createdAt_DESC
updatedAt_ASC
updatedAt_DESC
title_ASC
title_DESC
content_ASC
content_DESC
published_ASC
published_DESC
}
input PostWhereInput {
"""Logical AND on all given filters."""
AND: [PostWhereInput!]
"""Logical OR on all given filters."""
OR: [PostWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [PostWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
createdAt: DateTime
"""All values that are not equal to given value."""
createdAt_not: DateTime
"""All values that are contained in given list."""
createdAt_in: [DateTime!]
"""All values that are not contained in given list."""
createdAt_not_in: [DateTime!]
"""All values less than the given value."""
createdAt_lt: DateTime
"""All values less than or equal the given value."""
createdAt_lte: DateTime
"""All values greater than the given value."""
createdAt_gt: DateTime
"""All values greater than or equal the given value."""
createdAt_gte: DateTime
updatedAt: DateTime
"""All values that are not equal to given value."""
updatedAt_not: DateTime
"""All values that are contained in given list."""
updatedAt_in: [DateTime!]
"""All values that are not contained in given list."""
updatedAt_not_in: [DateTime!]
"""All values less than the given value."""
updatedAt_lt: DateTime
"""All values less than or equal the given value."""
updatedAt_lte: DateTime
"""All values greater than the given value."""
updatedAt_gt: DateTime
"""All values greater than or equal the given value."""
updatedAt_gte: DateTime
title: String
"""All values that are not equal to given value."""
title_not: String
"""All values that are contained in given list."""
title_in: [String!]
"""All values that are not contained in given list."""
title_not_in: [String!]
"""All values less than the given value."""
title_lt: String
"""All values less than or equal the given value."""
title_lte: String
"""All values greater than the given value."""
title_gt: String
"""All values greater than or equal the given value."""
title_gte: String
"""All values containing the given string."""
title_contains: String
"""All values not containing the given string."""
title_not_contains: String
"""All values starting with the given string."""
title_starts_with: String
"""All values not starting with the given string."""
title_not_starts_with: String
"""All values ending with the given string."""
title_ends_with: String
"""All values not ending with the given string."""
title_not_ends_with: String
content: String
"""All values that are not equal to given value."""
content_not: String
"""All values that are contained in given list."""
content_in: [String!]
"""All values that are not contained in given list."""
content_not_in: [String!]
"""All values less than the given value."""
content_lt: String
"""All values less than or equal the given value."""
content_lte: String
"""All values greater than the given value."""
content_gt: String
"""All values greater than or equal the given value."""
content_gte: String
"""All values containing the given string."""
content_contains: String
"""All values not containing the given string."""
content_not_contains: String
"""All values starting with the given string."""
content_starts_with: String
"""All values not starting with the given string."""
content_not_starts_with: String
"""All values ending with the given string."""
content_ends_with: String
"""All values not ending with the given string."""
content_not_ends_with: String
published: Boolean
"""All values that are not equal to given value."""
published_not: Boolean
author: UserWhereInput
categories_every: CategoryWhereInput
categories_some: CategoryWhereInput
categories_none: CategoryWhereInput
}
input PostWhereUniqueInput {
id: ID
}
type Profile implements Node {
id: ID!
bio: String
user: User!
}
input ProfileCreateOneWithoutUserInput {
create: ProfileCreateWithoutUserInput
connect: ProfileWhereUniqueInput
}
input ProfileCreateWithoutUserInput {
id: ID
bio: String
}
input ProfileWhereInput {
"""Logical AND on all given filters."""
AND: [ProfileWhereInput!]
"""Logical OR on all given filters."""
OR: [ProfileWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [ProfileWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
bio: String
"""All values that are not equal to given value."""
bio_not: String
"""All values that are contained in given list."""
bio_in: [String!]
"""All values that are not contained in given list."""
bio_not_in: [String!]
"""All values less than the given value."""
bio_lt: String
"""All values less than or equal the given value."""
bio_lte: String
"""All values greater than the given value."""
bio_gt: String
"""All values greater than or equal the given value."""
bio_gte: String
"""All values containing the given string."""
bio_contains: String
"""All values not containing the given string."""
bio_not_contains: String
"""All values starting with the given string."""
bio_starts_with: String
"""All values not starting with the given string."""
bio_not_starts_with: String
"""All values ending with the given string."""
bio_ends_with: String
"""All values not ending with the given string."""
bio_not_ends_with: String
user: UserWhereInput
}
input ProfileWhereUniqueInput {
id: ID
}
enum Role {
ADMIN
CUSTOMER
}
type User implements Node {
id: ID!
email: String
name: String!
posts(where: PostWhereInput, orderBy: Enumerable<PostOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [Post!]
role: Role!
profile: Profile
jsonData: Json
}
input UserCreateInput {
id: ID
email: String
name: String!
role: Role
jsonData: Json
posts: PostCreateManyWithoutAuthorInput
profile: ProfileCreateOneWithoutUserInput
}
enum UserOrderByInput {
id_ASC
id_DESC
email_ASC
email_DESC
name_ASC
name_DESC
role_ASC
role_DESC
jsonData_ASC
jsonData_DESC
}
input UserWhereInput {
"""Logical AND on all given filters."""
AND: [UserWhereInput!]
"""Logical OR on all given filters."""
OR: [UserWhereInput!]
"""Logical NOT on all given filters combined by AND."""
NOT: [UserWhereInput!]
id: ID
"""All values that are not equal to given value."""
id_not: ID
"""All values that are contained in given list."""
id_in: [ID!]
"""All values that are not contained in given list."""
id_not_in: [ID!]
"""All values less than the given value."""
id_lt: ID
"""All values less than or equal the given value."""
id_lte: ID
"""All values greater than the given value."""
id_gt: ID
"""All values greater than or equal the given value."""
id_gte: ID
"""All values containing the given string."""
id_contains: ID
"""All values not containing the given string."""
id_not_contains: ID
"""All values starting with the given string."""
id_starts_with: ID
"""All values not starting with the given string."""
id_not_starts_with: ID
"""All values ending with the given string."""
id_ends_with: ID
"""All values not ending with the given string."""
id_not_ends_with: ID
email: String
"""All values that are not equal to given value."""
email_not: String
"""All values that are contained in given list."""
email_in: [String!]
"""All values that are not contained in given list."""
email_not_in: [String!]
"""All values less than the given value."""
email_lt: String
"""All values less than or equal the given value."""
email_lte: String
"""All values greater than the given value."""
email_gt: String
"""All values greater than or equal the given value."""
email_gte: String
"""All values containing the given string."""
email_contains: String
"""All values not containing the given string."""
email_not_contains: String
"""All values starting with the given string."""
email_starts_with: String
"""All values not starting with the given string."""
email_not_starts_with: String
"""All values ending with the given string."""
email_ends_with: String
"""All values not ending with the given string."""
email_not_ends_with: String
name: String
"""All values that are not equal to given value."""
name_not: String
"""All values that are contained in given list."""
name_in: [String!]
"""All values that are not contained in given list."""
name_not_in: [String!]
"""All values less than the given value."""
name_lt: String
"""All values less than or equal the given value."""
name_lte: String
"""All values greater than the given value."""
name_gt: String
"""All values greater than or equal the given value."""
name_gte: String
"""All values containing the given string."""
name_contains: String
"""All values not containing the given string."""
name_not_contains: String
"""All values starting with the given string."""
name_starts_with: String
"""All values not starting with the given string."""
name_not_starts_with: String
"""All values ending with the given string."""
name_ends_with: String
"""All values not ending with the given string."""
name_not_ends_with: String
role: Role
"""All values that are not equal to given value."""
role_not: Role
"""All values that are contained in given list."""
role_in: [Role!]
"""All values that are not contained in given list."""
role_not_in: [Role!]
posts_every: PostWhereInput
posts_some: PostWhereInput
posts_none: PostWhereInput
profile: ProfileWhereInput
}
You'll notice that the new version of your GraphQL schema not only defines the models that were imported directly, but also additional types (e.g. input
types) that were not present in the schema before.
2. Set up your PrismaClient
instance
PrismaClient
is your new interface to the database in Prisma ORM 2. It lets you invoke various methods which build SQL queries and send them to the database, returning the results as plain JavaScript objects.
The PrismaClient
query API is inspired by the initial prisma-binding
API, so a lot of the queries you send with Prisma Client will feel familiar.
Similar to the prisma-binding
instance from Prisma 1, you also want to attach your PrismaClient
from Prisma ORM 2 to GraphQL's context
so that in can be accessed inside your resolvers:
const { PrismaClient } = require('@prisma/client')
// ...
const server = new GraphQLServer({
typeDefs: 'src/schema.graphql',
resolvers,
context: (req) => ({
...req,
prisma: new Prisma({
typeDefs: 'src/generated/prisma.graphql',
endpoint: 'http://localhost:4466',
}),
prisma: new PrismaClient(),
}),
})
In the code block above, the red lines are the lines to be removed from your current setup, the green lines are the ones that you should add. Of course, it's possible that your previous setup differed from this one (e.g. it's unlikely that your Prisma ORM endpoint
was http://localhost:4466
if you're running your API in production), this is just a sample to indicate what it could look like.
When you're now accessing context.prisma
inside of a resolver, you now have access to Prisma Client queries.
2. Write your GraphQL type resolvers
prisma-binding
was able to magically resolve relations in your GraphQL schema. When not using prisma-binding
though, you need to explicitly resolve your relations using so-called type resolvers.
Note You can learn more about the concept of type resolvers and why they're necessary in this article: GraphQL Server Basics: GraphQL Schemas, TypeDefs & Resolvers Explained
2.1. Implementing the type resolver for the User
type
The User
type in our sample GraphQL schema is defined as follows:
type User implements Node {
id: ID!
email: String
name: String!
posts(
where: PostWhereInput
orderBy: Enumerable<PostOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Post!]
role: Role!
profile: Profile
jsonData: Json
}
This type has two relations:
- The
posts
field denotes a 1-n relation toPost
- The
profile
field denotes a 1-1 relation toProfile
Since you're not using prisma-binding
any more, you now need to resolve these relations "manually" in type resolvers.
You can do so by adding a User
field to your resolver map and implement the resolvers for the posts
and profile
relations as follows:
const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
posts: (parent, args, context) => {
return context.prisma.user
.findUnique({
where: { id: parent.id },
})
.posts()
},
profile: (parent, args, context) => {
return context.prisma.user
.findUnique({
where: { id: parent.id },
})
.profile()
},
},
}
Inside of these resolvers, you're using your new PrismaClient
to perform a query against the database. Inside the posts
resolver, the database query loads all Post
records from the specified author
(whose id
is carried in the parent
object). Inside the profile
resolver, the database query loads the Profile
record from the specified user
(whose id
is carried in the parent
object).
Thanks to these extra resolvers, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the User
type in a query, e.g.:
{
users {
id
name
posts {
# fetching this relation is enabled by the new type resolver
id
title
}
profile {
# fetching this relation is enabled by the new type resolver
id
bio
}
}
}
2.2. Implementing the type resolver for the Post
type
The Post
type in our sample GraphQL schema is defined as follows:
type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String
published: Boolean!
author: User
categories(
where: CategoryWhereInput
orderBy: Enumerable<CategoryOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Category!]
}
This type has two relations:
- The
author
field denotes a 1-n relation toUser
- The
categories
field denotes a m-n relation toCategory
Since you're not using prisma-binding
any more, you now need to resolve these relations "manually" in type resolvers.
You can do so by adding a Post
field to your resolver map and implement the resolvers for the author
and categories
relations as follows:
const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
// ... your type resolvers for `User` from before
},
Post: {
author: (parent, args, context) => {
return context.prisma.post
.findUnique({
where: { id: parent.id },
})
.author()
},
categories: (parent, args, context) => {
return context.prisma.post
.findUnique({
where: { id: parent.id },
})
.categories()
},
},
}
Inside of these resolvers, you're using your new PrismaClient
to perform a query against the database. Inside the author
resolver, the database query loads the User
record that represents the author
of the Post
. Inside the categories
resolver, the database query loads all Category
records from the specified post
(whose id
is carried in the parent
object).
Thanks to these extra resolvers, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the User
type in a query, e.g.:
{
posts {
id
title
author {
# fetching this relation is enabled by the new type resolver
id
name
}
categories {
# fetching this relation is enabled by the new type resolver
id
name
}
}
}
2.3. Implementing the type resolver for the Profile
type
The Profile
type in our sample GraphQL schema is defined as follows:
type Profile implements Node {
id: ID!
bio: String
user: User!
}
This type has one relation: The user
field denotes a 1-n relation to User
.
Since you're not using prisma-binding
any more, you now need to resolve this relation "manually" in type resolvers.
You can do so by adding a Profile
field to your resolver map and implement the resolvers for the owner
relation as follows:
const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
// ... your type resolvers for `User` from before
},
Post: {
// ... your type resolvers for `Post` from before
},
Profile: {
user: (parent, args, context) => {
return context.prisma.profile
.findUnique({
where: { id: parent.id },
})
.owner()
},
},
}
Inside of this resolver, you're using your new PrismaClient
to perform a query against the database. Inside the user
resolver, the database query loads the User
records from the specified profile
(whose id
is carried in the parent
object).
Thanks to this extra resolver, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about the Profile
type in a query.
2.4. Implementing the type resolver for the Category
type
The Category
type in our sample GraphQL schema is defined as follows:
type Category implements Node {
id: ID!
name: String!
posts(
where: PostWhereInput
orderBy: Enumerable<PostOrderByInput>
skip: Int
after: String
before: String
first: Int
last: Int
): [Post!]
}
This type has one relation: The posts
field denotes a m-n relation to Post
.
Since you're not using prisma-binding
any more, you now need to resolve this relation "manually" in type resolvers.
You can do so by adding a Category
field to your resolver map and implement the resolvers for the posts
and profile
relations as follows:
const resolvers = {
Query: {
// ... your query resolvers
},
Mutation: {
// ... your mutation resolvers
},
User: {
// ... your type resolvers for `User` from before
},
Post: {
// ... your type resolvers for `Post` from before
},
Profile: {
// ... your type resolvers for `User` from before
},
Category: {
posts: (parent, args, context) => {
return context.prisma
.findUnique({
where: { id: parent.id },
})
.posts()
},
},
}
Inside of this resolver, you're using your new PrismaClient
to perform a query against the database. Inside the posts
resolver, the database query loads all Post
records from the specified categories
(whose id
is carried in the parent
object).
Thanks to this extra resolver, you'll now be able to nest relations in your GraphQL queries/mutations whenever you're requesting information about a Category
type in a query.
With all your type resolvers in place, you can start migrating the actual GraphQL API operations.
3. Migrate GraphQL operations
3.1. Migrate GraphQL queries
In this section, you'll migrate all GraphQL queries from prisma-binding
to Prisma Client.
3.1.1. Migrate the users
query (which uses forwardTo
)
In our sample API, the users
query from the sample GraphQL schema is defined and implemented as follows.
SDL schema definition with prisma-binding
type Query {
users(where: UserWhereInput, orderBy: Enumerable<UserOrderByInput>, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
# ... other queries
}
Resolver implementation with prisma-binding
const resolvers = {
Query: {
users: forwardTo('prisma'),
// ... other resolvers
},
}
Implementing the users
resolver with Prisma Client
To re-implement queries that were previously using forwardTo
, the idea is to pass the incoming filtering, ordering and pagination arguments to PrismaClient
:
const resolvers = {
Query: {
users: (_, args, context, info) => {
// this doesn't work yet
const { where, orderBy, skip, first, last, after, before } = args
return context.prisma.user.findMany({
where,
orderBy,
skip,
first,
last,
after,
before,
})
},
// ... other resolvers
},
}
Note that this approach does not work yet because the structures of the incoming arguments is different from the ones expected by PrismaClient
. To ensure the structures are compatible, you can use the @prisma/binding-argument-transform
npm package which ensures compatibility:
npm install @prisma/binding-argument-transform
You can now use this package as follows:
const {
makeOrderByPrisma2Compatible,
makeWherePrisma2Compatible,
} = require('@prisma/binding-argument-transform')
const resolvers = {
Query: {
users: (_, args, context, info) => {
// this still doesn't entirely work
const { where, orderBy, skip, first, last, after, before } = args
const prisma2Where = makeWherePrisma2Compatible(where)
const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
return context.prisma.user.findMany({
where: prisma2Where,
orderBy: prisma2OrderBy,
skip,
first,
last,
after,
before,
})
},
// ... other resolvers
},
}
The last remaining issue with this are the pagination arguments. Prisma ORM 2 introduces a new pagination API:
- The
first
,last
,before
andafter
arguments are removed - The new
cursor
argument replacesbefore
andafter
- The new
take
argument replacesfirst
andlast
Here is how you can adjust the call to make it compliant with the new Prisma Client pagination API:
const {
makeOrderByPrisma2Compatible,
makeWherePrisma2Compatible,
} = require('@prisma/binding-argument-transform')
const resolvers = {
Query: {
users: (_, args, context) => {
const { where, orderBy, skip, first, last, after, before } = args
const prisma2Where = makeWherePrisma2Compatible(where)
const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)
const skipValue = skip || 0
const prisma2Skip = Boolean(before) ? skipValue + 1 : skipValue
const prisma2Take = Boolean(last) ? -last : first
const prisma2Before = { id: before }
const prisma2After = { id: after }
const prisma2Cursor =
!Boolean(before) && !Boolean(after)
? undefined
: Boolean(before)
? prisma2Before
: prisma2After
return context.prisma.user.findMany({
where: prisma2Where,
orderBy: prisma2OrderBy,
skip: prisma2Skip,
cursor: prisma2Cursor,
take: prisma2Take,
})
},
// ... other resolvers
},
}
The calculations are needed to ensure the incoming pagination arguments map properly to the ones from the Prisma Client API.
3.1.2. Migrate the posts(searchString: String): [Post!]!
query
The posts
query is defined and implemented as follows.
SDL schema definition with prisma-binding
type Query {
posts(searchString: String): [Post!]!
# ... other queries
}
Resolver implementation with prisma-binding
const resolvers = {
Query: {
posts: (_, args, context, info) => {
return context.prisma.query.posts(
{
where: {
OR: [
{ title_contains: args.searchString },
{ content_contains: args.searchString },
],
},
},
info
)
},
// ... other resolvers
},
}
Implementing the posts
resolver with Prisma Client
To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Query: {
posts: (_, args, context) => {
return context.prisma.post.findMany({
where: {
OR: [
{ title: { contains: args.searchString } },
{ content: { contains: args.searchString } },
],
},
})
},
// ... other resolvers
},
}
You can now send the respective query in the GraphQL Playground:
{
posts {
id
title
author {
id
name
}
}
}
3.1.3. Migrate the user(uniqueInput: UserUniqueInput): User
query
In our sample app, the user
query is defined and implemented as follows.
SDL schema definition with prisma-binding
type Query {
user(userUniqueInput: UserUniqueInput): User
# ... other queries
}
input UserUniqueInput {
id: String
email: String
}
Resolver implementation with prisma-binding
const resolvers = {
Query: {
user: (_, args, context, info) => {
return context.prisma.query.user(
{
where: args.userUniqueInput,
},
info
)
},
// ... other resolvers
},
}
Implementing the user
resolver with Prisma Client
To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Query: {
user: (_, args, context) => {
return context.prisma.user.findUnique({
where: args.userUniqueInput,
})
},
// ... other resolvers
},
}
You can now send the respective query via the GraphQL Playground:
{
user(userUniqueInput: { email: "alice@prisma.io" }) {
id
name
}
}
3.1. Migrate GraphQL mutations
In this section, you'll migrate the GraphQL mutations from the sample schema.
3.1.2. Migrate the createUser
mutation (which uses forwardTo
)
In the sample app, the createUser
mutation from the sample GraphQL schema is defined and implemented as follows.
SDL schema definition with prisma-binding
type Mutation {
createUser(data: UserCreateInput!): User!
# ... other mutations
}
Resolver implementation with prisma-binding
const resolvers = {
Mutation: {
createUser: forwardTo('prisma'),
// ... other resolvers
},
}
Implementing the createUser
resolver with Prisma Client
To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Mutation: {
createUser: (_, args, context, info) => {
return context.prisma.user.create({
data: args.data,
})
},
// ... other resolvers
},
}
You can now write your first mutation against the new API, e.g.:
mutation {
createUser(data: { name: "Alice", email: "alice@prisma.io" }) {
id
}
}
3.1.3. Migrate the createDraft(title: String!, content: String, authorId: String!): Post!
query
In the sample app, the createDraft
mutation is defined and implemented as follows.
SDL schema definition with prisma-binding
type Mutation {
createDraft(title: String!, content: String, authorId: String!): Post!
# ... other mutations
}
Resolver implementation with prisma-binding
const resolvers = {
Mutation: {
createDraft: (_, args, context, info) => {
return context.prisma.mutation.createPost(
{
data: {
title: args.title,
content: args.content,
author: {
connect: {
id: args.authorId,
},
},
},
},
info
)
},
// ... other resolvers
},
}
Implementing the createDraft
resolver with Prisma Client
To get the same behavior with the new Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Mutation: {
createDraft: (_, args, context, info) => {
return context.prisma.post.create({
data: {
title: args.title,
content: args.content,
author: {
connect: {
id: args.authorId,
},
},
},
})
},
// ... other resolvers
},
}
You can now send the respective mutation via the GraphQL Playground:
mutation {
createDraft(title: "Hello World", authorId: "__AUTHOR_ID__") {
id
published
author {
id
name
}
}
}
3.1.4. Migrate the updateBio(bio: String, userUniqueInput: UserUniqueInput!): User
mutation
In the sample app, the updateBio
mutation is defined and implemented as follows.
SDL schema definition with prisma-binding
type Mutation {
updateBio(bio: String!, userUniqueInput: UserUniqueInput!): User
# ... other mutations
}
Resolver implementation with prisma-binding
const resolvers = {
Mutation: {
updateBio: (_, args, context, info) => {
return context.prisma.mutation.updateUser(
{
data: {
profile: {
update: { bio: args.bio },
},
},
where: { id: args.userId },
},
info
)
},
// ... other resolvers
},
}
Implementing the updateBio
resolver with Prisma Client
To get the same behavior with Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Mutation: {
updateBio: (_, args, context, info) => {
return context.prisma.user.update({
data: {
profile: {
update: { bio: args.bio },
},
},
where: args.userUniqueInput,
})
},
// ... other resolvers
},
}
You can now send the respective mutation via the GraphQL Playground :
mutation {
updateBio(
userUniqueInput: { email: "alice@prisma.io" }
bio: "I like turtles"
) {
id
name
profile {
id
bio
}
}
}
3.1.5. Migrate the addPostToCategories(postId: String!, categoryIds: [String!]!): Post
mutation
In our sample app, the addPostToCategories
mutation is defined and implemented as follows.
SDL schema definition with prisma-binding
type Mutation {
addPostToCategories(postId: String!, categoryIds: [String!]!): Post
# ... other mutations
}
Resolver implementation with prisma-binding
const resolvers = {
Mutation: {
addPostToCategories: (_, args, context, info) => {
const ids = args.categoryIds.map((id) => ({ id }))
return context.prisma.mutation.updatePost(
{
data: {
categories: {
connect: ids,
},
},
where: {
id: args.postId,
},
},
info
)
},
// ... other resolvers
},
}
Implementing the addPostToCategories
resolver with Prisma Client
To get the same behavior with Prisma Client, you'll need to adjust your resolver implementation:
const resolvers = {
Mutation: {
addPostToCategories: (_, args, context, info) => {
const ids = args.categoryIds.map((id) => ({ id }))
return context.prisma.post.update({
where: {
id: args.postId,
},
data: {
categories: { connect: ids },
},
})
},
// ... other resolvers
},
}
You can now send the respective query via the GraphQL Playground:
mutation {
addPostToCategories(
postId: "__AUTHOR_ID__"
categoryIds: ["__CATEGORY_ID_1__", "__CATEGORY_ID_2__"]
) {
id
title
categories {
id
name
}
}
}
4. Cleaning up
Since the entire app has now been upgraded to Prisma ORM 2, you can delete all unnecessary files and remove the no longer needed dependencies.
4.1. Clean up npm dependencies
You can start by removing npm dependencies that were related to the Prisma 1 setup:
npm uninstall graphql-cli prisma-binding prisma1
4.2. Delete unused files
Next, delete the files of your Prisma 1 setup:
rm prisma1/datamodel.prisma prisma1/prisma.yml
4.3. Stop the Prisma ORM server
Finally, you can stop running your Prisma ORM server.