UNPKG

@salaxy/jquery

Version:

Salaxy general plain JavaScript / TypeScript libraries with JQuery -ajax component (Palkkaus.fi)

1,098 lines (1,088 loc) 3.98 MB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.salaxy = global.salaxy || {}, global.salaxy.core = {}))); }(this, (function (exports) { 'use strict'; /** * Class for reading Environment specific configuration for Salaxy API's and JavaScript in general */ class Configs { static getGlobalConfig() { const g = this.getGlobal(); return (g.salaxy && g.salaxy.config) ? g.salaxy.config : null; } static setGlobalConfig(config) { const g = this.getGlobal(); g.salaxy = g.salaxy || {}; g.salaxy.config = config; } static getGlobal() { if (typeof globalThis !== "undefined") { return globalThis; } if (typeof self !== "undefined") { return self; } if (typeof window !== "undefined") { return window; } if (typeof global !== "undefined") { return global; } throw new Error("unable to locate global object"); } /** * Returns current configuration. */ static get current() { return this.getGlobalConfig(); } /** * Set current configuration. */ static set current(config) { this.setGlobalConfig(config); } /** Get global, globalThis object */ static get global() { return this.getGlobal(); } } /** * Very simple cookie implementation - at the moment for token storage only. * Expand if needed for other use cases (see todo section in the docs). * * @todo Expand this class as necessary e.g. something like this: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie/Simple_document.cookie_framework * or this https://github.com/Booyanach/cookie-wrapper ... which unfortunately cannot be used at the moment, because they are GPL license. */ class Cookies { /** * Gets a cookie value by a key * @param key - CName / key to look for. */ get(key) { const cookie = document.cookie; const cookieParts = cookie.split(";"); for (const item of cookieParts) { const cookiePart = decodeURIComponent(item).trim(); if (cookiePart.indexOf(key + "=") === 0) { return cookiePart.substring(key.length + 1); } } return null; } /** * Sets a cookie value for the specified CName * @param cname - CName for the cookie value. Name is uri encoded as a fallback, but generally, you should make sure this is a valid cname. * @param value - Value to set for the cookie. Value is uri encoded. * @param expirationDays - Days until the cookie expires. * Set this to null or 0 to not set the expiration date at all. * This defaults the behavior where cookie is deleted when browser is closed. */ setCookie(cname, value, expirationDays) { cname = encodeURIComponent(cname); value = encodeURIComponent(value); if (expirationDays && expirationDays > 0) { const d = new Date(); d.setTime(d.getTime() + (expirationDays * 24 * 60 * 60 * 1000)); const expires = "expires=" + d.toUTCString(); document.cookie = cname + "=" + value + ";" + expires + ";path=/"; } else { document.cookie = cname + "=" + value + ";path=/"; } } } /** * Provides wrapper methods for communicating with the Palkkaus.fi API. * The raw Ajax-access to the server methods: GET, POST and DELETE * with different return types and authentication / error events. * This is the JQuery based ajax implementation. */ class AjaxJQuery { /** * Creates a new instance of AjaxJQuery */ constructor() { /** * By default (true) the token is set to salaxy-token -cookie. * Disable cookies with this flag. */ this.useCookie = true; /** * By default credentials are not used in http-calls. * Enable credentials with this flag. */ this.useCredentials = false; /** * The server address - root of the server. This is settable field. * Will probably be changed to a configuration object in the final version. */ this.serverAddress = "https://test-api.salaxy.com"; const config = Configs.current; if (config) { // apiServer if (config.apiServer) { this.serverAddress = config.apiServer; } // useCredentials if (config.useCredentials != null) { this.useCredentials = config.useCredentials; } // useCookie if (config.useCookie != null) { this.useCookie = config.useCookie; } } } /** Gets the API address with version information. E.g. 'https://test-api.salaxy.com/v02/api' */ getApiAddress() { return this.serverAddress + "/v02/api"; } /** Gets the Server address that is used as bases to the HTML queries. E.g. 'https://test-api.salaxy.com' */ getServerAddress() { return this.serverAddress; } /** * Gets a JSON-message from server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result data. Standard Promise rejection to be used for error handling. */ getJSON(method) { const token = this.getCurrentToken(); return this.getJQuery().ajax({ dataType: "json", url: this.getUrl(method), xhrFields: { withCredentials: (token) ? false : this.useCredentials, }, beforeSend: (request) => { if (token) { request.setRequestHeader("Authorization", "Bearer " + token); } }, }); } /** * Gets a HTML-message from server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result html. Standard Promise rejection to be used for error handling. */ getHTML(method) { const token = this.getCurrentToken(); return this.getJQuery().ajax({ dataType: "html", url: this.getUrl(method), xhrFields: { withCredentials: (token) ? false : this.useCredentials, }, beforeSend: (request) => { if (token) { request.setRequestHeader("Authorization", "Bearer " + token); } }, }); } /** * POSTS data to server and receives back a JSON-message. * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * @param data - The data that is posted to the server. * * @returns A Promise with result data. Standard Promise rejection to be used for error handling. */ postJSON(method, data) { const token = this.getCurrentToken(); return this.getJQuery().ajax({ type: "POST", url: this.getUrl(method), data, dataType: "json", xhrFields: { withCredentials: (token) ? false : this.useCredentials, }, beforeSend: (request) => { if (token) { request.setRequestHeader("Authorization", "Bearer " + token); } }, }); } /** * POSTS data to server and receives back HTML. * * @param method - The API method is the url starting from api version, e.g. '/v02/api'. E.g. '/calculator/new' * @param data - The data that is posted to the server. * * @returns A Promise with result data. Standard Promise rejection to be used for error handling. */ postHTML(method, data) { const token = this.getCurrentToken(); return this.getJQuery().ajax({ type: "POST", url: this.getUrl(method), data, dataType: "html", xhrFields: { withCredentials: (token) ? false : this.useCredentials, }, beforeSend: (request) => { if (token) { request.setRequestHeader("Authorization", "Bearer " + token); } }, }); } /** * Sends a DELETE-message to server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result data. Standard Promise rejection to be used for error handling. */ remove(method) { const token = this.getCurrentToken(); return this.getJQuery().ajax({ type: "DELETE", url: this.getUrl(method), xhrFields: { withCredentials: (token) ? false : this.useCredentials, }, beforeSend: (request) => { if (token) { request.setRequestHeader("Authorization", "Bearer " + token); } }, }); } /** * Gets the current token. * Will check the salaxy-token cookie if the token is persisted there */ getCurrentToken() { if (!this.token && this.useCookie) { this.token = new Cookies().get("salaxy-token") || ""; } return this.token; } /** * Sets the current token. The token is set to cookie called "salaxy-token" or * if the HTML page is running from local computer, it is set to local storage. * * @param token - the authentication token to store. */ setCurrentToken(token) { if (this.useCookie) { new Cookies().setCookie("salaxy-token", token || ""); } this.token = token; } /** * Implements the OAuth2 "Resource Owner Password Credentials Grant" flow (RFC6749 4.3). * This method is not typically used in production web sites - use Implicit Grant instead for client side JavScript (SPAs). * However, it is very useful in development, testing, trusted helpers and server-side scenarios. */ resourceOwnerLogin(username, password) { return this.getJQuery().ajax({ type: "POST", url: this.getServerAddress() + "/oauth2/token", data: { grant_type: "password", username, password, }, // eslint-disable-next-line @typescript-eslint/no-unused-vars success: (data, textStatus, request) => { this.setCurrentToken(data.access_token); }, dataType: "json", }); } getJQuery() { const jQuery = Configs.global.jQuery; if (jQuery) { return jQuery; } else { throw new Error("ERROR: No jQuery. AjaxJQuery requires jQuery to be referenced."); } } /** If missing, append the API server address to the given url method string */ getUrl(method) { if (!method || method.trim() === "") { return null; } if (method.toLowerCase().startsWith("http")) { return method; } if (method.toLowerCase().startsWith("/v")) { return this.getServerAddress() + method; } return this.getApiAddress() + method; } } /** * Provides wrapper methods for communicating with the Palkkaus.fi API. * The raw Ajax-access to the server methods: GET, POST and DELETE * with different return types and authentication / error events. * This is the XMLHttpRequest based Ajax implementation. */ class AjaxXHR { /** * Creates a new instance of AjaxXHR */ constructor() { /** * By default (true) the token is set to salaxy-token -cookie. * Disable cookies with this flag. */ this.useCookie = true; /** * The server address - root of the server. This is settable field. * Will probably be changed to a configuration object in the final version. */ this.serverAddress = "https://test-api.salaxy.com"; const config = Configs.current; if (config) { // apiServer if (config.apiServer) { this.serverAddress = config.apiServer; } // useCredentials if (config.useCredentials != null) { this.useCredentials = config.useCredentials; } // useCookie if (config.useCookie != null) { this.useCookie = config.useCookie; } } } /** Gets the API address with version information. E.g. 'https://test-api.salaxy.com/v02/api' */ getApiAddress() { return this.serverAddress + "/v02/api"; } /** Gets the Server address that is used as bases to the HTML queries. E.g. 'https://test-api.salaxy.com' */ getServerAddress() { return this.serverAddress; } /** * Gets a JSON-message from server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result data. Standard Promise rejection to be used for error handling. */ getJSON(method) { return new Promise((resolve, reject) => { const xhr = this.createCORSRequest("GET", this.getUrl(method)); if (!xhr) { return reject(new Error("CORS not supported!")); } xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); xhr.setRequestHeader("Accept", "application/json; charset=UTF-8"); // xhr.setRequestHeader("Cache-Control", "max-age=0"); const token = this.getCurrentToken(); if (token) { xhr.setRequestHeader("Authorization", "Bearer " + token); } xhr.withCredentials = (token) ? false : this.useCredentials; xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); return resolve(response); } catch (error) { return reject(error); } } else { return reject(new Error("There was a problem with the request.")); } } }; xhr.onerror = () => { return reject(new Error("There was a problem with the request.")); }; xhr.send(); }); } /** * Gets a HTML-message from server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result html. Standard Promise rejection to be used for error handling. */ getHTML(method) { return new Promise((resolve, reject) => { const xhr = this.createCORSRequest("GET", this.getUrl(method)); if (!xhr) { return reject(new Error("CORS not supported!")); } xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); xhr.setRequestHeader("Accept", "application/json; charset=UTF-8"); // xhr.setRequestHeader("Cache-Control", "max-age=0"); const token = this.getCurrentToken(); if (token) { xhr.setRequestHeader("Authorization", "Bearer " + token); } xhr.withCredentials = (token) ? false : this.useCredentials; xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { return resolve(xhr.responseText); } else { return reject(new Error("There was a problem with the request.")); } } }; xhr.onerror = () => { return reject(new Error("There was a problem with the request.")); }; xhr.send(); }); } /** * POSTS data to server and receives back a JSON-message. * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * @param data - The data that is posted to the server. * * @returns A Promise with result. Standard Promise rejection to be used for error handling. */ postJSON(method, data) { return new Promise((resolve, reject) => { const xhr = this.createCORSRequest("POST", this.getUrl(method)); if (!xhr) { return reject(new Error("CORS not supported!")); } xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); xhr.setRequestHeader("Accept", "application/json; charset=UTF-8"); // xhr.setRequestHeader("Cache-Control", "max-age=0"); const token = this.getCurrentToken(); if (token) { xhr.setRequestHeader("Authorization", "Bearer " + token); } xhr.withCredentials = (token) ? false : this.useCredentials; xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); return resolve(response); } catch (error) { return reject(error); } } else { return reject(new Error("There was a problem with the request.")); } } }; xhr.onerror = () => { return reject(new Error("There was a problem with the request.")); }; const params = (!data || typeof data === "string") ? data : Object.keys(data).map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(data[k])).join("&"); xhr.send(params); }); } /** * POSTS data to server and receives back HTML. * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * @param data - The data that is posted to the server. * * @returns A Promise with result. Standard Promise rejection to be used for error handling. */ postHTML(method, data) { return new Promise((resolve, reject) => { const xhr = this.createCORSRequest("POST", this.getUrl(method)); if (!xhr) { return reject(new Error("CORS not supported!")); } xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); xhr.setRequestHeader("Accept", "application/json; charset=UTF-8"); // xhr.setRequestHeader("Cache-Control", "max-age=0"); const token = this.getCurrentToken(); if (token) { xhr.setRequestHeader("Authorization", "Bearer " + token); } xhr.withCredentials = (token) ? false : this.useCredentials; xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { return resolve(xhr.responseText); } else { return reject(new Error("There was a problem with the request.")); } } }; xhr.onerror = () => { return reject(new Error("There was a problem with the request.")); }; const params = (!data || typeof data === "string") ? data : Object.keys(data).map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(data[k])).join("&"); xhr.send(params); }); } /** * Sends a DELETE-message to server using the API * * @param method - The API method is the url path after the api version segments (e.g. '/v02/api') * and starts with a forward slash, e.g. '/calculator/new', or a full URL address. * * @returns A Promise with result. Standard Promise rejection to be used for error handling. */ remove(method) { return new Promise((resolve, reject) => { const xhr = this.createCORSRequest("DELETE", this.getUrl(method)); if (!xhr) { return reject(new Error("CORS not supported!")); } xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); xhr.setRequestHeader("Accept", "application/json; charset=UTF-8"); // xhr.setRequestHeader("Cache-Control", "max-age=0"); const token = this.getCurrentToken(); if (token) { xhr.setRequestHeader("Authorization", "Bearer " + token); } xhr.withCredentials = (token) ? false : this.useCredentials; xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { try { const response = JSON.parse(xhr.responseText); return resolve(response); } catch (error) { return reject(error); } } else { return reject(new Error("There was a problem with the request.")); } } }; xhr.onerror = () => { return reject(new Error("There was a problem with the request.")); }; xhr.send(); }); } /** * Gets the current token. * Will check the salaxy-token cookie if the token is persisted there */ getCurrentToken() { if (!this.token && this.useCookie) { this.token = new Cookies().get("salaxy-token") || ""; } return this.token; } /** * Sets the current token. The token is set to cookie called "salaxy-token" or * if the HTML page is running from local computer, it is set to local storage. * * @param token - the authentication token to store. */ setCurrentToken(token) { if (this.useCookie) { new Cookies().setCookie("salaxy-token", token || ""); } this.token = token; } /** If missing, append the API server address to the given url method string */ getUrl(method) { if (!method || method.trim() === "") { return null; } if (method.toLowerCase().startsWith("http")) { return method; } if (method.toLowerCase().startsWith("/v")) { return this.getServerAddress() + method; } return this.getApiAddress() + method; } createCORSRequest(method, url) { let xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // XHR for Chrome/Firefox/Opera/Safari. xhr.open(method, url, true); } else if (typeof XDomainRequest !== "undefined") { // XDomainRequest for IE8/9. xhr = new XDomainRequest(); xhr.open(method, url); } else { // CORS not supported. xhr = null; } return xhr; } } /** * Provides framework independent helpers for creating HTML forms * based on JSON schema and other metadata. */ class FormHelpers { /** * Gets a valid input type, format and other input properties from schema. * @param schema The property schema based on which the input is created. * @param propertyName Name of property or null if the input is not created from a property, but from model directly. * @param dataBindingPath Path for the data model. * Default is "form": The current form. Could be a a longer path e.g. "form.result.employerCalc" */ static getInputMetadata(schema, propertyName, dataBindingPath = "form") { var _a; const result = { name: propertyName, path: null, type: schema.type, format: schema.format, isEnum: false, }; propertyName = propertyName || ""; result.path = result.name ? (dataBindingPath ? (dataBindingPath + "." + result.name) : result.name) : dataBindingPath; switch (result.type) { case "number": case "integer": // TODO: Add enum support to numbers. case "boolean": case "object": case "array": break; case "string": result.isEnum = ((_a = schema.enum) === null || _a === void 0 ? void 0 : _a.length) > 0; break; default: result.type = "error"; result.content = `Unhandled data type '${result.type}', format '${result.format}' in '${propertyName || "no property name"}'.`; return result; } return result; } /** * Gets the necessary metadata for generating the inputs for the specified model of type "object". * If the schema is not of type "object", returns null. * @param schema Schema for which the user interface is generated. * @param dataBindingPath Path for the data model. * Default is "form": The current form. Could be a a longer path e.g. "form.result.employerCalc" */ static getInputsForObject(schema, dataBindingPath = "form") { if (schema.type === "object") { return Object.keys(schema.properties).map((key) => this.getInputMetadata(schema, key, dataBindingPath)); } else { return null; } } /** * Gets input for the schema itself. * @param schema Schema for which the input component is generated. * @param propertyName Property name in relation to schema. This may be used in language versioning / texts. * @param dataBindingPath Path for the data binding within the form. */ static getInputForSelf(schema, propertyName, dataBindingPath) { return this.getInputMetadata(schema, propertyName, dataBindingPath); } /** * Gets the necessary metadata for generating the inputs for the array items. * If the schema is not of type "array", returns null. * @param schema Schema for which the user interface is generated. * @param dataBindingPath Path for the data binding within the form. */ static getInputsForArray(schema, dataBindingPath) { if (schema.type === "array") { return FormHelpers.getInputsForObject(schema.items, dataBindingPath) || [FormHelpers.getInputForSelf(schema.items, null, dataBindingPath)]; } return null; } } /** Static utilities for JSON Schemas an Open API documents. */ class JsonSchemaUtils { /** * Gets a property from a schema document. Supports single property names or longer property paths. * @param schema The root type from which the property is found. * @param path Property path starting with the root schema. */ static getProperty(schema, path) { var _a; if (!schema || !(path === null || path === void 0 ? void 0 : path.trim())) { return null; } const pathArr = path.split("."); let member = schema; let parentName = null; let propertyName = null; let isRequired = false; while (pathArr.length) { propertyName = pathArr.shift().trim(); const memberOrRef = member.properties[propertyName]; parentName = member.format; isRequired = (((_a = member.required) === null || _a === void 0 ? void 0 : _a.indexOf(propertyName)) >= 0) || false; if (JsonSchemaUtils.isReferenceObject(memberOrRef)) { throw new Error(`Unresolved reference in ${propertyName}`); } member = memberOrRef; if (!member) { return null; } } return { parentName, propertyName, schema: member, isRequired, }; } /** Typeguard for OpenAPIV2 */ static isOpenAPIV3(doc) { var _a, _b; return ((_b = (_a = doc) === null || _a === void 0 ? void 0 : _a.openapi) === null || _b === void 0 ? void 0 : _b.startsWith("3.")) || false; } /** Typeguard for OpenAPIV3 */ static isOpenAPIV2(doc) { var _a, _b; return ((_b = (_a = doc) === null || _a === void 0 ? void 0 : _a.swagger) === null || _b === void 0 ? void 0 : _b.startsWith("2.")) || false; } /** Typeguard for JSON schema (TODO: consider adding more generic Schema typing).) */ static isSchemaDocument(doc) { var _a; return !!((_a = doc) === null || _a === void 0 ? void 0 : _a.$schema); } /** Typeguard for OpenAPIV3.ReferenceObject */ static isReferenceObject(schema) { var _a; return !!((_a = schema) === null || _a === void 0 ? void 0 : _a.$ref); } /** Typeguard for OpenAPIV3.NonArraySchemaObject */ static isNonArraySchemaObject(schema) { return ["boolean", "object", "number", "string", "integer"].indexOf(schema.type) >= 0; } /** Typeguard for OpenAPIV3.ArraySchemaObject */ static isArraySchemaObject(schema) { return schema.type === "array"; } } /** * Implemenatation of cache of resolved schemas and methods on reading their structure. */ class JsonSchemaCache { constructor(ajax) { this.ajax = ajax; /** Cache for loaded schema elements. */ this.schemaCache = []; } /** * Adds a local schema document and derefernces it. * @param url URL that identifies the schema * @param doc The schema document as object. * @returns A promise of the object. */ addSchemaDocument(url, doc) { url = url.toLowerCase().trim(); this.schemaCache = this.schemaCache.filter((x) => x.url !== url); if (JsonSchemaUtils.isOpenAPIV2(doc)) { const v3doc = { "openapi": "3.0.1", "info": doc.info, paths: doc.paths, components: { // HACK: There are differneces in two schema objects. Go through what they are exactly and migrate/mitigate if possible. schemas: doc.definitions, } }; doc = v3doc; } if (JsonSchemaUtils.isSchemaDocument(doc)) { const v3doc = { openapi: "3.0.1", info: { title: doc.title, version: "1.0", description: doc.description, }, paths: {}, components: { // TODO: There may be differneces in two schema objects (depending on the version). Go through what they are exactly and migrate/mitigate if possible. schemas: doc.definitions, } }; doc = v3doc; } if (JsonSchemaUtils.isOpenAPIV3(doc)) { this.prepareSchema(doc); // Add to cache this.schemaCache.push({ url: url.toLowerCase(), doc, type: "OpenAPIV3", }); return doc; } throw new Error("Unsupported JSON schema / open api: " + url); } /** * Prepares the scehma for our use: * Dereferences the internal references ($ref-attribute) as JavaScript references. * Also adds the schema names to the format attribute of schema. * External references are currently not handled. */ prepareSchema(doc) { Object.keys(doc.components.schemas).forEach((schemaName) => { const schema = doc.components.schemas[schemaName]; // TODO: Consider adding as a new property instead of recycling format attribute. schema.format = schemaName; if (JsonSchemaUtils.isNonArraySchemaObject(schema)) { // JsonSchemaCache.sortSchemaProperties(schema); if (schema.type === "object") { if (schema.properties) { // First resolve references as JavaScript references Object.keys(schema.properties).forEach((propName) => { var _a; const prop = schema.properties[propName]; let ref = prop.$ref; if (!ref && prop.oneOf) { // HACK: Just a quick hack => Go through in more detail and make more generic (oneOf, allOf etc.) // Also add support for JsonSchema style oneOf["string", "null"] for nullable items. ref = prop.oneOf[0].$ref; } if (ref) { if (ref.startsWith("#/components/schemas/")) { schema.properties[propName] = doc.components.schemas[ref.replace("#/components/schemas/", "")]; } else if (ref.startsWith("#/definitions/")) { schema.properties[propName] = doc.components.schemas[ref.replace("#/definitions/", "")]; } else { console.error(`Unhandled $ref: ${ref}`); } } else if (JsonSchemaUtils.isArraySchemaObject(prop)) { const arrayRef = (_a = prop.items) === null || _a === void 0 ? void 0 : _a.$ref; if (arrayRef) { if (arrayRef.startsWith("#/components/schemas/")) { prop.items = doc.components.schemas[arrayRef.replace("#/components/schemas/", "")]; } else if (arrayRef.startsWith("#/definitions/")) { prop.items = doc.components.schemas[arrayRef.replace("#/definitions/", "")]; } else { console.error(`Unhandled array items $ref in ${propName}: ${ref}`); } } } }); // Handle differences in Swagger/OpenApi 2, OpenApi 3 and Json Schema Object.keys(schema.properties).forEach((propName) => { const prop = schema.properties[propName]; if (prop.readonly != null) { prop.readOnly = prop.readonly; } else if (prop["x-readonly"] != null) { prop.readOnly = prop["x-readonly"]; } // TODO: Consider other cases, at least nullability. // See logic in NJsonSchema: https://github.com/RicoSuter/NJsonSchema/blob/master/src/NJsonSchema/JsonSchema.Serialization.cs }); } } } }); } /** * Sorts the properties in JSON schema according to some misc. logic rules * (e.g. id first, objects and arrays lower and unknown last). * TODO: We could potentially put this in another place: Server-side? UI framework? */ static sortSchemaProperties(schema) { if (schema.properties) { const orderedArray = Object.keys(schema.properties).map((key) => { const prop = schema.properties[key]; const orderResult = { key, prop, sort: 50, }; switch (prop.type) { case "number": case "integer": case "boolean": break; case "string": if (key.endsWith("At")) { orderResult.sort = 20; } else if (key === "id") { orderResult.sort = 10; } break; case "array": orderResult.sort = 70; break; case "object": { switch (prop.format) { case "DateRange": case "Avatar": orderResult.sort = 50; break; default: orderResult.sort = 60; break; } } break; default: orderResult.sort = 80; break; } return orderResult; }); const ordered = {}; orderedArray.sort((a, b) => (a.sort.toString() + a.key).localeCompare(b.sort.toString() + b.key)) .forEach((x) => { ordered[x.key] = x.prop; }); schema.properties = ordered; } } /** * Assures that a schema document identified by URL is loaded from server to the cache. * Currently, supports only OpenApi 3, but we may later support OpenApi 2 and/or plain JSON schema. * @param openApiUrl URL to the open API document. * @returns A promise that resolves when the document has been loaded and you can call other methods on it. */ assureSchemaDocument(openApiUrl) { const cachedSchema = this.schemaCache.find((x) => x.url === openApiUrl); if (cachedSchema) { if (cachedSchema.loadingDonePromise != null) { return cachedSchema.loadingDonePromise; } return Promise.resolve(cachedSchema.doc); } const loadingDonePromise = new Promise((resolve, reject) => { this.ajax.getJSON(openApiUrl).then((data) => { if (!data) { throw new Error("No data from the schema: " + openApiUrl); } resolve(this.addSchemaDocument(openApiUrl, data)); }).catch((reason) => { reject(reason); }); }); this.schemaCache.push({ url: openApiUrl, type: "OpenAPIV3", doc: null, loadingDonePromise, }); return loadingDonePromise; } /** * Finds a schema document from the cache. * This method is syncronous base method for other schema operations * Make sure that the schme document has been loaded before calling as this will not load the document, * but will fail instead if the schema document is not there. * @param openApiUrl URL to the open API document. * @param throwIfNotFound If true, will throw an error if the document is not found in the cache. */ findSchemaDoc(openApiUrl, throwIfNotFound) { openApiUrl = openApiUrl.toLowerCase().trim(); if (!openApiUrl) { throw new Error("Empty Open API URL."); } const result = this.schemaCache.find((x) => x.url === openApiUrl); if (!result && throwIfNotFound) { throw new Error("Schema document not loaded: " + openApiUrl); } return result; } /** * Finds a single schema (Data model) within a schema document * Make sure that the schme document has been loaded before calling as this will not load the document, * but will fail instead if the schema document is not there. * @param openApiUrl URL to the open API document. * @param name Name of the schema (data model) */ findSchema(openApiUrl, name) { var _a, _b; const schema = (_b = (_a = this.findSchemaDoc(openApiUrl, true)) === null || _a === void 0 ? void 0 : _a.doc.components) === null || _b === void 0 ? void 0 : _b.schemas[name]; if (!schema) { return null; } if (JsonSchemaUtils.isReferenceObject(schema)) { throw new Error(`Unresolved reference in ${openApiUrl} - ${name}`); } return schema; } } /** * Helpers for handling arrays. */ class Arrays { /** * Takes in an array or string or null and always returns an array. * If the incoming value is a string, splits it by commas. This is especaially useful for HTML attributes. * Note that null (or other falsy objects) are returned as an empty array. * * @param arrayCandidate - A string array or string that is split by comma. */ static assureArray(arrayCandidate) { if (!arrayCandidate) { return []; } if (Array.isArray(arrayCandidate)) { return arrayCandidate; } return arrayCandidate.split(","); } /** * Calculates a sum of array items. * Null items will be added as zero. * @param array Array to sum * @param selector Selector for a number to sum. */ static sum(array, selector) { // eslint-disable-next-line @typescript-eslint/no-unused-vars return array.reduce((prev, current, ix) => current ? (prev + (selector(current) || 0)) : 0, 0); } /** * Sequence generator function gets an array between two numbers (inclusive). * (commonly referred to as "range", e.g. Clojure, PHP etc) * @param start Start number of the sequence * @param stop End number of the sequence * @param step Step between the numbers, default is 1. * @example * Arrays.getRange(2019, 2023); * // [2019, 2020, 2021, 2022, 2023] */ static getRange(start, stop, step = 1) { const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + (i * step)); return range(start, stop, step); } } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function commonjsRequire (path) { throw new Error('Could not dynamically require "' + path + '". Pl