From 2309e215715d62eb735bd4ca27fa145224108fba Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 14 Jun 2016 16:53:18 +0200 Subject: [PATCH] Rewrite requests module using js-utility-belt's request --- js/components/ascribe_forms/form.js | 31 ++- .../ascribe_forms/form_delete_edition.js | 24 +- .../ascribe_forms/form_delete_piece.js | 12 +- .../ascribe_forms/form_piece_extradata.js | 18 +- .../form_remove_editions_from_collection.js | 20 +- .../form_remove_piece_from_collection.js | 11 +- .../cyland_additional_data_form.js | 11 +- .../ikonotv_artist_details_form.js | 11 +- .../ikonotv_artwork_details_form.js | 11 +- .../market_additional_data_form.js | 12 +- .../wallet/constants/wallet_api_urls.js | 14 +- js/constants/api_urls.js | 28 +-- js/fetchers/application_fetcher.js | 10 +- js/fetchers/edition_list_fetcher.js | 13 +- js/fetchers/license_fetcher.js | 8 +- js/fetchers/notification_fetcher.js | 17 +- js/fetchers/ownership_fetcher.js | 64 +++-- js/fetchers/piece_list_fetcher.js | 8 +- js/fetchers/s3_fetcher.js | 15 +- js/fetchers/wallet_settings_fetcher.js | 4 +- js/sources/coa_source.js | 21 +- js/sources/edition_source.js | 8 +- js/sources/piece_source.js | 6 +- js/sources/user_source.js | 6 +- js/sources/webhook_source.js | 11 +- js/sources/whitelabel_source.js | 8 +- js/utils/request.js | 236 ++++++------------ 27 files changed, 314 insertions(+), 324 deletions(-) diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js index 7e1371ed..1254fb76 100644 --- a/js/components/ascribe_forms/form.js +++ b/js/components/ascribe_forms/form.js @@ -11,10 +11,10 @@ import AlertDismissable from './alert'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; -import requests from '../../utils/requests'; - import { sanitize } from '../../utils/general'; import { getLangText } from '../../utils/lang'; +import request from '../../utils/request'; + let Form = React.createClass({ @@ -95,32 +95,29 @@ let Form = React.createClass({ } }, - post() { - requests - .post(this.props.url, { body: this.getFormData() }) + request(method) { + request(this.props.url, { + method, + jsonBody: this.getFormData() + }) .then(this.handleSuccess) .catch(this.handleError); }, + post() { + this.request('POST'); + }, + put() { - requests - .put(this.props.url, { body: this.getFormData() }) - .then(this.handleSuccess) - .catch(this.handleError); + this.request('PUT'); }, patch() { - requests - .patch(this.props.url, { body: this.getFormData() }) - .then(this.handleSuccess) - .catch(this.handleError); + this.request('PATCH'); }, delete() { - requests - .delete(this.props.url, this.getFormData()) - .then(this.handleSuccess) - .catch(this.handleError); + this.request('DELETE'); }, getFormData() { diff --git a/js/components/ascribe_forms/form_delete_edition.js b/js/components/ascribe_forms/form_delete_edition.js index 113d920a..5ba8366f 100644 --- a/js/components/ascribe_forms/form_delete_edition.js +++ b/js/components/ascribe_forms/form_delete_edition.js @@ -9,6 +9,7 @@ import AclInformation from '../ascribe_buttons/acl_information'; import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang'; +import { formatText } from '../../utils/text'; import { resolveUrl } from '../../utils/url_resolver'; @@ -22,25 +23,22 @@ let EditionDeleteForm = React.createClass({ }, getBitcoinIds() { - return this.props.editions.map(function(edition){ - return edition.bitcoin_id; + return this.props.editions.map((edition) => edition.bitcoin_id); + }, + + getUrl() { + return formatText(resolveUrl('edition_delete'), { + // Since this form can be used for either deleting a single edition or multiple we need + // to call getBitcoinIds to get the value of edition_id + editionId: this.getBitcoinIds().join(',') }); }, - // Since this form can be used for either deleting a single edition or multiple - // we need to call getBitcoinIds to get the value of edition_id - getFormData() { - return { - edition_id: this.getBitcoinIds().join(',') - }; - }, - - render () { + render() { return (
+ url={this.getUrl()}> diff --git a/js/components/ascribe_forms/form_remove_editions_from_collection.js b/js/components/ascribe_forms/form_remove_editions_from_collection.js index 3376d0fb..10c40b8e 100644 --- a/js/components/ascribe_forms/form_remove_editions_from_collection.js +++ b/js/components/ascribe_forms/form_remove_editions_from_collection.js @@ -7,6 +7,7 @@ import Form from './form'; import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang'; +import { formatText } from '../../utils/text'; import { resolveUrl } from '../../utils/url_resolver'; let EditionRemoveFromCollectionForm = React.createClass({ @@ -18,25 +19,22 @@ let EditionRemoveFromCollectionForm = React.createClass({ }, getBitcoinIds() { - return this.props.editions.map(function(edition){ - return edition.bitcoin_id; - }); + return this.props.editions.map((edition) => edition.bitcoin_id); }, - // Since this form can be used for either removing a single edition or multiple - // we need to call getBitcoinIds to get the value of edition_id - getFormData() { - return { - edition_id: this.getBitcoinIds().join(',') - }; + getUrl() { + return formatText(resolveUrl('edition_remove_from_collection'), { + // Since this form can be used for either deleting a single edition or multiple we need + // to call getBitcoinIds to get the value of edition_id + editionId: this.getBitcoinIds().join(',') + }); }, render() { return ( { - // If no coa is found here, fake a 404 error so the error action can pick it up - return (res && res.coa) ? res : Promise.reject({ json: { status: 404 } }); - }); + return request('coa', { + urlTemplateSpec: { + id: coaId + } + }).then((res) => ( + // If no coa is found here, fake a 404 error so the error action can pick it up + (res && res.coa) ? res : Promise.reject({ json: { status: 404 } }) + )); }, success: EditionActions.successFetchCoa, @@ -22,7 +24,10 @@ const CoaSource = { performCreateCoaForEdition: { remote(state, editionId) { - return requests.post('coa_create', { body: { bitcoin_id: editionId } }); + return request('coa_create', { + method: 'POST', + jsonBody: { bitcoin_id: editionId } + }); }, success: EditionActions.successFetchCoa, diff --git a/js/sources/edition_source.js b/js/sources/edition_source.js index dd295352..2382ad8e 100644 --- a/js/sources/edition_source.js +++ b/js/sources/edition_source.js @@ -1,14 +1,16 @@ 'use strict'; -import requests from '../utils/requests'; +import request from '../utils/request'; import EditionActions from '../actions/edition_actions'; const EditionSource = { lookupEdition: { - remote(state, editionId) { - return requests.get('edition', { bitcoin_id: editionId }); + remote(state, bitcoinId) { + return request('edition', { + urlTemplateSpec: { bitcoinId } + }); }, success: EditionActions.successFetchEdition, diff --git a/js/sources/piece_source.js b/js/sources/piece_source.js index 393c844c..23eba95b 100644 --- a/js/sources/piece_source.js +++ b/js/sources/piece_source.js @@ -1,6 +1,6 @@ 'use strict'; -import requests from '../utils/requests'; +import request from '../utils/request'; import PieceActions from '../actions/piece_actions'; @@ -8,7 +8,9 @@ import PieceActions from '../actions/piece_actions'; const PieceSource = { lookupPiece: { remote(state, pieceId) { - return requests.get('piece', { piece_id: pieceId }); + return request('piece', { + urlTemplateSpec: { pieceId } + }); }, success: PieceActions.successFetchPiece, diff --git a/js/sources/user_source.js b/js/sources/user_source.js index 2b434a2c..853dabca 100644 --- a/js/sources/user_source.js +++ b/js/sources/user_source.js @@ -2,13 +2,13 @@ import UserActions from '../actions/user_actions'; -import requests from '../utils/requests'; +import request from '../utils/request'; const UserSource = { lookupCurrentUser: { remote() { - return requests.get('user'); + return request('user'); }, local(state) { @@ -25,7 +25,7 @@ const UserSource = { performLogoutCurrentUser: { remote() { - return requests.get('users_logout'); + return request('users_logout'); }, success: UserActions.successLogoutCurrentUser, diff --git a/js/sources/webhook_source.js b/js/sources/webhook_source.js index 7174d74a..99cc0136 100644 --- a/js/sources/webhook_source.js +++ b/js/sources/webhook_source.js @@ -1,6 +1,6 @@ 'use strict'; -import requests from '../utils/requests'; +import request from '../utils/request'; import WebhookActions from '../actions/webhook_actions'; @@ -8,7 +8,7 @@ import WebhookActions from '../actions/webhook_actions'; const WebhookSource = { lookupWebhooks: { remote() { - return requests.get('webhooks'); + return request('webhooks'); }, local(state) { @@ -25,7 +25,7 @@ const WebhookSource = { lookupWebhookEvents: { remote() { - return requests.get('webhooks_events'); + return request('webhooks_events'); }, local(state) { @@ -42,7 +42,10 @@ const WebhookSource = { performRemoveWebhook: { remote(state, webhookId) { - return requests.delete('webhook', { 'webhook_id': webhookId }); + return request('webhook', { + method: 'DELETE', + urlTemplateSpec: { webhookId } + }); }, success: WebhookActions.successRemoveWebhook, diff --git a/js/sources/whitelabel_source.js b/js/sources/whitelabel_source.js index ade1b95b..ad15e87b 100644 --- a/js/sources/whitelabel_source.js +++ b/js/sources/whitelabel_source.js @@ -1,6 +1,6 @@ 'use strict'; -import requests from '../utils/requests'; +import request from '../utils/request'; import WhitelabelActions from '../actions/whitelabel_actions'; import { getCurrentSubdomain } from '../utils/url'; @@ -9,7 +9,11 @@ import { getCurrentSubdomain } from '../utils/url'; const WhitelabelSource = { lookupWhitelabel: { remote() { - return requests.get('whitelabel_settings', { 'subdomain': getCurrentSubdomain() }); + return request('whitelabel_settings', { + urlTemplateSpec: { + subdomain: getCurrentSubdomain() + } + }); }, local(state) { diff --git a/js/utils/request.js b/js/utils/request.js index b16038c4..7b9e148e 100644 --- a/js/utils/request.js +++ b/js/utils/request.js @@ -1,172 +1,94 @@ -'use strict'; +import { request as baseRequest, sanitize } from 'js-utility-belt/es6'; -import Q from 'q'; - -import AppConstants from '../constants/application_constants'; - -import { getCookie } from '../utils/fetch_api'; -import { omitFromObject } from '../utils/general'; -import { stringifyAsQueryParam } from '../utils/url'; +import { makeCsrfHeader } from './csrf'; +import { resolveUrl } from './url_resolver'; -class Requests { - unpackResponse(url) { - return (response) => { - if (response == null) { - throw new Error(`For: ${url} - Server did not respond to the request. (Not even displayed a 500)`); - } +const DEFAULT_REQUEST_CONFIG = { + credentials: 'include', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } +}; - if (response.status >= 500) { - let err = new Error(`${response.status} - ${response.statusText} - on URL: ${response.url}`); +/** + * Small wrapper around js-utility-belt's request that provides default settings, url mapping, and + * response handling. + */ +export default function request(url, config) { + // Load default fetch configuration and remove any falsy query parameters + const requestConfig = Object.assign({}, DEFAULT_REQUEST_CONFIG, config, config && { + query: config.query && sanitize(config.query) + }); - return response - .text() - .then((resText) => { - const resJson = JSON.parse(resText); - err = new Error(resJson.errors.pop()); + // Add CSRF token + Object.assign(requestConfig.headers, makeCsrfHeader()); - // ES6 promises don't have a .finally() clause so we fake that here by - // forcing the .catch() clause to run - return Promise.reject(); - }) - // If parsing the resText throws, just rethrow the original error we created - .catch(() => { throw err; }); - } + // Resolve url and send request + return new Promise((resolve) => { + resolve(resolveUrl(url)); + }) + .then((apiUrl) => ( + baseRequest(apiUrl, requestConfig) + // Catch any errors resulting from baseRequest first + .catch((res) => { + if (res == null) { + throw new Error(`For: ${apiUrl} - Server did not respond to the request. ` + + '(Not even displayed a 500)'); + } else { + let err = new Error(`${res.status} - ${res.statusText} - on URL: ${res.url}`); - return Q.Promise((resolve, reject) => { - response.text() - .then((responseText) => { - // If the responses' body does not contain any data, - // fetch will resolve responseText to the string 'None'. - // If this is the case, we can not try to parse it as JSON. - if (responseText && responseText !== 'None') { - const body = JSON.parse(responseText); + // Try to parse the response body to see if we added more descriptive errors + // before rejecting with the error above. + return res + .json() + .then((body) => { + if (body && Array.isArray(body.errors) && body.errors.length) { + err = new Error(body.errors.pop()); + } - if (body && body.errors) { - const error = new Error('Form Error'); + // ES6 promises don't have a .finally() clause so we fake that here + // by forcing the .catch() clause to run + return Promise.reject(); + }) + // If parsing the response body throws, just rethrow the original error + .catch(() => { throw err; }); + } + }) + // Handle successful requests + .then((res) => res + .json() + .then((body) => { + if (body) { + let error; + + if (body.errors) { + error = new Error('Form Error'); error.json = body; - reject(error); - } else if (body && body.detail) { - reject(new Error(body.detail)); - } else if (body && 'success' in body && !body.success) { - const error = new Error('Client Request Error'); - error.json = { - body: body, - status: response.status, - statusText: response.statusText, - type: response.type, - url: response.url - }; - reject(error); - } else { - resolve(body); + } else if (body.detail) { + error = Error(body.detail); + } else if ('success' in body && !body.success) { + const { status, statusText, type, url: resUrl } = res; + + error = new Error('Client Request Error'); + error.json = { body, status, statusText, type, url: resUrl }; + } + + if (error) { + throw error; + } else { + return body; } - } else if (response.status >= 400) { - reject(new Error(`${response.status} - ${response.statusText} - on URL: ${response.url}`)); } else { - resolve({}); + return {}; } - }).catch(reject); - }); - }; - } - - getUrl(url) { - // Handle case, that the url string is not defined at all - if (!url) { - throw new Error('Url was not defined and could therefore not be mapped.'); - } - - let name = url; - if (!url.match(/^http/)) { - url = this.urlMap[url]; - if (!url) { - throw new Error(`Cannot find a mapping for "${name}"`); - } - } - return url; - } - - prepareUrl(url, params, attachParamsToQuery) { - let newUrl; - let re = /\${(\w+)}/g; - - // catch errors and throw them to react - try { - newUrl = this.getUrl(url); - } catch(err) { + }) + ) + )) + // Log any errors and rethrow + .catch((err) => { + console.error(err); throw err; - } - - newUrl = newUrl.replace(re, (match, key) => { - let val = params[key]; - if (!val) { - throw new Error(`Cannot find param ${key}`); - } - delete params[key]; - return val; }); - - if (attachParamsToQuery && params && Object.keys(params).length > 0) { - newUrl += stringifyAsQueryParam(params); - } - - return newUrl; - } - - request(verb, url, options = {}) { - let merged = Object.assign({}, this.httpOptions, options); - let csrftoken = getCookie(AppConstants.csrftoken); - if (csrftoken) { - merged.headers['X-CSRFToken'] = csrftoken; - } - merged.method = verb; - return fetch(url, merged) - .then(this.unpackResponse(url)); - } - - get(url, params) { - if (url === undefined) { - throw new Error('Url undefined'); - } - let paramsCopy = Object.assign({}, params); - let newUrl = this.prepareUrl(url, paramsCopy, true); - return this.request('get', newUrl); - } - - delete(url, params) { - let paramsCopy = Object.assign({}, params); - let newUrl = this.prepareUrl(url, paramsCopy, true); - return this.request('delete', newUrl); - } - - _putOrPost(url, paramsAndBody, method) { - let params = omitFromObject(paramsAndBody, ['body']); - let newUrl = this.prepareUrl(url, params); - let body = paramsAndBody && paramsAndBody.body ? JSON.stringify(paramsAndBody.body) - : null; - return this.request(method, newUrl, { body }); - } - - post(url, params) { - return this._putOrPost(url, params, 'post'); - } - - put(url, params) { - return this._putOrPost(url, params, 'put'); - } - - patch(url, params) { - return this._putOrPost(url, params, 'patch'); - } - - defaults(options) { - this.httpOptions = options.http || {}; - this.urlMap = options.urlMap || {}; - } } - - -let requests = new Requests(); - -export default requests;