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:
datamodel.prisma
: basically all your types (e.g.type User
), notQuery
,Mutation
orinput
-typesdocker-compose.yml
: a docker file to run itprisma.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:
- operation args: input variables
- 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",