@salaxy/jquery
Version:
Salaxy general plain JavaScript / TypeScript libraries with JQuery -ajax component (Palkkaus.fi)
1,098 lines (1,088 loc) • 3.98 MB
JavaScript
(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