gracenode-staticdata
Version:
staticdata module for gracenode framework
310 lines (263 loc) • 7.44 kB
JavaScript
/**
* configurations
*
* {
* "staticdata": {
* "path": "directory path to the source static files",
* "delimiter": optional,
* "qoute": optional,
* "index": optional {
* "staticFileName": ["indexName"...] > this must be a cloumn name in the file
* }
* }
* }
*
* */
var fs = require('fs');
var async = require('async');
var EventEmitter = require('events').EventEmitter;
var fileWatcher = new EventEmitter();
var gracenode = require('../gracenode');
var csv = require('./csv');
var parser = require('./parser');
var log = gracenode.log.create('staticdata');
// maximum number of files to be opened and read at once on setup
var maxOpenFiles = 100;
var config;
var delimiter = ',';
var quote = '"';
var staticData = {}; // static data source object
var sdMap = {}; // static data object map
module.exports.readConfig = function (configIn) {
if (!configIn || !configIn.path) {
return new Error('invalid configuration: \n' + JSON.stringify(configIn, null, 4));
}
config = configIn;
// optional
if (config.delimiter !== undefined) {
delimiter = config.delimiter;
}
if (config.quote !== undefined) {
quote = config.quote;
}
if (config.maxOpenFiles > 0) {
maxOpenFiles = config.maxOpenFiles;
}
// set up CSV parser
csv.setup(delimiter);
};
module.exports.setup = function (cb) {
log.verbose('setting up static data module...');
gracenode.lib.walkDir(gracenode.getRootPath() + config.path, function (error, list) {
if (error) {
return cb(error);
}
async.eachLimit(list, maxOpenFiles, function (item, nextCallback) {
readFile(item.file, nextCallback);
}, cb);
});
};
/*
* dataName rule:
* configuration path: staticdata/
* example: staticdata/example/test.csv = example/test
*/
module.exports.create = function (dataName) {
// check for existing static data object first
if (sdMap[dataName]) {
return sdMap[dataName];
}
// create a new static data object
if (staticData[dataName]) {
var sd = new StaticData(dataName, staticData[dataName]);
sdMap[dataName] = sd;
return sd;
}
return null;
};
module.exports.csvToObj = function (csvData) {
return toObject('', csvData);
};
function readFile(path, cb) {
var lastDot = path.lastIndexOf('.');
var type = path.substring(lastDot + 1);
var name = path.substring(path.lastIndexOf(config.path) + config.path.length, lastDot);
fs.readFile(path, 'utf8', function (error, dataBuffer) {
if (error) {
return cb(error);
}
var data = dataBuffer.toString('utf-8');
var bytes = dataBuffer.length;
var kb = bytes / 1024;
var size = bytes + ' bytes';
if (kb >= 1) {
size = Math.round(kb) + ' kb';
}
log.verbose('static data loaded:', path + ' (' + size + ')');
switch (type) {
case 'csv':
data = toObject(path, data);
break;
case 'json':
try {
data = JSON.parse(data);
} catch (e) {
log.error('Could not turn', name, '(type:', type, ')', 'into object.');
return cb(e);
}
break;
default:
data = { data: data };
break;
}
// check for error
if (data instanceof Error) {
return cb(data);
}
// create index map(s) if asked
var indexMap = null;
var fileName = name + '.' + type;
if (config.index && config.index[fileName]) {
indexMap = mapIndex(data, config.index[fileName]);
log.verbose('indexed: ', config.index[fileName]);
}
// add it to cache
staticData[name] = { data: data, indexMap: indexMap, path: path };
log.verbose('mapped: ' + path + ' > ' + name);
// set up file watch listener
setupChangeListener(path);
cb();
});
}
function setupChangeListener(path) {
log.verbose('file change listener setup:', path);
fs.watch(path, function (event) {
if (event === 'change') {
readFile(path, function (error) {
if (error) {
return log.error(error);
}
log.info('file updated [' + event + ']:', path);
fileWatcher.emit(path);
});
}
});
}
function toObject(file, data) {
var parsed = csv.parse(file, data);
var obj = parser.execute(parsed.file, parsed.data);
log.verbose(obj);
return obj;
}
function mapIndex(data, indexNames) {
var map = {};
for (var c = 0, length = data.length; c < length; c++) {
var item = data[c];
for (var i = 0, len = indexNames.length; i < len; i++) {
var indexName = indexNames[i];
if (item[indexName] !== undefined) {
if (!map[indexName]) {
map[indexName] = {};
}
var index = item[indexName];
if (map[indexName][index]) {
// index is not unique
if (!Array.isArray(map[indexName][index])) {
map[indexName][index] = [map[indexName][index]];
}
map[indexName][index].push(item);
} else {
// index is unique or this is the first item of the index
map[indexName][index] = item;
}
}
}
}
return map;
}
function StaticData(name, src) {
this._name = name;
this._src = src.data;
this._indexMap = src.indexMap || {};
// file change listener
var that = this;
fileWatcher.on(src.path, function () {
that.update(staticData[name]);
});
}
// create multi-dimensional object with another staticdata
// child staticdata MUST be indexed by childKey
StaticData.prototype.inflate = function (childStaticdata, parentKey, childKey) {
var success = false;
// check to see if the child staticdata has been indexed by childKey or not
if (!childStaticdata._indexMap[childKey]) {
log.error('failed to inflate staticdata [' + this._name + ']: child staticdata [' + childStaticdata._name + '] MUST be index by "' + childKey + '"');
return success;
}
// now inflate the staticdata with childStaticdata
for (var i = 0, len = this._src.length; i < len; i++) {
var srcItem = this._src[i];
var child = childStaticdata.getOneByIndex(childKey, srcItem[parentKey]);
if (child) {
srcItem[parentKey] = child;
if (!success) {
success = true;
}
}
}
return success;
};
StaticData.prototype.getOneByIndex = function (indexName, key, props) {
if (!this._indexMap) {
return null;
}
var data = this._indexMap[indexName] || null;
if (!data || data[key] === undefined) {
return null;
}
var res = data[key];
if (typeof res === 'object') {
return gracenode.lib.cloneObj(res, props);
}
return res;
};
StaticData.prototype.getManyByIndex = function (indexName, keyList, props) {
var res = {};
for (var i = 0, len = keyList.length; i < len; i++) {
var key = keyList[i];
res[key] = this.getOneByIndex(indexName, key, props);
}
return res;
};
StaticData.prototype.getOne = function (index, props) {
var data = this._src[index];
if (data === undefined) {
return null;
}
if (typeof data === 'object') {
return gracenode.lib.cloneObj(data, props);
}
return data;
};
StaticData.prototype.getMany = function (indexList, props) {
var res = {};
for (var i = 0, len = indexList.length; i < len; i++) {
var key = indexList[i];
res[key] = this.getOne(key, props);
}
return res;
};
StaticData.prototype.getAll = function (props) {
return gracenode.lib.cloneObj(this._src, props);
};
StaticData.prototype.getAllByIndexName = function (indexName, props) {
if (!this._indexMap || this._indexMap[indexName] === undefined) {
return null;
}
return gracenode.lib.cloneObj(this._indexMap[indexName], props);
};
StaticData.prototype.update = function (src) {
log.verbose('static data [' + this._name + '] has been updated');
this._src = src.data;
this._indexMap = src.indexMap;
};