2017-09-19 13:32:04 -07:00
|
|
|
import fetch from 'fetch';
|
2019-08-31 20:51:14 -07:00
|
|
|
import { apiErrorStatusMessage, throwIfApiError, ApiError } from '@datahub/utils/api/error';
|
|
|
|
import { isNotFoundApiError } from '@datahub/utils/api/shared';
|
|
|
|
import { IFetchConfig, IFetchOptions } from '@datahub/utils/types/api/fetcher';
|
|
|
|
import { typeOf } from '@ember/utils';
|
2017-10-18 17:38:51 -07:00
|
|
|
/**
|
2017-11-27 00:51:20 -08:00
|
|
|
* Augments the user supplied headers with the default accept and content-type headers
|
2018-07-19 16:18:06 -07:00
|
|
|
* @param {IFetchConfig.headers} headers
|
2017-10-18 17:38:51 -07:00
|
|
|
*/
|
2018-07-19 16:18:06 -07:00
|
|
|
const withBaseFetchHeaders = (headers: IFetchConfig['headers']): { headers: IFetchConfig['headers'] } => ({
|
2019-08-31 20:51:14 -07:00
|
|
|
headers: {
|
|
|
|
Accept: 'application/json',
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
...headers
|
|
|
|
}
|
2017-10-18 17:38:51 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
2017-11-27 00:51:20 -08:00
|
|
|
* Sends a HTTP request and resolves with the JSON response
|
|
|
|
* @template T
|
|
|
|
* @param {string} url the url for the endpoint to request a response from
|
2018-02-21 14:41:05 -08:00
|
|
|
* @param {object} fetchConfig
|
|
|
|
* @returns {Promise<T>}
|
2017-10-18 17:38:51 -07:00
|
|
|
*/
|
2018-01-18 18:52:42 -08:00
|
|
|
const json = <T>(url: string = '', fetchConfig: IFetchOptions = {}): Promise<T> =>
|
2019-08-31 20:51:14 -07:00
|
|
|
fetch(url, fetchConfig).then<T>(response => throwIfApiError(response, response => response.json()));
|
2017-10-18 17:38:51 -07:00
|
|
|
|
2017-09-19 13:32:04 -07:00
|
|
|
/**
|
|
|
|
* Conveniently gets a JSON response using the fetch api
|
2017-11-27 00:51:20 -08:00
|
|
|
* @template T
|
2018-07-19 16:18:06 -07:00
|
|
|
* @param {IFetchConfig} config
|
2017-09-19 13:32:04 -07:00
|
|
|
* @return {Promise<T>}
|
|
|
|
*/
|
2019-08-31 20:51:14 -07:00
|
|
|
export const getJSON = <T>(config: IFetchConfig): Promise<T> => {
|
|
|
|
const fetchConfig = {
|
|
|
|
...withBaseFetchHeaders(config.headers),
|
|
|
|
method: 'GET'
|
|
|
|
};
|
2017-10-18 17:38:51 -07:00
|
|
|
|
|
|
|
return json<T>(config.url, fetchConfig);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-11-27 00:51:20 -08:00
|
|
|
* Initiates a POST request using the Fetch api
|
|
|
|
* @template T
|
2018-07-19 16:18:06 -07:00
|
|
|
* @param {IFetchConfig} config
|
2018-02-21 14:41:05 -08:00
|
|
|
* @returns {Promise<T>}
|
2017-10-18 17:38:51 -07:00
|
|
|
*/
|
2019-08-31 20:51:14 -07:00
|
|
|
export const postJSON = <T>(config: IFetchConfig): Promise<T> => {
|
2017-11-30 10:33:07 -08:00
|
|
|
const requestBody = config.data ? { body: JSON.stringify(config.data) } : {};
|
2017-10-18 17:38:51 -07:00
|
|
|
const fetchConfig = Object.assign(
|
2017-11-30 10:33:07 -08:00
|
|
|
requestBody,
|
2017-10-18 17:38:51 -07:00
|
|
|
config.data && { body: JSON.stringify(config.data) },
|
2018-01-18 15:21:34 -08:00
|
|
|
withBaseFetchHeaders(config.headers),
|
2017-10-18 17:38:51 -07:00
|
|
|
{ method: 'POST' }
|
|
|
|
);
|
|
|
|
return json<T>(config.url, fetchConfig);
|
|
|
|
};
|
|
|
|
|
2017-11-27 00:51:20 -08:00
|
|
|
/**
|
|
|
|
* Initiates a DELETE request using the Fetch api
|
|
|
|
* @template T
|
2018-07-19 16:18:06 -07:00
|
|
|
* @param {IFetchConfig} config
|
2017-11-27 00:51:20 -08:00
|
|
|
* @return {Promise<T>}
|
|
|
|
*/
|
2019-08-31 20:51:14 -07:00
|
|
|
export const deleteJSON = <T>(config: IFetchConfig): Promise<T> => {
|
2017-11-30 10:33:07 -08:00
|
|
|
const requestBody = config.data ? { body: JSON.stringify(config.data) } : {};
|
2018-01-18 15:21:34 -08:00
|
|
|
const fetchConfig = Object.assign(requestBody, withBaseFetchHeaders(config.headers), { method: 'DELETE' });
|
2017-10-18 17:38:51 -07:00
|
|
|
|
|
|
|
return json<T>(config.url, fetchConfig);
|
|
|
|
};
|
|
|
|
|
2017-11-27 00:51:20 -08:00
|
|
|
/**
|
|
|
|
* Initiates a PUT request using the Fetch api
|
|
|
|
* @template T
|
2018-07-19 16:18:06 -07:00
|
|
|
* @param {IFetchConfig} config
|
2017-11-27 00:51:20 -08:00
|
|
|
* @return {Promise<T>}
|
|
|
|
*/
|
2019-08-31 20:51:14 -07:00
|
|
|
export const putJSON = <T>(config: IFetchConfig): Promise<T> => {
|
2017-11-30 10:33:07 -08:00
|
|
|
const requestBody = config.data ? { body: JSON.stringify(config.data) } : {};
|
|
|
|
|
2018-01-18 15:21:34 -08:00
|
|
|
const fetchConfig = Object.assign(requestBody, withBaseFetchHeaders(config.headers), { method: 'PUT' });
|
2017-09-19 13:32:04 -07:00
|
|
|
|
2017-10-18 17:38:51 -07:00
|
|
|
return json<T>(config.url, fetchConfig);
|
2017-09-19 13:32:04 -07:00
|
|
|
};
|
|
|
|
|
2017-09-20 14:25:27 -07:00
|
|
|
/**
|
|
|
|
* Requests the headers from a resource endpoint
|
2018-07-19 16:18:06 -07:00
|
|
|
* @param {IFetchConfig} config
|
2017-11-27 00:51:20 -08:00
|
|
|
* @return {Promise<Headers>}
|
2017-09-20 14:25:27 -07:00
|
|
|
*/
|
2019-08-31 20:51:14 -07:00
|
|
|
export const getHeaders = async (config: IFetchConfig): Promise<Headers> => {
|
2017-09-20 14:25:27 -07:00
|
|
|
const fetchConfig = {
|
2018-01-18 15:21:34 -08:00
|
|
|
...withBaseFetchHeaders(config.headers),
|
2017-10-18 17:38:51 -07:00
|
|
|
method: 'HEAD'
|
2017-09-20 14:25:27 -07:00
|
|
|
};
|
2018-02-28 16:31:37 -08:00
|
|
|
const response = await fetch(config.url, fetchConfig);
|
|
|
|
const { ok, headers, status } = response;
|
2017-09-20 14:25:27 -07:00
|
|
|
|
|
|
|
if (ok) {
|
2017-09-20 16:10:52 -07:00
|
|
|
return headers;
|
2017-09-20 14:25:27 -07:00
|
|
|
}
|
|
|
|
|
2018-02-28 16:31:37 -08:00
|
|
|
throw new ApiError(status, apiErrorStatusMessage(status));
|
2017-09-20 14:25:27 -07:00
|
|
|
};
|
|
|
|
|
2018-07-19 16:18:06 -07:00
|
|
|
/**
|
|
|
|
* Wraps an api request or Promise that resolves a value, if the promise rejects with an
|
|
|
|
* @link ApiError and
|
|
|
|
* @link ApiResponseStatus.NotFound
|
|
|
|
* then the default value is returned then resolve with the default value
|
|
|
|
* @param {Promise<T>} request the request or promise to wrap
|
|
|
|
* @param {T} defaultValue resolved value if request throws ApiResponseStatus.NotFound
|
|
|
|
* @return {Promise<T>}
|
|
|
|
*/
|
2019-08-31 20:51:14 -07:00
|
|
|
export const returnDefaultIfNotFound = async <T>(request: Promise<T>, defaultValue: T): Promise<T> => {
|
2018-07-19 16:18:06 -07:00
|
|
|
try {
|
|
|
|
return await request;
|
|
|
|
} catch (e) {
|
|
|
|
if (isNotFoundApiError(e)) {
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-08-31 20:51:14 -07:00
|
|
|
/**
|
|
|
|
* Helper function to convert any object or type into a string that is not [Object object]
|
|
|
|
* @param arg
|
|
|
|
*/
|
|
|
|
const argToString = (arg: unknown): string => {
|
|
|
|
// @ts-ignore
|
|
|
|
if (typeOf(arg) === 'object') {
|
|
|
|
return JSON.stringify(arg);
|
|
|
|
} else {
|
|
|
|
return `${arg}`;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper fn to convert arguments of a fn to a string so we can use it as a key for
|
|
|
|
* the cache in cacheApi
|
|
|
|
* @param args
|
|
|
|
*/
|
|
|
|
const argsToKey = (args: Array<unknown>): string =>
|
|
|
|
args
|
|
|
|
.filter(arg => typeOf(arg) !== 'undefined')
|
|
|
|
.map(arg => argToString(arg))
|
|
|
|
.join('.');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Workaround to enable or disable cache during tests. See CacheEnabler instance-initializer
|
|
|
|
*/
|
|
|
|
let CACHE_ENABLED = false;
|
|
|
|
export const setCacheEnabled = (enabled: boolean): void => {
|
|
|
|
CACHE_ENABLED = enabled;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This fn will cache API request, so only 1 request will be made. This is useful for some
|
|
|
|
* configuration APIs where the data is not going to change. This way we make 1 api call
|
|
|
|
* and cache the result in memory.
|
|
|
|
*
|
|
|
|
* @param fn Fn that will call api, returns a promise.
|
|
|
|
*/
|
|
|
|
export const cacheApi = <T, R>(fn: (...args: Array<T>) => Promise<R>): ((...args: Array<T>) => Promise<R>) => {
|
|
|
|
let cachedResult: Record<string, R> = {};
|
|
|
|
let promises: Record<string, Promise<R>> = {};
|
|
|
|
return async (...args: Array<T>) => {
|
|
|
|
const key = argsToKey(args);
|
|
|
|
// We don't want to cache in test
|
|
|
|
if (CACHE_ENABLED) {
|
|
|
|
// if result is already cached, return data
|
|
|
|
if (cachedResult[key]) {
|
|
|
|
return cachedResult[key];
|
|
|
|
}
|
|
|
|
|
|
|
|
// if call is being made, just wait
|
|
|
|
if (promises[key]) {
|
|
|
|
return await promises[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// looks like you are the first one
|
|
|
|
// make the api call and wait for results
|
|
|
|
promises[key] = fn(...args);
|
|
|
|
cachedResult[key] = await promises[key];
|
|
|
|
return cachedResult[key];
|
|
|
|
};
|
|
|
|
};
|