Something went wrong on our end
Select Git revision
andoperator.js
-
Volker Schukai authoredVolker Schukai authored
rest.mjs 14.97 KiB
/**
* Copyright 2023 schukai GmbH
* SPDX-License-Identifier: AGPL-3.0
*/
import {addAttributeToken} from "../../../dom/attributes.mjs";
import {ATTRIBUTE_ERRORMESSAGE} from "../../../dom/constants.mjs";
import {Datasource, dataSourceSymbol} from "../datasource.mjs";
import {DatasourceStyleSheet} from "../stylesheet/datasource.mjs";
import {instanceSymbol} from "../../../constants.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../../dom/customelement.mjs";
import {RestAPI} from "../../../data/datasource/server/restapi.mjs";
import {Formatter} from "../../../text/formatter.mjs";
import {clone} from "../../../util/clone.mjs";
import {validateBoolean} from "../../../types/validate.mjs";
import {findElementWithIdUpwards} from "../../../dom/util.mjs";
import {Observer} from "../../../types/observer.mjs";
import {Pathfinder} from "../../../data/pathfinder.mjs";
import {fireCustomEvent} from "../../../dom/events.mjs";
export {Rest};
/**
* @private
* @type {symbol}
*/
const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler");
/**
* @private
* Original at source/components/datatable/datasource/rest.mjs
* @type {symbol}
*/
const rawDataSymbol = Symbol.for(
"@schukai/monster/data/datasource/server/restapi/rawdata",
);
/**
* @private
* @type {symbol}
*/
const intersectionObserverObserverSymbol = Symbol(
"intersectionObserverObserver",
);
/**
* @private
* @type {symbol}
*/
const filterObserverSymbol = Symbol("filterObserver");
/**
* The Datasource component is a basic class for the datatable component.
*
* <img src="./images/rest.png">
*
* Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
*
* @startuml rest.png
* skinparam monochrome true
* skinparam shadowing false
* HTMLElement <|-- CustomElement
* CustomElement <|-- Datasource
* Datasource <|-- Rest
* @enduml
*
* @copyright schukai GmbH
* @memberOf Monster.Components.Datatable.Datasource
* @summary A rest api datasource
*/
class Rest extends Datasource {
/**
* the constructor of the class
*/
constructor() {
super();
this[dataSourceSymbol] = new RestAPI();
}
/**
* This method is called by the `instanceof` operator.
* @returns {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/datasource/rest@@instance");
}
/**
* To set the options via the html tag the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* The individual configuration values can be found in the table.
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {Object} features Feature definitions
* @property {boolean} features.autoInit If true, the component is initialized automatically
* @property {boolean} features.filter If true, the component is initialized automatically
* @property {Object} autoInit Auto init definitions
* @property {boolean} autoInit.intersectionObserver If true, the intersection observer is initialized automatically
* @property {boolean} autoInit.oneTime If true, the intersection observer is initialized only once
* @property {Object} filter Filter definitions
* @property {string} filter.id The id of the filter control
* @property {Object} datatable Datatable definitions
* @property {string} datatable.id The id of the datatable control
* @property {Object} response Response definitions
* @property {Object} response.path Path definitions (changed in 3.56.0)
* @property {string} response.path.message Path to the message (changed in 3.56.0)
* @property {Object} read Read configuration
* @property {string} read.url The url of the rest api
* @property {string} read.method The method of the rest api
* @property {Object} read.parameters The parameters of the rest api
* @property {Object} read.parameters.filter The filter of the rest api
* @property {Object} read.parameters.orderBy The order by of the rest api
* @property {Object} read.parameters.page The page of the rest api
* @property {Object} write Write configuration
*/
get defaults() {
const restOptions = new RestAPI().defaults;
restOptions.read.parameters = {
filter: undefined,
oderBy: undefined,
page: "1",
};
return Object.assign({}, super.defaults, restOptions, {
templates: {
main: getTemplate(),
},
features: {
autoInit: false,
filter: false,
},
autoInit: {
intersectionObserver: false,
oneTime: true,
},
filter: {
id: undefined,
},
datatable: {
id: undefined,
},
response: {
path: {
message: "sys.message",
code: "sys.code",
}
},
});
}
/**
*
* @param {string} page
* @param {string} query
* @param {string} orderBy
* @returns {Monster.Components.Datatable.Datasource.Rest}
*/
setParameters({page, query, orderBy}) {
const parameters = this.getOption("read.parameters");
if (query !== undefined) {
parameters.query = `${query}`;
parameters.page = "1";
}
// after a query the page is set to 1, so if the page is not set, it is set to 1
if (page !== undefined) parameters.page = `${page}`;
if (orderBy !== undefined) parameters.order = `${orderBy}`;
this.setOption("read.parameters", parameters);
return this;
}
/**
*
* @return {Monster.Components.Form.Form}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initEventHandler.call(this);
initAutoInit.call(this);
}
/**
* @deprecated 2023-06-25
* @returns {Promise<never>|*}
*/
reload() {
return this.fetch();
}
/**
* Fetches the data from the rest api
* @returns {Promise<never>|*}
*/
fetch() {
const opt = clone(this.getOption("read"));
this[dataSourceSymbol].setOption("read", opt);
let url = this.getOption("read.url");
const formatter = new Formatter(this.getOption("read.parameters"));
if (!url) {
return Promise.reject(new Error("No url defined"));
}
url = formatter.format(url);
this[dataSourceSymbol].setOption("read.url", url);
return new Promise((resolve, reject) => {
fireCustomEvent(this, "monster-datasource-fetch", {
datasource: this,
});
setTimeout(() => {
this[dataSourceSymbol]
.read()
.then((response) => {
fireCustomEvent(this, "monster-datasource-fetched", {
datasource: this,
});
resolve(response);
})
.catch((error) => {
fireCustomEvent(this, "monster-datasource-error", {
error: error,
});
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
reject(error);
});
}, 0);
});
}
/**
*
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [DatasourceStyleSheet];
}
/**
* @private
* @return {string}
*/
static getTag() {
return "monster-datasource-rest";
}
/**
* This method activates the intersection observer manually.
* For this purpose, the option `autoInit.intersectionObserver` must be set to `false`.
*
* @returns {Monster.Components.Datatable.Datasource.Rest}
*/
initIntersectionObserver() {
initIntersectionObserver.call(this);
return this;
}
/**
* @private
*/
connectedCallback() {
super.connectedCallback();
setTimeout(() => {
if (this.getOption("features.filter", false) === true) {
initFilter.call(this);
}
}, 0);
}
/**
* @private
*/
disconnectedCallback() {
super.disconnectedCallback();
removeFilter.call(this);
}
read() {
return this.fetch();
}
/**
* Fetches the data from the rest api
* @returns {Promise<never>|*}
*/
write() {
const opt = clone(this.getOption("write"));
this[dataSourceSymbol].setOption("write", opt);
let url = this.getOption("write.url");
const formatter = new Formatter(this.getOption("write.parameters"));
if (!url) {
return Promise.reject(new Error("No url defined"));
}
url = formatter.format(url);
this[dataSourceSymbol].setOption("write.url", url);
return new Promise((resolve, reject) => {
fireCustomEvent(this, "monster-datasource-fetch", {
datasource: this,
});
setTimeout(() => {
this[dataSourceSymbol]
.write()
.then((response) => {
fireCustomEvent(this, "monster-datasource-fetched", {
datasource: this,
});
resolve(response);
})
.catch((error) => {
fireCustomEvent(this, "monster-datasource-error", {
error: error,
});
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
reject(error);
});
}, 0);
});
}
}
/**
* @private
*/
function removeFilter() {
const filterID = this.getOption("filter.id", undefined);
if (!filterID) return;
const filterControl = findElementWithIdUpwards(this, filterID);
if (filterControl && this[filterObserverSymbol]) {
filterControl?.detachObserver(this[filterObserverSymbol]);
}
}
/**
* @private
*/
function initFilter() {
const filterID = this.getOption("filter.id", undefined);
if (!filterID)
throw new Error("filter feature is enabled but no filter id is defined");
const filterControl = findElementWithIdUpwards(this, filterID);
if (!filterControl)
throw new Error(
"filter feature is enabled but no filter control with id " +
filterID +
" is found",
);
this[filterObserverSymbol] = new Observer(() => {
const query = filterControl.getOption("query", undefined);
this.setParameters({query: query});
this.fetch()
.then((response) => {
if (!(response instanceof Response)) {
throw new Error("Response is not an instance of Response");
}
if (response?.ok === true) {
this.dispatchEvent(new CustomEvent("reload", {bubbles: true}));
filterControl?.showSuccess();
}
if (response.bodyUsed === true) {
return handleIntersectionObserver.call(this, response[rawDataSymbol], response, filterControl);
}
response
.text()
.then((jsonAsText) => {
let json;
try {
json = JSON.parse(jsonAsText);
} catch (e) {
let message = e instanceof Error ? e.message : `${e}`;
filterControl?.showFailureMessage(message);
return Promise.reject(e);
}
return handleIntersectionObserver.call(this, json, response, filterControl);
})
.catch((e) => {
filterControl?.showFailureMessage(e.message);
});
})
.catch((e) => {
this.dispatchEvent(
new CustomEvent("error", {bubbles: true, detail: e}),
);
if (!(e instanceof Error)) {
e = new Error(e);
}
filterControl?.showFailureMessage(e.message);
return Promise.reject(e);
});
});
filterControl.attachObserver(this[filterObserverSymbol]);
}
function handleIntersectionObserver(json, response, filterControl) {
const path = new Pathfinder(json);
const codePath = this.getOption("response.path.code");
if (path.exists(codePath)) {
const code = `${path.getVia(codePath)}`;
if (code && code === "200") {
filterControl?.showSuccess();
return Promise.resolve(response);
}
const messagePath = this.getOption("response.path.message");
if (path.exists(messagePath)) {
const message = path.getVia(messagePath);
filterControl?.showFailureMessage(message);
return Promise.reject(new Error(message));
}
return Promise.reject(new Error("Response code is not 200"));
}
}
/**
* @private
*/
function initAutoInit() {
const autoInit = this.getOption("features.autoInit");
validateBoolean(autoInit);
if (autoInit !== true) return;
if (this.getOption("autoInit.intersectionObserver") === true) {
initIntersectionObserver.call(this);
return;
}
setTimeout(() => {
this.fetch().catch(() => {
});
}, 0);
}
function initEventHandler() {
this[intersectionObserverHandlerSymbol] = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (entry.intersectionRatio > 0) {
this.fetch();
}
// only load once
if (
this.getOption("autoInit.oneTime") === true &&
this[intersectionObserverObserverSymbol] !== undefined
) {
this[intersectionObserverObserverSymbol].unobserve(this);
}
}
});
};
}
function initIntersectionObserver() {
this.classList.add("intersection-observer");
const options = {
root: null,
rootMargin: "0px",
threshold: 0.1,
};
this[intersectionObserverObserverSymbol] = new IntersectionObserver(
this[intersectionObserverHandlerSymbol],
options,
);
this[intersectionObserverObserverSymbol].observe(this);
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<slot></slot>`;
}
registerCustomElement(Rest);