Giter Club home page Giter Club logo

node-wpapi's Introduction

A WordPress REST API client for JavaScript

This library is an isomorphic client for the WordPress REST API, designed to work with WordPress 5.0 or later. If you are using the older WP REST API plugin or WordPress 4.9, some commands will not work.

Gitter

Build Status

Index:

About

node-wpapi is an isomorphic JavaScript client for the WordPress REST API that makes it easy for your JavaScript application to request specific resources from a WordPress website. It uses a query builder-style syntax to let you craft the request being made to REST API endpoints, then returns the API's response to your application as a JSON object. And don't let the name fool you: with Webpack or Browserify, node-wpapi works just as well in the browser as it does on the server!

This library is maintained by K. Adam White at Human Made, with contributions from a great community of WordPress and JavaScript developers.

To get started, npm install wpapi or download the browser build and check out "Installation" and "Using the Client" below.

Installation

node-wpapi works both on the server or in the browser. Node.js version 8.6 or higher (or version 8.2.1 with the --harmony flag) is required, and the latest LTS release is recommended.

In the browser node-wpapi officially supports the latest two versions of all evergreen browsers, and Internet Explorer 11.

Install with NPM

To use the library from Node, install it with npm:

npm install --save wpapi

Then, within your application's script files, require the module to gain access to it. As wpapi is both a query builder and a transport layer (i.e. a tool for getting and sending HTTP requests), we leave it up to you as the author of your application whether you need both parts of this functionality. You may use wpapi with superagent if you wish to send and receive HTTP requests using this library, but you may also use only the query builder part of the library if you intend to submit your HTTP requests with fetch, axios or other tools.

To import only the query builder (without the .get(), .create(), .delete(), .update() or .then() chaining methods):

var WPAPI = require( 'wpapi' );

To import the superagent bundle, which contains the full suite of HTTP interaction methods:

var WPAPI = require( 'wpapi/superagent' );

This library is designed to work in the browser as well, via a build system such as Browserify or Webpack; just install the package and require( 'wpapi' ) (or 'wpapi/superagent') from your application code.

Download the UMD Bundle

Alternatively, you may download a ZIP archive of the bundled library code. These files are UMD modules, which may be included directly on a page using a regular <script> tag or required via AMD or CommonJS module systems. In the absence of a module system, the UMD modules will export the browser global variable WPAPI, which can be used in place of require( 'wpapi' ) to access the library from your code.

At present this browser bundle tracks the wpapi/superagent module, and includes Superagent itself.

Upgrading from v1

Require wpapi/superagent

Prior to version 2.0 (currently alpha status) this library shipped with built-in HTTP functionality using Superagent.

If you maintain an existing project which uses this library and wish to upgrade to v2, you may do so by manually installing Superagent:

npm i --save wpapi@alpha superagent

and then changing your require statements to use the wpapi/superagent entrypoint:

--- const WPAPI = require( 'wpapi' );
+++ const WPAPI = require( 'wpapi/superagent' );

Use Promises instead of Callbacks

In version 1, you could use "Node-style" error-first callback functions instead of chaining promises. As of version 2, this callback style is no longer supported; use Promise .then syntax or async/await instead.

// Version 1
wp.posts().get( function( error, posts ) { /* ... */ } );

// Version 2, Promises syntax
wp.posts().get().then( posts => { /* ... */ } );

// Version 2, await syntax
await wp.posts().get();

Using the Client

The module is a constructor, so you can create an instance of the API client bound to the endpoint for your WordPress install:

var WPAPI = require( 'wpapi/superagent' );
var wp = new WPAPI({ endpoint: 'http://src.wordpress-develop.dev/wp-json' });

Once an instance is constructed, you can chain off of it to construct a specific request. (Think of it as a query-builder for WordPress!)

Compatibility Note: As of Version 2.0, Node-style error-first callbacks are no longer supported by this library. All request methods return Promises.

// Request methods return Promises.
wp.posts().get()
    .then(function( data ) {
        // do something with the returned posts
    })
    .catch(function( err ) {
        // handle error
    });

The wp object has endpoint handler methods for every endpoint that ships with the default WordPress REST API plugin.

Once you have used the chaining methods to describe a resource, you may call .create(), .get(), .update() or .delete() to send the API request to create, read, update or delete content within WordPress. These methods are documented in further detail below.

Self-signed (Insecure) HTTPS Certificates

In a case where you would want to connect to a HTTPS WordPress installation that has a self-signed certificate (insecure), you will need to force a connection by placing the following line before you make any wp calls.

process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

Auto-Discovery

It is also possible to leverage the capability discovery features of the API to automatically detect and add setter methods for your custom routes, or routes added by plugins.

To utilize the auto-discovery functionality, call WPAPI.discover() with a URL within a WordPress REST API-enabled site:

var apiPromise = WPAPI.discover( 'http://my-site.com' );

If auto-discovery succeeds this method returns a promise that will be resolved with a WPAPI client instance object configured specifically for your site. You can use that promise as the queue that your client instance is ready, then use the client normally within the .then callback.

Custom Routes will be detected by this process, and registered on the client. To prevent name conflicts, only routes in the wp/v2 namespace will be bound to your instance object itself. The rest can be accessed through the .namespace method on the WPAPI instance, as demonstrated below.

apiPromise.then(function( site ) {
    // If default routes were detected, they are now available
    site.posts().then(function( posts ) {
        console.log( posts );
    }); // etc

    // If custom routes were detected, they can be accessed via .namespace()
    site.namespace( 'myplugin/v1' ).authors()
        .then(function( authors ) { /* ... */ });

    // Namespaces can be saved out to variables:
    var myplugin = site.namespace( 'myplugin/v1' );
    myplugin.authors()
        .id( 7 )
        .then(function( author ) { /* ... */ });
});

Authenticating with Auto-Discovery

While using WPAPI.discover( url ) to generate the handler for your site gets you up and running quickly, it does not provide the same level of customization as instantiating your own new WPAPI object. In order to specify authentication configuration when using autodiscovery, chain a .then onto the initial discovery query to call the .auth method on the returned site object with the relevant credentials (username & password, nonce, etc):

var apiPromise = WPAPI.discover( 'http://my-site.com' ).then(function( site ) {
    return site.auth({
        username: 'admin',
        password: 'always use secure passwords'
    });
});
apiPromise.then(function( site ) {
    // site is now configured to use authentication
})

Cross-Origin Auto-Discovery

When attempting auto-discovery against a remote server in a client-side environment, discovery will fail unless the server is configured for Cross-Origin Resource Sharing (CORS). CORS can be enabled by specifying a set of Access-Control- headers in your PHP code to instruct browsers that requests from remote clients are accepted; these headers also let you control what specific methods and links are exposed to those remote clients.

The WP-REST-Allow-All-Cors plugin will permit CORS requests for all API resources. Auto-discovery will still fail when using this plugin, however, because discovery depends on the presence of a Link header on WordPress pages outside of the root REST API endpoint.

To permit your site to be auto-discovered by client-side REST API clients, add a filter to send_headers to explicitly whitelist the Link header for HEAD requests:

add_action( 'send_headers', function() {
	if ( ! did_action('rest_api_init') && $_SERVER['REQUEST_METHOD'] == 'HEAD' ) {
		header( 'Access-Control-Allow-Origin: *' );
		header( 'Access-Control-Expose-Headers: Link' );
		header( 'Access-Control-Allow-Methods: HEAD' );
	}
} );

Enable CORS at your own discretion. Restricting Access-Control-Allow-Origin to a specific origin domain is often preferable to allowing all origins via *.

Bootstrapping

If you are building an application designed to interface with a specific site, it is possible to sidestep the additional asynchronous HTTP calls that are needed to bootstrap the client through auto-discovery. You can download the root API response, i.e. the JSON response when you hit the root endpoint such as your-site.com/wp-json, and save that JSON file locally; then, in your application code, just require in that JSON file and pass the routes property into the WPAPI constructor or the WPAPI.site method.

Note that you must specify the endpoint URL as normal when using this approach.

var apiRootJSON = require( './my-endpoint-response.json' );
var site = new WPAPI({
    endpoint: 'http://my-site.com/wp-json',
    routes: apiRootJSON.routes
});

// site is now ready to be used with all methods defined in the
// my-endpoint-response.json file, with no need to wait for a Promise.

site.namespace( 'myplugin/v1' ).authors()...

To create a slimmed JSON file dedicated to this particular purpose, see the npm script npm run update-default-routes-json, which will let you download and save an endpoint response to your local project.

In addition to retrieving the specified resource with .get(), you can also .create(), .update() and .delete() resources:

Creating Posts

To create posts, use the .create() method on a query to POST (the HTTP verb for "create") a data object to the server:

// You must authenticate to be able to POST (create) a post
var wp = new WPAPI({
    endpoint: 'http://your-site.com/wp-json',
    // This assumes you are using basic auth, as described further below
    username: 'someusername',
    password: 'password'
});
wp.posts().create({
    // "title" and "content" are the only required properties
    title: 'Your Post Title',
    content: 'Your post content',
    // Post will be created as a draft by default if a specific "status"
    // is not specified
    status: 'publish'
}).then(function( response ) {
    // "response" will hold all properties of your newly-created post,
    // including the unique `id` the post was assigned on creation
    console.log( response.id );
})

This will work in the same manner for resources other than post: you can see the list of required data parameters for each resource on the REST API Developer Handbook.

Updating Posts

To create posts, use the .update() method on a single-item query to PUT (the HTTP verb for "update") a data object to the server:

// You must authenticate to be able to PUT (update) a post
var wp = new WPAPI({
    endpoint: 'http://your-site.com/wp-json',
    // This assumes you are using basic auth, as described further below
    username: 'someusername',
    password: 'password'
});
// .id() must be used to specify the post we are updating
wp.posts().id( 2501 ).update({
    // Update the title
    title: 'A Better Title',
    // Set the post live (assuming it was "draft" before)
    status: 'publish'
}).then(function( response ) {
    console.log( response );
})

This will work in the same manner for resources other than post: you can see the list of required data parameters for each resource in the REST API Developer Handbook.

Requesting Different Resources

A WPAPI instance object provides the following basic request methods:

  • wp.posts()...: Request items from the /posts endpoints
  • wp.pages()...: Start a request for the /pages endpoints
  • wp.types()...: Get Post Type collections and objects from the /types endpoints
  • wp.comments()...: Start a request for the /comments endpoints
  • wp.taxonomies()...: Generate a request against the /taxonomies endpoints
  • wp.tags()...: Get or create tags with the /tags endpoint
  • wp.categories()...: Get or create categories with the /categories endpoint
  • wp.statuses()...: Get resources within the /statuses endpoints
  • wp.users()...: Get resources within the /users endpoints
  • wp.search()...: Find resources of any [REST-enabled] post type matching a ?search= string
  • wp.media()...: Get Media collections and objects from the /media endpoints
  • wp.themes()...: Read information about the active theme from the /themes endpoint (always requires authentication)
  • wp.settings()...: Read or update site settings from the /settings endpoint (always requires authentication)
  • wp.blocks()...: Create queries against the blocks endpoint

All of these methods return a customizable request object. The request object can be further refined with chaining methods, and/or sent to the server via .get(), .create(), .update(), .delete(), .headers(), or .then(). (Not all endpoints support all methods; for example, you cannot POST or PUT records on /types, as these are defined in WordPress plugin or theme code.)

Additional querying methods provided, by endpoint:

  • posts
    • wp.posts(): get a collection of posts (default query)
    • wp.posts().id( n ): get the post with ID n
    • wp.posts().id( n ).revisions(): get a collection of revisions for the post with ID n
    • wp.posts().id( n ).revisions( rn ): get revision rn for the post with ID n
  • pages
    • wp.pages(): get a collection of page items
    • wp.pages().id( n ): get the page with numeric ID n
    • wp.pages().path( 'path/str' ): get the page with the root-relative URL path path/str
    • wp.pages().id( n ).revisions(): get a collection of revisions for the page with ID n
    • wp.pages().id( n ).revisions( rn ): get revision rn for the page with ID n
  • comments
    • wp.comments(): get a collection of all public comments
    • wp.comments().id( n ): get the comment with ID n
  • taxonomies
    • wp.taxonomies(): retrieve all registered taxonomies
    • wp.taxonomies().taxonomy( 'taxonomy_name' ): get a specific taxonomy object with name taxonomy_name
  • categories
    • wp.categories(): retrieve all registered categories
    • wp.categories().id( n ): get a specific category object with id n
  • tags
    • wp.tags(): retrieve all registered tags
    • wp.tags().id( n ): get a specific tag object with id n
  • custom taxonomy terms
  • types
    • wp.types(): get a collection of all registered public post types
    • wp.types().type( 'cpt_name' ): get the object for the custom post type with the name cpt_name
  • statuses
    • wp.statuses(): get a collection of all registered public post statuses (if the query is authenticated—will just display "published" if unauthenticated)
    • wp.statuses().status( 'slug' ): get the object for the status with the slug slug
  • users
    • wp.users(): get a collection of users (will show only users with published content if request is not authenticated)
    • wp.users().id( n ): get the user with ID n (does not require authentication if that user is a published author within the blog)
    • wp.users().me(): get the authenticated user's record
  • media
    • wp.media(): get a collection of media objects (attachments)
    • wp.media().id( n ): get media object with ID n
  • settings
    • wp.settings(): get or update one or many site settings

For security reasons, methods like .revisions() and .settings() require the request to be authenticated, and others such as .users() and .posts() will return only a subset of their information without authentication.

toString()

To get the URI of the resource without making a request, call .toString() at the end of a query chain:

var uriString = wp.posts().id( 7 ).embed().toString();

As the name implies .toString() is not a chaining method, and will return a string containing the full URI; this can then be used with alternative HTTP transports like request, Node's native http, fetch, or jQuery.

API Query Parameters

To set a query parameter on a request, use the .param() method:

// All posts by author w/ ID "7" published before Sept 22, 2016
wp.posts()
  .param( 'before', new Date( '2016-09-22' ) )
  .param( 'author', 7 )...

You can continue to chain properties until you call .then, .get, .create, .update, or .delete on the request chain.

Parameter Shortcut Methods

This library provides convenience methods for many of the most common parameters, like search= (search for a string in post title or content), slug (query for a post by slug), and before and after (find posts in a given date range):

// Find a page with a specific slug
wp.pages().slug( 'about' )...

// Find a post authored by the user with ID #42
wp.posts().author( 42 )...

// Find trashed posts
wp.posts().status( 'trash' )...

// Find posts in status "future" or "draft"
wp.posts().status([ 'draft', 'future' ])...

// Find all categories containing the word "news"
wp.categories().search( 'news' )...

// Find posts from March 2013 (provide a Date object or full ISO-8601 date):
wp.posts().before( '2013-04-01T00:00:00.000Z' ).after( new Date( 'March 01, 2013' ) )...

// Return ONLY sticky posts
wp.posts().sticky( true )...

// Return NO sticky posts
wp.posts().sticky( false )...

// Supply the password for a password-protected post
wp.posts().id( 2501 ).password( 'correct horse battery staple' )...

Paging & Sorting

Convenience methods are also available to set paging & sorting properties like page, per_page (available as .perPage()), offset, order and orderby:

// perPage() sets the maximum number of posts to return. 20 latest posts:
wp.posts().perPage( 20 )...
// 21st through 40th latest posts (*i.e.* the second page of results):
wp.posts().perPage( 20 ).page( 2 )...
// Order posts alphabetically by title:
wp.posts().order( 'asc' ).orderby( 'title' )...

See the section on collection pagination for more information.

Filtering by Taxonomy Terms

A variety of other methods are available to further modify which posts are returned from the API. For example, to restrict the returned posts to only those in category 7, pass that ID to the .categories() method:

wp.posts().categories( 7 )...

Relationships in the REST API are always specified by ID. The slug of a term may change, but the term ID associated with the underlying post will not.

To find the ID of a tag or category for which the slug is known, you can query the associated collection with .slug() and use the ID of the returned object in a two-step process:

wp.categories().slug( 'fiction' )
    .then(function( cats ) {
        // .slug() queries will always return as an array
        var fictionCat = cats[0];
        return wp.posts().categories( fictionCat.id );
    })
    .then(function( postsInFiction ) {
        // These posts are all categorized "fiction":
        console.log( postsInFiction );
    });

To find posts in category 'fiction' and tagged either 'magical-realism' or 'historical', this process can be extended: note that this example uses the RSVP.hash utility for convenience and parallelism, but the same result could easily be accomplished with Promise.all or by chaining each request.

RSVP.hash({
  categories: wp.categories().slug( 'fiction' ),
  tags1: wp.tags().slug('magical-realism'),
  tags2: wp.tags().slug('historical')
}).then(function( results ) {
    // Combine & map .slug() results into arrays of IDs by taxonomy
    var tagIDs = results.tags1.concat( results.tags2 )
        .map(function( tag ) { return tag.id; });
    var categoryIDs = results.categories
        .map(function( cat ) { return cat.id; });
    return wp.posts()
        .tags( tags )
        .categories( categories );
}).then(function( posts ) {
    // These posts are all fiction, either magical realism or historical:
    console.log( posts );
});

This process may seem cumbersome, but it provides a more broadly reliable method of querying than querying by mutable slugs. The first requests may also be avoided entirely by pre-creating and storing a dictionary of term slugs and their associated IDs in your application; however, be aware that this dictionary must be updated whenever slugs change.

It is also possible to add your own slug-oriented query parameters to a site that you control by creating a plugin that registers additional collection parameter arguments.

Excluding terms

Just as .categories() and .tags() can be used to return posts that are associated with one or more taxonomies, two methods exist to exclude posts by their term associations.

  • .excludeCategories() is a shortcut for .param( 'categories_exclude', ... ) which excludes results associated with the provided category term IDs
  • .excludeTags() is a shortcut for .param( 'tags_exclude', ... ) which excludes results associated with the provided tag term IDs

Custom Taxonomies

Just as the ?categories and ?categories_exclude parameters are available for use with the built-in taxonomies, any custom taxonomy that is registered with a rest_base argument has a ?{taxonomy rest_base} and ?{taxonomy rest_base}_exclude parameter available, which can be set directly using .param. For the custom taxonomy genres, for example:

  • wp.posts().param( 'genres', [ array of genre term IDs ]): return only records associated with any of the provided genres
  • wp.posts().param( 'genres_exclude', [ array of genre term IDs ]): return only records associated with none of the provided genres

Retrieving posts by author

The .author() method also exists to query for posts authored by a specific user (specified by ID).

// equivalent to .param( 'author', 42 ):
wp.posts().author( 42 ).get();

// last value wins: this queries for author == 71
wp.posts().author( 42 ).author( 71 ).get();

As with categories and tags, the /users endpoint may be queried by slug to retrieve the ID to use in this query, if needed.

Password-Protected posts

The .password() method (not to be confused with the password property of .auth()!) sets the password to use to view a password-protected post. Any post for which the content is protected will have protected: true set on its content and excerpt properties; content.rendered and excerpt.rendered will both be '' until the password is provided by query string.

wp.posts().id( idOfProtectedPost )
    .then(function( result ) {
        console.log( result.content.protected ); // true
        console.log( result.content.rendered ); // ""
    });

wp.posts.id( idOfProtectedPost )
    // Provide the password string with the request
    .password( 'thepasswordstring' )
    .then(function( result ) {
        console.log( result.content.rendered ); // "The post content"
    });

Other Filters

The ?filter query parameter is not natively supported within the WordPress core REST API endpoints, but can be added to your site using the rest-filter plugin. filter is a special query parameter that lets you directly specify many WP_Query arguments, including tag, author_name, and other public query vars. Even more parameters are available for use with filter once you authenticate with the API.

If your environment supports this parameter, other filtering methods will be available if you initialize your site using auto-discovery, which will auto-detect the availability of filter:

WPAPI.discover( 'http://mysite.com' )
    .then(function( site ) {
        // Apply an arbitrary `filter` query parameter:
        // All posts belonging to author with nicename "jadenbeirne"
        wp.posts().filter( 'author_name', 'jadenbeirne' ).get();

        // Query by the slug of a category or tag
        // Get all posts in category "islands" and tags "clouds" & "sunset"
        // (filter can either accept two parameters, as above where it's called with
        // a key and a value, or an object of parameter keys and values, as below)
        wp.posts().filter({
            category_name: 'islands',
            tag: [ 'clouds', 'sunset' ]
        })...

        // Query for a page at a specific URL path
        wp.pages().filter( 'pagename', 'some/url/path' )..
    });

Date Filter Methods

?before and ?after provide first-party support for querying by date, but should you have access to filter then three additional date query methods are available to return posts from a specific month, day or year:

  • .year( year ): find items published in the specified year
  • .month( month ): find items published in the specified month, designated by the month index (1–12) or name (e.g. "February")
  • .day( day ): find items published on the specified day

Uploading Media

Files may be uploaded to the WordPress media library by creating a media record using the .media() collection handler.

The file to upload can be specified as

  • a String describing an image file path, e.g. '/path/to/the/image.jpg'
  • a Buffer with file content, e.g. Buffer.from() (or the result of a readFile call)
  • a file object from a <input> element, e.g. document.getElementById( 'file-input' ).files[0]

The file is passed into the .file() method:

wp.media().file(content [, name])...

The optional second string argument specifies the file name to use for the uploaded media. If the name argument is omitted file() will try to infer a filename from the provided file path. Note that when uploading a Buffer object name is a required argument, because no name can be automatically inferred from the buffer.

Adding Media to a Post

If you wish to associate a newly-uploaded media record to a specific post, you must use two calls: one to first upload the file, then another to associate it with a post. Example code:

wp.media()
    // Specify a path to the file you want to upload, or a Buffer
    .file( '/path/to/the/image.jpg' )
    .create({
        title: 'My awesome image',
        alt_text: 'an image of something awesome',
        caption: 'This is the caption text',
        description: 'More explanatory information'
    })
    .then(function( response ) {
        // Your media is now uploaded: let's associate it with a post
        var newImageId = response.id;
        return wp.media().id( newImageId ).update({
            post: associatedPostId
        });
    })
    .then(function( response ) {
        console.log( 'Media ID #' + response.id );
        console.log( 'is now associated with Post ID #' + response.post );
    });

If you are uploading media from the client side, you can pass a reference to a file input's file list entry in place of the file path:

wp.media()
    .file( document.getElementById( 'file-input' ).files[0] )
    .create()...

Custom Routes

Support for Custom Post Types is provided via the .registerRoute method. This method returns a handler function which can be assigned to your site instance as a method, and takes the same namespace and route string arguments as rest_register_route:

var site = new WPAPI({ endpoint: 'http://www.yoursite.com/wp-json' });
site.myCustomResource = site.registerRoute( 'myplugin/v1', '/author/(?P<id>)' );
site.myCustomResource().id( 17 ); // => myplugin/v1/author/17

The string (?P<id>) indicates that a level of the route for this resource is a dynamic property named ID. By default, properties identified in this fashion will not have any inherent validation. This is designed to give developers the flexibility to pass in anything, with the caveat that only valid IDs will be accepted on the WordPress end.

You might notice that in the example from the official WP-API documentation, a pattern is specified with a different format: this is a regular expression designed to validate the values that may be used for this capture group.

var site = new WPAPI({ endpoint: 'http://www.yoursite.com/wp-json' });
site.myCustomResource = site.registerRoute( 'myplugin/v1', '/author/(?P<id>\\d+)' );
site.myCustomResource().id( 7 ); // => myplugin/v1/author/7
site.myCustomResource().id( 'foo' ); // => Error: Invalid path component: foo does not match (?P<a>\d+)

Adding the regular expression pattern (as a string) enabled validation for this component. In this case, the \\d+ will cause only numeric values to be accepted.

NOTE THE DOUBLE-SLASHES in the route definition here, however:

'/author/(?P<id>\\d+)'

This is a JavaScript string, where \ must be written as \\ to be parsed properly. A single backslash will break the route's validation.

Each named group in the route will be converted into a named setter method on the route handler, as in .id() in the example above: that name is taken from the <id> in the route string.

The route string 'pages/(?P<parentPage>[\\d]+)/revisions/(?P<id>[\\d]+)' would create the setters .parentPage() and id(), permitting any permutation of the provided URL to be created.

Setter method naming for named route components

In the example above, registering the route string '/author/(?P<id>\\d+)' results in the creation of an .id() method on the resulting resource handler:

site.myCustomResource().id( 7 ); // => myplugin/v1/author/7

If a named route component (e.g. the "id" part in (?P<id>\\d+), above) is in snake_case, then that setter will be converted to camelCase instead, as with some_part below:

site.myCustomResource = site.registerRoute( 'myplugin/v1', '/resource/(?P<some_part>\\d+)' );
site.myCustomResource().somePart( 7 ); // => myplugin/v1/resource/7

Non-snake_cased route parameter names will be unaffected.

Query Parameters & Filtering Custom Routes

Many of the filtering methods available on the built-in collections are built in to custom-registered handlers, including .page(), .perPage(), .search(), .include()/.exclude() and .slug(); these parameters are supported across nearly all API endpoints, so they are made available automatically to custom endpoints as well.

However not every filtering method is available by default, so for convenience a configuration object may be passed to the registerRoute method with a params property specifying additional query parameters to support. This makes it very easy to add existing methods like .before() or .after() to your own endpoints:

site.handler = site.registerRoute( 'myplugin/v1', 'collection/(?P<id>)', {
    // Listing any of these parameters will assign the built-in
    // chaining method that handles the parameter:
    params: [ 'before', 'after', 'author', 'parent', 'post' ]
});
// yields
site.handler().post( 8 ).author( 92 ).before( dateObj )...

If you wish to set custom parameters, for example to query by the custom taxonomy genre, you can use the .param() method as usual:

site.handler().param( 'genre', genreTermId );

but you can also specify additional query parameter names and a .param() wrapper function will be added automatically. e.g. here .genre( x ) will be created as a shortcut for .param( 'genre', x ):

site.books = site.registerRoute( 'myplugin/v1', 'books/(?P<id>)', {
    params: [ 'genre' ]
});
// yields
site.books().genre([ genreId1, genreId2 ])...

Mixins

To assign completely arbitrary custom methods for use with your custom endpoints, a configuration object may be passed to the registerRoute method with a mixins property defining any functions to add:

site.handler = site.registerRoute( 'myplugin/v1', 'collection/(?P<id>)', {
    mixins: {
        myParam: function( val ) {
            return this.param( 'my_param', val );
        }
    }
});

This permits a developer to extend an endpoint with arbitrary parameters in the same manner as is done for the automatically-generated built-in route handlers.

Note that mixins should always return this to support method chaining.

Embedding Data

Data types in WordPress are interrelated: A post has an author, some number of tags, some number of categories, etc. By default, the API responses will provide pointers to these related objects, but will not embed the full resources: so, for example, the "author" property would come back as just the author's ID, e.g. "author": 4.

This functionality provides API consumers the flexibility to determine when and how they retrieve the related data. However, there are also times where an API consumer will want to get the most data in the fewest number of responses. Certain resources (author, comments, tags, and categories, to name a few) support embedding, meaning that they can be included in the response if the _embed query parameter is set.

To request that the API respond with embedded data, simply call .embed() as part of the request chain:

wp.posts().id( 2501 ).embed()...

This will include an ._embedded object in the response JSON, which contains all of those embeddable objects:

{
    "_embedded": {
        "author": [ /* ... */ ],
        "replies": [ /* ... */ ],
        "wp:attachment": [ /* ... */ ],
        "wp:term": [
            [ {}, {} /* category terms */ ],
            [ {} /* tag terms */ ],
            /* etc... */
        ],
        "wp:meta": [ /* ... */ ]
    }
}

For more on working with embedded data, check out the WP-API documentation.

Collection Pagination

WordPress sites can have a lot of content—far more than you'd want to pull down in a single request. The API endpoints default to providing a limited number of items per request, the same way that a WordPress site will default to 10 posts per page in archive views. The number of objects you can get back can be adjusted by calling the perPage method, but perPage is capped at 100 items per request for performance reasons. To work around these restrictions, the API provides headers so the API will frequently have to return your posts be unable to fit all of your posts in a single query.

Using Pagination Headers

Paginated collection responses are augmented with a _paging property derived from the collection's pagination headers. That _paging property on the response object contains some useful metadata:

  • .total: The total number of records matching the provided query
  • .totalPages: The number of pages available (total / perPage)
  • .next: A WPRequest object pre-bound to the next page of results
  • .prev: A WPRequest object pre-bound to the previous page of results
  • .links: an object containing the parsed link HTTP header data (when present)

The existence of the _paging.links.prev and _paging.links.next properties can be used as flags to conditionally show or hide your paging UI, if necessary, as they will only be present when an adjacent page of results is available.

You can use the next and prev properties to traverse an entire collection, should you so choose. For example, this snippet will recursively request the next page of posts and concatenate it with existing results, in order to build up an array of every post on your site:

var _ = require( 'lodash' );
function getAll( request ) {
  return request.then(function( response ) {
    if ( ! response._paging || ! response._paging.next ) {
      return response;
    }
    // Request the next page and return both responses as one collection
    return Promise.all([
      response,
      getAll( response._paging.next )
    ]).then(function( responses ) {
      return _.flatten( responses );
    });
  });
}
// Kick off the request
getAll( wp.posts() ).then(function( allPosts ) { /* ... */ });

Be aware that this sort of unbounded recursion can take a very long time: if you use this technique in your application, we strongly recommend caching the response objects in a local database rather than re-requesting from the WP remote every time you need them.

Depending on the amount of content in your site loading all posts into memory may also exceed Node's available memory, causing an exception. If this occurs, try to work with smaller subsets of your data at a time.

Requesting a Specific Page

You can also use a .page(pagenumber) method on calls that support pagination to directly get that page. For example, to set the API to return 5 posts on every page of results, and to get the third page of results (posts 11 through 15), you would write

wp.posts().perPage( 5 ).page( 3 ).then(/* ... */);

Using offset

If you prefer to think about your collections in terms of offset, or how many items "into" the collection you want to query, you can use the offset parameter (and parameter convenience method) instead of page. These are equivalent:

// With .page()
wp.posts().perPage( 5 ).page( 3 )...
// With .offset()
wp.posts().perPage( 5 ).offset( 10 )...

Customizing HTTP Request Behavior

By default node-wpapi uses the superagent library internally to make HTTP requests against the API endpoints. Superagent is a flexible tool that works on both the client and the browser, but you may want to use a different HTTP library, or to get data from a cache when available instead of making an HTTP request. To facilitate this, node-wpapi lets you supply a transport object when instantiating a site client to specify custom functions to use for one (or all) of GET, POST, PUT, DELETE & HEAD requests.

This is advanced behavior; you will only need to utilize this functionality if your application has very specific HTTP handling or caching requirements.

In order to maintain consistency with the rest of the API, custom transport methods should take in a WordPress API route handler query object (e.g. the result of calling wp.posts()... or any of the other chaining resource handlers)and a data object (for POST, PUT and DELETE requests).

Note: Node-style error-first callbacks are no longer supported by this library as of version 2.0. Custom transport methods should therefore not accept or expect a third optional callback parameter.

The default HTTP transport methods are available as WPAPI.transport (a property of the constructor object) and may be called within your transports if you wish to extend the existing behavior, as in the example below.

Example: Cache requests in a simple dictionary object, keyed by request URI. If a request's response is already available, serve from the cache; if not, use the default GET transport method to retrieve the data, save it in the cache, and return it to the consumer:

var site = new WPAPI({
  endpoint: 'http://my-site.com/wp-json',
  transport: {
    // Only override the transport for the GET method, in this example
    // Transport methods should take a wpreq object:
    get: function( wpreq ) {
      var result = cache[ wpreq ];
      // If a cache hit is found, return it wrapped in a Promise:
      if ( result ) {
        // Return the data as a promise
        return Promise.resolve( result );
      }

      // Delegate to default transport if no cached data was found
      return WPAPI.transport.get( wpreq, cb ).then(function( result ) {
        cache[ wpreq ] = result;
        return result;
      });
    }
  }
});

You may set one or many custom HTTP transport methods on an existing WP site client instance (for example one returned through auto-discovery by calling the .transport() method on the site client instance and passing an object of handler functions:

site.transport({
    get: function( wpreq ) { /* ... */},
    put: function( wpreq, data ) { /* ... */}
});

Note that these transport methods are the internal methods used by create and .update, so the names of these methods therefore map to the HTTP verbs "get", "post", "put", "head" and "delete"; name your transport methods accordingly or they will not be used.

Specifying HTTP Headers

If you need to send additional HTTP headers along with your request (for example to provide a specific Authorization header for use with alternative authentication schemes), you can use the .setHeaders() method to specify one or more headers to send with the dispatched request:

Set headers for a single request

// Specify a single header to send with the outgoing request
wp.posts().setHeaders( 'Authorization', 'Bearer xxxxx.yyyyy.zzzzz' )...

// Specify multiple headers to send with the outgoing request
wp.posts().setHeaders({
    Authorization: 'Bearer xxxxx.yyyyy.zzzzz',
    'Accept-Language': 'pt-BR'
})...

Set headers globally

You can also set headers globally on the WPAPI instance itself, which will then be used for all subsequent requests created from that site instance:

// Specify a header to be used by all subsequent requests
wp.setHeaders( 'Authorization', 'Bearer xxxxx.yyyyy.zzzzz' );

// These will now be sent with an Authorization header
wp.users().me()...
wp.posts().id( unpublishedPostId )...

Authentication

You must be authenticated with WordPress to create, edit or delete resources via the API. Some WP-API endpoints additionally require authentication for GET requests in cases where the data being requested could be considered private: examples include any of the /users endpoints, requests where the context query parameter is true, and /revisions for posts and pages, among others.

Basic Authentication

This library currently supports basic HTTP authentication. To authenticate with your WordPress install,

  1. Download and install the Basic Authentication handler plugin on your target WordPress site. (Note that the basic auth handler is not curently available through the plugin repository: you must install it manually.)
  2. Activate the plugin.
  3. Specify the username and password of an authorized user (a user that can edit_posts) when instantiating the WPAPI request object:
var wp = new WPAPI({
    endpoint: 'http://www.website.com/wp-json',
    username: 'someusername',
    password: 'thepasswordforthatuser'
});

Now any requests generated from this WPAPI instance will use that username and password for basic authentication if the targeted endpoint requires it.

As an example, wp.users().me() will automatically enable authentication to permit access to the /users/me endpoint. (If a username and password had not been provided, a 401 error would have been returned.)

Manually forcing authentication

Because authentication may not always be set when needed, an .auth() method is provided which can enable authentication for any request chain:

// This will authenticate the GET to /posts/id/817
wp.posts().id( 817 ).auth().get(...

This .auth method can also be used to manually specify a username and a password as part of a request chain:

// Use username "mcurie" and password "nobel" for this request
wp.posts().id( 817 ).auth( {username: 'mcurie', password: 'nobel'} ).get(...

This will override any previously-set username or password values.

Authenticate all requests for a WPAPI instance

It is possible to make all requests from a WPAPI instance use authentication by setting the auth option to true on instantiation:

var wp = new WPAPI({
    endpoint: // ...
    username: // ...
    password: // ...
    auth: true
});

SECURITY WARNING

Please be aware that basic authentication sends your username and password over the wire, in plain text. We only recommend using basic authentication in production if you are securing your requests with SSL.

More robust authentication methods will hopefully be added; we would welcome contributions in this area!

Cookie Authentication

When the library is loaded from the frontend of the WordPress site you are querying against, you may authenticate your REST API requests using the built in WordPress Cookie authentication by creating and passing a Nonce with your API requests.

First localize your scripts with an object with root-url and nonce in your theme's functions.php or your plugin:

function my_enqueue_scripts() {
    wp_enqueue_script( 'app', get_template_directory_uri() . '/assets/dist/bundle.js', array(), false, true );
    wp_localize_script( 'app', 'WP_API_Settings', array(
        'endpoint' => esc_url_raw( rest_url() ),
        'nonce' => wp_create_nonce( 'wp_rest' )
    ) );
}
add_action( 'wp_enqueue_scripts', 'my_enqueue_scripts' );

And then use this nonce when initializing the library:

var WPAPI = require( 'wpapi/superagent' );
var wp = new WPAPI({
    endpoint: window.WP_API_Settings.endpoint,
    nonce: window.WP_API_Settings.nonce
});

API Documentation

In addition to the above getting-started guide, we have automatically-generated API documentation.

Issues

If you identify any errors in this module, or have an idea for an improvement, please open an issue. We're excited to see what the community thinks of this project, and we would love your input!

Contributing

We welcome contributions large and small. See our contributor guide for more information.

node-wpapi's People

Contributors

aaronjorbin avatar artoliukkonen avatar bmac avatar brad-decker avatar breadadams avatar bt avatar carldanley avatar computamike avatar dimadin avatar edygar avatar freepad avatar gambry avatar gerhardsletten avatar gitter-badger avatar jasonphillips avatar jerolan avatar kadamwhite avatar luisherranz avatar ntwb avatar ohar avatar panman avatar sdgluck avatar tbranyen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

node-wpapi's Issues

Implement lo-dash/underscore support for return values

I had the idea of extending the return values to support lo-dash or underscore so the developer could make use of sorting functions (among everything else these libraries support).

Though, after talking with @kadamwhite - I feel that we should make this as lightweight as possible and let the developer choose how they want to take action on the return results.

Still opening the Issue for discussion on this topic.

filter() doesn't work

running this

wp.posts().filter( 'posts_per_page', 5 ).get(function( err, results ) {
        if ( err ) {
            // handle err
        }
            // do something with the returned posts
                locals.data.posts = results;
                console.log(results);
                next();
            });

the result are always all the published post, with any kind of filter even

wp.posts().filter({post_status: 'draft',category_name: 'news'}).get(function( err, results ) {
        if ( err ) {
            // handle err
        }
            // do something with the returned posts
                locals.data.posts = results;
                console.log(results);
                next();
            })

Am I doing something wrong?

Auth fails?

Auth seems to fail, even when explicityly setting it in the call:

wp.users().auth("USER","PASS").me(); 

gives an empty object, while calling wp-json/users/me with basic auth with curl returns my user info.

Add convenience method .posts().tags for setting tags filters

See the Tag Parameter filter reference from WP_Query documentation:

The following query filter parameters should be accepted by the filter method, but it would be ideal to make a wrapper that will intelligently maintain an internal array of tags to apply to the query:

  • tag (string) - use tag slug.
  • tag_id (int) - use tag id.
  • tag__and (array) - use tag ids.
  • tag__in (array) - use tag ids.
  • tag__not_in (array) - use tag ids.
  • tag_slug__and (array) - use tag slugs.
  • tag_slug__in (array) - use tag slugs.

As an example of intended usage, these should all be valid syntax:

wp.posts().filter({ tag: 'current-events' })...
wp.posts().tag( 'current-events' )...
wp.posts().tag( 'current-events' ).tag( 'boston' )...
wp.posts().tag([ 'current-events', 'boston' ])...

Don't mock 3rd-party request library in tests

It was suggested in code review that using nock or an alternative to mock the server responses would be a more robust approach to testing the library, vs the current approach (in which our tests depend on the specific API provided by superagent).

Use "var" instead of "const" to permit opting-in to strict mode

From a code review session, @jugglinmike points out

"the const keyword for your modules prevents code from opting into Strict mode."

I agree with his assessment that the benefits of Strict outweigh the benefits of const; we should change all usages of const back to var and add strict-mode enforcement to the .jshintrc

Expose Pagination to client consumers

Many responses from the API will be paginated, either explicitly or by default. For example, if post collections return 10 items by default, and you have 100 items in your DB, both of these will come back with a header link with rel="next":

wp.posts()...
wp.posts().filter('posts_per_page', 2)...

The general approach I have taken with this library is to let the user specify a request, and return what they asked for. At the moment the way this manifests is that if you request wp.posts()..., you get the posts collection (that is, the response body) back in the request callback: the headers are not included. I favored this over returning the full object, because this felt like a very clunky thing to have to do every time:

wp.posts().then(function(resp) {
  return resp.body;
}).then(function(posts) {
  // do something with the posts collection
});

when in many cases it's only the response body that you care about. The one exception is paginated collections, as it is the response header that includes the pagination information (via the link header and two custom header properties).

I want to provide an intuitive way to expose collection paging to users, preferably without making it substantially more verbose to request a specific resource. I am not sure how to proceed.

Option 1: Return the raw request object

This is what is outlined above, where the request object is passed back in full and you can manually grab the body or check the link header as you need to. This feels like a bad solution because it makes the default usage (get a specific resource) more difficult by requiring a pipe through a then, and it doesn't give you any additional functionality (you need to manually parse headers to handle paging).

Option 2: Augment the response data with paging properties

The idea is to augment the response object with properties or methods relating to pagination in cases when the resulting data is determined to be paginated. There were various ways it could be implemented:

Example 1, just expose the links and pagination headers:

wp.posts().then(function(posts) {
  // How many posts did I get back?
  console.log( posts.length ); // normal array

  // posts also has a .paging property, on which is set custom
  // pagination-related information

  // How many posts, total...?
  console.log( posts.paging.total );
  // Next collection URL?
  console.log( posts.paging.next );
  // Previous?
  console.log( posts.paging.prev );
  // total number of pages?
  console.log( posts.paging.totalPages );
});

Example 2, give the returned collection methods that would function as fully-fledged WPRequest objects

wp.posts().then(function(posts) {
  // Make a request for the next page of posts
  posts.next().then(function(nextPagePosts) {
    // and so on
    posts.next().then(function(thirdPagePosts) {
      // and (halcyon and) on and on
    })
  });
});

Things I like about this approach:

  • Doesn't expose the headers unless you asked for them directly (with wp.posts.head()), which keeps the data returned from the service directly accessible to clients without transformation
  • Fits the existing paradigm we've been discussing in #5, where collections would get augmented with Lodash operators (like Backbone collections/LayoutManager child view collections)

That said, when I pitched this to a collaborator he described it as "yucky" and I couldn't think of any other examples of systems that work this way. I want to present it for review anyway, though, because it was my first thought.

Option 2A: Let users define custom data augmentation methods

This is an extension of one way the above could be implemented. If you imagine a prototype method on CollectionRequest called transform, which could be used to register a methods that would be run on the request response before being returned (which would replace the existing returnBody and returnHeaders methods), you could let users opt-in to things like lodash collection methods, pagination handling, or their own custom transformations:

wp.posts().transform(wp.transforms.paginate).then(function(posts) {
  // posts has some default pagination methods/props
});
wp.posts().transform(wp.transforms.collection).then(function(posts) {
  // Now it's got Underscore methods, and this would work:
  posts.pluck('titles')...
});
wp.posts().transform(myPluckTitles).then(function(posts) {
  // posts is now an array of titles
});
wp.posts().transform(myGetPagingLinks).then(function(links) {
  // now it's a bare object of links -- or whatever you want it to be
});
// Could also be exposed directly, through a custom chaining method
wp.posts().paginated().get(/* ... */);

Upshots:

  • Much more flexible
  • Provides a consistent API for handling pagination, and for other types of transformation like augmenting collections with lodash collection methods

Downsides:

  • Would probably require making some sort of wp.transforms or wp.utils namespace to hold the default transformation methods
  • More chaining methods is not/should not be the answer to everything
  • Introduces unintuitive behavior vs having a consistent default

Option 3: Return a custom object

This is in some ways an extension of 1, just with some of the things I noted addressed. Basically, all requests come back as an object with four properties:

  • headers: the raw, unprocessed HTTP headers
  • body: the raw, unprocessed response body
  • data: the data returned by the request (which is the body, but the important part is that data feels more intuitive and accurate vs what the information actually is: I shouldn't care that it's the body, I care that it's the data returned from the server.)
  • paging/links: custom properties designating previous/next collections, etc

The advantage of this over the first option is that this, to me, feels more intuitive than returning resp.body:

wp.posts().then(function(resp) {
  // collection:
  console.log( resp.data );
  // pagination URL:
  console.log( resp.links.next );
});

The disadvantage is that I'm right back where I started in terms of having to pipe responses through yet another then in order to boil them down to the actual response body, if that's what I wanted originally.


I am probably overlooking additional options, here. These are the actual pagination-related header properties exposed by the API endpoint, for an example collection of two posts:

link: '</wp-json/posts?filter%5Bposts_per_page%5D=2&page=1>; rel="prev",
    </wp-json/posts?filter%5Bposts_per_page%5D=2&page=3>; rel="next",
    <http://my.site.com/wp-json/posts/214>; rel="item"; title="Article Title",
    <http://my.site.com/wp-json/posts/211>; rel="item"; title="Article Title"',
'x-wp-total': '17',
'x-wp-totalpages': '9',

It's possible the WordPress API itself could be extended to provide more headers or more links such as a first or last page, as it's a work-in-progress, but for the time being I'm trying to figure out if there's any other approach than the ones outlined above to make answering these questions (and others like them) easy:

Questions a solution should answer

  1. Are there more pages in this collection?
  2. What page am I on?
  3. How do I get to the previous/next pages?

Any recommendations for good API clients that handle this sort of behavior are welcome, as is commentary or dissension around the above options I have outlined.

HTTP requests should be triggerable by HTTP verb methods or by promise syntax

For maximum flexibility and convenience, both callback and promise-style syntax should be valid: This is the rationale for pulling in a Promise lib as per #2

// Callback-style
wp.posts().filter({ posts_per_page: 7 }).get(function(err, data) {
  if ( err ) {
    // handle error
  } else {
    // do something with data
  }
});

// Promise-style
wp.posts().filter({ posts_per_page: 7 }).then(function(data) {
  // do something with data
}, function(err) {
  // handle error
});

Additionally, if .then is called on a query chain, it assumes the GET method unless otherwise specified:

wp.posts().id(7).data(postObj).put().then(function(response) { /* ... */ });

Filters should DO something

Calling .filter currently has no effect on the generated URL. That's less than ideal.

TODO:

  • Build a whitelist of accepted filter values, so we can fail out faster if an invalid value is provided
  • Build a list of filter values that should get treated as arrays in the resulting API request URI
  • Build a helper to create a query string to append to the generated URIs

The latter could potentially be accomplished by having generateRequestUri call back to the inherited WPRequest's own generateRequestUri, which could in turn process the _filters object. I'm inclined to pursue this because it keeps 100% of the filtering logic within WPRequest, without requiring any special treatment on the part of the inheriting request modules.

You should have a better syntax for querying custom post types

Idea: our WP install has a custom post type (CPT) with name cpt_event_listing: we should be able to register a convenience method to instantiate post queries

var wp = new WP( options );

wp.eventListings = wp.registerPostType( 'cpt_event_listing' );

wp.eventListings().get().then(/* ... */);

Having registerPostType also mutate the parent wp object automatically was also considered; coop recommendation was to make the interface a little more declarative. Some alternatives to that syntax:

// Automatically make a method for any registered CPT name:
// Should be safe b/c of similarity b/w valid CPT names and valid JS identifiers
wp.registerType('cpt_event_listing');
wp.cpt_event_listing().then(/* ... */);

// Don't make a convenience method at all and just always pass a CPT
// identifier to a wp.types function:
wp.types( 'cpt_event_listing' )...
wp.types( 'other_cpt_name' ).get()....

Queries should be scoped to a particular client object method call

It should be possible to run wp.posts() and wp.taxonomies() without the latter changing any properties or state of the former request. Top-level methods on the wp object should create and return a new query object that can be chained, rather than setting private properties on the wp instance itself.

Methods that will create query objects:

  • wp.posts
  • wp.pages
  • wp.users
  • wp.taxonomies
  • wp.categories (alias)
  • wp.tags (alias)
  • wp.types (?)

OAuth support

Hi!

What's involved with making oauth work (either oauth1, for which there is a WP API plugin plugin, or OAuth 2 for which there is a standalone plugin) with this plugin instead of standard HTTP authentication?

Kevin N.

Support Auto-Discovery from top-level API root endpoint response

One major point of discussion at WCNYC surrounding the API is how to handle discoverability, as it was recommended to consider templated URLs (which are common with SAAS-provided APIs) to be an anti-pattern when the WP ReST API can be considered to be more of an API builder for individual sites than an API in and of itself. In a polymorphic world of individually-configured and customized WordPress installs, having non-templated paths that are all discovered from a root endpoint is the solution deemed most flexible and least likely to paint us into a corner.

Significant drawbacks to this approach:

  • A first request is always needed: There is no way to query for a resource without first interrogating the root endpoint for location information. You can safely assume you know how things are set up in most cases, but there's no way around this added request overhead if you're requesting content from a third-party site for the first time.
  • To avoid having to make a first request for all subsequent requests, the information from that request (an object defining available endpoints, from which all following requests will be derived) must be cached somehow. This is of minor concern in server-side applications, which can cache in-memory within the process, in session storage, or persistently in a database; but for client applications this data would need to be persisted in local storage or a similar client-side cache in order to avoid needing to be requested on every page load.

I find this concept appealing from the standpoint of conceptual flexibility, and in the way it forces you to confront resources very specifically as members of collections, but I still can't but think it would be extremely frustrating and limiting in implementation. I want to study more APIs and figure out if there's anything out there operating in the fashion described by @maxcutler, to evaluate the best way to work around these limitations and alleviate my concerns.

Make the WP constructor return a WP client instance

Currently, the wp function is designed to be called each time a request is made. To enable passing the same configured WP client instance around between functions, we should make WP return a configured client object that can be used to instantiate new requests.

Before:

wp( options ).posts()...

Proposal:

var wp = new WP( options );
wp.posts()...

Deprecate "/posts/path" route

Conversation at WCNYC indicates the /posts/path route is not long for this world; filtering on post name is preferred. i.e.,

/pages?filter[pagename]=some/path

will replace

/wp-json/pages/path/some/path

This should work today, so the .path handler on the page collection should just be re-purposed to set .filter({ pagename: pathArg })

Superagent adds duplicative array syntax

superagent is transforming the (already-formatted) type[]=type1&type[]=type2 portion of the URL into type[][0]=type1&type[][1]=type2. This results in the following error:

http://[test site url]/wp-json/posts?filter%5Bcity%5D=cityname&type%5B%5D=event&type%5B%5D=evttype
{ [Error: cannot GET /wp-json/posts?filter%5Bcity%5D=cityname&type%5B%5D[0]=event&type%5B%5D[1]=evttype (403)]
  status: 403,
  method: 'GET',
  path: '/wp-json/posts?filter%5Bcity%5D=cityname&type%5B%5D[0]=event&type%5B%5D[1]=evttype' }

This can be avoided by removing the [] around the type property, since superagent evidently inserts the array syntax and indices itself; effectively, the resolution here will be to eliminate the prepareParameters method from inside the wp-request module, as this behavior of superagent obviates our need to write the syntax ourselves.

I speculated in #84 (whence this issue) that we should consider tossing Superagent in favor of a request library with unexpected behavior; upon reflection, Superagent is doing an OK job and we'd be making undue extra work were we to switch. This bug will be fixed in isolation.

Document authentication

We have hypothetical support for basic authentication, but it hasn't been tested. We should validate that it works, and figure out what would be needed to support OAuth.

Add code coverage reporting

@tbranyen has indicated that setting up Istanbul may be difficult with simplemocha; I'm willing to give it a try though, as code coverage reporting would be, to borrow a phrase, "rad."

Improve the way in which query URLs are constructed

At present we're concatenating a collection of instance properties like _id, _action, and _actionId into the paths for API requests. This seems brittle, as it both clutters the instance properties and can't really systematically enforce the order in which they're assembled.

I've been looking at @cowboy's route-matcher, and I think this may be a more robust way of treating with these paths. Instead of having the method responsible for generating the PostsRequest's URI hard-code 'posts' as the first level of the path, we could specify posts to have a route template like 'posts/', then swap that out with 'posts/:id/:action/:actionId' when a method like posts().id( n ) is called. This way we can maintain two properties, a ._routeTemplate string and a _path object, and pass that object into the routeMatcher template to construct the main URL.

Note that this has no bearing on how the query parameters used for filtering will be handled; it just proposes a more rigorous structure for constructing the Path part of the URLs.

Support mixing into or processing response data?

It's been suggested that we mix in Underscore/Lodash functions to collection responses (#5), and it may be beneficial to provide a way to process response data objects into more structured models. (This library does not actually provide models: we should not presume how users want to structure those data models, but it might be convenient to give them the option to do so.)

This issue is for brainstorming an interface for providing these models. It may not be feasible, given the variety of data types that you can retrieve even from a single endpoint (e.g. /posts and its descendants yields Posts, Post Collections, Comments, Revisions, and Types).

WP api version

It's not mentioned in the readme: is this driver for the v1 or v2 edition of the WP API plugin?

Support Custom Post Types

Before this is released, we need to have support for custom post types.

Alternative syntaxes under consideration:

Generic types handled by a single types endpoint handler:

var wp = new WP( options );

wp.types( 'cpt_event_listing' ).then(/* ... */);

Create custom endpoint handlers with a registerType utility method (see #7):

var wp = new WP( options );
wp.eventListings = wp.registerPostType( 'cpt_event_listing' );

wp.eventListings().get().then(/* ... */);

In either case, the type method would wrap .posts() but would set properties to specify a type query parameter to filter results.

@carldanley, I'm particularly interested in your opinion here since you're familiar with the WP internals

Find a utility library for constructing URIs

String concatenation works OK for simple endpoint route requests, but when we start getting into filters (see #18) the query structure's going to get significantly more complex. I would like to pull in a 3rd-party utility for constructing URIs with query parameters, etc; I've been looking at URI.js (link goes to the npm package), but I'm open to alternative suggestions.

Flesh out TaxonomiesRequest

Presently the only route that is anything close to fully-formed is the PostsRequest endpoint handler. Once #30 has been resolved, we should add support for getting lists of taxonomies and their terms through the /taxonomies API endpoint.

Add convenience method .posts().category for setting category filters

See the Category filter reference from WP_Query documentation:

The following query filter parameters should be accepted by the filter method, but it would be ideal to make a wrapper that will intelligently maintain an internal array of category IDs:

  • cat (int) - use category id.
  • category_name (string) - use category slug (NOT name).
  • category__and (array) - use category id.
  • category__in (array) - use category id.
  • category__not_in (array) - use category id.

As an example of usage,

// filters posts by those in categories 1, 2 & 3:
wp.posts().category( 1 ).category([ 2, 3]).get();

// specify a category by slug:
wp.posts().category( 'news' );

Support Node.js 0.8

The tests bomb out in travis in 0.8. I need to try this in Node.js 0.8 to see if I can figure out why.

Add .meta handler for posts

The /posts endpoint provides two routes for interacting with post meta values:

  • /posts/<id>/meta
  • /posts/<id>/meta/<mid>

Both of these depend on authentication, so that should be taken care of (and documented, see #33) before we implement this.

Add method to bind new endpoint handlers

Per @rmccue, providers of the WP-API can expose new query endpoints and we should acocunt for those.

[6/30/14, 3:01:29 AM] Ryan McCue:
So e.g. for WooCommerce, implementing /products as a shorthand for /posts?type=products

The option we discussed was to add a .customEndpoint method, in the manner of .registerType (see #7); however, the other option would be to make a function on WP that could be used to define a new constructor and specify the types at instantiation. Worth discussing. cc @carldanley

Add .taxonomy('tax_name') shortcut for .taxonomies().taxonomy('tax_name')

The .taxonomies().taxonomy( 'taxonomy_name' ) pattern is a bit repetitive.

To avoid having to type "taxonomy" twice whenever querying for a custom taxonomy or custom taxonomy terms, we should add a .taxonomy( 'tax_name' ) shortcut method that will return a TaxonomiesRequest object pre-bound to the taxonomy with "tax_name".

Multiple categories?

Is there a way to fetch an article with multiple categories, such as article has category "foo" and "bar" ?

Setup a testing suite

I'd like to get some unit testing in place so we can change the API around as needed but still run tests against things to make sure we're functioning as intended.

Make core lib environment agnostic to support use in the browser

It seems like a great deal of the value of this module would be useful in the browser as well, it wouldn't be too difficult to delegate all node specific code out into it's own location so that it can be swapped out for browser equivalents.

Is this something you all are potentially interested in? I could do this work.

Double callback when using multiple custom post types?

Hi!
If I do this
wp.posts().type(['cityreport','place']).get(function (err, data) {
I get a console error "double callback", and then a "syntax error". Also my error callback gets called.
The URL called does include the right data, but parsing it fails? It is valid JSON.
Or am I doing something wrong?

Unknown method posts

I have installed the basic-auth and json-api plugins on wordpress.

This is my app.js file

wp.posts().then(function( posts ) {
   console.log( posts );
})

which returns this error:
{ status: 'error', error: 'Uknown method \'posts\'.' }

But using Postman, I am getting the desired output of an array of posts.
So my question is which url is this npm module hitting ?

<api-path>/get_posts/ -> gives correct output

or

<api-path>/posts/ -> gives error.

Thanks in advance.

Library is now 0.3.0 RC1

@carldanley I'm considering punting .media() to the next release in order to get this online and announced this week; whether or not we make that call, this whole thing is ready for code review.

Get taxonomy by slug

In the docs (frontpage) it says:

wp.taxonomies().taxonomy( 'taxonomy_name' ).term( termIdentifier ): get the term with slug or ID termIdentifier from the taxonomy taxonomy_name

This works with the ID, but not with the slug :(. Is this something that changed?

HOST/wp-json/taxonomies/post_tag/terms/42 works
HOST/wp-json/taxonomies/post_tag/terms/sociaal-ondernemerschap doesn't

Create Vagrant VM and document testing steps

We should create (probably as a separate repository) a Vagrant box that can be provisioned with the WP-API plugin and some dummy content, so that we can provide easy testing and development steps for potential library users. It's better to say "want to try this out? Download and install this thing, then run this to see it work, then customize it to see what else it can do" than to leave people to their own devices; Node is still relatively unbroken ground for the WP audience, the more we can do to make this easy for them to get started, the more they'll build with it.

I'd made a previous effort in this direction with my wpapi-vagrant-varietal repo, but it's a bit heavy and not tailored to be a foundation for learning about the client lib. @carldanley said he was taking a stab at putting together something better!

Add integration test to ensure URI's are requested as intended

(For background see #89)

Superagent automatically converts multiple occurrences of the same query parameter key into array syntax: param=val&param=val becomes param[0]=val&param[1]=val. Because this behavior was not intuitive (other request libraries use the provided URL exactly as-is), we should add a test to verify that this behavior still occurs in case it changes in future versions of superagent.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.