UNPKG

hopjs

Version:

A RESTful declarative API framework, with stub generators for Shell, and Android

1,884 lines (1,520 loc) 49.9 kB
/** Hop Core module This is the primary impelementation behind Hop @module Hop **/ var crypto = require('crypto'); var util = require('hopjs-common'); var url = require('url'); /* - display docs - enable unit tests - log functions - protect docs /w password - site name - readme.md - doc.css - */ var Hop=function(){ this.objects={}; this.interfaces={}; } Hop.use=function(what){ what(Hop); } if(process.env.NODE_ENV!="production"){ Hop.showErrors=true; } Hop.info=console.info; Hop.log=console.log; Hop.error=console.error; Hop.warn=console.warn; /** Given a string representation of a stack for a trace made during a call, return a more focused stack trace This function will attempt to cull a larger stack trace into something more relevant for what went on during a call. @param {String} errorStack a stack trace captured from an error @returns stack a focused stack trace */ Hop.callStack=function(errorStack){ var lines = errorStack.split("\n"); var stack=[]; var capture=false; for(var i=lines.length-1;i>0;i--){ var l = lines[i]; var f = (l.replace(/^\s+at\s+([^ ]+).+/,"$1")); if(f=="Hop_call_preTask"){ capture=true; } if(f=="Hop_call_postTask"){ capture=false; } if(capture==true && f.indexOf("Hop_call")!=0){ stack.push(l); } } return stack.join("\n"); } Hop.logCall=function(method,type,err,result){ var error = new Error(); var stack = Hop.callStack(error.stack); Hop[type](method,err+"\n"+stack); } Hop.sendFile=function(file,options){ return new Hop.File(file,options); } Hop.sendAttachment=function(file,options){ options=options||{}; options.attachment=true; return new Hop.File(file,options); } Hop.href=function(method,input){ this.method=Hop.Method.findMethod(method); if(!this.method){ throw new Error("Invalid method specified:"+this.method); } if(this.method.method!="get") throw new Error("Hop.href used to call a method which was not a GET"); this.input=input; } Hop.href.prototype.call=function(onComplete,request,response){ Hop.call(this.method.getMethod(),this.input,onComplete,request,response); } Hop.href.prototype.createRef=function(){ var ref = JSON.parse(JSON.stringify(this.input)); ref.href = this.toString(); return ref; } Hop.href.prototype.toJSON=function(){ var path = this.method.getPath(); var q = ""; for(var i in this.input){ if(path.indexOf(":"+i)!=-1){ path=path.replace(":"+i,this.input[i]); } else if(this.method.params[i]){ if(q.length>0){ q+="&"; } if(this.input[i] instanceof Array){ this.input[i].map(function(e){ if(q.length>0){ q+="&"; } q+=i+"="+encodeURI(e) }); } else { q+=i+"="+encodeURI(this.input[i]) } } } if(q==""){ } else { path=path+"?"+q; } if(Hop._host){ return url.resolve("http://"+Hop._host+"/",path); } else return path; } Hop.href.prototype.toString=Hop.href.prototype.toJSON; /** Render a template as a response This will render the specified template using express.js's template engine @param {string} template the name of the template to render @param {object} context the context for the template @example UserService.explore=function(input,onComplete,request){ return orComplete(null,Hop.render("explore",{user:request.session.user}) } @for Hop @method render @static **/ Hop.render=function(template,input){ return new Hop.Template(template,input); } Hop.Template=function(templateName,input){ this.template=templateName; this.input=input; } Hop.Template.prototype.send=function(response){ response.render(this.template,this.input); } /** * This will set the host name used to reference other objects in the returned URLs * by default HopJS will set this to the 'Host' header found in the request, but * this may not always be the right behavior if you're using Hop behind a * HA setup * * @param {string} host the hostname to use, etc 'www.foo.com' * */ Hop.setHost=function(host){ Hop._host=host; } /** A return error or response which indicates that there was an error with one of the fields provided in the request By default this will return a json object of the form { field:, :err } and a status code of 400 @param {string} field field associated with the error @param {string} errorMsg the message to be returned @example UserService.auth=function(input,onComplete){ if(input.password!=input.verifyPassword) return onComplete(Hop.FieldError("password","The passwords do not match")); } @for Hop @method FieldError @static **/ Hop.FieldError=function(field,errorMsg){ return { send: function(response){ response.send(400, { field: field, msg: errorMsg }); }, toString:function(){ return errorMsg; } } } /** A return error or response which indicates that there was an authentication error By default this will return 'Invalid authentication attempt' and a status code of 400 @param {string} msg the message to be returned @example UserService.auth=function(input,onComplete){ User.auth(input,function(err,user){ if(err) return onComplete(Hop.AuthError()); }) } @for Hop @method AuthError @static **/ Hop.AuthError=function(msg){ var msg = msg || "Authentication Error"; return { send: function(response){ response.send(400, msg); }, toString:function(){ return msg; } } } /** A return error or response which indicates that there was an internal server error By default this will return 'Internal error' and a status code of 500 and will include an error reference ID which is also logged via Hop.error @param {string} returnedError the message to be returned to the user @param {string} internalErrorMsg the internal error message which will be sent to the log @example UserService.delete=function(input,onComplete){ User.delete(input,function(err,user){ if(err) return onComplete(Hop.InternalError("Unable to delete user",err)); }) } @for Hop @method InternalError @static **/ Hop.InternalError=function(returnedError,internalErrorMsg){ var errorId = Math.random().toString(36).substring(2); var extMsg = (returnedError||"Internal error")+" (reference Id: "+errorId+")" + (Hop.showErrors?" "+internalErrorMsg:""); return { send: function(response){ Hop.error("Error (reference Id: "+errorId+"):",internalErrorMsg||returnedError); response.status(500); response.send(extMsg); }, toString:function(){ Hop.error("Error (reference Id: "+errorId+"):",internalErrorMsg||returnedError); return (extMsg) } } } /** A return error or response which indicates that there was an DAO error By default this will return 'Internal error' and a status code of 500 and will include an error reference ID which is also logged via Hop.error @param {string} returnedError the message to be returned to the user @param {string} internalErrorMsg the internal error message which will be sent to the log @example UserService.delete=function(input,onComplete){ User.delete(input,function(err,user){ if(err) return onComplete(Hop.DAOError("Unable to delete user",err)); }) } @for Hop @method DAOError @static **/ Hop.DAOError=Hop.InternalError; Hop.File=function(file,options){ this.file=file; this.options=options||{}; } Hop.File.prototype.send=function(response){ var filename = this.file || this.options.filename; if(this.options.attachment==true) response.download(this.file,filename); else response.sendfile(this.file); } /** Redirect to another url @param {string} url the message to be returned to the user @param {object} options options for the redirect @param {bool} options.noCache Do not cache this redirect @example UserService.logout=function(input,onComplete,request){ delete request.session.user; return onComplete(null,Hop.redirect("/login")); } @for Hop @method redirect @static **/ Hop.redirect=function(url,options){ return new Hop.Redirect(url,options); } Hop.Redirect=function(url,options){ this.url=url; this.options=options; } Hop.Redirect.prototype.send=function(response){ if(this.options && this.options.noCache){ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1. response.setHeader("Pragma","no-cache"); // HTTP 1.0. response.setHeader("Expires","0"); // Proxies. } response.redirect(this.url); } Hop.Objects={}; Hop.Interfaces={}; /** Calculate a checksum for the Hop This is used to detect changes in the version of the Hop @for Hop @method checksum @static **/ Hop.checksum = function(){ if(Hop._checksum) return Hop._checksum; var md5 = crypto.createHash('md5'); md5.update(Hop.toJSON(true)); Hop._checksum = md5.digest('hex'); return Hop._checksum; } /** Utility class for providing mock response objects @class Hop.StubResponse **/ Hop.StubResponse=function(){ this.header={}; } Hop.StubResponse.prototype.set=function(name,value){ this.header[name]=value; } Hop.StubResponse.prototype.get=function(name){ return this.header[name]; } /** Utility class for providing mock request objects @class Hop.StubRequest **/ Hop.StubRequest=function(){ this.header={}; this.response = new Hop.StubResponse(); this.session={}; } Hop.StubRequest.prototype.getResponse=function(){ return this.response; } Hop.StubRequest.prototype.set=function(name,value){ this.header[name]=value; } Hop.StubRequest.prototype.get=function(name){ return this.header[name]; } Hop.getTemplates=function(obj,when,type){ if(!obj._templates) return null; if(!obj._templates[type]) return null; if(!obj._templates[type][when]) return null; return obj._templates[type][when]; } Hop.renderTemplates=function(obj,when,type,input){ try { if(!obj._templates) return ""; if(!obj._templates[type]) return ""; if(!obj._templates[type][when]) return ""; var _input={}; for(var i in input){ _input[i]=input[i]; } _input.Hop=Hop; var out = ""; obj._templates[type][when].map(function(template){ out+=(Hop.renderTemplate(template)(_input)); }); return out; } catch(e){ Hop.error(e); Hop.error(e.stack); } } Hop.addTemplate=function(obj,when,type,template){ if(!obj._templates) obj._templates={}; if(!obj._templates[type]) obj._templates[type]={}; if(!obj._templates[type][when]) obj._templates[type][when]=[]; if(obj._templates[type][when].indexOf(template)==-1){ obj._templates[type][when].push(template); } } Hop.renderBeforeTemplates=function(type,input){ return Hop.renderTemplates(Hop,"before",type,input); } Hop.renderAfterTemplates=function(type,input){ return Hop.renderTemplates(Hop,"after",type,input); } Hop.getBeforeTemplates=function(type){ return Hop.getTemplates(Hop,"before",type); } Hop.getAfterTemplates=function(type){ return Hop.getTemplates(Hop,"after",type); } Hop.addBeforeTemplate=function(type,template){ Hop.addTemplate(Hop,"before",type,template); } Hop.addAfterTemplate=function(type,template){ Hop.addTemplate(Hop,"after",type,template); } /** Define a new class * Use #classname to have the class name substituted into the URL @param {string} name the name of the class @param {object} [instance] an instance of the object @param {function} onDefine the lambda used to define the interface @example Hop.defineClass("Email",new Email(),function(api){ //define the class }); @for Hop @method defineClass @static **/ Hop.defineClass=function(name,instance,onDefine){ delete Hop._checksum; var api = new Hop.Object(name,instance); api.setLocalInterface(); onDefine(api); } /** Define a new interface * Use #classname to have the class name substituted into the URL @param {string} name the name of the interface @param {function} onDefine the lambda used to define the interface @example Hop.defineInterface("Notification",function(api){ api.post("send","#classname/send").usage("Sends a message").demand("msg").demand("subject").demand("to"); } @for Hop @method defineInterface @static **/ Hop.defineInterface=function(name,onDefine){ delete Hop._checksum; var intf = new Hop.Interface(name,onDefine); } Hop.toJSON=function(noChecksum){ var obj = {}; obj.Objects=Hop.Objects; obj.basePath = Hop.basePath; if(noChecksum!=true) obj.checksum = Hop.checksum(); for(var i in this._toJSONHandler){ var handler = this._toJSONHandler[i]; handler(obj); } return JSON.stringify(obj,function(key,value){ if(/^_/.test(key)){ return undefined; } return value; }," "); } Hop.fromJSON=function(jsonString){ if(typeof jsonString=="object") var obj = jsonString; else if(typeof jsonString=="string") var obj = JSON.parse(jsonString); if(obj.Objects){ for(var objectName in obj.Objects){ var _object = obj.Objects[objectName]; var object = new Hop.Object(objectName); for(var j in _object){ object[j]=_object[j]; } for(var methodName in obj.Objects[i].methods){ var _method = obj.Objects[i].methods[methodName]; var method = new Hop.Method(_method.method,object,_method.name,_method.path); object[_method.name]=method; for(var j in _method){ method[j]=_method[j]; } } } } } /** Add a call back which will be called when a version of the API must be built from JSON @param {function} onJSON Callback to be called when a json version of Hop is requested @param {object} The stub object which is being populated for conversion to JSON @for Hop @method addToJSONHandler @static **/ Hop.addToJSONHandler=function(onJSON){ if(!this._fromJSONHandler){ this._fromJSONHandler=[]; } this._fromJSONHandler.push(onJSON); } /** Add a call back which will be called when a JSON version of the API is requested @param {function} onJSON Callback to be called when a json version of Hop is requested @param {object} The stub object which is being populated for conversion to JSON @for Hop @method addToJSONHandler @static **/ Hop.addToJSONHandler=function(onJSON){ if(!this._toJSONHandler){ this._toJSONHandler=[]; } this._toJSONHandler.push(onJSON); } Hop.Interface = function(name,onDefine){ this.onDefine=onDefine; this.name=name; Hop.Interfaces[name]=this; } /** Defines an object This object is created by .defineClass @class Hop.Object **/ Hop.Object = function(name,instance){ this.name=name; this._instance=instance; this.methods={}; Hop.Objects[name]=this; } /** Wrap an object so that it may be called localy with all of the functionality that Hop normally provides This function will create a wrapped version of a defined object with all of the functionality that Hop would normally provide. @example Hop.defineClass("UserService",function(api){ api.get("load","/user/:userID").demand("userID").cacheId("/user/:userID"); }); var userService = Hop.Object.wrap("UserService"); //This call will now be subject to all of the normal constraints and functionality provided by Hop userService.load({ userID:5 },function(err,result){ },request); @param {String} objectName Then name of the object to wrap @returns {Object} An object with implementations of all the functions defined by the object. The returned functions will have the calling signature (input,onComplete,request) @method Hop.Object.wrap **/ Hop.Object.wrap=function(objectName){ var result = {}; var obj = Hop.Object.findObject(objectName); if(obj){ for(var i in obj.methods){ var method = obj.methods[i]; (function(method){ result[method.name]=function(input,onComplete,request,response){ Hop.call(method.getMethod(),input,onComplete,request,response); }; })(method); } } else throw "Invalid object name specified"; return result; } Hop.Object.prototype.callOnError=function(method,request,input,error,stack){ if(!stack){ var exp = new Error(); var stack = Hop.callStack(exp.stack); } else { stack = Hop.callStack(stack); } if(this.onError){ var res = this.onError(method,request,input,error,stack); if(res!==null) return res; } var e = error; if(error && error.toString) e=error.toString(); Hop.error(method.getMethod(),":returned an error result of '",e,"'\n",stack); return error; } /** Provide a function that will be called when an error occured for any call in the class This function can be used to filter error messages. A typical use case would be to have a small set of allowed error messages, and return a generic error message in all other cases. Or to attach a unique number to each error and provide details in the console log. This function should return a string for the desired error string or null if no custom error is returned. This function is not asynchronous. @param {Function} onError function to be called when an error has occured @param {Object} onError.method The method call associated with the error @param {Object} onError.request The request for the call @param {Object} onError.input The input for the call @param {String} onError.error The error string @param {String} onError.stack A stack trace of where the error occured @example Hop.defineClass("UserService",function(api){ api.post("update","/user/:userID"); api.errorHandler(function(method,request,input,error,stack){ //If we know what the error is simply return it if(allowedErrors.contains(error)){ return null; } else { //If we don't recognize the error produce a unique ID for it and spit it out in the logs var id = Date.now(); var err = "Error ("+id+")"; Log.error(method.getName(),request,input,id,error,stack); return err; } }); }); @for Hop.Object @method errorHandler */ Hop.Object.prototype.errorHandler=function(onError){ this.onError=onError; } Hop.Object.prototype.setLocalInterface=function(){ this._localInterface=true; } Hop.Object.prototype.isLocalInterface=function(){ return this._localInteraface===true; } Hop.Object.prototype.renderBeforeTemplates=function(type,input){ return Hop.renderTemplates(this,"before",type,input); } Hop.Object.prototype.renderAfterTemplates=function(type,input){ return Hop.renderTemplates(this,"after",type,input); } Hop.Object.prototype.getBeforeTemplates=function(type){ return Hop.getTemplates(this,"before",type); } Hop.Object.prototype.getAfterTemplates=function(type){ return Hop.getTemplates(this,"after",type); } Hop.Object.prototype.addBeforeTemplate=function(type,template){ Hop.addTemplate(this,type,"before",type,template); } Hop.Object.prototype.addAfterTemplate=function(type,template){ Hop.addTemplate(this,type,"after",type,template); } /** Have this object extend from an interface @example Hop.defineInterface("Notification",function(api){ api.post("send","#classname/send").usage("Sends a message").demand("msg").demand("subject").demand("to"); } Hop.defineClass("Email",function(api){ //This will essentially evaluate the interface defined above against thsi class adding the send function api.extend("Notification"); }); @for Hop.Object @method extend **/ Hop.Object.prototype.extend=function(intf){ if(!this.interfaces){ this.interfaces={}; } this.interfaces[intf]=true; if(Hop.Interfaces[intf]){ Hop.Interfaces[intf].onDefine(this); } else throw ("Invalid interface specified:"+intf); } /** Perform a modification to all methods defined on a class within the specified lamda @example Hop.defineClass("UserService",function(api){ api.each(function(api){ api.get("load","/user/:userID"); api.del("del","/user/:userID"); },function(method){ method.demand("userId","The user ID"); method.requireUser(); }); }); @for Hop.Object @method each **/ Hop.Object.prototype.each=function(onDefine,onMethod){ var self = this; var methods = []; var proxy = { get: function(){ var ret = self.get.apply(self,arguments); methods.push(ret); return ret;}, post: function(){ var ret = self.post.apply(self,arguments); methods.push(ret); return ret;}, put: function(){ var ret = self.put.apply(self,arguments); methods.push(ret); return ret;}, del: function(){ var ret = self.del.apply(self,arguments); methods.push(ret); return ret;}, create: function(){ var ret = self.create.apply(self,arguments); methods.push(ret); return ret;}, read: function(){ var ret = self.read.apply(self,arguments); methods.push(ret); return ret;}, update: function(){ var ret = self.update.apply(self,arguments); methods.push(ret); return ret;}, list: function(){ var ret = self.list.apply(self,arguments); methods.push(ret); return ret;}, } onDefine(proxy); methods.map(function(method){ onMethod(method); }); } /** Expand any href's found in the result */ Hop.postCallExpand=function(method,name,options){ var self = method; options = options || {}; return function(req,input,err,result,next,response){ if(result && !err){ var thisInput = JSON.parse(JSON.stringify(input)); if(!result.href && options.injectHref) result.href = new Hop.href(self.methods[name].getMethod(),thisInput); var expands=[]; if(typeof input.expand=="string"){ expands.push(input.expand); } else if(input.expand instanceof Array){ expands = input.expand.slice(0); } doExpands=function(){ if(expands.length>0){ var expand = expands.pop(); //FIXME This should go to any level of depth which means we could do an eval? // We will only expand calls we could make locally so we don't find ourselves victim to attack if(result[expand] && result[expand].href && result[expand].href instanceof Hop.href){ result[expand].href.call(function Hop_expand_results(err,res){ if(err){ result[expand]={error: err} } else { result[expand]=res; } doExpands(); },req,response); } else { doExpands(); } } else { next(); } } doExpands(); } else next(); } } /** Define a HTTP create CRUD based post @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' The returned result is a URL to the created resource as either a Hop.href or a string See http://java.dzone.com/articles/beautiful-rest-json-apis-les?mz=62823-enterprise-integration for details @example UserService.create=function(input,onComplete){ var user = { name: input.name, email: input.email } UserDAO.save(user,function(err,savedUser){ if(!err){ return onComplete(null,new Hop.href("UserService.read",{id:savedUser.id}); } else return onComplete(err,null); }); } Hop.defineClass("UserService",function(api){ api.create("create","/user/"); //.. }); @for Hop.Object @method create @chainable **/ Hop.Object.prototype.create=function(name,path){ var self = this; this.methods[name]=new Hop.Method("post",this,name,path); this.methods[name].optional("expand","Expand the result to include the resource located in the href (true/false)"); this.methods[name].addPostCall(function(req,input,err,result,next,response){ if(result){ if(result instanceof Hop.href || typeof result=="string"){ if(input.expand){ var location = result.toString(); result.call(function(e,r){ response.set("Location",location); response.status(201); next(e,r); },req); } else { result.send=function(res){ var location = result.toString(); res.set('Location',location); res.send(201,result.createRef()); }; next(); } } else { throw new Error("Invalid return type from create, must be an instance of Hop.href or a url (as a string)"); next(); } } else { next(); } },"data"); this.methods[name].addPostCall(Hop.postCallExpand(self,name),"data"); return this.methods[name]; } /** Define a HTTP update CRUD based put @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' This method will expand any href's inline in the returned result if asked to do so See http://java.dzone.com/articles/beautiful-rest-json-apis-les?mz=62823-enterprise-integration for details @example UserService.update=function(input,onComplete){ var user = { name: input.name, email: input.email } UserDAO.save(user.id, user,function(err,savedUser){ if(!err){ var res = {}; res.href=new Hop.href("UserService.read",{id:savedUser.id}); return onComplete(null,res); } else return onComplete(err,null); }); } Hop.defineClass("UserService",function(api){ api.create("update","/user/:id"); //.. }); @for Hop.Object @method update @chainable **/ Hop.Object.prototype.update=function(name,path){ var self = this; //TBD should this be a patch or a put ?!? this.methods[name]=new Hop.Method("put",this,name,path); this.methods[name].optional("expand","Which in-line hrefs to expand"); this.methods[name].addPostCall(Hop.postCallExpand(self,name),"data"); return this.methods[name]; } /** Define a HTTP read CRUD based get @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' This method will always inject the href for the current link into the resulting object @example { href: link to itself } See http://java.dzone.com/articles/beautiful-rest-json-apis-les?mz=62823-enterprise-integration for details @example UserService.read=function(input,onComplete){ return onComplete(null,users[input.id]); } Hop.defineClass("UserService",function(api){ api.read("read","/user/:id"); //.. }); @for Hop.Object @method read @chainable **/ Hop.Object.prototype.read=function(name,path){ var self = this; this.methods[name]=new Hop.Method("get",this,name,path); this.methods[name].optional("expand","Which in-line hrefs to expand"); this.methods[name].addPostCall(Hop.postCallExpand(self,name,{injectHref:true}),"data"); return this.methods[name]; } /** Define a HTTP get of a list on this method @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' The list method by default will have optional values of offset and limit. And will return an object that has the following structure: @example { href: link to itself offset: offset value limit: number of items returned prev: { href: link to previous part of the set } next: { href: link to next part of the set }, items:[ ... ] } See http://java.dzone.com/articles/beautiful-rest-json-apis-les?mz=62823-enterprise-integration for details @example UserService.list=function(input,onComplete){ return onComplete(null,arrayOfUsers); } Hop.defineClass("UserService",function(api){ api.list("list","/user/"); //.. }); @for Hop.Object @method list @chainable **/ Hop.Object.prototype.list=function(name,path){ var self = this; this.methods[name]=new Hop.Method("get",this,name,path); this.methods[name].optional("limit","The number of results to return").optional("offset","Where to start in the result set"); this.methods[name].addPreCall(function Hop_Method_preCall_list(req,input,onComplete,next){ if(typeof input.limit=="undefined" || !(/[0-9]+/.test(input.limit))) input.limit=25; if(typeof input.offset=="undefined" || !(/[0-9]+/.test(input.offset))) input.offset=0; return next(); },"demand"); this.methods[name].addPostCall(function Hop_Method_postCall_list(req,input,err,result,next){ if(result){ input.offset=parseInt(input.offset); input.limit=parseInt(input.limit); var thisInput = JSON.parse(JSON.stringify(input)); var prevInput = JSON.parse(JSON.stringify(input)); var nextInput = JSON.parse(JSON.stringify(input)); var itemCount = (result.items instanceof Array?result.items.length:(typeof result.items=="object"?Object.keys(result.items).length:0)); if(!result.href) result.href = new Hop.href(self.methods[name].getMethod(),thisInput); if(!result.next && itemCount>=thisInput.limit){ result.next={}; nextInput.offset+=nextInput.limit; result.next.href = new Hop.href(self.methods[name].getMethod(),nextInput); for(var i in nextInput){ result.next[i]=nextInput[i]; } } if(!result.prev && (prevInput.offset-(prevInput.limit))>=0){ result.prev={}; prevInput.offset-=(prevInput.limit); result.prev.href = new Hop.href(self.methods[name].getMethod(),prevInput); for(var i in prevInput){ result.prev[i]=prevInput[i]; } } } next(); },"data"); return this.methods[name]; } /** Define a HTTP patch call on this method @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' @example Hop.defineClass("UserService",function(api){ api.patch("patch","/user/:id"); //.. }); @for Hop.Object @method patch @chainable **/ Hop.Object.prototype.patch=function(name,path){ this.methods[name]=new Hop.Method("patch",this,name,path); return this.methods[name]; } /** Define a HTTP get call on this method @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' @example Hop.defineClass("UserService",function(api){ api.get("load","/user/:userID"); //.. }); @for Hop.Object @method get @chainable **/ Hop.Object.prototype.get=function(name,path){ this.methods[name]=new Hop.Method("get",this,name,path); return this.methods[name]; } /** Define a HTTP post call on this method @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' @example Hop.defineClass("UserService",function(api){ api.post("update","/user/:userID"); //.. }); @for Hop.Object @method post @chainable **/ Hop.Object.prototype.post=function(name,path){ this.methods[name]=new Hop.Method("post",this,name,path); return this.methods[name]; } /** Define a HTTP del call on this method @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' @example Hop.defineClass("UserService",function(api){ api.del("delete","/user/:userID"); //.. }); @for Hop.Object @method del @chainable **/ Hop.Object.prototype.del=function(name,path){ this.methods[name]=new Hop.Method("delete",this,name,path); return this.methods[name]; } /** Define a HTTP put call on this method @params {String} method The name of the method on the associated object @params {String} path The HTTP path that this call can be found on. Variables can be specified as part of the path utilizing ':' @example Hop.defineClass("UserService",function(api){ api.put("create","/user/"); //.. }); @for Hop.Object @method put @chainable **/ Hop.Object.prototype.put=function(name,path){ this.methods[name]=new Hop.Method("put",this,name,path); return this.methods[name]; } /** Define the usage for this class @example Hop.defineClass("UserService",function(api){ api.usage("Manages Users"); //.. }); @for Hop.Object @method usage @chainable **/ Hop.Object.prototype.usage=function(usage){ this.desc=usage; return this; } /** Find an object by name @return {string} The name of the method @example Hop.defineClass("UserService",function(api){ api.get("load","/user/:userID") }); var method = Hop.Method.findMethod("UserService.load"); Hop.log(method.getMethod()); //returns UserService.load @for Hop.Object @method findObject @static **/ Hop.Object.findObject=function(objName){ if(objName.indexOf(".")!=-1){ var parts = objName.split("."); objName = parts.splice(0,parts.length-1).join("."); } return Hop.Objects[objName]; } /** Class used to define methods @class Hop.Method @constructor **/ Hop.Method = function(method,object,name,_path){ this._className = object.name; this.method=method; this.name=name; this.path=_path; this.params={}; this._preCall=[]; this._postCall=[]; this.defaults={}; this.options={}; //Let's figure out what parameters we must have! var self=this; var pathParts = _path.split("/"); pathParts.map(function(part){ if(/^:.*/.test(part)){ self.demand(part.replace(/^:/,"")); } }); } Hop.Method.prototype.toJSON=function(noChecksum){ var obj = {}; this.fullPath = Hop.Method.getPath(Hop.basePath,this._className,this); return this; } /** Find a method by name @return {string} The name of the method @example Hop.defineClass("UserService",function(api){ api.get("load","/user/:userID") }); var method = Hop.Method.findMethod("UserService.load"); Hop.log(method.getMethod()); //returns UserService.load @method findMethod @static **/ Hop.Method.findMethod=function(objName){ var obj = Hop.Object.findObject(objName); if(obj){ var parts = objName.split("."); var method = parts.splice(parts.length-1); return obj.methods[method]; } else return null; } /** Get the name of the method This will get the name of the method @return {string} The name of the method @example Hop.defineClass("UserService",function(api){ api.get("load","/user/:userID") }); var method = Hop.Method.findMethod("UserService.load"); Hop.log(method.getMethod()); //returns UserService.load @method getMethod **/ Hop.Method.prototype.getMethod=function(){ return Hop.Method.getMethod(this._className,this); } Hop.Method.getMethod=function(className,method){ return className+"."+method.name; } /** Get the full url for the method This will get the full path for the url for the method. @return {string} The URL for the method @method getPath **/ Hop.Method.prototype.getPath=function(){ return Hop.Method.getPath(Hop.basePath,this._className,this); } Hop.Method.getPath=function(basePath,className,method){ var _path = method.path.replace("#classname",className.toLowerCase()); _path = _path.replace("#ClassName",className); return util.webpath.join(basePath,_path); } /** Specify the default values for this call These values will be copied into the input if no existing value is found. @param {object} defaults @method defaultValues @chainable **/ Hop.Method.prototype.defaultValues=function(defaults){ this.defaults=defaults; return this; } /** Specify a number of optionals for for a call @example api.post("create","/user/profile/").optionals("email","name","password"); @param {string} name of parameter (+) @for Hop.Method @method optionals @chainable **/ Hop.Method.prototype.optionals=function(){ for(var i=0;i<arguments.length;i++) this.optional(arguments[i]); return this; } /** Specify a number of demands for for a call @example api.post("create","/user/profile/").demands("email","name","password"); @param {string} name name of parameter (+) @for Hop.Method @method demands @chainable **/ Hop.Method.prototype.demands=function(){ for(var i=0;i<arguments.length;i++) this.demand(arguments[i]); return this; } /** Demand a parameter for a call @example api.post("create","/user/profile/").demand("email","Email address"); @param {string} name of parameter @param {string} desc description of parameter @for Hop.Method @method demand @chainable **/ Hop.Method.prototype.demand=function(name,desc,validate){ this.params[name]={ desc: desc, validate: validate, demand:true }; return this; } /** Describe this method @param {string} usage text describing the function for documentation purposes @chainable @method usage **/ Hop.Method.prototype.usage=function(desc){ this.desc=desc; return this; } /** Note that this method can only be called when there is a user in the session *This will only allow this method to be called if req.session.user exists* @example Hop.defineClass("UserService",UserService,function(api){ //Only allow this method to be called if a user is in the session api.post("sendMsg","/user/:userId/message").authed(); }); @chainable @method authed **/ Hop.Method.prototype.authed=function(desc){ this.authed=true; this.addPreCall(function Hop_Method_preCall_authed(req,input,onComplete,next){ if(!req.session.user){ return onComplete("Permission denied"); } else return next(); },"demand"); return this; } /** Note that this method can only be called using https @chainable @method secure **/ Hop.Method.prototype.secure=function(desc){ this.secure=true; this.addPreCall(function Hop_Method_preCall_secure(req,input,onComplete,next){ if(!req.secure){ return onComplete("Permission denied"); } else return next(); },"demand"); return this; } /** Include the express CSRF token in the response headers for this method The CSRF token is utilized to prevent cross site request forgery attacks, and is a middleware component for express. See here for information: http://www.senchalabs.org/connect/middleware-csrf.html By default HopJS will attempt to utilize the CSRF functionality in express if it is enabled, but some clients require a means to access the CSRF token, hence this function will will send the csrf token in the headers as 'x-crsf-token' The primary usage for this function is with secure login functions @example Hop.defineClass("UserService",UserService,function(api){ api.post("login","/login").demands("username","password").sendCSRFToken(); }); @chainable @method sendCSRFToken **/ Hop.Method.prototype.sendCSRFToken=function(){ this.sendsCSRFToken=true; this.addPostCall(function Hop_Method_postCall_sendCSRFToken(req,input,err,result,next){ if(!err && req.session._csrf){ req._response.set('X-CSRF-Token',req.session._csrf); } next(); },"last"); return this; } /** Optional parameter for a call @example api.post("create","/user/profile/").optional("phoneNumber","Phone Number"); @param {string} name of parameter @param {string} desc description of parameter @for Hop.Method @method optional @chainable **/ Hop.Method.prototype.optional=function(name,desc,validate){ this.params[name]={ desc: desc, validate: validate, optional:true }; return this; } /** Indicate this function performs longPolling @example api.get("status","/server/:serverID/status").longPoll(); @for Hop.Method @method longPoll @chainable **/ Hop.Method.prototype.longPoll=function(){ this.options.noCache=true; this.options.longPoll=true; return this; } /** Indicate this function should avoid caching @example api.post("create","/user/profile/").noCache(); @for Hop.Method @method noCache @chainable **/ Hop.Method.prototype.noCache=function(){ this.options.noCache=true; return this; } Hop.Method.prototype.renderBeforeTemplates=function(type,input){ return Hop.renderTemplates(this,"before",type,input); } Hop.Method.prototype.renderAfterTemplates=function(type,input){ return Hop.renderTemplates(this,"after",type,input); } Hop.Method.prototype.getBeforeTemplates=function(type){ return Hop.getTemplates(this,"before",type); } Hop.Method.prototype.getAfterTemplates=function(type){ return Hop.getTemplates(this,"after",type); } Hop.Method.prototype.addBeforeTemplate=function(type,template){ Hop.addTemplate(this,"before",type,template); } Hop.Method.prototype.addAfterTemplate=function(type,template){ Hop.addTemplate(this,"after",type,template); } /** Demand a file be provided for this method. @example api.post("create","/user/profile/").demandFile("avatar","Users avatar image"); @for Hop.Method @method demandFile @chainable **/ Hop.Method.prototype.demandFile=function(name,desc,validate){ this.params[name]={ desc: desc, validate: validate, demandFile:true, demand:true }; return this; } /** Specify that a file may optionally be provided as an input to this call. @example api.post("create","/user/profile/").optionalFile("avatar","Users avatar image"); @for Hop.Method @method optionalFile @chainable **/ Hop.Method.prototype.optionalFile=function(name,desc,validate){ this.params[name]={ desc: desc, validate: validate, optionalFile:true }; return this; } /** Add a function that will be called after this call is completed @param {function} call function to be called when this call is completed, which is passed the following parameters: @param {object} call.request the ExpressJS / HTTP request object @param {object} call.input the input parameters to the call @param call.err the resulting err from the call @param {object} call.result - the result of the call @param {function} call.next - to be called when the callback is completed, causing the next call back to be called @param {object} call.response the ExpressJS / HTTP response object @param {string} phase the phase in which this function will be called (see below) *Phases* 1. (Pre call phases) 2. **CALL** 3. first - called before any other phases 4. data - data processing and conversion 5. event - event emission 6. cache - cache phase 7. last - called last @example api.get("load","/user/:userID").addPostCall(function(req,input,err,result,next){ //Let's caclulate the users age: if(result && result.birthdate){ result.age = User.calculateAge(result.birthdate); } next(); },"data"); @for Hop.Method @method addPostCall @chainable **/ Hop.Method.postCallPhases = [ "first", "data", "event", "cache", "last" ]; Hop.Method.prototype.addPostCall=function(call,phase){ if(Hop.Method.postCallPhases.indexOf(phase)==-1) throw "Invalid post call phase specified '"+phase+"' valid phases are "+Hop.Method.postCallPhases.join(", "); this._postCall.push({ call: call, phase: phase }); this._postCall.sort(function(a,b){ var aPhase = Hop.Method.postCallPhases.indexOf(a.phase); var bPhase = Hop.Method.postCallPhases.indexOf(b.phase); if(aPhase < bPhase){ return -1; } else if(bPhase > aPhase){ return 1; } else return 0; }); return this; } /** Add a function that will be called before this call is executed @param {function} call function to be called prior to when this call is executed , which is passed the following parameters: @param {object} call.request the ExpressJS / HTTP request object @param {object} call.input the input parameters to the call @param {function} call.onComplete to be called if the function wants to short circuit and return a result @param {function} call.next to be called when the callback is completed, causing the next call back to be called @param {object} call.response the ExpressJS / HTTP reponse object @param {string} phase the phase in which this function will be called (see below) *Phases* 1. first - called first 2. demand - verifies that the required parameters are in place 3. conversion - will convert the input types to the expected types 4. validation - input validation 5. auth - authentication phase 6. event - event emission 7. cache - cache phase 8. last - the last set of calls to be called prior to the function call 9. **CALL** 10. (post calls) @example api.get("load","/user/:userID").addPreCall(function(req,input,err,onComplete,next){ //If we have a user allow this call to complete if(req && req.session && req.session.user){ next(); //If not return an error } else { return onComplete("Permission denied"); } },"auth"); @for Hop.Method @method addPreCall @chainable **/ Hop.Method.preCallPhases=[ "first","demand", "conversion", "validation", "auth", "event","cache","last"]; Hop.Method.prototype.addPreCall=function(call,phase){ if(Hop.Method.preCallPhases.indexOf(phase)==-1) throw "Invalid pre call phase specified '"+phase+"' valid phases are "+Hop.Method.preCallPhases.join(", "); this._preCall.push({ call: call, phase: phase }); this._preCall.sort(function(a,b){ var aPhase = Hop.Method.preCallPhases.indexOf(a.phase); var bPhase = Hop.Method.preCallPhases.indexOf(b.phase); if(aPhase < bPhase){ return -1; } else if(bPhase > aPhase){ return 1; } else return 0; }); return this; } /** Indicate that this method call has been depricated @for Hop.Method @method depricated @chainable **/ Hop.Method.prototype.depricated=function(){ this.depricated=true; } /** Calls the specified method This function is provided so that all functionality around a specific call may be utilized. @param {string} name Name of the function to call @param {object} input Input for the call @param {function} callback for completion @param callback.err The error returned from the call @param callback.result The result returned from the call @param {object} [request] ExpressJS/HTTP request object @example var input = { username: "cfox", email:"cfox@gmail.com"} Hop.call("UserService.create",input,function(err,res){ Hop.log(err,res); }); @for Hop @method call @static **/ Hop.call=function(name,input,onComplete,request,response){ var output = {}; var obj = Hop.Object.findObject(name); if(!obj) throw ("Invalid object specified: "+name); var method = Hop.Method.findMethod(name); if(!method) throw ("Invalid method specified: "+name); for(var paramName in method.defaults){ output[paramName]=method.defaults[paramName]; } for(var paramName in method.params){ var param = method.params[paramName]; if(typeof input[paramName]!="undefined"){ output[paramName]=input[paramName]; } } for(var paramName in method.params){ var param = method.params[paramName]; if(param.demand && typeof output[paramName]=="undefined"){ throw ("Missing parameter:" +paramName); } } if(obj._instance){ if(!obj._instance[method.name]) throw ("Method not found on object instance: "+method.name); var preTasks = method._preCall.length; var runPreTaskFunctions=function Hop_call_preTask(){ if(preTasks>0){ method._preCall[method._preCall.length-preTasks].call(request,output,onComplete,function Hop_call_preCall(){ preTasks--; runPreTaskFunctions(); },response); } else { obj._instance[method.name](output,function Hop_call_instance(err,result,statusCode){ if(typeof statusCode!="undefined"){ response.status(statusCode); } var postTasks = method._postCall.length; var runPostTaskFunctions=function Hop_call_postTask(){ if(postTasks>0){ try { method._postCall[method._postCall.length-postTasks].call(request,output,err,result,function Hop_call_postCall(e,r){ if(typeof e!="undefined") err=e; if(typeof r!="undefined") result=r; postTasks--; runPostTaskFunctions(); },response); } catch (e){ return onComplete(e); } } else { return onComplete(err,result); } } runPostTaskFunctions(); },request,response); } } runPreTaskFunctions(); } else { throw new Error("Invalid API object instance"); } } var Stream = require('stream'); Hop.DataEncoder=util.DataEncoder; Hop.addslashes=function(string) { return string.replace(/\\/g, '\\\\'). replace(/\u0008/g, '\\b'). replace(/\t/g, '\\t'). replace(/\n/g, '\\n'). replace(/\f/g, '\\f'). replace(/\r/g, '\\r'). replace(/'/g, '\\\''). replace(/"/g, '\\"'); } module.exports=Hop;