@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
251 lines (225 loc) • 5.65 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 } from "../constants.mjs";
import { instanceSymbol } from "../constants.mjs";
import { Base } from "../types/base.mjs";
import { parseDataURL } from "../types/dataurl.mjs";
import { isString } from "../types/is.mjs";
import { ProxyObserver } from "../types/proxyobserver.mjs";
import { validateObject } from "../types/validate.mjs";
import { diff } from "./diff.mjs";
import { extend } from "./extend.mjs";
import { Pathfinder } from "./pathfinder.mjs";
export { Datasource };
/**
* 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
* @see Monster.Data.Datasource
*/
/**
* @private
* @type {symbol}
* @license AGPLv3
* @since 1.24.0
*/
const internalDataSymbol = Symbol.for(
"@schukai/monster/data/datasource/@@data",
);
/**
* The datasource class is the basis for dealing with different data sources.
* It provides a unified interface for accessing data
* @externalExample ../../example/data/datasource.mjs
* @license AGPLv3
* @since 1.22.0
* @copyright Volker Schukai
* @summary The datasource class encapsulates the access to data objects.
*/
class Datasource extends Base {
/**
* creates a new datasource
*
*/
constructor() {
super();
this[internalSymbol] = new ProxyObserver({
options: extend({}, this.defaults),
});
this[internalDataSymbol] = new ProxyObserver({});
}
/**
* attach a new observer
*
* @param {Observer} observer
* @return {Datasource}
*/
attachObserver(observer) {
this[internalDataSymbol].attachObserver(observer);
return this;
}
/**
* detach a observer
*
* @param {Observer} observer
* @return {Datasource}
*/
detachObserver(observer) {
this[internalDataSymbol].detachObserver(observer);
return this;
}
/**
* @param {Observer} observer
* @return {boolean}
*/
containsObserver(observer) {
return this[internalDataSymbol].containsObserver(observer);
}
/**
* Derived classes can override and extend this method as follows.
*
* ```
* get defaults() {
* return Object.assign({}, super.defaults, {
* myValue:true
* });
* }
* ```
*/
get defaults() {
return {
features: {
// Feature flag: default-on no-op event suppression, can be rolled back.
skipNoopEvents: true,
},
};
}
/**
* Set option
*
* @param {string} path
* @param {*} value
* @return {Datasource}
*/
setOption(path, value) {
new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia(
path,
value,
);
return this;
}
/**
* @param {string|object} options
* @return {Datasource}
* @throws {Error} the options does not contain a valid json definition
*/
setOptions(options) {
if (isString(options)) {
options = parseOptionsJSON(options);
}
extend(
this[internalSymbol].getSubject()["options"],
this.defaults,
options,
);
return this;
}
/**
* nested options can be specified by path `a.b.c`
*
* @param {string} path
* @param {*} defaultValue
* @return {*}
*/
getOption(path, defaultValue) {
let value;
try {
value = new Pathfinder(
this[internalSymbol].getRealSubject()["options"],
).getVia(path);
} catch (e) {}
if (value === undefined) return defaultValue;
return value;
}
/**
* @throws {Error} this method must be implemented by derived classes.
* @return {Promise}
*/
read() {
throw new Error("this method must be implemented by derived classes");
}
/**
* @throws {Error} this method must be implemented by derived classes.
* @return {Promise}
*/
write() {
throw new Error("this method must be implemented by derived classes");
}
/**
* Returns real object
*
* @return {Object|Array}
*/
get() {
return this[internalDataSymbol].getRealSubject();
}
/**
* @param {Object|Array} data
* @return {Datasource}
*/
set(data) {
if (this.getOption("features.skipNoopEvents") === true) {
const current = this[internalDataSymbol].getRealSubject();
if (diff(current, data).length === 0) {
return this;
}
}
this[internalDataSymbol].setSubject(data);
return this;
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
* @since 2.1.0
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/data/datasource");
}
}
/**
* @private
* @param {String} data
* @return {Object}
* @throws {Error} the options does not contain a valid json definition
*/
function parseOptionsJSON(data) {
if (isString(data)) {
// the configuration can be specified as a data url.
try {
const dataUrl = parseDataURL(data);
data = dataUrl.content;
} catch (e) {}
try {
const obj = JSON.parse(data);
validateObject(obj);
return obj;
} catch (e) {
throw new Error(
`the options does not contain a valid json definition (actual: ${data}).`,
);
}
}
return {};
}