WordPress with Node, React, and GraphQL (Part 3— The Schema)

What follows is my attempt to replace PHP with Javascript in WordPress development.

Ramsay Lanier
nclud
8 min readJan 25, 2016

--

Part 2 — The Setup
Part 1—The Introduction

Update: Since originally posting this article, I’ve created a NPM package that takes care of connecting to the WordPress database and defining Sequelize models. Check it out on GitHub, or read about it in this introduction. You should still read on to see how it works, but know that I made it much easier for you!

Announcement: I’ve launched WordExpress.io which will contain more detailed documentation about this project as it unfolds. Right now, it will just replicate these Medium posts.

In the post, I’ll discuss in detail how the GraphQL schema works in conjunction with WordPress and Apollo on the server side (I’ll get into the client side in another post). I’ll be using Sequelize to connect to the WordPress database, so we won’t have to write any MYSQL queries. It’s going to be a long-ish post, so grab a Fresca, and let's get to it.

Using Sequelize To Define a Connection and Models

As discussed last post, ApolloServer requires two things: a schema, and resolvers. Resolvers are simply functions that go and get data from somewhere and return it in a form that matches your GraphQL schema. In order to create our resolvers, we need to create connectors. Connectors are used to connect the resolvers to a backend that stores the actual data. In this case, I’m creating connectors that will connect to my WordPress MySQL database.

To make life easy, I’ll be using Sequelize. Sequelize is pretty great. From their website, “Sequelize is a promise-based ORM for Node.js and io.js. It supports the dialects PostgreSQL, MySQL, MariaDB, SQLite and MSSQL and features solid transaction support, relations, read replication and more.”

Basically, we’re going to provide it some database connection details; we’ll then use that connection object to define models and export a query object that has a bunch of functions that will feed a GraphQL schema.

The Connection Setup

// ./schema/connectors.js import { publicSettings, privateSettings } from '../settings/settings';import WordExpressDatabase from './db';const { name, username, password, host } = privateSettings.database;
const { amazonS3, uploads } = publicSettings;
const connectionDetails = {
name: name,
username: username,
password: password,
host: host,
amazonS3: amazonS3,
uploadDirectory: uploads
}
const Database = new WordExpressDatabase(connectionDetails);
const Connectors = Database.connectors;
export default Connectors;

We import our production and development settings, which are determined by the Node ENV variable. This is important because you will probably have a development database and a production database. Then, we define a connection details using the determined settings.

Using the connection details, we import WordExpressDatabase. WordExpressDatabase is a class that has some methods that are used to connect to the database, define the various data models, and provide the required connectors! The various parts of WordExpressDatabase are defined below.

The Connection

// ./schema/db.js//...const { name, username, password, host} = this.connectionDetails;
const Conn = new Sequelize(
name,
username,
password,
{
dialect: ‘mysql’,
host: host,
define: {
timestamps: false,
freezeTableName: true,
}
}
);
//...

We pass in the connectionDetails when we create a new instance of WordExpressDatabase in the ./schema/connection.js file. This contains all the database details contained in our public/private settings files. We’ll use this connection when we define the data models.

Data Models

Let's take a look at the Post model as an example.

// ./schema/db.js//...const Conn = this.connection;return {Post: Conn.define(prefix + 'posts', {
id: { type: Sequelize.INTEGER, primaryKey: true},
post_author: { type: Sequelize.INTEGER },
post_title: { type: Sequelize.STRING },
post_content: { type: Sequelize.STRING },
post_excerpt: { type: Sequelize.STRING },
post_status:{ type: Sequelize.STRING },
post_type:{ type: Sequelize.STRING },
post_name:{ type: Sequelize.STRING},
post_parent: { type: Sequelize.INTEGER},
menu_order: { type: Sequelize.INTEGER}
}),
//...

Here I’m defining a Post model using object keys that map directly to the posts table in the WordPress database. Notice that I’m using the wp_prefix from private settings. By default, Wordpress uses a “wp_” prefix, but you can change this and people often do for security reasons. For my setup I’m using the default extension, so this translates to “wp_posts”.

Each key must have a Sequelize type — for WordPress these will always either be Strings or Integers. Notice that for the “id” key, we also set it to be the primary key. This is important for building relationships when querying (which I’ll get into shortly).

You don’t need to include EVERY column in the SQL table you are defining your model from. Indeed, a lot of the columns we don’t really need on the front-end.

Side Note: If you don’t have any SQL management software, check out Sequel Pro. It’s free, and it helps alot when building out models and schemas.

Defining Relationships

Here is the definition of the Postmeta model. A Post hasmany Postmeta, and Postmeta belongsTo a Post. First, let’s define a Postmeta model. Then, let’s define the relationsip. Sequelize makes this VERY easy.

// ./schema/db.jsreturn{...Postmeta: Conn.define(prefix + 'postmeta', {
meta_id: { type: Sequelize.INTEGER, primaryKey: true, field: 'meta_id' },
post_id: { type: Sequelize.INTEGER },
meta_key: { type: Sequelize.STRING },
meta_value: { type: Sequelize.INTEGER },
}),
...Post.hasMany(Postmeta, {foreignKey: ‘post_id’});
Postmeta.belongsTo(Post, {foreignKey: ‘post_id’});

As you can see, the Postmeta model has a post_id field, which is the primary key for the Post model. We can relate these two by defining a hasMany/belongsTo relationship. Now, we can perform a query for a Post and include all of its Postmeta.

Defining The Database Queries

So, not that we have a basic Post model setup, we’ll need some functions that a GraphQL query can resolve. These functions are also defined in the db.js file, and are comprised of Sequelize queries. Lets take a look at the basic getPostByName function.

// ./schema/db.jsgetConnectors(){...return{
getPostByName(name){
return Post.findOne({
where: {
post_status: 'publish',
post_name: name
}
})
},
}
...

getPostByName accepts the name of a post (aka its slug) and finds one that is published. This will return a Promise to our GraphQL query, which is great because GraphQL knows how to handle promises. Notice, we are just finding one. Lets create a query that finds all posts by post type.

// .schema/db.jsgetPosts(args){

const { post_type, limit = 10, skip = 0 } = args;
return Post.findAll({
where: {
post_type: post_type,
post_status: 'publish',
},
limit: limit,
offset: skip
})
},

The only difference here is that we are using findAll, which will still return a promise, but it returns an array of promises, which is an important distinction that will matter later. So, we’ve defined a Post and Postmeta — now, let’s look at the GraphQL side of things. Without GraphQL/Apollo, we don’t have an easy way of querying this stuff from the client side.

Defining the GraphQL Schema

We’re going to take a bottoms up approach here. In order to define the schema, we need to define the root query, which contains other fields that need to be defined. But it’s the most simple approach. For reference, the entire GraphQL schema, written in GraphQL type language, is located in the ./schema/typeDefinitions.js file.

Here’s the schema definition:

schema {
query: Query
}

Simple, right? Yup.

Defining the Root Query

The root query looks like this:

type Query {
settings: Setting
posts(post_type: String = "post", limit: Int, skip: Int): [Post]
menus(name: String): Menu
page(name: String): Post
postmeta(post_id: Int, after: String, first: Int, before: String, last: Int): Postmeta
}

The root query provides several “entry points”. Entry points can take arguments. For example, the posts entry point takes a post_type argument that is of type string. If no argument is provided, the default is “post”. We can also limit the number of returned posts, and skip a certain number of posts. This is useful for pagination. Also note that at the end of the posts entry point declaration, there is a [Post]. [Post] means that the posts entry point is expecting to return a list of Posts, which are another GraphQL type.

The Post type looks like this:

type Post {
id: Int
post_title: String
post_content: String
post_excerpt: String
post_status: String
post_type: String
post_name: String
menu_order: Int
layout: Postmeta
thumbnail: String
post_meta(keys: [MetaType], after: String, first: Int, before: String, last: Int): Postmeta
}

This should look familiar, as it contains all of the fields (plus a few extras) from the Post model defined above in the db.js file. The extra fields (layout and post_meta) are not tables in the MySQL database. Instead, they are of the Postmeta GraphQL type.

This all makes sense when you look at the entire GraphQL schema.

Defining the Resolvers

Remeber when I said the ApolloServer takes two very important arguments — schema and resolvers. We’ve covered the schema, which is everything in the typeDefinitions.js file. Now, we need to discuss the resolvers.

The entire resolveFunctions.js file is pretty small. It looks like this:

First, we import Connectors. Remember, getPostsByName(name) and getPosts(post_type) discussed above are part of Connectors. Then, we create a resolveFunctions object that contains all of the functions that resolve stuff. The import thing to note here is that for every field in our GraphQL schema that is of a non enumerable type (ie. Post, Postmeta, Menu, etc), we are required to have a resolving function. The best example of this is the Query type. Remember in our Query schema we had 5 fields — Settings, Posts, Page, Menu and Postmeta. Notice in the resolving functions above that each one of those fields has a function that calls a specific connector (except for Settings, because that information isn’t stored in a database).

So, on the client (which I’ll get into in another post) when I go to, say, the “Blog” page, a PostList component renders that queries Posts. When that query is run, it calls the Query:Posts resolving function which then calls Connectors.getPosts(), which in turn runs a MySQL query (thanks to Sequelize) that returns all of our posts. You can see below how it works using GraphiQL.

The wonderful thing about GraphiQL is that it autocompletes for you. So, if you start with a blank slate you can get to where you want to be quickly. Play around with it!

Next Steps

The WordExpress project has a slightly more complex schema than displayed here. I urge your to clone the repo and play with it yourself. I’m updating documentation on GitHub pretty regularly. If you need help, submit an issue. If you want to contribute, submit a pull request!

In the next article, I’ll show you how routing works, and we’ll look at how the Landing Page of WordExpress.io was built using our shiny new GraphQL/Apollo implementation.

Ramsay Lanier is a Sr. Developer at nclud, a provocative digital agency in Washington, D.C. You can find him on twitter, github, or at &pizza.

--

--

Ramsay Lanier
nclud
Writer for

I’m a senior web developer at @Novettasol and senior dad-joke developer at home.