/**
* A WP REST API client for Node.js
*
* @example
* var wp = new WPAPI({ endpoint: 'http://src.wordpress-develop.dev/wp-json' });
* wp.posts().then(function( posts ) {
* console.log( posts );
* }).catch(function( err ) {
* console.error( err );
* });
*
* @license MIT
})
*/
'use strict';
var extend = require( 'node.extend' );
var objectReduce = require( './lib/util/object-reduce' );
// This JSON file provides enough data to create handler methods for all valid
// API routes in WordPress 4.7
var defaultRoutes = require( './lib/data/default-routes.json' );
var buildRouteTree = require( './lib/route-tree' ).build;
var generateEndpointFactories = require( './lib/endpoint-factories' ).generate;
// The default endpoint factories will be lazy-loaded by parsing the default
// route tree data if a default-mode WPAPI instance is created (i.e. one that
// is to be bootstrapped with the handlers for all of the built-in routes)
var defaultEndpointFactories;
// Constant used to detect first-party WordPress REST API routes
var apiDefaultNamespace = 'wp/v2';
// Pull in autodiscovery methods
var autodiscovery = require( './lib/autodiscovery' );
// Pull in base module constructors
var WPRequest = require( './lib/constructors/wp-request' );
// Pull in default HTTP transport
var httpTransport = require( './lib/http-transport' );
/**
* Construct a REST API client instance object to create
*
* @constructor WPAPI
* @param {Object} options An options hash to configure the instance
* @param {String} options.endpoint The URI for a WP-API endpoint
* @param {String} [options.username] A WP-API Basic Auth username
* @param {String} [options.password] A WP-API Basic Auth password
* @param {String} [options.nonce] A WP nonce for use with cookie authentication
* @param {Object} [options.routes] A dictionary of API routes with which to
* bootstrap the WPAPI instance: the instance will
* be initialized with default routes only
* if this property is omitted
* @param {String} [options.transport] An optional dictionary of HTTP transport
* methods (.get, .post, .put, .delete, .head)
* to use instead of the defaults, e.g. to use
* a different HTTP library than superagent
*/
function WPAPI( options ) {
// Enforce `new`
if ( this instanceof WPAPI === false ) {
return new WPAPI( options );
}
if ( typeof options.endpoint !== 'string' ) {
throw new Error( 'options hash must contain an API endpoint URL string' );
}
// Dictionary to be filled by handlers for default namespaces
this._ns = {};
this._options = {
// Ensure trailing slash on endpoint URI
endpoint: options.endpoint.replace( /\/?$/, '/' )
};
// If any authentication credentials were provided, assign them now
if ( options && ( options.username || options.password || options.nonce ) ) {
this.auth( options );
}
return this
// Configure custom HTTP transport methods, if provided
.transport( options.transport )
// Bootstrap with a specific routes object, if provided
.bootstrap( options && options.routes );
}
/**
* Set custom transport methods to use when making HTTP requests against the API
*
* Pass an object with a function for one or many of "get", "post", "put",
* "delete" and "head" and that function will be called when making that type
* of request. The provided transport functions should take a WPRequest handler
* instance (_e.g._ the result of a `wp.posts()...` chain or any other chaining
* request handler) as their first argument; a `data` object as their second
* argument (for POST, PUT and DELETE requests); and an optional callback as
* their final argument. Transport methods should invoke the callback with the
* response data (or error, as appropriate), and should also return a Promise.
*
* @example <caption>showing how a cache hit (keyed by URI) could short-circuit a get request</caption>
*
* var site = new WPAPI({
* endpoint: 'http://my-site.com/wp-json'
* });
*
* // Overwrite the GET behavior to inject a caching layer
* site.transport({
* get: function( wpreq, cb ) {
* var result = cache[ wpreq ];
* // If a cache hit is found, return it via the same callback/promise
* // signature as the default transport method
* if ( result ) {
* if ( cb && typeof cb === 'function' ) {
* cb( null, result );
* }
* 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;
* });
* }
* });
*
* This is advanced behavior; you will only need to utilize this functionality
* if your application has very specific HTTP handling or caching requirements.
* Refer to the "http-transport" module within this application for the code
* implementing the built-in transport methods.
*
* @memberof! WPAPI
* @method transport
* @chainable
* @param {Object} transport A dictionary of HTTP transport methods
* @param {Function} [transport.get] The function to use for GET requests
* @param {Function} [transport.post] The function to use for POST requests
* @param {Function} [transport.put] The function to use for PUT requests
* @param {Function} [transport.delete] The function to use for DELETE requests
* @param {Function} [transport.head] The function to use for HEAD requests
* @returns {WPAPI} The WPAPI instance, for chaining
*/
WPAPI.prototype.transport = function( transport ) {
// Local reference to avoid need to reference via `this` inside forEach
var _options = this._options;
// Create the default transport if it does not exist
if ( ! _options.transport ) {
_options.transport = Object.create( WPAPI.transport );
}
// Whitelist the methods that may be applied
[ 'get', 'head', 'post', 'put', 'delete' ].forEach(function( key ) {
if ( transport && transport[ key ] ) {
_options.transport[ key ] = transport[ key ];
}
});
return this;
};
/**
* Default HTTP transport methods object for all WPAPI instances
*
* These methods may be extended or replaced on an instance-by-instance basis
*
* @memberof! WPAPI
* @static
* @property transport
* @type {Object}
*/
WPAPI.transport = Object.create( httpTransport );
Object.freeze( WPAPI.transport );
/**
* Convenience method for making a new WPAPI instance
*
* @example
* These are equivalent:
*
* var wp = new WPAPI({ endpoint: 'http://my.blog.url/wp-json' });
* var wp = WPAPI.site( 'http://my.blog.url/wp-json' );
*
* `WPAPI.site` can take an optional API root response JSON object to use when
* bootstrapping the client's endpoint handler methods: if no second parameter
* is provided, the client instance is assumed to be using the default API
* with no additional plugins and is initialized with handlers for only those
* default API routes.
*
* @example
* These are equivalent:
*
* // {...} means the JSON output of http://my.blog.url/wp-json
* var wp = new WPAPI({
* endpoint: 'http://my.blog.url/wp-json',
* json: {...}
* });
* var wp = WPAPI.site( 'http://my.blog.url/wp-json', {...} );
*
* @memberof! WPAPI
* @static
* @param {String} endpoint The URI for a WP-API endpoint
* @param {Object} routes The "routes" object from the JSON object returned
* from the root API endpoint of a WP site, which should
* be a dictionary of route definition objects keyed by
* the route's regex pattern
* @returns {WPAPI} A new WPAPI instance, bound to the provided endpoint
*/
WPAPI.site = function( endpoint, routes ) {
return new WPAPI({
endpoint: endpoint,
routes: routes
});
};
/**
* Generate a request against a completely arbitrary endpoint, with no assumptions about
* or mutation of path, filtering, or query parameters. This request is not restricted to
* the endpoint specified during WPAPI object instantiation.
*
* @example
* Generate a request to the explicit URL "http://your.website.com/wp-json/some/custom/path"
*
* wp.url( 'http://your.website.com/wp-json/some/custom/path' ).get()...
*
* @memberof! WPAPI
* @param {String} url The URL to request
* @returns {WPRequest} A WPRequest object bound to the provided URL
*/
WPAPI.prototype.url = function( url ) {
var options = extend( {}, this._options, {
endpoint: url
});
return new WPRequest( options );
};
/**
* Generate a query against an arbitrary path on the current endpoint. This is useful for
* requesting resources at custom WP-API endpoints, such as WooCommerce's `/products`.
*
* @memberof! WPAPI
* @param {String} [relativePath] An endpoint-relative path to which to bind the request
* @returns {WPRequest} A request object
*/
WPAPI.prototype.root = function( relativePath ) {
relativePath = relativePath || '';
var options = extend( {}, this._options );
// Request should be
var request = new WPRequest( options );
// Set the path template to the string passed in
request._path = { '0': relativePath };
return request;
};
/**
* Set the default headers to use for all HTTP requests created from this WPAPI
* site instance. Accepts a header name and its associated value as two strings,
* or multiple headers as an object of name-value pairs.
*
* @example <caption>Set a single header to be used by all requests to this site</caption>
*
* site.setHeaders( 'Authorization', 'Bearer trustme' )...
*
* @example <caption>Set multiple headers to be used by all requests to this site</caption>
*
* site.setHeaders({
* Authorization: 'Bearer comeonwereoldfriendsright',
* 'Accept-Language': 'en-CA'
* })...
*
* @memberof! WPAPI
* @since 1.1.0
* @chainable
* @param {String|Object} headers The name of the header to set, or an object of
* header names and their associated string values
* @param {String} [value] The value of the header being set
* @returns {WPAPI} The WPAPI site handler instance, for chaining
*/
WPAPI.prototype.setHeaders = WPRequest.prototype.setHeaders;
/**
* Set the authentication to use for a WPAPI site handler instance. Accepts basic
* HTTP authentication credentials (string username & password) or a Nonce (for
* cookie authentication) by default; may be overloaded to accept OAuth credentials
* in the future.
*
* @example <caption>Basic Authentication</caption>
*
* site.auth({
* username: 'admin',
* password: 'securepass55'
* })...
*
* @example <caption>Cookie/Nonce Authentication</caption>
*
* site.auth({
* nonce: 'somenonce'
* })...
*
* @memberof! WPAPI
* @method
* @chainable
* @param {Object} credentials An authentication credentials object
* @param {String} [credentials.username] A WP-API Basic HTTP Authentication username
* @param {String} [credentials.password] A WP-API Basic HTTP Authentication password
* @param {String} [credentials.nonce] A WP nonce for use with cookie authentication
* @returns {WPAPI} The WPAPI site handler instance, for chaining
*/
WPAPI.prototype.auth = WPRequest.prototype.auth;
// Apply the registerRoute method to the prototype
WPAPI.prototype.registerRoute = require( './lib/wp-register-route' );
/**
* Deduce request methods from a provided API root JSON response object's
* routes dictionary, and assign those methods to the current instance. If
* no routes dictionary is provided then the instance will be bootstrapped
* with route handlers for the default API endpoints only.
*
* This method is called automatically during WPAPI instance creation.
*
* @memberof! WPAPI
* @chainable
* @param {Object} routes The "routes" object from the JSON object returned
* from the root API endpoint of a WP site, which should
* be a dictionary of route definition objects keyed by
* the route's regex pattern
* @returns {WPAPI} The bootstrapped WPAPI client instance (for chaining or assignment)
*/
WPAPI.prototype.bootstrap = function( routes ) {
var routesByNamespace;
var endpointFactoriesByNamespace;
if ( ! routes ) {
// Auto-generate default endpoint factories if they are not already available
if ( ! defaultEndpointFactories ) {
routesByNamespace = buildRouteTree( defaultRoutes );
defaultEndpointFactories = generateEndpointFactories( routesByNamespace );
}
endpointFactoriesByNamespace = defaultEndpointFactories;
} else {
routesByNamespace = buildRouteTree( routes );
endpointFactoriesByNamespace = generateEndpointFactories( routesByNamespace );
}
// For each namespace for which routes were identified, store the generated
// route handlers on the WPAPI instance's private _ns dictionary. These namespaced
// handler methods can be accessed by calling `.namespace( str )` on the
// client instance and passing a registered namespace string.
// Handlers for default (wp/v2) routes will also be assigned to the WPAPI
// client instance object itself, for brevity.
return objectReduce( endpointFactoriesByNamespace, function( wpInstance, endpointFactories, namespace ) {
// Set (or augment) the route handler factories for this namespace.
wpInstance._ns[ namespace ] = objectReduce( endpointFactories, function( nsHandlers, handlerFn, methodName ) {
nsHandlers[ methodName ] = handlerFn;
return nsHandlers;
}, wpInstance._ns[ namespace ] || {
// Create all namespace dictionaries with a direct reference to the main WPAPI
// instance's _options property so that things like auth propagate properly
_options: wpInstance._options
} );
// For the default namespace, e.g. "wp/v2" at the time this comment was
// written, ensure all methods are assigned to the root client object itself
// in addition to the private _ns dictionary: this is done so that these
// methods can be called with e.g. `wp.posts()` and not the more verbose
// `wp.namespace( 'wp/v2' ).posts()`.
if ( namespace === apiDefaultNamespace ) {
Object.keys( wpInstance._ns[ namespace ] ).forEach(function( methodName ) {
wpInstance[ methodName ] = wpInstance._ns[ namespace ][ methodName ];
});
}
return wpInstance;
}, this );
};
/**
* Access API endpoint handlers from a particular API namespace object
*
* @example
*
* wp.namespace( 'myplugin/v1' ).author()...
*
* // Default WP endpoint handlers are assigned to the wp instance itself.
* // These are equivalent:
* wp.namespace( 'wp/v2' ).posts()...
* wp.posts()...
*
* @memberof! WPAPI
* @param {string} namespace A namespace string
* @returns {Object} An object of route endpoint handler methods for the
* routes within the specified namespace
*/
WPAPI.prototype.namespace = function( namespace ) {
if ( ! this._ns[ namespace ] ) {
throw new Error( 'Error: namespace ' + namespace + ' is not recognized' );
}
return this._ns[ namespace ];
};
/**
* Take an arbitrary WordPress site, deduce the WP REST API root endpoint, query
* that endpoint, and parse the response JSON. Use the returned JSON response
* to instantiate a WPAPI instance bound to the provided site.
*
* @memberof! WPAPI
* @static
* @param {string} url A URL within a REST API-enabled WordPress website
* @returns {Promise} A promise that resolves to a configured WPAPI instance bound
* to the deduced endpoint, or rejected if an endpoint is not found or the
* library is unable to parse the provided endpoint.
*/
WPAPI.discover = function( url ) {
// local placeholder for API root URL
var endpoint;
// Try HEAD request first, for smaller payload: use WPAPI.site to produce
// a request that utilizes the defined HTTP transports
var req = WPAPI.site( url ).root();
return req.headers()
.catch(function() {
// On the hypothesis that any error here is related to the HEAD request
// failing, provisionally try again using GET because that method is
// more widely supported
return req.get();
})
// Inspect response to find API location header
.then( autodiscovery.locateAPIRootHeader )
.then(function( apiRootURL ) {
// Set the function-scope variable that will be used to instantiate
// the bound WPAPI instance,
endpoint = apiRootURL;
// then GET the API root JSON object
return WPAPI.site( apiRootURL ).root().get();
})
.then(function( apiRootJSON ) {
// Instantiate & bootstrap with the discovered methods
return new WPAPI({
endpoint: endpoint,
routes: apiRootJSON.routes
});
})
.catch(function( err ) {
console.error( err );
if ( endpoint ) {
console.warn( 'Endpoint detected, proceeding despite error...' );
console.warn( 'Binding to ' + endpoint + ' and assuming default routes' );
return new WPAPI.site( endpoint );
}
throw new Error( 'Autodiscovery failed' );
});
};
module.exports = WPAPI;