/** * 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 */