├── index.js ├── example ├── views │ ├── layout.jade │ ├── 500.jade │ ├── resterror.jade │ └── person │ │ └── index.jade ├── models │ └── person.js ├── controllers │ └── person.js ├── routes │ └── person.js ├── app.js ├── JakeFile.js └── tests │ └── integration │ └── person.integration.test.js ├── bin └── restmvc ├── tests └── unit │ ├── .DS_Store │ ├── errorMapper.test.js │ └── errors.test.js ├── package.json ├── lib ├── baseobject.js ├── templates │ ├── app.js.ejs │ └── Jakefile.js.ejs ├── resterrors.js ├── cli.js └── restmvc.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/restmvc'); -------------------------------------------------------------------------------- /example/views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | body!= body -------------------------------------------------------------------------------- /bin/restmvc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('restmvc.js').cli(process.argv.slice(2)); 3 | -------------------------------------------------------------------------------- /tests/unit/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pose/restmvc.js/master/tests/unit/.DS_Store -------------------------------------------------------------------------------- /example/views/500.jade: -------------------------------------------------------------------------------- 1 | .page 2 | h2 Error 3 | p An Unexpected Server Error was encountered. 4 | h3 Error Details 5 | pre #{error} -------------------------------------------------------------------------------- /example/views/resterror.jade: -------------------------------------------------------------------------------- 1 | .page 2 | h2 #{error.httpStatus}: #{error.title} 3 | p #{error.description} 4 | h3 Details 5 | pre #{error.message} -------------------------------------------------------------------------------- /example/views/person/index.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title="Here is some People" 5 | body 6 | h1 Junk 7 | #container 8 | - each item in collection 9 | li= item -------------------------------------------------------------------------------- /example/models/person.js: -------------------------------------------------------------------------------- 1 | module.exports.person = function (mongoose) { 2 | // Standard Mongoose stuff here... 3 | var schema = mongoose.Schema; 4 | var objectId = schema.ObjectId; 5 | 6 | mongoose.model('Person', new schema({ 7 | _id: objectId, 8 | firstName: String, 9 | lastName: String, 10 | initial: String, 11 | dateOfBirth: Date 12 | })); 13 | 14 | return mongoose.model('Person'); 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restmvc.js", 3 | "description": "A micro framework that helps you quickly build RESTful web services", 4 | "version": "0.0.3", 5 | "homepage": "http://github.com/keithnlarsen/restmvc.js", 6 | "author": "Keith Larsen", 7 | "main": "./index.js", 8 | "directories": {"lib": "./lib"}, 9 | "engines": { 10 | "node": ">= 0.4.1" 11 | }, 12 | "dependencies": { 13 | "jake": "0.1.8", 14 | "nodeunit": "0.5.0", 15 | "express": "2.0.0beta3", 16 | "mongoose": "1.0.10", 17 | "jade": "0.8.5", 18 | "ejs": "0.3.1" 19 | }, 20 | "bin": { 21 | "restmvc": "./bin/restmvc" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/controllers/person.js: -------------------------------------------------------------------------------- 1 | module.exports.personController = function(baseController, restMvc){ 2 | 3 | // this file is not neccessary but is here for demonstration perposes that you can. 4 | // you just need to return the controller, or one that extends the base one. 5 | baseController.plural = "people"; 6 | return baseController; 7 | 8 | // Example of how to extend the base controller if you need to... 9 | // var Controller = baseController.extend({ 10 | // toString: function(){ 11 | // // calls parent "toString" method without arguments 12 | // return this._super(Controller, "toString") + " (Controller)"; 13 | // } 14 | // }); 15 | }; -------------------------------------------------------------------------------- /example/routes/person.js: -------------------------------------------------------------------------------- 1 | module.exports.personRoutes = function(personController, app, restMvc){ 2 | 3 | //Example route implemtation. 4 | // app.get('/people/:id', function(request, response, next) { 5 | // personController.get(request.params.id, function(err, instance) { 6 | // if (err) 7 | // next(new Error('Internal Server Error: see logs for details: ' + err), request, response); 8 | // else if (!instance) 9 | // next(new restMvc.RestError.NotFound('Person Id: "' + request.params.id + '" was not found.'), request, response); 10 | // else 11 | // response.send(instance.toObject()); 12 | // }); 13 | // }); 14 | }; -------------------------------------------------------------------------------- /lib/baseobject.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | create: function create() { 3 | var instance = Object.create(this); 4 | instance._construct.apply(instance, arguments); 5 | return instance; 6 | }, 7 | 8 | extend: function extend(properties) { 9 | var propertyDescriptors = {}; 10 | 11 | if(properties){ 12 | var simpleProperties = Object.getOwnPropertyNames(properties); 13 | for (var i = 0, len = simpleProperties.length; i < len; i += 1) { 14 | var propertyName = simpleProperties[i]; 15 | if(propertyDescriptors.hasOwnProperty(propertyName)) { 16 | continue; 17 | } 18 | 19 | propertyDescriptors[propertyName] = Object.getOwnPropertyDescriptor(properties, propertyName); 20 | } 21 | } 22 | 23 | return Object.create(this, propertyDescriptors); 24 | }, 25 | 26 | _construct: function _construct() {}, 27 | 28 | _super: function _super(definedOn, methodName, args) { 29 | if (typeof methodName !== "string") { 30 | args = methodName; 31 | methodName = "_construct"; 32 | } 33 | 34 | return Object.getPrototypeOf(definedOn)[methodName].apply(this, args); 35 | } 36 | }; -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var express = require('express@2.0.0beta3'); 3 | var app = module.exports = express.createServer(); 4 | 5 | var mongoose = require('mongoose@1.0.10'); 6 | mongoose.connect('mongodb://localhost/restmvc'); 7 | 8 | app.configure('debug', function() { 9 | app.use(express.logger({ format: '\x1b[1m :date \x1b[1m:method\x1b[0m \x1b[33m:url\x1b[0m :response-time ms\x1b[0m :status' })); 10 | }); 11 | 12 | app.configure(function() { 13 | app.set('root', __dirname); 14 | app.set('views', __dirname + '/views'); 15 | app.set('view engine', 'jade'); 16 | app.use(express.favicon()); 17 | app.use(express.bodyParser()); 18 | app.use(express.methodOverride()); 19 | app.use(app.router); 20 | }); 21 | 22 | // This grabs the index.js file at the root and uses that 23 | var restMVC = require('../'); 24 | restMVC.Initialize(app, mongoose); 25 | 26 | app.error(restMVC.ErrorHandler); 27 | 28 | app.use(function(req, res, next){ 29 | next(restMVC.RestError.NotFound.create(req.url)); 30 | }); 31 | 32 | // example of how to throw a 404 33 | app.get('/404', function(req, res, next){ 34 | next(restMVC.RestError.NotFound.create(req.url)); 35 | }); 36 | 37 | // example of how to throw a 500 38 | app.get('/500', function(req, res, next){ 39 | next(new Error('keyboard cat!')); 40 | }); 41 | 42 | if (!module.parent) { 43 | app.listen(3000); 44 | console.log('Server running at http://127.0.0.1:3000/' + '\r'); 45 | } 46 | -------------------------------------------------------------------------------- /lib/templates/app.js.ejs: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var express = require('express@2.0.0beta3'); 3 | var app = module.exports = express.createServer(); 4 | 5 | var mongoose = require('mongoose@1.0.10'); 6 | mongoose.connect('mongodb://localhost/<%= appName %>'); 7 | 8 | app.configure('debug', function() { 9 | app.use(express.logger({ format: '\x1b[1m :date \x1b[1m:method\x1b[0m \x1b[33m:url\x1b[0m :response-time ms\x1b[0m :status' })); 10 | }); 11 | 12 | app.configure(function() { 13 | app.set('root', __dirname); 14 | app.set('views', __dirname + '/views'); 15 | app.set('view engine', 'jade'); 16 | app.use(express.favicon()); 17 | app.use(express.bodyParser()); 18 | app.use(express.methodOverride()); 19 | app.use(app.router); 20 | }); 21 | 22 | // This grabs the index.js file at the root and uses that 23 | var restMVC = require('restmvc'); 24 | restMVC.Initialize(app, mongoose); 25 | 26 | app.error(restMVC.ErrorHandler); 27 | 28 | app.use(function(req, res, next){ 29 | next(restMVC.RestError.NotFound.create(req.url)); 30 | }); 31 | 32 | // example of how to throw a 404 33 | app.get('/404', function(req, res, next){ 34 | next(restMVC.RestError.NotFound.create(req.url)); 35 | }); 36 | 37 | // example of how to throw a 500 38 | app.get('/500', function(req, res, next){ 39 | next(new Error('keyboard cat!')); 40 | }); 41 | 42 | if (!module.parent) { 43 | app.listen(3000); 44 | console.log('Server running at http://127.0.0.1:3000/' + '\r'); 45 | } 46 | -------------------------------------------------------------------------------- /example/JakeFile.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var reporters = require('nodeunit').reporters; 3 | 4 | desc('Run applicaiton with full debug information turned on.'); 5 | task('debug', [], function () { 6 | console.log('Starting Nodejs Server'); 7 | 8 | process.env.NODE_ENV = 'debug'; 9 | 10 | var app = require('./app'); 11 | app.listen(3000); 12 | console.log('Server running at http://127.0.0.1:3000/' + ' in debug mode.\r'); 13 | }); 14 | 15 | desc('Run application in release mode with minimal debug information.'); 16 | task('release', [], function () { 17 | console.log('Starting Nodejs Server'); 18 | 19 | process.env.NODE_ENV = 'release'; 20 | 21 | var app = require('./app'); 22 | app.listen(3000); 23 | console.log('Server running at http://127.0.0.1:3000/' + ' in release mode.\r'); 24 | }); 25 | 26 | desc('Run the applications integration tests.'); 27 | task('test', [], function () { 28 | console.log('Starting Nodejs'); 29 | 30 | process.env.NODE_ENV = 'test'; 31 | 32 | var app = require('./app'); 33 | 34 | app.listen(3000); 35 | console.log('Running testing server at http://127.0.0.1:3000/' + '\r'); 36 | 37 | // Delay to make sure that node server has time to start up on slower computers before running the tests. 38 | setTimeout( function(){ 39 | require.paths.push(__dirname); 40 | 41 | var testRunner = reporters.default; 42 | 43 | process.chdir(__dirname); 44 | 45 | console.log('Running integration tests.'); 46 | testRunner.run(['tests/integration']); 47 | }, 250); 48 | }); 49 | -------------------------------------------------------------------------------- /lib/templates/Jakefile.js.ejs: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var reporters = require('nodeunit').reporters; 3 | 4 | desc('Run applicaiton with full debug information turned on.'); 5 | task('debug', [], function () { 6 | console.log('Starting Nodejs Server'); 7 | 8 | process.env.NODE_ENV = 'debug'; 9 | 10 | var app = require('./app'); 11 | app.listen(3000); 12 | console.log('Server running at http://127.0.0.1:3000/' + ' in debug mode.\r'); 13 | }); 14 | 15 | desc('Run application in release mode with minimal debug information.'); 16 | task('release', [], function () { 17 | console.log('Starting Nodejs Server'); 18 | 19 | process.env.NODE_ENV = 'release'; 20 | 21 | var app = require('./app'); 22 | app.listen(3000); 23 | console.log('Server running at http://127.0.0.1:3000/' + ' in release mode.\r'); 24 | }); 25 | 26 | desc('Run the applications integration tests.'); 27 | task('test', [], function () { 28 | console.log('Starting Nodejs'); 29 | 30 | process.env.NODE_ENV = 'test'; 31 | 32 | var app = require('./app'); 33 | 34 | app.listen(3000); 35 | console.log('Running testing server at http://127.0.0.1:3000/' + '\r'); 36 | 37 | // Delay to make sure that node server has time to start up on slower computers before running the tests. 38 | setTimeout( function(){ 39 | require.paths.push(__dirname); 40 | 41 | var testRunner = reporters.default; 42 | 43 | process.chdir(__dirname); 44 | 45 | console.log('Running integration tests.'); 46 | testRunner.run(['tests/integration']); 47 | }, 250); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/errorMapper.test.js: -------------------------------------------------------------------------------- 1 | var testFixture = require('nodeunit').testCase; 2 | var restMVC = require('../../libs/restmvc'); 3 | var restError = restMVC.RestError; 4 | 5 | exports['ErrorMapper'] = testFixture({ 6 | setUp: function (callback) { 7 | callback(); 8 | }, 9 | 10 | tearDown: function (callback) { 11 | // Clean up 12 | callback(); 13 | }, 14 | 15 | 'Should map NotFound Error to 404 NotFound response' : function(test){ 16 | test.expect(1); 17 | 18 | var notFound = new restError.NotFound('Test Error'); 19 | var mockRequest = {}; 20 | var mockResponse = { 21 | send: function (content, status) { 22 | //test.equals(template, '404.jade'); 23 | test.equals(status, '404'); 24 | //test.equals(config.locals.error.message, "Test Error"); 25 | test.done(); 26 | } 27 | }; 28 | 29 | restMVC.ErrorHandler(notFound, mockRequest, mockResponse); 30 | }, 31 | 32 | 'Should map Generic Errors to 500 InternalServerError response' : function(test){ 33 | test.expect(1); 34 | 35 | var error = new Error('Test Error'); 36 | var mockRequest = {}; 37 | var mockResponse = { 38 | send: function (content, status) { 39 | //test.equals(template, '500.jade'); 40 | test.equals(status, '500'); 41 | //test.equals(config.locals.error.message, "Test Error"); 42 | test.done(); 43 | } 44 | }; 45 | 46 | restMVC.ErrorHandler(error, mockRequest, mockResponse); 47 | } 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /tests/unit/errors.test.js: -------------------------------------------------------------------------------- 1 | var testFixture = require('nodeunit').testCase; 2 | var restMVC = require('../../libs/restmvc'); 3 | var restError = restMVC.RestError; 4 | 5 | exports['NotFound'] = testFixture({ 6 | setUp: function (callback) { 7 | // Do set up 8 | callback(); 9 | }, 10 | 11 | tearDown: function (callback) { 12 | // Clean up 13 | callback(); 14 | }, 15 | 16 | 'Should have constructor named NotFound' : function(test){ 17 | test.expect(1); 18 | var NotFound = new restError.NotFound("Test Error"); 19 | test.equals(NotFound.constructor.name, "NotFound"); 20 | test.done(); 21 | }, 22 | 23 | 'Should inherit from Error' : function(test){ 24 | test.expect(1); 25 | var NotFound = new restError.NotFound("Test Error"); 26 | test.equals(NotFound.constructor.super_.name, "Error"); 27 | test.done(); 28 | }, 29 | 30 | 'Should pass error message' : function(test){ 31 | test.expect(1); 32 | var NotFound = new restError.NotFound("Test NotFound message."); 33 | test.equals(NotFound.message, "Test NotFound message."); 34 | test.done(); 35 | }, 36 | 37 | 'Should throw NotFound error' : function(test){ 38 | test.expect(3); 39 | 40 | try { 41 | throw new restError.NotFound("Test NotFound message."); 42 | } 43 | catch(err){ 44 | test.equals(err.constructor.name, "NotFound"); 45 | test.equals(err.constructor.super_.name, "Error"); 46 | test.equals(err.message, "Test NotFound message."); 47 | } 48 | test.done(); 49 | } 50 | }); 51 | 52 | -------------------------------------------------------------------------------- /lib/resterrors.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys'); 2 | var baseObject = require('./baseobject'); 3 | 4 | var baseRestError = function() { 5 | var baseRestError = baseObject.extend({ 6 | name: 'BaseRestError', 7 | title: 'Base Rest Error', 8 | description: '', 9 | message: '', 10 | 11 | _construct: function(message){ 12 | this.message = message; 13 | Error.call(this, message); 14 | Error.captureStackTrace(this, arguments.callee); 15 | }, 16 | 17 | toString: function(){ 18 | return this.title + ": " + this.message; 19 | } 20 | }); 21 | 22 | sys.inherits(baseRestError, Error); 23 | 24 | return baseRestError; 25 | }(); 26 | 27 | module.exports.BaseRestError = baseRestError; 28 | 29 | module.exports.RestError = { 30 | NotFound: baseRestError.extend({ 31 | name: 'NotFound', 32 | title: 'Not Found', 33 | description: 'The requested resource could not be found.', 34 | httpStatus: 404 35 | }) 36 | }; 37 | 38 | module.exports.ErrorMapper = errorMapper = function() { 39 | return { 40 | 'NotFound': function(error, request, response){ 41 | response.render('resterror.jade', { 42 | status: error.httpStatus, 43 | error: error 44 | }); 45 | }, 46 | 'default': function(error, request, response) { 47 | response.render('500.jade', { 48 | status: 500, 49 | error: error 50 | }); 51 | } 52 | } 53 | }(); 54 | 55 | module.exports.ErrorHandler = function(error, request, response) { 56 | var errorHandler = errorMapper[error.name] || errorMapper['default']; 57 | 58 | errorHandler(error, request, response); 59 | }; -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | var mkdir = require('fs').mkdirSync, 2 | fs = require('fs'), 3 | path = require('path'), 4 | ejs = require('ejs'); 5 | 6 | var usage = 'Usage: restmvc app \n'; 7 | 8 | 9 | function helpMessage() { 10 | console.info('Error: ' + usage); 11 | process.exit(1); 12 | } 13 | 14 | var FileTemplate = function (templatePath, appName) { 15 | this.templatePath = templatePath; 16 | this.appName = appName; 17 | }; 18 | 19 | FileTemplate.prototype.render = function(path) { 20 | var str = fs.readFileSync(this.templatePath, 'utf8'); 21 | str = ejs.render(str, {"locals": {"appName": this.appName}}); 22 | fs.writeFileSync(path, str); 23 | }; 24 | 25 | 26 | function createApp(appName) { 27 | var pathsToGenerate = {}; 28 | 29 | pathsToGenerate[appName] = { 'controllers': {}, 30 | 'models': {}, 31 | 'routes': {}, 32 | 'tests': {'integration': {}}, 33 | 'views': {}, 34 | 'app.js': new FileTemplate(path.join(__dirname, 35 | 'templates', 'app.js.ejs'),appName), 36 | 'Jakefile.js': new FileTemplate(path.join(__dirname, 37 | 'templates', 'Jakefile.js.ejs'),appName) 38 | }; 39 | 40 | function generatePaths(pathSoFar, toGenerate) { 41 | var currentPath, 42 | paths = Object.keys(toGenerate); 43 | 44 | if ( paths ) { 45 | paths.forEach(function (name) { 46 | currentPath = path.join(pathSoFar, name); 47 | if ( toGenerate[name] instanceof FileTemplate ) { 48 | toGenerate[name].render(currentPath); 49 | return; 50 | } 51 | try { 52 | mkdir(currentPath, 0755); 53 | } catch (e) { 54 | console.error("Error creating " + currentPath + "\n" + 55 | e.message); 56 | } 57 | generatePaths(currentPath, toGenerate[name]); 58 | }); 59 | } 60 | } 61 | 62 | generatePaths('.', pathsToGenerate); 63 | } 64 | 65 | 66 | exports.cli = function (argv) { 67 | 68 | if ( argv.length != 2 ) { 69 | helpMessage(); 70 | } 71 | 72 | var actions = { 73 | 'app': function () { 74 | var appName = argv[1]; 75 | createApp(appName); 76 | } 77 | }; 78 | 79 | if ( argv[0] in actions ) 80 | actions[argv[0]](); 81 | else 82 | helpMessage(); 83 | } 84 | -------------------------------------------------------------------------------- /example/tests/integration/person.integration.test.js: -------------------------------------------------------------------------------- 1 | var createJSON = "{\"firstName\":\"Test\",\"lastName\":\"User\",\"dateOfBirth\": \"10/07/1971\"}"; 2 | var updateJSON = "{\"firstName\":\"Test2\",\"lastName\":\"User2\",\"dateOfBirth\": \"10/07/1971\"}"; 3 | var newPersonId = ''; 4 | 5 | var http = require('http'); 6 | var TestFixture = require('nodeunit').testCase; 7 | 8 | module.exports['HTTP Method'] = TestFixture({ 9 | setUp: function (callBack) { 10 | 11 | this.localhost = http.createClient(3000, 'localhost'); 12 | 13 | this.requestHelper = function(request, fn){ 14 | request.end(); 15 | 16 | request.on('response', function (response) { 17 | var responseBody = ""; 18 | response.setEncoding('utf8'); 19 | 20 | response.addListener("data", function(chunk) { 21 | responseBody += chunk; 22 | }); 23 | 24 | response.on('end', function() { 25 | response.body = responseBody; 26 | fn(response); 27 | }); 28 | }); 29 | }; 30 | 31 | callBack(); 32 | }, 33 | 34 | tearDown: function (callBack) { 35 | // clean up 36 | callBack(); 37 | }, 38 | 39 | 'POST Should create a new Person' : function(test){ 40 | test.expect(4); 41 | 42 | var request = this.localhost.request('POST', '/People/', {'Host': 'localhost', 'Accept': 'application/json', 'Content-Type': 'application/json'}); 43 | request.write(createJSON); 44 | 45 | this.requestHelper(request, function(response){ 46 | var actualPerson = JSON.parse(response.body); 47 | var expectedPerson = JSON.parse(createJSON); 48 | 49 | newPersonId = actualPerson._id; 50 | 51 | test.ok(newPersonId != null); 52 | test.equals(expectedPerson.firstName, actualPerson.firstName); 53 | test.equals(expectedPerson.lastName, actualPerson.lastName); 54 | // test.equals(new Date(expectedPerson.dateOfBirth), new Date(actualPerson.dateOfBirth)); 55 | 56 | test.equals(response.statusCode, 201); 57 | 58 | test.done(); 59 | }); 60 | }, 61 | 62 | 'GET Should return a single Person when calling /People/{ID}' : function(test){ 63 | test.expect(3); 64 | 65 | var request = this.localhost.request('GET', '/People/' + newPersonId + '.json', {'Host': 'localhost', 'Accept': 'application/json'}); 66 | 67 | this.requestHelper(request, function(response){ 68 | var actualPerson = JSON.parse(response.body); 69 | var expectedPerson = JSON.parse(createJSON); 70 | 71 | test.equals(expectedPerson.firstName, actualPerson.firstName); 72 | test.equals(expectedPerson.lastName, actualPerson.lastName); 73 | // test.equals(expectedPerson.dateOfBirth, actualPerson.dateOfBirth); 74 | 75 | test.equals(response.statusCode, 200); 76 | test.done(); 77 | }); 78 | }, 79 | 80 | 'PUT Should update an existing Person' : function(test){ 81 | test.expect(3); 82 | 83 | var request = this.localhost.request('PUT', '/People/' + newPersonId, {'Host': 'localhost', 'Accept': 'application/json', 'Content-Type': 'application/json'}); 84 | request.write(updateJSON); 85 | 86 | this.requestHelper(request, function(response){ 87 | var actualPerson = JSON.parse(response.body); 88 | var expectedPerson = JSON.parse(updateJSON); 89 | 90 | test.equals(expectedPerson.firstName, actualPerson.firstName); 91 | test.equals(expectedPerson.lastName, actualPerson.lastName); 92 | // test.equals(expectedPerson.dateOfBirth, actualPerson.dateOfBirth); 93 | 94 | test.equals(response.statusCode, 200); 95 | 96 | test.done(); 97 | }); 98 | }, 99 | 100 | 'PUT Should return 404 when trying to Update Person That Doesn\'t Exist' : function(test){ 101 | test.expect(2); 102 | 103 | var request = this.localhost.request('PUT', '/People/XXXXX', {'Host': 'localhost', 'Accept': 'application/json'}); 104 | request.write(updateJSON); 105 | 106 | this.requestHelper(request, function(response){ 107 | test.ok(response.body.length > 0); 108 | test.equals(response.statusCode, 404); 109 | test.done(); 110 | }); 111 | }, 112 | 113 | 'GET Should return all people when calling /People/' : function(test){ 114 | test.expect(2); 115 | 116 | var request = this.localhost.request('GET', '/People.json', {'Host': 'localhost', 'Accept': 'application/json'}); 117 | 118 | this.requestHelper(request, function(response){ 119 | test.equals(response.statusCode, 200); 120 | test.ok(response.body.length > 0); 121 | test.done(); 122 | }); 123 | }, 124 | 125 | 'GET Should return a 404 when calling /People/{ID} with an ID that doesn\'t exist' : function(test){ 126 | test.expect(2); 127 | 128 | var request = this.localhost.request('GET', '/People/XXXXX.json', {'Host': 'localhost', 'Accept': 'application/json'}); 129 | 130 | this.requestHelper(request, function(response){ 131 | test.ok(response.body.length > 0); 132 | test.equals(response.statusCode, 404); 133 | test.done(); 134 | }); 135 | }, 136 | 137 | 'DELETE Should delete person when calling /People/{ID}' : function(test){ 138 | test.expect(1); 139 | 140 | var request = this.localhost.request('DELETE', '/People/' + newPersonId, {'Host': 'localhost', 'Accept': 'application/json'}); 141 | 142 | this.requestHelper(request, function(response){ 143 | test.equals(response.statusCode, 200); 144 | test.done(); 145 | }); 146 | }, 147 | 148 | 'DELETE Should return a 404 when calling /People/{ID} with an ID that doesn\'t exist' : function(test){ 149 | test.expect(1); 150 | 151 | var request = this.localhost.request('DELETE', '/People/XXXXX', {'Host': 'localhost', 'Accept': 'application/json'}); 152 | 153 | this.requestHelper(request, function(response){ 154 | test.equals(response.statusCode, 404); 155 | test.done(); 156 | }); 157 | } 158 | }); 159 | -------------------------------------------------------------------------------- /lib/restmvc.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var restErrors = require('./resterrors'); 4 | var baseObject = require('./baseobject'); 5 | var restMvc = {}; 6 | 7 | module.exports.BaseRestError = restMvc.BaseRestError = restErrors.BaseRestError; 8 | module.exports.RestError = restMvc.RestError = restErrors.RestError; 9 | module.exports.ErrorMapper = restMvc.ErrorMapper = restErrors.ErrorMapper; 10 | module.exports.ErrorHandler = restMvc.ErrorHandler = restErrors.ErrorHandler; 11 | module.exports.Models = restMvc.Models = {}; 12 | module.exports.Controllers = restMvc.Controllers = {}; 13 | module.exports.cli = require('./cli').cli; 14 | 15 | module.exports.BaseController = restMvc.BaseController = baseObject.extend({ 16 | model : null, 17 | name : '', 18 | plural : '', 19 | 20 | _construct: function(model, name) { 21 | this.model = model; 22 | this.name = name; 23 | this.plural = name + 's'; 24 | }, 25 | 26 | get : function(id, fn) { 27 | this.model.findById(id, function(err, instance) { 28 | fn(err, instance); 29 | }); 30 | }, 31 | 32 | list : function(fn) { 33 | this.model.find({}, function(err, list) { 34 | fn(err, list); 35 | }); 36 | }, 37 | 38 | update : function(id, json, fn){ 39 | var options = { safe: true, upsert: false, multi: false}; 40 | var self = this; 41 | this.model.update({_id: id}, json, options, function(err) { 42 | if (err){ 43 | // TODO: Swallowing this error is bad, but this seems to be thrown when mongo can't find the document we are looking for. 44 | if (err == 'Error: Element extends past end of object') 45 | fn(null, null); 46 | else 47 | fn(err, null); 48 | } 49 | else { 50 | self.model.findById(id, function(err, instance) { 51 | fn(err, instance); 52 | }); 53 | } 54 | }); 55 | }, 56 | 57 | insert : function(json, fn){ 58 | var instance = new this.model(json); 59 | 60 | instance.save( function(err){ 61 | fn(err, instance); 62 | }); 63 | }, 64 | 65 | remove : function(id, fn){ 66 | this.model.findById(id, function(err, instance) { 67 | if (instance) { 68 | instance.remove(function(err){ 69 | fn(err, instance) 70 | }); 71 | } 72 | else { 73 | fn(err, instance); 74 | } 75 | }); 76 | } 77 | }); 78 | 79 | module.exports.RegisterRoutes = restMvc.RegisterRoutes = function(app, controller) { 80 | app.get('/' + controller.plural + '/:id.:format?', function(request, response, next) { 81 | controller.get(request.params.id, function(err, instance) { 82 | if (err) 83 | next(new Error('Internal Server Error: see logs for details: ' + err), request, response); 84 | else if (!instance) 85 | next(restMvc.RestError.NotFound.create(controller.name + ' Id: "' + request.params.id + '" was not found.'), request, response); 86 | else { 87 | if (request.params.format == 'json') 88 | response.send(instance.toObject()); 89 | else 90 | response.render(controller.name, { collection: instance.toObject() } ); 91 | } 92 | }); 93 | }); 94 | 95 | app.get('/' + controller.plural + '.:format?', function(request, response) { 96 | controller.list(function(err, results) { 97 | if (err) { 98 | next(new Error('Internal Server Error: see logs for details: ' + err), request, response); 99 | } 100 | else { 101 | if (request.params.format == 'json') { 102 | response.send(results.map(function(instance) { 103 | return instance.toObject(); 104 | })); 105 | } 106 | else { 107 | response.render(controller.name, { collection: results.map(function(instance) { 108 | return instance.toObject(); 109 | })}); 110 | } 111 | } 112 | }); 113 | }); 114 | 115 | app.put('/' + controller.plural + '/:id', function(request, response, next) { 116 | controller.update(request.params.id, request.body, function(err, instance){ 117 | if (err) 118 | next(new Error('Internal Server Error: see logs for details: ' + err), request, response); 119 | else if (!instance) 120 | next(restMvc.RestError.NotFound.create(controller.name + ' Id: "' + request.params.id + '" was not found.'), request, response); 121 | else 122 | response.send(instance.toObject()); 123 | }); 124 | }); 125 | 126 | app.post('/' + controller.plural, function(request, response, next) { 127 | controller.insert(request.body, function(err, instance){ 128 | if (err) 129 | next(new Error('Internal Server Error: see logs for details: ' + err), request, response); 130 | else 131 | response.send(instance.toObject(), null, 201); 132 | }); 133 | }); 134 | 135 | app.del('/' + controller.plural + '/:id', function(request, response, next) { 136 | controller.remove(request.params.id, function(err, instance){ 137 | if (err) 138 | next(new Error('Internal Server Error: see logs for details: ' + err), request, response); 139 | else if (!instance) 140 | next(restMvc.RestError.NotFound.create(controller.name + ' Id: "' + request.params.id + '" was not found.'), request, response); 141 | else 142 | response.send(instance.toObject()); 143 | }); 144 | }); 145 | }; 146 | 147 | module.exports.Initialize = restMvc.Initialize = function(app, mongoose) { 148 | var modelsPath = path.normalize(app.set('root') + '/models'); 149 | 150 | fs.readdir(modelsPath, function(err, files){ 151 | if (err) throw err; 152 | files.forEach(function(file){ 153 | 154 | // Get the Model 155 | var name = file.replace('.js', ''); 156 | var model = require(modelsPath + '/' + name)[name](mongoose); 157 | restMvc.Models[name] = model; 158 | 159 | // Create the base controller 160 | var controller = restMvc.BaseController.create(model, name); 161 | 162 | // Check for a custom controller, load it if exists 163 | var controllerPath = path.normalize(app.set('root') + '/controllers/') + name; 164 | 165 | path.exists(controllerPath + '.js', function(exists){ 166 | if (exists) 167 | controller = require(controllerPath)[name + 'Controller'](controller, restMvc); 168 | 169 | restMvc.Controllers[name] = controller; 170 | 171 | // Register all the routes 172 | restMvc.RegisterRoutes(app, controller); 173 | 174 | // Check for custom Routes, load if exists 175 | var routesPath = path.normalize(app.set('root') + '/routes/') + name; 176 | path.exists(routesPath + '.js', function(exists){ 177 | if (exists) require(routesPath)[name + 'Routes'](controller, app, restMvc); 178 | }); 179 | }); 180 | }); 181 | }); 182 | }; 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RestMVC 2 | ======= 3 | 4 | The goal of RestMVC is to provide a simple framework that helps you to write a RESTful webservice using NodeJs, Express, Mongoose, and MongoDB. It's goal is to take out the repetitive crap everyone hates doing, while staying out of your ways as much as possible and letting you do things how you want to. 5 | 6 | ## Contribute 7 | 8 | This project is just begining, it arose from my attempts to create a RESTfull service and discovering that it wasn't as easy as I would have liked. I'd really appreciate any contributions, bug reports, advice, or suggestions for improvement. 9 | 10 | ## Features 11 | 12 | This is the first release, but so far given a mongoose model object it will: 13 | 14 | * Auto-generate controllers 15 | * Auto-generate routes 16 | * Handle 'NotFound' and '500' errors in a nice clean way. 17 | 18 | Planned in the near future: 19 | 20 | * Security. 21 | * More complex List actions on the controller. 22 | * A tool that auto-generates a template project for you. 23 | * More error types. 24 | 25 | ## Installation 26 | 27 | npm install restmvc.js 28 | 29 | ### Dependancies 30 | 31 | So far this is dependant on: 32 | 33 | * Nodejs 0.4.2 34 | * Mongoose 1.0.10 35 | * MongoDB 1.6.5 36 | * Express 2.0beta3 37 | * NodeUnit 0.5.0 38 | * Node-Jake 39 | 40 | ## Example 41 | 42 | I've created an example project in the example folder. If you download the complete RestMVC.js project and from the command line navigate into the example folder, you can run a set of integration tests by typing: 43 | 44 | jake test 45 | 46 | You can also start up the the REST service by typing: 47 | 48 | jake debug 49 | 50 | ## Setup 51 | 52 | I plan to provide a tool that will eventually auto-generate a project structure for you, but for now, you have to lay it out as follows. 53 | 54 | /controllers 55 | /controllers/{entity_name}.js 56 | /models 57 | /models/{entity_name}.js 58 | /routes 59 | /routes/{entity_name}.js 60 | app.js 61 | 62 | ### Creating a Model 63 | 64 | Models are just standard Mongoose models, you can create a new model by creating a javascript file inside of the 'models' folder. You need to name the file, and the export the same. Your object will get a Mongoose object passed to it, you use that to create your model. 65 | 66 | Here's an example of how you'd define one: 67 | 68 | exports.person = function (mongoose) { 69 | var schema = mongoose.Schema; 70 | var objectId = schema.ObjectId; 71 | 72 | mongoose.model('Person', new schema({ 73 | _id: objectId, 74 | firstName: String, 75 | lastName: String, 76 | initial: String, 77 | dateOfBirth: Date 78 | })); 79 | 80 | return mongoose.model('Person'); 81 | }; 82 | 83 | ### Creating a Controller 84 | 85 | You don't have to do anything to create a basic controller, one that provides get, list, insert, update, and remove is generated for you. However if you wanted to extend or change the base controller, you'd create a file inside of the 'controllers' folder and name it the same as your model file. The file should export an object named {entity_name}Controller, for example personController. 86 | 87 | Here's an example of how you'd define one: 88 | 89 | module.exports.personController = function(baseController, restMvc){ 90 | // By default pluralization are done by adding 's' 91 | // you can change the default plural name from persons to people like so 92 | baseController.plural = 'people'; 93 | return baseController; 94 | } 95 | 96 | From this basic framework a controller that implements: 97 | 98 | * get(id) 99 | * list(), 100 | * insert(json) 101 | * update(id, json) 102 | * remove(id) 103 | 104 | You can extend the base functionality by defining your controller something like this: 105 | 106 | module.exports.personController = function(baseController, restMvc){ 107 | // Change the default plural name from 'persons' to 'people' 108 | baseController.plural = 'people'; 109 | 110 | //Example of how to extend the base controller if you need to... 111 | var extendedController = baseController.extend({ 112 | toString: function(){ 113 | // calls parent "toString" method without arguments 114 | return this._super(extendedController, "toString") + this.name; 115 | } 116 | }); 117 | 118 | return extendedController; 119 | }; 120 | 121 | ### Routes 122 | 123 | The default routes that get added to your express app are: 124 | 125 | * GET /{entity_plural_name}/ - Lists all entities in the colleciton 126 | * GET /{entity_plural_name}/{id} - Gets a specific entity 127 | * PUT /{entity_plural_name}/ JSON - Inserts a new record using the json passed in 128 | * POST /{entity_plural_name}/{id} JSON - Updates a record using the json passed in 129 | * DELETE /{entity_plural_name}/{id} - Deletes the specified record 130 | 131 | You don't need to define a route at all as they are setup for you, but if you want to extend the defaults by defining routes for your entity type, it would look something like the following: 132 | 133 | module.exports.employeeRoutes = function(employeeController, app, restMvc){ 134 | //Example route implemtation. 135 | app.get('/employees/:id', function(request, response, next) { 136 | employeeController.get(request.params.id, function(err, instance) { 137 | if (err) 138 | next(new Error('Internal Server Error: see logs for details: ' + err), request, response); 139 | else if (!instance) 140 | next(restMvc.RestError.NotFound.create('Employee Id: "' + request.params.id + '" was not found.'), request, response); 141 | else 142 | response.send(instance.toObject()); 143 | }); 144 | }); 145 | }; 146 | 147 | ## Initialization 148 | 149 | In your app.js file after connecting to mongoose and defining your express app, you should initialize everything like so: 150 | 151 | var express = require('express@2.0.0beta3'); 152 | var restMVC = require('restmvc@0.0.3'); 153 | var mongoose = require('mongoose@1.0.10'); 154 | 155 | var app = module.exports = express.createServer(); 156 | 157 | mongoose.connect('mongodb://localhost/restmvc'); 158 | 159 | ... a bunch of standard app configuration junk ... 160 | 161 | restMVC.Initialize(app, mongoose); 162 | 163 | app.error(restMVC.ErrorHandler); 164 | 165 | if (!module.parent) { 166 | app.listen(3000); 167 | console.log('Server running at http://127.0.0.1:3000/' + '\r'); 168 | } 169 | 170 | You can then start your sever using app.listen... 171 | 172 | ## Customize RestErrors 173 | 174 | So far only one error is handled, 404. If you want to extend this, it is very easy to do. Just do something like this in your app.js file. 175 | 176 | // Add a custom rest error for Forbidden 177 | restMVC.RestError.Forbidden = restMVC.RestError.BaseRestError.extend({ 178 | name: 'Forbidden', 179 | title: 'Forbidden', 180 | description: 'Access denied.', 181 | httpStatus: 403 182 | }) 183 | 184 | // Add a custom handler for Forbidden 185 | restMVC.ErrorMapper['Forbidden'] = function(error, request, response){ 186 | response.render('resterror.jade', { 187 | status: error.httpStatus, 188 | error: error 189 | }); 190 | } 191 | 192 | ## API 193 | 194 | RestMVC make all the models, controllers, and RestErrors junk available to you via the following: 195 | 196 | * restMVC.Models[] - all your models are available here by name, such as: var personModel = restMVC.Models['person']; 197 | * restMVC.Controllers[] - all your controllers are available here by name, such as: var personController = restMVC.Controllers['person']; 198 | * restMVC.RestError - the RestError infrastructure is available to you here for customization. 199 | * restMVC.BaseRestError - use this to extend the default error handling and create new types of RestErrors (based of standard http status codes of course) 200 | * restMVC.ErrorMapper - this defines what the error handler does when it gets passed a particular error. 201 | * restMVC.ErrorHandler - this is used by the Express app to handle the errors, actually you have to wire this up by doing: app.error(restMVC.ErrorHandler); 202 | * restMVC.Initialize(app, mongoose) - This is what kicks off everything, called by you in your app.js file. 203 | * restMVC.BaseController - This is the base controller that all others are created from, exposed for testing purposes. 204 | * restMVC.RegisterRoutes(app, controller) - Registers the default routes for the default controller, exposed for testing purposes. 205 | 206 | ## License 207 | 208 | (The MIT License) 209 | 210 | Copyright (c) 2011 Keith Larsen 211 | 212 | 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: 213 | 214 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 215 | 216 | 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. --------------------------------------------------------------------------------