@kwiz/common
Version:
KWIZ common utilities and helpers for M365 platform
442 lines • 19.7 kB
JavaScript
import { jsonParse } from "../helpers/json";
import { assign, getGlobal, hasOwnProperty, jsonClone } from "../helpers/objects";
import { isNullOrEmptyString, isNullOrUndefined, isNumber, isObject, isPrimitiveValue, isString } from "../helpers/typecheckers";
import { AllRestCacheOptionsKeys, jsonTypes } from "../types/rest.types";
import { ConsoleLogger } from "./consolelogger";
import { getCacheItem, setCacheItem } from "./localstoragecache";
import { getFormDigest } from "./sharepoint.rest/web";
var logger = ConsoleLogger.get("utils/rest");
const supressDebugMessages = true;
/** cache for 1 day */
export const noLocalCache = { allowCache: false };
/** cache for 1 days */
export const longLocalCache = { allowCache: true, localStorageExpiration: { days: 1 } };
/** cache for 2 days */
export const extraLongLocalCache = { allowCache: true, localStorageExpiration: { days: 2 } };
/** cache for 7 days */
export const weeekLongLocalCache = { allowCache: true, localStorageExpiration: { days: 7 } };
/** cache for 30 days */
export const monthLongLocalCache = { allowCache: true, localStorageExpiration: { days: 30 } };
/** cache for 5 minutes */
export const shortLocalCache = { allowCache: true, localStorageExpiration: { minutes: 5 } };
/** cache for 15 minutes */
export const mediumLocalCache = { allowCache: true, localStorageExpiration: { minutes: 15 } };
//if allowCache is true, results will be stored/returned from here
function _getCachedResults() {
var _cachedResults = 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 = getGlobal("utils_restmodule_pendingRequests", undefined, true);
return _pendingRequests;
}
function getDefaultOptions() {
return {
includeDigestInPost: true,
headers: {}
};
}
function fillHeaders(xhr, headers) {
for (let header in headers)
if (hasOwnProperty(headers, header)) {
let val = headers[header];
if (!isNullOrEmptyString(val))
xhr.setRequestHeader(header, val);
}
}
function getXhr(url, body, options, async = false) {
let [myOptions, myCacheOptions] = configureXhrHeaders(url, body, options);
const xhr = new XMLHttpRequest();
xhr.open(myOptions.method, url, async);
fillHeaders(xhr, myOptions.headers);
if (myOptions.cors) {
xhr.withCredentials = true;
}
if (!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;
}
const needsDigest = (myOptions.method === "GET" && myOptions.includeDigestInGet) ||
(myOptions.method === "POST" && myOptions.includeDigestInPost);
const applyDigest = (digest) => {
if (digest) {
xhr.setRequestHeader("X-RequestDigest", digest);
}
else {
console.warn("X-RequestDigest header not set due to getFormDigest returning null");
}
};
const result = { xhr, options: myOptions, cacheOptions: myCacheOptions };
if (needsDigest && async) {
return getFormDigest(myOptions.spWebUrl, true).then(digest => {
applyDigest(digest);
return result;
});
}
else if (needsDigest && !async) {
const digest = getFormDigest(myOptions.spWebUrl, false);
applyDigest(digest);
return result;
}
return result;
}
function configureXhrHeaders(url, body, options) {
var optionsWithDefaults = assign({}, getDefaultOptions(), options);
let myCacheOptions = {};
Object.keys(AllRestCacheOptionsKeys).forEach(key => {
if (hasOwnProperty(optionsWithDefaults, key)) {
myCacheOptions[key] = optionsWithDefaults[key];
delete optionsWithDefaults[key];
}
});
let myOptions = { ...optionsWithDefaults };
let jsonType = myOptions.jsonMetadata || jsonTypes.verbose;
if (isNullOrUndefined(myOptions.headers))
myOptions.headers = {}; //issue 660 in case the sender sent headers as null
if (isNullOrUndefined(myOptions.headers["Accept"])) {
myOptions.headers["Accept"] = jsonType;
}
if (isNullOrEmptyString(myOptions.method)) {
myOptions.method = isNullOrUndefined(body) ? "GET" : "POST";
}
if (myOptions.method === "POST" && isNullOrUndefined(myOptions.headers["content-type"])) {
myOptions.headers["content-type"] = jsonType;
}
if (!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
}
}
//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 (isNullOrUndefined(body) || !isNullOrEmptyString(myOptions.postCacheKey)) {
myCacheOptions.cacheKey = (url + '|' + JSON.stringify(myOptions)).trim().toLowerCase();
}
return [myOptions, myCacheOptions];
}
function getCachedResult(objects) {
var cacheKey = objects.cacheOptions.cacheKey;
if (objects.cacheOptions.allowCache === true && objects.cacheOptions.forceCacheUpdate !== true) {
if (isNullOrEmptyString(cacheKey)) {
//logger.warn('cache is not supported for this type of request.');
return null;
}
let _cachedResults = _getCachedResults();
if (isNullOrUndefined(_cachedResults[cacheKey])) {
//try to load from local storage
let result = getCacheItem('jsr_' + cacheKey);
if (!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 (!isNullOrUndefined(_cachedResults[cacheKey])) {
let result = _cachedResults[cacheKey];
var maxAge = 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) ?
jsonClone(_cachedResults[cacheKey].result) :
_cachedResults[cacheKey].result
};
}
}
return null;
}
function setCachedResult(cacheOptions, response) {
if (isNullOrEmptyString(cacheOptions.cacheKey)) {
return;
}
response.cachedTime = new Date().getTime();
let isResultSerializable = _canSafelyStringify(response.result);
let _cachedResults = _getCachedResults();
_cachedResults[cacheOptions.cacheKey] = {
...response,
result: isResultSerializable ? 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 && !isNullOrUndefined(cacheOptions.localStorageExpiration) && response && response.success === true) {
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 (!isNullOrEmptyString(cacheKey) && !isNullOrUndefined(_pendingRequests[cacheKey])) {
//returned from cache
return _pendingRequests[cacheKey];
}
return null;
}
function getParsedResponse(objects) {
let parsedResponse = null;
if (!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 = jsonParse(objects.xhr.responseText);
}
if (isNullOrUndefined(parsedResponse)) {
parsedResponse = objects.xhr.responseText;
}
}
return parsedResponse;
}
function setPendingRequest(cacheKey, objects, promise) {
if (isNullOrEmptyString(cacheKey)) {
return null;
}
let _pendingRequests = _getPendingRequests();
_pendingRequests[cacheKey] = { objects: objects, promise: promise, listeners: [] };
return _pendingRequests[cacheKey];
}
function removePendingRequest(cacheKey) {
if (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 (!isNullOrUndefined(xhr) && !isNullOrEmptyString(xhr.responseText)) {
let error = jsonParse(xhr.responseText);
if (!isNullOrUndefined(error) && !isNullOrEmptyString(error.code)) {
if (isString(error.code) && error.code.indexOf("SPQueryThrottledException") !== -1) {
return !isNullOrEmptyString(error.message) ? `${error.message} (SPQueryThrottledException)` : `an error occured (SPQueryThrottledException)`;
}
if (!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 (isPrimitiveValue(result)) {
return true;
}
else if (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
}
}
export function GetJsonSync(url, body, options) {
let xhr = null;
let syncResult = null;
let objects = getXhr(url, body, options, false);
try {
var cachedResult = getCachedResult(objects);
if (!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 (!isNullOrEmptyString(responseText)) {
errorData = jsonParse(responseText);
if (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;
}
export async function GetJson(url, body, options) {
try {
let objects = await getXhr(url, body, options, true);
var cachedResult = getCachedResult(objects);
if (!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: isNullOrEmptyString(cachedResult.errorMessage) ? "an error occured in cached results" : cachedResult.errorMessage,
errorData: cachedResult.errorData
});
}
}
var pendingRequest = getPendingRequest(objects);
var xhrPromise = null;
if (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({ error: e, message: e.message, stack: e.stack });
}
}
/** if you detected a change that invalidates all requests stored in memory - this will clear all in-memory cached results */
export function GetJsonClearCache() {
let _cachedResults = _getCachedResults();
Object.keys(_cachedResults).forEach(key => {
delete _cachedResults[key];
});
}
//# sourceMappingURL=rest.js.map