| 
									
										
										
										
											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'; | 
					
						
							| 
									
										
										
										
											2020-08-26 15:44:50 -07:00
										 |  |  | import getCSRFToken from '@datahub/utils/helpers/csrf-token'; | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2020-08-26 15:44:50 -07:00
										 |  |  |  * @param {IFetchConfig.headers} headers additional headers to add onto the request | 
					
						
							| 
									
										
										
										
											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: { | 
					
						
							| 
									
										
										
										
											2020-08-26 15:44:50 -07:00
										 |  |  |     // https://github.com/playframework/playframework/blob/9e9e26f40a4941fa306116471694d67040331b28/web/play-filters-helpers/src/main/scala/play/filters/csrf/csrf.scala#L99
 | 
					
						
							|  |  |  |     Accept: '*/*', | 
					
						
							|  |  |  |     'Content-Type': 'application/json', // Content-Type is required to prevent a unsupported media type exception
 | 
					
						
							|  |  |  |     'Csrf-Token': getCSRFToken(), | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     ...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 | 
					
						
							| 
									
										
										
										
											2020-08-26 15:44:50 -07:00
										 |  |  |  * @param {IFetchOptions} fetchConfig A configuration object with optional attributes for the Fetch object | 
					
						
							| 
									
										
										
										
											2017-10-18 17:38:51 -07:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2020-08-26 15:44:50 -07:00
										 |  |  | const json = <T>(url = '', fetchConfig: IFetchOptions = {}): Promise<T> => | 
					
						
							|  |  |  |   fetch(url, fetchConfig).then<T>( | 
					
						
							|  |  |  |     (response: Response): Promise<T> => throwIfApiError<T>(response, (response): Promise<T> => 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); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-09 12:17:51 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Conveniently makes a simple get request | 
					
						
							|  |  |  |  * @template T | 
					
						
							|  |  |  |  * @param {IFetchConfig} config | 
					
						
							|  |  |  |  * @return {Promise<T>} | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | export const getRequest = (config: IFetchConfig): Promise<Response> => { | 
					
						
							|  |  |  |   const fetchConfig = { | 
					
						
							|  |  |  |     ...withBaseFetchHeaders(config.headers), | 
					
						
							|  |  |  |     method: 'GET' | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return fetch(config.url, fetchConfig); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-18 17:38:51 -07:00
										 |  |  | /** | 
					
						
							| 
									
										
										
										
											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 => { | 
					
						
							| 
									
										
										
										
											2019-09-04 21:46:02 -07:00
										 |  |  |   // @ts-ignore https://github.com/typed-ember/ember-cli-typescript/issues/799
 | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |   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 | 
					
						
							| 
									
										
										
										
											2020-08-26 15:44:50 -07:00
										 |  |  |     .filter((arg): boolean => typeOf(arg) !== 'undefined') | 
					
						
							|  |  |  |     .map((arg): string => argToString(arg)) | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     .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>) => { | 
					
						
							| 
									
										
										
										
											2020-08-26 15:44:50 -07:00
										 |  |  |   const cachedResult: Record<string, R> = {}; | 
					
						
							|  |  |  |   const promises: Record<string, Promise<R>> = {}; | 
					
						
							|  |  |  |   return async (...args: Array<T>): Promise<R> => { | 
					
						
							| 
									
										
										
										
											2019-08-31 20:51:14 -07:00
										 |  |  |     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]; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | }; |