@kwiz/common
Version:
KWIZ common utilities and helpers for M365 platform
438 lines • 20.5 kB
JavaScript
;
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