angoose
Version:
Angoose is a Remote Method Invocation module that comes with built-in mongoose/angular support. Now you can call server side module in browser just like you're in the server side!
344 lines (294 loc) • 13.5 kB
JavaScript
var path= require("path"),traverse = require("traverse"),hooks= require("hooks"), Q = require("q");
var fs = require("fs"),logging = require("log4js"), _ =require("underscore");
var domain = require("domain");
var toolbox = require("./util/toolbox");
var async = toolbox.async;
var logger = logging.getLogger('angoose');
var Context = require("./Context");
var Exception = require("./Exception");
/**@todo: move to mongoose plugin */
toolbox.patchMongoCallback();
function Pipeline(){}
for (var k in hooks) { Pipeline[k] = hooks[k];}
var proto = Pipeline.prototype;
proto.authorize= function authroize(invocation, authorizeBack){
logger.debug("STEP 100: pipline.authorize");
if(invocation.allowed !== undefined) authorizeBack && authorizeBack();
/** default implementation always grant access */
var defaultMode = angoose().config()['authorization-mode'];
invocation.allowed = (defaultMode !== 'deny-all');
authorizeBack && authorizeBack();
}
proto.resolveTarget = function resolveTarget(invocation, resolveBack){
logger.debug("STEP 200: pipline.resolveTarget");
/**logger.debug("Preparing target from JSON");*/
if(invocation.target){
logger.debug("Skipping default: resolveTarget");
return resolveBack();
}
if(!invocation.static && typeof(invocation.instance) != 'object')
return resolveBack( new Error("Missing invocation instance property" ));
var modelClass = angoose().getClass( invocation.clazz );
if(invocation.static){
invocation.target = modelClass;
return resolveBack();
}
invocation.target = new modelClass(invocation.instance);
return resolveBack( );
}
proto.resolveArguments = function resolveArguments(invocation, resolveArgBack){
logger.debug("STEP 300: pipline.resolveArguments");
//async(resolveArgBack)(false, invocation.args);
if(invocation.arguments !== undefined){
logger.debug("Skipping default: resolveArguments");
return resolveArgBack();
}
invocation.arguments = invocation.args;
resolveArgBack ();
}
proto.invoke = function invoke(invocation , invokeBack){
//logger.debug("STEP 400: pipline.invoke");
if(invocation.results !== undefined){
logger.debug("Skipping default: invoke");
return invokeBack();
}
var method = invocation.method;
var seqnum = invocation.seqnumber;
var target = invocation.target;
var args = invocation.arguments;
var useCallback = false;
function callbackHandler(err, result){
/** converting to Q promise */
logger.trace("In invoke callback handler #"+seqnum, err);
if(err){
logger.debug("Method invocation returned error:", err);
//return deferred.reject(err);
err = err instanceof Error? err: new Error(err);
return invokeBack( err);
}
logger.trace("Method invocation result:", result)
invocation.result = result;
invokeBack();
}
if( invocation.method == 'save' && args.indexOf("$callback")<0 ){
args.push('$callback'); // hack for now
}
useCallback = injectDependencies(target[method], args, callbackHandler);
logger.debug("invoking method ", method, "arguments", args );
if(!target[method]) return invokeBack(new Error('Method '+ method+" is not defined on module "+ invocation.clazz));
var ret = target[method].apply(target, args);
logger.trace("invoking result", ret);
if(ret instanceof Error) return callbackHandler(ret);
if(Q.isPromise(ret) || (ret && typeof(ret.done) == 'function')){
ret.done(function(result){
callbackHandler(false, result);
}, function(err){
callbackHandler(err);
});
}
else if(!useCallback || ret !== undefined) {
/** if method returns data directly or returned undefined */
return callbackHandler(false, ret);
}
}; /** end invoke */
proto.redact = function redact(invocation, redactBack){
logger.debug("STEP 500: pipline.redact");
if(invocation.redacted !== undefined){
logger.debug("Skipping default: redact");
return redactBack();
}
invocation.redacted = invocation.result;
redactBack();
}
proto.pack = function pack(invocation, packBack){
logger.debug("STEP 600: pipline.pack");
if(invocation.packed !== undefined){
logger.debug("Skipping default: pack");
return packBack();
}
var target = invocation.target;
var redacted = invocation.redacted;
var valueType = getValueType(redacted); /** model, models, object, string*/
var retdata = { success:true,
exception:null,
retval:redacted,
datatype: valueType,
seqnumber:invocation.seqnumber
};
if( !invocation.static && (! _.isEqual(target.toJSON(), invocation.target)))
{
/** return the instance data if it has changed */
retdata.instance = target.toJSON();
}
invocation.packed = retdata;
packBack();
}
proto.formatError = function formatError(invocation, formatBack){
if(invocation.packed) return formatBack();
var ex = invocation.exception;
ex.invocation = invocation.clazz +"."+ invocation.method;
ex.seqnumber = invocation.seqnumber;
invocation.packed = {
success:false,
exception:ex,
seqnumber: invocation.seqnumber
}
formatBack();
}
function getValueType(obj){
if(!obj || typeof(obj) == 'string') return 'string';
return 'object';
}
//hookIt('authorize', proto.authorize);
function injectDependencies(func, args, callback ){
var declaredArguments = toolbox.parseDeclaredArguments(func);
declaredArguments = declaredArguments.replace(/\s+/g, "").split(",");
logger.trace("injectDependencies() declaredArguments: ", declaredArguments, "provided args:", args);
var useCallback = false;
var len = args.length > declaredArguments.length? args.length:declaredArguments.length;
for(var i=0;i< len ;i++){ // this is problematic: functions may be wrapped and lost the arguments data
/** if client did not provide enough number of arguments, we fill it with null*/
if(typeof( args[i]) == 'undefined')
args[i] = undefined;
else if(args[i] === "$callback"){
useCallback = true;
args[i] = callback;
}
switch(declaredArguments[i]){
case '$callback':
logger.debug("Injecting callback handler");
args[i] = callback;
useCallback = true;
break;
case '$context':
logger.debug("Injecting angoose context");
args[i] = angoose().getContext();
break;
default:
break;
}
}
return useCallback;
}
function angoose(){
return require("./angoose");
}
/**
* Main execution method
*
* @param invocation Invocation object
*/
Pipeline.execute = function(ctx, invocation, callback){
['allowed', 'target', 'arguments','result','redacted'].forEach(function(key){
delete invocation[key];
});
var dmain = domain.create();
dmain.context = ctx;
//var excutionStorage = storageFactory.createNamespace("angoose");
/** here comes the main body of the processing */
dmain.run(function(){
var pipeline = new Pipeline();
var seqnum = invocation.seqnumber;
var sent = false;
logger.debug("------ BEGIN RMI #", seqnum, invocation.clazz, invocation.method );
logger.trace("Invocation object: ",invocation);
//invocation.method = req.params.method; /** method must be part of the path for routes permission etc */
ctx.seqnum = seqnum;
ctx.invocation = invocation;
function handleError(err, exName){
if(sent){
logger.debug("Response already sent")
return;
}
/**@todo: err may cause Circular issue when converting to JSON */
var ex = new Exception(err, exName);
invocation.exception = ex;
pipeline.formatError(invocation, function(er){
logger.debug("====== END RMI with Error #", seqnum, ex);
callback(invocation.packed);
sent = true;
});
}
dmain.on('error', function uncaught(ex){
logger.error("Uncaught error in invocation #"+seqnum, ex);
/**@todo: restart server if too many errors */
handleError( "Unexpected Server Error", 'RuntimeError');
});
/** setting execution context */
var sent = false;
var modelName = invocation.clazz;
var seqnum = invocation.seqnumber;
//excutionStorage.set("context", ctx);
pipeline.authorize(invocation, function(err){
if(err) return handleError(err);
if(! invocation.allowed) return handleError("Access Denied", "AuthError");
pipeline.resolveTarget(invocation, function(err){
if(err) return handleError(err);
if(!invocation.target) return handleError("Target not resolved.", "ModuleNotFoundError");
pipeline.resolveArguments(invocation,function(err){
if(err) return handleError(err);
if(!invocation.arguments) return handleError("Arguments not resolved.");
pipeline.invoke(invocation, function(ex){
if(ex) return handleError(ex);
pipeline.redact(invocation, function(err){
if(err) return handleError(err);
pipeline.pack(invocation, function(err){
if(err) return handleError(err);
//res.send(200, packedData);
callback(invocation.packed);
sent = true;
logger.debug("====== END RMI Succeeded #", seqnum);
if(Array.isArray(invocation.packed && invocation.packed.retval) && invocation.packed.retval.length>0){
logger.trace("Response is an array with length:", invocation.packed.retval.length, "First element:",invocation.packed.retval[0] );
}
else logger.trace("Response data is ", invocation.packed);
});
});
});
});
});
});
/** catch the strayed handling, returns error in 5 seconds if response not sent */
var tmout = setTimeout(function(){
if(sent) return;
var msg = "Timeout occurred when processing call "+invocation.clazz +"." + invocation.method;
logger.error(msg, seqnum);
handleError(msg);
//res.send(200, packError( {message: msg, value: msg, code:500 }, invocation.seqnumber ));
}, angoose().config().request_timeout || 5000);
tmout.unref();
});/** end context closure */
}
Pipeline.accept = function accept(req, res){
var invocation = decode(req.body);
var ctx = new Context({request:req, response:res} )
Pipeline.execute(ctx, invocation, function(data){
// traverse(data).forEach(function(){
// //if(this.circular) this.remove(); // circular ref will cause JSON.stringify fail
// });
res.send(200, data);
});
}
Pipeline.hookables = 'authorize,resolveTarget,resolveArguments,invoke,redact,pack,formatError'.split(",");
toolbox.addHookPoints(Pipeline);
module.exports = Pipeline;
function decode(obj){
/** due to Angular HTTP library's stupidity, keys starting with $ are ignored. hence _mongo_$ to work around this*/
if(!obj || typeof obj != 'object') return obj;
if(Array.isArray(obj)){
for(var i=0;i<obj.length;i++){
obj[i] = decode(obj[i]);
};
}
else{
Object.keys(obj).forEach(function(key){
var val = decode(obj[key]);
if(key.indexOf('_mongo_$')==0){
delete obj[key];
key = key.substring(7)
}
obj[key] = val;
})
}
return obj;
}