1.0.0 • Published 7 years ago

wp-graphql v1.0.0

Weekly downloads
21
License
MIT
Repository
github
Last release
7 years ago

wp-graphql Build Status codecov npm

Client-side GraphQL convenience wrapper for the WordPress REST API

Why?

  • Declarative data fetching.
  • Human-readable queries/mutations.
  • Zero nested callbacks or long promise chains.

Install

$ npm install --save wp-graphql

GraphiQL Demo

Note: The demo above queries against the public WordPress REST API demo provided by WordPress. Because of this, some queries and all mutations will not work.

Usage

Load and localize script

When loading the script, localize it with a rest_url and generated nonce (only required if you need to make authenticated requests).

<?php

function load_scripts() {
    wp_register_script('myscript', 'path/to/myscript.js');
    wp_localize_script('myscript', 'AUTH', array(
        root => esc_url_raw(rest_url()),
        nonce => wp_create_nonce('wp_rest'),
    ));
    wp_enqueue_script('myscript');
}
add_action('wp_enqueue_scripts', 'load_scripts');

Create and use instance

Once an instance is created, you are able to query and mutate data using standard GraphQL syntax.

// myscript.js

import WPGraphQL from 'wp-graphql';

const gql = new WPGraphQL(AUTH.root, { auth: AUTH.nonce });

gql.send(`
    query {
        user(id: 1, context: edit) {
            id
            name
            email
        }
        firstThreeUsers: users(per_page: 3, orderby: id) {
            name
        }
        settings {
            title
        }
    }
`).then(data => console.log(data));

API

Configuration

The WPGraphQL class accepts an object with the Config interface as the second argument to configure the instance.

/** Authenticate with Basic Auth (Requires Basic Auth plugin) */
interface BasicAuth {
    username: string;
    password: string;
}

/** Either a BasicAuth object or a WordPress nonce string */
type Authentication = BasicAuth | string;

interface CustomPostTypeParams {
    /** The singular name of the custom content type. (e.g. "book") */
    name: string;
    /** The plural name of the custom content type. (e.g. "books") */
    namePlural: string;
    /** The URL base name for the custom content type. (e.g. "books") */
    restBase: string;
}

interface Config {
    /** Additional context to be passed to all resolvers. */
    context?: any;
    /** Custom mutations to be merged into the library upon instantiation. */
    mutations?: GraphQLFieldConfigMap<any, any>;
    /** Cookie generated by WordPress used for authentication. */
    nonce?: string;
    /** List of `postTypeConfig` to be merged into the library upon instantiation. */
    postTypes?: CustomPostTypeParams[];
    /** Custom queries to be merged into the library upon instantiation. */
    queries?: GraphQLFieldConfigMap<any, any>;
}

Methods

send

Execute a single GraphQL query or mutation.

type send = <TResponse>(gql: string, variables?: object, operationName?: string) => PromiseLike<TResponse>

batch

Execute a chain of GraphQL queries and/or mutations.

If at any point a response returns with a field labelled extract, all first-level fields are available to be used in the immediately subsequent query or mutation. Variables may also still be passed to batch, but keep in mind that the variables passed to the batch signature will always win if a name conflict occurs in an extracted variable.

Keep in mind also that if queries are batched with the same field names, each subsequent query will overwrite the last one. To avoid this, just tag the fields with a unique name.

type batch = <TResponse>(gql: string, operationNames: string[], variables?: object) => PromiseLike<TResponse>

Example:

import WPGraphQL from 'wp-graphql';

const gql = new WPGraphQL(AUTH.root, { auth: AUTH.nonce });

gql.batch(`
    query first {
        extract: me {
            id
            name
        }
    }
    query second($id: Int!) {
        user(id: $id) {
            name
        }
    }
    mutation third($anotherId: Int!, $name: String) {
        updateUser(id: $anotherId, name: $name) {
            id
            name
        }
    }
`, ['first', 'second', 'third'], { anotherId: 2, name: 'Sally Jones' }).then(data => console.log(data));

Resolvers

The preloaded queries and mutations are listed below with their required parameters where applicable.

Note: Some of the parameters are enum type (e.g. context). Do not surround these in quotes when performing your queries/mutations. Additionally, parameters of type String must be surrounded in double quotes (").

Queries

QueryDescription
categoriesFetch a list of categories.
category(id: Int!)Fetch a single category by ID.
commentsFetch a list of comments.
comment(id: Int!)Fetch a single comment by ID.
mediaListFetch a list of media items.
media(id: Int!)Fetch a single media item by ID.
pagesFetch a list of pages.
page(id: Int!)Fetch a single page by ID.
postStatusesFetch a list of post statuses.
postStatus(id: PostStatus!)Fetch a single post status by name.
postTypesFetch a list of post types.
postType(slug: String!)Fetch a post type by slug.
postsFetch a list of posts.
post(id: Int!)Fetch a single post by ID.
revisions(id: Int!)Fetch a list of revisions by post ID.
revision(id: Int!, parentId: Int!)Fetch a single revision by revision ID and parent post ID.
settingsFetch site settings.
tagsFetch a list of tags.
tag(id: Int!)Fetch a single tag by ID.
taxonomiesFetch a list of taxonomies.
taxonomy(slug: String!)Fetch a single taxonomy by slug.
usersFetch a list of users.
user(id: Int!)Fetch a single user by ID.
meFetch the currently logged in user.

Mutations

MutationDescription
addCategory(name: String!)Create a new category.
updateCategory(id: Int!)Update a category by ID.
deleteCategory(id: Int!)Delete a category by ID.
addComment(content: String!)Create a new comment.
updateComment(id: Int!)Update a comment by ID.
deleteComment(id: Int!)Delete a comment by ID.
addMedia(file: String!, filename: String!)Create a new media item.Note: file must be of type Blob, File, or ArrayBuffer.
updateMedia(id: Int!)Update media by ID.
deleteMedia(id: Int!)Delete media by ID.
addPage()Create a new page.
updatePage(id: Int!)Update a page by ID.
deletePage(id: Int!)Delete a page by ID.
addPost(title: String!, content: String!, excerpt: String!)Create a new post.Note: Only one of title, content, or excerpt is required for a sucessful request.
updatePost(id: Int!)Update a post by ID.
deletePost(id: Int!)Delete a post by ID.
deleteRevision(id: Int!, parentId: Int!)Delete a single revision by revision ID and parent post ID.
updateSettings()Update site settings.
addTag(name: String!)Create a new tag.
updateTag(id: Int!)Update a tag by ID.
deleteTag(id: Int!)Delete a tag by ID.
addUser(email: String!, password: String!, username: String!)Create a new user.
updateUser(id: Int!)Update a user by ID.
deleteUser(id: Int!)Delete a user by ID.

Advanced Usage

Static type checking with TypeScript

Note: Requires TypeScript ^2.3.0. (At time of writing, that's currently typescript@next).

All types in this library are available for consumption. Types that contain a meta property can optionally be passed the shape of the expected metadata as a Generic to expand your type checking into the meta fields. The API response returned from the send() method can also optionally be typed by passing the expected shape of the response as a Generic.

If desired, the fields of each type interface can be narrowed by using TypeScript's built-in Pick method.

import WPGraphQL, { User as U, Settings } from 'wp-graphql';

const gql = new WPGraphQL(AUTH.root, { auth: AUTH.nonce });

interface UserMeta {
    hobby: string;
    luckyNumber: number;
}

type User = U<UserMeta>;

interface Response {
    user: Pick<User, 'id'|'name'|'meta'|'email'>;
    firstThreeUsers: Pick<User, 'name'>;
    settings: Pick<Settings, 'title'>;
}

gql.send<Response>(`
    query {
        user(id: 1, context: edit) {
            id
            name
            meta
            email
        }
        firstThreeUsers: users(per_page: 3, orderby: id) {
            name
        }
        settings {
            title
        }
    }
`).then(data => {
    console.log(data.user.description); // Compile Error: The description field is not defined in the Response interface.
    const hobby = data.user.meta.hobby; // Type inferred as "string".
});

Custom queries and mutations

// customQuery.js
import {
    GraphQLInt,
    GraphQLNonNull,
    GraphQLString,
} from 'graphql';
const NAMESPACE = 'myRoute/v1';

const getStringData = {
    description: 'Get a string from a custom endpoint.',
    type: GraphQLString,
    args: {
        id: {
            description: 'The ID of the string I need.',
            type: new GraphQLNonNull(GraphQLInt),
        },
        someOtherArg: {
            description: 'Some other argument to pass as a parameter',
            type: GraphQLString,
        },
    }
    resolve: (root, { id, ...args }) => (
        root.get(`/${NAMESPACE}/path/to/endpoint/${id}`, args)
    ),
};

export default {
    getStringData,
}
// customMutation.js
import {
    GraphQLInt,
    GraphQLNonNull,
    GraphQLString,
} from 'graphql';
const NAMESPACE = 'myRoute/v1';

const postStringData = {
    description: 'Post a string to a custom endpoint.',
    type: GraphQLString,
    args: {
        myString: {
            description: 'Some other argument to pass as a parameter',
            type: new GraphQLNonNull(GraphQLString),
        },
    }
    resolve: (root, args) => (
        root.post(`/${NAMESPACE}/path/to/endpoint`, args)
    ),
};

const deleteStringData = {
    description: 'Delete a string from a custom endpoint.',
    type: GraphQLString,
    args: {
        id: {
            description: 'The ID of the string to delete.',
            type: new GraphQLNonNull(GraphQLInt),
        },
    }
    resolve: (root, { id }) => (
        root.delete(`/${NAMESPACE}/path/to/endpoint/${id}`)
    ),
};

export default {
    postStringData,
    deleteStringData,
}
// Using the custom queries and mutations.
import WPGraphQL from 'wp-graphql';
import queries from './customQuery';
import mutations from './customMutation';

const gql = new WPGraphQL('http://localhost:8080/wp-json', { queries, mutations });

Generating queries and mutations for custom post types

Let's assume that your site has a custom post type called books. If you'd like to query and mutate this custom post type similarly to how you would for regular posts, all you need to do is register the custom type with wp-graphql on instantiation by setting the postTypes config parameter. This will set up resolvers using the same conventions as regular posts.

import WPGraphQL from  'wp-graphql';

const gql = new WPGraphQL('http://localhost:8080/wp-json', {
    postTypes: [
        { name: 'book', namePlural: 'books', restBase: 'books' },
    ],
});

gql.send(`
    mutation {
        addBook(title: "My book", content: "My book content") {
            title
        }
        updateBook(id: 1, title: "My new book title") {
            id
        }
        deleteBook(id: 1) {
            ... on DeletedBook {
                deleted
            }
        }
    }
    query {
        books {
            id
        }
        book(id: 1) {
            author
        }
    }
`);

Using default queries, mutations, and schema in your own server side JS codebase

import * as express from 'express';
import * as graphqlHTTP from 'express-graphql';
import WPGraphQL, { schema } from 'wp-graphql';

const app = express();

const gql = new WPGraphQL('https://my-wordpess-site.com/wp-json');

app.use('/', graphqlHTTP({
  schema,
  rootValue: gql,
}));

app.listen(3000);

Limitations

The primary limitation of this library is that it only provides a convenient way to fetch and mutate data over top of the existing REST API on the client side. Because of this, the primary benefit of GraphQL, querying and mutating data in a single round trip, is lost.

The "Meta" Limitiation

In order to be declarative, GraphQL requires users to be explicit about the shape of their API responses. This creates a unique problem with Post, User, and Comment meta, since all three objects can have essentially an unlimited number of shapes.

Because there is no reasonable way to know up front what shape the Meta fields are going to be, the meta must be JSON.stringified prior to being transacted by GraphQL. This process is done automatically for queries, but must be done manually for mutations.

With that in mind, it's important to remember that GraphQL only sees the meta fields as a String. When the meta field is returned to you, it will be converted to back to an object by wp-graphql. Although it is an object when you receive it, you will not be able to query into the meta fields like you would with a typical GraphQL object. In other words, the meta field is a leaf type.

1.0.0

7 years ago

0.12.0

7 years ago

0.11.0

7 years ago

0.10.1

7 years ago

0.10.0

7 years ago

0.9.0

7 years ago

0.8.0

7 years ago

0.7.1

7 years ago

0.7.0

7 years ago

0.6.0

7 years ago

0.5.0

7 years ago

0.4.0

7 years ago

0.3.0

7 years ago

0.2.1

7 years ago

0.2.0

7 years ago

0.1.0

7 years ago