/** * 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 { internalSymbol, instanceSymbol } from "../../../constants.mjs"; import { isObject, isFunction, isArray } from "../../../types/is.mjs"; import { diff } from "../../diff.mjs"; import { Server } from "../server.mjs"; import { WriteError } from "./restapi/writeerror.mjs"; import { DataFetchError } from "./restapi/data-fetch-error.mjs"; import { clone } from "../../../util/clone.mjs"; export { RestAPI }; /** * @type {symbol} * @license AGPLv3 * @since 3.12.0 */ const rawDataSymbol = Symbol.for( "@schukai/monster/data/datasource/server/restapi/rawdata", ); /** * The RestAPI is a class that enables a REST API server. * * @externalExample ../../../../example/data/datasource/server/restapi.mjs * @license AGPLv3 * @since 1.22.0 * @copyright schukai GmbH * @summary The RestAPI is a class that binds a REST API server. */ class RestAPI extends Server { /** * * @param {Object} [options] options contains definitions for the datasource. */ constructor(options) { super(); if (isObject(options)) { this.setOptions(options); } } /** * This method is called by the `instanceof` operator. * @return {symbol} * @since 2.1.0 */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/data/datasource/server/restapi"); } /** * @property {Object} write={} Options * @property {Object} write.init={} An option 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 {Headers} write.init.headers Object containing any custom headers that you want to apply to the request. * @property {string} write.responseCallback Callback function to be executed after the request has been completed. * @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 {exampleCallback[]} write.mapping.callback with the help of the callback, the structures can be adjusted before writing. * @property {Object} write.report * @property {String} write.report.path Path to validations * @property {Object} write.partial * @property {Function} write.partial.callback Callback function to be executed after the request has been completed. (obj, diffResult) => obj * @property {Object} write.sheathing * @property {Object} write.sheathing.object Object to be wrapped * @property {string} write.sheathing.path Path to the data * @property {Object} read={} Options * @property {Object} read.init={} An option 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 {array} 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 {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", headers: { Accept: "application/json", "Content-Type": "application/json", }, }, responseCallback: null, acceptedStatus: [200, 201], url: null, mapping: { transformer: null, callbacks: [], }, sheathing: { object: null, path: null, }, report: { path: null, }, partial: { callback: null, }, }, read: { init: { method: "GET", headers: { Accept: "application/json", }, }, path: null, responseCallback: null, acceptedStatus: [200], url: null, mapping: { transformer: null, 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() { let init = this.getOption("read.init"); if (!isObject(init)) init = {}; if (!(init["headers"] instanceof Headers) && !isObject(init["headers"])) { init["headers"] = new Headers(); init["headers"].append("Accept", "application/json"); } if (!init["method"]) init["method"] = "GET"; let callback = this.getOption("read.responseCallback"); if (!callback) { callback = (obj) => { this.set(this.transformServerPayload.call(this, obj)); }; } return fetchData.call(this, init, "read", callback); } /** * @return {Promise} * @throws {WriteError} the data cannot be written */ write() { let init = this.getOption("write.init"); if (!isObject(init)) init = {}; if (!(init["headers"] instanceof Headers) && !isObject(init["headers"])) { init["headers"] = new Headers(); init["headers"].append("Accept", "application/json"); init["headers"].append("Content-Type", "application/json"); } if (!init["method"]) init["method"] = "POST"; const obj = this.prepareServerPayload(this.get()); init["body"] = JSON.stringify(obj); const callback = this.getOption("write.responseCallback"); return fetchData.call(this, init, "write", callback); } /** * @return {RestAPI} */ getClone() { const api = new RestAPI(); const read = clone(this[internalSymbol].getRealSubject()["options"].read); const write = clone(this[internalSymbol].getRealSubject()["options"].write); api.setOption("read", read); api.setOption("write", write); return api; } } /** * @private * @param init * @param key * @param callback * @return {Promise<string>} */ function fetchData(init, key, callback) { let response; if (init?.headers === null) { init.headers = new Headers(); } return fetch(this.getOption(`${key}.url`), init) .then((resp) => { response = resp; const acceptedStatus = this.getOption(`${key}.acceptedStatus`, [200]).map( Number, ); if (acceptedStatus.indexOf(resp.status) === -1) { throw new DataFetchError( `the response does not contain an accepted status (actual: ${resp.status}).`, response, ); } return resp.text(); }) .then((body) => { let obj; try { obj = JSON.parse(body); response[rawDataSymbol] = obj; } catch (e) { if (body.length > 100) { body = `${body.substring(0, 97)}...`; } throw new DataFetchError( `the response does not contain a valid json (actual: ${body}).`, response, ); } if (callback && isFunction(callback)) { callback(obj); } return response; }) .catch((e) => { throw e; }); }