zetabasejs
Version:
A NoSQL database for quick deployment.
255 lines (227 loc) • 8.75 kB
JavaScript
const fs = require("fs");
const DataNode = require("./libs/dataStructures/DataNode");
const Stack = require("./libs/dataStructures/Stack");
const Deprecator = require("./libs/Deprecator");
const Query = require("./libs/Query");
const Observer = require("./libs/Observer");
const { Model } = require("./orm");
const WRITE_BUFFER_TIME = 10;
class Zetabase {
constructor(path, option = {}) {
this.path = path;
this.memory = new DataNode("/", null);
this.observer = new Observer();
this.writeService = null;
this.writeBufferTime = WRITE_BUFFER_TIME;
this._start();
Model.setDBCONN(option.orm ? this : null)
}
setWriteBufferTime(duration) {
this.writeBufferTime = duration;
}
monitor(key, callback) {
this.observer.observe(key, callback);
}
write(key, value) {
if (key === "/") {
this.memory.data = value;
return this.invalidate();
}
let { childNodeKey, nodePtr } = this.traverse(key, true);
nodePtr.cur.get(childNodeKey).data = value;
this.invalidate();
this.observer.check(key, value, "WRITE");
}
query(key, filters = {}) {
if (key.indexOf(":") === -1 && key.indexOf("@") === -1)
throw "Please provide variable(s) for query or use read function instead."
let result = this._varTraverse(key, filters)
return Query.reduce(result)
}
read(key) {
if (key.indexOf(":") !== -1 || key.indexOf("@") !== -1)
throw "read function does not support variable(s), please use query function instead."
if (key === "/") return this.memory.data;
return this.traverse(key).nodePtr.next.data;
}
list(key, callback) {
if (key === "/") return this.memory.list(callback);
let { childNodeKey, nodePtr } = this.traverse(key);
nodePtr.cur.get(childNodeKey).list(callback)
}
append(key, value) {
if (key === "/") {
if (!this.memory.has(key)) this.memory.childNodes[key] = new DataNode(key);
let id = this.memory.childNodes[key].push(value);
this.invalidate();
return this.id;
}
let { childNodeKey, nodePtr } = this.traverse(key, true);
let id = nodePtr.cur.get(childNodeKey).push(value);
this.invalidate();
if (typeof (value) === 'object') value.key = id;
this.observer.check(key, value, "APPEND");
return id;
}
containsKey(key) {
if (key === "/") return true;
try {
let { childNodeKey, nodePtr } = this.traverse(key);
return nodePtr.cur.has(childNodeKey);
} catch (e) { return false; }
}
wipe(key) {
if (key === "/") return this.memory = new DataNode();
let { childNodeKey, nodePtr, backStack } = this.traverse(key);
let data = nodePtr.next.data;
nodePtr.cur.remove(childNodeKey)
this.purge(nodePtr, backStack);
this.observer.check(key, data, "WIPE", true);
}
purge(nodePtr, backStack) {
if (nodePtr.cur.isEmpty()) {
let parentNodePtr = backStack.pop();
parentNodePtr.cur.remove(nodePtr.cur.id)
this.invalidate();
if (!backStack.isEmpty()) this.purge(parentNodePtr, backStack);
}
this.invalidate();
}
traverse(strPath, createIfMissing = false) {
return strPath.indexOf(":") !== -1 || strPath.indexOf("@") !== -1 ?
this._varTraverse(strPath) :
this._traverse(strPath, createIfMissing)
}
_traverse(strPath, createIfMissing = false) {
var ptr = this.memory;
let backStack = new Stack();
let nodePtr = null;
let childNodeKey = null;
let dirs = this.tokenize(strPath);
for (let i in dirs) {
if (!createIfMissing && !ptr.has(dirs[i]))
throw "The provided path " + dirs[i] + " does not exist.";
if (!ptr.has(dirs[i]) && createIfMissing) ptr.add(dirs[i])
nodePtr = ptr.traverse(dirs[i])
backStack.push(nodePtr);
ptr = nodePtr.next;
childNodeKey = dirs[i];
}
return { childNodeKey, nodePtr, backStack };
}
_varTraverse(strPath, filters = {}) {
let answers = null;
let queries = this.tokenize(strPath)
answers = this._queryByLevel(queries, filters);
return answers
}
_objectReader(obj, path) {
let value = obj, tmp = null, i = 0;
let tokens = path.split(".");
for (i = 0; i < tokens.length; i++) {
tmp = value[tokens[i]]
value = tmp;
}
return { key: tokens[i - 1], value: value }
}
_filter(filters, obj) {
try {
let keys = Object.keys(filters)
for (let i = 0; i < keys.length; i++)
if (!filters[keys[i]](obj[keys[i]])) return false
return true
} catch (err) {
console.error(err)
return false
}
}
_queryByLevel(queries, filters = {}, ptr = this.memory, level = 0) {
let query = queries[level];
let answers = {}, subAns = null;
if (!queries[level]) return answers;
let queryKey = ((query.indexOf(":") !== -1 || query.indexOf("@") !== -1) ? this._getQueryKey(query) : query).replace(new RegExp(' ', 'g'), '');
if (query[0] === "@") {
if (!this._filter(filters, ptr.data)) return null
if (/\{|\}/.test(queryKey)) {
let props = queryKey.match(/{(.+)}/)
if (props) {
props[1].split(",").map(prop => {
let objVal = this._objectReader(ptr.data, prop)
answers[prop] = objVal.value
})
} else answers = ptr.data;
}
else {
if (!queryKey) throw "Please provide variable(s) for query or use read function instead."
let objVal = this._objectReader(ptr.data, queryKey)
answers[objVal.key] = ptr.data ? objVal.value : null;
}
}
else if (query[0] === ":") {
let keys = [...ptr.childKeys()]
answers[queryKey] = {}
keys.forEach(key => {
let nextPtr = ptr.traverse(key).next
let result = this._queryByLevel(queries, filters, nextPtr, level + 1, key)
if (result) answers[queryKey][key] = Object.assign({}, result)
})
} else answers[queryKey] = this._queryByLevel(queries, filters, ptr.traverse(queryKey).next, level + 1, queryKey)
if (!Object.keys(answers[queryKey] || queryKey).length) delete answers[queryKey]
return answers;
}
_getQueryKey(query) {
return query.substring(1, query.length);
}
tokenize(strPath) {
return this.trim(strPath).split("/");
}
trim(strPath) {
if (strPath[0] == "/") strPath = strPath.substr(1, strPath.length);
if (strPath[strPath.length - 1] == "/") strPath = strPath.substr(0, strPath.length - 1);
return strPath.trim(new RegExp(" ", 'g'), "");
}
invalidate(immediate = false) {
if (immediate) return this._writeToFile();
if (this.writeService !== null) clearTimeout(this.writeService);
this.writeService = setTimeout(() => {
this._writeToFile();
}, this.writeBufferTime);
}
_writeToFile() {
fs.writeFileSync(this.path, JSON.stringify(this.memory));
}
_start() {
if (fs.existsSync(this.path)) this._resume();
else this.invalidate(true);
}
_resume() {
this.memory = DataNode.fromObject(JSON.parse(fs.readFileSync(this.path)));
}
/*
For ORM module
*/
_key() {
return DataNode.key()
}
/*
The below functions are deprecated.
*/
children(key) {
Deprecator.deprecated("children(key)", "list(key)");
if (key === "/") return this.memory.childNodes;
let { childNodeKey, nodePtr } = this.traverse(key);
return nodePtr.cur.childNodes;
}
iterate(key, callback) {
Deprecator.deprecated("iterate(key, callback)", "list(key, callback)");
if (key === "/") {
if (!this.memory.has(key)) this.memory.childNodes[key] = new DataNode(key);
this.memory.childNodes[key].iterate(callback);
return this.invalidate();
}
let { childNodeKey, nodePtr } = this.traverse(key);
nodePtr.cur.get(childNodeKey).iterate(callback);
this.invalidate();
}
}
module.exports = Zetabase;