UNPKG

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
/** * 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(); };