mirror of
https://github.com/appy-one/acebase.git
synced 2026-06-30 06:02:02 -06:00
309 lines
No EOL
11 KiB
JavaScript
309 lines
No EOL
11 KiB
JavaScript
const { Storage } = require('./storage');
|
|
const { NodeInfo } = require('./node-info');
|
|
const { VALUE_TYPES, getValueTypeName } = require('./node-value-types');
|
|
const colors = require('colors');
|
|
|
|
class Node {
|
|
static get VALUE_TYPES() { return VALUE_TYPES; }
|
|
|
|
/**
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.no_cache=false] Whether to use cache for lookups
|
|
* @returns {Promise<NodeInfo>} promise that resolves with info about the node
|
|
*/
|
|
static getInfo(storage, path, options = { no_cache: false }) {
|
|
|
|
// Check if the info has been cached
|
|
if (options && !options.no_cache) {
|
|
let cachedInfo = storage.nodeCache.find(path);
|
|
if (cachedInfo) {
|
|
return Promise.resolve(cachedInfo);
|
|
}
|
|
}
|
|
|
|
// Cache miss. Check if node is being looked up already
|
|
return storage.getNodeInfo(path)
|
|
.then(info => {
|
|
if (options && !options.no_cache) {
|
|
storage.nodeCache.update(info);
|
|
}
|
|
return info;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates or overwrite an existing node, or creates a new node. Handles storing of subnodes,
|
|
* freeing old node and subnodes allocation, updating/creation of parent nodes, and removing
|
|
* old cache entries. Triggers event notifications and index updates after the update succeeds.
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {any} value Any value will do. If the value is small enough to be stored in a parent record, it will take care of it
|
|
* @param {object} [options]
|
|
* @param {boolean} [options.merge=true] whether to merge or overwrite the current value if node exists
|
|
*/
|
|
static update(storage, path, value, options = { merge: true }) {
|
|
|
|
// debug.log(`Update request for node "/${path}"`);
|
|
|
|
if (options.merge) {
|
|
return storage.updateNode(path, value);
|
|
}
|
|
else {
|
|
return storage.setNode(path, value);
|
|
}
|
|
}
|
|
|
|
/** Checks if a node exists
|
|
*
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
static exists(storage, path) {
|
|
return storage.getNodeInfo(path)
|
|
.then(nodeInfo => {
|
|
return nodeInfo.exists;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets the value of a node
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {object} [options] when omitted retrieves all nested data. If include is set to an array of keys it will only return those children. If exclude is set to an array of keys, those values will not be included
|
|
* @param {string[]} [options.include] keys to include
|
|
* @param {string[]} [options.exclude] keys to exclude
|
|
* @param {boolean} [options.child_objects=true] whether to include child objects
|
|
* @returns {Promise<any>}
|
|
*/
|
|
static getValue(storage, path, options = { include: undefined, exclude: undefined, child_objects: true }) {
|
|
if (!options) { options = {}; }
|
|
if (typeof options.include !== "undefined" && !(options.include instanceof Array)) {
|
|
throw new TypeError(`options.include must be an array of key names`);
|
|
}
|
|
if (typeof options.exclude !== "undefined" && !(options.exclude instanceof Array)) {
|
|
throw new TypeError(`options.exclude must be an array of key names`);
|
|
}
|
|
if (["undefined","boolean"].indexOf(typeof options.child_objects) < 0) {
|
|
throw new TypeError(`options.child_objects must be a boolean`);
|
|
}
|
|
return storage.getNodeValue(path, options);
|
|
}
|
|
|
|
/**
|
|
* Gets info about a child node by delegating to getChildren with keyFilter
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {string|number} childKeyOrIndex
|
|
* @returns {Promise<NodeInfo>}
|
|
*/
|
|
static getChildInfo(storage, path, childKeyOrIndex) {
|
|
let childInfo;
|
|
return storage.getChildren(path, { keyFilter: [childKeyOrIndex] })
|
|
.next(info => {
|
|
childInfo = info;
|
|
})
|
|
.then(() => {
|
|
return childInfo;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enumerates all children of a given Node for reflection purposes
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {string[]|number[]} keyFilter
|
|
* @returns {{ next(child: NodeInfo) => Promise<void>}} returns a generator object that calls .next for each child until the .next callback returns false
|
|
*/
|
|
static getChildren(storage, path, keyFilter = undefined) {
|
|
return storage.getChildren(path, { keyFilter });
|
|
}
|
|
|
|
/**
|
|
* Removes a Node. Short for Node.update with value null
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
*/
|
|
static remove(storage, path) {
|
|
return storage.removeNode(path);
|
|
}
|
|
|
|
/**
|
|
* Sets the value of a Node. Short for Node.update with option { merge: false }
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {any} value
|
|
*/
|
|
static set(storage, path, value) {
|
|
return Node.update(storage, path, value, { merge: false });
|
|
}
|
|
|
|
/**
|
|
* Performs a transaction on a Node
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {(currentValue: any) => Promise<any>} callback callback is called with the current value. The returned value (or promise) will be used as the new value. When the callbacks returns undefined, the transaction will be canceled. When callback returns null, the node will be removed.
|
|
*/
|
|
static transaction(storage, path, callback) {
|
|
return storage.transactNode(path, callback);
|
|
}
|
|
|
|
/**
|
|
* Check if a node's value matches the passed criteria
|
|
* @param {Storage} storage
|
|
* @param {string} path
|
|
* @param {Array<{ key: string, op: string, compare: string }>} criteria criteria to test
|
|
* @returns {Promise<boolean>} returns a promise that resolves with a boolean indicating if it matched the criteria
|
|
*/
|
|
static matches(storage, path, criteria, options) {
|
|
return storage.matchNode(path, criteria, options);
|
|
}
|
|
}
|
|
|
|
|
|
class NodeChange {
|
|
static get CHANGE_TYPE() {
|
|
return {
|
|
UPDATE: 'update',
|
|
DELETE: 'delete',
|
|
INSERT: 'insert'
|
|
};
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string|number} keyOrIndex
|
|
* @param {string} changeType
|
|
* @param {any} oldValue
|
|
* @param {any} newValue
|
|
*/
|
|
constructor(keyOrIndex, changeType, oldValue, newValue) {
|
|
this.keyOrIndex = keyOrIndex;
|
|
this.changeType = changeType;
|
|
this.oldValue = oldValue;
|
|
this.newValue = newValue;
|
|
}
|
|
}
|
|
|
|
class NodeChangeTracker {
|
|
/**
|
|
*
|
|
* @param {string} path
|
|
*/
|
|
constructor(path) {
|
|
this.path = path;
|
|
/** @type {NodeChange[]} */
|
|
this._changes = [];
|
|
/** @type {object|Array} */
|
|
this._oldValue = undefined;
|
|
this._newValue = undefined;
|
|
}
|
|
|
|
addDelete(keyOrIndex, oldValue) {
|
|
this._changes.push(new NodeChange(keyOrIndex, NodeChange.CHANGE_TYPE.DELETE, oldValue, null));
|
|
}
|
|
addUpdate(keyOrIndex, oldValue, newValue) {
|
|
this._changes.push(new NodeChange(keyOrIndex, NodeChange.CHANGE_TYPE.UPDATE, oldValue, newValue));
|
|
}
|
|
addInsert(keyOrIndex, newValue) {
|
|
this._changes.push(new NodeChange(keyOrIndex, NodeChange.CHANGE_TYPE.INSERT, null, newValue));
|
|
}
|
|
add(keyOrIndex, currentValue, newValue) {
|
|
if (currentValue === null) {
|
|
if (newValue === null) {
|
|
throw new Error(`Wrong logic for node change on "${this.nodeInfo.path}/${keyOrIndex}" - both old and new values are null`);
|
|
}
|
|
this.addInsert(keyOrIndex, newValue);
|
|
}
|
|
else if (newValue === null) {
|
|
this.addDelete(keyOrIndex, currentValue);
|
|
}
|
|
else {
|
|
this.addUpdate(keyOrIndex, currentValue, newValue);
|
|
}
|
|
}
|
|
|
|
get updates() {
|
|
return this._changes.filter(change => change.changeType === NodeChange.CHANGE_TYPE.UPDATE);
|
|
}
|
|
get deletes() {
|
|
return this._changes.filter(change => change.changeType === NodeChange.CHANGE_TYPE.DELETE);
|
|
}
|
|
get inserts() {
|
|
return this._changes.filter(change => change.changeType === NodeChange.CHANGE_TYPE.INSERT);
|
|
}
|
|
get all() {
|
|
return this._changes;
|
|
}
|
|
get totalChanges() {
|
|
return this._changes.length;
|
|
}
|
|
get(keyOrIndex) {
|
|
return this._changes.find(change => change.keyOrIndex === keyOrIndex);
|
|
}
|
|
hasChanged(keyOrIndex) {
|
|
return !!this.get(keyOrIndex);
|
|
}
|
|
|
|
get newValue() {
|
|
if (typeof this._newValue === 'object') { return this._newValue; }
|
|
if (typeof this._oldValue === 'undefined') { throw new TypeError(`oldValue is not set`); }
|
|
let newValue = {};
|
|
Object.keys(this.oldValue).forEach(key => newValue[key] = oldValue[key]);
|
|
this.deletes.forEach(change => delete newValue[change.key]);
|
|
this.updates.forEach(change => newValue[change.key] = change.newValue);
|
|
this.inserts.forEach(change => newValue[change.key] = change.newValue);
|
|
return newValue;
|
|
}
|
|
set newValue(value) {
|
|
this._newValue = value;
|
|
}
|
|
|
|
get oldValue() {
|
|
if (typeof this._oldValue === 'object') { return this._oldValue; }
|
|
if (typeof this._newValue === 'undefined') { throw new TypeError(`newValue is not set`); }
|
|
let oldValue = {};
|
|
Object.keys(this.newValue).forEach(key => oldValue[key] = newValue[key]);
|
|
this.deletes.forEach(change => oldValue[change.key] = change.oldValue);
|
|
this.updates.forEach(change => oldValue[change.key] = change.oldValue);
|
|
this.inserts.forEach(change => delete oldValue[change.key]);
|
|
return oldValue;
|
|
}
|
|
set oldValue(value) {
|
|
this._oldValue = value;
|
|
}
|
|
|
|
get typeChanged() {
|
|
return typeof this.oldValue !== typeof this.newValue
|
|
|| (this.oldValue instanceof Array && !(this.newValue instanceof Array))
|
|
|| (this.newValue instanceof Array && !(this.oldValue instanceof Array));
|
|
}
|
|
|
|
static create(path, oldValue, newValue) {
|
|
const changes = new NodeChangeTracker(path);
|
|
changes.oldValue = oldValue;
|
|
changes.newValue = newValue;
|
|
|
|
typeof oldValue === 'object' && Object.keys(oldValue).forEach(key => {
|
|
if (typeof newValue === 'object' && key in newValue && newValue !== null) {
|
|
changes.add(key, oldValue[key], newValue[key]);
|
|
}
|
|
else {
|
|
changes.add(key, oldValue[key], null);
|
|
}
|
|
});
|
|
typeof newValue === 'object' && Object.keys(newValue).forEach(key => {
|
|
if (typeof oldValue !== 'object' || !(key in oldValue) || oldValue[key] === null) {
|
|
changes.add(key, null, newValue[key]);
|
|
}
|
|
});
|
|
return changes;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
Node,
|
|
NodeInfo
|
|
}; |