Advanced GraphQL Directive Permissions with Prisma

Jordan Last
6 min readMar 12, 2018

--

A deeper dive into a sane Prisma setup with declarative GraphQL directive permissions.

Licensing

Let’s just consider all of my code examples here in the public domain as far as is possible.

Prerequisites

I won’t be covering the basics of GraphQL directive permissions here. If you need an introduction, please take a look at the following resources:

You should also already be somewhat familiar with Prisma. I’m going to walk through how I’ve setup my Prisma project.

Background

I work for an education technology startup called Prendus. We’re still in the very early stages, and we’re heavily investing in our infrastructure right now. Because of the amazing promise of GraphQL, we decided to make the jump last summer. GraphQL has been a joy to work with, and we haven’t looked back since.

In our attempts to save time and simplify development, we decided to use Graphcool for our backend infrastructure. It allowed us to move very quickly, but we ran into some limitations. In the midst of these limitations, the Graphcool team was always coming up with solutions, and so we decided to ride it out. I love the Graphcool team. They have been amazing to work with.

Their latest pivot is Prisma. At first, I was very confused. What was going to happen to the old Graphcool that I had come to know and love? Was my dream of simple infrastructure and a powerful, flexible, automatically generated GraphQL API vanishing?

Prisma was unknown. Prisma looked complicated. I tried to fight the current, but eventually I decided it was time to make the switch. I’m very happy I did. It’s taken a bit of work over the last few days to create the GraphQL experience that I want with Prisma, but I think it’s going to pay off in the end. It’s still early days for my project structure, but let me share with you what I’ve found.

Keep in mind that the project structure and code I am going to show was built to simplify Prisma and allow for powerful directive permissions. It isn’t perfect, and I’m still learning. I hope this provides some direction for others and that we can work together to create the best GraphQL experience possible.

Directory Structure

I decided to mostly throw out the automatically generated directory structure. Here’s what I came up with:

prendus-app/
/backend
/directive-resolvers
index.ts
/generated
prisma.graphql
prisma.ts
/node_modules
/resolvers
index.ts
/schema
datamodel.graphql
dataops.graphql
directives.graphql
/services
utilities.ts
.gitignore
.graphqlconfig.yml
index.ts
package-lock.json
package.json
prisma.yml

The root directory of my application is prendus-app. There are other files and folders in prendus-app to support the frontend web application. All of the backend code I’ve decided to put into the backend directory.

Let’s go through the important pieces one by one.

Schema

The following three files define our entire custom schema:

datamodel.graphql

dataops.graphql

This is where we add custom directives to the generated mutations. We only need to copy over the mutations that we want to put permissions on. As far as I know, we do not need to put any custom directives on generated queries, because queries return basic types, and we should have already put all of the custom directives that we want on our basic types in datamodel.graphql.

directives.graphql

Separating out our schema into these three files minimizes the amount of duplicate code we need to implement directive permissions and expose our generated and custom GraphQL API to the client.

index.ts

This is the entry point to our GraphQL application server. Prisma makes an important distinction between the GraphQL application server and the Prisma database server. There really are two servers running here. The Prisma database server exposes your entire generated schema (prisma.graphql). The GraphQL application server that we’re creating in index.ts mediates access to the Prisma database server. This is the server that we are choosing to expose to the public. You could expose the entire Prisma database server to the public if you wanted to. You would lose out on things like authorization though (all your data would be exposed). Perhaps Prisma could change and simplify this whole two server thing in the future (I’ve got some thoughts on this if anyone is interested), but for now this is what we’re dealing with. And finally, here’s index.ts:

Let’s explore what’s happening. I’ll skip the imports, they should be self-explanatory. The first thing that might not make sense is the following:

If our directive permissions are going to be powerful enough to handle advanced use cases like checking arbitrary fields against the current user, we’re going to need to manipulate incoming queries and request arbitrary fields to send to our directive resolvers. We do this by adding fragment definitions to our resolver definitions.

addFragmentToFieldResolvers

Let’s take a look at addFragmentToFieldResolvers:

Given a schema AST and a selection for the fragment, this function will go through each of your ObjectTypeDefinitions and apply the fragment to each field on that type. Without this function, we would have to create a custom fragment definition and resolver definition for each field on each type…that could get messy.

Back in our index.ts file, the first thing we do is prepare our field resolvers for each of our basic data types by passing in the AST for datamodel.graphql. Next we pass the preparedFieldResolvers to the extractFragmentReplacements function from the prisma-binding package. We pass the result of that into the Prisma constructor configuration object as the fragmentReplacements property. Now the queries that Prisma handles will return the extra fields that the user has not defined, but that we will need for our directive resolvers.

prepareTopLevelResolvers

First for some background. The following may be controversial, but I think it is the better way. With the Graphcool framework, I had an entire database with all of the basic queries and mutations I would want generated automatically. This was amazingly simple and allowed me to move quickly. Switching over to Prisma, some of that simplicity was lost. I still had access to the automatically generated queries and mutations, but Prisma required me to manually create resolvers in my GraphQL application to hook up to the automatically generated queries and mutations. Boilerplate! I didn’t want to have to maintain this extra hookup code, so I decided not to.

Let’s take a look at prepareTopLevelResolvers:

This simply walks through each of the resolvers in the query or mutation field of the generated Prisma class, and creates the resolver that can be exposed in our GraphQL application server. Simple and clean. No more boilerplate.

But wait, isn’t the GraphQL application server protecting your database from being completely exposed to the outside world? Won’t manually hooking up each generated resolver allow you to expose only what you want, thus keeping your data safe? Sure, that’s one way to go about it. Maybe for your use case that is best. But directive permissions provide another way. We’ll get to those soon, and you can make the choice for yourself.

Resolvers

Now let’s hook up all of our resolvers:

As you can see, we have one top-level resolvers object. We simply spread the query resolvers and the mutation resolvers into their correct places, while adding any custom resolvers that we want. We also throw in the field resolvers that we automatically created to help us with our directive permissions. Done.

Directive Resolvers

Finally we can reap the benefits of all of this extra setup:

Let’s look at each resolver individually.

userOwnsDirectiveResolver

Honestly, this isn’t fully implemented yet. But if you only need the top-level field of the type that you’re querying, this should work fine. The fragments that we added to each field on each type will put their data into the source parameter. Because we requested the id to be added to our query, our source parameter has an id property. Note that the client is free to make queries without the id property, but we will always have access to the id property in any of our directive resolvers.

authenticatedDirectiveResolver

privateDirectiveResolver

I really like the private directive resolver. As you can see, we have some extremely granular control over authorization with directive permissions…perhaps even arbitrary control.

And now for the final piece:

We use a really neat package called merge-graphql-schemas to merge all four of our schemas (3 custom, 1 generated) to create the Ultimate Schema. This schema has all of our directives, custom queries, custom mutations, custom types, and generated queries, generated mutations, and generated types. We pass this schema into the GraphQLServer constructor and boot up our application server. Please note that this pull request needs to go through before you can depend directly on merge-graphql-schemas for our purposes. You can depend on the forked version in the pull request in the meantime.

Final Thoughts

These are early ideas. This is the structure that I will be building off of as I move forward with Prisma and GraphQL. I’m sure that it needs improvement, and maybe there are some fundamental issues here. I hope this article provides some direction and opens up the conversation toward more simple yet powerful GraphQL backends with powerful directive permissions.

--

--