prisma-binding to SDL-first

Overview

This upgrade guide describes how to migrate a Node.js project that's based on and uses prisma-binding to implement a GraphQL server.

The code will keep the 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 called schema.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 ) is based on importing GraphQL types from the generated prisma.graphql file (in Prisma 1, this is typically called ). 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:

Downloading the GraphQL schema with GraphQL Playground

Alternatively, you can use the get-schema command of the 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 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 (used with Prisma ORM 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
}

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:

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 to Post
  • The profile field denotes a 1-1 relation to Profile

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 to User
  • The categories field denotes a m-n relation to Category

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 :

  • The first, last, before and after arguments are removed
  • The new cursor argument replaces before and after
  • The new take argument replaces first and last

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.