This post originally appeared on the RisingStack blog.
With the Mongoose adapter for Graffiti, you can use your existing Mongoose schema for developing a GraphQL application.
We are going to cover the following topics:
- Introduction to Graffiti
- The Mongoose adapter
- Relay & GraphQL
- Getting started with Graffiti
- Graffiti TodoMVC - a Relay example
Introduction to Graffiti
Developers generally tend to be lazy. We usually don’t write boilerplate code. GraphQL is great, but manually specifying the schema can be painful. That’s the reason we created Graffiti.
Graffiti consists of two main components. You can use graffiti to add a GraphQL endpoint to your web server. Either you can use a schema generated by an adapter, or you can pass in your own. An adapter like graffiti-mongoose can generate the GraphQL schema from your database specific schema description.
The Mongoose adapter for Graffiti
Graffiti currently has an adapter for the Mongoose ORM - with more adapters to come later.
Graffiti Mongoose can help you to use your existing Mongoose schema to generate a Relay compatible GraphQL schema.
We will use the following schema throughout this blog post:
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({
name: {
type: String
},
age: {
type: Number,
index: true
},
createdAt: {
type: Date,
default: Date.now()
},
friends: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
]
});
const User = mongoose.model('User', UserSchema);
export default User;
The generated GraphQL schema looks the following:
input addUserInput {
name: String
age: Float
createdAt: Date
friends: [ID]
clientMutationId: String!
}
type addUserPayload {
viewer: Viewer
changedUserEdge: changedUserEdge
clientMutationId: String!
}
type changedUser {
name: String
age: Float
createdAt: Date
friends(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): friendsConnection
_id: ID
id: ID!
}
type changedUserEdge {
node: changedUserNode
cursor: String!
}
type changedUserNode {
name: String
age: Float
createdAt: Date
friends(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): friendsConnection
_id: ID
id: ID!
}
scalar Date
input deleteUserInput {
id: ID!
clientMutationId: String!
}
type deleteUserPayload {
viewer: Viewer
ok: Boolean
id: ID!
clientMutationId: String!
}
type friendsConnection {
pageInfo: PageInfo!
edges: [friendsEdge]
count: Float
}
type friendsEdge {
node: User
cursor: String!
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type RootMutation {
addUser(input: addUserInput!): addUserPayload
updateUser(input: updateUserInput!): updateUserPayload
deleteUser(input: deleteUserInput!): deleteUserPayload
}
type RootQuery {
viewer: Viewer
node(id: ID!): Node
user(id: ID!): User
users(id: [ID], ids: [ID], name: String, age: Float, createdAt: Date, _id: ID): [User]
}
input updateUserInput {
name: String
age: Float
createdAt: Date
friends: [ID]
id: ID!
clientMutationId: String!
}
type updateUserPayload {
changedUser: changedUser
clientMutationId: String!
}
type User implements Node {
name: String
age: Float
createdAt: Date
friends(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): friendsConnection
_id: ID
id: ID!
}
type UserConnection {
pageInfo: PageInfo!
edges: [UserEdge]
count: Float
}
type UserEdge {
node: User
cursor: String!
}
type Viewer implements Node {
id: ID!
users(after: String, first: Int, before: String, last: Int, name: String, age: Float, createdAt: Date, _id: ID): UserConnection
user(id: ID!): User
}
The road towards Relay compatibility
Relay is a framework for building data-driven React applications. You can declare your data requirements using GraphQL for each component and Relay handles the requests efficiently. Relay makes a few assumptions about the GraphQL schema that is provided by the GraphQL server.
The Node interface
Every type must implement the Node interface, which contains a single id
field. This is a globally unique identifier encoding the type and type-specific ID. This makes it possible to re-fetch objects using only the id
.
query ReFetch {
node(id: "VXNlcjo1NjQwOThlNTI3ZjUyYTg0NTBiNWE5NDQ=") {
__typename
... on User {
name
age
}
}
}
{
"data": {
"node": {
"__typename": "User",
"name": "User0",
"age": 0
}
}
}
Pagination and the Connection type
The pagination and slicing rely on the standardized Connection
type. We can use an introspection query to see what it looks like.
query ConnectionTypeIntrospection {
__type(name: "UserConnection") {
name
fields {
name
type {
kind
ofType {
name
kind
fields {
name
}
}
}
}
}
}
{
"data": {
"__type": {
"name": "UserConnection",
"fields": [
{
"name": "pageInfo",
"type": {
"kind": "NON_NULL",
"ofType": {
"name": "PageInfo",
"kind": "OBJECT",
"fields": [
{
"name": "hasNextPage"
},
{
"name": "hasPreviousPage"
},
{
"name": "startCursor"
},
{
"name": "endCursor"
}
]
}
}
},
{
"name": "edges",
"type": {
"kind": "LIST",
"ofType": {
"name": "UserEdge",
"kind": "OBJECT",
"fields": [
{
"name": "node"
},
{
"name": "cursor"
}
]
}
}
},
{
"name": "count",
"type": {
"kind": "SCALAR",
"ofType": null
}
}
]
}
}
}
The edge type describes the collection, and the pageInfo contains metadata about the current page. We have also added a count field, which can be very handy in certain situations. Slicing is done via the passed in arguments: first
, after
, last
and before
. For example, we could ask for the first two users after a specified cursor on the viewer root field.
query UsersPagination {
viewer {
users(first: 2, after: "Y29ubmVjdGlvbi41NjQwOWMwZjU1MzBmOGFhNTBlM2NjZWE=") {
count
edges {
cursor
node {
name
}
}
}
}
}
{
"data": {
"viewer": {
"users": {
"count": 98,
"edges": [
{
"cursor": "Y29ubmVjdGlvbi41NjQwOWMwZjU1MzBmOGFhNTBlM2NjZWI=",
"node": {
"name": "User2"
}
},
{
"cursor": "Y29ubmVjdGlvbi41NjQwOWMwZjU1MzBmOGFhNTBlM2NjZWM=",
"node": {
"name": "User3"
}
}
]
}
}
}
}
Mutations
Mutations like add, update and delete are also supported. Let’s try to add a new user!
mutation AddUser {
addUser(input: {clientMutationId: "1", name: "New Usr"}) {
changedUserEdge {
node {
id
name
createdAt
}
}
}
}
{
"data": {
"addUser": {
"changedUserEdge": {
"node": {
"id": "VXNlcjo1NjQwYTJiNTU1MzBmOGFhNTBlM2NkNGQ=",
"name": "New Usr",
"createdAt": "2015-11-09T13:13:51.523Z"
}
}
}
}
}
As you can see, we just made a typo. We can fix the user’s name by using an update mutation.
mutation FixName {
updateUser(input: {clientMutationId: "2", id: "VXNlcjo1NjQwYTJiNTU1MzBmOGFhNTBlM2NkNGQ=", name: "New User"}) {
changedUser {
name
}
}
}
{
"data": {
"updateUser": {
"changedUser": {
"name": "New User"
}
}
}
}
Nice, isn’t it?
Resolve hooks
Most likely you’ll need some custom logic in your application. For example, to authorize a request or to filter certain fields before returning it to the client. You can specify pre- and post-resolve hooks to extend the functionality of Graffiti Mongoose. You can add hooks to type fields and query fields (singular & plural queries, mutations) too. By passing arguments to the next function, you can modify the parameters of the next hook or the return value of the resolve function.
For example, this pre-mutation hook filters bad words.
const filter = new BadWordsFilter();
const hooks = {
mutation: {
pre: (next, todo, ...rest) => {
if (todo.text) {
todo.text = filter.clean(todo.text);
}
next(todo, ...rest);
}
}
};
const schema = getSchema(mongooseSchema, {hooks});
Creating a GraphQL server
First, we need to define our Mongoose models.
We all like pets, right? For our application, we’ll keep track of users and pets. Let’s define the User and Pet models!
import mongoose from 'mongoose';
const PetSchema = new mongoose.Schema({
name: {
type: String
},
type: {
type: String
},
age: {
type: Number
}
});
const Pet = mongoose.model('Pet', PetSchema);
export default Pet;
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({
name: {
type: String
},
age: {
type: Number,
index: true
},
friends: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}],
pets: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Pet'
}],
createdAt: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', UserSchema);
export default User;
We can generate the GraphQL schema from the Mongoose models using graffiti-mongoose.
import mongoose from 'mongoose';
import User from './user';
import Pet from './pet';
import {getSchema} from '@risingstack/graffiti-mongoose';
mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost/graphql');
export default getSchema([Pet, User]);
Now, we can add graffiti to the project.
import express from 'express';
import graffiti from '../';
import schema from './schema';
const app = express();
app.use(graffiti.express({
schema
}));
app.listen(3001, (err) => {
if (err) {
throw err;
}
console.log('Express server is listening on port 3001');
});
Our server is ready to use. You can use GraphiQL, an in-browser GraphQL IDE, to explore our GraphQL API by navigating to localhost:3001/graphql.
You can find examples for koa and hapi alongside express in the main repository.
TodoMVC
To demonstrate the Relay compatibility, we also created a Relay application based on the well known TodoMVC. The source code can be found here.
You can take a look here if you want to try it out: graffiti-todo.herokuapp.com.