UNPKG

@gooddata/gooddata-js

Version:
379 lines (378 loc) • 18.1 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); // (C) 2007-2020 GoodData Corporation var isPlainObject_1 = __importDefault(require("lodash/isPlainObject")); var isFunction_1 = __importDefault(require("lodash/isFunction")); var set_1 = __importDefault(require("lodash/set")); var defaults_1 = __importDefault(require("lodash/defaults")); var merge_1 = __importDefault(require("lodash/merge")); var result_1 = __importDefault(require("lodash/result")); var util_1 = require("./util"); /** * Ajax wrapper around GDC authentication mechanisms, SST and TT token handling and polling. * Interface is the same as original jQuery.ajax. * * If token is expired, current request is "paused", token is refreshed and request is retried and result * is transparently returned to the original call. * * Additionally polling is handled. Only final result of polling returned. * @module xhr * @class xhr */ var DEFAULT_POLL_DELAY = 1000; var REST_API_VERSION_HEADER = "X-GDC-VERSION"; var REST_API_DEPRECATED_VERSION_HEADER = "X-GDC-DEPRECATED"; // The version used in X-GDC-VERSION header (see https://confluence.intgdc.com/display/Development/REST+API+versioning) var LATEST_REST_API_VERSION = 5; function simulateBeforeSend(url, settings) { var xhrMockInBeforeSend = { setRequestHeader: function (key, value) { set_1.default(settings, ["headers", key], value); }, }; if (isFunction_1.default(settings.beforeSend)) { settings.beforeSend(xhrMockInBeforeSend, url); } } function enrichSettingWithCustomDomain(originalUrl, originalSettings, domain) { var url = originalUrl; var settings = originalSettings; if (domain) { // protect url to be prepended with domain on retry if (originalUrl.indexOf(domain) === -1) { url = domain + originalUrl; } settings.mode = "cors"; settings.credentials = "include"; } return { url: url, settings: settings }; } function handlePolling(url, settings, sendRequest) { var pollingDelay = result_1.default(settings, "pollDelay"); return new Promise(function (resolve, reject) { setTimeout(function () { sendRequest(url, settings).then(resolve, reject); }, pollingDelay); }); } exports.handlePolling = handlePolling; function originPackageHeaders(_a) { var name = _a.name, version = _a.version; return { "X-GDC-JS-PKG": name, "X-GDC-JS-PKG-VERSION": version, }; } exports.originPackageHeaders = originPackageHeaders; var ApiError = /** @class */ (function (_super) { __extends(ApiError, _super); function ApiError(message, cause) { var _this = _super.call(this, message) || this; _this.cause = cause; return _this; } return ApiError; }(Error)); exports.ApiError = ApiError; var ApiResponseError = /** @class */ (function (_super) { __extends(ApiResponseError, _super); function ApiResponseError(message, response, responseBody) { var _this = _super.call(this, message, null) || this; _this.response = response; _this.responseBody = responseBody; return _this; } return ApiResponseError; }(ApiError)); exports.ApiResponseError = ApiResponseError; var ApiNetworkError = /** @class */ (function (_super) { __extends(ApiNetworkError, _super); function ApiNetworkError() { return _super !== null && _super.apply(this, arguments) || this; } return ApiNetworkError; }(ApiError)); exports.ApiNetworkError = ApiNetworkError; var ApiResponse = /** @class */ (function () { function ApiResponse(response, responseBody) { this.response = response; this.responseBody = responseBody; } Object.defineProperty(ApiResponse.prototype, "data", { get: function () { try { return JSON.parse(this.responseBody); } catch (error) { return this.responseBody; } }, enumerable: true, configurable: true }); ApiResponse.prototype.getData = function () { try { return JSON.parse(this.responseBody); } catch (error) { return this.responseBody; } }; ApiResponse.prototype.getHeaders = function () { return this.response; }; return ApiResponse; }()); exports.ApiResponse = ApiResponse; // the variable must be outside of the scope of the XhrModule to not log the message multiple times in SDK and KD var shouldLogDeprecatedRestApiCall = true; var XhrModule = /** @class */ (function () { function XhrModule(fetch, configStorage) { this.fetch = fetch; this.configStorage = configStorage; defaults_1.default(configStorage, { xhrSettings: {} }); } /** * Back compatible method for setting common XHR settings * * Usually in our apps we used beforeSend ajax callback to set the X-GDC-REQUEST header with unique ID. * * @param settings object XHR settings as */ XhrModule.prototype.ajaxSetup = function (settings) { Object.assign(this.configStorage.xhrSettings, settings); }; XhrModule.prototype.ajax = function (originalUrl, customSettings) { if (customSettings === void 0) { customSettings = {}; } return __awaiter(this, void 0, void 0, function () { var firstSettings, _a, url, settings, response, e_1, responseBody, finalUrl, finalSettings; return __generator(this, function (_b) { switch (_b.label) { case 0: firstSettings = this.createRequestSettings(customSettings); _a = enrichSettingWithCustomDomain(originalUrl, firstSettings, this.configStorage.domain), url = _a.url, settings = _a.settings; simulateBeforeSend(url, settings); // mutates `settings` param if (this.tokenRequest) { return [2 /*return*/, this.continueAfterTokenRequest(url, settings)]; } _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); return [4 /*yield*/, this.fetch(url, settings)]; case 2: // TODO: We should clean up the settings at this point to be pure `RequestInit` object response = _b.sent(); return [3 /*break*/, 4]; case 3: e_1 = _b.sent(); throw new ApiNetworkError(e_1.message, e_1); // TODO is it really necessary? couldn't we throw just Error? case 4: return [4 /*yield*/, response.text()]; case 5: responseBody = _b.sent(); if (response.status === 401) { // if 401 is in login-request, it means wrong user/password (we wont continue) if (url.indexOf("/gdc/account/login") !== -1) { throw new ApiResponseError("Unauthorized", response, responseBody); } return [2 /*return*/, this.handleUnauthorized(url, settings)]; } // Note: Fetch does redirects automagically for 301 (and maybe more .. TODO when?) // see https://fetch.spec.whatwg.org/#ref-for-concept-request%E2%91%A3%E2%91%A2 if (response.status === 202 && !settings.dontPollOnResult) { finalUrl = response.url || url; finalSettings = settings; // if the response is 202 and Location header is not empty, let's poll on the new Location if (response.headers.has("Location")) { finalUrl = response.headers.get("Location"); } finalSettings.method = "GET"; delete finalSettings.data; delete finalSettings.body; return [2 /*return*/, handlePolling(finalUrl, finalSettings, this.ajax.bind(this))]; } this.verifyRestApiDeprecationStatus(response.headers); if (response.status >= 200 && response.status <= 399) { return [2 /*return*/, new ApiResponse(response, responseBody)]; } // throws on 400, 500, etc. throw new ApiResponseError(response.statusText, response, responseBody); } }); }); }; /** * Wrapper for xhr.ajax method GET */ XhrModule.prototype.get = function (url, settings) { return this.ajax(url, merge_1.default({ method: "GET" }, settings)); }; /** * Wrapper for xhr.ajax method HEAD */ XhrModule.prototype.head = function (url, settings) { return this.ajax(url, merge_1.default({ method: "HEAD" }, settings)); }; /** * Wrapper for xhr.ajax method POST */ XhrModule.prototype.post = function (url, settings) { return this.ajax(url, merge_1.default({ method: "POST" }, settings)); }; /** * Wrapper for xhr.ajax method PUT */ XhrModule.prototype.put = function (url, settings) { return this.ajax(url, merge_1.default({ method: "PUT" }, settings)); }; /** * Wrapper for xhr.ajax method DELETE */ XhrModule.prototype.del = function (url, settings) { return this.ajax(url, merge_1.default({ method: "DELETE" }, settings)); }; XhrModule.prototype.createRequestSettings = function (customSettings) { var _a; var settings = merge_1.default({ headers: __assign((_a = { Accept: "application/json; charset=utf-8", "Content-Type": "application/json" }, _a[REST_API_VERSION_HEADER] = LATEST_REST_API_VERSION, _a), originPackageHeaders(this.configStorage.originPackage || util_1.thisPackage)), }, this.configStorage.xhrSettings, customSettings); settings.pollDelay = settings.pollDelay !== undefined ? settings.pollDelay : DEFAULT_POLL_DELAY; // TODO jquery compat - add to warnings settings.body = settings.data ? settings.data : settings.body; settings.mode = "same-origin"; settings.credentials = "same-origin"; if (isPlainObject_1.default(settings.body)) { settings.body = JSON.stringify(settings.body); } return settings; }; XhrModule.prototype.continueAfterTokenRequest = function (url, settings) { var _this = this; return this.tokenRequest.then(function (response) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (!response.ok) { throw new ApiResponseError("Unauthorized", response, null); } this.tokenRequest = null; return [2 /*return*/, this.ajax(url, settings)]; }); }); }, function (reason) { _this.tokenRequest = null; return reason; }); }; XhrModule.prototype.handleUnauthorized = function (originalUrl, originalSettings) { return __awaiter(this, void 0, void 0, function () { var _a, url, settings, response, responseBody; return __generator(this, function (_b) { switch (_b.label) { case 0: // Create only single token request for any number of waiting request. // If token request exist, just listen for it's end. if (this.tokenRequest) { return [2 /*return*/, this.continueAfterTokenRequest(originalUrl, originalSettings)]; } _a = enrichSettingWithCustomDomain("/gdc/account/token", this.createRequestSettings({}), this.configStorage.domain), url = _a.url, settings = _a.settings; this.tokenRequest = this.fetch(url, settings); return [4 /*yield*/, this.tokenRequest]; case 1: response = _b.sent(); return [4 /*yield*/, response.text()]; case 2: responseBody = _b.sent(); this.tokenRequest = null; // TODO jquery compat - allow to attach unauthorized callback and call it if attached // if ((xhrObj.status === 401) && (isFunction(req.unauthorized))) { // req.unauthorized(xhrObj, textStatus, err, deferred); // return; // } // unauthorized handler is not defined or not http 401 // unauthorized when retrieving token -> not logged if (response.status === 401) { throw new ApiResponseError("Unauthorized", response, responseBody); } return [2 /*return*/, this.ajax(originalUrl, originalSettings)]; } }); }); }; XhrModule.prototype.logDeprecatedRestApiCall = function (deprecatedVersionDetails) { // tslint:disable-next-line:no-console console.warn("The REST API version " + LATEST_REST_API_VERSION + " is deprecated (" + deprecatedVersionDetails + "). " + "Please migrate your application to use GoodData.UI SDK or @gooddata/gooddata-js package that " + "supports newer version of the API."); }; XhrModule.prototype.isRestApiDeprecated = function (responseHeaders) { return responseHeaders.has(REST_API_DEPRECATED_VERSION_HEADER); }; XhrModule.prototype.verifyRestApiDeprecationStatus = function (responseHeaders) { if (shouldLogDeprecatedRestApiCall && this.isRestApiDeprecated(responseHeaders)) { var deprecatedVersionDetails = responseHeaders.get(REST_API_DEPRECATED_VERSION_HEADER); this.logDeprecatedRestApiCall(deprecatedVersionDetails); shouldLogDeprecatedRestApiCall = false; } }; return XhrModule; }()); exports.XhrModule = XhrModule;