UNPKG

d2

Version:

Javascript library for DHIS2

478 lines (403 loc) 18.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; require("isomorphic-fetch"); var _check = require("../lib/check"); var _utils = require("../lib/utils"); var _System = _interopRequireDefault(require("../system/System")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function getMergeStrategyParam() { var mergeType = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'REPLACE'; var system = _System.default.getSystem(); if (system.version && Number(system.version.minor) <= 22) { return "mergeStrategy=".concat(mergeType); } return "mergeMode=".concat(mergeType); } function getUrl(baseUrl, url) { // If we are dealing with an absolute url use that instead if (new RegExp('^(:?https?:)?//').test(url)) { return url; } var urlParts = []; if (baseUrl) { urlParts.push(baseUrl); } urlParts.push(url); return urlParts.join('/').replace(new RegExp('(.(?:[^:]))//+', 'g'), '$1/').replace(new RegExp('/$'), ''); } /** * @description * Used for interaction with the dhis2 api. * * This class is used as the backbone for d2 and handles all the interaction with the server. There is a singleton * available to be reused across your applications. The singleton can be grabbed from the d2 instance. The api methods all handle URL-encoding for you, so you can just pass them unencoded strings * * ```js * import { getInstance } from 'd2/lib/d2'; * * getInstance() * .then(d2 => { * const api = d2.Api.getApi() // Returns the Api singleton. * * api.get('resources'); * }); * ``` * * Uses {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API|Fetch} to do network requests. * * @memberof module:api */ var Api = /*#__PURE__*/ function () { /** * @constructor * * @param {Fetch} [fetchImpl] The fetch implementation to use. Can be used to pass a different implementation * similar to the fetch Api. Will default to `window.fetch` in a browser context. */ function Api(fetchImpl) { _classCallCheck(this, Api); // Optionally provide fetch to the constructor so it can be mocked during testing if (typeof fetchImpl === 'function') { this.fetch = fetchImpl.bind(typeof window !== 'undefined' ? window : global); } else if (typeof fetch !== 'undefined') { this.fetch = fetch.bind(typeof window !== 'undefined' ? window : global); } else { throw new Error('Failed to initialise D2 Api: No fetch implementation is available'); } this.baseUrl = '/api'; this.defaultFetchOptions = { mode: 'cors', // requests to different origins fail credentials: 'include', // include cookies with same-origin requests cache: 'default' // See https://fetch.spec.whatwg.org/#concept-request-cache-mode }; this.defaultHeaders = { 'X-Requested-With': 'XMLHttpRequest' }; this.unauthorizedCallback = null; } /** * Used for setting default headers that should be send with every request. * * @example * const api = Api.getApi(); * * api.setDefaultHeaders({ * 'x-requested-with': 'XMLHttpRequest', // Make sure the Api does not redirect when authorization is expired. * }); * * @param {Object.<string, string>} headers Default headers that should be set on every request. */ _createClass(Api, [{ key: "setDefaultHeaders", value: function setDefaultHeaders(headers) { this.defaultHeaders = headers; } /** * When any request encounters a 401 - Unauthorized. This callback is called. * Useful for when you want an session expiration-handler API-wide. * * @param {*} cb - Function to call when any request recieves a 401. Called with the response from the server. */ }, { key: "setUnauthorizedCallback", value: function setUnauthorizedCallback(cb) { if (typeof cb !== 'function') { throw new Error('Callback must be a function.'); } this.unauthorizedCallback = cb; } /** * Performs a GET request. * * @param {string} url The url for the request, should be unencoded. Will return a rejected promise for malformed urls and urls that contain encoded query strings. * @param {*} data Any data that should be sent with the request. For a GET request these are encoded and turned into * query parameters. For POST and PUT requests it becomes the body. * @param {Object.<string, any>} options The request options are passed as options to the fetch request. * These options are passed as the {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} * parameter to the fetch request. * * @returns {Promise.<*>} The response body. */ }, { key: "get", value: function get(url, data, options) { return this.request('GET', getUrl(this.baseUrl, url), data, options); } /* eslint-disable complexity */ /** * Performs a POST request. * * @param {string} url The url for the request * @param {*} data Any data that should be send with the request this becomes the body for the POST request * @param {Object.<string, any>} options The request options are passed as options to the fetch request. * These options are passed as the {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} * parameter to the fetch request. * * @returns {Promise.<*>} The response body. */ }, { key: "post", value: function post(url, data) { var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var requestUrl = getUrl(this.baseUrl, url); var payload = data; // Ensure that headers are defined and are treated without case sensitivity options.headers = new Headers(options.headers || {}); if (data !== undefined) { if (data.constructor.name === 'FormData') { // Ensure that the browser will set the correct Content-Type header for FormData, including boundary options.headers.delete('Content-Type'); payload = data; } else if (options.headers.has('Content-Type') && options.headers.get('Content-Type').toLocaleLowerCase().startsWith('text/')) { payload = String(data); } else { // Send JSON data by default options.headers.set('Content-Type', 'application/json'); payload = JSON.stringify(data); } } return this.request('POST', requestUrl, payload, options); } /** * Performs a DELETE request. * * @param {string} url The url for the request * @param {Object.<string, any>} options The request options are passed as options to the fetch request. * These options are passed as the {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} * parameter to the fetch request. * * @returns {Promise.<*>} The response body. */ /* eslint-enable complexity */ }, { key: "delete", value: function _delete(url, options) { return this.request('DELETE', getUrl(this.baseUrl, url), undefined, options); } /** * Perform a PUT request. * * @param {string} url The url for the request * @param {*} data Any data that should be send with the request. This becomes the body of the PUT request. * @param {boolean} [useMergeStrategy=false] * @param {Object.<string, any>} options The request options are passed as options to the fetch request. * * @returns {Promise.<*>} The response body. */ }, { key: "update", value: function update(url, data) { var useMergeStrategy = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; var payload = data; // Ensure that headers are defined and are treated without case sensitivity var requestOptions = _objectSpread({}, options, { headers: new Headers(options.headers || {}) }); if (data !== undefined) { if (!requestOptions.headers.has('Content-Type') && typeof payload === 'string') { requestOptions.headers.set('Content-Type', 'text/plain'); } else { payload = JSON.stringify(data); } } var urlForUpdate = useMergeStrategy === true ? "".concat(url, "?").concat(getMergeStrategyParam()) : url; return this.request('PUT', getUrl(this.baseUrl, urlForUpdate), payload, requestOptions); } /** * Perform a PATCH request. * * @param {string} url The url for the request * @param {*} data Any data that should be send with the request. This becomes the body of the PATCH request. * * @returns {Promise.<*>} The response body. */ }, { key: "patch", value: function patch(url, data) { return this.request('PATCH', getUrl(this.baseUrl, url), JSON.stringify(data)); } /** * General purpose request function for making http requests. * * The more specific functions like `delete`, `post` and `get`, utilize this function to make the requests. * * @param {string} method The HTTP request method (e.g. POST/GET/PATCH) * @param {string} url The url for the request * @param {*} data Any data that should be send with the request. For a GET request these are turned into * query parameters. For POST and PUT requests it becomes the body. * @param {Object.<string, any>} options The request options are passed as options to the fetch request. * These options are passed as the {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} * parameter to the fetch request. * * @returns {Promise.<*>} The response body. */ /* eslint-disable complexity */ }, { key: "request", value: function request(method, url, data) { var _this = this; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; (0, _check.checkType)(method, 'string', 'Request type'); (0, _check.checkType)(url, 'string', 'Url'); var api = this; var requestUrl = url; var query = ''; if (requestUrl.indexOf('?') !== -1) { query = requestUrl.substr(requestUrl.indexOf('?') + 1); requestUrl = requestUrl.substr(0, requestUrl.indexOf('?')); } // Encode existing query parameters, since tomcat does not accept unencoded brackets. Throw // an error if they're already encoded to prevent double encoding. if (query) { var decodedURL; try { decodedURL = decodeURIComponent(query); } catch (err) { return Promise.reject(new Error('Query parameters in URL are invalid')); } var isEncoded = query !== decodedURL; if (isEncoded) { return Promise.reject(new Error('Cannot process URL-encoded URLs, pass an unencoded URL')); } query = (0, _utils.customEncodeURIComponent)(query); } // Transfer filter properties from the data object to the query string if (data && Array.isArray(data.filter)) { var encodedFilters = data.filter.map(function (filter) { return filter.split(':').map(encodeURIComponent).join(':'); }); query = "".concat(query).concat(query.length ? '&' : '', "filter=").concat(encodedFilters.join('&filter=')); delete data.filter; } // When using the GET method, transform the data object to query parameters if (data && method === 'GET') { Object.keys(data).forEach(function (key) { query = "".concat(query).concat(query.length > 0 ? '&' : '') + "".concat(encodeURIComponent(key), "=").concat(encodeURIComponent(data[key])); }); } function getOptions(defaultHeaders, mergeOptions, requestData) { var resultOptions = Object.assign({}, api.defaultFetchOptions, mergeOptions); var headers = new Headers(mergeOptions.headers || {}); Object.keys(defaultHeaders).filter(function (header) { return !headers.get(header); }).forEach(function (header) { return headers.set(header, defaultHeaders[header]); }); resultOptions.method = method; // Only set content type when there is data to send // GET requests and requests without data do not need a Content-Type header // 0 and false are valid requestData values and therefore should have a content type if (resultOptions.method === 'GET' || !requestData && requestData !== 0 && requestData !== false) { headers.delete('Content-Type'); } else if (requestData) { if (data.constructor.name === 'FormData') { headers.delete('Content-Type'); } else if (!headers.get('Content-Type')) { headers.set('Content-Type', 'application/json'); } resultOptions.body = requestData; } resultOptions.headers = headers; return resultOptions; } if (query.length) { requestUrl = "".concat(requestUrl, "?").concat(query); } var requestOptions = getOptions(this.defaultHeaders, options, data); // If the provided value is valid JSON, return the parsed JSON object. If not, return the raw value as is. function parseResponseData(value) { try { return JSON.parse(value); } catch (e) { return value; } } return new Promise(function (resolve, reject) { // fetch returns a promise that will resolve with any response received from the server // It will be rejected ONLY if no response is received from the server, i.e. because there's no internet _this.fetch(requestUrl, requestOptions).then(function (response) { // If the request failed, response.ok will be false and response.status will be the status code if (response.ok) { response.text().then(function (text) { return resolve(parseResponseData(text)); }); } else { response.text().then(function (text) { var parsedResponseData = parseResponseData(text); if (response.status === 401) { var request = { method: method, url: url, data: data, options: options }; if (_this.unauthorizedCallback) { _this.unauthorizedCallback(request, parsedResponseData); } } if (!process.env || process.env.npm_lifecycle_event !== 'test') { // eslint-disable-next-line console.warn("API request failed with status ".concat(response.status, " ").concat(response.statusText, "\n"), "Request: ".concat(requestOptions.method, " ").concat(requestUrl)); } reject(parsedResponseData); }); } }).catch(function (err) { // It's not usually possible to get much info about the cause of the error programmatically, but // the user can check the browser console for more info if (!process.env || process.env.npm_lifecycle_event !== 'test') { console.error('Server connection error:', err); // eslint-disable-line } reject("Server connection failed for API request: ".concat(requestOptions.method, " ").concat(requestUrl)); }); }); } /* eslint-enable complexity */ /** * Sets the baseUrl that should be used for the api. * * When working against the dhis2 demo instance at {@link https://play.dhis2.org/demo} the * baseUrl would be set as `https://play.dhis2.org/demo/api`. * * This method is used when calling the `d2.init` method with the `baseUrl` config property * to configure the Api singleton. * * @param {string} baseUrl The base url to be used for the API. * * @returns {this} Itself for chaining purposes */ }, { key: "setBaseUrl", value: function setBaseUrl(baseUrl) { (0, _check.checkType)(baseUrl, 'string', 'Base url'); this.baseUrl = baseUrl; return this; } }]); return Api; }(); /** * Retrieve the Api singleton or create one. * * When called for the first time it creates and Api singleton object. * Any subsequent calls will return the previously created singleton. * * @returns {Api} The Api singleton. * @memberof module:api~Api */ function getApi() { if (getApi.api) { return getApi.api; } return getApi.api = new Api(); } Api.getApi = getApi; var _default = Api; exports.default = _default; //# sourceMappingURL=Api.js.map