ramlitedb
Version:
NoSQL Database for node.js with all data loaded in ram and backup in json file
283 lines (242 loc) • 6.21 kB
JavaScript
/**
* Main node module
*/
var pfsLib = require('../lib/promises').file;
module.exports = function(conf) {
var _conf = {
user: conf || {},
state: {
// Is DB loaded
loaded: false,
// Is there data to backup
dataToBackup: false,
interval: null
},
indexes: {},
events: {
load: [],
backup: [],
get: [],
set: [],
unload: []
},
database: {}
};
// Load default conf
_conf.user.log = _conf.user.log || console.log;
_conf.user.backupFile = _conf.user.backupFile || `${__dirname}/../backup/default.json`;
_conf.user.backupOnSet = _conf.user.backupOnSet || true;
_conf.user.backupInterval = _conf.user.backupInterval || null;
// Load DB (backup file)
const _load = async () => {
if (! (await pfsLib.exists(_conf.user.backupFile))) {
try {
await pfsLib.write(_conf.user.backupFile, "{}");
} catch (e) {
_conf.user.log('Error : Unable to create the backup file :', e);
return;
}
}
try {
const data = await pfsLib.read(_conf.user.backupFile);
try {
_conf.database = JSON.parse(data);
} catch (e) {
_conf.user.log('Error : Ill-formed backup file :', e);
return;
}
_conf.state.loaded = true;
try {
_raise('load');
} catch (e) {
_conf.user.log('Error : A function attached to the load event failed :', e);
}
} catch (e) {
_conf.user.log('Error : Unable to read the backup file :', e);
}
}
// Save Backup file
function _backup() {
if (!_conf.state.dataToBackup) {
return;
}
pfsLib.write(_conf.user.backupFile, JSON.stringify(_conf.database)).then(() => {
_conf.state.dataToBackup = false;
_raise('backup');
}).catch(() => {
_conf.user.log('Warning : Unable to save the backup file');
});
}
// Check if the DB is ready (and log)
function _checkDbReady() {
if (!_conf.state.loaded) {
_conf.user.log('Warning : trying to access/modify the DB while it is not loaded !!!');
throw 'Database not ready';
}
return true;
}
// raise an event
function _raise(event, ...args) {
_conf.events[event].forEach((callback) => {
callback.call(this, ...args);
});
}
// Gather objects on index
const _buildIndex = (path, by) => {
const node = this.get(path);
_conf.indexes[path] = _conf.indexes[path] || [];
_conf.indexes[path][by] = [];
if (node === null) {
return;
}
Object.values(node).forEach((object) => {
const keyValue = object[by];
if (keyValue !== undefined) {
_conf.indexes[path][by][keyValue] = _conf.indexes[path][by][keyValue] || [];
_conf.indexes[path][by][keyValue].push(object);
}
})
}
function _updateIndexes(path, object) {
Object.keys(_conf.indexes).forEach((indexPath) => {
if ( indexPath.indexOf(path) === 0) {
// Parent of index has been modified
Object.keys(_conf.indexes[indexPath]).forEach((by) => {
_buildIndex(indexPath, by);
});
} else if (path.indexOf(indexPath) === 0 ) {
// Index key may have been modified
Object.keys(_conf.indexes[indexPath]).forEach((by) => {
// Index key has been modified
_buildIndex(indexPath, by);
// TODO : optimisation: detect when needed. (detect key modification)
});
}
})
}
/**
* Create an index, to increase future researchs
*
* @param {str/array} key Key Path to the data to index (dot separated)
* @param {str} by name of the propety to index
* @return {bool} is that a success ?
*/
this.index = (path, by) => {
if (Array.isArray(path)) {
path = path.join('.');
}
_buildIndex(path, by);
return true;
}
/**
* Get every objects located at key, having 'by' set to 'value'
*
* @param {str/array} key Key Path to the indexed data (dot separated)
* @param {str} by name of the propety to index
* @param {str} value value of the propety
* @return {mixed} null/data
*/
this.getBy = (path, by, value) => {
if (Array.isArray(path)) {
path = path.join('.');
}
if (_conf.indexes[path] === undefined
|| _conf.indexes[path][by] === undefined
) {
_conf.user.log('Error: no index for : ', JSON.stringify({ path, by }));
return null;
}
return _conf.indexes[path][by][value] || [];
}
/**
* Attach an action to a specific event
*
* @param {str} event Event Name
* @param {Function} callback Action to attach
*/
this.on = (event, callback) => {
_conf.events[event].push(callback);
}
/**
* Set a value
*
* @param {str/array} key Key Path to the data (dot separated)
* @param {mixed} value The value to record
* @return {bool} is that a success ?
*/
this.set = (key = [], value) => {
// Checks
_checkDbReady();
// Set in Ram
var path = _conf.database;
if (key === '') {
key = [];
} else if (typeof key === 'string' || key instanceof String) {
key = key.split('.');
}
if (key.length === 0) {
_conf.database = value;
} else {
key.forEach((node, depth) => {
if (depth === (key.length-1)) {
path[node] = value;
} else if (path[node] === undefined) {
path[node] = {};
}
path = path[node];
})
}
// Update index
_updateIndexes(key.join('.'), value);
// Backup
_conf.state.dataToBackup = true;
if (_conf.user.backupOnSet) {
setTimeout(_backup, 1);
}
_raise('set');
return true;
}
/**
* Get a value.
*
* @param {str/array} key Path to the data (dot separated)
* @return {mixed} null/data
*/
this.get = (key = []) => {
// Checks
_checkDbReady();
// get in Ram
var path = _conf.database;
if (key === '') {
key = [];
} else if (typeof key === 'string' || key instanceof String) {
key = key.split('.');
}
for (var i = 0; i < key.length; ++i) {
let node = key[i];
if (path[node] === undefined) {
return null;
}
path = path[node];
}
_raise('get');
return path;
}
/**
* Free all ressources
*/
this.destroy = () => {
clearInterval(_conf.state.interval);
if (this.dataToBackup) {
_backup();
this.on('backup', () => {
_raise('unload');
})
} else {
_raise('unload');
}
}
if (_conf.user.backupInterval)
_conf.state.interval = setInterval(_backup, _conf.user.backupInterval);
_load();
};