@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
271 lines (240 loc) • 8.02 kB
JavaScript
/**
* Copyright © Volker Schukai 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 Volker Schukai.
*
* 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";
import { getInternalLocalizationMessage } from "../../../i18n/internal.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 Volker Schukai
* @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) => {
const transformedPayload = this.transformServerPayload.call(this, obj);
if (typeof transformedPayload === "undefined") {
return;
}
this.set(transformedPayload);
};
}
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(
getInternalLocalizationMessage(
`i18n{the-response-does-not-contain-an-accepted-status::status=${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(
getInternalLocalizationMessage(
`i18n{the-response-does-not-contain-a-valid-json::actual=${body}}`,
),
response,
);
}
if (callback && isFunction(callback)) {
callback(obj);
}
return response;
})
.catch((e) => {
throw e;
});
}