UNPKG

dbjs-persistence

Version:
397 lines (379 loc) 13.8 kB
// Simple persistence driver that saves data to plain text files 'use strict'; var assign = require('es5-ext/object/assign') , forEach = require('es5-ext/object/for-each') , setPrototypeOf = require('es5-ext/object/set-prototype-of') , some = require('es5-ext/object/some') , toArray = require('es5-ext/object/to-array') , randomUniq = require('es5-ext/string/random-uniq') , camelToHyphen = require('es5-ext/string/#/camel-to-hyphen') , startsWith = require('es5-ext/string/#/starts-with') , d = require('d') , lazy = require('d/lazy') , memoizeMethods = require('memoizee/methods') , deferred = require('deferred') , resolveKeyPath = require('dbjs/_setup/utils/resolve-key-path') , resolve = require('path').resolve , mkdir = require('fs2/mkdir') , readFile = require('fs2/read-file') , readdir = require('fs2/readdir') , rename = require('fs2/rename') , rmdir = require('fs2/rmdir') , writeFile = require('fs2/write-file') , filterComputedValue = require('../lib/filter-computed-value') , isObjectPart = require('../lib/is-object-part') , resolveValue = require('../lib/resolve-direct-value') , Storage = require('../storage') , isDirectName = RegExp.prototype.test.bind(/^[a-z0-9][a-z0-9A-Z]*$/) , isComputedName = RegExp.prototype.test.bind(/^[a-z0-9A-Z-=]+$/) , isReducedName = RegExp.prototype.test.bind(/^[$_a-z0-9][a-z0-9A-Z]*$/) , isArray = Array.isArray, keys = Object.keys, create = Object.create , parse = JSON.parse, stringify = JSON.stringify; var byStamp = function (a, b) { return (this[a].stamp - this[b].stamp) || a.toLowerCase().localeCompare(b.toLowerCase()); }; var toComputedFilename = (function () { var isIdent = RegExp.prototype.test.bind(/^[$_a-z][a-zA-Z0-9]*(?:\/[$a-z][a-zA-Z0-9]*)*$/) , slashesRe = /\//g; return function (keyPath) { return isIdent(keyPath) ? keyPath.replace(slashesRe, '-') : '=' + (new Buffer(keyPath)).toString('base64'); }; }()); var fromComputedFilename = (function () { var dashesRe = /-/g; return function (filename) { return (filename[0] === '=') ? String(new Buffer(filename.slice(1), 'base64')) : filename.replace(dashesRe, '/'); }; }()); var resolveObjectMap = function (ownerId, objectPath, map, keyPaths, result) { if (!result) result = create(null); forEach(map, function (data, path) { if (!isObjectPart(objectPath, (path === '.') ? null : path)) return; if (keyPaths && (path !== '.')) { if (!keyPaths.has(resolveKeyPath(ownerId + '/' + path))) return; } result[(path === '.') ? ownerId : ownerId + '/' + path] = data; }); return result; }; var TextFileStorage = module.exports = function (driver, name/*, options*/) { if (!(this instanceof TextFileStorage)) return new TextFileStorage(driver, name, arguments[2]); Storage.call(this, driver, name, arguments[2]); this.dirPath = resolve(driver.dirPath, camelToHyphen.call(name)); this.dbDir = mkdir(this.dirPath, { intermediate: true }) .aside(null, function (err) { this.isClosed = true; this.emitError(err); }.bind(this)); }; setPrototypeOf(TextFileStorage, Storage); TextFileStorage.prototype = Object.create(Storage.prototype, assign({ constructor: d(TextFileStorage), // Any data __getRaw: d(function (cat, ns, path) { if (cat === 'reduced') { return this._getReducedStorage_(ns)(function (map) { return map[path || '.'] || null; }); } if (cat === 'computed') { return this._getComputedStorage_(ns)(function (map) { return map[path] || null; }); } return this._getDirectStorage_(ns)(function (map) { return map[path || '.'] || null; }); }), __storeRaw: d(function (cat, ns, path, data) { if (cat === 'reduced') { return this._getReducedStorage_(ns)(function (map) { map[path || '.'] = data; return this._writeStorage_('reduced/' + ns, map); }.bind(this)); } if (cat === 'computed') { return this._getComputedStorage_(ns)(function (map) { map[path] = data; return this._writeStorage_('computed/' + toComputedFilename(ns), map); }.bind(this)); } return this._getDirectStorage_(ns)(function (map) { this._objectIds_[ns] = map['.'] || null; map[path || '.'] = data; return this._writeStorage_('direct/' + ns, map); }.bind(this)); }), // Direct data __getObject: d(function (ownerId, objectPath, keyPaths) { return this._getDirectStorage_(ownerId)(function (map) { return resolveObjectMap(ownerId, objectPath, map, keyPaths); }); }), __getAllObjectIds: d(function () { return this._allObjectIdsPromise_; }), __getAll: d(function () { return this.getAllObjectIds().map(function (ownerId) { return this._getDirectStorage_(ownerId)(function (map) { return { ownerId: ownerId, map: map }; }); }, this)(function (maps) { var result = create(null); maps.forEach(function (data) { resolveObjectMap(data.ownerId, null, data.map, null, result); }); return result; }); }), // Reduced data __getReducedObject: d(function (ns, keyPaths) { return this._getReducedStorage_(ns)(function (map) { var result = create(null); forEach(map, function (data, path) { if (path === '.') path = null; if (path && keyPaths && !keyPaths.has(path)) return; result[ns + (path ? ('/' + path) : '')] = data; }); return result; }); }), __search: d(function (keyPath, value, callback) { return this.getAllObjectIds().some(function (ownerId) { return this._getDirectStorage_(ownerId)(function (map) { if (map['.']) { if (keyPath !== undefined) { if (!keyPath) { if (value != null) { if (map['.'].value !== value) return; } return callback(ownerId, map['.']); } } else if (value != null) { if (map['.'].value === value) callback(ownerId, map['.']); } else { callback(ownerId, map['.']); } } return some(map, function (data, path) { var recordValue; if (!path) return; if (keyPath !== undefined) { if (keyPath !== path) { if (!startsWith.call(path, keyPath + '*')) return; } } if (value != null) { recordValue = resolveValue(ownerId, path, data.value); if (value !== recordValue) return; } return callback(ownerId + '/' + path, data); }); }); }, this)(Function.prototype); }), __searchComputed: d(function (keyPath, value, callback) { if (keyPath) { return this._getComputedStorage_(keyPath)(function (map) { some(map, function (data, ownerId) { if ((value != null) && !filterComputedValue(value, data.value)) return; return callback(ownerId + '/' + keyPath, data); }); }); } return this._getAllComputedKeyPaths_.some(function (keyPath) { return this._getComputedStorage_(keyPath)(function (map) { return some(map, function (data, ownerId) { if ((value != null) && !filterComputedValue(value, data.value)) return; return callback(ownerId + '/' + keyPath, data); }); }); }); }), // Storage import/export __exportAll: d(function (destDriver) { var count = 0; var promise = this.dbDir()(function () { return deferred( readdir(resolve(this.dirPath, 'direct'), { type: { file: true } }).catch(function (e) { if (e.code === 'ENOENT') return []; throw e; }).map(deferred.gate(function (filename) { if (!isDirectName(filename)) return; var ownerId = filename; return this._getDirectStorage_(ownerId)(function (map) { return deferred.map(keys(map), function (path) { var data = this[path]; if (path === '.') path = null; if (!(++count % 1000)) promise.emit('progress'); return destDriver._storeRaw('direct', ownerId, path, data); }, map); }); }.bind(this), 100)), readdir(resolve(this.dirPath, 'computed'), { type: { file: true } }).catch(function (e) { if (e.code === 'ENOENT') return []; throw e; }).map(deferred.gate(function (filename) { if (!isComputedName(filename)) return; var keyPath = fromComputedFilename(filename); return this._getComputedStorage_(keyPath)(function (map) { return deferred.map(keys(map), function (ownerId) { if (!(++count % 1000)) promise.emit('progress'); return destDriver._storeRaw('computed', keyPath, ownerId, this[ownerId]); }, map); }); }.bind(this), 100)), readdir(resolve(this.dirPath, 'reduced'), { type: { file: true } }).catch(function (e) { if (e.code === 'ENOENT') return []; throw e; }).map(deferred.gate(function (ns) { if (!isReducedName(ns)) return; return this._getReducedStorage_(ns)(function (map) { return deferred.map(keys(map), function (path) { if (!(++count % 1000)) promise.emit('progress'); return destDriver._storeRaw('reduced', ns, (path === '.') ? null : path, this[path]); }, map); }); }.bind(this), 100)) ); }.bind(this)); return promise; }), __clear: d(function () { return this.__drop()(function () { return (this.dbDir = mkdir(this.dirPath, { intermediate: true })); }.bind(this)); }), __drop: d(function () { return rmdir(this.dirPath, { recursive: true, force: true, loose: true })(function () { this._getDirectStorage_.clear(); this._getComputedStorage_.clear(); this._getReducedStorage_.clear(); }.bind(this)); }), // Connection related __close: d(function () { return deferred(undefined); }), // Nothing to close // Driver specific methods _writeStorage_: d(function (name, map) { this._writeMap_[name] = map; return this._writePromise_; }), _getAllComputedKeyPaths_: d(function () { return readdir(resolve(this.dirPath, 'computed'), { type: { file: true } }).catch(function (e) { if (e.code === 'ENOENT') return []; throw e; })(function (filenames) { return filenames.filter(isComputedName).map(fromComputedFilename); }); }) }, lazy({ _writeMap_: d(function () { return create(null); }), _getInitializeWrite_: d(function () { return deferred.delay(deferred.gate(function () { var map = this._writeMap_; delete this._writeMap_; delete this._writePromise_; return deferred.map(keys(map), function (name) { var tmpFilename = resolve(this.dirPath, name + ' -tmp-' + randomUniq()); return writeFile(tmpFilename, toArray(map[name], function (data, id) { var value = data.value; if (value === '') value = '-'; else if (isArray(value)) value = stringify(value); return id + '\n' + data.stamp + '\n' + value; }, this, byStamp).join('\n\n'), { intermediate: true })(function () { return rename(tmpFilename, resolve(this.dirPath, name)); }.bind(this)); }, this); }.bind(this), 1)); }), _writePromise_: d(function () { return this.dbDir(function () { return this._getInitializeWrite_(); }.bind(this)); }), _objectIds_: d(function () { return create(null); }), _allObjectIdsPromise_: d(function () { var data = this._objectIds_; return this.dbDir()(function () { return readdir(resolve(this.dirPath, 'direct'), { type: { file: true } })(function (names) { return deferred.map(names, function (id) { if (!isDirectName(id)) return; return this._getDirectStorage_(id)(function (map) { data[id] = map['.'] || null; }); }, this); }.bind(this), function (e) { if (e.code !== 'ENOENT') throw e; }.bind(this)); }.bind(this))(data); }) }), memoizeMethods({ _getDirectStorage_: d(function (ownerId) { return this.dbDir()(function () { var map = create(null); return readFile(resolve(this.dirPath, 'direct', ownerId))(function (data) { var value; try { String(data).split('\n\n').forEach(function (data) { data = data.split('\n'); if (!data[0] || !data[1] || !data[2]) return; value = data[2]; if (value === '-') value = ''; map[data[0]] = { stamp: Number(data[1]), value: value }; }); } catch (ignore) {} return map; }, function (err) { if (err.code !== 'ENOENT') throw err; return map; }); }.bind(this)); }, { primitive: true }), _getComputedStorage_: d(function (keyPath) { return this.dbDir()(function () { var map = create(null), filename = toComputedFilename(keyPath); return readFile(resolve(this.dirPath, 'computed', filename))(function (data) { var value; try { String(data).split('\n\n').forEach(function (data) { data = data.split('\n'); if (!data[0] || !data[1] || !data[2]) return; value = data[2]; if (value[0] === '[') value = parse(data[2]); else if (value === '-') value = ''; map[data[0]] = { stamp: Number(data[1]), value: value }; }); } catch (ignore) {} return map; }, function (err) { if (err.code !== 'ENOENT') throw err; return map; }); }.bind(this)); }, { primitive: true }), _getReducedStorage_: d(function (ns) { return this.dbDir()(function () { var map = create(null); return readFile(resolve(this.dirPath, 'reduced', ns))(function (data) { var value; try { String(data).split('\n\n').forEach(function (data) { data = data.split('\n'); if (!data[0] || !data[1] || !data[2]) return; value = data[2]; if (value[0] === '[') value = parse(data[2]); else if (value === '-') value = ''; map[data[0]] = { stamp: Number(data[1]), value: value }; }); } catch (ignore) {} return map; }, function (err) { if (err.code !== 'ENOENT') throw err; return map; }); }.bind(this)); }, { primitive: true }) })));