Skip to main content

Prisma 1

Prisma is a "next-gen" ORM which is used together with the GraphQL server of your choice. It is primarily a database toolkit that simplifies database access. Prisma v1 supports the following databases:

  • PostgreSQL
  • MySQL
  • SQLite
  • MongoDB

Prisma 1

To get started you can follow this guide.

You basically need to install:

And you should have docker installed on your machine as well. Now before you setup Prisma you should have your database set up, e.g. for a free PostgreSQL instance you can sign up with Heroku. Also don't forget to get pgadmin.

Then run prima init and answer the questions to create a Prisma project which consists of 3 files:

  1. datamodel.prisma: basically all your types (e.g. type User), not Query, Mutation or input-types
  2. docker-compose.yml: a docker file to run it
  3. prisma.yml: defines the endpoint and name of the data model

Note: You apply your data model to your database via prisma deploy.

Prisma takes the data model - all your types - and generates all possible CRUD definitions from it. When you go to the endpoint (see prisma.yml, e.g. http://localhost:4466), you will see a GraphQL playground and there you can take a look at the full schema. You should treat this schema as your internal API which you can use to create your public API that your GraphQL Server exposes. A convenient way to use this internal API is to install prisma-binding.

Prisma Bindings

To set this up you have to add this npm command to your package.json: "get-schema": "npx get-graphql-schema http://localhost:4466 > src/generated/prisma.graphql". It will get and save the full schema that is based on your data model (datamodel.prisma). Then you can create your Prisma instance and export it like so:

import { Prisma } from 'prisma-binding';

export const prisma = new Prisma({
typeDefs: 'src/generated/prisma.graphql',
endpoint: 'http://localhost:4466'
});

Now you share this variable via the context-mechanism to write typed queries in your resolver. The syntax is prisma.[query | mutation | subscription | exists].[type([args])]. The following (async) query returns (a promise of) the id and email of all users: prisma.query.users(null, '{ id email }'). Note: When using exists you have to write the type capitalized as so: const userExists = await prisma.exists.User({ email });.

Query

A query accepts the following two arguments:

  1. operation args: input variables
  2. selection set: the result you want to get back - it can be null, string or object

This query returns the id, name and email of all users: prisma.query.users(null, '{ id name email }'). If we would pass null as the second argument, we would get back all scalar values only. The third option, passing an object, is very useful, because that object tells prisma what we want. And this object is actually the 4th parameter, the info property. Because the GraphQL server does not know what the client wants to select, therefore we can simply pass the info object to prisma. E.g. users: (parent, args, { prisma }, info) => prisma.query.users(null, info)

Mutation

As mentioned before, prisma generates all kinds of mutations (CRUD) for a given type. These methods are available via prisma.mutation.<method>, e.g.

await prisma.mutation.createUser({ 
data: {
name: 'Tom',
email: 'tom@test.com'
}
}, '{ id name email }');

If we have non-scalar values then we have to use create or connect to set up the proper relation to the table. The following example associates an existing user (author) via connect to the post he/she created:

await prisma.mutation.createPost({ 
data: {
title: 'Tom',
author: {
connet: {
id: 'some-id'
}
}
}
}, '{ id title }');

To be able to delete a user we have to use prisma.mutation.deleteUser and pass the where-property a unique field.

await prisma.mutation.deleteUser({ 
where: {
id: 'some-id'
}
}, '{ id title }');

When deleting something that has relations to other tables we have to be careful to leave the database in an consistent state. To handle that we have to tweak our data model and add the relation-directive (@relation) which takes a name- and onDelete-property. The name-property points to the intersection table, e.g. UserToPost. The onDelete-property must contain one of the following two values:

  • SET_NULL (default): don't remove related data (e.g. when a child gets deleted we don't want to delete the parent)
  • CASCADE: related data should be removed as well (e.g. when a parent gets deleted we want to delete the children)

And to update a user we have to use prisma.mutation.updateUser and pass the data- and where-property.

await prisma.mutation.updateUser({ 
data: {
name: 'Tom Taylor',
},
where: {
id: 'some-id'
}
}, '{ id name }');

Exists

You can easily check if an object exists in the table or not. Prisma generates the exists property for each type. So via const userExists = await prisma.exists.User({ id: 'some-id})`.

Example Code

The following example demonstrates how Prisma gets integrated into an existing GraphQL-Server. It also includes Authorization handling and how one can split the schema and resolver into separate files for better maintainability.

Your GraphQL-Server:

import { GraphQLServer } from 'graphql-yoga'

import { authContext } from './middleware/requireAuth';
import resolvers from './resovler/resolver';
import schemas from './schemas/schemas';
import { prisma } from './prisma-v1';

const server = new GraphQLServer({
typeDefs: schemas,
resolvers,
context: {
user: req => authContext(req),
prisma
}
});

const options = {
port: 4000,
endpoint: '/graphql',
subscriptions: '/subscriptions'
};

server.start(options, ({ port }) =>
console.log(
`Server started, listening on port ${ port } for incoming requests.`,
),
);

The requireAuth.ts:

import { prisma } from '../prisma';

const jwt = require('jsonwebtoken');

export function authContext({ request }) {
return new Promise((resolve) => {
const { authorization } = request.headers;

if (!authorization) {
return resolve({ user: null });
}

const token = authorization.replace('Bearer ', '');

jwt.verify(token, 'MY_SECRET_KEY', async (err, payload) => {
if (err) {
return resolve({ user: null });
}

const { userId } = payload;
const user = await prisma.query.users({where:{id: userId}}, '{ id }');
resolve({ user });
});
});
}

The schema-barrel:

const { mergeTypeDefs } = require('@graphql-tools/merge');

import UserSchema from './UserSchema';
import TrackSchema from './TrackSchema';

export default mergeTypeDefs([UserSchema, TrackSchema]);

A part of the user schema:

export default [`
type Query {
users: [User]
}

type User {
email: String,
password: String
}

//...
`];

The resolvers-barrel:

import UserResolver from './UserResolver';
import TrackResolver from './TrackResolver';

export default [UserResolver, TrackResolver];

A part of the user resolver:

export default {
Query: {
users: (parent, args, { prisma }) => prisma.query.users(null, '{ id email }')
// ...
}
};

When you have to handle an array you have to use the map-operator, e.g. a track consists of multiple locations and each location consists of coordinates. To be able to create a track you have to iterate through all locations and return the data structure that prisma expects.

const { name, locations } = input;

prisma.mutation.createTrack({
data: {
name,
locations: {
create: locations.map(l => {
return {
timestamp: l.timestamp,
coords: {
create: {
...l.coords
}
}
}
})
}
}
});

Locking Prisma down

This section shows you how you can secure or lock down Prisma to not expose it publicly. We only want to use Prisma internally as we have our own public API, otherwise any client could perform literally any operation on our data.

To not publicly expose Prisma we can configure a secret that only Prisma and our GraphQL server know. First, configure a secret in prisma.yml:

endpoint: http://localhost:4466
datamodel: datamodel.prisma
secret: my_secret

Then pass the same secret to our Prisma instance:

import { Prisma } from 'prisma-binding';

export const prisma = new Prisma({
typeDefs: 'src/generated/prisma.graphql',
endpoint: 'http://localhost:4466',
secret: 'my_secret'
});

To be able to still use Prisma from the playground we now have to generate a token and provide that as Authorization-header in our playground. To do that run prisma token. Your response will be a JWT token (see below).

> prisma token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InNlcnZpY2UiOiJkZWZhdWx0QGRlZmF1bHQiLCJyb2xlcyI6WyJhZG1pbiJdfSwiaWF0IjoxNjA4MjgzOTg0LCJleHAiOjE2MDg4ODg3ODR9.8ezmEeTsJoJIDPqQ1DprxnZqSTmenjqGDI-rRNXjhAE

Now use this token as Authorization-header like so:

{
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InNlcnZpY2UiOiJkZWZhdWx0QGRlZmF1bHQiLCJyb2xlcyI6WyJhZG1pbiJdfSwiaWF0IjoxNjA4MjgzOTg0LCJleHAiOjE2MDg4ODg3ODR9.8ezmEeTsJoJIDPqQ1DprxnZqSTmenjqGDI-rRNXjhAE"
}

Also, as the endpoint is locked down, we have to provide the same token to our get-schema-command in our package.json like so: "get-schema": "npx get-graphql-schema -h 'Authorization=Bearer <token>' http://localhost:4466 > src/generated/prisma.graphql",