├── templates ├── emptyMethod.tpl ├── package.tpl ├── newHandler.tpl ├── newSchema.tpl ├── createMethod.tpl ├── deleteMethod.tpl ├── index.tpl ├── updateMethod.tpl └── listMethod.tpl ├── index.js ├── vatican-conf.json ├── test ├── fixtures │ ├── handlerParser │ │ ├── withoutNameParam │ │ │ └── default.js │ │ ├── notFollowAnyCharacter │ │ │ └── default.js │ │ ├── supportedMethods │ │ │ ├── get │ │ │ │ └── default.js │ │ │ ├── post │ │ │ │ └── default.js │ │ │ ├── put │ │ │ │ └── default.js │ │ │ ├── delete │ │ │ │ └── default.js │ │ │ └── unsuported │ │ │ │ └── default.js │ │ ├── withNameParam │ │ │ └── default.js │ │ ├── independentNewLines │ │ │ └── default.js │ │ ├── independentSpacesTabs │ │ │ └── default.js │ │ ├── independentComments │ │ │ └── default.js │ │ ├── es6-no-names │ │ │ └── default.js │ │ ├── es6-basic │ │ │ └── default.js │ │ └── es6-multi-version │ │ │ ├── default.js │ │ │ └── booksV2.js │ └── vatican │ │ └── handlers │ │ └── people.js ├── testModels │ └── Books.js ├── processingChain.js ├── vatican.js └── handlerParser.js ├── lib ├── commands │ ├── index.js │ ├── listCommand.js │ ├── newCommand.js │ └── generateCommand.js ├── logger.js ├── eventsEnum.js ├── views │ ├── generateView.js │ ├── newView.js │ └── listView.js ├── optionsresponse.js ├── commandFactory.js ├── viewHandler.js ├── defaultRequestParser.js ├── processingChain.js ├── vaticanResponse.js ├── handlerParser.js └── vatican.js ├── bin └── vatican ├── package.json └── readme.md /templates/emptyMethod.tpl: -------------------------------------------------------------------------------- 1 | [METHOD_NAME](req, res, next) { 2 | 3 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Vatican = require("./lib/vatican"); 2 | 3 | 4 | module.exports = Vatican; 5 | -------------------------------------------------------------------------------- /vatican-conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 8003, 3 | "handlers": "../vatican-example/lib", 4 | "cors": true 5 | } -------------------------------------------------------------------------------- /templates/package.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "[APP_NAME]", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "vatican": "*" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/handlerParser/withoutNameParam/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: get) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/notFollowAnyCharacter/default.js: -------------------------------------------------------------------------------- 1 | o @endpoint (url: /books method: get) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/supportedMethods/get/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: get) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/supportedMethods/post/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: post) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/supportedMethods/put/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: put) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/supportedMethods/delete/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: delete) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/supportedMethods/unsuported/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: xxx) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/withNameParam/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: get name: name_param) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/independentNewLines/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: get name: name_param) 2 | 3 | 4 | 5 | 6 | default.prototype.list = function(req, res) {} 7 | -------------------------------------------------------------------------------- /lib/commands/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | list: require("./listCommand.js"), 4 | generate: require("./generateCommand.js"), 5 | newProject: require("./newCommand.js") 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/independentSpacesTabs/default.js: -------------------------------------------------------------------------------- 1 | @endpoint (url: /books method: get name: name_param ) 2 | default.prototype.list = function(req, res) {} 3 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | var winston = require("winston"); 2 | 3 | module.exports = new (winston.Logger)({ 4 | transports: [ new (winston.transports.Console)({ colorize: true, timestamp: true, level: 'debug' }) ] 5 | }); 6 | -------------------------------------------------------------------------------- /templates/newHandler.tpl: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class [HANDLER_NAME] { 3 | 4 | constructor(model, dbModels) { 5 | this.model = model; 6 | this.dbModels = dbModels; 7 | } 8 | 9 | [[CONTENT]] 10 | 11 | } -------------------------------------------------------------------------------- /templates/newSchema.tpl: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(mongoose) { 3 | var Schema = mongoose.Schema 4 | 5 | 6 | var SchemaObj = new Schema({ 7 | [[FIELDS]] 8 | }) 9 | 10 | return mongoose.model([[NAME]], SchemaObj) 11 | } -------------------------------------------------------------------------------- /lib/eventsEnum.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "VATICANREADY": "READY", 3 | "INITERROR": "INIT-ERROR", 4 | "DBSTART": "DB-READY", 5 | "INTERNAL_DBSTART": "__DB-READY", 6 | "DBERROR": "DB-ERROR", 7 | "HTTPSERVERREADY": "SERVER-READY" 8 | } 9 | -------------------------------------------------------------------------------- /lib/views/generateView.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(files) { 3 | return { 4 | render: function() { 5 | files.forEach(function(fname) { 6 | console.log(("File written in: " + fname).green); 7 | }) 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /templates/createMethod.tpl: -------------------------------------------------------------------------------- 1 | [METHOD_NAME](req, res, next) { 2 | var data = req.params.body 3 | //...maybe do validation here? 4 | this.model.create(data, function(err, obj) { 5 | if(err) return next(err) 6 | res.send(obj) 7 | }) 8 | } -------------------------------------------------------------------------------- /templates/deleteMethod.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | [METHOD_NAME](req, res, next) { 4 | var id = req.params.query.id || req.params.url.id || req.params.body.id 5 | 6 | this.model.remove({_id: id}, function(err) { 7 | if(err) return next(err) 8 | res.send({success: true}) 9 | }); 10 | } -------------------------------------------------------------------------------- /test/testModels/Books.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(mongoose) { 3 | var Schema = mongoose.Schema 4 | 5 | 6 | var SchemaObj = new Schema({ 7 | title: String, 8 | isbn: String, 9 | author: { type: Schema.Types.ObjectId, ref: 'Author' } 10 | }) 11 | 12 | return mongoose.model('Books', SchemaObj) 13 | } -------------------------------------------------------------------------------- /templates/index.tpl: -------------------------------------------------------------------------------- 1 | var Vatican = require("vatican") 2 | 3 | //Use all default settings 4 | var app = new Vatican() 5 | 6 | app.on('READY', (err, paths) => { 7 | console.log("Vatican is ready..."); 8 | //your code goes here... 9 | }) 10 | 11 | app.start(function() { 12 | console.log("VaticanJS is up and running...") 13 | 14 | } ) 15 | -------------------------------------------------------------------------------- /lib/optionsresponse.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class OptionsResponse { 4 | 5 | 6 | constructor(validMethods) { 7 | this.validMethods = validMethods; 8 | this.name = "OptionsResponse"; 9 | } 10 | 11 | action(req, res, next) { 12 | res.setHeader(["Allow", this.validMethods]); 13 | next(); 14 | } 15 | } 16 | 17 | module.exports = OptionsResponse; -------------------------------------------------------------------------------- /templates/updateMethod.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | [METHOD_NAME](req, res, next) { 4 | var id = req.params.query.id || req.params.url.id || req.params.body.id 5 | var data = req.params.body 6 | 7 | 8 | this.model.update({_id: id}, {$set: data}, function(err, affected) { 9 | if(err) return next(err) 10 | res.send({success: true, affected_documents: affected}) 11 | }) 12 | } -------------------------------------------------------------------------------- /test/fixtures/handlerParser/independentComments/default.js: -------------------------------------------------------------------------------- 1 | //comment 2 | @endpoint (url: /books method: get/* comment */ name: name_param) /*commen 3 | t*/ 4 | //comment 5 | default.prototype.list = function(req, res) {} 6 | 7 | //@endpoint (url: /books method: post name: new_book) 8 | default.prototype.newBook = function(req, res) {} 9 | -------------------------------------------------------------------------------- /lib/commandFactory.js: -------------------------------------------------------------------------------- 1 | var commands = require("./commands"); 2 | 3 | var MAPPINGS = { 4 | list: commands.list, 5 | g: commands.generate, 6 | generate: commands.generate, 7 | "new": commands.newProject 8 | } 9 | 10 | module.exports.getCommand = function(args) { 11 | 12 | var cmd = args[0]; 13 | if(!MAPPINGS[cmd]) { 14 | return new MAPPINGS.default(args); 15 | } else { 16 | return new MAPPINGS[cmd](args.slice(1)); 17 | } 18 | }; -------------------------------------------------------------------------------- /lib/viewHandler.js: -------------------------------------------------------------------------------- 1 | var logger = require("./logger"); 2 | 3 | module.exports.show = function(err, command, data) { 4 | if(err) { 5 | return handleError(err); 6 | } else { 7 | return handleSuccess(command, data); 8 | } 9 | }; 10 | 11 | function handleError(err) { 12 | logger.error("Error during command execution:"); 13 | logger.error(err); 14 | } 15 | 16 | function handleSuccess(cmd, data) { 17 | return cmd.view(data).render(); 18 | } -------------------------------------------------------------------------------- /test/fixtures/handlerParser/es6-no-names/default.js: -------------------------------------------------------------------------------- 1 | 2 | class myHandler { 3 | 4 | constructor() { 5 | 6 | } 7 | 8 | @endpoint(url: /books method: get) 9 | list(req, res) { 10 | 11 | } 12 | 13 | 14 | @endpoint(url: /books method: put) 15 | update(req, res) { 16 | 17 | } 18 | 19 | @endpoint(url: /books method: post) 20 | create(req, res) { 21 | 22 | } 23 | 24 | @endpoint(url: /books method: delete) 25 | remove(req, res) { 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /templates/listMethod.tpl: -------------------------------------------------------------------------------- 1 | 2 | [METHOD_NAME](req, res, next) { 3 | var page = null, 4 | size = null 5 | if(req.params.query.page || req.params.query.size) { 6 | page = req.params.query.page || 0 7 | size = req.params.query.size || 10 8 | } 9 | 10 | var query = {} 11 | 12 | var finder = this.model.find(query) 13 | 14 | if(page !== null && size !== null) { 15 | finder 16 | .skip(page * size) 17 | .limit(size) 18 | } 19 | 20 | finder.exec(function(err, list) { 21 | if(err) return next(err) 22 | res.send(list) 23 | }) 24 | } -------------------------------------------------------------------------------- /lib/views/newView.js: -------------------------------------------------------------------------------- 1 | var colors = require("colors"), 2 | _ = require("lodash"); 3 | 4 | module.exports = function(paths) { 5 | return { 6 | render: function() { 7 | console.log("New project started:".green); 8 | _( paths ).each(function(p) { 9 | console.log(("Creating " + p + " ...").green) 10 | }) 11 | console.log("\n Project files created, now just follow these steps: ") 12 | console.log("1- cd into your new project folder") 13 | console.log("2- npm install") 14 | console.log("3- node index.js") 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/es6-basic/default.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class myHandler { 3 | 4 | constructor() { 5 | 6 | } 7 | 8 | @endpoint(url: /books method: get name: my_list_of_books versions: [1.1.2]) 9 | list(req, res) { 10 | 11 | } 12 | 13 | 14 | @endpoint(url: /books method: put name: update_book versions: [1.0]) 15 | update(req, res) { 16 | 17 | } 18 | 19 | @endpoint(url: /books method: post name: new_book versions: [1.1.2,1.0]) 20 | create(req, res) { 21 | 22 | } 23 | 24 | 25 | @endpoint(url: /books method: delete name: delete_book) 26 | remove(req, res) { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/fixtures/handlerParser/es6-multi-version/default.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class myHandler { 3 | 4 | constructor() { 5 | 6 | } 7 | 8 | @endpoint(url: /books method: get name: my_list_of_books versions: [1.1.2]) 9 | list(req, res) { 10 | 11 | } 12 | 13 | 14 | @endpoint(url: /books method: put name: update_book versions: [1.0]) 15 | update(req, res) { 16 | 17 | } 18 | 19 | @endpoint(url: /books method: post name: new_book versions: [1.1.2,1.0]) 20 | create(req, res) { 21 | 22 | } 23 | 24 | 25 | @endpoint(url: /books method: delete name: delete_book) 26 | remove(req, res) { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/views/listView.js: -------------------------------------------------------------------------------- 1 | var colors = require("colors"), 2 | _ = require("lodash"); 3 | 4 | module.exports = function(paths) { 5 | return { 6 | render: function() { 7 | console.log("List of routes found:".green); 8 | _( paths ).each(function(p) { 9 | console.log(_printMethod(p.method) + (p.url).yellow + " -> " + p.handlerPath + "::" + (p.action).blue); 10 | }) 11 | } 12 | } 13 | } 14 | 15 | function _printMethod(m) { 16 | var colors = { 17 | "GET": "green", 18 | "PUT": "yellow", 19 | "DELETE": "red", 20 | "POST": "grey" 21 | } 22 | var method = m.toUpperCase(); 23 | return "[" + (method)[colors[method]] + "] "; 24 | } -------------------------------------------------------------------------------- /test/fixtures/handlerParser/es6-multi-version/booksV2.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = class BooksV2 { 3 | 4 | constructor() { 5 | 6 | } 7 | 8 | @endpoint(url: /books method: get name: my_list_of_books versions: [2.1.2]) 9 | list(req, res) { 10 | 11 | } 12 | 13 | @endpoint(url: /books method: get name: my_list_of_books_2_1_3 versions: [2.1.3]) 14 | list2(req, res) { 15 | 16 | } 17 | 18 | @endpoint(url: /books method: put name: update_book versions: [2.0]) 19 | update(req, res) { 20 | 21 | } 22 | 23 | @endpoint(url: /books method: post name: new_book versions: [2.1.2,2.0,2.1.3]) 24 | create(req, res) { 25 | 26 | } 27 | 28 | 29 | @endpoint(url: /books method: delete name: delete_book versions: [2.0]) 30 | remove(req, res) { 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/vatican/handlers/people.js: -------------------------------------------------------------------------------- 1 | module.exports = People; 2 | var peeps = []; 3 | function People() {} 4 | @endpoint (url: /people/cool method: get) 5 | People.prototype.getCool = function(req, res) { 6 | res.send(peeps); 7 | } 8 | 9 | @endpoint (url: /people/:id method: put) 10 | People.prototype.update = function(req, res) { 11 | peeps[req.params.url.id] = req.params.body; 12 | res.send(peeps); 13 | } 14 | 15 | @endpoint (url: /people/lame method: get) 16 | People.prototype.getLame = function(req, res) { 17 | res.send("getting lame peeps") 18 | } 19 | 20 | @endpoint (url: /people method: post) 21 | People.prototype.newPeep = function(req, res) { 22 | peeps.push(req.params.body) 23 | res.send("thanks, added!"); 24 | } 25 | 26 | @endpoint (url: /people/:id method: delete) 27 | People.prototype.killPeep = function(req, res) { 28 | delete peeps[req.params.url.id]; 29 | res.send("dead!"); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /lib/commands/listCommand.js: -------------------------------------------------------------------------------- 1 | var handlerParser = require("../handlerParser"), 2 | _ = require("lodash"), 3 | colors = require("colors"); 4 | 5 | function listCommand(args) { 6 | this.view = require("../views/listView.js"); 7 | this.handlersFolder = null; 8 | if( args[0] === "-h") { 9 | this.handlersFolder = args[1]; 10 | } 11 | } 12 | 13 | listCommand.prototype.run = function(cb) { 14 | var dir = this.handlersFolder; 15 | 16 | if(!dir) { 17 | try { 18 | var config = require(process.cwd() + "/vatican-conf.json"); 19 | dir = config.handlers; 20 | } catch(ex) { 21 | cb("Error loading vatican-conf.json file: " + ex); 22 | return; 23 | } 24 | } 25 | 26 | handlerParser.parse(dir, function(err, paths) { 27 | if(err) { 28 | logger.error("Error: " + err); 29 | } else { 30 | if(paths && Array.isArray(paths)) { 31 | cb(null, paths); 32 | } else { 33 | cb("No paths found"); 34 | } 35 | } 36 | }); 37 | } 38 | 39 | module.exports = listCommand; -------------------------------------------------------------------------------- /bin/vatican: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var ViewHandler = require("../lib/viewHandler"), 4 | CommandFactory = require("../lib/commandFactory"); 5 | 6 | var arguments = process.argv.slice(2); 7 | 8 | if(arguments.length == 0) { 9 | console.log("Vatican Command Line Tool helps you deal with the routes of your api easily."); 10 | console.log("Commands: "); 11 | console.log(" list [-h handlers_path]: Lists all the routes from your handlers. The -h modified can be passed to specify the folder where the handlers are stored."); 12 | console.log(" g [handlerName] [options] [actions] : Adds a handler with the routes listed (i.e: vatican g books list get delete post)."); 13 | console.log(" Options: -h[folder relative path] - The path to the handlers folder (optional)"); 14 | return; 15 | } 16 | 17 | 18 | var command = CommandFactory.getCommand(arguments); 19 | command.run(function(err, result) { 20 | ViewHandler.show(err, command, result); 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vatican", 3 | "author": "Fernando Doglio ", 4 | "description": "Micro-framework designed to create REST APIs with minor effort and using annotations", 5 | "keywords": [ 6 | "rest", 7 | "api", 8 | "framework", 9 | "annotations" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/deleteman/vatican" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Badarau Petru", 18 | "email": "smart__petea@mail.rku" 19 | } 20 | ], 21 | "scripts": { 22 | "test": "istanbul cover node_modules/.bin/_mocha -- -u exports -R spec test/*.js" 23 | }, 24 | "bin": { 25 | "vatican": "./bin/vatican" 26 | }, 27 | "version": "1.5.0", 28 | "dependencies": { 29 | "colors": "0.6.2", 30 | "lodash": "2.4.1", 31 | "mongoose": "*", 32 | "sinon": "^5.0.6", 33 | "strip-json-comments": "1.0.1", 34 | "winston": "0.7.3" 35 | }, 36 | "devDependencies": { 37 | "istanbul": "^0.4.5", 38 | "mocha": "1.21.4", 39 | "should": "4.0.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/defaultRequestParser.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"), 2 | logger = require("./logger"); 3 | 4 | module.exports = { 5 | getUrlParams: function(path, template) { 6 | var pathParts = path.split("/"), 7 | urlParams = {}, 8 | tmpltParts = template.split("/"); 9 | 10 | _.each(pathParts, function(p, idx) { 11 | if(tmpltParts[idx][0] === ":") { 12 | urlParams[tmpltParts[idx].slice(1)] = p; 13 | } 14 | }); 15 | return urlParams; 16 | }, 17 | getBodyContent: function(req, cb) { 18 | var self = this; 19 | if( ["POST", "PUT"].indexOf(req.method.toUpperCase()) == -1 ) { 20 | cb({}); 21 | return; 22 | } 23 | var bodyStr = ""; 24 | req.on('data', function(chunk) { 25 | bodyStr += chunk.toString(); 26 | }) 27 | 28 | req.on('end', function() { 29 | if(bodyStr.match(/^\S+=\S+&?/)) { 30 | cb(self.getQueryParams("?" + bodyStr)); 31 | } else { 32 | try { 33 | return cb(JSON.parse(bodyStr)) 34 | } catch (ex) { 35 | return cb(bodyStr); 36 | } 37 | } 38 | }); 39 | }, 40 | getQueryParams: function (url) { 41 | var paramsStr = url.split("?")[1]; 42 | if(!paramsStr) return {}; 43 | var params = paramsStr.split("&"), 44 | paramsObj = {}; 45 | 46 | _(params).each(function(param) { 47 | var parts = param.split("="); 48 | paramsObj[parts[0]] = parts[1]; 49 | }); 50 | return paramsObj; 51 | }, 52 | parse: function(req, template, originalReq, cb) { 53 | var path = req.url.split("?")[0], 54 | self = this, 55 | queryParams = {}; 56 | 57 | this.getBodyContent(originalReq, function(postBody) { 58 | req.params = { 59 | url: self.getUrlParams(path, template), 60 | query: self.getQueryParams(req.url), 61 | body: postBody 62 | }; 63 | cb(req); 64 | }); 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/processingChain.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"), 2 | logger = require("./logger") 3 | 4 | module.exports = ProcessingChain; 5 | 6 | 7 | function ProcessingChain( ) { 8 | this.chain = []; 9 | this.errorHandlers = []; 10 | } 11 | 12 | ProcessingChain.prototype.add = function( proc ) { 13 | if(proc.fn.length == 4) //it's an error handler 14 | this.errorHandlers.push(proc); 15 | else 16 | this.chain.push(proc); 17 | } 18 | 19 | ProcessingChain.prototype.getTotal = function() { 20 | return this.chain.length; 21 | }; 22 | 23 | ProcessingChain.prototype.pop = function() { 24 | this.chain.pop(); 25 | } 26 | 27 | ProcessingChain.prototype.runChain = function( req, res, finalFn, handler ) { 28 | var currentItem = 0; 29 | var totalItems = this.chain.length; 30 | var self = this; 31 | if(totalItems == 0) { 32 | if(typeof finalFn == 'function') finalFn(req, res); 33 | return; 34 | } 35 | 36 | var nextError = function ( err ) { 37 | if ( currentItem < totalItems - 1 ) { 38 | currentItem++ 39 | self.errorHandlers[currentItem].fn(err, req, res, nextError) 40 | } else { 41 | if(typeof finalFn == 'function') finalFn(req, res); 42 | else { 43 | throw new Error("Next called on error handler, but there are no more function s in the middleware chain") 44 | } 45 | } 46 | } 47 | 48 | var next = function(err) { 49 | var chain = self.chain; 50 | if ( err ) { //If there is an error, switch to the error handlers chain 51 | chain = self.errorHandlers; 52 | currentItem = -1; 53 | totalItems = self.errorHandlers.length; 54 | } 55 | if ( currentItem < totalItems - 1 ) { 56 | for(var idx = currentItem + 1; idx < chain.length; idx++) { 57 | if( (chain[idx].names && (handler && chain[idx].names.indexOf(handler.name) != -1)) || !chain[idx].names || _.isEmpty(chain[idx].names)) { 58 | break 59 | } 60 | } 61 | if(idx === chain.length) { 62 | throw new Error("Next called, but there are no more functions in the middleware chain.") 63 | } 64 | currentItem = idx 65 | if(err) { 66 | chain[currentItem].fn(err, req, res, nextError) 67 | } else { 68 | chain[currentItem].fn(req, res, next) 69 | } 70 | } else { 71 | if(typeof finalFn == 'function') finalFn(req, res); 72 | else res.send(''); 73 | } 74 | } 75 | if(handler) { 76 | var firstItem = self.findFirstValidItem(handler.name) 77 | firstItem.fn(req, res, next) 78 | } else { 79 | this.chain[0].fn(req, res, next ) 80 | } 81 | }; 82 | 83 | ProcessingChain.prototype.findFirstValidItem = function(name) { 84 | if(!name) return this.chain[0] 85 | return _.find(this.chain, function(item) { 86 | if(item.names && Array.isArray(item.names) && item.names.length > 0) { 87 | return item.names.indexOf(name) != -1 88 | } else { 89 | return true 90 | } 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /lib/vaticanResponse.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"), 2 | logger = require("./logger"); 3 | 4 | //Custom response 5 | 6 | function VaticanResponse(originalResp, originalReq, options, postProcessChain) { 7 | this.response = originalResp; 8 | this.request = originalReq; 9 | this.statusCode = 200; //default response code, used on the send method 10 | this.options = options; 11 | this.headers = []; 12 | this.ppChain = postProcessChain; 13 | this.body = ""; 14 | } 15 | 16 | VaticanResponse.prototype.send = function(txt) { 17 | var headers = {}; 18 | var self = this; 19 | this.body = txt; 20 | this.ppChain.runChain(this.request, this, function(req, resp) { 21 | //Check for CORS config 22 | if(self.options.cors !== false) { 23 | headers = _getCORSHeaders(self.options.cors); 24 | } 25 | 26 | //Adds the rest of the headers 27 | for(var i in resp.headers) { 28 | headers = _.assign(headers, resp.headers[i]); 29 | } 30 | 31 | //Write the headers 32 | resp.response.writeHead(resp.statusCode, headers); 33 | if( typeof resp.body == 'object') { 34 | resp.body = JSON.stringify(resp.body); 35 | } 36 | 37 | //Write out the response text 38 | resp.response.write(resp.body); 39 | resp.response.end(); 40 | }) 41 | 42 | }; 43 | 44 | VaticanResponse.prototype.write = function(txt) { 45 | this.response.write(txt); 46 | } 47 | 48 | VaticanResponse.prototype.end = function() { 49 | this.response.end(); 50 | } 51 | 52 | VaticanResponse.prototype.setHeader = function(head) { 53 | if(!Array.isArray(this.headers)) { 54 | this.headers = []; 55 | } 56 | var header = {}; 57 | header[head[0]] = head[1]; 58 | this.headers.push(header); 59 | }; 60 | 61 | ///Module level methods 62 | 63 | function _improveResponse(resp, req, options, postprocessChain) { 64 | var res = new VaticanResponse(resp, req, options, postprocessChain); 65 | return res; 66 | } 67 | /* 68 | Writes the 404 response 69 | */ 70 | function _writeNotFound( res ){ 71 | logger.warn("404 Not found"); 72 | res.writeHead(404); 73 | res.end(); 74 | } 75 | 76 | function _getCORSHeaders(corsOpts) { 77 | if ( corsOpts === true ) { 78 | return { 79 | "Access-Allow-Origin": "*", 80 | "Access-Control-Allow-Methods": "*" 81 | } 82 | } else { 83 | return { 84 | "Access-Allow-Origin": corsOpts.access_allow_origin || "", 85 | "Access-Control-Allow-Methods": corsOpts.acess_control_allow_methods || "", 86 | "Access-Control-Expose-Headers": corsOpts.access_control_expose_headers || "", 87 | "Access-Control-Max-Age": corsOpts.access_control_max_age || "" , 88 | "Access-Control-Allow-Credentials": corsOpts.access_control_allow_credentials || "", 89 | "Access-Control-Allow-Headers": corsOpts.access_control_allow_headers || "" 90 | } 91 | } 92 | } 93 | 94 | 95 | module.exports = { 96 | improveResponse: _improveResponse, 97 | writeNotFound: _writeNotFound 98 | }; -------------------------------------------------------------------------------- /lib/commands/newCommand.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"), 2 | fs = require("fs"); 3 | 4 | var DEFAULT_PORT = 8753; 5 | var DEFAULT_HANDLERS_FOLDER = "./handlers"; 6 | 7 | function NewCommand(args) { 8 | this.view = require("../views/newView"); 9 | this.projectName = args[0]; 10 | this.projectFolder = null; 11 | this.params = args.slice(1); 12 | 13 | this.port = DEFAULT_PORT; 14 | this.handlersFolder = DEFAULT_HANDLERS_FOLDER; 15 | 16 | this.parseParams(); 17 | 18 | } 19 | 20 | NewCommand.prototype.parseParams = function() { 21 | var self = this; 22 | _( this.params ).each(function(p, idx) { 23 | switch(p.toLowerCase()) { 24 | case '-p': 25 | self.port = self.params[idx + 1]; 26 | break; 27 | case '-h': 28 | self.handlersFolder = self.params[idx + 1]; 29 | break; 30 | } 31 | }) 32 | } 33 | 34 | NewCommand.prototype.run = function(cb) { 35 | var created = []; 36 | var self = this; 37 | this.createProjectFolder(function(err, path) { 38 | if(!err) { 39 | created.push(path); 40 | self.createHandlersFolder(function(err, hpath) { 41 | if(!err) { 42 | created.push(hpath) 43 | self.createConfigFile(function(err, configPath) { 44 | if(!err) { 45 | created.push(configPath); 46 | self.createIndexAndPackageFile(function(err, paths) { 47 | if(!err) { 48 | created = _.union(created, paths) 49 | cb(null, created); 50 | } else { 51 | cb(err) 52 | } 53 | }) 54 | } else { 55 | cb(err); 56 | } 57 | }) 58 | } else { 59 | cb(err); 60 | } 61 | }) 62 | } else { 63 | cb(err); 64 | } 65 | }) 66 | } 67 | 68 | NewCommand.prototype.createIndexAndPackageFile = function(cb) { 69 | var indexFilePath = __dirname + "/../../templates/index.tpl", 70 | packageFilePath = __dirname + "/../../templates/package.tpl", 71 | self = this 72 | 73 | fs.readFile(indexFilePath, function(err, content) { 74 | if(err) return cb(err) 75 | fs.writeFile(self.projectFolder + "/index.js", content, createPackage) 76 | }) 77 | 78 | 79 | function createPackage(err) { 80 | if(err) return cb(err) 81 | fs.readFile(packageFilePath, function(err, content) { 82 | if(err) return cb(err) 83 | content = content.toString().replace("[APP_NAME]", self.projectName) 84 | fs.writeFile(self.projectFolder + "/package.json", content, function() { 85 | cb(null, [ 86 | self.projectFolder + "/index.js", 87 | self.projectFolder + "/package.json" 88 | ]) 89 | }) 90 | }) 91 | } 92 | } 93 | 94 | NewCommand.prototype.createProjectFolder = function(cb) { 95 | var newFolder = process.cwd() + "/" + this.projectName; 96 | var self = this; 97 | fs.mkdir(newFolder, function(err) { 98 | if(err) { 99 | cb("Error creating project folder '" + newFolder + "': " + err); 100 | } else { 101 | self.projectFolder = newFolder; 102 | cb(null,newFolder); 103 | } 104 | }) 105 | } 106 | 107 | NewCommand.prototype.createHandlersFolder = function(cb) { 108 | var newFolder = this.projectFolder + "/" + this.handlersFolder; 109 | var self = this; 110 | fs.mkdir(newFolder, function(err) { 111 | if(err) { 112 | cb("Error creating handlers folder '" + newFolder + "': " + err); 113 | } else { 114 | cb(null,newFolder); 115 | } 116 | }) 117 | } 118 | 119 | NewCommand.prototype.createConfigFile = function(cb) { 120 | var newFile = this.projectFolder + "/vatican-conf.json"; 121 | var self = this; 122 | 123 | var config = { 124 | port: this.port, 125 | handlers: this.handlersFolder 126 | }; 127 | 128 | fs.writeFile(newFile, JSON.stringify(config), function(err) { 129 | if(err) { 130 | cb("Error creating config file '" + newFile + "': " + err); 131 | } else { 132 | cb(null,newFile); 133 | } 134 | }) 135 | } 136 | 137 | module.exports = NewCommand; -------------------------------------------------------------------------------- /test/processingChain.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); //for mocha tests 2 | var ProcessingChain = require('../lib/processingChain.js') 3 | var _ = require('lodash'); 4 | 5 | describe('Processing Chain methods', function() { 6 | 7 | var processingChain = new ProcessingChain() 8 | 9 | var simpleProc = { 10 | fn: function(req, res, next) { 11 | 12 | }, 13 | names: [] 14 | } 15 | var errorProc = { 16 | fn: function(err, req, res, next) { 17 | 18 | } 19 | } 20 | describe("@add", function() { 21 | it("should add a normal function to the right chain", function(done) { 22 | processingChain.add(simpleProc) 23 | processingChain.getTotal().should.equal(1) 24 | done() 25 | }) 26 | 27 | it("should add an error handler to the error chain", function(done) { 28 | processingChain.add(errorProc) 29 | processingChain.errorHandlers.length.should.equal(1) 30 | done() 31 | }) 32 | 33 | }) 34 | describe("@pop", function() { 35 | it("should correctly remove the last item from the chain", function(done) { 36 | processingChain.add({fn: true}) 37 | processingChain.getTotal().should.equal(2) 38 | processingChain.pop() 39 | processingChain.getTotal().should.equal(1) 40 | processingChain.chain[0].should.not.equal(true) 41 | done() 42 | }) 43 | }) 44 | 45 | describe("@findFirstValidItem", function() { 46 | it("should correctly find the first valid process when there is no endpoint name set", function() { 47 | pc = new ProcessingChain() 48 | pc.add({fn: 1}) 49 | pc.add({fn: 2}) 50 | pc.findFirstValidItem().fn.should.equal(1) 51 | }) 52 | 53 | it("should correctly find the first valid process when there is an endpoint name set", function() { 54 | pc = new ProcessingChain() 55 | pc.add({fn: 1, names: ['first']}) 56 | pc.add({fn: 2, names: ['second']}) 57 | pc.findFirstValidItem('second').fn.should.equal(2) 58 | }) 59 | }) 60 | 61 | describe("@runChain", function() { 62 | it("should run the chain correctly", function(done) { 63 | var result = "" 64 | pc = new ProcessingChain() 65 | pc.add({fn: function(req, res, n) { 66 | result+= "1" 67 | n() 68 | }}) 69 | pc.add({fn: function(req, res, n) { 70 | result+= "2" 71 | n() 72 | }}) 73 | pc.add({fn: function(req, res, n) { 74 | result+= "3" 75 | n() 76 | }}) 77 | pc.runChain({}, {}, function() { 78 | result.should.equal("123") 79 | done() 80 | }, null) 81 | }) 82 | 83 | it("should switch to the error chain if there is a problem", function(done) { 84 | var result = "" 85 | pc = new ProcessingChain() 86 | pc.add({fn: function(req, res, n) { 87 | result+= "1" 88 | n() 89 | }}) 90 | pc.add({fn: function(req, res, n) { 91 | result+= "2" 92 | n('error') 93 | }}) 94 | pc.add({fn: function(req, res, n) { 95 | result+= "3" 96 | n() 97 | }}) 98 | 99 | pc.add({fn: function(err, req, res, n) { 100 | result += err 101 | n() 102 | }}) 103 | pc.add({fn: function(err, req, res, n) { 104 | result += 'e2' 105 | n() 106 | }}) 107 | pc.runChain({}, {}, function() { 108 | result.should.equal("12errore2") 109 | done() 110 | }, null) 111 | }) 112 | 113 | it("should switch to the error chain if there is a problem and only one error handler", function(done) { 114 | var result = "" 115 | pc = new ProcessingChain() 116 | pc.add({fn: function(req, res, n) { 117 | result+= "1" 118 | n() 119 | }, names: []}) 120 | pc.add({fn: function(req, res, n) { 121 | result+= "2" 122 | n('error') 123 | }, names: []}) 124 | pc.add({fn: function(req, res, n) { 125 | result+= "3" 126 | n() 127 | }, names: []}) 128 | 129 | pc.add({fn: function(err, req, res, n) { 130 | result += err 131 | n() 132 | }, names: []}) 133 | 134 | pc.runChain({}, {}, function() { 135 | result.should.equal("12error") 136 | done() 137 | }, null) 138 | }) 139 | 140 | 141 | it("should run correctly if there are named endpoints involved", function(done) { 142 | var result = "" 143 | pc = new ProcessingChain() 144 | pc.add({fn: function(req, res, n) { 145 | result+= "1" 146 | n() 147 | }}) 148 | pc.add({fn: function(req, res, n) { 149 | result+= "2" 150 | n('error') 151 | }, names: ['endpoint1']}) 152 | pc.add({fn: function(req, res, n) { 153 | result+= "3" 154 | n() 155 | }, names: ['endpoint2']}) 156 | 157 | pc.add({fn: function(req, res, n) { 158 | result += "4" 159 | n() 160 | }, names: ["endpoint2", "endpoint1"]}) 161 | 162 | pc.runChain({}, {}, function() { 163 | result.should.equal("134") 164 | done() 165 | }, {name: 'endpoint2'}) 166 | }) 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /lib/handlerParser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require("./logger"), 4 | fs = require("fs"), 5 | stripComments = require("strip-json-comments"), 6 | os = require("os"), 7 | _ = require("lodash"); 8 | 9 | 10 | const CONFIG_FILE_PATH = process.cwd() + "/vatican-conf.json"; 11 | 12 | 13 | const CLASS_HEADER_REGEXP = /class ([a-zA-Z0-9]+ *){/; 14 | 15 | const ES5endpointRE = /@endpoint\s*\((url:.+)\s*(method:(?:[ \t]*)(?:get|put|post|delete))\s*(name:.+)?\s*\)[\s\n]*([^\s]*)\.prototype\.([^\s]*)/gim 16 | 17 | const ES6endpointRE = /(?:@endpoint)?\(?((?:[a-z]+:\s*[a-z_:,\.\[\]\/0-9]+)+)[ ]*\)?/gim 18 | const ES6ActionNameRE = /[\t]*([a-z]+)\([a-z, ]+\)/i 19 | 20 | const ES6classNameRE = /class ([a-zA-Z-0-9_ ]+) *{/; 21 | 22 | 23 | module.exports = class HandlerParser { 24 | 25 | static parse(dir, cb) { 26 | var paths = []; 27 | fs.readdir(dir, function(err, files) { 28 | 29 | if(err) { 30 | logger.error("Error reading folder: " + dir); 31 | cb("Error reading folder: " + dir); 32 | } else { 33 | let fpath = ""; 34 | let matches = null, 35 | handlerName = null, 36 | content = null; 37 | 38 | 39 | _(files).where(function(f) { return f.match(/\.js$/i); }).forEach(function(fname) { 40 | fpath = dir + "/" + fname; 41 | logger.info("Openning file: " + fpath); 42 | content = fs.readFileSync(fpath).toString(); 43 | let matchesParser = new ES5MatchesParser(fpath); 44 | 45 | if(content.match(CLASS_HEADER_REGEXP)) { 46 | logger.info("ES6 class detected, using ES6 params"); 47 | let classNameParts = content.match(ES6classNameRE); 48 | let handlerClassName = classNameParts[1].trim(); 49 | matchesParser = new ES6MatchesParser(fpath, handlerClassName); 50 | } 51 | content = content.replace(/\/\/\s*@/g, "@") //We allow commenting the line of the endpoint for correct editor syntax coloring 52 | content = stripComments(content) //we remove the comments so we don't deal with commented out endpoints 53 | 54 | matchesParser.parse(content, (match) => { 55 | paths.push(matchesParser.getPath(match)); 56 | }) 57 | 58 | }); 59 | cb(null, paths); 60 | } 61 | }); 62 | } 63 | 64 | } 65 | 66 | function parseVersionsMetadata(data) { 67 | return data.replace(/\[/g, "").replace(/\]/g, "").split(","); 68 | } 69 | 70 | class ES6MatchesParser { 71 | 72 | constructor(fpath, className) { 73 | this.fpath = fpath; 74 | this.className = className; 75 | } 76 | 77 | parse(content, matchCB) { 78 | let lines = content.split(os.EOL); 79 | let annotationMatches; 80 | lines.forEach( (line, idx) => { 81 | let metadata = []; 82 | if(line.indexOf("@endpoint") != -1) { 83 | while (annotationMatches = ES6endpointRE.exec(line)){ 84 | metadata.push(annotationMatches[1]) 85 | } 86 | if(metadata.length > 0) { 87 | let nextLine = lines[idx + 1] 88 | let actionMetadata = nextLine.match(ES6ActionNameRE)//.exec(nextLine) 89 | if(actionMetadata){ 90 | metadata.push('action: ' + actionMetadata[1]) 91 | } 92 | matchCB(metadata); 93 | } 94 | } 95 | }) 96 | 97 | } 98 | 99 | getPath(matches) { 100 | let currentPath = { 101 | versions: [] 102 | }; 103 | let actionStr = ""; 104 | matches.forEach( p => { 105 | var parts = p.split(":"), 106 | key = parts.shift(), 107 | value = parts.join(":").trim(); 108 | if(key == "versions") { 109 | value = parseVersionsMetadata(value); 110 | } 111 | if(key == "action") { 112 | actionStr = value.trim(); 113 | } 114 | if(value) currentPath[key] = value; 115 | }) 116 | 117 | //currentPath['action'] = actionStr.trim(); 118 | currentPath['handlerPath'] = this.fpath; 119 | currentPath['handlerName'] = this.className; 120 | currentPath.method = currentPath.method.toUpperCase() 121 | return currentPath; 122 | } 123 | } 124 | 125 | class ES5MatchesParser { 126 | constructor(fpath) { 127 | this.fpath = fpath; 128 | } 129 | 130 | parse(content, matchCB) { 131 | let matches = null; 132 | while( (matches = ES5endpointRE.exec(content)) !== null) { 133 | matchCB(matches); 134 | } 135 | } 136 | 137 | getPath(matches) { 138 | let params = _.compact(matches.slice(1,4)) 139 | let currentPath = {}; 140 | params.forEach(function(p) { 141 | let parts = p.split(":"), 142 | key = parts.shift(), 143 | value = parts.join(":").trim(); 144 | if(value) currentPath[key] = value; 145 | }) 146 | let actionStr = matches[5], 147 | handlerName = matches[4] 148 | 149 | currentPath['action'] = actionStr.trim(); 150 | currentPath['handlerPath'] = this.fpath; 151 | currentPath['handlerName'] = handlerName 152 | currentPath.method = currentPath.method.toUpperCase() 153 | return currentPath; 154 | } 155 | } 156 | 157 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Vatican 2 | 3 | [![NPM](https://nodei.co/npm/vatican.png?downloads=true&stars=true)](https://nodei.co/npm/vatican/) 4 | 5 | Vatican attemps to be a micro-framework for creating APIs as quickly as possible. 6 | One of the key features of Vatican is the use of annotations on methods to define the endpoints of the API. 7 | 8 | For a full code example of an app using Vatican, check out this repo: https://github.com/deleteman/vatican-example 9 | 10 | 11 | ## Installing Vatican 12 | 13 | ```bash 14 | $ npm install vatican 15 | ``` 16 | ## Running the tests 17 | 18 | ```bash 19 | $ npm test 20 | ``` 21 | # More info 22 | 23 | + Check out VaticanJS official documentation: https://www.fernandodoglio.com/vaticanjs-docs 24 | 25 | # Changelog 26 | 27 | ## v 1.5.0 28 | + Added support for OPTIONS methods 29 | + Added endpoint versioning support 30 | + Added on-ready event, to avoid code referencing handler that haven't been parsed yet. 31 | + Added on-ready event for the database, this way, working with DB events and Vatican's events works the same way 32 | + Removed the need to start working with dbStart 33 | + Added SERVER-READY event, optional, if you want to be notified when the HTTP server is up and running 34 | 35 | 36 | ## v 1.4.0 37 | + Added support for ES6 classes as resource handlers 38 | + Changed generator code to create ES6 compatible classes for resource handlers 39 | + Cleaned-up some code 40 | 41 | ## v 1.3.2 42 | + Added _close_ method on main server. 43 | + Fixed bug #30 44 | 45 | ## v 1.3.1 46 | 47 | + Added new _name_ attribute to @endpoint annotation 48 | + Added ability to set pre-processors on specific endpoints by name 49 | + Added model generator working with MongoDB 50 | + Auto generate handlers method's code based on their name 51 | + New generate syntax, allowing to specify attributes, types and http methods 52 | + Added index.js and package.json generator 53 | + Added tests to main components (Still needs more work) 54 | + Added removal of comments on handlers files, so now if you comment out an endpoint, it won't be parsed. 55 | + Improved handler parser regex 56 | + Improved general processing of handler file code. 57 | + Changed request parser to auto-parse content of PUT and POST requests into JSON (when possible) 58 | 59 | ## v 1.2.4 60 | 61 | + Fixed bug causing incorrect parsing of post/put body content 62 | 63 | ## v 1.2.3 64 | 65 | + Fixed bug preventing the handlers from loading installed modules using 'require' 66 | 67 | 68 | ## v 1.2.2 69 | 70 | + Fixed bug causing problems with the pre-processing chain and the handler methods. 71 | 72 | ## v 1.2.1 73 | 74 | + Fixed bug causing vatican to match incorrecly urls with similar templates 75 | + Changed preprocessing chain, so that now handler methods recieve the _next_ function and can trigger the generic error handler functions 76 | 77 | ## v 1.2.0 78 | 79 | + Fixed support for PUT requests 80 | + Added configuration overwrite on Vatican constructor and cli commands 81 | + Added callback function to _start_ method of _Vatican_ called after successful start of http server. 82 | + Updated cli help 83 | + Handlers are now stored in-memory after the first time they're loaded, so they're not loaded on every request. 84 | 85 | ## v 1.1.0 86 | 87 | + Added pre-processor to request 88 | + Added post-processor to response 89 | + Fixed bug causing incorrect request parsing on non-post requests. 90 | 91 | ## v 1.0.1 92 | 93 | + Changed default handler folder for create command 94 | + Minor readme fixes 95 | 96 | ## v 1.0.0 97 | 98 | + Added create new project command 99 | + Minor fixes on readme 100 | 101 | ## v 0.1.1 102 | 103 | + Added auto-stringify of objects passed to the send method on the response object. 104 | + Edited readme 105 | 106 | ## v 0.0.1 107 | 108 | + First version 109 | 110 | 111 | # Contributing 112 | 113 | If you feel like helping out by bug-fixing, or contributing with a new feature or whatever, just follow these simple steps: 114 | 115 | 1. Create an issue for it. 116 | 2. Fork the repo. 117 | 3. Make your changes. 118 | 4. Commit and create a pull request. 119 | 5. Be happy :) 120 | 121 | # Contact me 122 | 123 | If you have questions, or just want to send your love/hate, drop me an e-mail at: deleteman@gmail.com 124 | Or visit my official site at www.fernandodoglio.com 125 | 126 | # License 127 | 128 | The MIT License (MIT) 129 | 130 | Copyright (c) 2013 Fernando Doglio 131 | 132 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 135 | 136 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 137 | 138 | -------------------------------------------------------------------------------- /lib/commands/generateCommand.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"), 2 | path = require("path"), 3 | logger = require("../logger"), 4 | fs = require("fs"); 5 | 6 | var DEFAULT_SCHEMAS_FOLDER = process.cwd() + "/schemas" 7 | var DEFAULT_CRUD_ACTIONS = [ 8 | 'create:post', 9 | 'update:put', 10 | 'list:get', 11 | 'remove:delete' 12 | ]; 13 | 14 | const TEMPLATES_FOLDER = __dirname + "/../../templates/"; 15 | const NEW_HANDLER_TPL = TEMPLATES_FOLDER + "newHandler.tpl" 16 | 17 | function GenerateCommand(args) { 18 | var self = this; 19 | this.view = require("../views/generateView"); 20 | this.handlerName = args.shift(); 21 | this.handlersFolder = null; 22 | this.actions = []; 23 | this.fields = [] 24 | 25 | this.schemaName = _titlelize(this.handlerName) 26 | 27 | this.filesCreated = [] 28 | 29 | var option = "" 30 | for(var i = 0; i < args.length; i++) { 31 | if(args[i].indexOf("-") === 0){ 32 | option = args[i].toLowerCase().replace("-", "") 33 | } else { 34 | switch(option) { 35 | case 'a': //gather fields 36 | this.fields.push(args[i]) 37 | break; 38 | case 'h': //get the handlers folder 39 | this.handlersFolder = args[i]; 40 | break; 41 | case 'm': //gather method information 42 | this.actions.push(args[i]); 43 | break; 44 | } 45 | } 46 | } 47 | } 48 | 49 | GenerateCommand.prototype.loadConfig = function(cb) { 50 | try { 51 | var config = require(process.cwd() + "/vatican-conf.json"); 52 | cb(null, config) 53 | } catch(ex) { 54 | cb("Error loading vatican-conf.json file: " + ex); 55 | return; 56 | } 57 | } 58 | 59 | GenerateCommand.prototype.run = function(cb) { 60 | var self = this; 61 | var handlersFolder = this.handlersFolder; 62 | var config = null 63 | this.loadConfig(function(err, config) { 64 | if(err) return cb(err) 65 | config = config 66 | if( !handlersFolder ) { 67 | self.handlersFolder = config.handlers; 68 | } 69 | if(config.schemasFolder) 70 | self.schemasFolder = process.cwd() + config.schemasFolder 71 | else 72 | self.schemasFolder = DEFAULT_SCHEMAS_FOLDER 73 | 74 | self.generateSchema() 75 | .generateHandler(cb) 76 | 77 | 78 | }) 79 | } 80 | 81 | GenerateCommand.prototype.generateSchema = function() { 82 | var folder = this.schemasFolder 83 | if(!fs.existsSync(folder)) { 84 | fs.mkdirSync(folder) 85 | } 86 | var self = this 87 | var tplContent = fs.readFileSync(__dirname + "/../../templates/newSchema.tpl").toString() 88 | tplContent = tplContent.replace("[[NAME]]", "'" + this.schemaName + "'") 89 | var attrs = _.map(this.fields , function(attr) { 90 | var parts = attr.split(":") 91 | return parts[0] + ': ' + self.parseType(parts[1]) 92 | }) 93 | tplContent = tplContent.replace("[[FIELDS]]", attrs.join(",\n\r\t")) 94 | var filepath = folder + "/" + this.schemaName + ".js" 95 | this.filesCreated.push(filepath) 96 | fs.writeFileSync(filepath, tplContent) 97 | return this 98 | } 99 | 100 | GenerateCommand.prototype.parseType = function(type) { 101 | if(!type) return 'String' 102 | var commonTypes = { 103 | text: 'String', 104 | string: 'String', 105 | bool: 'Boolean', 106 | 'boolean': 'Boolean', 107 | string: 'String', 108 | number: 'Number', 109 | numeric: 'Number', 110 | integer: 'Number', 111 | 'double': 'Number' 112 | } 113 | type = type.toLowerCase() 114 | if(commonTypes[type]) { 115 | return commonTypes[type] 116 | } else { 117 | var matches = type.match(/\[([a-zA-Z]+)\]/) 118 | if(matches) { 119 | type = this.parseType(matches[1]) 120 | return "[" + type + "]" 121 | } else { 122 | return "{ type: Schema.Types.ObjectId, ref: '" + _titlelize(type) + "' }" 123 | } 124 | } 125 | } 126 | 127 | GenerateCommand.prototype.generateHandler = function(cb) { 128 | 129 | var relativeRequirePath = path.relative(this.handlersFolder, this.schemasFolder) 130 | 131 | var handlerFuncName = _titlelize(this.handlerName) + "Hdlr"; 132 | 133 | let content = null; 134 | var self = this 135 | 136 | fs.readFile(NEW_HANDLER_TPL, (err, content) => { 137 | if(err) return cb(err); 138 | 139 | content = ("" +content).replace("[HANDLER_NAME]", handlerFuncName) 140 | 141 | if(this.actions.length === 0) { 142 | this.actions = DEFAULT_CRUD_ACTIONS 143 | } 144 | 145 | let actionMethodsCode = this.actions.map((act) => { 146 | let parts = act.split(":") 147 | let method = parts[1]; 148 | if(method) { 149 | method = method.split("[")[0] 150 | } 151 | let actionName = parts[0].split("[")[0] 152 | if(act.indexOf("[") != -1) { //if there are versions specified for the method, parse them... 153 | let versionsRegExp = /.+\[([0-9\.,]+)\]/g; 154 | let versions = versionsRegExp.exec(act)[1] 155 | if(versions) { 156 | method += " versions: [" + versions.split(",").join(",") + "]"; 157 | } 158 | } 159 | let code = "@endpoint (url: /" + _toUrlFormat(self.handlerName) + " method: " + method + ")\n"; 160 | code += self.getMethodCode(actionName); 161 | return code; 162 | }) 163 | 164 | content = content.replace("[[CONTENT]]" , actionMethodsCode.join("\n")); 165 | 166 | fs.writeFile(self.handlersFolder + "/" + self.handlerName + ".js", content, function(err) { 167 | if(err) { 168 | var msg = "Error saving handler file: " + err; 169 | cb(msg); 170 | } 171 | }); 172 | 173 | var filepath = self.handlersFolder + "/" + self.handlerName + ".js" 174 | this.filesCreated.push(filepath) 175 | 176 | cb(null, this.filesCreated); 177 | }) 178 | } 179 | 180 | GenerateCommand.prototype.getMethodCode = function(methodName) { 181 | var action = null 182 | 183 | if(methodName.match(/(new|save|create)/i)) { 184 | action = 'create' 185 | } 186 | if(methodName.match(/(update)/i)) { 187 | action = 'update' 188 | } 189 | 190 | if(methodName.match(/(delete|remove|erase|kill)/i)) { 191 | action = 'delete' 192 | } 193 | 194 | if(methodName.match(/(list|search|find|index)/i)) { 195 | action = 'list' 196 | } 197 | 198 | if(action === null) { 199 | logger.info("Don't know how to auto-generate code for method: " + methodName) 200 | action = 'empty'; 201 | } 202 | 203 | var tplContent = fs.readFileSync(TEMPLATES_FOLDER + action + "Method.tpl").toString() 204 | tplContent = tplContent.replace("[[SCHEMA]]", this.schemaName); 205 | tplContent = tplContent.replace("[METHOD_NAME]", methodName); 206 | return tplContent 207 | } 208 | 209 | function _titlelize (txt) { 210 | var parts = txt.split(" "); 211 | return _( parts ).map(function(t) { 212 | return t.charAt(0).toUpperCase() + t.slice(1); 213 | }).join(""); 214 | } 215 | 216 | function _toUrlFormat (txt) { 217 | var parts = txt.split(" "); 218 | return _( parts ).map(function(t) { 219 | return t.toLowerCase(); 220 | }).join("_"); 221 | } 222 | 223 | module.exports = GenerateCommand; -------------------------------------------------------------------------------- /test/vatican.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); //for mocha tests 2 | const Vatican = require("../lib/vatican") 3 | const _ = require('lodash'); 4 | const OptionsResponse = require("../lib/optionsresponse") 5 | const sinon = require("sinon"); 6 | 7 | 8 | describe("Vatican methods", function() { 9 | 10 | var vatican = new Vatican({ 11 | handlers: __dirname + '/fixtures/vatican/handlers', 12 | port: 88 13 | }) 14 | var matchFound = null 15 | 16 | 17 | describe("@checkOptions", function(){ 18 | it("should throw an error if no port is specified", function() { 19 | (function() { 20 | let v = new Vatican({handlers: ''}) 21 | }).should.throw("Port not specified") 22 | }) 23 | it("should throw an error if no handlers folder is specified", function() { 24 | (function() { 25 | let v = new Vatican({port: 123}) 26 | }).should.throw("Handlers folder not specified") 27 | }) 28 | it("should throw an error if you don't define a matching function when you define a custom version format",() => { 29 | (() => { 30 | let v = new Vatican({ 31 | handlers: '/no/matching/function', 32 | port: 88, 33 | versioning: { 34 | format: () => { } 35 | } 36 | }) 37 | }).should.throw("Versioning error: matching function is needed when format function is provided") 38 | }); 39 | }) 40 | 41 | describe("@preprocess", function() { 42 | it("should add a processor to the pre-processors chain", function() { 43 | vatican.preprocess(function(req, res, next) { 44 | 45 | }) 46 | 47 | vatican.preprocessors.chain.length.should.equal(1) 48 | }) 49 | }) 50 | 51 | describe("@postprocess", function() { 52 | it("should add a processor to the post-processors chain", function() { 53 | vatican.postprocess(function(req, res, next) { 54 | 55 | }) 56 | 57 | vatican.postprocessors.chain.length.should.equal(1) 58 | }) 59 | }) 60 | 61 | describe("@parseHandlers", function() { 62 | it("should return a structure with the handlers data", function(done) { 63 | vatican.parseHandlers(function(err, hdlrs) { 64 | hdlrs.length.should.equal(5) 65 | done() 66 | }) 67 | }) 68 | }) 69 | 70 | describe("@findMethod", function() { 71 | it("should find the right method from the paths", function(done) { 72 | matchFound = vatican.findMethod('/people/123', 'DELETE') 73 | matchFound.action.should.equal("killPeep") 74 | matchFound.handlerName.should.equal('People') 75 | done() 76 | }) 77 | 78 | it("should return an OptionsResponse with the list of acceptable methods for a URL when method = OPTIONS", () => { 79 | let ret = vatican.findMethod("/people", 'OPTIONS') 80 | should(ret).be.instanceOf(OptionsResponse); 81 | }) 82 | 83 | it("should return an OptionsResponse with the list of acceptable methods for a URL when method = OPTIONS taking into account the version provided", (done) => { 84 | let localVatican = new Vatican({ 85 | handlers: __dirname + '/fixtures/handlerParser/es6-multi-version', 86 | port: 88, 87 | versioning: { 88 | strategy: "url" 89 | } 90 | }) 91 | localVatican.on('READY', () => { 92 | let res = localVatican.findMethod("/2.1.3/books", "OPTIONS") 93 | should(res).be.instanceOf(OptionsResponse); 94 | res.validMethods.split(",").sort().should.eql(['GET', 'POST'].sort()); 95 | done(); 96 | }) 97 | }) 98 | it("should use the full version defined on the annotation if no matching function is defined", (done) => { 99 | var localVatican = new Vatican({ 100 | handlers: __dirname + '/fixtures/handlerParser/es6-multi-version', 101 | port: 88, 102 | versioning: { 103 | strategy: "url" 104 | } 105 | }) 106 | localVatican.on('READY', () => { 107 | let method = localVatican.findMethod("/2.1.3/books", "GET") 108 | method.handlerName.should.equal("BooksV2") 109 | method.name.should.equal("my_list_of_books_2_1_3") 110 | done(); 111 | }) 112 | }) 113 | 114 | it("should find the right endpoint based on the version provided in the URL", (done) => { 115 | var localVatican = new Vatican({ 116 | handlers: __dirname + '/fixtures/handlerParser/es6-multi-version', 117 | port: 88, 118 | versioning: { 119 | strategy: "url", 120 | matching: (urlVersion, endpointVersion) => { 121 | let v = urlVersion.substring(1); 122 | return +v == +endpointVersion.split(".")[0]; 123 | } 124 | } 125 | }) 126 | localVatican.on('READY', () => { 127 | let method = localVatican.findMethod("/v2/books", "GET") 128 | method.handlerName.should.equal("BooksV2") 129 | done(); 130 | }) 131 | 132 | }) 133 | 134 | 135 | it("should find the right endpoint based on the version provided in the ACCEPT header", (done) => { 136 | var localVatican = new Vatican({ 137 | handlers: __dirname + '/fixtures/handlerParser/es6-multi-version', 138 | port: 88, 139 | versioning: { 140 | strategy: "header", 141 | matching: (urlVersion, endpointVersion) => { 142 | let v = urlVersion.substring(1); 143 | return +v == +endpointVersion.split(".")[0]; 144 | } 145 | } 146 | }) 147 | localVatican.on('READY', () => { 148 | let method = localVatican.findMethod("/books", "GET", { 149 | "accept": "accept/vnd.vatican-version.v2+json" 150 | }) 151 | method.handlerName.should.equal("BooksV2") 152 | done(); 153 | }) 154 | }) 155 | 156 | }) 157 | 158 | describe("@getCorrectModel", function() { 159 | it("should return FALSE if there is no db connection to load models", function(done) { 160 | vatican.getCorrectModel().should.equal(false) 161 | done() 162 | }) 163 | 164 | it("should return the correct model if there is one", function(done) { 165 | var fakeVat = vatican 166 | fakeVat.__dbStart = function() { 167 | let self = this; 168 | return { 169 | on: function() {}, 170 | error: function () {}, 171 | once: function(str, cb) { 172 | console.log("once called with: ", str) 173 | self.eventEmitter.emit('DB-READY') 174 | cb() 175 | } 176 | } 177 | } 178 | 179 | fakeVat.dbStart({schemasFolder: __dirname + "/testModels"}, function (err) { 180 | _.keys(fakeVat.dbmodels).length.should.equal(1); 181 | done() 182 | }) 183 | }) 184 | }) 185 | 186 | describe("@loadHandler", function() { 187 | it("should load and return the handler data", function(done) { 188 | var handler = vatican.loadHandler(matchFound.handlerPath) 189 | _.keys(vatican.handlers).length.should.equal(1) 190 | var type = typeof handler == 'function' 191 | type.should.be.ok 192 | done() 193 | }) 194 | }) 195 | 196 | describe("@close", function() { 197 | it('should close server', function() { 198 | var app = new Vatican({ 199 | handlers: __dirname + '/fixtures/vatican/handlers', 200 | port: 8888 201 | }) 202 | 203 | sinon.stub(app, 'dbStart'); 204 | app.start(); 205 | 206 | ( app.server._handle != null ).should.be.true; 207 | 208 | app.close(); 209 | 210 | ( app.server._handle == null ).should.be.true; 211 | app.dbStart.restore(); 212 | }); 213 | 214 | it('should call callback on close', function( done ) { 215 | var app = new Vatican({ 216 | handlers: __dirname + '/fixtures/vatican/handlers', 217 | port: 8888 218 | }) 219 | 220 | sinon.stub(app, 'dbStart'); 221 | app.start(); 222 | app.close(function() { 223 | app.dbStart.restore(); 224 | done(); 225 | }); 226 | }); 227 | }); 228 | }) 229 | -------------------------------------------------------------------------------- /test/handlerParser.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); //for mocha tests 2 | var parse = require('../lib/handlerParser.js').parse; 3 | var fs = require('fs'); 4 | var _ = require('lodash'); 5 | 6 | var dir = __dirname + '/fixtures/handlerParser'; 7 | let supportedMethodsDir = dir + '/supportedMethods'; 8 | 9 | describe("handlerParser.parse method", function() { 10 | describe("ES6 supported methods", () => { 11 | it("should support all basic methods method", (done) => { 12 | let dirname = dir + "/es6-basic"; 13 | parse(dirname, (err, paths) => { 14 | if(err) return done(err); 15 | paths[0].method.should.be.equal('GET'); 16 | paths[1].method.should.be.equal('PUT'); 17 | paths[2].method.should.be.equal('POST'); 18 | paths[3].method.should.be.equal('DELETE'); 19 | done(); 20 | }) 21 | }) 22 | it("should correctly parse version information on all endpoints", (done) => { 23 | let dirname = dir + "/es6-basic"; 24 | parse(dirname, (err, paths) => { 25 | if(err) return done(err); 26 | paths[0].versions.sort().should.be.eql(["1.1.2"].sort()); 27 | paths[1].versions.sort().should.be.eql(["1.0"].sort()); 28 | paths[2].versions.sort().should.be.eql(["1.1.2", "1.0"].sort()); 29 | paths[3].versions.length.should.be.equal(0); 30 | done(); 31 | }) 32 | }) 33 | it("should support all basic methods methods without names", (done) => { 34 | let dirname = dir + "/es6-no-names"; 35 | parse(dirname, (err, paths) => { 36 | if(err) return done(err); 37 | paths[0].method.should.be.equal('GET'); 38 | paths[1].method.should.be.equal('PUT'); 39 | paths[2].method.should.be.equal('POST'); 40 | paths[3].method.should.be.equal('DELETE'); 41 | done(); 42 | }) 43 | }) 44 | }) 45 | 46 | describe('ES5 supported methods', function() { 47 | ['get', 'put', 'post', 'delete'] 48 | .forEach(function(method) { 49 | it(method, function(done) { 50 | var dirname = supportedMethodsDir + '/' + method; 51 | parse(dirname, function(err, paths) { 52 | if (err) return done(err); 53 | paths[0].method.should.be.equal(method.toUpperCase()); 54 | done(); 55 | }); 56 | 57 | }); 58 | }); 59 | 60 | it('unsuported', function(done) { 61 | var dirname = supportedMethodsDir + '/unsuported'; 62 | parse(dirname, function(err, paths) { 63 | if (err) return done(err); 64 | 65 | paths.length.should.be.equal(0); 66 | done(); 67 | }); 68 | 69 | }); 70 | }); 71 | 72 | describe('', function() { 73 | //Originally this was meant to test that no endpoint could be parsed, but since it works I think we can leave it at that 74 | it("the @endpoint does not follow any character", function(done) { 75 | var dirname = dir + '/notFollowAnyCharacter'; 76 | parse(dirname, function(err, paths) { 77 | if (err) return done(err); 78 | 79 | paths.length.should.be.equal(1); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | 85 | it("independent of number of spaces and tabs between the elements", function(done) { 86 | var dirname = dir + '/independentSpacesTabs'; 87 | 88 | parse(dirname, function(err, paths) { 89 | if (err) return done(err); 90 | 91 | paths.length.should.not.equal(0); 92 | 93 | var compareWith = { 94 | url: '/books', 95 | method: 'GET', 96 | action: 'list', 97 | handlerPath: dirname + '/default.js', 98 | handlerName: 'default', 99 | name: 'name_param', 100 | }; 101 | 102 | paths[0].url.should.be.equal(compareWith.url); 103 | paths[0].method.should.be.equal(compareWith.method); 104 | paths[0].action.should.be.equal(compareWith.action); 105 | paths[0].handlerPath.should.be.equal(compareWith.handlerPath); 106 | paths[0].handlerName.should.be.equal(compareWith.handlerName); 107 | paths[0].name.should.be.equal(compareWith.name); 108 | 109 | _.isEqual(paths[0], compareWith).should.be.true; 110 | done(); 111 | }); 112 | }); 113 | 114 | 115 | it("independent of comments", function(done) { 116 | var dirname = dir + '/independentComments'; 117 | 118 | parse(dirname, function(err, paths) { 119 | if (err) return done(err); 120 | 121 | paths.length.should.equal(2); 122 | 123 | var compareWith = [{ 124 | url: '/books', 125 | method: 'GET', 126 | action: 'list', 127 | handlerPath: dirname + '/default.js', 128 | handlerName: 'default', 129 | name: 'name_param', 130 | }, 131 | { url: '/books', 132 | method: 'POST', 133 | action: 'newBook', 134 | handlerPath: dirname + '/default.js', 135 | handlerName: 'default', 136 | name: 'new_book' 137 | } 138 | ] 139 | 140 | paths[0].url.should.be.equal(compareWith[0].url); 141 | paths[0].method.should.be.equal(compareWith[0].method); 142 | paths[0].action.should.be.equal(compareWith[0].action); 143 | paths[0].handlerPath.should.be.equal(compareWith[0].handlerPath); 144 | paths[0].handlerName.should.be.equal(compareWith[0].handlerName); 145 | paths[0].name.should.be.equal(compareWith[0].name); 146 | 147 | paths[1].url.should.be.equal(compareWith[1].url); 148 | paths[1].method.should.be.equal(compareWith[1].method); 149 | paths[1].action.should.be.equal(compareWith[1].action); 150 | paths[1].handlerPath.should.be.equal(compareWith[1].handlerPath); 151 | paths[1].handlerName.should.be.equal(compareWith[1].handlerName); 152 | paths[1].name.should.be.equal(compareWith[1].name); 153 | 154 | _.isEqual(paths[0], compareWith[0]).should.be.true; 155 | _.isEqual(paths[1], compareWith[1]).should.be.true; 156 | done(); 157 | }); 158 | }); 159 | 160 | it("independent of number of new lines between @endpoint and function declaration", function(done) { 161 | var dirname = dir + '/independentSpacesTabs'; 162 | 163 | parse(dirname, function(err, paths) { 164 | if (err) return done(err); 165 | 166 | paths.length.should.not.equal(0); 167 | 168 | var compareWith = { 169 | url: '/books', 170 | method: 'GET', 171 | action: 'list', 172 | handlerPath: dirname + '/default.js', 173 | handlerName: 'default', 174 | name: 'name_param', 175 | }; 176 | 177 | paths[0].url.should.be.equal(compareWith.url); 178 | paths[0].method.should.be.equal(compareWith.method); 179 | paths[0].action.should.be.equal(compareWith.action); 180 | paths[0].handlerPath.should.be.equal(compareWith.handlerPath); 181 | paths[0].handlerName.should.be.equal(compareWith.handlerName); 182 | paths[0].name.should.be.equal(compareWith.name); 183 | 184 | _.isEqual(paths[0], compareWith).should.be.true; 185 | done(); 186 | }); 187 | }); 188 | 189 | describe("generated by cli", function() { 190 | it("without name param" ,function(done) { 191 | 192 | var dirname = dir + '/withoutNameParam'; 193 | parse(dirname, function(err, paths) { 194 | if (err) return done(err); 195 | 196 | var compareWith = { 197 | url: '/books', 198 | method: 'GET', 199 | action: 'list', 200 | handlerPath: dirname + '/default.js', 201 | handlerName: 'default', 202 | }; 203 | 204 | paths[0].url.should.be.equal(compareWith.url); 205 | paths[0].method.should.be.equal(compareWith.method); 206 | paths[0].action.should.be.equal(compareWith.action); 207 | paths[0].handlerPath.should.be.equal(compareWith.handlerPath); 208 | paths[0].handlerName.should.be.equal(compareWith.handlerName); 209 | 210 | _.isEqual(paths[0], compareWith).should.be.true; 211 | done(); 212 | }); 213 | }); 214 | 215 | it("with name param" ,function(done) { 216 | 217 | var dirname = dir + '/withNameParam'; 218 | parse(dirname, function(err, paths) { 219 | if (err) return done(err); 220 | 221 | var compareWith = { 222 | url: '/books', 223 | method: 'GET', 224 | action: 'list', 225 | name: 'name_param', 226 | handlerPath: dirname + '/default.js', 227 | handlerName: 'default', 228 | }; 229 | 230 | paths[0].url.should.be.equal(compareWith.url); 231 | paths[0].method.should.be.equal(compareWith.method); 232 | paths[0].action.should.be.equal(compareWith.action); 233 | paths[0].handlerPath.should.be.equal(compareWith.handlerPath); 234 | paths[0].handlerName.should.be.equal(compareWith.handlerName); 235 | paths[0].name.should.be.equal(compareWith.name); 236 | 237 | _.isEqual(paths[0], compareWith).should.be.true; 238 | done(); 239 | }); 240 | }); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /lib/vatican.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const http = require("http"), 3 | EventEmitter = require("events"), 4 | logger = require("./logger"), 5 | util = require("util"), 6 | fs = require("fs"), 7 | vaticanResp = require("./vaticanResponse"), 8 | handlerParser = require("./handlerParser"), 9 | processingChain = require("./processingChain"), 10 | mongoose = require("mongoose"), 11 | EventsEnum = require("./eventsEnum"), 12 | OptionsResponse = require("./optionsresponse"), 13 | _ = require("lodash"); 14 | 15 | 16 | module.exports = Vatican; 17 | var CONFIG_FILE_PATH = process.cwd() + "/vatican-conf.json"; 18 | const HEADER_VERSION_REGEXP = /accept\/vnd.vatican-version\.(.+)\+json/gi 19 | 20 | const DEFAULT_SCHEMAS_FOLDER = process.cwd() + '/schemas'; 21 | 22 | var DEFAULT_CONFIG = { 23 | cors: false, 24 | schemasFolder: DEFAULT_SCHEMAS_FOLDER, 25 | db: { 26 | host: 'localhost', 27 | dbname: 'vatican-project' 28 | } 29 | } 30 | 31 | function Vatican(options) { 32 | let config = null; 33 | this.dbconn = null 34 | try { 35 | config = (options) ? options : require(CONFIG_FILE_PATH); 36 | } catch(ex) { 37 | logger.error("Error loading config file '" + CONFIG_FILE_PATH + "': " + ex); 38 | return; 39 | } 40 | 41 | this.eventEmitter = new EventEmitter(); 42 | this.options = _.defaults( config, DEFAULT_CONFIG); 43 | this.checkOptions(); 44 | this.requestParser = config.requestParser ? require(config.requestParser) : require("./defaultRequestParser"); 45 | 46 | this.handlers = {}; 47 | 48 | this.parseHandlers((err, paths) => { 49 | if(err) return this.eventEmitter.emit(EventsEnum.INITERROR, err); 50 | this.eventEmitter.emit(EventsEnum.VATICANREADY, paths); 51 | }); 52 | this.on(EventsEnum.INTERNAL_DBSTART, this.onDbStart.bind(this)); 53 | this.paths = []; 54 | this.server = null; 55 | this.totalPreprocessors = 0; 56 | this.preprocessors = new processingChain(); 57 | this.postprocessors = new processingChain(); 58 | } 59 | 60 | Vatican.prototype.on = function(event, cb) { 61 | this.eventEmitter.on(event, cb); 62 | } 63 | 64 | Vatican.prototype.preprocess = function(fn, endpointNames) { 65 | this.preprocessors.add({fn: fn, names: endpointNames ? endpointNames : []}) 66 | this.totalPreprocessors = this.preprocessors.getTotal(); 67 | } 68 | 69 | Vatican.prototype.postprocess = function(fn, endpointNames) { 70 | this.postprocessors.add({fn: fn, names: endpointNames ? endpointNames : []}); 71 | } 72 | 73 | Vatican.prototype.parseHandlers = function(cb) { 74 | var dir = this.options.handlers; 75 | var self = this; 76 | handlerParser.parse(dir, function(err, path) { 77 | if(typeof cb == 'function' && err) return cb(err) 78 | if(!err) { 79 | self.paths = path; 80 | } 81 | if(typeof cb == 'function') cb(null, self.paths) 82 | }) 83 | }; 84 | 85 | /** 86 | Makes sure that the required options are there 87 | */ 88 | Vatican.prototype.checkOptions = function() { 89 | if( !this.options.port ) { 90 | logger.error("Port not specified, throwing error!"); 91 | throw new Error("Port not specified"); 92 | } 93 | 94 | if( !this.options.handlers ) { 95 | logger.error("Handlers folder not specified, throwing error!"); 96 | throw new Error("Handlers folder not specified"); 97 | } 98 | 99 | if(this.options.versioning) { 100 | if( this.options.versioning.format && !this.options.versioning.matching ) { 101 | logger.error("Versioning error: Missing matching function"); 102 | throw new Error("Versioning error: matching function is needed when format function is provided"); 103 | } 104 | } 105 | }; 106 | 107 | Vatican.prototype.parseRequest = function( req, template, originalReq , cb) { 108 | this.requestParser.parse(req, template, originalReq, cb); 109 | }; 110 | 111 | 112 | function matchVersions(versionList, version, versioning) { 113 | //If there are no versions defined on the annotation, the endpoint is meant to be 114 | //used on all versions 115 | if(!util.isArray(versionList)) return true; 116 | 117 | if(versionList.indexOf(version) != -1) return true; 118 | 119 | return versionList.find((v) => { 120 | if(versioning.matching) { 121 | return versioning.matching(version, v); 122 | } else { 123 | return version == v; 124 | } 125 | }); 126 | } 127 | 128 | 129 | /** 130 | Helper function used to find registered urls 131 | */ 132 | Vatican.prototype.findURLs = function findURLs(path, methodToMatch, versionToMatch) { 133 | let nmbrOfParts = path.split("/").length; 134 | let self = this; 135 | 136 | return function (item) { 137 | let regExpStr = item.url.replace(/\:.+(\/)?/g,".+$1"); 138 | let nmbrOfPartsPath = item.url.split("/").length; 139 | let regExp = new RegExp(regExpStr + "$"); 140 | 141 | logger.debug("Matching: " + path + " to " + regExp) 142 | 143 | let boolExpression = nmbrOfPartsPath == nmbrOfParts && regExp.test(path); 144 | if(methodToMatch) { 145 | boolExpression = boolExpression && item.method.toLowerCase() == methodToMatch.toLowerCase(); 146 | } 147 | if(versionToMatch) { 148 | boolExpression = boolExpression && matchVersions(item.versions, versionToMatch, self.options.versioning); 149 | } 150 | return boolExpression; 151 | } 152 | } 153 | 154 | Vatican.prototype.handleOptionsRequest = function (path, versionToMatch) { 155 | 156 | //should also allow for url = * 157 | let validMethods = _(this.paths); 158 | if(["*", "/*"].indexOf(path) == -1 ) { 159 | validMethods = validMethods.filter(this.findURLs(path, null, versionToMatch)) 160 | } 161 | validMethods = validMethods.pluck('method') 162 | .unique() 163 | .value() 164 | .join(",") 165 | 166 | return validMethods; 167 | } 168 | 169 | Vatican.prototype.parseURLVersioning = function(url, headers) { 170 | if(this.options.versioning.strategy == "url") { 171 | let urlParts = url.split("/"); 172 | let version = urlParts[1]; 173 | let path= urlParts.slice(2).join("/"); //remove the version part from thepath 174 | if(path[0] != "/") path= "/" + path; //adjust, because we need the url to start with a "/" 175 | return { 176 | path: path, 177 | version: version 178 | } 179 | } 180 | 181 | if(this.options.versioning.strategy == "header") { 182 | logger.debug("Headers: ", headers); 183 | let acceptHeader = headers.accept; 184 | let match = HEADER_VERSION_REGEXP.exec(acceptHeader); 185 | return { 186 | path: url, 187 | version: match[1] 188 | } 189 | 190 | } 191 | } 192 | 193 | /** 194 | Checks all paths until one that matches the current url is found. 195 | If there is no match, then false is returned 196 | */ 197 | Vatican.prototype.findMethod = function( url, method, headers ) { 198 | let path = (url.indexOf("?") == -1) ? url : url.split("?")[0]; 199 | 200 | ///URL version 201 | let version = null; 202 | 203 | if(this.options.versioning) { 204 | ({path, version} = this.parseURLVersioning(url, headers)); 205 | } 206 | 207 | if(method.toUpperCase() == 'OPTIONS') { 208 | let validMethods = this.handleOptionsRequest(path, version) 209 | return new OptionsResponse(validMethods); 210 | } 211 | 212 | logger.debug("URL: " + path); 213 | logger.debug("URL version: " + version); 214 | var match = _.find(this.paths, this.findURLs(path, method, version) ); 215 | return match; 216 | }; 217 | 218 | /* 219 | Adds usefull methods and properties to node's original request object. 220 | Note: Right now, there is nothing to be added, but I left the method here in case 221 | I think of something in the future. 222 | */ 223 | Vatican.prototype.createRequest = function(req) { 224 | return req; 225 | }; 226 | 227 | /* 228 | Reads the handler code, comments out the annotation, then 229 | loads the code as a require would and finally returns the new 230 | handler instance 231 | */ 232 | Vatican.prototype.loadHandler = function(path) { 233 | var key = new Buffer(path).toString('base64') 234 | 235 | if(this.handlers[key]) return this.handlers[key]; 236 | 237 | var Module = module.constructor 238 | var m = new Module(); 239 | 240 | var handlerContent = fs.readFileSync(path); 241 | handlerContent = handlerContent.toString().replace(/(@endpoint.+\n)/g,"//$1"); 242 | m.paths = module.paths; 243 | try { 244 | m._compile(handlerContent, path); 245 | this.handlers[key] = m.exports; 246 | } catch (ex) { 247 | logger.error("Error compiling handler: ", ex) 248 | } 249 | return this.handlers[key]; 250 | }; 251 | 252 | /** 253 | Handles each request, by looking up the right handler/method to execute 254 | */ 255 | Vatican.prototype.requestHandler = function (req, res) { 256 | logger.info("Request received: [" + req.method + "]: " + req.url); 257 | var self = this; 258 | var methodFound = this.findMethod(req.url, req.method, req.headers); 259 | if( !methodFound ) { 260 | vaticanResp.writeNotFound(res); 261 | } else { 262 | try { 263 | res = vaticanResp.improveResponse(res, request, this.options, this.postprocessors); 264 | if(methodFound instanceof OptionsResponse) { 265 | self.preprocessors.add({fn: methodFound.action.bind(methodFound) }); 266 | self.preprocessors.runChain(null, res, null); 267 | } else { 268 | var request = this.createRequest(req); 269 | var hdlr = this.loadHandler(process.cwd() + "/" + methodFound.handlerPath); 270 | //Parse the request to grab the parameters 271 | this.parseRequest(request, methodFound.url, req, function(newRequest) { 272 | //Run the pre-process chain and finally, call the handler 273 | if(self.preprocessors.getTotal() > self.totalPreprocessors) { 274 | self.preprocessors.pop(); 275 | } 276 | var hdlrInstance = new hdlr(self.getCorrectModel(methodFound), self.dbmodels) 277 | ///hdlrInstance.models = self.dbmodels //Let the handler access all other models in case they're neeeded 278 | logger.debug("Action to execute: ", methodFound) 279 | logger.debug(hdlrInstance) 280 | self.preprocessors.add({fn: hdlrInstance[methodFound.action].bind(hdlrInstance)}) 281 | self.preprocessors.runChain(newRequest, res, null, methodFound); 282 | }); 283 | } 284 | } catch (ex) { 285 | logger.error("Error instantiating handler: " + ex.message); 286 | logger.error("Stacktrace: " + ex.stack); 287 | res.end(); 288 | } 289 | } 290 | }; 291 | 292 | Vatican.prototype.getCorrectModel = function(handler) { 293 | if(this.dbmodels) { 294 | var modelName = handler.handlerName.replace("Hdlr", '') 295 | return this.dbmodels[modelName] 296 | } 297 | return false 298 | } 299 | 300 | 301 | /** 302 | Starts up the server 303 | Param: cb (optional) A callback to execute after the server has been instantiated 304 | */ 305 | Vatican.prototype.start = function(cb) { 306 | try { 307 | this.dbStart(); //we initiate the db connection automatically 308 | this.server = http.createServer(this.requestHandler.bind(this)); 309 | this.server.listen(this.options.port); 310 | logger.info("Server started on port: " + this.options.port); 311 | this.eventEmitter.emit(EventsEnum.HTTPSERVERREADY) 312 | if(typeof cb == 'function') { 313 | cb(); 314 | } 315 | } catch (ex) { 316 | logger.error("Error creating server: " + ex.message); 317 | return false; 318 | } 319 | }; 320 | 321 | 322 | /** 323 | Close the server 324 | */ 325 | Vatican.prototype.close = function(cb) { 326 | try { 327 | this.server.close(); 328 | logger.info("Server closed"); 329 | if(typeof cb == 'function') { 330 | cb(); 331 | } 332 | } catch (ex) { 333 | logger.error("Error closing server: " + ex.message); 334 | return false; 335 | } 336 | } 337 | 338 | Vatican.prototype.__dbStart = function() { 339 | var connString = 'mongodb://' + this.options.db.host + '/' + this.options.db.dbname 340 | mongoose.connect(connString) 341 | return mongoose.connection 342 | }; 343 | 344 | /** 345 | Handles connection to the database 346 | */ 347 | Vatican.prototype.onDbStart = function() { 348 | 349 | let schemasFolder = this.options.schemasFolder 350 | 351 | this.loadDbModels(schemasFolder, (err, models) => { 352 | if(err) { 353 | return this.eventEmitter.emit(EventsEnum.DBERROR); 354 | } 355 | this.dbmodels = models 356 | this.eventEmitter.emit(EventsEnum.DBSTART) 357 | }) 358 | } 359 | 360 | Vatican.prototype.dbStart = function(opts, cb) { 361 | if(typeof opts === 'function') { 362 | cb = opts 363 | opts = {} 364 | } else { 365 | if(!opts) { 366 | opts = {}; 367 | } 368 | } 369 | this.options.schemasFolder = opts.schemasFolder || DEFAULT_SCHEMAS_FOLDER 370 | 371 | if(!this.dbconn) { //make sure if this gets called twice, it won't try to connect again 372 | this.dbconn = this.__dbStart() 373 | this.dbconn.on('error', (err) => { 374 | this.eventEmitter.emit(EventsEnum.DBERROR, err); 375 | }) 376 | this.dbconn.once('open', () => { 377 | if(cb) { 378 | logger.warn("Deprecation warning: You're using 'dbStart' when you should be directly calling 'start' "); 379 | this.on(EventsEnum.DBSTART, cb); 380 | } 381 | this.eventEmitter.emit(EventsEnum.INTERNAL_DBSTART); 382 | }) 383 | } 384 | 385 | } 386 | 387 | Vatican.prototype.loadDbModels = function(folder, cb) { 388 | var models = {} 389 | var self = this 390 | fs.readdir(folder, function(err, files) { 391 | if(err) return cb(err) 392 | var path = null, tmp = null 393 | files.forEach(function(f) { 394 | path = folder + "/" + f 395 | tmp = require(path)(mongoose) 396 | models[tmp.modelName] = tmp 397 | }) 398 | cb(null, models) 399 | }) 400 | } 401 | 402 | --------------------------------------------------------------------------------