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 layer. This means you already:
- installed the Prisma 2 CLI
- created your Prisma 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 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 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):
# 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 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 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 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: Stringname: String!posts(where: PostWhereInputorderBy: Enumerable<PostOrderByInput>skip: Intafter: Stringbefore: Stringfirst: Intlast: Int): [Post!]role: Role!profile: ProfilejsonData: 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 {idnameposts {# fetching this relation is enabled by the new type resolveridtitle}profile {# fetching this relation is enabled by the new type resolveridbio}}}
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: Stringpublished: Boolean!author: Usercategories(where: CategoryWhereInputorderBy: Enumerable<CategoryOrderByInput>skip: Intafter: Stringbefore: Stringfirst: Intlast: 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 {idtitleauthor {# fetching this relation is enabled by the new type resolveridname}categories {# fetching this relation is enabled by the new type resolveridname}}}
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: Stringuser: 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: PostWhereInputorderBy: Enumerable<PostOrderByInput>skip: Intafter: Stringbefore: Stringfirst: Intlast: 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 yetconst { where, orderBy, skip, first, last, after, before } = argsreturn 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:
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 workconst { where, orderBy, skip, first, last, after, before } = argsconst 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 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 } = argsconst prisma2Where = makeWherePrisma2Compatible(where)const prisma2OrderBy = makeOrderByPrisma2Compatible(orderBy)const skipValue = skip || 0const prisma2Skip = Boolean(before) ? skipValue + 1 : skipValueconst prisma2Take = Boolean(last) ? -last : firstconst prisma2Before = { id: before }const prisma2After = { id: after }const prisma2Cursor =!Boolean(before) && !Boolean(after)? undefined: Boolean(before)? prisma2Before: prisma2Afterreturn 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 {idtitleauthor {idname}}}
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: Stringemail: 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" }) {idname}}
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__") {idpublishedauthor {idname}}}
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") {idnameprofile {idbio}}}
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__"]) {idtitlecategories {idname}}}
4. Cleaning up
Since the entire app has now been upgraded to Prisma 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:
4.2. Delete unused files
Next, delete the files of your Prisma 1 setup:
4.3. Stop the Prisma server
Finally, you can stop running your Prisma server.