Skip to content

Commit f954eed

Browse files
committed
Implement rough first-draft axios transport
1 parent 8c9a2bc commit f954eed

File tree

6 files changed

+559
-0
lines changed

6 files changed

+559
-0
lines changed

axios/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# `wpapi/superagent`
2+
3+
This endpoint returns a version of the WPAPI library configured to use Axios for HTTP requests.
4+
5+
## Installation & Usage
6+
7+
Install both `wpapi` and `axios` using the command `npm install --save wpapi axios`.
8+
9+
```js
10+
import WPAPI from 'wpapi/axios';
11+
12+
// Configure and use WPAPI as normal
13+
const site = new WPAPI( { /* ... */ } );
14+
```

axios/axios-transport.js

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
/**
2+
* @module http-transport
3+
*/
4+
'use strict';
5+
6+
const axios = require( 'axios' );
7+
const parseLinkHeader = require( 'li' ).parse;
8+
const FormData = require( 'form-data' );
9+
10+
const WPRequest = require( '../lib/constructors/wp-request' );
11+
const objectReduce = require( '../lib/util/object-reduce' );
12+
const isEmptyObject = require( '../lib/util/is-empty-object' );
13+
14+
/**
15+
* Utility method to set a header value on an axios configuration object.
16+
*
17+
* @method _setHeader
18+
* @private
19+
* @param {Object} config A configuration object of unknown completeness
20+
* @param {string} header String name of the header to set
21+
* @param {string} value Value of the header to set
22+
* @returns {Object} The modified configuration object
23+
*/
24+
/**
25+
*
26+
*/
27+
const _setHeader = ( config, header, value ) => ( {
28+
...config,
29+
headers: {
30+
...( config && config.headers ? config.headers : null ),
31+
[ header ]: value,
32+
},
33+
} );
34+
35+
/**
36+
* Set any provided headers on the outgoing request object. Runs after _auth.
37+
*
38+
* @method _setHeaders
39+
* @private
40+
* @param {Object} config An axios request configuration object
41+
* @param {Object} options A WPRequest _options object
42+
* @param {Object} An axios config object, with any available headers set
43+
*/
44+
function _setHeaders( config, options ) {
45+
// If there's no headers, do nothing
46+
if ( ! options.headers ) {
47+
return config;
48+
}
49+
50+
return objectReduce(
51+
options.headers,
52+
( config, value, key ) => _setHeader( config, key, value ),
53+
config,
54+
);
55+
}
56+
57+
/**
58+
* Conditionally set basic or nonce authentication on a server request object.
59+
*
60+
* @method _auth
61+
* @private
62+
* @param {Object} config An axios request configuration object
63+
* @param {Object} options A WPRequest _options object
64+
* @param {Boolean} forceAuthentication whether to force authentication on the request
65+
* @param {Object} An axios request object, conditionally configured to use basic auth
66+
*/
67+
function _auth( config, options, forceAuthentication ) {
68+
// If we're not supposed to authenticate, don't even start
69+
if ( ! forceAuthentication && ! options.auth && ! options.nonce ) {
70+
return config;
71+
}
72+
73+
// Enable nonce in options for Cookie authentication http://wp-api.org/guides/authentication.html
74+
if ( options.nonce ) {
75+
return _setHeader( config, 'X-WP-Nonce', options.nonce );
76+
}
77+
78+
// If no username or no password, can't authenticate
79+
if ( ! options.username || ! options.password ) {
80+
return config;
81+
}
82+
83+
// Can authenticate: set basic auth parameters on the config
84+
return {
85+
...config,
86+
auth: {
87+
username: options.username,
88+
password: options.password,
89+
},
90+
};
91+
}
92+
93+
// Pagination-Related Helpers
94+
// ==========================
95+
96+
/**
97+
* Extract the body property from the axios response, or else try to parse
98+
* the response text to get a JSON object.
99+
*
100+
* @private
101+
* @param {Object} response The response object from the HTTP request
102+
* @param {String} response.text The response content as text
103+
* @param {Object} response.body The response content as a JS object
104+
* @returns {Object} The response content as a JS object
105+
*/
106+
function extractResponseBody( response ) {
107+
let responseBody = response.data;
108+
if ( isEmptyObject( responseBody ) && response.type === 'text/html' ) {
109+
// Response may have come back as HTML due to caching plugin; try to parse
110+
// the response text into JSON
111+
try {
112+
responseBody = JSON.parse( response.text );
113+
} catch ( e ) {
114+
// Swallow errors, it's OK to fall back to returning the body
115+
}
116+
}
117+
return responseBody;
118+
}
119+
120+
/**
121+
* If the response is not paged, return the body as-is. If pagination
122+
* information is present in the response headers, parse those headers into
123+
* a custom `_paging` property on the response body. `_paging` contains links
124+
* to the previous and next pages in the collection, as well as metadata
125+
* about the size and number of pages in the collection.
126+
*
127+
* The structure of the `_paging` property is as follows:
128+
*
129+
* - `total` {Integer} The total number of records in the collection
130+
* - `totalPages` {Integer} The number of pages available
131+
* - `links` {Object} The parsed "links" headers, separated into individual URI strings
132+
* - `next` {WPRequest} A WPRequest object bound to the "next" page (if page exists)
133+
* - `prev` {WPRequest} A WPRequest object bound to the "previous" page (if page exists)
134+
*
135+
* @private
136+
* @param {Object} response The response object from the HTTP request
137+
* @param {Object} options The options hash from the original request
138+
* @param {String} options.endpoint The base URL of the requested API endpoint
139+
* @param {Object} httpTransport The HTTP transport object used by the original request
140+
* @returns {Object} The pagination metadata object for this HTTP request, or else null
141+
*/
142+
function createPaginationObject( response, options, httpTransport ) {
143+
let _paging = null;
144+
145+
if ( ! response.headers ) {
146+
// No headers: return as-is
147+
return _paging;
148+
}
149+
150+
const headers = response.headers;
151+
152+
// Guard against capitalization inconsistencies in returned headers
153+
Object.keys( headers ).forEach( ( header ) => {
154+
headers[ header.toLowerCase() ] = headers[ header ];
155+
} );
156+
157+
if ( ! headers[ 'x-wp-totalpages' ] ) {
158+
// No paging: return as-is
159+
return _paging;
160+
}
161+
162+
const totalPages = +headers[ 'x-wp-totalpages' ];
163+
164+
if ( ! totalPages || totalPages === 0 ) {
165+
// No paging: return as-is
166+
return _paging;
167+
}
168+
169+
// Decode the link header object
170+
const links = headers.link ?
171+
parseLinkHeader( headers.link ) :
172+
{};
173+
174+
// Store pagination data from response headers on the response collection
175+
_paging = {
176+
total: +headers[ 'x-wp-total' ],
177+
totalPages: totalPages,
178+
links: links,
179+
};
180+
181+
// Create a WPRequest instance pre-bound to the "next" page, if available
182+
if ( links.next ) {
183+
_paging.next = new WPRequest( {
184+
...options,
185+
transport: httpTransport,
186+
endpoint: links.next,
187+
} );
188+
}
189+
190+
// Create a WPRequest instance pre-bound to the "prev" page, if available
191+
if ( links.prev ) {
192+
_paging.prev = new WPRequest( {
193+
...options,
194+
transport: httpTransport,
195+
endpoint: links.prev,
196+
} );
197+
}
198+
199+
return _paging;
200+
}
201+
202+
// HTTP-Related Helpers
203+
// ====================
204+
205+
/**
206+
* Return the body of the request, augmented with pagination information if the
207+
* result is a paged collection.
208+
*
209+
* @private
210+
* @param {WPRequest} wpreq The WPRequest representing the returned HTTP response
211+
* @param {Object} response The axios response object for the HTTP call
212+
* @returns {Object} The "body" property of the response, conditionally augmented with
213+
* pagination information if the response is a partial collection.
214+
*/
215+
function returnBody( wpreq, response ) {
216+
// console.log( response );
217+
console.log( wpreq.toString() );
218+
console.log( response.headers );
219+
const body = extractResponseBody( response );
220+
const _paging = createPaginationObject( response, wpreq._options, wpreq.transport );
221+
if ( _paging ) {
222+
body._paging = _paging;
223+
}
224+
return body;
225+
}
226+
227+
/**
228+
* Handle errors received during axios requests.
229+
*
230+
* @param {Object} err Axios error response object.
231+
*/
232+
function handleErrors( err ) {
233+
// Check to see if a request came back at all.
234+
// If the API provided an error object, it will be available within the
235+
// axios response object as .response.data (containing the response
236+
// JSON). If that object exists, it will have a .code property if it is
237+
// truly an API error (non-API errors will not have a .code).
238+
if ( err.response && err.response.data && err.response.data.code ) {
239+
// Forward API error response JSON on to the calling method: omit
240+
// all transport-specific (axios-specific) properties
241+
throw err.response.data;
242+
}
243+
// Re-throw the unmodified error for other issues, to aid debugging.
244+
throw err;
245+
}
246+
247+
// HTTP Methods: Private HTTP-verb versions
248+
// ========================================
249+
250+
/**
251+
* @method get
252+
* @async
253+
* @param {WPRequest} wpreq A WPRequest query object
254+
* @returns {Promise} A promise to the results of the HTTP request
255+
*/
256+
function _httpGet( wpreq ) {
257+
const url = wpreq.toString();
258+
259+
const config = _setHeaders( _auth( {}, wpreq._options ), wpreq._options );
260+
return axios.get( url, {
261+
auth: {
262+
username: 'admin',
263+
password: 'password',
264+
},
265+
} )
266+
.then( (r) => {
267+
console.log( r.headers );
268+
console.log( '<<' );
269+
return r;
270+
} )
271+
.then( response => returnBody( wpreq, response ) )
272+
.catch( handleErrors );
273+
}
274+
275+
/**
276+
* Invoke an HTTP "POST" request against the provided endpoint
277+
* @method post
278+
* @async
279+
* @param {WPRequest} wpreq A WPRequest query object
280+
* @param {Object} data The data for the POST request
281+
* @returns {Promise} A promise to the results of the HTTP request
282+
*/
283+
function _httpPost( wpreq, data = {} ) {
284+
const url = wpreq.toString();
285+
286+
let config = _setHeaders( _auth( {}, wpreq._options, true ), wpreq._options );
287+
288+
if ( wpreq._attachment ) {
289+
// Data must be form-encoded alongside image attachment
290+
const form = new FormData();
291+
data = objectReduce(
292+
data,
293+
( form, value, key ) => form.append( key, value ),
294+
// TODO: Probably need to read in the file if a string path is given
295+
form.append( 'file', wpreq._attachment, wpreq._attachmentName )
296+
);
297+
config = objectReduce(
298+
form.getHeaders(),
299+
( config, value, key ) => _setHeader( config, key, value ),
300+
config
301+
);
302+
}
303+
304+
return axios.post( url, data, config )
305+
.then( response => returnBody( wpreq, response ) )
306+
.catch( handleErrors );
307+
}
308+
309+
/**
310+
* @method put
311+
* @async
312+
* @param {WPRequest} wpreq A WPRequest query object
313+
* @param {Object} data The data for the PUT request
314+
* @returns {Promise} A promise to the results of the HTTP request
315+
*/
316+
function _httpPut( wpreq, data = {} ) {
317+
const url = wpreq.toString();
318+
319+
const config = _setHeaders( _auth( {}, wpreq._options, true ), wpreq._options );
320+
321+
return axios.put( url, data, config )
322+
.then( response => returnBody( wpreq, response ) )
323+
.catch( handleErrors );
324+
}
325+
326+
/**
327+
* @method delete
328+
* @async
329+
* @param {WPRequest} wpreq A WPRequest query object
330+
* @param {Object} [data] Data to send along with the DELETE request
331+
* @returns {Promise} A promise to the results of the HTTP request
332+
*/
333+
function _httpDelete( wpreq, data ) {
334+
const url = wpreq.toString();
335+
const config = _setHeaders( _auth( {}, wpreq._options, true ), wpreq._options );
336+
337+
// See https://github.com/axios/axios/issues/897#issuecomment-343715381
338+
if ( data ) {
339+
config.data = data;
340+
}
341+
342+
return axios.delete( url, config )
343+
.then( response => returnBody( wpreq, response ) )
344+
.catch( handleErrors );
345+
}
346+
347+
/**
348+
* @method head
349+
* @async
350+
* @param {WPRequest} wpreq A WPRequest query object
351+
* @returns {Promise} A promise to the header results of the HTTP request
352+
*/
353+
function _httpHead( wpreq ) {
354+
const url = wpreq.toString();
355+
const config = _setHeaders( _auth( {}, wpreq._options, true ), wpreq._options );
356+
357+
return axios.head( url, config )
358+
.then( response => response.headers )
359+
.catch( handleErrors );
360+
}
361+
362+
module.exports = {
363+
delete: _httpDelete,
364+
get: _httpGet,
365+
head: _httpHead,
366+
post: _httpPost,
367+
put: _httpPut,
368+
};

axios/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const WPAPI = require( '../wpapi' );
2+
const axiosTransport = require( './axios-transport' );
3+
const bindTransport = require( '../lib/bind-transport' );
4+
5+
// Bind the axios-based HTTP transport to the WPAPI constructor
6+
module.exports = bindTransport( WPAPI, axiosTransport );

axios/tests/.eslintrc.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
'env': {
3+
jest: true,
4+
},
5+
};

0 commit comments

Comments
 (0)