Skip to content

Commit f4a23a9

Browse files
authored
Merge pull request WP-API#432 from WP-API/implement-fetch-transport
Implement fetch transport
2 parents 8c9a2bc + 2f06556 commit f4a23a9

31 files changed

+836
-249
lines changed

Diff for: bin/jekyll

-40
This file was deleted.

Diff for: bin/jekyll.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* This script will start the Jekyll server in the context of the docs
3+
* directory. It is only for use in local development, and sets the --baseurl
4+
* option to override the production-only baseurl in _config.yml.
5+
*/
6+
/* eslint-disable no-console */
7+
'use strict';
8+
9+
const path = require( 'path' );
10+
const spawn = require( 'child_process' ).spawn;
11+
const argv = require( 'minimist' )( process.argv.slice( 2 ) );
12+
13+
// Execute within the context of the docs directory
14+
const docsDir = path.resolve( __dirname, '../documentation' );
15+
16+
if ( argv.install || argv.i ) {
17+
// Install the ruby bundle needed to run jekyll
18+
const server = spawn( 'bundle', [ 'install' ], {
19+
cwd: docsDir,
20+
stdio: 'inherit',
21+
} );
22+
23+
server.on( 'error', err => console.error( err ) );
24+
} else {
25+
// Start the server in local dev mode
26+
const bundleOptions = [ 'exec', 'jekyll', 'serve', '--baseurl', '' ];
27+
if ( argv.host ) {
28+
bundleOptions.push( '--host', argv.host );
29+
}
30+
31+
const server = spawn( 'bundle', bundleOptions, {
32+
cwd: docsDir,
33+
stdio: 'inherit',
34+
} );
35+
36+
server.on( 'error', err => console.error( err ) );
37+
}

Diff for: fetch/README.md

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

Diff for: fetch/fetch-transport.js

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* @module fetch-transport
3+
*/
4+
'use strict';
5+
6+
const fetch = require( 'node-fetch' );
7+
const FormData = require( 'form-data' );
8+
const fs = require( 'fs' );
9+
10+
const objectReduce = require( '../lib/util/object-reduce' );
11+
const { createPaginationObject } = require( '../lib/pagination' );
12+
13+
/**
14+
* Utility method to set a header value on a fetch configuration object.
15+
*
16+
* @method _setHeader
17+
* @private
18+
* @param {Object} config A configuration object of unknown completeness
19+
* @param {string} header String name of the header to set
20+
* @param {string} value Value of the header to set
21+
* @returns {Object} The modified configuration object
22+
*/
23+
const _setHeader = ( config, header, value ) => ( {
24+
...config,
25+
headers: {
26+
...( config && config.headers ? config.headers : null ),
27+
[ header ]: value,
28+
},
29+
} );
30+
31+
/**
32+
* Set any provided headers on the outgoing request object. Runs after _auth.
33+
*
34+
* @method _setHeaders
35+
* @private
36+
* @param {Object} config A fetch request configuration object
37+
* @param {Object} options A WPRequest _options object
38+
* @param {Object} A fetch config object, with any available headers set
39+
*/
40+
function _setHeaders( config, options ) {
41+
// If there's no headers, do nothing
42+
if ( ! options.headers ) {
43+
return config;
44+
}
45+
46+
return objectReduce(
47+
options.headers,
48+
( config, value, key ) => _setHeader( config, key, value ),
49+
config,
50+
);
51+
}
52+
53+
/**
54+
* Conditionally set basic or nonce authentication on a server request object.
55+
*
56+
* @method _auth
57+
* @private
58+
* @param {Object} config A fetch request configuration object
59+
* @param {Object} options A WPRequest _options object
60+
* @param {Boolean} forceAuthentication whether to force authentication on the request
61+
* @param {Object} A fetch request object, conditionally configured to use basic auth
62+
*/
63+
function _auth( config, options, forceAuthentication ) {
64+
// If we're not supposed to authenticate, don't even start
65+
if ( ! forceAuthentication && ! options.auth && ! options.nonce ) {
66+
return config;
67+
}
68+
69+
// Enable nonce in options for Cookie authentication http://wp-api.org/guides/authentication.html
70+
if ( options.nonce ) {
71+
config.credentials = 'same-origin';
72+
return _setHeader( config, 'X-WP-Nonce', options.nonce );
73+
}
74+
75+
// If no username or no password, can't authenticate
76+
if ( ! options.username || ! options.password ) {
77+
return config;
78+
}
79+
80+
// Can authenticate: set basic auth parameters on the config
81+
let authorization = `${ options.username }:${ options.password }`;
82+
if ( global.Buffer ) {
83+
authorization = global.Buffer.from( authorization ).toString( 'base64' );
84+
} else if ( global.btoa ) {
85+
authorization = global.btoa( authorization );
86+
}
87+
88+
return _setHeader( config, 'Authorization', `Basic ${ authorization }` );
89+
}
90+
91+
// HTTP-Related Helpers
92+
// ====================
93+
94+
/**
95+
* Get the response headers as a regular JavaScript object.
96+
*
97+
* @param {Object} response Fetch response object.
98+
*/
99+
function getHeaders( response ) {
100+
const headers = {};
101+
response.headers.forEach( ( value, key ) => {
102+
headers[ key ] = value;
103+
} );
104+
return headers;
105+
}
106+
107+
/**
108+
* Return the body of the request, augmented with pagination information if the
109+
* result is a paged collection.
110+
*
111+
* @private
112+
* @param {WPRequest} wpreq The WPRequest representing the returned HTTP response
113+
* @param {Object} response The fetch response object for the HTTP call
114+
* @returns {Object} The JSON data of the response, conditionally augmented with
115+
* pagination information if the response is a partial collection.
116+
*/
117+
const parseFetchResponse = ( response, wpreq ) => {
118+
// Check if an HTTP error occurred.
119+
if ( ! response.ok ) {
120+
// Extract and return the API-provided error object if the response is
121+
// not ok, i.e. if the error was from the API and not internal to fetch.
122+
return response.json().then( ( err ) => {
123+
// Throw the error object to permit proper error handling.
124+
throw err;
125+
}, () => {
126+
// JSON serialization failed; throw the underlying response.
127+
throw response;
128+
} );
129+
}
130+
131+
// If the response is OK, process & return the JSON data.
132+
return response.json().then( ( body ) => {
133+
// Construct a response the pagination helper can understand.
134+
const mockResponse = {
135+
headers: getHeaders( response ),
136+
};
137+
138+
const _paging = createPaginationObject( mockResponse, wpreq._options, wpreq.transport );
139+
if ( _paging ) {
140+
body._paging = _paging;
141+
}
142+
return body;
143+
} );
144+
};
145+
146+
// HTTP Methods: Private HTTP-verb versions
147+
// ========================================
148+
149+
const send = ( wpreq, config ) => fetch(
150+
wpreq.toString(),
151+
_setHeaders( _auth( config, wpreq._options ), wpreq._options )
152+
).then( ( response ) => {
153+
// return response.headers.get( 'Link' );
154+
return parseFetchResponse( response, wpreq );
155+
} );
156+
157+
/**
158+
* @method get
159+
* @async
160+
* @param {WPRequest} wpreq A WPRequest query object
161+
* @returns {Promise} A promise to the results of the HTTP request
162+
*/
163+
function _httpGet( wpreq ) {
164+
return send( wpreq, {
165+
method: 'GET',
166+
} );
167+
}
168+
169+
/**
170+
* Invoke an HTTP "POST" request against the provided endpoint
171+
* @method post
172+
* @async
173+
* @param {WPRequest} wpreq A WPRequest query object
174+
* @param {Object} data The data for the POST request
175+
* @returns {Promise} A promise to the results of the HTTP request
176+
*/
177+
function _httpPost( wpreq, data = {} ) {
178+
let file = wpreq._attachment;
179+
if ( file ) {
180+
// Handle files provided as a path string
181+
if ( typeof file === 'string' ) {
182+
file = fs.createReadStream( file );
183+
}
184+
185+
// Build the form data object
186+
const form = new FormData();
187+
form.append( 'file', file, wpreq._attachmentName );
188+
Object.keys( data ).forEach( key => form.append( key, data[ key ] ) );
189+
190+
// Fire off the media upload request
191+
return send( wpreq, {
192+
method: 'POST',
193+
redirect: 'follow',
194+
body: form,
195+
} );
196+
}
197+
198+
return send( wpreq, {
199+
method: 'POST',
200+
headers: {
201+
'Content-Type': 'application/json',
202+
},
203+
redirect: 'follow',
204+
body: JSON.stringify( data ),
205+
} );
206+
}
207+
208+
/**
209+
* @method put
210+
* @async
211+
* @param {WPRequest} wpreq A WPRequest query object
212+
* @param {Object} data The data for the PUT request
213+
* @returns {Promise} A promise to the results of the HTTP request
214+
*/
215+
function _httpPut( wpreq, data = {} ) {
216+
return send( wpreq, {
217+
method: 'PUT',
218+
headers: {
219+
'Content-Type': 'application/json',
220+
},
221+
redirect: 'follow',
222+
body: JSON.stringify( data ),
223+
} );
224+
}
225+
226+
/**
227+
* @method delete
228+
* @async
229+
* @param {WPRequest} wpreq A WPRequest query object
230+
* @param {Object} [data] Data to send along with the DELETE request
231+
* @returns {Promise} A promise to the results of the HTTP request
232+
*/
233+
function _httpDelete( wpreq, data ) {
234+
const config = {
235+
method: 'DELETE',
236+
headers: {
237+
'Content-Type': 'application/json',
238+
},
239+
redirect: 'follow',
240+
};
241+
242+
if ( data ) {
243+
config.body = JSON.stringify( data );
244+
}
245+
246+
return send( wpreq, config );
247+
}
248+
249+
/**
250+
* @method head
251+
* @async
252+
* @param {WPRequest} wpreq A WPRequest query object
253+
* @returns {Promise} A promise to the header results of the HTTP request
254+
*/
255+
function _httpHead( wpreq ) {
256+
const url = wpreq.toString();
257+
const config = _setHeaders( _auth( {
258+
method: 'HEAD',
259+
}, wpreq._options, true ), wpreq._options );
260+
261+
return fetch( url, config )
262+
.then( response => getHeaders( response ) );
263+
}
264+
265+
module.exports = {
266+
delete: _httpDelete,
267+
get: _httpGet,
268+
head: _httpHead,
269+
post: _httpPost,
270+
put: _httpPut,
271+
};

Diff for: fetch/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const WPAPI = require( '../wpapi' );
2+
const fetchTransport = require( './fetch-transport' );
3+
const bindTransport = require( '../lib/bind-transport' );
4+
5+
// Bind the fetch-based HTTP transport to the WPAPI constructor
6+
module.exports = bindTransport( WPAPI, fetchTransport );

Diff for: fetch/tests/.eslintrc.js

+5
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)