Skip to content
Snippets Groups Projects
Select Git revision
  • 8831c5ed1e85f83461379b22edc9c830500eefdf
  • master default protected
  • 1.31
  • 4.24.3
  • 4.24.2
  • 4.24.1
  • 4.24.0
  • 4.23.6
  • 4.23.5
  • 4.23.4
  • 4.23.3
  • 4.23.2
  • 4.23.1
  • 4.23.0
  • 4.22.3
  • 4.22.2
  • 4.22.1
  • 4.22.0
  • 4.21.0
  • 4.20.1
  • 4.20.0
  • 4.19.0
  • 4.18.0
23 results

rest.mjs

Blame
  • 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);