silverstripe-framework/admin/javascript/src/silverstripe-backend.js

222 lines
6.4 KiB
JavaScript
Raw Normal View History

import fetch from 'isomorphic-fetch';
import es6promise from 'es6-promise';
import qs from 'qs';
es6promise.polyfill();
/**
* @see https://github.com/github/fetch#handling-http-error-statuses
*/
function checkStatus(response) {
let ret;
let error;
if (response.status >= 200 && response.status < 300) {
ret = response;
} else {
error = new Error(response.statusText);
error.response = response;
throw error;
}
return ret;
}
class SilverStripeBackend {
constructor() {
// Allow mocking
this.fetch = fetch;
}
/**
* Create an endpoint fetcher from an endpoint spec.
*
* An endpoint fetcher is a anonymous function that returns a Promise.
* The function receives an JS object with properties, and will pass another JS object to
* the handler callbacks attached Promise. Other consumers don't need to deal with payload
* encoding, etc.
*
* An endpoint spec is a JS object with the following properties:
*
* - url: A fully-qualified URL
* - method: 'get', 'post', 'put', or 'delete'
* - payloadFormat: the content-type of the request data.
* - responseFormat: the content-type of the response data. Decoding will be handled for you.
*
* There is a special payloadFormat value, 'querystring', that will appear url-encoded data to
* the request URL instead of encoding data in the request body. It's a useful format to use with
* get requests.
*
* Both payloadFormat and responseFormat can use the following shortcuts for their corresponding
* mime types:
*
* - urlencoded: application/x-www-form-url-encoded
* - json: application/json
*
* For now, these are the only two mime types supported.
*
* The intent is that your endpoint spec can keep track of the mechanics of interacting with the
* backend server, and your application code can just pass a JS object endpoint fetcher. This also
* simplifies mocking.
*/
createEndpointFetcher(endpointSpec) {
// Encode a payload based on the given contentType
function encode(contentType, data) {
switch (contentType) {
case 'application/x-www-form-url-encoded':
return qs.stringify(data);
case 'application/json':
case 'application/x-json':
case 'application/x-javascript':
case 'text/javascript':
case 'text/x-javascript':
case 'text/x-json':
return JSON.stringify(data);
default:
throw new Error(`Can\'t encode format: ${contentType}`);
}
}
// Decode a payload based on the given contentType
function decode(contentType, text) {
switch (contentType) {
case 'application/x-www-form-url-encoded':
return qs.parse(text);
case 'application/json':
case 'application/x-json':
case 'application/x-javascript':
case 'text/javascript':
case 'text/x-javascript':
case 'text/x-json':
return JSON.parse(text);
default:
throw new Error(`Can\'t decode format: ${contentType}`);
}
}
// Add a querystring to a url
function addQuerystring(url, querystring) {
if (url.match(/\?/)) return `${url}&${querystring}`;
return `${url}?${querystring}`;
}
// Parse the response based on the content type returned
function parseResponse(response) {
return response.text().then(
body => decode(response.headers.get('Content-Type'), body)
);
}
// Parameter defaults
const refinedSpec = Object.assign({
method: 'get',
payloadFormat: 'application/x-www-form-url-encoded',
responseFormat: 'application/json',
}, endpointSpec);
// Substitute shorcut format values with their full mime types
const formatShortcuts = {
json: 'application/json',
urlencoded: 'application/x-www-form-url-encoded',
};
['payloadFormat', 'responseFormat'].forEach(
(key) => {
if (formatShortcuts[refinedSpec[key]]) refinedSpec[key] = formatShortcuts[refinedSpec[key]];
}
);
// Different execution path for using querystring as the payload format
if (refinedSpec.payloadFormat === 'querystring') {
return (data) => {
const headers = {
Accept: refinedSpec.responseFormat,
};
const encodedData = encode('application/x-www-form-url-encoded', data);
const url = addQuerystring(endpointSpec.url, encodedData);
return this[refinedSpec.method](url, null, headers)
.then(parseResponse);
};
}
// Return the default fetcher function
return (data) => {
const headers = {
Accept: refinedSpec.responseFormat,
'Content-Type': refinedSpec.payloadFormat,
};
const encodedData = encode(refinedSpec.payloadFormat, data);
return this[refinedSpec.method](endpointSpec.url, encodedData, headers)
.then(parseResponse);
};
}
/**
* Makes a network request using the GET HTTP verb.
*
* @param string url - Endpoint URL.
* @return object - Promise
*/
get(url) {
return this.fetch(url, { method: 'get', credentials: 'same-origin' })
.then(checkStatus);
}
/**
* Makes a network request using the POST HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
post(url, data) {
return this.fetch(url, {
method: 'post',
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded',
}),
credentials: 'same-origin',
body: data,
})
.then(checkStatus);
}
/**
* Makes a newtwork request using the PUT HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
put(url, data) {
return this.fetch(url, { method: 'put', credentials: 'same-origin', body: data })
.then(checkStatus);
}
/**
* Makes a newtwork request using the DELETE HTTP verb.
*
* @param string url - Endpoint URL.
* @param object data - Data to send with the request.
* @return object - Promise
*/
delete(url, data) {
return this.fetch(url, { method: 'delete', credentials: 'same-origin', body: data })
.then(checkStatus);
}
}
// Exported as a singleton so we can implement things like
// global caching and request batching at some stage.
const backend = new SilverStripeBackend();
export default backend;