Skip to content
Snippets Groups Projects
Select Git revision
  • c590f9945dff54e7250c6266669a79ed5321ec4e
  • 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

main.js

Blame
  • restapi.mjs 8.24 KiB
    
    
    /**
     * Copyright schukai GmbH and contributors 2022. All Rights Reserved.
     * Node module: @schukai/monster
     * This file is licensed under the AGPLv3 License.
     * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
     */
    
    
    import {internalSymbol} from "../../constants.mjs";
    import {isObject} from "../../types/is.mjs";
    import {Datasource} from "../datasource.mjs";
    import {Pathfinder} from "../pathfinder.mjs";
    import {Pipe} from "../pipe.mjs";
    import {WriteError} from "./restapi/writeerror.mjs";
    
    export {RestAPI}
    
    /**
     * The RestAPI is a class that enables a REST API server.
     * 
     * ```
     * <script type="module">
     * import {RestAPI} from '@schukai/monster/source/data/datasource/restapi.mjs';
     * new RestAPI()
     * </script>
     * ```
     *
     * @example
     *
     * import {RestAPI} from '@schukai/monster/source/data/datasource/restapi.mjs';
     *
     * const ds = new RestAPI({
     *   url: 'https://httpbin.org/get'
     * },{
     *   url: 'https://httpbin.org/post'
     * });
     *
     * ds.set({flag:true})
     * ds.write().then(()=>console.log('done'));
     * ds.read().then(()=>console.log('done'));
     *
     * @license AGPLv3
     * @since 1.22.0
     * @copyright schukai GmbH
     * @memberOf Monster.Data.Datasource
     * @summary The LocalStorage class encapsulates the access to data objects.
     */
    class RestAPI extends Datasource {
    
        /**
         *
         * @param {Object} [readDefinition] An options object containing any custom settings that you want to apply to the read request.
         * @param {Object} [writeDefinition] An options object containing any custom settings that you want to apply to the write request.
         * @throws {TypeError} value is not a string
         */
        constructor(readDefinition, writeDefinition) {
            super();
    
            const options = {}
    
            if (isObject(readDefinition)) options.read = readDefinition;
            if (isObject(writeDefinition)) options.write = writeDefinition;
    
            this.setOptions(options);
    
        }
    
        /**
         * @property {string} url=undefined Defines the resource that you wish to fetch.
         * @property {Object} write={} Options
         * @property {Object} write.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor}
         * @property {string} write.init.method=POST
         * @property {string} write.acceptedStatus=[200,201]
         * @property {string} write.url URL
         * @property {Object} write.mapping the mapping is applied before writing.
         * @property {String} write.mapping.transformer Transformer to select the appropriate entries
         * @property {Object} write.report
         * @property {String} write.report.path Path to validations
         * @property {Monster.Data.Datasource~exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing.
         * @property {Object} read.init={} An options object containing any custom settings that you want to apply to the request. The parameters are identical to those of the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/Request|Request constructor}
         * @property {string} read.init.method=GET
         * @property {string} read.acceptedStatus=[200]
         * @property {string} read.url URL
         * @property {Object} read.mapping the mapping is applied after reading.
         * @property {String} read.mapping.transformer Transformer to select the appropriate entries
         * @property {Monster.Data.Datasource~exampleCallback[]} read.mapping.callback with the help of the callback, the structures can be adjusted after reading.
         */
        get defaults() {
            return Object.assign({}, super.defaults, {
                write: {
                    init: {
                        method: 'POST',
                    },
                    acceptedStatus: [200, 201],
                    url: undefined,
                    mapping: {
                        transformer: undefined,
                        callbacks: []
                    },
                    report: {
                        path: undefined
                    }
                },
                read: {
                    init: {
                        method: 'GET'
                    },
                    acceptedStatus: [200],
                    url: undefined,
                    mapping: {
                        transformer: undefined,
                        callbacks: []
                    },
                },
    
            });
        }
    
        /**
         * @return {Promise}
         * @throws {Error} the options does not contain a valid json definition
         * @throws {TypeError} value is not a object
         * @throws {Error} the data cannot be read
         */
        read() {
            const self = this;
            let response;
    
            let init = self.getOption('read.init');
            if (!isObject(init)) init = {};
    
            return fetch(self.getOption('read.url'), init).then(resp => {
                response = resp;
    
                const acceptedStatus = self.getOption('read.acceptedStatus', [200]);
    
                if (acceptedStatus.indexOf(resp.status) === -1) {
                    throw Error('the data cannot be read (response ' + resp.status + ')')
                }
    
                return resp.text()
            }).then(body => {
    
                let obj;
    
                try {
                    obj = JSON.parse(body);
    
                } catch (e) {
    
                    if (body.length > 100) {
                        body = body.substring(0, 97) + '...';
                    }
    
                    throw new Error('the response does not contain a valid json (actual: ' + body + ').');
                }
    
                let transformation = self.getOption('read.mapping.transformer');
                if (transformation !== undefined) {
                    const pipe = new Pipe(transformation);
                    obj = pipe.run(obj);
                }
    
                self.set(obj);
                return response;
            })
        }
    
        /**
         * @return {Promise}
         * @throws {WriteError} the data cannot be written
         */
        write() {
            const self = this;
    
    
            let init = self.getOption('write.init');
            if (!isObject(init)) init = {};
            if (typeof init['headers'] !== 'object') {
                init['headers'] = {
                    'Content-Type': 'application/json'
                }
            }
    
            let obj = self.get();
            let transformation = self.getOption('write.mapping.transformer');
            if (transformation !== undefined) {
                const pipe = new Pipe(transformation);
                obj = pipe.run(obj);
            }
    
            let sheathingObject = self.getOption('write.sheathing.object');
            let sheathingPath = self.getOption('write.sheathing.path');
            let reportPath = self.getOption('write.report.path');
    
            if (sheathingObject && sheathingPath) {
                const sub = obj;
                obj = sheathingObject;
                (new Pathfinder(obj)).setVia(sheathingPath, sub);
            }
    
            init['body'] = JSON.stringify(obj);
    
            return fetch(self.getOption('write.url'), init).then(response => {
    
                const acceptedStatus = self.getOption('write.acceptedStatus', [200, 2001]);
    
                if (acceptedStatus.indexOf(response.status) === -1) {
    
                    return response.text().then((body) => {
    
                        let obj, validation;
                        try {
                            obj = JSON.parse(body);
                            validation = new Pathfinder(obj).getVia(reportPath)
    
                        } catch (e) {
    
                            if (body.length > 100) {
                                body = body.substring(0, 97) + '...';
                            }
    
                            throw new Error('the response does not contain a valid json (actual: ' + body + ').');
                        }
    
                        throw new WriteError('the data cannot be written (response ' + response.status + ')', response, validation)
    
                    })
    
    
                }
    
                return response;
            });
        }
    
    
        /**
         * @return {RestAPI}
         */
        getClone() {
            const self = this;
            return new RestAPI(self[internalSymbol].getRealSubject()['options'].read, self[internalSymbol].getRealSubject()['options'].write);
        }
    
    }
    
    
    /**
     * This callback can be passed to a datasource and is used to adapt data structures.
     *
     * @callback Monster.Data.Datasource~exampleCallback
     * @param {*} value Value
     * @param {string} key  Key
     * @memberOf Monster.Data
     * @see Monster.Data.Datasource
     */