router rebuild

This commit is contained in:
Ritchie Martori
2012-05-25 09:08:12 -07:00
parent efe7dcb3f9
commit 462915b2e7
14 changed files with 591 additions and 179 deletions

View File

@@ -23,19 +23,21 @@ realtime bridge
- be client agnostic (usable from browser, servers, mobile apps, etc)
- be a good web citizen / support native web best practices
- be a good node citizen / use node best practices
- can be hosted by modern cloud platforms
- support extension through node modules and npm
- follow the [ways of node](http://www.mikealrogers.com/posts/the-way-of-node.html)
- follow the [12 factor methodology](http://www.12factor.net/)
## modules
**core**
- resource - base module, mountable at a URL
- router - determines a resource based on a URL
- collection - allows users to query, save, and delete JSON objects
- users collection - allows users to register / login / logout
- ??? - allow users to listen and emit events
- file system - allows users to upload / download / stream files
- router - determines a resource based on a URL
- emitter - global message bus / event emitter
- sessions - manage authentication of users
- resources - internal access to mounted resources
@@ -54,6 +56,12 @@ realtime bridge
Consult the [documentation](http://deployd.github.com/deployd) or contact `ritchie at deployd com`.
## changelog
### 0.5
- removed `property.optional` in favor of `property.required`
## license
Copyright 2012 deployd, llc

12
TODO.md Normal file
View File

@@ -0,0 +1,12 @@
# high
# medium
- dates should be numbers in the database so they can be sorted
- dates should be JS date objects in events
- dates should be transfered as JSON dates over http
# low
- _id should be id

106
lib/db.js Normal file
View File

@@ -0,0 +1,106 @@
var db = module.exports = {}
, util = require('util')
, EventEmitter = require('events').EventEmitter
, mongodb = require('mongodb');
db.connect = function (options, fn) {
var db = new Db(options);
db.open(fn);
return db;
}
function Db(options) {
this.options = options;
}
util.inherits(Db, EventEmitter);
db.Db = Db;
Db.prototype.createStore = function (namespace) {
return new Store(namespace, this);
}
Db.prototype.open = function (fn) {
var self = this
, mdb = new mongodb.Db(this.options.name, new mongodb.Server(this.options.host, this.options.port));
self.connecting = true;
self._mdb = mdb;
mdb.open(function (err) {
self.connecting = false;
if(err) return fn && fn(err);
self.emit('connected');
})
}
function Store(namespace, db) {
this.namespace = namespace;
this._db = db;
}
function collection(store, fn) {
var db = store._db
, mdb = db._mdb;
function execute(err) {
if(err) return fn(err);
mdb.collection(store.namespace, function (err, collection) {
if(err || !collection) return fn(err || Error('collection was undefined or an error occured'));
fn(null, collection)
});
}
if(db.connecting) {
db.once('connected', execute);
} else {
execute();
}
};
Store.prototype.insert = function (object, fn) {
collection(this, function (err, col) {
col.insert(object, function (err, result) {
if(Array.isArray(result) && !Array.isArray(object)) result = result[0];
fn(err, result);
});
});
};
Store.prototype.find = function (query, fn) {
if(typeof query == 'function') {
fn = query;
query = {};
}
collection(this, function (err, col) {
col.find(query).toArray(function (err, arr) {
if(arr.length === 0) arr = undefined;
fn(err, arr);
});
});
};
Store.prototype.first = function (query, fn) {
collection(this, function (err, col) {
col.findOne(query, fn);
});
};
Store.prototype.update = function (query, object, fn) {
collection(this, function (err, col) {
col.update(query, object, fn);
});
};
Store.prototype.remove = function (query, fn) {
collection(this, function (err, col) {
col.remove(query, fn);
});
};
Store.prototype.rename = function (namespace, fn) {
collection(this, function (err, col) {
col.rename(namespace, fn);
});
};

16
lib/http.js Normal file
View File

@@ -0,0 +1,16 @@
/**
* Dependencies
*/
var http = require('http')
, Router = require('./router')
, db = require('./db')
, resources = require('./resources');
function Server(options) {
var server = http.createServer(options.host, options.port)
, db = db.connect(options.db)
, router = new Router(resources.build(db.createStore('resources')));
server.on('request', router.route);
}

View File

@@ -0,0 +1,9 @@
function Resource() {
}
/**
* Export
*/
module.exports = Resource;

44
lib/resources.js Normal file
View File

@@ -0,0 +1,44 @@
/**
* Dependencies
*/
var fs = require('fs');
exports.build = function (store) {
var definitions = define(__dirname);
store.find = function (query, fn) {
if(typeof query == 'function') {
fn = query;
query = {};
}
store.__proto__.find.call(store, query, function (err, resources) {
if(err) return fn(err);
// build resources from data
var result = [];
if(resources) {
resources.forEach(function (resource) {
result.push(new definitions[resource.type](resource));
})
}
fn(err, result);
})
}
return store;
}
function define(dir) {
var definitions = {};
fs.readdirSync(dir + '/resources').forEach(function (path) {
if(~path.indexOf('.js')) {
var constructor = require(__dirname + '/resources/' + path);
definitions[constructor.name] = constructor;
}
});
return definitions;
}

View File

@@ -0,0 +1,78 @@
/**
* Dependencies
*/
var validation = require('validation');
function Collection(settings, req, res) {
this.req = req;
this.res = res;
this.properties = settings.properties;
}
Collection.prototype.authorize = function () {
var req = this.req
, query = req.query;
if(req.user && req.user.root) return;
if(!(query && query._id)) {
switch(req.method) {
case 'PUT':
case 'DELETE':
return 'An _id must be included when modifying a Collection.';
break;
}
}
}
Collection.prototype.validate = function (body) {
var keys = Object.keys(this.properties)
, props = this.properties
, errors = {};
keys.forEach(function (key) {
var prop = props[key]
, val = body[key]
, type = prop.type || 'string';
if(validation.exists(val)) {
if(!validation.isType(val, type)) {
errors[key] = 'must be a ' + type;
}
} else if(prop.required) {
errors[key] = 'is required';
}
});
if(Object.keys(errors).length) return errors;
}
Collection.prototype.sanitize = function (body) {
var sanitized = {}
, props = this.properties
, keys = Object.keys(props);
keys.forEach(function (key) {
var prop = props[key]
, expected = prop.type
, val = body[key]
, actual = typeof val;
// skip properties that dont exist
if(!prop) return;
if(expected == actual) {
sanitized[key] = val;
} else if(expected == 'number' && actual == 'string') {
sanitized[key] = parseInt(val);
}
});
return sanitized;
}
/**
* Export
*/
module.exports = Collection;

25
lib/router.js Normal file
View File

@@ -0,0 +1,25 @@
/**
* Dependencies
*/
var db = require('./db');
function Router(resources) {
this.resources = resources;
}
Router.prototype.route = function (req, res) {
var router = this;
this.resources.find(function (err, resources) {
var handler;
resources.forEach(function (resource) {
if(resource.handle(req, res)) {
handler = resource;
return false;
}
if(!handler) res.error('resource not found', 404);
});
})
};

View File

@@ -1,66 +0,0 @@
/**
* Validates the given value against the specified type.
*
* @param {Object} value
* @param {string} type
* @return {Boolean} true if the validation passes, otherwise false
* @api public
*
* Examples:
*
* validation.isType('foobar', 'string')
* validation.isType(7, 'number')
* validation.isType(true, 'boolean')
* validation.isType(new Date().getTime(), 'date')
* validation.isType({}, 'object')
* validation.isType([], 'array')
*
*/
exports.isType = function (value, type) {
if(typeof type != 'string') throw Error('bad arguments when calling validation.isValid');
switch(type) {
case 'date':
if(!value) return false;
try {
value = new Date(value);
} catch(e) {
return false;
}
return value instanceof Date && isFinite(value);
break;
case 'object':
return !!(value && toString.call(value) === '[object Object]' && 'isPrototypeOf' in value);
break;
case 'array':
return Array.isArray(value);
break;
case 'number':
if(value === Infinity || isNaN(value)) return false;
default:
return typeof value == type;
break;
}
}
/**
* Validates the given value exists.
*
* @param {Object} value
* @return {Boolean} true if the value exists, otherwise false
* @api public
*
* Examples:
*
* validation.exists('foobar') // true
* validation.exists(0) // true
* validation.exists(null) // false
* validation.exists(undefined) // false
*
*/
exports.exists = function (value) {
return !!(value || value === 0);
}

View File

@@ -0,0 +1,114 @@
var Collection = require('../lib/resources/collection');
describe('collection', function(){
describe('.validate(req)', function(){
it('should validate the request', function() {
var r = new Collection({
properties: {
title: {
type: 'string'
}
}
});
var errs = r.validate({title: 'foobar'});
expect(errs).to.not.exist;
})
it('should fail to validate the invalid request', function() {
var r = new Collection({
properties: {
title: {
type: 'string'
}
}
});
var errs = r.validate({title: 7});
expect(errs).to.eql({'title': 'must be a string'});
})
it('should fail to validate the invalid request with multiple errors', function() {
var r = new Collection({
properties: {
title: {
type: 'string',
required: true
},
age: {
type: 'number',
required: true
},
created: {
type: 'date'
}
}
});
var errs = r.validate({title: 7, created: 'foo'});
expect(errs).to.eql({title: 'must be a string', age: 'is required', created: 'must be a date'});
})
})
describe('.sanitize(body)', function(){
it('should remove properties outside the schema', function() {
var r = new Collection({
properties: {
title: {
type: 'string'
}
}
});
var sanitized = r.sanitize({foo: 7, bar: 8, title: 'foo'});
expect(sanitized.foo).to.not.exist;
expect(sanitized.bar).to.not.exist;
expect(sanitized.title).to.equal('foo');
})
it('should convert int strings to numbers', function() {
var r = new Collection({
properties: {
age: {
type: 'number'
}
}
});
var sanitized = r.sanitize({age: '22'});
expect(sanitized.age).to.equal(22);
})
})
describe('.authorize()', function(){
it('should allow users to GET Collections', function(done) {
freq('/foo', {}, function (req, res) {
var r = new Collection({}, req, res);
var errs = r.authorize();
done(errs);
})
})
it('should not allow users to PUT Collections without an _id', function(done) {
freq('/foo', {method: 'PUT'}, function (req, res) {
var r = new Collection({}, req, res);
var errs = r.authorize();
expect(errs).to.exist;
done();
})
})
it('should not allow users to DELETE Collections without an _id', function(done) {
freq('/foo', {method: 'DELETE'}, function (req, res) {
var r = new Collection({}, req, res);
var errs = r.authorize();
expect(errs).to.exist;
done();
})
})
})
})

View File

@@ -0,0 +1,132 @@
var db = require('../lib/db')
, TEST_DB = {name: 'test-db', host: 'localhost', port: 27017}
, tester = db.connect(TEST_DB)
, store = tester.createStore('test-store');
beforeEach(function(done){
store.remove(function () {
store.find(function (err, result) {
expect(err).to.not.exist;
expect(result).to.not.exist;
done(err);
})
})
})
describe('db', function(){
describe('.connect(options)', function(){
it('should connect to the database', function(done) {
var tester = db.connect(TEST_DB);
tester.on('connected', function () {
done();
});
})
})
})
describe('store', function(){
describe('.find(query, fn)', function(){
it('should not find anything when the store is empty', function(done) {
store.find(function (err, empty) {
expect(empty).to.not.exist;
done(err);
})
})
it('should pass the query to the underline database', function(done) {
store.insert([{i:1},{i:2},{i:3}], function () {
store.find({i: {$lt: 3}}, function (err, result) {
expect(result).to.exist;
expect(result).to.have.length(2);
done(err);
})
})
})
})
describe('.remove(query, fn)', function(){
it('should remove all the objects that match the query', function(done) {
store.insert([{i:1},{i:2},{i:3}], function () {
store.remove({i: {$lt: 3}}, function (err, result) {
expect(result).to.not.exist;
store.find(function (err, result) {
expect(result).to.have.length(1);
done(err);
})
})
})
})
it('should remove all the objects', function(done) {
store.insert([{i:1},{i:2},{i:3}], function () {
store.remove(function (err, result) {
expect(result).to.not.exist;
store.find(function (err, result) {
expect(result).to.not.exist;
done(err);
})
})
})
})
})
describe('.insert(namespace, object, fn)', function(){
it('should insert the given object into the namespace', function(done) {
store.insert({testing: 123}, function (err, result) {
expect(result._id).to.exist;
expect(result.testing).to.equal(123);
done();
})
})
it('should insert the given array into the namespace', function(done) {
store.insert([{a:1}, {b:2}], function (err, result) {
expect(Array.isArray(result)).to.equal(true);
expect(result[0]._id).to.exist;
expect(result[0].a).to.equal(1);
expect(result[1]._id).to.exist;
expect(result[1].b).to.equal(2);
expect(result).to.have.length(2);
done(err);
})
})
})
describe('.update(query, updates, fn)', function(){
it('should update only the properties provided', function(done) {
store.insert({foo: 'bar'}, function (err, result) {
expect(err).to.not.exist;
var query = {_id: result._id};
store.update(query, {foo: 'baz'}, function (err) {
expect(err).to.not.exist;
store.first(query, function (err, result) {
expect(result.foo).to.equal('baz');
done(err);
})
})
})
})
})
describe('.rename(namespace, fn)', function(){
it('should rename the underlying database representation of the store', function(done) {
store.insert([{i:1},{i:2},{i:3}], function () {
store.rename('foo-store', function () {
store.find({i: {$lt: 3}}, function (err, result) {
expect(result).to.exist;
expect(result).to.have.length(2);
store.rename('test-store', function () {
store.find({i: {$lt: 3}}, function (err, result) {
expect(result).to.exist;
expect(result).to.have.length(2);
done(err);
})
})
})
})
})
})
})
})

View File

@@ -0,0 +1,24 @@
var resources = require('../lib/resources')
, db = require('../lib/db').connect({name: 'test-db', host: 'localhost', port: 27017})
, store = db.createStore('resources')
, testCollection = {type: 'Collection', path: '/my-objects', properties: {title: {type: 'string'}}}
, Collection = require('../lib/resources/collection');
beforeEach(function(done){
store.remove(function (err) {
store.insert(testCollection, done)
})
})
describe('resources', function(){
describe('.build(store)', function(){
it('should return a set of resource instances', function(done) {
resources.build(store).find(function (err, resources) {
expect(resources).to.have.length(1);
expect(resources[0].properties).to.be.a('object');
expect(resources[0] instanceof Collection).to.equal(true);
done(err);
})
})
})
})

View File

@@ -1 +1,21 @@
expect = require('chai').expect;
/**
* Dependencies
*/
expect = require('chai').expect;
request = require('request');
http = require('http');
TEST_DB = {name: 'test-db', host: 'localhost', port: 27017};
// request mock
freq = function(url, options, fn) {
options.url = 'http://localhost:7777' + url;
var s = http.createServer(function (req, res) {
fn(req, res);
s.close();
})
.listen(7777)
.on('listening', function () {
request(options);
})
}

View File

@@ -1,110 +0,0 @@
var validation = require('../lib/validation');
describe('validation', function() {
describe('.isValid(value, description)', function () {
function example(type, value, valid) {
var v = validation.isType(value, type);
if(v != valid) {
console.log(type, value, valid, v);
}
expect(v).to.equal(valid);
}
it('should validate the given property as a number', function() {
example('number', 10, true);
example('number', 1, true);
example('number', 100000000000000000, true);
example('number', -10000000000000000, true);
example('number', Infinity, false);
example('number', '10', false);
example('number', '10.0', false);
example('number', "10", false);
example('number', new Date(), false);
example('number', {}, false);
example('number', null, false);
example('number', undefined, false);
example('number', /hello/, false);
example('number', NaN, false);
})
it('should validate the given property as a string', function() {
example('string', 'test', true);
example('string', "another test", true);
example('string', 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', true);
example('string', '<html>', true);
example('string', 1, false);
example('string', {}, false);
example('string', null, false);
example('string', undefined, false);
example('string', new Date(), false);
example('string', function() {}, false);
example('string', NaN, false);
})
it('should validate the given property as a date', function() {
example('date', new Date(), true);
example('date', new Date().toString(), true);
example('date', new Date().getTime(), true);
example('date', 'blah', false);
example('date', /regex/, false);
example('date', function() {}, false);
example('date', null, false);
example('date', undefined, false);
example('date', NaN, false);
})
it('should validate the given property as a boolean', function() {
example('boolean', true, true);
example('boolean', false, true);
example('boolean', 0, false);
example('boolean', 1, false);
example('boolean', 'true', false);
example('boolean', 'false', false);
example('boolean', null, false);
})
it('should validate the given property as an object', function() {
example('object', {}, true);
example('object', {length: 7}, true);
example('object', {foo: 'bar'}, true);
example('object', [], false);
example('object', '{}', false);
example('object', function() {}, false);
example('object', undefined, false);
example('object', null, false);
example('object', NaN, false);
example('object', 'foo', false);
example('object', /test/, false);
example('object', Infinity, false);
example('object', Array, false);
})
it('should validate the given property as an array', function() {
example('array', [], true);
example('array', [1,2,3], true);
example('array', {length: 7}, false);
example('array', {}, false);
example('array', arguments, false);
})
it('should validate the given property exists', function() {
function required(value, exists) {
var e = validation.exists(value);
expect(e).to.equal(exists);
}
required('foobar', true);
required(function() {}, true);
required(-1, true);
required(0.000000000000000000000001, true);
required(/foo/, true);
required(null, false);
required(undefined, false);
required(NaN, false);
required(NaN, false);
required(0, true);
})
})
})