From b8915e7bb84b54fac23572b358a9f8448434b577 Mon Sep 17 00:00:00 2001 From: Typicode Date: Sat, 30 Nov 2013 14:42:14 +0100 Subject: [PATCH] First commit --- .gitignore | 1 + LICENSE | 20 ++++ README.md | 179 +++++++++++++++++++++++++++++++++++ bin/cli.js | 48 ++++++++++ package.json | 52 ++++++++++ public/images/logo.png | Bin 0 -> 2723 bytes public/index.html | 79 ++++++++++++++++ public/stylesheets/style.css | 43 +++++++++ routes/read-only.js | 71 ++++++++++++++ routes/read-write.js | 79 ++++++++++++++++ server.js | 95 +++++++++++++++++++ test/fixture.js | 18 ++++ test/read-only.js | 138 +++++++++++++++++++++++++++ test/read-write.js | 130 +++++++++++++++++++++++++ test/static.js | 31 ++++++ utils/db-mixins.js | 86 +++++++++++++++++ utils/logger.js | 10 ++ utils/utils.js | 11 +++ 18 files changed, 1091 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 bin/cli.js create mode 100644 package.json create mode 100644 public/images/logo.png create mode 100644 public/index.html create mode 100644 public/stylesheets/style.css create mode 100644 routes/read-only.js create mode 100644 routes/read-write.js create mode 100644 server.js create mode 100644 test/fixture.js create mode 100644 test/read-only.js create mode 100644 test/read-write.js create mode 100644 test/static.js create mode 100644 utils/db-mixins.js create mode 100644 utils/logger.js create mode 100644 utils/utils.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d7d922a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 typicode + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccf5ee8 --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +![](http://i.imgur.com/dLeJmw6.png) + +__JSON Server__ + +> Give it a JSON or JS seed file and it will serve it through REST routes. + +_Created with :heart: for front-end developers who need a flexible back-end for quick prototyping and mocking._ + + +## Examples + +### Command line interface + +```bash +$ cat db.json +{ + posts: [ + { id: 1, body: 'foo' } + ] +} +$ json-server --file db.json +$ curl -i http://localhost:3000/posts/1 +``` + +### Node module + +```javascript +var server = require('json-server'); + +var db = { + posts: [ + {id: 1, body: 'foo'} + ] +} + +server.run(db); +``` + + +## Why? + +* Supports __GET__ but also __POST__, __PUT__, __DELETE__ and even __PATCH__ requests +* Can be used from anywhere through __cross domain__ requests (JSONP or CORS) +* Can load remote JSON files +* Can be deployed on Nodejitsu, Heroku, ... + + +## Installation + +```bash +$ npm install -g json-server +``` + + +## Usage + +### Command line interface + +```bash +json-server --help + + Usage: json-server [options] + + Options: + + -h, --help output usage information + -V, --version output the version number + -f --file load db from a js or json file + -u --url load db from a URL + -p --port [port] server port + --read-only read only mode +``` + +JSON Server can load JSON from multiple sources: + +```bash +$ json-server --file db.json +$ json-server --file seed.js +$ json-server --url http://example.com/db.json +``` + +And be run in read-only mode (useful if deployed on a public server): + +```bash +$ json-server --file db.json --read-only +``` + +#### Input + +Here's 2 examples showing how to format JSON or JS seed file: + +* __db.json__ +```javascript +{ + posts: [ + { id: 1, body: 'foo'}, + { id: 2, body: 'bar'} + ], + comments: [ + { id: 1, body: 'baz', postId: 1} + { id: 2, body: 'qux', postId: 2} + ] +} +``` + +* __seed.js__ +```javascript +exports.run = function() { + var data = {}; + + data.posts = []; + data.posts.push({id: 1, body: 'foo'}); + //... + + return data; +} +``` + +JSON Server expects JS files to export a ```run``` method that returns an object. + +Seed files are useful if you need to programmaticaly create a lot of data. + + +### Node module + +#### run(db, [options]) + +```javascript +var server = require('json-server'), + db = require('./seed').run(); + +var options = { port: 4000, readOnly: true }; + +server.run(db, options); +``` + +By default, ```port``` is set to 3000 and ```readOnly``` to false. + +## Routes + +``` +GET /:resource +GET /:resource?attr=&attr=& +GET /:parent/:parentId/:resource +GET /:resource/:id +POST /:resource +PUT /:resource/:id +PATCH /:resource/:id +DEL /:resource/:id +``` + +For routes usage information, have a look at [JSONPlaceholder](https://github.com/typicode/jsonplaceholder) code examples. + +``` +GET /db +``` + +Returns database state. + + +``` +GET / +``` + +Returns default index file or content of ./public/index.html (useful if you need to set a custom home page). + + +## Support + +If you like the project, please tell your friends about it, star it or give feedback :) It's very much appreciated! + +For project updates or to get in touch, [@typicode](http://twitter.com/typicde). You can also send me a mail. + +## Test + +```bash +$ npm install +$ npm test +``` \ No newline at end of file diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..4dae86b --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +var program = require('commander'), + request = require('superagent'), + url = require('url'), + fs = require('fs'), + server = require('../server'), + logger = require('../utils/logger'), + options = {}; + +function loadFile(file, cb) { + var path = process.cwd() + '/' + file, + db; + + if (/\.json$/.test(file)) db = require(path); + if (/\.js$/.test(file)) db = require(path).run(); + + cb(db); +} + +function loadURL(url, cb) { + logger.info('Fetching ' + url + '...') + request + .get(url) + .end(function(error, res) { + if (error) { + logger.error(error); + } else { + cb(JSON.parse(res)); + } + }); +} + +function onDatabaseLoaded(db) { + server.run(db, options); +} + +program + .version('0.1.0') + .option('-f --file ', 'load db from a js or json file') + .option('-u --url ', 'load db from a URL') + .option('-p --port [port]', 'server port') + .option('--read-only', 'read only mode') + .parse(process.argv); + +if (program.port) options.port = program.port; +if (program.readOnly) options.readOnly = true; +if (program.file) loadFile(program.file, onDatabaseLoaded); +if (program.url) loadURL(program.url, onDatabaseLoaded); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..1b2ce6a --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "json-server", + "version": "0.1.0", + "description": "Serves JSON file through REST routes", + "main": "server.js", + "bin": "./bin/cli.js", + "directories": { + "test": "test" + }, + "dependencies": { + "commander": "~2.0.0", + "cors": "~2.1.0", + "express": "~3.4.4", + "logan": "~0.0.2", + "request": "~2.27.0", + "underscore": "~1.5.2", + "underscore.inflections": "~0.2.1" + }, + "devDependencies": { + "superagent": "~0.15.7", + "supertest": "~0.8.1" + }, + "scripts": { + "test": "mocha -R spec test", + "start": "node server.js" + }, + "repository": { + "type": "git", + "url": "git://github.com/typicode/json-server.git" + }, + "keywords": [ + "JSON", + "server", + "fake", + "REST", + "API", + "prototyping", + "mock", + "mocking", + "test", + "testing", + "rest", + "data", + "dummy" + ], + "author": "Typicode ", + "license": "MIT", + "bugs": { + "url": "https://github.com/typicode/json-server/issues" + }, + "homepage": "https://github.com/typicode/json-server" +} diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8fd3cc15cf312dc3e8b13c5af161ba64a7c2579e GIT binary patch literal 2723 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 literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..8f8b172 --- /dev/null +++ b/public/index.html @@ -0,0 +1,79 @@ + + + JSON Server + + + + +
+ + +

+ Congrats! You're successfully running JSON Server. +

+ +
+ +

Resources

+

+

    loading, please wait...
+

+ +

+ To view database current state: +

    +
  • + db +
  • +
+

+ +

Requests

+

+ Resources can be accessed in various ways. +

+

+ JSON Server supports: +

    +
  • GET, POST, PUT, PATCH, DESTROY and OPTIONS verbs.
  • +
  • JSONP or CORS cross domain requests.
  • +
+

+ +

Documentation

+

+ View + README + on GitHub. +

+ +

Issues

+

Please go + here. +

+ +
+ +

+ To customize this page, just create a ./public/index.html file. +

+
+ + + + + + + \ No newline at end of file diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..1464e6c --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,43 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +h1 { + font-weight: lighter; +} + +a { + /*color: #00B7FF;*/ + color: #3D72D1; +} + +hr { + border: 1px #EEE solid; +} + +a.logo { + display: block; + text-align: center; + margin-bottom: 50px; +} + +.alert-success { + font-size: 18px; +} + +/*.logo { + background: #00B7FF; + color: white; + font-size: 48px; + width: 100px; + height: 100px; + text-align: center; + vertical-align: middle; + line-height: 90px; + border-radius: 100px; + transform:rotate(90deg); + -ms-transform:rotate(90deg); + -webkit-transform:rotate(90deg); + margin-bottom: 50px; +}*/ \ No newline at end of file diff --git a/routes/read-only.js b/routes/read-only.js new file mode 100644 index 0000000..da0b2f7 --- /dev/null +++ b/routes/read-only.js @@ -0,0 +1,71 @@ +var _ = require('underscore'), + utils = require('../utils/utils'), + db = {}; + +_.mixin(require('../utils/db-mixins')); + +exports.setDatabase = function(object) { + db = object; +} + +exports.database = function(req, res) { + res.jsonp(db) +} + +// GET /:resource?attr=&attr= +exports.list = function(req, res) { + var collection = db[req.params.resource], + properties = {}, + result; + + Object.keys(req.query).forEach(function (key) { + var value = req.query[key]; + properties[key] = utils.toNative(value); + }); + + if (_(properties).isEmpty()) { + result = collection; + } else { + result = _(collection).where(properties); + } + + res.jsonp(result); +} + +// GET /:parent/:parentId/:resource +exports.nestedList = function(req, res) { + var properties = {}, + resource; + + // Set parentID + properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId; + + // Filter using parentID + resource = _.where(db[req.params.resource], properties); + + res.jsonp(resource); +} + +// GET /:resource/:id +exports.show = function(req, res) { + var resource = _.get(db, req.params.resource, +req.params.id); + + res.jsonp(resource); +} + +exports.create = function(req, res) { + req.body.id = Math.round(new Date().getTime() / 1000); + res.jsonp(req.body); +} + +exports.update = function(req, res) { + var resource = _.get(db, req.params.resource, +req.params.id), + clonedResource = _.clone(resource), + result = _.extend(clonedResource, req.body); + + res.jsonp(result); +} + +exports.destroy = function(req, res) { + res.send(204) +} diff --git a/routes/read-write.js b/routes/read-write.js new file mode 100644 index 0000000..4634e11 --- /dev/null +++ b/routes/read-write.js @@ -0,0 +1,79 @@ +var _ = require('underscore'), + utils = require('../utils/utils'), + db = {}; + +_.mixin(require('../utils/db-mixins')); + +exports.setDatabase = function(object) { + db = object; +} + +exports.database = function(req, res) { + res.jsonp(db) +} + +// GET /:resource?attr=&attr= +exports.list = function(req, res) { + var collection = db[req.params.resource], + properties = {}, + result; + + Object.keys(req.query).forEach(function (key) { + var value = req.query[key]; + properties[key] = utils.toNative(value); + }); + + if (_(properties).isEmpty()) { + result = collection; + } else { + result = _(collection).where(properties); + } + + res.jsonp(result); +} + +// GET /:parent/:parentId/:resource +exports.nestedList = function(req, res) { + var properties = {}, + resource; + + // Set parentID + properties[req.params.parent.slice(0, - 1) + 'Id'] = +req.params.parentId; + + // Filter using parentID + resource = _.where(db[req.params.resource], properties); + + res.jsonp(resource); +} + +// GET /:resource/:id +exports.show = function(req, res) { + var resource = _.get(db, req.params.resource, +req.params.id); + + res.jsonp(resource); +} + +// POST /:resource +exports.create = function(req, res) { + var resource = _.create(db, req.params.resource, req.body); + + res.jsonp(resource); +} + +// PUT /:resource/:id +// PATCH /:resource/:id +exports.update = function(req, res) { + _.update(db, req.params.resource, +req.params.id, req.body); + var resource = _.get(db, req.params.resource, +req.params.id); + + res.jsonp(resource); +} + +// DELETE /:resource/:id +exports.destroy = function(req, res) { + _.remove(db, req.params.resource, +req.params.id); + _.clean(db); + + res.send(204); +} + diff --git a/server.js b/server.js new file mode 100644 index 0000000..af39d89 --- /dev/null +++ b/server.js @@ -0,0 +1,95 @@ +var express = require('express'), + cors = require('cors'), + http = require('http'), + path = require('path'), + fs = require('fs'), + _ = require('underscore'), + logger = require('./utils/logger'); + + +var defaultOptions = { + port: process.env.PORT || 3000, + readOnly: false +} + +function createApp(db, options) { + // Create app + var app = express(), + options = options || {}, + routes; + + // Configure all environments + app.use(express.favicon()); + app.use(express.logger('dev')); + app.use(express.json()); + app.use(express.urlencoded()); + app.use(express.methodOverride()); + + + // Configure development + if ('development' == app.get('env')) { + app.use(express.errorHandler()); + } + + // Configure using options provided + app.set('port', options.port); + routes = options.readOnly ? './routes/read-only' : './routes/read-write'; + routes = require(routes); + + // Use default or user public directory + // Note: should be done before CORS and app.router setting + console.log(fs.existsSync(process.cwd() + '/public'), process.cwd() + '/public') + if (fs.existsSync(process.cwd() + '/public')) { + app.use(express.static(process.cwd() + '/public')); + } else { + app.use(express.static(path.join(__dirname, './public'))); + } + + // Enable CORS for everything + app.use(cors()); + app.options('*', cors()); + + // Set app.router + app.use(app.router); + + // Set API entry points + app.get('/db', routes.database) + app.get('/:resource', routes.list); + app.get('/:parent/:parentId/:resource', routes.nestedList); + app.get('/:resource/:id', routes.show); + app.post('/:resource', routes.create); + app.put('/:resource/:id', routes.update); + app.patch('/:resource/:id', routes.update); + app.del('/:resource/:id', routes.destroy); + + // Set database + routes.setDatabase(db); + + // And done! Ready to serve JSON! + return app; +} + +function run(db, options) { + options = _.defaults(options, defaultOptions); + + var app = createApp(db, options); + + if (_.isEmpty(db)) { + logger.error('No resources found!'); + } else { + logger.success('Available resources'); + for (var prop in db) { + logger.url(options.port, prop); + } + } + + http + .createServer(app) + .listen((options.port), function(){ + logger.success('Express server listening on port ' + options.port); + logger.success('Congrats! Open http://localhost:' + options.port); + }); +} + +exports.createApp = createApp; +exports.run = run; \ No newline at end of file diff --git a/test/fixture.js b/test/fixture.js new file mode 100644 index 0000000..453be2c --- /dev/null +++ b/test/fixture.js @@ -0,0 +1,18 @@ +// Small database to be used during tests +module.exports = function() { + var db = {}; + + db.posts = [ + {id: 1, body: 'foo'}, + {id: 2, body: 'bar'} + ] + + db.comments = [ + {id: 1, published: true, postId: 1}, + {id: 2, published: false, postId: 1}, + {id: 3, published: false, postId: 2}, + {id: 4, published: false, postId: 2}, + ] + + return db; +} \ No newline at end of file diff --git a/test/read-only.js b/test/read-only.js new file mode 100644 index 0000000..aca0e33 --- /dev/null +++ b/test/read-only.js @@ -0,0 +1,138 @@ +var request = require('supertest'), + assert = require('assert'), + server = require('../server'), + fixture = require('./fixture'), + db, + app; + +describe('Read only routes', function() { + + beforeEach(function() { + db = fixture(); + app = server.createApp(db, { readOnly: true }); + }); + + describe('GET /:resource', function() { + it('should respond with json and resources and corresponding resources', function(done) { + request(app) + .get('/posts') + .expect('Content-Type', /json/) + .expect(db.posts) + .expect(200, done); + }); + }); + + describe('GET /:resource?attr=&attr=', function() { + it('should respond with json and filter resources', function(done) { + request(app) + .get('/comments?postId=1&published=true') + .expect('Content-Type', /json/) + .expect([db.comments[0]]) + .expect(200, done); + }); + }); + + describe('GET /:parent/:parentId/:resource', function() { + it('should respond with json and corresponding nested resources', function(done) { + request(app) + .get('/posts/1/comments') + .expect('Content-Type', /json/) + .expect([db.comments[0], db.comments[1]]) + .expect(200, done); + }); + }); + + + describe('GET /:resource/:id', function() { + it('should respond with json and corresponding resource', function(done) { + request(app) + .get('/posts/1') + .expect('Content-Type', /json/) + .expect(db.posts[0]) + .expect(200, done); + }); + }); + + describe('GET /db', function() { + it('should respond with json and full database', function(done) { + request(app) + .get('/db') + .expect('Content-Type', /json/) + .expect(db) + .expect(200, done); + }); + }); + + describe('POST /:resource', function() { + it('should respond with fake json and not create a resource', function(done) { + request(app) + .post('/posts') + .send({body: '...'}) + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res){ + if (err) return done(err); + assert(res.body.hasOwnProperty('id')); + assert.equal(res.body.body, '...'); + assert.equal(db.posts.length, 2); + done() + }); + }); + }); + + describe('PUT /:resource/:id', function() { + it('should respond with fake json and not update resource', function(done) { + request(app) + .put('/posts/1') + .send({id: 999, body: '...'}) + .expect('Content-Type', /json/) + .expect({id: 999, body: '...'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err); + // Checking that first post wasn't updated + assert.deepEqual(db.posts[0], {id: 1, body: 'foo'}); + done() + }); + }); + }); + + describe('PATCH /:resource/:id', function() { + it('should respond with fake json and not update resource', function(done) { + request(app) + .patch('/posts/1') + .send({body: '...'}) + .expect('Content-Type', /json/) + .expect({id: 1, body: '...'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err); + // Checking that first post wasn't updated + assert.deepEqual(db.posts[0], {id: 1, body: 'foo'}); + done() + }); + }); + }); + + describe('DELETE /:resource/:id', function() { + it('should respond with empty data and not destroy resource', function(done) { + request(app) + .del('/posts/1') + .expect(204) + .end(function(err, res){ + if (err) return done(err); + assert.equal(db.posts.length, 2); + assert.equal(db.comments.length, 4); + done() + }); + }); + }); + + describe('OPTIONS /:resource/:id', function() { + it('should respond with empty data and not destroy resource', function(done) { + request(app) + .options('/posts/1') + .expect(204, done); + }); + }); +}); \ No newline at end of file diff --git a/test/read-write.js b/test/read-write.js new file mode 100644 index 0000000..388d4da --- /dev/null +++ b/test/read-write.js @@ -0,0 +1,130 @@ +var request = require('supertest'), + assert = require('assert'), + server = require('../server'), + routes = require('../routes/read-write'), + fixture = require('./fixture'), + db, + app; + +describe('Read write routes', function() { + + beforeEach(function() { + db = fixture(); + app = server.createApp(db); + }); + + describe('GET /:resource', function() { + it('should respond with json and corresponding resources', function(done) { + request(app) + .get('/posts') + .expect('Content-Type', /json/) + .expect(db.posts) + .expect(200, done); + }); + }); + + describe('GET /:resource?attr=&attr=', function() { + it('should respond with json and filter resources', function(done) { + request(app) + .get('/comments?postId=1&published=true') + .expect('Content-Type', /json/) + .expect([db.comments[0]]) + .expect(200, done); + }); + }); + + describe('GET /:parent/:parentId/:resource', function() { + it('should respond with json and corresponding nested resources', function(done) { + request(app) + .get('/posts/1/comments') + .expect('Content-Type', /json/) + .expect([ + db.comments[0], + db.comments[1] + ]) + .expect(200, done); + }); + }); + + describe('GET /:resource/:id', function() { + it('should respond with json and corresponding resource', function(done) { + request(app) + .get('/posts/1') + .expect('Content-Type', /json/) + .expect(db.posts[0]) + .expect(200, done); + }); + }); + + describe('GET /db', function() { + it('should respond with json and full database', function(done) { + request(app) + .get('/db') + .expect('Content-Type', /json/) + .expect(db) + .expect(200, done); + }); + }); + + describe('POST /:resource', function() { + it('should respond with json and create a resource', function(done) { + request(app) + .post('/posts') + .send({body: 'foo'}) + .expect('Content-Type', /json/) + .expect({id: 3, body: 'foo'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err); + assert.equal(db.posts.length, 3); + done(); + }); + }); + }); + + describe('PUT /:resource/:id', function() { + it('should respond with json and update resource', function(done) { + request(app) + .put('/posts/1') + .send({id: 1, body: 'foo'}) + .expect('Content-Type', /json/) + .expect({id: 1, body: 'foo'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err); + assert.deepEqual(db.posts[0], {id: 1, body: 'foo'}); + done(); + }); + }); + }); + + describe('PATCH /:resource/:id', function() { + it('should respond with json and update resource', function(done) { + request(app) + .patch('/posts/1') + .send({body: 'bar'}) + .expect('Content-Type', /json/) + .expect({id: 1, body: 'bar'}) + .expect(200) + .end(function(err, res){ + if (err) return done(err); + assert.deepEqual(db.posts[0], {id: 1, body: 'bar'}); + done(); + }); + }); + }); + + describe('DELETE /:resource/:id', function() { + it('should respond with empty data, destroy resource and dependent resources', function(done) { + request(app) + .del('/posts/1') + .expect(204) + .end(function(err, res){ + if (err) return done(err); + assert.equal(db.posts.length, 1); + assert.equal(db.comments.length, 2); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/static.js b/test/static.js new file mode 100644 index 0000000..f623d76 --- /dev/null +++ b/test/static.js @@ -0,0 +1,31 @@ +var request = require('supertest'), + assert = require('assert'), + server = require('../server'), + routes = require('../routes/read-write'), + app; + +describe('Static routes', function() { + + beforeEach(function() { + app = server.createApp({}, routes); + }); + + describe('GET /', function() { + it('should respond with html', function(done) { + request(app) + .get('/') + .expect('Content-Type', /html/) + .expect(200, done); + }); + }); + + describe('GET /stylesheets/style.css', function() { + it('should respond with css', function(done) { + request(app) + .get('/stylesheets/style.css') + .expect('Content-Type', /css/) + .expect(200, done); + }); + }); + +}); \ No newline at end of file diff --git a/utils/db-mixins.js b/utils/db-mixins.js new file mode 100644 index 0000000..d06a395 --- /dev/null +++ b/utils/db-mixins.js @@ -0,0 +1,86 @@ +(function(root) { + + var _ = root._ || require('underscore'); + + if (!root._) { + _.mixin(require('underscore.inflections')); + } + + function get(db, table, id) { + return _.find(db[table], function (row) { + return row.id === id + }); + } + + function exist(db, table, id) { + return !_.isUndefined(_.get(db, table, id)); + } + + function createId(db, table) { + if (_.isEmpty(db[table])) { + return 1; + } else { + return _.max(db[table], function(row) { + return row.id; + }).id + 1; + } + } + + function create(db, table, obj) { + var clone = _.clone(obj); + + if (_.isUndefined(clone.id)) clone.id = _.createId(db, table); + + db[table].push(clone); + + return clone; + } + + function update(db, table, id, attrs) { + var row = get(db, table, id), + updatedRow = _.extend(row, attrs), + index = _.indexOf(db[table], row); + + db[table][index] = updatedRow; + } + + function clean(db) { + var toBeRemoved = []; + + _(db).each(function(table, tableName) { + _(table).each(function(row) { + _(row).each(function(value, key) { + if (/Id$/.test(key)) { + var reference = _.pluralize(key.slice(0, - 2)); + if (!_.exist(db, reference, row[key])) { + toBeRemoved.push({ + tableName: tableName, + id: row.id + }); + } + } + }); + }); + }); + + _(toBeRemoved).each(function(row) { + _.remove(db, row.tableName, row.id); + }); + } + + function remove(db, table, id) { + var newTable = _.reject(db[table], function(row) { + return row.id === id; + }); + + db[table] = newTable; + } + + _.get = get; + _.exist = exist; + _.createId = createId; + _.create = create; + _.update = update; + _.clean = clean; + _.remove = remove; +})(this); \ No newline at end of file diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..3e2b10c --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,10 @@ +var logan = require('logan'); + +logan.set({ + error: ['%', 'red'], + success: ['%', 'green'], + info: ['%', 'grey'], + url: [' http://localhost:%/'.grey + '%'.cyan, '.'] +}) + +module.exports = logan \ No newline at end of file diff --git a/utils/utils.js b/utils/utils.js new file mode 100644 index 0000000..c70615f --- /dev/null +++ b/utils/utils.js @@ -0,0 +1,11 @@ +function toNative(value) { + if (value === 'true' || value === 'false') { + return value === 'true'; + } else if (!isNaN(+value)) { + return +value; + } else { + return value; + } +} + +exports.toNative = toNative; \ No newline at end of file