UNPKG

@talend/react-cmf

Version:

A framework built on top of best react libraries

373 lines (356 loc) 12.2 kB
import { call, put } from 'redux-saga/effects'; import interceptors from '../httpInterceptors'; import { ACTION_TYPE_HTTP_ERRORS, HTTP_METHODS, HTTP_STATUS, testHTTPCode } from '../middlewares/http/constants'; import { mergeCSRFToken } from '../middlewares/http/csrfHandling'; /** * Storage point for the doc setup using `setDefaultConfig` */ import { curry, get, merge } from "lodash"; export const HTTP = { defaultConfig: null }; /** * merge the CSRFToken handling rule from the module defaultConfig * into config argument * @param {Object} config * @returns {Function} */ export function handleCSRFToken(config) { return mergeCSRFToken({ security: config.security })(config); } export class HTTPError extends Error { constructor({ data, response }) { super(response.statusText); this.name = `HTTP ${response.status}`; this.data = data; this.response = response; } } /** * handleHttpResponse - handle the http body * * @param {Response} response A response object * @return {Promise} A promise that resolves with the result of parsing the body */ export function handleBody(response, { method } = {}) { if (response.status === HTTP_STATUS.NO_CONTENT || method === HTTP_METHODS.HEAD) { return Promise.resolve({ data: '', response }); } let methodBody = 'text'; const headers = get(response, 'headers', new Headers()); const contentType = headers.get('Content-Type'); if (contentType && contentType.includes('application/json')) { methodBody = 'json'; } if (contentType && contentType.includes('application/zip')) { methodBody = 'blob'; } return response[methodBody]().then(data => ({ data, response })); } /** * handleHttpResponse - handle the http error * * @param {Response} response A response object * @return {Promise} A promise that reject with the result of parsing the body */ export function handleError(response, request = {}) { // in case of network issue if (response instanceof Error) { return new HTTPError({ response, data: response }); } return handleBody(response, request).then(body => new HTTPError(body)); } /** * handleHttpResponse - handle the http response * * @param {Response} response A response object * @return {Promise} A promise that: * - resolves with the result of parsing the body * - reject the response */ export function handleHttpResponse(response, request = {}) { if (!testHTTPCode.isSuccess(response.status)) { return Promise.reject(response); } return handleBody(response, request); } /** * encodePayload - encore the payload if necessary * * @param {object} headers request headers * @param {object} payload payload to send with the request * @return {string|FormData} The encoded payload. */ export function encodePayload(headers, payload) { const type = headers['Content-Type']; if (payload instanceof FormData || typeof payload === 'string') { return payload; } else if (type && type.includes('json')) { return JSON.stringify(payload); } return payload; } /** * httpFetch - call the api fetch to request the url * * @param {string} url url to request * @param {object} config option that you want apply to the request * @param {string} method = HTTP_METHODS.GET method to apply * @param {object} payload payload to send with the request * @return {Promise} A Promise that resolves to a Response object. */ export function httpFetch(url, config, method, payload) { const defaultHeaders = { Accept: 'application/json', 'Content-Type': 'application/json' }; /** * If the playload is an instance of FormData the body should be set to this object * and the Content-type header should be stripped since the browser * have to build a special headers with file boundary in if said FormData is used to upload file */ if (payload instanceof FormData) { delete defaultHeaders['Content-Type']; } const params = merge({ credentials: 'include', headers: defaultHeaders, method }, { ...HTTP.defaultConfig, ...config }); return fetch(url, handleCSRFToken({ ...params, body: encodePayload(params.headers, payload) })).then(response => handleHttpResponse(response, params)).catch(response => handleError(response, params)); } /** * function - wrap the fetch request with the actions errors * * @param {string} url url to request * @param {object} config option that you want apply to the request * @param {string} method = HTTP_METHODS.GET method to apply * @param {object} payload payload to send with the request * @param {object} options options to deal with cmf automatically * @return {object} the response of the request */ export function* wrapFetch(url, config, method = HTTP_METHODS.GET, payload, options) { const newConfig = yield call(interceptors.onRequest, { url, method, payload, ...config }); const answer = yield call(httpFetch, newConfig.url, newConfig, newConfig.method, newConfig.payload); yield call(interceptors.onResponse, answer); const silent = get(options, 'silent'); if (silent !== true && answer instanceof Error) { yield put({ error: { // allow RFC-7807 compliance ...get(answer, 'data', {}), // legacy properties message: get(answer, 'data.message'), stack: { status: get(answer, 'response.status') } }, url, config, method, payload, options, type: ACTION_TYPE_HTTP_ERRORS }); } return answer; } /** * function - fetch a url with POST method * * @param {string} url url to request * @param {object} payload payload to send with the request * @param {object} config option that you want apply to the request * @param {object} options options to deal with cmf automatically * @example * import { sagas } from '@talend/react-cmf'; * import { call } from 'redux-saga/effects' * yield call(sagas.http.post, '/foo', {foo: 42}); */ export function* httpPost(url, payload, config, options) { return yield* wrapFetch(url, config, HTTP_METHODS.POST, payload, options); } /** * function - fetch a url with PATCH method * * @param {string} url url to request * @param {object} payload payload to send with the request * @param {object} config option that you want apply to the request * @param {object} options options to deal with cmf automatically * @example * import { sagas } from '@talend/react-cmf'; * import { call } from 'redux-saga/effects' * yield call(sagas.http.patch, '/foo', {foo: 42}); */ export function* httpPatch(url, payload, config, options) { return yield* wrapFetch(url, config, HTTP_METHODS.PATCH, payload, options); } /** * function - fetch a url with PUT method * * @param {string} url url to request * @param {object} payload payload to send with the request * @param {object} config option that you want apply to the request * @param {object} options options to deal with cmf automatically * @example * import { sagas } from '@talend/react-cmf'; * import { call } from 'redux-saga/effects' * yield call(sagas.http.put, '/foo', {foo: 42}); */ export function* httpPut(url, payload, config, options) { return yield* wrapFetch(url, config, HTTP_METHODS.PUT, payload, options); } /** * function - fetch a url with DELETE method * * @param {string} url url to request * @param {object} config option that you want apply to the request * @param {object} options options to deal with cmf automatically * @example * import { sagas } from '@talend/react-cmf'; * import { call } from 'redux-saga/effects' * yield call(sagas.http.delete, '/foo'); */ export function* httpDelete(url, config, options) { return yield* wrapFetch(url, config, HTTP_METHODS.DELETE, undefined, options); } /** * function - fetch a url with GET method * * @param {string} url url to request * @param {object} config option that you want apply to the request * @param {object} options options to deal with cmf automatically * @example * import { sagas } from '@talend/react-cmf'; * import { call } from 'redux-saga/effects' * yield call(sagas.http.get, '/foo'); */ export function* httpGet(url, config, options) { return yield* wrapFetch(url, config, HTTP_METHODS.GET, undefined, options); } /** * function - fetch a url with GET method * * @param {string} url url to request * @param {object} config option that you want apply to the request * @param {object} options options to deal with cmf automatically * @example * import { sagas } from '@talend/react-cmf'; * import { call } from 'redux-saga/effects' * yield call(sagas.http.get, '/foo'); */ export function* httpHead(url, config, options) { return yield* wrapFetch(url, config, HTTP_METHODS.HEAD, undefined, options); } /** * setDefaultHeader - define a default config to use with the saga http * this default config is stored in this module for the whole application * * @param {object} config key/value of header to apply * @example * import { setDefaultConfig } from '@talend/react-cmf/sagas/http'; * setDefaultConfig({headers: { * 'Accept-Language': preferredLanguage, * }}); */ export function setDefaultConfig(config) { if (HTTP.defaultConfig) { throw new Error('ERROR: setDefaultConfig should not be called twice, if you wish to change the language use setDefaultLanguage api.'); } HTTP.defaultConfig = config; } /** * To change only the Accept-Language default headers * on the global http defaultConfig * @param {String} language */ export function setDefaultLanguage(language) { if (get(HTTP, 'defaultConfig.headers')) { HTTP.defaultConfig.headers['Accept-Language'] = language; } else { // eslint-disable-next-line no-console throw new Error('ERROR: you should call setDefaultConfig.'); } } export const handleDefaultHttpConfiguration = curry((defaultHttpConfig, httpConfig) => /** * Wall of explain * merge mutate your object see https://lodash.com/docs/4.17.10#merge little note at the * end of the documentation, so why ? don't know but its bad. * * so defaultHttpConfig was mutated inside the curried function and applied to * all other call providing httpConfig, leading to interesting bug like having one time * httpConfig override merged into defaultHttConfig. * a test with two sccessive call will detect this issue. */ merge({}, defaultHttpConfig, httpConfig)); /** * getDefaultConfig - return the defaultConfig * * @return {object} the defaultConfig used by cmf */ export function getDefaultConfig() { return HTTP.defaultConfig; } export default { delete: httpDelete, get: httpGet, head: httpHead, post: httpPost, put: httpPut, patch: httpPatch, setDefaultConfig, setDefaultLanguage, getDefaultConfig, create(createConfig = {}) { const configEnhancer = handleDefaultHttpConfiguration(createConfig); return { delete: function* configuredDelete(url, config = {}, options = {}) { return yield call(httpDelete, url, configEnhancer(config), options); }, get: function* configuredGet(url, config = {}, options = {}) { return yield call(httpGet, url, configEnhancer(config), options); }, post: function* configuredPost(url, payload, config = {}, options = {}) { return yield call(httpPost, url, payload, configEnhancer(config), options); }, put: function* configuredPut(url, payload, config = {}, options = {}) { return yield call(httpPut, url, payload, configEnhancer(config), options); }, patch: function* configuredPatch(url, payload, config = {}, options = {}) { return yield call(httpPatch, url, payload, configEnhancer(config), options); }, head: function* configuredPatch(url, config = {}, options = {}) { return yield call(httpHead, url, configEnhancer(config), options); } }; } }; //# sourceMappingURL=http.js.map