dataflo.ws
Version:
Zero-code JSON config-based dataflow engine for Node, PhoneGap and browser.
647 lines (535 loc) • 16.9 kB
JavaScript
"use strict";
var path = require ('path');
var fs = require ('fs');
var util = require ('util');
var EventEmitter = require ('events').EventEmitter;
var dataflows = require ('./index');
var io = require ('./io/easy');
var paint = require ('./color');
var common = require ('./common');
// var fsm = StateMachine.create({
// events: [
// {name: 'prepare', from: 'none', to: 'prepared'},
// {name: 'instantiate', from: 'prepared', to: 'instantiated'},
// {name: 'configure', from: 'instantiated', to: 'configured'},
// ]});
// alert(fsm.current); // "none"
// fsm.prepare ();
// alert(fsm.current); // "green"
var Project = function (rootPath) {
this.root = new io (rootPath || process.env.PROJECT_ROOT || process.cwd());
this.configDir = process.env.PROJECT_CONF || '.dataflows';
this.varDir = process.env.PROJECT_VAR || '.dataflows';
this.on ('legacy-checked', this.checkConfig.bind(this));
this.on ('config-checked', this.readInstance.bind(this));
this.on ('instantiated', this.loadConfig.bind(this));
this.checkLegacy ();
// common.waitAll ([
// [this, 'legacy-checked'], // check legacy config
// [this, 'config-checked'], // check current config
// ], this.readInstance.bind(this));
};
module.exports = Project;
util.inherits (Project, EventEmitter);
Project.prototype.checkLegacy = function (cb) {
var self = this;
this.root.fileIO ('etc/project').stat(function (err, stats) {
if (!err && stats && stats.isFile()) {
console.error (paint.error ('project has legacy configuration layout. you can migrate by running those commands:'));
console.error ("\n\tcd "+self.root.path);
console.error ("\tmv etc .dataflows");
// console.warn ("in", paint.dataflows ("@0.60.0"), "we have changed configuration layout. please run", paint.path("dataflows doctor"));
self.configDir = 'etc';
self.varDir = 'var';
self.legacy = true;
}
self.emit ('legacy-checked');
});
};
Project.prototype.checkConfig = function (cb) {
var self = this;
if (self.legacy) {
self.emit ('config-checked');
return;
}
// search for config root
var guessedRoot = this.root;
guessedRoot.findUp (this.configDir, function (foundConfigDir) {
var detectedRoot = foundConfigDir.parent();
if (self.root.path !== detectedRoot.path) {
console.log (paint.dataflows (), 'using', paint.path (detectedRoot.path), 'as project root');
}
self.root = detectedRoot;
self.emit ('config-checked');
return true;
}, function () {
self.emit ('error', 'no project config');
});
};
Project.prototype.readInstance = function () {
var self = this;
this.instance = process.env.PROJECT_INSTANCE;
if (this.instance) {
console.log (paint.dataflows(), 'instance is:', paint.path (instance));
self.emit ('instantiated');
return;
}
var instanceFile = this.root.fileIO (path.join (this.varDir, 'instance'));
instanceFile.readFile (function (err, data) {
if (err) {
var instanceName = [
(process.env.USER || process.env.USERNAME),
(process.env.HOSTNAME || process.env.COMPUTERNAME)
].join ('@');
// it is ok to have instance name defined and have no instance
// or fixup file because fixup is empty
self.instance = instanceName;
self.root.fileIO (path.join (self.varDir, instanceName)).mkdir ();
instanceFile.writeFile (instanceName);
self.emit ('instantiated');
return;
}
// assume .dataflows dir always correct
// if (err && self.varDir != '.dataflows') {
// console.error ("PROBABLY HARMFUL: can't access "+self.varDir+"/instance: "+err);
// console.warn (paint.dataflows(), 'instance not defined');
// } else {
var instance = (""+data).split (/\n/)[0];
self.instance = instance == "undefined" ? null : instance;
var args = [paint.dataflows(), 'instance is:', paint.path (instance)];
if (err) {
args.push ('(' + paint.error (err) + ')');
} else if (self.legacy) {
console.error ("\tmv var/instance .dataflows/");
}
if (self.legacy) console.log ();
console.log.apply (console, args);
// }
self.emit ('instantiated');
});
};
Project.prototype.logUnpopulated = function(varPaths) {
console.error ("those config variables is unpopulated:");
for (var varPath in varPaths) {
var value = varPaths[varPath][0];
console.log ("\t", paint.path(varPath), '=', value);
varPaths[varPath] = value || "<#undefined>";
}
console.error (
"you can run",
paint.dataflows ("config set <variable> <value>"),
"to define individual variable\nor edit",
paint.path (".dataflows/"+this.instance+"/fixup"),
"to define all those vars at once"
);
// console.log (this.logUnpopulated.list);
};
Project.prototype.setVariables = function (fixupVars, force) {
var self = this;
// ensure fixup is defined
if (!this.instance) {
console.log ('Cannot write to the fixup file with undefined instance. Please run', paint.dataflows('init'));
process.kill ();
}
if (!self.fixupConfig)
self.fixupConfig = {};
// apply patch to fixup config
Object.keys (fixupVars).forEach (function (varPath) {
var pathChunks = [];
var root = self.fixupConfig;
varPath.split ('.').forEach (function (chunk, index, chunks) {
pathChunks[index] = chunk;
var newRoot = root[chunk];
if (index === chunks.length - 1) {
if (force || !(chunk in root)) {
root[chunk] = fixupVars[varPath][0] || "<#undefined>";
}
} else if (!newRoot) {
root[chunk] = {};
newRoot = root[chunk];
}
root = newRoot;
});
});
// wrote config to the fixup file
fs.writeFileSync (
this.fixupFile,
JSON.stringify (this.fixupConfig, null, "\t")
);
};
Project.prototype.formats = [{
type: "json",
check: /(\/\/\s*json[ \t\n\r]*)?[\{\[]/,
parse: function (match, configData) {
try {
var config = JSON.parse ((""+configData).substr (match[0].length - 1));
return {object: config};
} catch (e) {
return {object: null, error: e};
}
},
stringify: JSON.stringify.bind (JSON),
}, {
type: "ini",
check: /^;\s*ini/,
require: "ini",
parse: function () {
},
stringify: function () {
}
}];
Project.prototype.parseConfig = function (configData, configFile) {
var self = this;
var result;
this.formats.some (function (format) {
var match = (""+configData).match (format.check);
if (match) {
result = format.parse (match, configData);
result.type = format.type;
return true;
}
});
if (!result) {
var message =
'Unknown file format in '+(configFile.path || configFile)+'; '
+ 'for now only JSON supported. You can add new formats using Project.prototype.formats.';
console.error (paint.error (message));
self.emit ('error', message);
}
return result;
}
Project.prototype.interpolateVars = function (error) {
// var variables = {};
var self = this;
function iterateNode (node, key, depth) {
var value = node[key];
var fullKey = depth.join ('.');
var match;
if (self.variables[fullKey]) {
self.variables[fullKey][1] = value;
}
if ('string' !== typeof value)
return;
var enchanted = self.isEnchantedValue (value);
if (!enchanted) {
// WTF???
if (self.variables[fullKey]) {
self.variables[fullKey][1] = value.toString ? value.toString() : value;
}
return;
}
if ("placeholder" in enchanted) {
// this is a placeholder, not filled in fixup
self.variables[fullKey] = [value];
if (enchanted.optional) {
self.variables[fullKey][1] = null;
node[key] = null;
} else if (enchanted.default) {
self.variables[fullKey][1] = enchanted.default;
node[key] = enchanted.default;
}
return;
}
if ("variable" in enchanted) {
// this is a variable, we must fill it now
// current match is a variable path
// we must write both variable path and a key,
// containing it to the fixup
var varValue = self.getKeyDesc (enchanted.variable.substr (1));
if (varValue.enchanted !== undefined) {
if ("variable" in varValue.enchanted) {
console.error (
"variable value cannot contains another variables. used variable",
paint.path(enchanted.variable),
"which resolves to",
paint.path (varValue.value),
"in key",
paint.path(fullKey)
);
process.kill ();
}
self.variables[fullKey] = [value];
} else if (varValue.value !== undefined) {
node[key] = value.interpolate (self.config, {start: '<', end: '>'});
self.variables[fullKey] = [value, node[key]];
} else {
self.variables[fullKey] = [value];
}
return;
}
// this cannot happens, but i can use those checks for assertions
if ("error" in enchanted || "include" in enchanted) {
// throw ("this value must be populated: \"" + value + "\"");
}
}
self.iterateTree (self.config, iterateNode, []);
var unpopulatedVars = {};
var varNames = Object.keys (self.variables);
varNames.forEach (function (varName) {
if (self.variables[varName][1] !== undefined) {
} else {
unpopulatedVars[varName] = self.variables[varName];
}
});
this.setVariables (self.variables);
// any other error take precendence over unpopulated vars
if (Object.keys(unpopulatedVars).length || error) {
if (unpopulatedVars) {
self.logUnpopulated(unpopulatedVars);
}
self.emit ('error', error || 'unpopulated variables');
return;
}
// console.log ('project ready');
self.emit ('ready');
}
Project.prototype.loadConfig = function () {
var self = this;
var configFile = this.root.fileIO (path.join(this.configDir, 'project'))
configFile.readFile (function (err, data) {
if (err) {
var message = "Can't access "+self.configDir+"/project file. Create one and define project id";
console.error (paint.dataflows(), paint.error (message));
// process.kill ();
self.emit ('error', message);
return;
}
var config;
var parsed = self.parseConfig (data, configFile);
if (parsed.object) {
config = parsed.object;
} else {
var message = 'Project config cannot be parsed:';
console.error (message, paint.error (parsed.error));
self.emit ('error', message + ' ' + parsed.error.toString());
process.kill ();
}
self.id = config.id;
// TODO: load includes after fixup is loaded
self.loadIncludes(config, 'projectRoot', function (err, config, variables, placeholders) {
self.variables = variables;
self.placeholders = placeholders;
if (err) {
console.error (err);
console.warn ("Couldn't load includes.");
// actually, failure when loading includes is a warning, not an error
self.interpolateVars();
return;
}
self.config = config;
if (!self.instance) {
self.interpolateVars ();
return;
}
self.fixupFile = path.join(self.configDir, self.instance, 'fixup');
self.root.fileIO (self.fixupFile).readFile (function (err, data) {
var fixupConfig = {};
if (err) {
console.error (
"Config fixup file unavailable ("+paint.path (path.join(self.configDir, self.instance, 'fixup'))+")",
"Please run", paint.dataflows ('init')
);
} else {
var parsedFixup = self.parseConfig (data, self.fixupFile);
if (parsedFixup.object) {
self.fixupConfig = fixupConfig = parsedFixup.object;
} else {
var message = 'Config fixup cannot be parsed:';
console.error (message, paint.error (parsedFixup.error));
self.emit ('error', message + ' ' + parsedFixup.error.toString());
process.kill ();
}
}
util.extend (true, self.config, fixupConfig);
self.interpolateVars ();
});
});
});
};
function Config () {
}
Config.prototype.getValueByKey = function (key) {
// TODO: techdebt to remove such dep
var value = common.getByPath (key, this);
if (this.isEnchanted (value)) {
return null;
}
return value;
}
Project.prototype.connectors = {};
Project.prototype.connections = {};
Project.prototype.getModule = function (type, name, optional) {
var self = this;
optional = optional || false;
var mod;
var taskFound = [
path.join('dataflo.ws', type, name),
path.resolve(this.root.path, type, name),
path.resolve(this.root.path, 'node_modules', type, name),
name
].some (function (modPath) {
try {
mod = require(modPath);
return true;
} catch (e) {
// assuming format: Error: Cannot find module 'csv2array' {"code":"MODULE_NOT_FOUND"}
if (e.toString().indexOf(name + '\'') > 0 && e.code == "MODULE_NOT_FOUND") {
return false;
} else {
console.error ('requirement failed:', paint.error (e.toString()), "in", paint.path (self.root.relative (modPath)));
return true;
}
}
});
if (!mod && !optional)
console.error ("module " + type + " " + name + " cannot be used");
return mod;
};
Project.prototype.getInitiator = function (name) {
return this.getModule('initiator', name);
};
Project.prototype.getTask = function (name) {
return this.getModule('task', name);
};
Project.prototype.require = function (name, optional) {
return this.getModule('', name, optional);
};
var configCache = {};
Project.prototype.iterateTree = function iterateTree (tree, cb, depth) {
if (null == tree)
return;
var level = depth.length;
var step = function (node, key, tree) {
depth[level] = key;
cb (tree, key, depth);
iterateTree (node, cb, depth.slice (0));
};
if (Array === tree.constructor) {
tree.forEach (step);
} else if (Object === tree.constructor) {
Object.keys(tree).forEach(function (key) {
step (tree[key], key, tree);
});
}
};
Project.prototype.getKeyDesc = function (key) {
var result = {};
var value = common.getByPath (key, this.config);
result.value = value.value;
result.enchanted = this.isEnchantedValue (result.value);
// if value is enchanted, then it definitely a string
if (result.enchanted && "variable" in result.enchanted) {
result.interpolated = result.value.interpolate();
return result;
}
return result;
}
Project.prototype.getValue = function (key) {
var value = common.getByPath (key, this.config).value;
if (value === undefined)
return;
var enchanted = this.isEnchantedValue (value);
// if value is enchanted, then it definitely a string
if (enchanted && "variable" in enchanted) {
var result = new String (value.interpolate());
result.rawValue = value;
return result;
}
return value;
}
Project.prototype.isEnchantedValue = function (value) {
var tagRe = /<(([\$\#]*)((optional|default):)?([^>]+))>/;
var result;
if ('string' !== typeof value) {
return;
}
var check = value.match (tagRe);
if (check) {
if (check[2] === "$") {
return {"variable": check[1]};
} else if (check[2] === "#") {
result = {"placeholder": check[1]};
if (check[4]) result[check[4]] = check[5];
return result;
} else if (check[0].length === value.length) {
return {"include": check[1]};
} else {
return {"error": true};
}
}
}
Project.prototype.loadIncludes = function (config, level, cb) {
var self = this;
var DEFAULT_ROOT = this.configDir,
DELIMITER = ' > ',
cnt = 0,
len = 0;
var levelHash = {};
var variables = {};
var placeholders = {};
level.split(DELIMITER).forEach(function(key) {
levelHash[key] = true;
});
function onLoad() {
cnt += 1;
if (cnt >= len) {
cb(null, config, variables, placeholders);
}
}
function onError(err) {
console.log('[WARNING] Level:', level, 'is not correct.\nError:', paint.error (err));
cb(err, config, variables, placeholders);
}
function iterateNode (node, key, depth) {
var value = node[key];
if ('string' !== typeof value)
return;
var enchanted = self.isEnchantedValue (value);
if (!enchanted)
return;
if ("variable" in enchanted) {
variables[depth.join ('.')] = [value];
return;
}
if ("placeholder" in enchanted) {
variables[depth.join ('.')] = [value];
return;
}
if ("error" in enchanted) {
console.error ('bad include tag:', "\"" + value + "\"");
onError();
return;
}
if ("include" in enchanted) {
len ++;
var incPath = enchanted.include;
if (0 !== incPath.indexOf('/')) {
incPath = path.join (DEFAULT_ROOT, incPath);
}
if (incPath in levelHash) {
//console.error('\n\n\nError: on level "' + level + '" key "' + key + '" linked to "' + value + '" in node:\n', node);
throw new Error('circular linking');
}
delete node[key];
if (configCache[incPath]) {
node[key] = util.clone(configCache[incPath]);
onLoad();
return;
}
self.root.fileIO(incPath).readFile(function (err, data) {
if (err) {
onError(err);
return;
}
self.loadIncludes(JSON.parse(data), path.join(level, DELIMITER, incPath), function(tree, includeConfig) {
configCache[incPath] = includeConfig;
node[key] = util.clone(configCache[incPath]);
onLoad();
});
});
}
}
this.iterateTree(config, iterateNode, []);
// console.log('including:', level, config);
!len && cb(null, config, variables, placeholders);
};