import * as axios from 'axios'; import { WebApiError, ConfigurationError } from '../core/error'; import { WebApiConfiguration } from '../core/web-api-configuration'; import { WebApiContext } from '../core/web-api-context'; import { Signer } from '../core/signer'; /** * @export * @interface CallOptions */ export interface CallOptions { url: string; method?: string; params?: any; data?: any; } /** * @export * @interface Response */ export interface Response { data?: any; status?: number; headers?: any; config?: any; } /** * @param {any=} response * @returns {string} */ function extractErrorMessageFromResponse(response: any): string { var errorMessage = "Error during api call"; if (!response.data) { return errorMessage; } if (!response.data.errors || typeof (response.data.errors) != "object") { return errorMessage; } if (response.data.errors.length < 1 || !response.data.errors[0].error) { return errorMessage; } return errorMessage + ' - ' + response.data.errors[0].error; } /** * @export * @abstract * @class WebApiClient * @implements {PathSegment} */ export abstract class WebApiClient implements PathSegment { public config: WebApiConfiguration; public sharedContext: WebApiContext; private apiBasePath: string; //Hash of all global headers that should be sent public static GlobalRequestHeaders: any = {}; /** * Creates an instance of WebApiClient. * @param {WebApiConfiguration} config * @param {string} basePath * * @memberOf WebApiClient */ public constructor(config: WebApiConfiguration, basePath: string) { //Obtain a copy of config so that we can do modifications to it. this.apiBasePath = basePath; this.config = config; this._ensureRightConfiguration = function () { if (typeof this.config.webApiKey != 'string' || this.config.webApiKey.length == 0) { throw new ConfigurationError("You have to provide WEB-API-KEY to the SDK before calling the API!") } if (typeof this.config.language != 'string' || this.config.language.length == 0) { //FIXME: Better error message when the packaging becomes more stable throw new ConfigurationError("You have to provide language in which to communicate with the API!") } } } /** * Returns Base URL for all resources in this client. * @returns {string} * * @memberOf WebApiClient */ getPath(): string { return this.config.environment.apiContextBaseUrl + this.apiBasePath; } /** * Prepares request for API call * @param {string} url * @param {string} method * @param {any=} params * @param {any=} data * @param {any=} headers * @param {string} [responseType] * @returns {Promise} * * @memberOf WebApiClient */ public callApi( url: string, method: string, params?: any, data?: any, headers?: any, responseType?: string ): Promise { this._ensureRightConfiguration(); headers = headers || {}; // If client has accessTokenProvider try to get access token and then call makeCall method otherwise only call makeCall method if (this.sharedContext) { const accessTokenErrors = [ '"error":"invalid_token"', '"error":"invalid_request"', ]; return this.sharedContext .getAccessToken() .then(token => { // add access token to headers var tokenHeader = `${token.tokenType} ${token.token}` headers['Authorization'] = tokenHeader; // call makeCall method with access token in headers and return the result return this.makeCall(url, method, params, data, headers); }) .catch(e => { // if access token is invalid try to refresh access token if (e.statusCode === 403 && e.response.data.errors && e.response.data.errors[0] && (e.response.data.errors[0].error.indexOf(accessTokenErrors[0]) !== -1 || e.response.data.errors[0].error.indexOf(accessTokenErrors[1]) !== -1) && this.sharedContext.accessTokenProvider) { return this.sharedContext.refreshAccessToken() .then(token => { // add access token to headers headers['Authorization'] = `${token.tokenType} ${token.token}`; // call makeCall method with access token in headers and return the result return this.makeCall(url, method, params, data, headers); }) .catch(e => { // If access token cannot be refreshed or there was any other error then just throw the error throw e; }); } throw e; }); } else { // call makeCall method and return the result return this.makeCall(url, method, params, data, headers, responseType); } } /** * @private * @param {string} url * @param {string} method * @param {any=} params * @param {any=} data * @param {any=} headers * @param {string=} responseType * @returns {any} * * @memberOf WebApiClient */ private makeCall( url: string, method: string, params?: any, data?: any, headers?: any, responseType?: string ): any { this._ensureRightConfiguration(); var signer = null; var requestTransforms: [any] = (axios).defaults.transformRequest.slice(); if (this.config.signingKey) { signer = new Signer(this.config.webApiKey, this.config.signingKey); requestTransforms.push(function (request, headers) { if (signer) { signer.signRequest(url, data, headers); } return request; }); } var reqHeaders = { 'web-api-key': this.config.webApiKey, 'Accept-Language': this.config.language, "Content-Type": "application/json", "accept": "application/json" }; //Add global headers if (WebApiClient.GlobalRequestHeaders) { for (var key in WebApiClient.GlobalRequestHeaders) { reqHeaders[key] = WebApiClient.GlobalRequestHeaders[key]; } } //Add custom headers if (headers) { for (var key in headers) { if (headers.hasOwnProperty(key)) { reqHeaders[key] = headers[key]; } } } var config: axios.RequestOptions = { url: url, method: method, params: params, data: data, headers: reqHeaders, transformRequest: requestTransforms, responseType: responseType ? responseType : 'json' }; var promise = >axios(config); return promise .then(response => { //TODO: Check status codes, emit relevant errors return response.data; }) .catch(err => { if (err.data && (Object.prototype.toString.call(err.data) === '[object Uint8Array]') || Object.prototype.toString.call(err.data) === '[object ArrayBuffer]') { err.data = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(err.data))); } throw new WebApiError(extractErrorMessageFromResponse(err), { "response": err, "statusCode": err.status, "statusText": err.statusText }) }); } private _ensureRightConfiguration: () => void; } /** * @export * @interface PathSegment */ export interface PathSegment { /** * @returns {string} * * @memberOf PathSegment */ getPath(): string }