-
Notifications
You must be signed in to change notification settings - Fork 190
/
wpapi.js
394 lines (362 loc) · 14.5 KB
/
wpapi.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
/**
* 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';
const 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
const defaultRoutes = require( './lib/data/default-routes.json' );
const buildRouteTree = require( './lib/route-tree' ).build;
const 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)
let defaultEndpointFactories;
// Constant used to detect first-party WordPress REST API routes
const apiDefaultNamespace = 'wp/v2';
// Pull in base module constructors
const WPRequest = require( './lib/constructors/wp-request' );
/**
* 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 ) {
* var result = cache[ wpreq ];
* // If a cache hit is found, return it via the same promise
* // signature as that of the default transport method
* if ( result ) {
* return Promise.resolve( result );
* }
*
* // Delegate to default transport if no cached data was found
* return this.constructor.transport.get( wpreq ).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
const _options = this._options;
// Attempt to use the default transport if no override was provided
if ( ! _options.transport ) {
_options.transport = this.constructor.transport ?
Object.create( this.constructor.transport ) :
{};
}
// Whitelist the methods that may be applied
[ 'get', 'head', 'post', 'put', 'delete' ].forEach( ( key ) => {
if ( transport && transport[ key ] ) {
_options.transport[ key ] = transport[ key ];
}
} );
return this;
};
/**
* 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 ) {
return new WPRequest( {
...this._options,
endpoint: url,
} );
};
/**
* 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 || '';
const options = {
...this._options,
};
// Request should be
const 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 ) {
let routesByNamespace;
let 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, ( wpInstance, endpointFactories, namespace ) => {
// Set (or augment) the route handler factories for this namespace.
wpInstance._ns[ namespace ] = objectReduce(
endpointFactories,
( 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( ( 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 ];
};
/**
* Convenience method for making a new WPAPI instance for a given API root
*
* @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 = ( endpoint, routes ) => {
return new WPAPI( {
endpoint: endpoint,
routes: routes,
} );
};
module.exports = WPAPI;