├── .npmignore ├── .babelrc ├── .gitignore ├── src ├── schema │ ├── setting.js │ ├── inputs │ │ └── orderInput.js │ ├── menu.js │ ├── metaType.js │ ├── menuItem.js │ ├── user.js │ ├── postmeta.js │ ├── thumbnail.js │ ├── category.js │ ├── post.js │ └── schema.js ├── index.js ├── modules │ ├── Term │ │ ├── connectors │ │ │ ├── index.js │ │ │ └── getTerm.js │ │ └── model.js │ ├── User │ │ ├── connectors │ │ │ ├── index.js │ │ │ └── getUser.js │ │ └── model.js │ ├── Postmeta │ │ ├── connectors │ │ │ ├── index.js │ │ │ └── getPostmeta.js │ │ └── model.js │ ├── Menu │ │ └── connectors │ │ │ ├── index.js │ │ │ └── getMenu.js │ ├── Post │ │ ├── connectors │ │ │ ├── getPostLayout.js │ │ │ ├── getPostTerms.js │ │ │ ├── index.js │ │ │ ├── getPosts.js │ │ │ ├── getPost.js │ │ │ └── getTermPosts.js │ │ └── model.js │ └── Thumbnail │ │ ├── connectors │ │ ├── index.js │ │ ├── getThumbnails.js │ │ └── getPostThumbnail.js │ │ └── shapeThumbnail.js ├── resolvers.js └── db.js ├── .eslintrc.json ├── History.md ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | lib 5 | /yarn.lock 6 | -------------------------------------------------------------------------------- /src/schema/setting.js: -------------------------------------------------------------------------------- 1 | const Setting = ` 2 | type Setting { 3 | uploads: String 4 | amazonS3: Boolean 5 | } 6 | ` 7 | 8 | export default Setting -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export WordExpressDatabase from './db' 2 | export WordExpressResolvers from './resolvers' 3 | export WordExpressDefinitions from './schema/schema' 4 | -------------------------------------------------------------------------------- /src/modules/Term/connectors/index.js: -------------------------------------------------------------------------------- 1 | import getTerm from './getTerm' 2 | 3 | export default function ({Terms}) { 4 | return { 5 | getTerm: getTerm(Terms) 6 | } 7 | } -------------------------------------------------------------------------------- /src/modules/User/connectors/index.js: -------------------------------------------------------------------------------- 1 | import getUser from './getUser' 2 | 3 | export default function ({User}) { 4 | return { 5 | getUser: getUser(User) 6 | } 7 | } -------------------------------------------------------------------------------- /src/schema/inputs/orderInput.js: -------------------------------------------------------------------------------- 1 | const OrderInput = ` 2 | input OrderInput { 3 | orderBy: String, 4 | direction: String 5 | } 6 | ` 7 | 8 | export default OrderInput -------------------------------------------------------------------------------- /src/modules/Postmeta/connectors/index.js: -------------------------------------------------------------------------------- 1 | import getPostmeta from './getPostmeta' 2 | 3 | 4 | export default function ({Postmeta}) { 5 | return { 6 | getPostmeta: getPostmeta(Postmeta) 7 | } 8 | } -------------------------------------------------------------------------------- /src/schema/menu.js: -------------------------------------------------------------------------------- 1 | import MenuItem from './menuItem' 2 | 3 | const Menu = ` 4 | type Menu { 5 | id: ID! 6 | name: String 7 | items: [MenuItem] 8 | } 9 | ` 10 | 11 | export default () => [Menu, MenuItem] -------------------------------------------------------------------------------- /src/schema/metaType.js: -------------------------------------------------------------------------------- 1 | const MetaType = ` 2 | enum MetaType { 3 | _thumbnail_id 4 | _wp_attached_file 5 | react_layout 6 | amazonS3_info 7 | order 8 | } 9 | ` 10 | 11 | export default MetaType -------------------------------------------------------------------------------- /src/modules/Menu/connectors/index.js: -------------------------------------------------------------------------------- 1 | import getMenu from './getMenu' 2 | 3 | export default function ({Post, Postmeta, Terms, TermRelationships}) { 4 | return { 5 | getMenu: getMenu(Post, Postmeta, Terms, TermRelationships) 6 | } 7 | } -------------------------------------------------------------------------------- /src/schema/menuItem.js: -------------------------------------------------------------------------------- 1 | import Post from './post' 2 | 3 | const MenuItem = ` 4 | type MenuItem { 5 | id: ID! 6 | linkedId: Int 7 | order: Int 8 | navitem: Post 9 | children: [MenuItem] 10 | } 11 | ` 12 | 13 | export default () => [MenuItem, Post] -------------------------------------------------------------------------------- /src/schema/user.js: -------------------------------------------------------------------------------- 1 | const User = ` 2 | type User { 3 | id: Int 4 | user_nicename: String 5 | user_email: String 6 | user_registered: String 7 | display_name: String, 8 | posts(post_type: String): [Post] 9 | } 10 | ` 11 | 12 | export default User -------------------------------------------------------------------------------- /src/modules/Post/connectors/getPostLayout.js: -------------------------------------------------------------------------------- 1 | export default function (Postmeta) { 2 | return function (postId) { 3 | return Postmeta.findOne({ 4 | where: { 5 | post_id: postId, 6 | meta_key: 'page_layout_component' 7 | } 8 | }) 9 | } 10 | } -------------------------------------------------------------------------------- /src/schema/postmeta.js: -------------------------------------------------------------------------------- 1 | import Post from './post' 2 | 3 | const Postmeta = ` 4 | type Postmeta { 5 | meta_id: Int 6 | post_id: Int 7 | meta_key: String 8 | meta_value: String 9 | connecting_post: Post 10 | } 11 | ` 12 | 13 | export default () => [Postmeta, Post] -------------------------------------------------------------------------------- /src/schema/thumbnail.js: -------------------------------------------------------------------------------- 1 | const Thumbnail = ` 2 | type Thumbnail { 3 | id: Int 4 | src: String 5 | sizes: [ThumbnailSize] 6 | } 7 | ` 8 | 9 | const Size = ` 10 | type ThumbnailSize { 11 | size: String, 12 | file: String 13 | } 14 | ` 15 | 16 | export default [Thumbnail, Size] -------------------------------------------------------------------------------- /src/schema/category.js: -------------------------------------------------------------------------------- 1 | import Post from './post' 2 | 3 | const Category = ` 4 | type Category { 5 | term_id: Int! 6 | name: String 7 | slug: String 8 | posts(post_type: String = "post", limit: Int, skip: Int, order: OrderInput): [Post] 9 | } 10 | ` 11 | 12 | export default () => [Category, Post] -------------------------------------------------------------------------------- /src/modules/Term/connectors/getTerm.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | const Op = Sequelize.Op 3 | 4 | export default function (Terms) { 5 | return function (termId, name) { 6 | return Terms.findOne({ 7 | where: { 8 | [Op.or]: [{term_id: termId}, {name: name}] 9 | } 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/User/connectors/getUser.js: -------------------------------------------------------------------------------- 1 | export default function (User) { 2 | return function({userId, name}) { 3 | 4 | let where = {} 5 | 6 | if (userId){ 7 | where.ID = userId 8 | } 9 | 10 | if (name){ 11 | where.user_nicename = name 12 | } 13 | 14 | return User.findOne({ 15 | where: where 16 | }) 17 | } 18 | } -------------------------------------------------------------------------------- /src/modules/Thumbnail/connectors/index.js: -------------------------------------------------------------------------------- 1 | 2 | import getPostThumbnail from './getPostThumbnail' 3 | import getThumbnails from './getThumbnails' 4 | 5 | export default function ({Post, Postmeta, Terms, TermRelationships}, settings) { 6 | return { 7 | getPostThumbnail: getPostThumbnail(Postmeta, Post, settings), 8 | getThumbnails: getThumbnails(Postmeta, Post, settings) 9 | } 10 | } -------------------------------------------------------------------------------- /src/modules/Postmeta/model.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | 3 | export default function (Conn, prefix) { 4 | return Conn.define(prefix + 'postmeta', { 5 | meta_id: { type: Sequelize.INTEGER, primaryKey: true, field: 'meta_id' }, 6 | post_id: { type: Sequelize.INTEGER }, 7 | meta_key: { type: Sequelize.STRING }, 8 | meta_value: { type: Sequelize.INTEGER }, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/User/model.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | 3 | export default function (Conn, prefix) { 4 | return Conn.define(prefix + 'users', { 5 | id: { type: Sequelize.INTEGER, primaryKey: true }, 6 | user_nicename: { type: Sequelize.STRING }, 7 | user_email: { type: Sequelize.STRING }, 8 | user_registered: { type: Sequelize.STRING }, 9 | display_name: { type: Sequelize.STRING } 10 | }) 11 | } -------------------------------------------------------------------------------- /src/modules/Postmeta/connectors/getPostmeta.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | 3 | const Op = Sequelize.Op 4 | 5 | export default function (Postmeta) { 6 | return function(postId, {keys}) { 7 | const condition = { 8 | post_id: postId 9 | } 10 | 11 | 12 | if (keys && keys.length > 0){ 13 | condition.meta_key = { 14 | [Op.in]: keys 15 | } 16 | } 17 | 18 | return Postmeta.findAll({ 19 | where: condition 20 | }) 21 | } 22 | } -------------------------------------------------------------------------------- /src/modules/Post/connectors/getPostTerms.js: -------------------------------------------------------------------------------- 1 | export default function (Terms, TermRelationships, settings) { 2 | const {wp_prefix} = settings.privateSettings 3 | 4 | return function(postId) { 5 | return TermRelationships.findAll({ 6 | where: { 7 | object_id: postId, 8 | }, 9 | include: [{ 10 | model: Terms 11 | }] 12 | }).then(relationships => { 13 | return relationships.map(r => { 14 | return r.dataValues[`${wp_prefix}term`] 15 | }) 16 | }) 17 | } 18 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "experimentalObjectRestSpread": true, 11 | "jsx": false 12 | }, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "indent": [ 17 | 2, 18 | 2 19 | ], 20 | "linebreak-style": [ 21 | "error", 22 | "unix" 23 | ], 24 | "quotes": [ 25 | "error", 26 | "single" 27 | ], 28 | "semi": [ 29 | "error", 30 | "never" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/Post/connectors/index.js: -------------------------------------------------------------------------------- 1 | import getPost from './getPost' 2 | import getPosts from './getPosts' 3 | import getPostTerms from './getPostTerms' 4 | import getTermPosts from './getTermPosts' 5 | import getPostLayout from './getPostLayout' 6 | 7 | export default function ({Post, Postmeta, Terms, TermRelationships, TermTaxonomy}, settings) { 8 | return { 9 | getPost: getPost(Post), 10 | getPosts: getPosts(Post), 11 | getPostTerms: getPostTerms(Terms, TermRelationships, settings), 12 | getTermPosts: getTermPosts(TermRelationships, Post, TermTaxonomy, settings), 13 | getPostLayout: getPostLayout(Postmeta), 14 | } 15 | } -------------------------------------------------------------------------------- /src/modules/Post/model.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | 3 | export default function (Conn, prefix) { 4 | return Conn.define(prefix + 'posts', { 5 | id: { type: Sequelize.INTEGER, primaryKey: true}, 6 | post_author: { type: Sequelize.INTEGER }, 7 | post_title: { type: Sequelize.STRING }, 8 | post_content: { type: Sequelize.STRING }, 9 | post_excerpt: { type: Sequelize.STRING }, 10 | post_status: { type: Sequelize.STRING }, 11 | post_type: { type: Sequelize.STRING }, 12 | post_name: { type: Sequelize.STRING}, 13 | post_date: { type: Sequelize.STRING}, 14 | post_parent: { type: Sequelize.INTEGER}, 15 | menu_order: { type: Sequelize.INTEGER} 16 | }) 17 | } -------------------------------------------------------------------------------- /src/schema/post.js: -------------------------------------------------------------------------------- 1 | import Postmeta from './postmeta' 2 | import User from './user' 3 | import Thumbnail from './thumbnail' 4 | 5 | const Post = ` 6 | type Post { 7 | id: Int 8 | post_title: String 9 | post_content: String 10 | post_excerpt: String 11 | post_status: String 12 | post_type: String 13 | post_name: String 14 | post_parent: Int 15 | post_date: String 16 | menu_order: Int 17 | layout: Postmeta 18 | thumbnail: Thumbnail 19 | categories: [Category] 20 | post_meta(keys: [MetaType], after: String, first: Int, before: String, last: Int): [Postmeta] 21 | author: User 22 | } 23 | ` 24 | 25 | export default () => [Post, Postmeta, User, ...Thumbnail] -------------------------------------------------------------------------------- /src/modules/Post/connectors/getPosts.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | const Op = Sequelize.Op 3 | 4 | export default function (Post) { 5 | return function({ post_type, order, limit = 10, skip = 0, userId }) { 6 | const orderBy = order ? [order.orderBy, order.direction] : ['menu_order', 'ASC'] 7 | const where = { 8 | post_status: 'publish', 9 | post_type: { 10 | [Op.in]: ['post'] 11 | } 12 | } 13 | 14 | if (post_type) { 15 | where.post_type = { 16 | [Op.in]: post_type 17 | } 18 | } 19 | 20 | if (userId) { 21 | where.post_author = userId 22 | } 23 | 24 | return Post.findAll({ 25 | where: where, 26 | order: [orderBy], 27 | limit: limit, 28 | offset: skip 29 | }).then(r => { 30 | return r 31 | }) 32 | } 33 | } -------------------------------------------------------------------------------- /src/modules/Thumbnail/connectors/getThumbnails.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | import shapeThumbnail from '../shapeThumbnail' 3 | 4 | const Op = Sequelize.Op 5 | 6 | export default function (Postmeta, Post, settings) { 7 | return function(ids) { 8 | const where = { 9 | post_type: 'attachment', 10 | id: { 11 | [Op.in]: ids 12 | } 13 | } 14 | 15 | const {amazonS3} = settings.publicSettings 16 | const metaKeys = amazonS3 ? ['amazonS3_info'] : ['_wp_attached_file'] 17 | metaKeys.push('_wp_attachment_metadata') 18 | 19 | return Post.findAll({ 20 | where: where, 21 | include: { 22 | model: Postmeta 23 | } 24 | }).then(thumbnails => { 25 | if (thumbnails) { 26 | return thumbnails.map(thumbnail => { 27 | return shapeThumbnail(thumbnail, settings) 28 | }) 29 | } 30 | return null 31 | }) 32 | } 33 | } -------------------------------------------------------------------------------- /src/schema/schema.js: -------------------------------------------------------------------------------- 1 | import Category from './category' 2 | import Post from './post.js' 3 | import Menu from './menu.js' 4 | import MetaType from './metaType' 5 | import Setting from './setting' 6 | import OrderInput from './inputs/orderInput' 7 | 8 | const RootQuery = ` 9 | type Query { 10 | settings: Setting 11 | posts(post_type: [String], limit: Int, skip: Int, order: OrderInput, userId: Int): [Post] 12 | post(name: String, id: Int): Post 13 | attachments(ids: [Int]): [Thumbnail] 14 | postmeta(post_id: Int!, keys: [MetaType]): [Postmeta] 15 | menus(name: String!): Menu 16 | category(term_id: Int, name: String): Category 17 | user(name: String, id: Int): User 18 | } 19 | ` 20 | 21 | const SchemaDefinition = ` 22 | schema { 23 | query: Query 24 | } 25 | ` 26 | 27 | export default [ 28 | Category, 29 | Menu, 30 | MetaType, 31 | Post, 32 | Setting, 33 | OrderInput, 34 | RootQuery, 35 | SchemaDefinition 36 | ] -------------------------------------------------------------------------------- /src/modules/Post/connectors/getPost.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | const Op = Sequelize.Op 3 | 4 | export default function (Post){ 5 | return function (postId, name){ 6 | return Post.findOne({ 7 | where: { 8 | post_status: 'publish', 9 | [Op.or]: [{id: postId}, {post_name: name}] 10 | } 11 | }).then(post => { 12 | if (post) { 13 | const { id, post_type } = post.dataValues 14 | post.dataValues.children = [] 15 | return Post.findAll({ 16 | attributes: ['id'], 17 | where: { 18 | [Op.and]: [ 19 | {post_parent: id}, 20 | {post_type: post_type} 21 | ] 22 | } 23 | }).then(childPosts => { 24 | if (childPosts.length > 0) { 25 | childPosts.map(childPost => { 26 | post.dataValues.children.push({ id: Number(childPost.dataValues.id) }) 27 | }) 28 | } 29 | 30 | return post 31 | }) 32 | } 33 | return null 34 | }) 35 | } 36 | } -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # 4.X 2 | 3 | ## Non-breaking Changes 4 | 5 | - Restructured application, connectors and models out of db.js and into their own modules 6 | 7 | ## 4.3 8 | 9 | - Fixed typo in Postmeta causing import issues on some systems. 10 | 11 | ## 4.2.3 12 | 13 | - Fixed issue with Post Categories and Category Posts not using wp_prefix setting. 14 | - Provide default order by date and post type for Category Posts. 15 | 16 | ## 4.2.2 17 | 18 | - Fixed issue with `shapeThumbnail` and postmeta ordering - #18. 19 | 20 | ## 4.2.1 21 | 22 | - Fixed issue wuth `shapeThumbnail` not using the `wp_prefix` setting 23 | 24 | ## 4.2.0 25 | 26 | - Added `attachments` query for getting a list of Thumbnails by ID (used for things like galleries) 27 | 28 | ## 4.1.0 29 | 30 | - Added posts to User schema 31 | - Changed post_type argument for `Posts` queries from `String` to `[String]` to allow for querying posts by multiple post types 32 | - Query User by id and name, instead of just id 33 | 34 | ### Terms 35 | 36 | - Changed `getCategory` query to get category by term id OR name. Previously it was just by term id. -------------------------------------------------------------------------------- /src/modules/Term/model.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | 3 | const TermModel = (Conn, prefix) => { 4 | return Conn.define(prefix + 'terms', { 5 | term_id: { type: Sequelize.INTEGER, primaryKey: true }, 6 | name: { type: Sequelize.STRING }, 7 | slug: { type: Sequelize.STRING }, 8 | term_group: { type: Sequelize.INTEGER }, 9 | }) 10 | } 11 | 12 | const TermRelationshipModel = (Conn, prefix) => { 13 | return Conn.define(prefix + 'term_relationships', { 14 | object_id: { type: Sequelize.INTEGER, primaryKey: true }, 15 | term_taxonomy_id: { type: Sequelize.INTEGER }, 16 | term_order: { type: Sequelize.INTEGER }, 17 | }) 18 | } 19 | 20 | const TermTaxonomyModel = (Conn, prefix) => { 21 | return Conn.define(prefix + 'term_taxonomy', { 22 | term_taxonomy_id: { type: Sequelize.INTEGER, primaryKey: true }, 23 | term_id: { type: Sequelize.INTEGER }, 24 | taxonomy: { type: Sequelize.STRING }, 25 | parent: { type: Sequelize.INTEGER }, 26 | count: { type: Sequelize.INTEGER }, 27 | }) 28 | } 29 | 30 | export {TermModel, TermRelationshipModel, TermTaxonomyModel} -------------------------------------------------------------------------------- /src/modules/Thumbnail/shapeThumbnail.js: -------------------------------------------------------------------------------- 1 | import PHPUnserialize from 'php-unserialize' 2 | import {map} from 'lodash' 3 | 4 | export default function (thumbnail, settings) { 5 | let file, fileMeta 6 | const {amazonS3, uploads} = settings.publicSettings 7 | const {wp_prefix} = settings.privateSettings 8 | 9 | thumbnail[`${wp_prefix}postmeta`].forEach(postmeta => { 10 | switch(postmeta.meta_key) { 11 | case '_wp_attached_file': 12 | file = postmeta.meta_value 13 | break 14 | case '_wp_attachment_metadata': 15 | fileMeta = postmeta.meta_value 16 | break 17 | } 18 | }) 19 | 20 | if (file) { 21 | const thumbnailSrc = amazonS3 ? 22 | uploads + PHPUnserialize.unserialize(file).key : 23 | uploads + file 24 | 25 | const thumbMeta = PHPUnserialize.unserialize(fileMeta) 26 | const sizes = map(thumbMeta.sizes, (size, key) => { 27 | return { 28 | size: key, 29 | file: size.file 30 | } 31 | }) 32 | 33 | return { 34 | id: thumbnail.id, 35 | src: thumbnailSrc, 36 | sizes: sizes 37 | } 38 | } 39 | 40 | return null 41 | } -------------------------------------------------------------------------------- /src/modules/Thumbnail/connectors/getPostThumbnail.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | import shapeThumbnail from '../shapeThumbnail' 3 | 4 | const Op = Sequelize.Op 5 | 6 | export default function (Postmeta, Post, settings) { 7 | return function(postId) { 8 | return Postmeta.findOne({ 9 | where: { 10 | post_id: postId, 11 | meta_key: '_thumbnail_id' 12 | } 13 | }).then(res => { 14 | if (res) { 15 | const {amazonS3} = settings.publicSettings 16 | const metaKeys = amazonS3 ? ['amazonS3_info'] : ['_wp_attached_file'] 17 | metaKeys.push('_wp_attachment_metadata') 18 | 19 | return Post.findOne({ 20 | where: { 21 | id: Number(res.dataValues.meta_value) 22 | }, 23 | include: { 24 | model: Postmeta, 25 | where: { 26 | meta_key: { 27 | [Op.in]: metaKeys 28 | } 29 | }, 30 | limit: 2 31 | } 32 | }).then( post => { 33 | return shapeThumbnail(post, settings) 34 | }) 35 | } 36 | return null 37 | }) 38 | } 39 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordexpress-schema", 3 | "version": "4.3.0", 4 | "description": "A GraphQL Schema for WordPress", 5 | "files": [ 6 | "lib" 7 | ], 8 | "main": "lib/index.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "build": "rimraf lib && babel src -d lib", 12 | "buildwatch": "nodemon --exec npm run build --watch ./src" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/ramsaylanier/wordexpress-schema.git" 17 | }, 18 | "keywords": [ 19 | "WordPress", 20 | "Javascript", 21 | "React", 22 | "GraphQL" 23 | ], 24 | "author": "Ramsay Lanier (ramsaylanier.com)", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/ramsaylanier/wordexpress-schema/issues" 28 | }, 29 | "homepage": "https://github.com/ramsaylanier/wordexpress-schema#readme", 30 | "dependencies": { 31 | "lodash": "^4.13.1", 32 | "mysql2": "^1.5.1", 33 | "nodemon": "^1.14.9", 34 | "php-unserialize": "0.0.1", 35 | "sequelize": "^4.28.0" 36 | }, 37 | "devDependencies": { 38 | "babel-cli": "^6.3.17", 39 | "babel-eslint": "^7.2.1", 40 | "babel-preset-es2015": "^6.3.13", 41 | "babel-preset-stage-0": "^6.3.13", 42 | "eslint": "^3.19.0", 43 | "rimraf": "^2.5.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/Post/connectors/getTermPosts.js: -------------------------------------------------------------------------------- 1 | export default function (TermRelationships, Post, TermTaxonomy, settings){ 2 | const {wp_prefix} = settings.privateSettings 3 | 4 | return function(termId, { post_type, order, limit = 10, skip = 0 }) { 5 | const orderBy = order ? [Post, order.orderBy, order.direction] : [Post, 'post_date', 'DESC'] 6 | 7 | let termIds = [termId] 8 | 9 | function getTermIds(parentTermIds) { 10 | if (!parentTermIds.length) return termIds 11 | 12 | return TermTaxonomy.findAll({ 13 | attributes: ['term_taxonomy_id'], 14 | include: [], 15 | where: { 16 | parent: parentTermIds 17 | }, 18 | limit: limit, 19 | offset: skip 20 | }) 21 | .then(function (posts) { 22 | const p = posts.map(post => post.term_taxonomy_id) 23 | termIds.push(...p) 24 | return p 25 | }) 26 | .then(getTermIds) 27 | } 28 | 29 | return getTermIds([termId]) 30 | .then((termIds) => { 31 | return TermRelationships.findAll({ 32 | attributes: [], 33 | include: [{ 34 | model: Post, 35 | where: { 36 | post_type: post_type, 37 | post_status: 'publish' 38 | } 39 | }], 40 | where: { 41 | term_taxonomy_id: termIds 42 | }, 43 | order: [orderBy], 44 | limit: limit, 45 | offset: skip 46 | }) 47 | }) 48 | .then(posts => { 49 | const p = posts.map(post => post[`${wp_prefix}post`]) 50 | return p 51 | }) 52 | } 53 | } -------------------------------------------------------------------------------- /src/resolvers.js: -------------------------------------------------------------------------------- 1 | export default function WordExpressResolvers(Connectors, publicSettings) { 2 | const Resolvers = { 3 | Query: { 4 | settings() { 5 | return publicSettings 6 | }, 7 | category(_, { term_id, name }) { 8 | return Connectors.getTerm(term_id, name) 9 | }, 10 | posts(_, args) { 11 | return Connectors.getPosts(args) 12 | }, 13 | post(_, {name, id}) { 14 | return Connectors.getPost(id, name) 15 | }, 16 | postmeta(_, {post_id, keys}) { 17 | return Connectors.getPostmeta(post_id, keys) 18 | }, 19 | menus(_, {name}) { 20 | return Connectors.getMenu(name) 21 | }, 22 | user(_, {id, name}) { 23 | return Connectors.getUser({id, name}) 24 | }, 25 | attachments(_, {ids}) { 26 | return Connectors.getThumbnails(ids) 27 | } 28 | }, 29 | Category: { 30 | posts(category, args) { 31 | return Connectors.getTermPosts(category.term_id, args) 32 | } 33 | }, 34 | Post: { 35 | layout(post) { 36 | return Connectors.getPostLayout(post.id) 37 | }, 38 | post_meta(post, keys) { 39 | return Connectors.getPostmeta(post.id, keys) 40 | }, 41 | thumbnail(post) { 42 | return Connectors.getPostThumbnail(post.id) 43 | }, 44 | author(post) { 45 | return Connectors.getUser({userId: post.post_author}) 46 | }, 47 | categories(post) { 48 | return Connectors.getPostTerms(post.id) 49 | } 50 | }, 51 | Postmeta: { 52 | connecting_post(postmeta) { 53 | return Connectors.getPost(postmeta.meta_value) 54 | } 55 | }, 56 | Menu: { 57 | items(menu) { 58 | return menu.items 59 | } 60 | }, 61 | MenuItem: { 62 | navitem(menuItem) { 63 | return Connectors.getPost(menuItem.linkedId) 64 | }, 65 | children(menuItem) { 66 | return menuItem.children 67 | } 68 | }, 69 | User: { 70 | posts(user, args) { 71 | const a = { 72 | ...args, 73 | userId: user.id 74 | } 75 | return Connectors.getPosts(a) 76 | }, 77 | } 78 | } 79 | 80 | return Resolvers 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/Menu/connectors/getMenu.js: -------------------------------------------------------------------------------- 1 | import flow from 'lodash/fp/flow' 2 | import filter from 'lodash/fp/filter' 3 | import map from 'lodash/fp/map' 4 | import sortBy from 'lodash/fp/sortBy' 5 | 6 | export default function (Post, Postmeta, Terms, TermRelationships){ 7 | return function (name) { 8 | return Terms.findOne({ 9 | where: { 10 | slug: name 11 | }, 12 | include: [{ 13 | model: TermRelationships, 14 | include: [{ 15 | model: Post, 16 | attributes: ['post_parent', 'id', 'menu_order'], 17 | include: [{ 18 | model: Postmeta 19 | }] 20 | }] 21 | }] 22 | }).then(res => { 23 | if (res) { 24 | const menu = { 25 | id: null, 26 | name: name, 27 | items: null, 28 | } 29 | menu.id = res.term_id 30 | const relationship = res.wp_term_relationships 31 | 32 | const postFuncs = [ 33 | map(x => x.wp_post), 34 | map(x => x.dataValues), 35 | sortBy(x => x.menu_order), 36 | map(x => { 37 | const postMeta = x.wp_postmeta.map(x => x.dataValues) 38 | 39 | const item = { 40 | id: x.id, 41 | order: x.menu_order 42 | } 43 | 44 | item.parentMenuId = parseInt(flow( 45 | filter(x => x.meta_key === '_menu_item_menu_item_parent'), 46 | map(x => x.meta_value) 47 | )(postMeta)[0]) 48 | 49 | item.linkedId = parseInt(flow( 50 | filter(x => x.meta_key === '_menu_item_object_id'), 51 | map(x => x.meta_value) 52 | )(postMeta)[0]) 53 | 54 | return item 55 | }) 56 | ] 57 | 58 | const items = flow(postFuncs)(relationship) 59 | 60 | const itemsWithChildren = flow( 61 | map(item => { 62 | item.children = items.filter(i => i.parentMenuId === item.id) 63 | return item 64 | }), 65 | filter(item => item.parentMenuId === 0) 66 | )(items) 67 | 68 | menu.items = itemsWithChildren 69 | 70 | return menu 71 | } 72 | return null 73 | }) 74 | } 75 | } -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize' 2 | 3 | // MODELS 4 | import PostModel from './modules/Post/model' 5 | import PostmetaModel from './modules/Postmeta/model' 6 | import UserModel from './modules/User/model' 7 | import {TermModel, TermRelationshipModel, TermTaxonomyModel} from './modules/Term/model' 8 | 9 | // CONNECTORS 10 | import PostConnectors from './modules/Post/connectors' 11 | import PostmetaConnectors from './modules/Postmeta/connectors' 12 | import TermConnectors from './modules/Term/connectors' 13 | import UserConnectors from './modules/User/connectors' 14 | import MenuConnectors from './modules/Menu/connectors' 15 | import ThumbnailConnectors from './modules/Thumbnail/connectors' 16 | 17 | export default class WordExpressDatabase { 18 | constructor(settings) { 19 | this.settings = settings 20 | this.connection = this.connect(settings) 21 | this.connectors = this.getConnectors() 22 | this.models = this.getModels() 23 | } 24 | 25 | connect() { 26 | const { name, username, password, host, port } = this.settings.privateSettings.database 27 | 28 | const Conn = new Sequelize( 29 | name, 30 | username, 31 | password, 32 | { 33 | logging: false, 34 | dialect: 'mysql', 35 | host: host, 36 | port: port || 3306, 37 | define: { 38 | timestamps: false, 39 | freezeTableName: true, 40 | } 41 | } 42 | ) 43 | 44 | return Conn 45 | } 46 | 47 | getModels() { 48 | const prefix = this.settings.privateSettings.wp_prefix 49 | const Conn = this.connection 50 | 51 | return { 52 | Post: PostModel(Conn, prefix), 53 | Postmeta: PostmetaModel(Conn, prefix), 54 | User: UserModel(Conn, prefix), 55 | Terms: TermModel(Conn, prefix), 56 | TermRelationships: TermRelationshipModel(Conn, prefix), 57 | TermTaxonomy: TermTaxonomyModel(Conn, prefix) 58 | } 59 | } 60 | 61 | getConnectors() { 62 | const models = this.getModels() 63 | const { Post, Postmeta, Terms, TermRelationships } = models 64 | 65 | Terms.hasMany(TermRelationships, {foreignKey: 'term_taxonomy_id'}) 66 | TermRelationships.belongsTo(Terms, {foreignKey: 'term_taxonomy_id'}) 67 | 68 | TermRelationships.hasMany(Postmeta, {foreignKey: 'post_id'}) 69 | Postmeta.belongsTo(TermRelationships, {foreignKey: 'post_id'}) 70 | 71 | TermRelationships.belongsTo(Post, {foreignKey: 'object_id'}) 72 | 73 | Post.hasMany(Postmeta, {foreignKey: 'post_id'}) 74 | Postmeta.belongsTo(Post, {foreignKey: 'post_id'}) 75 | 76 | return { 77 | ...PostConnectors(models, this.settings), 78 | ...PostmetaConnectors(models), 79 | ...TermConnectors(models), 80 | ...UserConnectors(models), 81 | ...MenuConnectors(models), 82 | ...ThumbnailConnectors(models, this.settings) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/wordexpress-schema.svg)](https://badge.fury.io/js/wordexpress-schema) 2 | 3 | # wordexpress-schema 4 | 5 | WordExpress Schema is a GraphQL schema that is modeled off of how WordPress stores data in a MySQL database. It provides modular GraphQL type definitions and GraphQL query resolvers, as well as an easy connection to a WordPress database. 6 | 7 | WordExpress Schema exports the following: 8 | 9 | - **WordExpress Database**: provides a connection to your WordPress database and returns some models and queries using Sequelize. These queries replace MYSQL queries, and return promises. 10 | 11 | - **WordExpress Resolvers**: resolving functions that work with the `WordExpressDatabase` connectors to resolve GraphQL queries 12 | 13 | - **WordExpress Definitions**: a modular GraphQL schema definition. 14 | 15 | Combined, this package can be used with any GraphQL server (like [Apollo Server](https://www.apollographql.com/docs/apollo-server/)) to provide an easy connection to your WordPress database. An example of using this package with Apollo Server and Webpack is provided below. 16 | 17 | If you'd like a solution that already includes a GraphQL server for you, check out the [WordExpress Server repository](https://github.com/ramsaylanier/WordExpress-Server). WordExpress Server uses `WordExpress Schema` and provides you with a GraphQL server out of the box. 18 | 19 | ## Installation 20 | 21 | ``` 22 | npm install --save-dev wordexpress-schema 23 | ``` 24 | 25 | ## Usage 26 | 27 | * [Using WordExpress Database](#wordexpressdatabase) 28 | 29 | * [Connection Settings](#connection-settings) 30 | 31 | * [The Database Class](#the-database-class) 32 | 33 | * [The Models](#the-models) 34 | 35 | 36 | * [Making an Executable Schema](#creating-the-schema) 37 | 38 | * [Using Definitions and Resolvers with Apollo Server](#using-definitions-and-resolvers-with-apollo-server) 39 | 40 | * [Types](#types) 41 | 42 | * [Post](#post) 43 | 44 | * [Postmeta](#postmeta) 45 | 46 | * [MetaType](#metatype) 47 | 48 | * [Category](#category) 49 | 50 | * [Menu](#menu) 51 | 52 | * [MenuItem](#menuitem) 53 | 54 | * [Setting](#setting) 55 | 56 | * [Inputs](#inputs) 57 | 58 | * [Queries](#queries) 59 | 60 | 61 | 62 | ## WordExpressDatabase 63 | The first part of WordExpress Schema is **WordExpressDatabase**. This class provides an easy connection to your WordPress database using some connection settings. Typically, you'll want to put the database in its own file in case you want to extend the Models. 64 | 65 | Below is the basic implementation: 66 | ```es6 67 | 68 | //db.js 69 | import Config from 'config' 70 | import {WordExpressDatabase} from 'wordexpress-schema' 71 | 72 | /* 73 | Example settings object: 74 | public: { 75 | uploads: "http://wordexpress.s3.amazonaws.com/", 76 | amazonS3: true 77 | }, 78 | private: { 79 | wp_prefix: "wp_", 80 | database: { 81 | name: "wpexpress_dev", 82 | username: "root", 83 | password: "", 84 | host: "127.0.0.1" 85 | } 86 | } 87 | */ 88 | 89 | 90 | const publicSettings = Config.get('public') 91 | const privateSettings = Config.get('private') 92 | 93 | const Database = new WordExpressDatabase({publicSettings, privateSettings}) 94 | const {connectors, models} = Database 95 | 96 | export default Database 97 | export {connectors, models} 98 | ``` 99 | 100 | ### Connection Settings 101 | 102 | In the above example, **WordExpressDatabase** is passed a settings object that contains some WordPress database settings. Name, username, password, and host are all self-explanatory. 103 | 104 | WordExpress will work with Amazon S3; passing in a truthy value for amazonS3 will alter the query for getting Post Thumbnail images. If you are using S3, you just need the include the base path to your S3 bucket (which means you should exclude the wp-content/uploads/ part of the path). If you are hosting images on your own server, include the full path to the uploads folder. 105 | 106 | Lastly, you can modify the wordpress database prefix. Some people don't use the default "wp_" prefix for various reasons. If that's you, I got your back. 107 | 108 | ### The Database Class 109 | 110 | The Database class above contains the connectionDetails, the actual Sequelize connection, the database queries, and the database models. Really, all you need for GraphQL setup are the queries; however, if you'd like to extend queries with your own, the Database Models are exposed. 111 | 112 | #### The Models 113 | Here are the models and their definitions. As you can see, for the Post model, not every column in the wp_posts table is included. I've included the most relevant columns; however because the Database class exposes the models, you can extend them to your liking. 114 | 115 | ```es6 116 | Post: Conn.define(prefix + 'posts', { 117 | id: { type: Sequelize.INTEGER, primaryKey: true}, 118 | post_author: { type: Sequelize.INTEGER }, 119 | post_title: { type: Sequelize.STRING }, 120 | post_content: { type: Sequelize.STRING }, 121 | post_excerpt: { type: Sequelize.STRING }, 122 | post_status: { type: Sequelize.STRING }, 123 | post_type: { type: Sequelize.STRING }, 124 | post_name: { type: Sequelize.STRING}, 125 | post_date: { type: Sequelize.STRING}, 126 | post_parent: { type: Sequelize.INTEGER}, 127 | menu_order: { type: Sequelize.INTEGER} 128 | }), 129 | Postmeta: Conn.define(prefix + 'postmeta', { 130 | meta_id: { type: Sequelize.INTEGER, primaryKey: true, field: 'meta_id' }, 131 | post_id: { type: Sequelize.INTEGER }, 132 | meta_key: { type: Sequelize.STRING }, 133 | meta_value: { type: Sequelize.INTEGER }, 134 | }), 135 | User: Conn.define(prefix + 'users', { 136 | id: { type: Sequelize.INTEGER, primaryKey: true }, 137 | user_nicename: { type: Sequelize.STRING }, 138 | user_email: { type: Sequelize.STRING }, 139 | user_registered: { type: Sequelize.STRING }, 140 | display_name: { type: Sequelize.STRING } 141 | }), 142 | Terms: Conn.define(prefix + 'terms', { 143 | term_id: { type: Sequelize.INTEGER, primaryKey: true }, 144 | name: { type: Sequelize.STRING }, 145 | slug: { type: Sequelize.STRING }, 146 | term_group: { type: Sequelize.INTEGER }, 147 | }), 148 | TermRelationships: Conn.define(prefix + 'term_relationships', { 149 | object_id: { type: Sequelize.INTEGER, primaryKey: true }, 150 | term_taxonomy_id: { type: Sequelize.INTEGER }, 151 | term_order: { type: Sequelize.INTEGER }, 152 | }), 153 | TermTaxonomy: Conn.define(prefix + 'term_taxonomy', { 154 | term_taxonomy_id: { type: Sequelize.INTEGER, primaryKey: true }, 155 | term_id: { type: Sequelize.INTEGER }, 156 | taxonomy: { type: Sequelize.STRING }, 157 | parent: { type: Sequelize.INTEGER }, 158 | count: { type: Sequelize.INTEGER }, 159 | }) 160 | ``` 161 | 162 | ## Creating The Schema 163 | WordExpress uses [GraphQL Tools](https://github.com/apollographql/graphql-tools)'s [makeExecutableSchema](https://www.apollographql.com/docs/graphql-tools/generate-schema.html#makeExecutableSchema) to generate an executable schema. `makeExecutableSchema` requires type definitions and resolvers. WordExpress gives you both of those! Here's the basic implementation of the schema: 164 | 165 | ```es6 166 | import {makeExecutableSchema} from 'graphql-tools' 167 | import {WordExpressDefinitions, WordExpressResolvers} from 'wordexpress-schema' 168 | import {connectors} from './db' 169 | import Config from 'config' 170 | 171 | const RootResolvers = WordExpressResolvers(connectors, Config.get('public')) 172 | 173 | const schema = makeExecutableSchema({ 174 | typeDefs: [WordExpressDefinitions] 175 | resolvers: RootResolvers 176 | }) 177 | 178 | export default schema 179 | ``` 180 | 181 | `WordExpressResolvers` requires some database connectors that the `WordExpressDatabase` provides. These connectors provide the root sequelize queries. `WordExpressResolvers` is simply a (resolving map)[https://www.apollographql.com/docs/graphql-tools/resolvers.html#Resolver-map] that tell the GraphQl queries how to fetch the data from the WordPress database. 182 | 183 | `WordExpressDefinitions` is a modular GraphQL schema written in the [GraphQL Schema language](https://www.apollographql.com/docs/graphql-tools/generate-schema.html#schema-language). 184 | 185 | 186 | ## Using Definitions and Resolvers with Apollo Server 187 | This example is from the [WordExpress Server](https://github.com/ramsaylanier/WordExpress-Server). 188 | After creating an executable schema, all we need to do is provide the schema to [apollo-server-express](https://www.apollographql.com/docs/apollo-server/servers/express.html). 189 | 190 | ```es6 191 | import {ApolloServer} from 'apollo-server' 192 | import {WordExpressDefinitions, WordExpressResolvers} from 'wordexpress-schema' 193 | import {connectors} from './db' 194 | import Config from 'config' 195 | 196 | const PORT = 4000 197 | 198 | const resolvers = WordExpressResolvers(connectors, Config.get('public')) 199 | 200 | const server = new ApolloServer({ 201 | typeDefs: [...WordExpressDefinitions], 202 | resolvers 203 | }) 204 | 205 | server.listen({port: PORT}, () => { 206 | console.log(`wordexpress server is now running on port ${PORT}`) 207 | }) 208 | ``` 209 | 210 | ## Types 211 | 212 | ### Post 213 | ```es6 214 | import Postmeta from './Postmeta' 215 | import User from './User' 216 | 217 | const Post = ` 218 | type Post { 219 | id: Int 220 | post_title: String 221 | post_content: String 222 | post_excerpt: String 223 | post_status: String 224 | post_type: String 225 | post_name: String 226 | post_parent: Int 227 | post_date: String 228 | menu_order: Int 229 | post_author: Int 230 | layout: Postmeta 231 | thumbnail: String 232 | post_meta(keys: [MetaType], after: String, first: Int, before: String, last: Int): [Postmeta] 233 | author: User 234 | } 235 | ` 236 | 237 | export default () => [Post, Postmeta, User] 238 | ``` 239 | 240 | ### Postmeta 241 | ``` es6 242 | import Post from './post' 243 | 244 | const Postmeta = ` 245 | type Postmeta { 246 | id: Int 247 | meta_id: Int 248 | post_id: Int 249 | meta_key: String 250 | meta_value: String 251 | connecting_post: Post 252 | } 253 | ` 254 | 255 | export default () => [Postmeta, Post] 256 | ``` 257 | 258 | ### MetaType 259 | ```es6 260 | const MetaType = ` 261 | enum MetaType { 262 | _thumbnail_id 263 | _wp_attached_file 264 | react_layout 265 | amazonS3_info 266 | order 267 | } 268 | ` 269 | 270 | export default MetaType 271 | ``` 272 | 273 | ### Category 274 | ``` es6 275 | import Post from './post' 276 | 277 | const Category = ` 278 | type Category { 279 | term_id: Int! 280 | name: String 281 | slug: String 282 | posts(post_type: String = "post", limit: Int, skip: Int): [Post] 283 | } 284 | ` 285 | 286 | export default () => [Category, Post] 287 | ``` 288 | 289 | ### Menu 290 | ```es6 291 | import MenuItem from './menuItem' 292 | 293 | const Menu = ` 294 | type Menu { 295 | id: ID! 296 | name: String 297 | items: [MenuItem] 298 | } 299 | ` 300 | 301 | export default () => [Menu, MenuItem] 302 | ``` 303 | 304 | ### MenuItem 305 | ```es6 306 | import Post from './post' 307 | 308 | const MenuItem = ` 309 | type MenuItem { 310 | id: ID! 311 | post_title: String 312 | linkedId: Int 313 | object_type: String 314 | order: Int 315 | navitem: Post 316 | children: [MenuItem] 317 | } 318 | ` 319 | 320 | export default () => [MenuItem, Post] 321 | ``` 322 | 323 | ### Setting 324 | ```es6 325 | const Setting = ` 326 | type Setting { 327 | uploads: String 328 | amazonS3: Boolean 329 | } 330 | ` 331 | 332 | export default Setting 333 | ``` 334 | 335 | ## Inputs 336 | 337 | ### OrderInput 338 | ```es6 339 | const OrderInput = ` 340 | input OrderInput { 341 | orderBy: String, 342 | direction: String 343 | } 344 | ` 345 | 346 | export default OrderInput 347 | ``` 348 | 349 | 350 | ## Queries 351 | WordExpress provides some out-of-the-box queries to do some basic stuff like getting posts, getting posts by category, getting a post by post_type, etc. 352 | 353 | ### Posts 354 | ``` 355 | posts(post_type: String = "post", limit: Int, skip: Int, order: OrderInput): [Post] 356 | ``` 357 | 358 | You can query posts by `post_type`. If you don't provide a post_type, it will default to 'post'. You can also limit the results and skip results (allowing for pagination). Also, you can provide a custom sorting object to sort the results. Here's an example of sorting: 359 | 360 | screen shot 2017-12-13 at 12 31 14 pm 361 | 362 | #### Layouts for Pages and Posts 363 | Posts and pages can be assigned a Component to use as a layout. You can use the [WordExpress Companion Plugin](https://github.com/ramsaylanier/WordExpress-Plugin) for WordPress which will allow you to add the custom field to any page or post. Or you can add your own custom field. It needs to be called `page_layout_component`. Here's an example of the querying with a layout: 364 | 365 | screen shot 2017-12-13 at 12 41 43 pm 366 | 367 | ### Post 368 | ``` 369 | post(name: String, id: Int): Post 370 | ``` 371 | 372 | Returns a Post by either its ID or its name. 373 | 374 | screen shot 2017-12-13 at 12 57 05 pm 375 | 376 | ### Menu 377 | ``` 378 | menus(name: String!): Menu 379 | ``` 380 | 381 | Returns a menu and the menu items associated with it, as well as children items. Uses the slug of the menu that is registered with WordPress. 382 | 383 | screen shot 2017-12-13 at 12 52 01 pm 384 | 385 | 386 | ### Category 387 | ``` 388 | category(term_id: Int!): Category 389 | ``` 390 | 391 | Gets a category by its ID. Also capable of returning all posts with the category id. Here's an example: 392 | 393 | 394 | screen shot 2017-12-13 at 12 59 57 pm 395 | 396 | ### Postmeta 397 | ``` 398 | postmeta(post_id: Int!, keys:[MetaType]): [Postmeta] 399 | ``` 400 | Gets the postmeta of a post by the post id. 401 | 402 | screen shot 2017-12-13 at 1 28 34 pm 403 | 404 | If `keys` are passed it, it will only return those keys which are part of the `MetaType`. Example: 405 | 406 | screen shot 2017-12-13 at 1 32 49 pm 407 | 408 | 409 | 410 | 411 | --------------------------------------------------------------------------------