UNPKG

@kwiz/common

Version:

KWIZ common utilities and helpers for M365 platform

438 lines 20.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GetJsonClearCache = exports.GetJson = exports.GetJsonSync = exports.mediumLocalCache = exports.shortLocalCache = exports.monthLongLocalCache = exports.weeekLongLocalCache = exports.extraLongLocalCache = exports.longLocalCache = exports.noLocalCache = void 0; const json_1 = require("../helpers/json"); const objects_1 = require("../helpers/objects"); const typecheckers_1 = require("../helpers/typecheckers"); const rest_types_1 = require("../types/rest.types"); const consolelogger_1 = require("./consolelogger"); const localstoragecache_1 = require("./localstoragecache"); const web_1 = require("./sharepoint.rest/web"); var logger = consolelogger_1.ConsoleLogger.get("kwizcom.rest.module"); const supressDebugMessages = true; /** cache for 1 day */ exports.noLocalCache = { allowCache: false }; /** cache for 1 days */ exports.longLocalCache = { allowCache: true, localStorageExpiration: { days: 1 } }; /** cache for 2 days */ exports.extraLongLocalCache = { allowCache: true, localStorageExpiration: { days: 2 } }; /** cache for 7 days */ exports.weeekLongLocalCache = { allowCache: true, localStorageExpiration: { days: 7 } }; /** cache for 30 days */ exports.monthLongLocalCache = { allowCache: true, localStorageExpiration: { days: 30 } }; /** cache for 5 minutes */ exports.shortLocalCache = { allowCache: true, localStorageExpiration: { minutes: 5 } }; /** cache for 15 minutes */ exports.mediumLocalCache = { allowCache: true, localStorageExpiration: { minutes: 15 } }; //if allowCache is true, results will be stored/returned from here function _getCachedResults() { var _cachedResults = (0, objects_1.getGlobal)("utils_restmodule_cachedResults"); return _cachedResults; } //cannot use from top window, example: DVP, if you open view item popup and close it too fast, there might be a pending request that never resolves since the handler code was unloaded from the window. function _getPendingRequests() { var _pendingRequests = (0, objects_1.getGlobal)("utils_restmodule_pendingRequests", undefined, true); return _pendingRequests; } function getDefaultOptions() { return { includeDigestInPost: true, headers: {} }; } function fillHeaders(xhr, headers) { for (let header in headers) if ((0, objects_1.hasOwnProperty)(headers, header)) { let val = headers[header]; if (!(0, typecheckers_1.isNullOrEmptyString)(val)) xhr.setRequestHeader(header, val); } } function getXhr(url, body, options, async = true) { var optionsWithDefaults = (0, objects_1.assign)({}, getDefaultOptions(), options); let myCacheOptions = {}; Object.keys(rest_types_1.AllRestCacheOptionsKeys).forEach(key => { if ((0, objects_1.hasOwnProperty)(optionsWithDefaults, key)) { myCacheOptions[key] = optionsWithDefaults[key]; delete optionsWithDefaults[key]; } }); let myOptions = { ...optionsWithDefaults }; var xhr = new XMLHttpRequest(); let jsonType = myOptions.jsonMetadata || rest_types_1.jsonTypes.verbose; if (myOptions.cors) { xhr.withCredentials = true; } if ((0, typecheckers_1.isNullOrUndefined)(myOptions.headers)) myOptions.headers = {}; //issue 660 in case the sender sent headers as null if ((0, typecheckers_1.isNullOrUndefined)(myOptions.headers["Accept"])) { myOptions.headers["Accept"] = jsonType; } let method = myOptions.method; if ((0, typecheckers_1.isNullOrEmptyString)(method)) { method = (0, typecheckers_1.isNullOrUndefined)(body) ? "GET" : "POST"; } myOptions.method = method; xhr.open(method, url, async !== false); if (method === "GET") { if (myOptions.includeDigestInGet === true) { //by default don't add it, unless explicitly asked in options xhr.setRequestHeader("X-RequestDigest", (0, web_1.getFormDigest)(myOptions.spWebUrl)); } } else if (method === "POST") { if ((0, typecheckers_1.isNullOrUndefined)(myOptions.headers["content-type"])) { myOptions.headers["content-type"] = jsonType; } if (myOptions.includeDigestInPost !== false) { //if explicitly set to false - don't include it xhr.setRequestHeader("X-RequestDigest", (0, web_1.getFormDigest)(myOptions.spWebUrl)); } } if (!(0, typecheckers_1.isNullOrEmptyString)(myOptions.xHttpMethod)) { myOptions.headers["X-HTTP-Method"] = myOptions.xHttpMethod; if (myOptions.xHttpMethod === "MERGE" || myOptions.xHttpMethod === "DELETE" || myOptions.xHttpMethod === "PUT") { myOptions.headers["If-Match"] = "*"; // update regadless of other user changes } } fillHeaders(xhr, myOptions.headers); if (!(0, typecheckers_1.isNullOrEmptyString)(myOptions.responseType) && myOptions.responseType !== "text") { if (myCacheOptions.allowCache === true && (myOptions.responseType === "blob" || myOptions.responseType === "arraybuffer" || myOptions.responseType === "document")) { logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); } xhr.responseType = myOptions.responseType; } //we do not support cache if there is a request body //postCacheKey - allow cache on post request for stuff like get item by CamlQuery if ((0, typecheckers_1.isNullOrUndefined)(body) || !(0, typecheckers_1.isNullOrEmptyString)(myOptions.postCacheKey)) { myCacheOptions.cacheKey = (url + '|' + JSON.stringify(myOptions)).trim().toLowerCase(); } return { xhr: xhr, options: myOptions, cacheOptions: myCacheOptions }; } function getCachedResult(objects) { var cacheKey = objects.cacheOptions.cacheKey; if (objects.cacheOptions.allowCache === true && objects.cacheOptions.forceCacheUpdate !== true) { if ((0, typecheckers_1.isNullOrEmptyString)(cacheKey)) { //logger.warn('cache is not supported for this type of request.'); return null; } let _cachedResults = _getCachedResults(); if ((0, typecheckers_1.isNullOrUndefined)(_cachedResults[cacheKey])) { //try to load from local storage let result = (0, localstoragecache_1.getCacheItem)('jsr_' + cacheKey); if (!(0, typecheckers_1.isNullOrUndefined)(result) && (result.success === true || result.status === 404)) { if (!result.cachedTime) { let now = new Date(); now.setDate(-1); result.cachedTime = now.getTime(); } _cachedResults[cacheKey] = result; } } if (!(0, typecheckers_1.isNullOrUndefined)(_cachedResults[cacheKey])) { let result = _cachedResults[cacheKey]; var maxAge = (0, typecheckers_1.isNumber)(objects.cacheOptions.maxAge) && objects.cacheOptions.maxAge > 0 ? objects.cacheOptions.maxAge : null; if (maxAge && result.cachedTime) { let now = new Date().getTime(); var cachedTime = result.cachedTime; var validUntil = cachedTime + (maxAge * 1000); if (now > validUntil) { logger.debug("getCachedResult - entry has out lived max age"); return null; } } return { ..._cachedResults[cacheKey], result: _canSafelyStringify(_cachedResults[cacheKey].result) ? (0, objects_1.jsonClone)(_cachedResults[cacheKey].result) : _cachedResults[cacheKey].result }; } } return null; } function setCachedResult(cacheOptions, response) { if ((0, typecheckers_1.isNullOrEmptyString)(cacheOptions.cacheKey)) { return; } response.cachedTime = new Date().getTime(); let isResultSerializable = _canSafelyStringify(response.result); let _cachedResults = _getCachedResults(); _cachedResults[cacheOptions.cacheKey] = { ...response, result: isResultSerializable ? (0, objects_1.jsonClone)(response.result) : response.result }; if (!isResultSerializable) { logger.warn("When allowCache is true, Blob, ArrayBuffer and Document response types will only be stored in runtime memory and not committed to local storage."); } if (isResultSerializable && !(0, typecheckers_1.isNullOrUndefined)(cacheOptions.localStorageExpiration) && response && response.success === true) { (0, localstoragecache_1.setCacheItem)('jsr_' + cacheOptions.cacheKey, response, cacheOptions.localStorageExpiration); } } function getPendingRequest(objects) { var cacheKey = objects.cacheOptions.cacheKey; // if (isNullOrEmptyString(cacheKey)) { // logger.warn('cache is not supported for this type of request.'); // } let _pendingRequests = _getPendingRequests(); if (!(0, typecheckers_1.isNullOrEmptyString)(cacheKey) && !(0, typecheckers_1.isNullOrUndefined)(_pendingRequests[cacheKey])) { //returned from cache return _pendingRequests[cacheKey]; } return null; } function getParsedResponse(objects) { let parsedResponse = null; if (!(0, typecheckers_1.isNullOrEmptyString)(objects.options.responseType) && objects.options.responseType !== "text") { parsedResponse = objects.xhr.response; } else { if (objects.options.responseType !== "text") { //Only try to parse if caller didn't expect text explicitly parsedResponse = (0, json_1.jsonParse)(objects.xhr.responseText); } if ((0, typecheckers_1.isNullOrUndefined)(parsedResponse)) { parsedResponse = objects.xhr.responseText; } } return parsedResponse; } function setPendingRequest(cacheKey, objects, promise) { if ((0, typecheckers_1.isNullOrEmptyString)(cacheKey)) { return null; } let _pendingRequests = _getPendingRequests(); _pendingRequests[cacheKey] = { objects: objects, promise: promise, listeners: [] }; return _pendingRequests[cacheKey]; } function removePendingRequest(cacheKey) { if ((0, typecheckers_1.isNullOrEmptyString)(cacheKey)) { return; } try { let _pendingRequests = _getPendingRequests(); _pendingRequests[cacheKey] = null; delete _pendingRequests[cacheKey]; } catch (ex) { } } function _getRestErrorMessage(xhr) { try { //issue 245, external datasource might return error.code as a number with a plain text message. if (!(0, typecheckers_1.isNullOrUndefined)(xhr) && !(0, typecheckers_1.isNullOrEmptyString)(xhr.responseText)) { let error = (0, json_1.jsonParse)(xhr.responseText); if (!(0, typecheckers_1.isNullOrUndefined)(error) && !(0, typecheckers_1.isNullOrEmptyString)(error.code)) { if ((0, typecheckers_1.isString)(error.code) && error.code.indexOf("SPQueryThrottledException") !== -1) { return !(0, typecheckers_1.isNullOrEmptyString)(error.message) ? `${error.message} (SPQueryThrottledException)` : `an error occured (SPQueryThrottledException)`; } if (!(0, typecheckers_1.isNullOrEmptyString)(error.message)) return error.message; } } } catch (e) { } return `an error occured`; } function _canSafelyStringify(result) { //this would return false positives on some response strings if ((0, typecheckers_1.isPrimitiveValue)(result)) { return true; } else if ((0, typecheckers_1.isObject)(result)) { if (("ArrayBuffer" in globalThis && (result instanceof ArrayBuffer)) || ("Blob" in globalThis && (result instanceof Blob)) || ("Document" in globalThis && (result instanceof Document))) { return false; } return true; } else { return false; //shouldn't get here... since result should either be primitive value or an object } } function GetJsonSync(url, body, options) { let xhr = null; let syncResult = null; let objects = getXhr(url, body, options, false); try { var cachedResult = getCachedResult(objects); if (!(0, typecheckers_1.isNullOrUndefined)(cachedResult)) { return cachedResult; } xhr = objects.xhr; if (objects.options.method === "GET") { objects.xhr.send(); } else { objects.xhr.send(body); } if (objects.options.returnXhrObject === true) { return { status: xhr.status, success: xhr.status >= 200 && xhr.status < 400, result: xhr }; } // status < 300 leaves out 304 responses which are successful responses so we should use < 400 if (objects.xhr.status >= 200 && objects.xhr.status < 400) { let result = getParsedResponse(objects); syncResult = { status: xhr.status, success: true, result: result }; setCachedResult(objects.cacheOptions, syncResult); } else { throw new Error("Error code: " + objects.xhr.status); } } catch (e) { //make sure errors get here and not returned without catch... let responseText = xhr.responseText; let errorData; if (!(0, typecheckers_1.isNullOrEmptyString)(responseText)) { errorData = (0, json_1.jsonParse)(responseText); if ((0, typecheckers_1.isNullOrUndefined)(errorData)) { errorData = responseText; } } let errorMessage = _getRestErrorMessage(xhr); syncResult = { status: xhr && xhr.status || -1, success: false, errorData: errorData, errorMessage: errorMessage }; setCachedResult(objects.cacheOptions, syncResult); } return syncResult; } exports.GetJsonSync = GetJsonSync; function GetJson(url, body, options) { try { let objects = getXhr(url, body, options); var cachedResult = getCachedResult(objects); if (!(0, typecheckers_1.isNullOrUndefined)(cachedResult)) { if (!supressDebugMessages) { logger.debug(`GetJson - request fulfilled by cached results: ${url}`); } if (cachedResult.success) { return Promise.resolve(cachedResult.result); } else { return Promise.reject({ message: (0, typecheckers_1.isNullOrEmptyString)(cachedResult.errorMessage) ? "an error occured in cached results" : cachedResult.errorMessage, errorData: cachedResult.errorData }); } } var pendingRequest = getPendingRequest(objects); var xhrPromise = null; if ((0, typecheckers_1.isNullOrUndefined)(pendingRequest)) { if (!supressDebugMessages) { logger.debug(`GetJson - request fulfilled by new request: ${url}`); } xhrPromise = new Promise((resolve, reject) => { let promiseResolved = false; objects.xhr.addEventListener("readystatechange", () => { if (objects.xhr.readyState === XMLHttpRequest.DONE) { try { if (!supressDebugMessages) { logger.debug(`readystate changed: ${url}`); } if (objects.options.returnXhrObject === true) { promiseResolved = true; resolve(objects.xhr); } let parsedResponse = getParsedResponse(objects); // status < 300 leaves out 304 responses which are successful responses so we should use < 400 if (objects.xhr.status >= 200 && objects.xhr.status < 400) { setCachedResult(objects.cacheOptions, { status: objects.xhr.status, success: true, result: parsedResponse }); promiseResolved = true; resolve(parsedResponse); if (pendingRequest) { pendingRequest.listeners.forEach(l => { let listenerParsedResponse = getParsedResponse(objects); l.resolve(listenerParsedResponse); }); } } else { let errorMessage = _getRestErrorMessage(objects.xhr); setCachedResult(objects.cacheOptions, { status: objects.xhr.status, success: false, errorData: parsedResponse, errorMessage: errorMessage }); promiseResolved = true; reject({ message: errorMessage, errorData: parsedResponse, xhr: objects.xhr }); if (pendingRequest) { pendingRequest.listeners.forEach(l => l.reject({ message: errorMessage, errorData: parsedResponse, xhr: objects.xhr })); } } } catch (e) { if (!supressDebugMessages) { logger.error(`readystate error: ${e}: ${url}`); } } if (!promiseResolved) { if (!supressDebugMessages) { logger.debug(`promise NOT resolved. resoving myself...: ${url}`); } promiseResolved = true; reject({ message: "an unknown error occured", xhr: objects.xhr }); } else if (!supressDebugMessages) { logger.debug(`promise resolved. removing pending request object: ${url}`); } removePendingRequest(objects.cacheOptions.cacheKey); } }); }); if (objects.xhr.readyState === XMLHttpRequest.OPENED) { //only set this if our request is on the way pendingRequest = setPendingRequest(objects.cacheOptions.cacheKey, objects, xhrPromise); if (!supressDebugMessages) { logger.debug(`${url}: sending request, setPendingRequest`); } if (objects.options.method === "GET") { objects.xhr.send(); } else { objects.xhr.send(body); } } else logger.error('xhr not opened'); } else if (pendingRequest) { if (!supressDebugMessages) { logger.debug(`GetJson - request fulfilled by pending requests: ${url}`); } //must add a separate promise, so that I can make a full(not shallow) copy of the result. //this way if the first caller changes the object, the second caller gets it unchanged. xhrPromise = new Promise((resolve, reject) => { pendingRequest.listeners.push({ resolve: (result) => resolve(result), reject: (reason) => reject(reason) }); }); } return xhrPromise; } catch (e) { return Promise.reject({ message: "an error occured" }); } } exports.GetJson = GetJson; /** if you detected a change that invalidates all requests stored in memory - this will clear all in-memory cached results */ function GetJsonClearCache() { let _cachedResults = _getCachedResults(); Object.keys(_cachedResults).forEach(key => { delete _cachedResults[key]; }); } exports.GetJsonClearCache = GetJsonClearCache; //# sourceMappingURL=rest.js.map