Something went wrong on our end
Select Git revision
update-files.nix
-
Volker Schukai authoredVolker Schukai authored
rest.mjs 14.47 KiB
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { diff } from "../../../data/diff.mjs";
import { addAttributeToken } from "../../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
import { isArray } from "../../../types/is.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");
/**
* A rest api datasource
*
* @fragments /fragments/components/datatable/datasource/rest
*
* @example /examples/components/datatable/datasource-rest-simple Simple Rest datasource
* @example /examples/components/datatable/datasource-rest-auto-init Auto init
* @example /examples/components/datatable/datasource-rest-do-fetch Rest datasource with fetch
*
* @issue https://localhost.alvine.dev:8443/development/issues/closed/272.html
*
* @copyright schukai GmbH
* @summary A rest api datasource for the datatable or other components
*/
class Rest extends Datasource {
/**
* the constructor of the class
*/
constructor() {
super();
this[dataSourceSymbol] = new RestAPI();
}
/**
* This method is called by the `instanceof` operator.
* @return {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} 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 {string} read.mapping.currentPage The current page
* @property {Object} write Write configuration
* @property {string} write.url The url of the rest api
* @property {string} write.method The method of the rest api
* @property {Object} write Write configuration
*/
get defaults() {
const restOptions = new RestAPI().defaults;
restOptions.read.parameters = {
filter: null,
oderBy: null,
page: "1",
};
restOptions.read.mapping.currentPage = "sys.pagination.currentPage";
return Object.assign({}, super.defaults, restOptions, {
templates: {
main: getTemplate(),
},
features: {
autoInit: false,
filter: false,
},
autoInit: {
intersectionObserver: false,
oneTime: true,
},
filter: {
id: null,
},
/*datatable: {
id: undefined, // not used?
}, */
response: {
path: {
message: "sys.message",
code: "sys.code",
},
},
});
}
/**
* With this method, you can set the parameters for the rest api. The parameters are
* used for building the url.
*
* @param {string} page
* @param {string} query
* @param {string} orderBy
* @return {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;
}
/**
* @private
* @return {void}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initEventHandler.call(this);
initAutoInit.call(this);
}
/**
* This method reloads the data from the rest api, this method is deprecated.
* You should use the method `read` instead.
*
* @deprecated 2023-06-25
* @return {Promise<never>|*}
*/
reload() {
return this.read();
}
/**
* Fetches the data from the rest api, this method is deprecated.
* You should use the method `read` instead.
*
* @deprecated 2024-12-24
* @return {Promise<never>|*}
*/
fetch() {
return this.read();
}
/**
*
* @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`.
*
* @return {Rest}
*/
initIntersectionObserver() {
initIntersectionObserver.call(this);
return this;
}
/**
* @private
*/
connectedCallback() {
super.connectedCallback();
queueMicrotask(() => {
if (this.getOption("features.filter", false) === true) {
initFilter.call(this);
}
});
}
/**
* @private
*/
disconnectedCallback() {
super.disconnectedCallback();
removeFilter.call(this);
}
/**
* This method reads the data from the rest api.
* The data is stored in the internal dataset object.
*
* @return {Promise}
* @fires monster-datasource-fetch
* @fires monster-datasource-fetched
* @fires monster-datasource-error
*/
read() {
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,
});
queueMicrotask(() => {
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);
});
});
});
}
/**
* Fetches the data from the rest api.
* @return {Promise}
*/
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,
});
queueMicrotask(() => {
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);
});
});
});
}
// /**
// * @return {int}
// */
// currentPage() {
//
// const key = this.getOption("read.mapping.currentPage")
// if (key === undefined) {
// return 1;
// }
//
// const pf = new Pathfinder(this.data);
// if (pf.exists(key)) {
// return parseInt(pf.getVia(key), 10);
// }
//
// return 1;
//
// }
}
/**
* @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");
if (query === undefined) {
return;
}
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) {
const 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]);
}
/**
* @private
* @param json
* @param response
* @param filterControl
* @returns {Promise<never>|Promise<Awaited<unknown>>}
*/
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;
}
queueMicrotask(() => {
this.fetch().catch(() => {});
});
}
/**
* @private
*/
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);
}
}
});
};
}
/**
* @private
*/
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);