/** * @flow */ import _ from 'lodash'; import ExtendableError from 'es6-error'; import QueryString from 'querystring'; import axios from 'axios'; import idx from 'idx'; import Config from './Config'; import logger from './Logger'; import type { User } from './User'; let ROOT_BASE_URL = `${Config.api.scheme}://${Config.api.host}`; if (Config.api.port) { ROOT_BASE_URL += ':' + Config.api.port; } const API_BASE_URL = ROOT_BASE_URL + '/--/api/v2'; export class ApiV2Error extends ExtendableError { code: string; details: Object; serverStack: ?string; constructor(message: string, code: string = 'UNKNOWN') { super(message); this.code = code; this._isApiError = true; } } type RequestOptions = { httpMethod: 'get' | 'post' | 'put', queryParameters?: ?QueryParameters, body?: ?Object, json?: boolean, }; type QueryParameters = { [key: string]: ?(string | number | boolean) }; type ErrorWithResponseBody = Error & { responseBody?: any, }; type APIV2ClientOptions = { idToken?: string, accessToken?: string, }; export default class ApiV2Client { idToken: ?string = null; accessToken: ?string = null; static clientForUser(user: ?User): ApiV2Client { if (user) { return new ApiV2Client({ accessToken: user.accessToken, idToken: user.idToken, }); } return new ApiV2Client(); } constructor(options: APIV2ClientOptions = {}) { if (options.idToken) { this.idToken = options.idToken; } if (options.accessToken) { this.accessToken = options.accessToken; } } async getAsync( methodName: string, args: QueryParameters = {}, extraOptions: Object = {} ): Promise<*> { return this._requestAsync( methodName, { httpMethod: 'get', queryParameters: args, json: true, }, extraOptions ); } async postAsync(methodName: string, data: Object = {}, extraOptions: Object = {}): Promise<*> { return this._requestAsync( methodName, { httpMethod: 'post', body: data, }, extraOptions ); } async putAsync(methodName: string, data: Object = {}, extraOptions: Object = {}): Promise<*> { return this._requestAsync( methodName, { httpMethod: 'put', body: data, }, extraOptions ); } async _requestAsync( methodName: string, options: RequestOptions, extraRequestOptions: Object ): Promise<*> { const url = `${API_BASE_URL}/${methodName}`; let reqOptions: Object = { url, method: options.httpMethod, headers: { 'Exponent-Client': 'xdl', }, json: typeof options.json !== 'undefined' ? options.json : false, }; if (this.idToken) { reqOptions.headers['Authorization'] = `Bearer ${this.idToken}`; } if (this.accessToken) { reqOptions.headers['Exp-Access-Token'] = this.accessToken; } // Handle qs if (options.queryParameters) { reqOptions.params = options.queryParameters; reqOptions.paramsSerializer = QueryString.stringify; } // Handle body if (options.body) { reqOptions.data = options.body; } reqOptions = _.merge({}, reqOptions, extraRequestOptions); let response; let result; try { response = await axios.request(reqOptions); result = response.data; } catch (e) { // axios errors on 400's, pass this case to better error handling downstream const maybeErrorData = idx(e, _ => _.response.data.errors.length); if (maybeErrorData) { result = e.response.data; } else { const error: ErrorWithResponseBody = new Error( `There was a problem understanding the server. Please try again.` ); error.responseBody = result; logger.error(error); throw error; } } if (!result || typeof result !== 'object') { let error: ErrorWithResponseBody = new Error(`There was a problem understanding the server.`); error.responseBody = result; throw error; } if (result.errors && result.errors.length) { let responseError = result.errors[0]; let error = new ApiV2Error(responseError.message, responseError.code); error.serverStack = responseError.stack; error.details = responseError.details; throw error; } return result.data; } }