redmix
Version:
A fun and friendly Appcelerator Cloud MBaaS command line
536 lines (502 loc) • 13.4 kB
JavaScript
var path = require('path'),
fs = require('fs'),
vm = require('vm'),
_ = require('lodash'),
util = require('util'),
chalk = require('chalk'),
ACSNode = require('acs-node'),
objects = require('../conf/objects'),
configDir = path.join(process.env.HOME || process.env.USERPROFILE),
configFile = path.join(configDir,'.redmix.json'),
cachedConfig;
function saveConfig (config) {
if (!fs.existsSync(configDir)) {
fs.mkdir(configDir);
}
cachedConfig = config;
fs.writeFileSync(configFile, JSON.stringify(config||{}, null, 2));
}
function getConfig () {
if (!fs.existsSync(configDir)) {
fs.mkdir(configDir);
}
if (fs.existsSync(configFile)) {
return (cachedConfig = JSON.parse(fs.readFileSync(configFile)));
}
return (cachedConfig = {});
}
function fail(err) {
console.log(chalk.red(err.message || String(err)));
process.exit(1);
}
function isConnected(config) {
return (config.username && config.session && config.key) ||
(config.aliases && config.alias && config.alias in config.aliases &&
config.aliases[config.alias].username && config.aliases[config.alias].session &&
config.aliases[config.alias].key);
}
function getACSConfig(config) {
return config.alias && config.aliases && config.aliases[config.alias] || config;
}
/**
* return a masked version of key
*/
function maskSessionValue(key) {
return key && key.substring(0,Math.floor(Math.min(key.length/2,6))) + '******';
}
/**
* used to mask session details so we don't ever log them to console or file
* since they are sensitive and shouldn't be shared
*/
function maskSessionDetails(obj) {
// make a copy so we don't jack with real config
obj = _.clone(obj);
if ('session' in obj) {
obj.session = maskSessionValue(obj.session);
}
if ('key' in obj) {
obj.key = maskSessionValue(obj.key);
}
if (obj.aliases) {
Object.keys(obj.aliases).forEach(function(key){
var entry = obj.aliases[key];
entry.session = maskSessionValue(entry.session);
entry.key = maskSessionValue(entry.key);
});
}
return obj;
}
/**
* return the ACS connection object
*/
function getACS (opts) {
var config = _.merge(getConfig(), opts|| {});
if (!isConnected(config)) {
fail("Not connected. Run redmix connect to connect to your ACS app.");
}
var acsEntry = getACSConfig(config),
acsApp = new ACSNode(acsEntry.key,{
prettyJson: true,
autoSessionManagement: false
});
trace(opts,'acs','ACS Env',maskSessionDetails(acsEntry));
acsApp.sessionCookieString = '_session_id='+acsEntry.session;
return acsApp;
}
function evalResult(code, result) {
code = code.trim();
// if we have a return, trim it out
if (/^return\s+/.test(code)) {
code = code.substring(6);
}
var jscode = 'function f(){ return ' + code + '; }; f';
var fnobj = vm.runInNewContext(jscode);
return fnobj.call(result);
}
var keyValueWithArray=/(\w+)\s?=\s?(\[[\w,'"\s]+\])/;
var keyValueWithObject=/(\w+)\s?=\s?(\{[\w,:'"\s]+\})/;
// make it easy to make the JSON look more like JS objects so
// the user doesn't have to actually do weird escaping
function parseObjectAsJS(value) {
if (isStringAnObject(value)) {
var code = 'var code = '+value+'; code;';
return vm.runInNewContext(code);
}
else {
var result = {}, m, key, originalValue = value;
if (keyValueWithArray.test(value)) {
// attempt to clean up the string to keep the regex more basic
value = value.trim()
.replace(/\s=\s/g,'=')
.replace(/,\s/g,',')
.replace(/\[\s/g,'[')
.replace(/\s\]/g,']')
.replace(/\s+,\s+/g,',')
.replace(/\s+,/g,',');
originalValue = value; // reset after trim
var firstIndex = -1, lastIndex = -1;
while(keyValueWithArray.test(value)) {
m = keyValueWithArray.exec(value);
key = m[1];
result[key] = makeArray(m[2]);
value = value.substring(m.index+m[0].length+1);
if (firstIndex < 0) {
firstIndex = m.index;
}
else {
lastIndex = m.index;
}
}
// if we went passed our original
if (firstIndex > 0) {
value = originalValue.substring(0, firstIndex);
}
// if we didn't go far enough
else if (lastIndex > 0 && lastIndex < value.length) {
value = value.substring(lastIndex);
}
}
if (keyValueWithObject.test(value)) {
value = value.trim()
.replace(/\s=\s/g,'=')
.replace(/,\s/g,',')
.replace(/\{\s/g,'{')
.replace(/\s\}/g,'}')
.replace(/\s+,\s+/g,',')
.replace(/\s+,/g,',');
while(keyValueWithObject.test(value)) {
m = keyValueWithObject.exec(value);
key = m[1];
result[key] = parseObjectAsJS(m[2]);
value = value.substring(m.index+m[0].length+1);
}
}
else {
var tokens = value.split(',');
tokens.forEach(function(line){
var token = line.split("="),
key = token[0].trim(),
value = (token[1] || '').trim();
result[key] = isStringAnObject(value) ? parseObjectAsJS(value) : value;
});
}
return result;
}
}
var isObjectPrefix = /^\s?[\[\{]/,
isObjectSuffix = /[\}\]]\s?$/;
function isStringAnObject (value) {
return isObjectPrefix.test(value) && isObjectSuffix.test(value);
}
function makeArray(value) {
if (!value) { return []; }
if (isStringAnObject(value)) {
value = parseObjectAsJS(value);
if (Array.isArray(value)) {
return value;
}
return [value];
}
var tok = value.split(',');
return tok.map(function(v) {
return v.trim();
});
}
function resolveBuiltinObject(objectName, objectList){
if (objectName in objectList) {
return objectList[objectName];
}
var properCase = objectName.charAt(0).toUpperCase() + objectName.substring(1);
if (properCase in objectList) {
return objectList[properCase];
}
if (properCase+'s' in objectList) {
return objectList[properCase+'s'];
}
// look up using exact match
var keys = Object.keys(objectList);
for (var c=0;c<keys.length;c++) {
var key = keys[c].toLowerCase();
if (key === objectName) {
return objectList[keys[c]];
}
}
// if we couldn't find, we use CustomObjects
return objectList.CustomObjects;
}
function createACSFunctor(objectName, methods, opts) {
opts = opts||{};
var acs = getACS(opts),
objectList = acs.getACSObjects(),
object = resolveBuiltinObject(objectName,objectList),
desc = objects[object.objectName.toLowerCase()],
methodObj,
methodlist = Array.isArray(methods) ? methods : [methods];
// search for a method in order of precedence
for (var c=0;c<methodlist.length;c++) {
var method = methodlist[c];
var methodName = desc && desc[method] || method;
if (methodName) {
methodObj = object.methods[methodName];
if (methodObj) {
break;
}
}
}
// check to make sure we got one
methodObj = methodObj || object.methods[methodlist[0]];
if (!methodObj) {
throw new Error("couldn't find a valid method using "+methodlist.join(",")+" for "+objectName);
}
var executor = methodObj && acs[methodObj.apiMethodName],
custom = object.objectName==='customObjects',
fn = function(query, callback) {
var expressions, hook;
if (query) {
expressions = query.expressions;
delete query.expressions;
hook = query.hook;
delete query.hook;
}
query = query || {};
if (custom) {
query.classname = objectName;
}
trace(opts,'query','ACS QUERY',query);
executor.apply(acs, [query, function(err,result){
if (err) {
if (callback) {
return callback && callback(err);
}
else {
fail(err);
}
}
try {
var key = custom ? objectName : object.objectName.toLowerCase(),
body = result.body && result.body.response && result.body.response[key],
meta = result.body && result.body.meta;
trace(opts,'meta','ACS META',meta);
if (expressions) {
// if we have expressions, we hook the callback to run them first
if (body && body.length) {
var newbody = {};
for (var e=0;e<expressions.length;e++) {
var expr = expressions[e];
var exprResult = expr(body);
if (Array.isArray(exprResult)) {
body = newbody = exprResult;
}
else {
// merge in each result
newbody = _.merge(newbody,exprResult);
}
}
if (!Array.isArray(newbody)) {
body = [newbody];
}
}
}
if (hook) {
body = hook(body);
}
/*jshint -W061 */
if (opts.eval) {
body = evalResult(opts.eval,body);
}
if (!callback) {
return output(opts, body, meta, query);
}
else {
return callback && callback(null, body, meta, result);
}
}
catch (E) {
if (callback) {
return callback(E);
}
fail(E);
}
}]);
};
fn.objectName = object.objectName;
fn.object = object;
fn.custom = custom;
return fn;
}
function resolvePath (fn) {
var home = process.env.HOME || process.env.USERPROFILE;
return path.resolve(process.cwd(), fn.replace(/~/g,home));
}
var validSelectExpressions = /^(count|max|min|avg|sum|round)\s?\(([\*\w]+)\)/i;
function parseSelectExpression(value) {
if (validSelectExpressions.test(value)) {
var match = validSelectExpressions.exec(value),
op = match[1].toLowerCase(),
field = match[2],
jscode,
record = field==='*' ? '' : '.'+field,
makeFieldAccessor = function() {
if (record) {
return 'this[c]'+record+' || (this[c].custom_fields && this[c].custom_fields'+record+') ';
}
return 'this[c]';
};
switch (op) {
case 'count': {
if (record) {
jscode = [
'var value = 0',
'for (var c=0;c<this.length;c++){',
' var found = !!('+makeFieldAccessor()+')',
' if (found) { value += 1; }',
'}',
'return value',
].join(';\n');
}
else {
jscode = 'return this.length';
}
break;
}
case 'max': {
jscode = [
'var value = 0',
'for (var c=0;c<this.length;c++){',
' value = Math.max(value, ('+makeFieldAccessor()+'|| value))',
'}',
'return value',
].join(';\n');
break;
}
case 'min': {
jscode = [
'var value = Number.MAX_VALUE',
'for (var c=0;c<this.length;c++){',
' value = Math.min(value, ('+makeFieldAccessor()+'|| value))',
'}',
'return value',
].join(';');
break;
}
case 'sum': {
jscode = [
'var value = 0',
'for (var c=0;c<this.length;c++){',
' value += ('+makeFieldAccessor()+'|| 0)',
'}',
'return value',
].join(';\n');
break;
}
case 'avg': {
jscode = [
'var value = 0',
'for (var c=0;c<this.length;c++){',
' value += '+makeFieldAccessor()+'||0',
'}',
'return value > 0 ? value/this.length : 0',
].join(';\n');
break;
}
case 'round': {
jscode = [
'var value = 0',
'for (var c=0;c<this.length;c++){',
' value += '+makeFieldAccessor()+'||0',
'}',
'return Math.round(value)',
].join(';\n');
break;
}
}
jscode = 'function f(){'+jscode+'}; f';
// console.log(jscode);
var fn = vm.runInNewContext(jscode);
fn.op = op;
fn.field = field;
return fn;
}
}
/**
* get the output formats string
*/
function getOutputFormats() {
return 'text, json, columns, csv, tsv';
}
/**
* print result output
*/
function output(opts, body, meta, query) {
if (!body && meta.status === 'ok') {
if (/delete/i.test(meta.method_name)) {
console.log("Deleted!");
}
return;
}
var format = opts.output || 'text';
switch (format) {
case 'text': {
if (_.isString(body)) {
console.log(body);
}
else {
console.log(util.inspect(body,{colors:chalk.enabled,depth:null}));
}
break;
}
case 'json': {
if (_.isString(body)) {
console.log(body);
}
else {
console.log(JSON.stringify(body,null,2));
}
break;
}
case 'tsv':
case 'csv': {
var json2csv = require('json-2-csv').json2csv,
options = {};
if (format==='tsv') {
options.DELIMITER = {FIELD:'\t'};
}
if (query.sel && query.sel.all) {
// put them in the right order by using the columns
options.columns = query.sel.all;
}
json2csv(body, function(err, csv) {
if (err) fail(err);
console.log(csv);
},options);
break;
}
case 'columns': {
var columnify = require('columnify'),
columns = columnify(body,{
headingTransform: function(value) {
return chalk.white.bold.underline(value.toUpperCase());
},
columns: query.sel && query.sel.all,
columnSplitter: ' ' // give it a little more space
});
console.log(columns);
break;
}
default: {
fail("Unsupported format: "+format);
}
}
}
/**
* trace logging
*/
function trace(opts, key, label, msg) {
if (opts && !opts.quiet && (opts[key] || opts.debug) && msg) {
var obj = _.isObject(msg) ? util.inspect(msg,{colors:chalk.enabled,depth:null}) : msg;
console.log(chalk.grey('['+label.toUpperCase()+'] ')+obj);
}
}
/**
* config color coding
*/
function setupColors(opts) {
if (opts.colors===false) {
chalk.enabled = false;
}
}
exports.saveConfig = saveConfig;
exports.getConfig = getConfig;
exports.fail = fail;
exports.getACS = getACS;
exports.evalResult = evalResult;
exports.parseObjectAsJS = parseObjectAsJS;
exports.makeArray = makeArray;
exports.createACSFunctor = createACSFunctor;
exports.isStringAnObject = isStringAnObject;
exports.resolvePath = resolvePath;
exports.parseSelectExpression = parseSelectExpression;
exports.trace = trace;
exports.output = output;
exports.setupColors = setupColors;
exports.getOutputFormats = getOutputFormats;