Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 56 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@ A WordPress REST API client for JavaScript

This is a client for the [WordPress REST API](http://wp-api.org/). It is **under active development**, and should be considered beta software. More features are in progress, and **[issues](https://github.com/kadamwhite/wordpress-rest-api/issues)** are welcome if you find something that doesn't work!

**`wordpress-rest-api` is designed to work with [WP-API](https://github.com/WP-API/WP-API) v2 beta 11 or higher.** If you use a prior version of the beta, some commands will not work.
**`wordpress-rest-api` is designed to work with [WP-API](https://github.com/WP-API/WP-API) v2 beta 11 or higher.** If you use a prior version of the beta, some commands will not work. The latest beta is always recommended!

[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kadamwhite/wordpress-rest-api?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

[![Build Status](https://api.travis-ci.org/kadamwhite/wordpress-rest-api.png?branch=master)](https://travis-ci.org/kadamwhite/wordpress-rest-api)

**Index**:
[Purpose](#purpose) •
[Installation](#installation) •
[Using The Client](#using-the-client) •
[Authentication](#authentication) •
[API Documentation](#api-documentation) •
[Issues](#issues) •
[Contributing](#contributing)

- [Purpose](#purpose)
- [Installation](#installation)
- [Using The Client](#using-the-client)
- [Creating Posts](#creating-posts)
- [Updating Posts](#updating-posts)
- [Requesting Different Resources](#requesting-different-resources)
- [Filtering Collections](#filtering-collections)
- [Custom Routes](#custom-routes)
- [Embedding Data](#embedding-data)
- [Paginated Collections](#working-with-paged-response-data)
- [Authentication](#authentication)
- [API Documentation](#api-documentation)
- [Issues](#issues)
- [Contributing](#contributing)

## Purpose

Expand Down Expand Up @@ -223,9 +231,47 @@ The following methods are shortcuts for filtering the requested collection down
* `.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

### Custom Post Types
### 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`](http://v2.wp-api.org/extending/adding/#bare-basics):

```js
var site = new WP({ 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](http://www.regular-expressions.info/tutorial.html) designed to validate the values that may be used for this capture group.
```js
var site = new WP({ 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.

To permit custom parameter support methods on custom endpoints, a configuration object may be passed to the `registerRoute` method with a `mixins` property defining any functions to add:

```js
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.

Support for Custom Post Types has been removed temporarily, but will be reinstated soon once the client supports the new custom post handling changes introduced in the new v2 API betas.
Auto-discovery of all available routes will be supported in the near future, as will re-utilizing existing mixins (like `.search()`) on custom routes.

## Embedding data

Expand Down
5 changes: 5 additions & 0 deletions lib/endpoint-factories.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ function generateEndpointFactories( namespace, routeDefinitions ) {
return new EndpointRequest( options );
};

// Expose the constructor as a property on the factory function, so that
// auto-generated endpoint request constructors may be further customized
// when needed
handlers[ resource ].Ctor = EndpointRequest;

return handlers;
}, {} );
}
Expand Down
35 changes: 21 additions & 14 deletions lib/endpoint-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,29 @@ function createEndpointRequest( handlerSpec, resource, namespace ) {

// Mix in all available shortcut methods for GET request query parameters that
// are valid within this endpoint tree
Object.keys( handlerSpec._getArgs ).forEach(function( supportedQueryParam ) {
var mixinsForParam = mixins[ supportedQueryParam ];

// Only proceed if there is a mixin available AND the specified mixins will
// not overwrite any previously-set prototype method
if ( mixinsForParam ) {
Object.keys( mixinsForParam ).forEach(function( methodName ) {
if ( ! EndpointRequest.prototype[ methodName ] ) {
EndpointRequest.prototype[ methodName ] = mixinsForParam[ methodName ];
}
});
}
});
if ( typeof handlerSpec._getArgs === 'object' ) {
Object.keys( handlerSpec._getArgs ).forEach(function( supportedQueryParam ) {
var mixinsForParam = mixins[ supportedQueryParam ];

// Only proceed if there is a mixin available AND the specified mixins will
// not overwrite any previously-set prototype method
if ( mixinsForParam ) {
Object.keys( mixinsForParam ).forEach(function( methodName ) {
if ( ! EndpointRequest.prototype[ methodName ] ) {
EndpointRequest.prototype[ methodName ] = mixinsForParam[ methodName ];
}
});
}
});
}

Object.keys( handlerSpec._setters ).forEach(function( setterFnName ) {
EndpointRequest.prototype[ setterFnName ] = handlerSpec._setters[ setterFnName ];
if ( EndpointRequest.prototype[ setterFnName ] ) {
console.warn( 'Warning: method .' + setterFnName + '() is already defined!' );
console.warn( 'Cannot overwrite .' + resource + '().' + setterFnName + '() method' );
} else {
EndpointRequest.prototype[ setterFnName ] = handlerSpec._setters[ setterFnName ];
}
});

return EndpointRequest;
Expand Down
6 changes: 4 additions & 2 deletions lib/path-part-setter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function createPathPartSetter( node ) {
// Local references to `node` properties used by returned functions
var nodeLevel = node.level;
var nodeName = node.names[ 0 ];
var supportedMethods = node.methods;
var supportedMethods = node.methods || [];
var dynamicChildren = node.children ? Object.keys( node.children )
.map(function( key ) {
return node.children[ key ];
Expand All @@ -42,7 +42,9 @@ function createPathPartSetter( node ) {
return function( val ) {
/* jshint validthis:true */
this.setPathPart( nodeLevel, val );
this._supportedMethods = supportedMethods;
if ( supportedMethods.length ) {
this._supportedMethods = supportedMethods;
}
return this;
};
} else {
Expand Down
7 changes: 2 additions & 5 deletions lib/resource-handler-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function extractSetterFromNode( handler, node ) {
function createNodeHandlerSpec( routeDefinition, resource ) {

var handler = {
// A "path" is an ordered set of
// A "path" is an ordered (by key) set of values composed into the final URL
_path: {
'0': resource
},
Expand Down Expand Up @@ -109,8 +109,5 @@ function createNodeHandlerSpec( routeDefinition, resource ) {
}

module.exports = {
create: createNodeHandlerSpec,
_extractSetterFromNode: extractSetterFromNode,
_assignSetterFnForNode: assignSetterFnForNode,
_addLevelOption: addLevelOption
create: createNodeHandlerSpec
};
15 changes: 12 additions & 3 deletions lib/route-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
// pattern as the unique identifier: this is done because the same group
// could be assigned different names in different endpoint handlers, e.g.
// "id" for posts/:id vs "parent_id" for posts/:parent_id/revisions.
var levelKey = namedGroup ? groupPattern : component;
//
// There is an edge case where groupPattern will be "" if we are registering
// a custom route via `.registerRoute` that does not include parameter
// validation. In this case we assume the groupName is sufficiently unique,
// and fall back to `|| groupName` for the levelKey string.
var levelKey = namedGroup ? groupPattern : component || groupName;

// Level name, on the other hand, would take its value from the group's name
var levelName = namedGroup ? groupName : component;
Expand All @@ -66,7 +71,11 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
// on the request URL is of the proper type for the location in which it
// is specified. If a group pattern was found, the validator checks whether
// the input string exactly matches the group pattern.
var groupPatternRE = new RegExp( groupPattern ? '^' + groupPattern + '$' : component );
var groupPatternRE = groupPattern === '' ?
// If groupPattern is an empty string, accept any input without validation
/.*/ :
// Otherwise, validate against the group pattern or the component string
new RegExp( groupPattern ? '^' + groupPattern + '$' : component, 'i' );

// Only one validate function is maintained for each node, because each node
// is defined either by a string literal or by a specific regular expression.
Expand All @@ -85,7 +94,7 @@ function reduceRouteComponents( routeObj, topLevel, parentLevel, component, idx,
}) : [];
// Ensure HEAD is included whenever GET is supported: the API automatically
// adds support for HEAD if you have GET
if ( currentLevel.methods.indexOf( 'get' ) > -1 ) {
if ( currentLevel.methods.indexOf( 'get' ) > -1 && currentLevel.methods.indexOf( 'head' ) === -1 ) {
currentLevel.methods.push( 'head' );
}

Expand Down
2 changes: 1 addition & 1 deletion lib/util/named-group-regexp.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = new RegExp([
'[>\']',
// Get everything up to the end of the capture group: this is the RegExp used
// when matching URLs to this route, which we can use for validation purposes.
'([^\\)]+)',
'([^\\)]*)',
// Capture group end
'\\)'
].join( '' ) );
87 changes: 87 additions & 0 deletions lib/wp-register-route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

var extend = require( 'node.extend' );

var buildRouteTree = require( './route-tree' ).build;
var generateEndpointFactories = require( './endpoint-factories' ).generate;

/**
* Create and return a handler for an arbitrary WP REST API endpoint.
*
* The first two parameters mirror `register_rest_route` in the REST API
* codebase:
*
* @class wp
* @method registerRoute
* @param {string} namespace A namespace string, e.g. 'myplugin/v1'
* @param {string} restBase A REST route string, e.g. '/author/(?P<id>\d+)'
* @param {object} [options] An (optional) options object
* @param {object} [options.mixins] A hash of functions to apply as mixins
* @param {string[]} [options.methods] An array of methods to whitelist (on the leaf node only)
* @returns {Function} An endpoint handler factory function for the
* specified route
*/
function registerRoute( namespace, restBase, options ) {
// Support all methods until requested to do otherwise
var supportedMethods = [ 'head', 'get', 'patch', 'put', 'post', 'delete' ];

if ( options && Array.isArray( options.methods ) ) {
// Permit supported methods to be specified as an array
supportedMethods = options.methods.map(function( method ) {
return method.trim().toLowerCase();
});
} else if ( options && typeof options.methods === 'string' ) {
// Permit a supported method to be specified as a string
supportedMethods = [ options.methods.trim().toLowerCase() ];
}

// Ensure that if GET is supported, then HEAD is as well, and vice-versa
if ( supportedMethods.indexOf( 'get' ) !== -1 && supportedMethods.indexOf( 'head' ) === -1 ) {
supportedMethods.push( 'head' );
} else if ( supportedMethods.indexOf( 'head' ) !== -1 && supportedMethods.indexOf( 'get' ) === -1 ) {
supportedMethods.push( 'get' );
}

var fullRoute = namespace
// Route should always have preceding slash
.replace( /^[\s/]*/, '/' )
// Route should always be joined to namespace with a single slash
.replace( /[\s/]*$/, '/' ) + restBase.replace( /^[\s/]*/, '' );

var routeObj = {};
routeObj[ fullRoute ] = {
namespace: namespace,
methods: supportedMethods
};

// Go through the same steps used to bootstrap the client to parse the
// provided route out into a handler request method
var routeTree = buildRouteTree( routeObj );
var endpointFactories = generateEndpointFactories( namespace, routeTree[ namespace ] );
var EndpointRequest = endpointFactories[ Object.keys( endpointFactories )[ 0 ] ].Ctor;

if ( options && typeof options.mixins === 'object' ) {

// Set any specified mixin functions on the response
Object.keys( options.mixins ).forEach(function( key ) {
var mixin = options.mixins[ key ];

// Will not overwrite existing methods
if ( typeof mixin === 'function' && ! EndpointRequest.prototype[ key ] ) {
EndpointRequest.prototype[ key ] = options.mixins[ key ];
}
});
}

function endpointFactory( options ) {
/* jshint validthis:true */
options = options || {};
options = extend( options, this && this._options );
return new EndpointRequest( options );
}
endpointFactory.Ctor = EndpointRequest;

return endpointFactory;
}

module.exports = registerRoute;
1 change: 1 addition & 0 deletions tests/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

"globals": {
"beforeEach": false,
"afterEach": false,
"describe": false,
"it": false
}
Expand Down
13 changes: 12 additions & 1 deletion tests/integration/posts.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ var SUCCESS = 'success';
chai.use( require( 'chai-as-promised' ) );
var expect = chai.expect;

/*jshint -W079 */// Suppress warning about redefiniton of `Promise`
var Promise = require( 'bluebird' );

var WP = require( '../../' );
var WPRequest = require( '../../lib/constructors/wp-request.js' );

Expand Down Expand Up @@ -361,7 +364,15 @@ describe( 'integration: posts()', function() {
expect( post.title ).to.have.property( 'rendered' );
expect( post.title.rendered ).to.equal( 'Updated Title' );
// Re-authenticate & delete (trash) this post
return wp.posts().auth( credentials ).id( id ).delete();
// Use a callback to exercise that part of the functionality
return new Promise(function( resolve, reject ) {
wp.posts().auth( credentials ).id( id ).delete(function( err, data ) {
if ( err ) {
return reject( err );
}
resolve( data );
});
});
}).then(function( response ) {
expect( response ).to.be.an( 'object' );
// DELETE action returns the post object
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/lib/util/named-group-regexp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
var expect = require( 'chai' ).expect;

var namedGroupRE = require( '../../../../lib/util/named-group-regexp' );

describe( 'named PCRE group RegExp', function() {

it( 'is a regular expression', function() {
expect( namedGroupRE ).to.be.an.instanceof( RegExp );
});

it( 'will not match an arbitrary string', function() {
var pathComponent = 'author';
var result = pathComponent.match( namedGroupRE );
expect( result ).to.be.null;
});

it( 'identifies the name and RE pattern for a PCRE named group', function() {
var pathComponent = '(?P<parent>[\\d]+)';
var result = pathComponent.match( namedGroupRE );
expect( result ).not.to.be.null;
expect( result[ 1 ] ).to.equal( 'parent' );
expect( result[ 2 ] ).to.equal( '[\\d]+' );
});

it( 'identifies the name and RE pattern for another group', function() {
var pathComponent = '(?P<id>\\d+)';
var result = pathComponent.match( namedGroupRE );
expect( result ).not.to.be.null;
expect( result[ 1 ] ).to.equal( 'id' );
expect( result[ 2 ] ).to.equal( '\\d+' );
});

it( 'will match an empty string if a "RE Pattern" if the pattern is omitted', function() {
var pathComponent = '(?P<id>)';
var result = pathComponent.match( namedGroupRE );
expect( result ).not.to.be.null;
expect( result[ 1 ] ).to.equal( 'id' );
expect( result[ 2 ] ).to.equal( '' );
});

});
Loading