mirror of
https://github.com/zhigang1992/deployd.git
synced 2026-05-15 17:47:51 +08:00
added socket session support
This commit is contained in:
17
TODO.md
17
TODO.md
@@ -1,7 +1,10 @@
|
||||
# high
|
||||
- repl
|
||||
- [x] events should support io
|
||||
- event io should be wrappable in `set(key, fn)` so it only runs when `key` is selected
|
||||
|
||||
- server init
|
||||
- [x] var server = new Server(config);
|
||||
- [x] server.defineResource({path: '/todos', ...})
|
||||
- config
|
||||
- $ dpd create hello-world
|
||||
- session w/ socket.io connection
|
||||
- user collection
|
||||
- file system / file store
|
||||
@@ -9,8 +12,7 @@
|
||||
- json body parsing
|
||||
- resource crud / resource http api
|
||||
- dashboard / dev integration
|
||||
- client lib / client project
|
||||
- integration test port
|
||||
- http client
|
||||
- cli
|
||||
- docs
|
||||
- separate modules / projects
|
||||
@@ -18,13 +20,16 @@
|
||||
# medium
|
||||
|
||||
- travis ci
|
||||
- repl
|
||||
- socket client
|
||||
- dates should be sortable
|
||||
- dates should be JS date objects in events
|
||||
- dates should be transfered as JSON dates over http
|
||||
- event io should be wrappable in `set(key, fn)` so it only runs when `key` is selected
|
||||
|
||||
# low
|
||||
|
||||
- _id should be id
|
||||
- [x] _id should be id
|
||||
- /__dashboard should be /dashboard
|
||||
- /resources should be /__resources
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ ClientStore.prototype.first = function (query, fn) {
|
||||
* db
|
||||
* .connect({host: 'localhost', port: 27015, name: 'test'})
|
||||
* .createStore('testing-store')
|
||||
* .update({_id: '<an object id>'}, fn)
|
||||
* .update({id: '<an object id>'}, fn)
|
||||
*
|
||||
* @param {Object} query
|
||||
* @param {Object} object
|
||||
@@ -124,7 +124,7 @@ ClientStore.prototype.update = function (query, object, fn) {
|
||||
* db
|
||||
* .connect({host: 'localhost', port: 27015, name: 'test'})
|
||||
* .createStore('testing-store')
|
||||
* .remove({_id: '<an object id>'}, fn)
|
||||
* .remove({id: '<an object id>'}, fn)
|
||||
*
|
||||
* @param {Object} query
|
||||
* @param {Function} callback(err, obj)
|
||||
|
||||
104
lib/db.js
104
lib/db.js
@@ -1,7 +1,8 @@
|
||||
var db = module.exports = {}
|
||||
, util = require('util')
|
||||
, EventEmitter = require('events').EventEmitter
|
||||
, mongodb = require('mongodb');
|
||||
, mongodb = require('mongodb')
|
||||
, uuid = require('./util/uuid');
|
||||
|
||||
/**
|
||||
* Create a new database connection with the given options. You can start making
|
||||
@@ -71,7 +72,7 @@ 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.connecting = true;
|
||||
self._mdb = mdb;
|
||||
mdb.open(function (err) {
|
||||
self.connecting = false;
|
||||
@@ -80,6 +81,17 @@ Db.prototype.open = function (fn) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the underlying database.
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @api private
|
||||
*/
|
||||
|
||||
Db.prototype.drop = function (fn) {
|
||||
this._mdb.dropDatabase(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new database store (eg. a collection).
|
||||
*
|
||||
@@ -110,6 +122,7 @@ function Store(namespace, db) {
|
||||
this.namespace = namespace;
|
||||
this._db = db;
|
||||
}
|
||||
module.exports.Store = Store;
|
||||
|
||||
function collection(store, fn) {
|
||||
var db = store._db
|
||||
@@ -121,7 +134,7 @@ function collection(store, fn) {
|
||||
mdb.collection(store.namespace, function (err, collection) {
|
||||
if(err || !collection) return fn(err || Error('collection was undefined or an error occured'));
|
||||
|
||||
fn(null, collection)
|
||||
fn(null, collection);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -132,6 +145,49 @@ function collection(store, fn) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Change public IDs to private IDs.
|
||||
*
|
||||
* IDs are generated with a psuedo random number generator.
|
||||
* 24 hexidecimal chars, ~2 trillion combinations.
|
||||
*
|
||||
* @param {Object} object
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
Store.prototype.identify = function (object) {
|
||||
if(!object) return;
|
||||
if(typeof object != 'object') throw new Error('identify requires an object');
|
||||
var store = this;
|
||||
function set(object) {
|
||||
if(object._id) {
|
||||
object.id = object._id;
|
||||
delete object._id;
|
||||
} else {
|
||||
var u = object.id || store.createUniqueIdentifier();
|
||||
object._id = u;
|
||||
delete object.id;
|
||||
}
|
||||
}
|
||||
if(Array.isArray(object)) {
|
||||
object.forEach(set);
|
||||
} else {
|
||||
set(object);
|
||||
}
|
||||
return object;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a unique identifier. Override this is derrived stores
|
||||
* to change the way IDs are generated.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
Store.prototype.createUniqueIdentifier = function() {
|
||||
return uuid.create();
|
||||
};
|
||||
|
||||
/**
|
||||
* Insert an object into the store.
|
||||
*
|
||||
@@ -147,10 +203,14 @@ function collection(store, fn) {
|
||||
*/
|
||||
|
||||
Store.prototype.insert = function (object, fn) {
|
||||
var store = this;
|
||||
this.identify(object);
|
||||
collection(this, function (err, col) {
|
||||
col.insert(object, function (err, result) {
|
||||
if(Array.isArray(result) && !Array.isArray(object)) result = result[0];
|
||||
fn(err, result);
|
||||
if(Array.isArray(result) && !Array.isArray(object)) {
|
||||
result = result[0];
|
||||
}
|
||||
fn(err, store.identify(result));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -170,6 +230,7 @@ Store.prototype.insert = function (object, fn) {
|
||||
*/
|
||||
|
||||
Store.prototype.find = function (query, fn) {
|
||||
var store = this;
|
||||
if(typeof query == 'function') {
|
||||
fn = query;
|
||||
query = {};
|
||||
@@ -177,7 +238,7 @@ Store.prototype.find = function (query, fn) {
|
||||
collection(this, function (err, col) {
|
||||
col.find(query).toArray(function (err, arr) {
|
||||
if(arr.length === 0) arr = undefined;
|
||||
fn(err, arr);
|
||||
fn(err, store.identify(arr));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -197,8 +258,12 @@ Store.prototype.find = function (query, fn) {
|
||||
*/
|
||||
|
||||
Store.prototype.first = function (query, fn) {
|
||||
this.identify(query);
|
||||
var store = this;
|
||||
collection(this, function (err, col) {
|
||||
col.findOne(query, fn);
|
||||
col.findOne(query, function (err, result) {
|
||||
fn(err, store.identify(result));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -210,7 +275,7 @@ Store.prototype.first = function (query, fn) {
|
||||
* db
|
||||
* .connect({host: 'localhost', port: 27015, name: 'test'})
|
||||
* .createStore('testing-store')
|
||||
* .update({_id: '<an object id>'}, fn)
|
||||
* .update({id: '<an object id>'}, fn)
|
||||
*
|
||||
* @param {Object} query
|
||||
* @param {Object} object
|
||||
@@ -218,8 +283,16 @@ Store.prototype.first = function (query, fn) {
|
||||
*/
|
||||
|
||||
Store.prototype.update = function (query, object, fn) {
|
||||
var store = this;
|
||||
if(typeof query == 'string') query = {id: query};
|
||||
if(typeof query != 'object') throw new Error('update requires a query object or string id');
|
||||
if(query.id) store.identify(query);
|
||||
|
||||
collection(this, function (err, col) {
|
||||
col.update(query, object, fn);
|
||||
col.update(query, object, function(err) {
|
||||
store.identify(query);
|
||||
fn(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -231,13 +304,22 @@ Store.prototype.update = function (query, object, fn) {
|
||||
* db
|
||||
* .connect({host: 'localhost', port: 27015, name: 'test'})
|
||||
* .createStore('testing-store')
|
||||
* .remove({_id: '<an object id>'}, fn)
|
||||
* .remove({id: '<an object id>'}, fn)
|
||||
*
|
||||
* @param {Object} query
|
||||
* @param {Function} callback(err, obj)
|
||||
*/
|
||||
|
||||
Store.prototype.remove = function (query, fn) {
|
||||
var store = this;
|
||||
if(typeof query === 'string') query = {id: query};
|
||||
if(typeof query == 'function') {
|
||||
fn = query;
|
||||
query = {};
|
||||
}
|
||||
if(query.id) {
|
||||
store.identify(query);
|
||||
}
|
||||
collection(this, function (err, col) {
|
||||
col.remove(query, fn);
|
||||
});
|
||||
@@ -258,7 +340,9 @@ Store.prototype.remove = function (query, fn) {
|
||||
*/
|
||||
|
||||
Store.prototype.rename = function (namespace, fn) {
|
||||
var store = this;
|
||||
collection(this, function (err, col) {
|
||||
store.namespace = namespace;
|
||||
col.rename(namespace, fn);
|
||||
});
|
||||
};
|
||||
|
||||
31
lib/http.js
31
lib/http.js
@@ -1,31 +0,0 @@
|
||||
var http = require('http')
|
||||
, Router = require('./router')
|
||||
, db = require('./db')
|
||||
, resources = require('./resources');
|
||||
|
||||
/**
|
||||
* Create an http server with the given options and create a `Router` to handle its requests.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `db` the database connection info
|
||||
* - `host` the server's hostname
|
||||
* - `port` the server's port
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var server = new Server({port: 3000, db: {host: 'localhost', port: 27015, name: 'my-db'}});
|
||||
*
|
||||
* server.listen();
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {HttpServer}
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ Collection.prototype.remove = function (session, query, fn) {
|
||||
var collection = this
|
||||
, store = this.store;
|
||||
|
||||
if(!(query && query._id)) return fn('You must include a query with an _id when deleting an object from a collection.');
|
||||
if(!(query && query.id)) return fn('You must include a query with an id when deleting an object from a collection.');
|
||||
store.find(query, function (err) {
|
||||
if(err) return fn(err);
|
||||
collection.execListener('Delete', session, query, null, function (err) {
|
||||
@@ -313,14 +313,14 @@ Collection.prototype.save = function (session, item, query, fn) {
|
||||
|
||||
if(!item) return fn('You must include an object when saving or updating.');
|
||||
|
||||
// handle _id on either body or query
|
||||
if(item._id) {
|
||||
query._id = item._id;
|
||||
delete item._id;
|
||||
// handle id on either body or query
|
||||
if(item.id) {
|
||||
query.id = item.id;
|
||||
delete item.id;
|
||||
}
|
||||
|
||||
// handle upsert
|
||||
store[query._id ? 'update' : 'insert'].apply(store, query._id ? [query, item, fn] : [item, fn]);
|
||||
store[query.id ? 'update' : 'insert'].apply(store, query.id ? [query, item, fn] : [item, fn]);
|
||||
}
|
||||
|
||||
module.exports = Collection;
|
||||
@@ -16,7 +16,7 @@ function Router(resources) {
|
||||
|
||||
/**
|
||||
* Route requests to resources with matching root paths.
|
||||
* Generate a `ctx` object andhand it to the resource, along with the `res` by calling its `resource.handle(ctx, res, next)` method.
|
||||
* Generate a `ctx` object and hand it to the resource, along with the `res` by calling its `resource.handle(ctx, next)` method.
|
||||
* If a resource calls `next()`, move on to the next resource.
|
||||
*
|
||||
* If all matching resources call next(), or if the router does not find a resource, respond with `404`.
|
||||
@@ -29,16 +29,19 @@ function Router(resources) {
|
||||
Router.prototype.route = function (req, res) {
|
||||
var router = this
|
||||
, url = req.url
|
||||
, resources = this.matchResources(url);
|
||||
, resources = this.matchResources(url)
|
||||
, i = 0;
|
||||
|
||||
//TODO: Handle edge case where next() is called more than once
|
||||
function nextResource() {
|
||||
var resource = resources.shift()
|
||||
var resource = resources[i++]
|
||||
, ctx;
|
||||
|
||||
if (resource) {
|
||||
ctx = new Context(resource, req, res);
|
||||
resource.handle(ctx, nextResource);
|
||||
process.nextTick(function () {
|
||||
resource.handle(ctx, nextResource);
|
||||
});
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end("Not Found");
|
||||
|
||||
165
lib/server.js
Normal file
165
lib/server.js
Normal file
@@ -0,0 +1,165 @@
|
||||
var http = require('http')
|
||||
, Router = require('./router')
|
||||
, db = require('./db')
|
||||
, util = require('util')
|
||||
, resources = require('./resources')
|
||||
, Resource = require('./resource')
|
||||
, SessionStore = require('./session').SessionStore
|
||||
, fs = require('fs')
|
||||
, io = require('socket.io')
|
||||
, setupReqRes = require('./util/http').setup;
|
||||
|
||||
function extend(origin, add) {
|
||||
// don't do anything if add isn't an object
|
||||
if (!add || typeof add !== 'object') return origin;
|
||||
|
||||
var keys = Object.keys(add);
|
||||
var i = keys.length;
|
||||
while (i--) {
|
||||
origin[keys[i]] = add[keys[i]];
|
||||
}
|
||||
return origin;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an http server with the given options and create a `Router` to handle its requests.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `db` the database connection info
|
||||
* - `host` the server's hostname
|
||||
* - `port` the server's port
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var server = new Server({port: 3000, db: {host: 'localhost', port: 27015, name: 'my-db'}});
|
||||
*
|
||||
* server.listen();
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {HttpServer}
|
||||
*/
|
||||
|
||||
function Server(options) {
|
||||
http.Server.call(this);
|
||||
|
||||
// defaults
|
||||
this.options = options = extend({
|
||||
port: 2403,
|
||||
host: 'localhost',
|
||||
db: {port: 27017, host: '127.0.0.1', name: 'deployd'}
|
||||
}, options);
|
||||
|
||||
// an object to map a server to its stores
|
||||
this.stores = {};
|
||||
|
||||
// back all memory stores with a db
|
||||
this.db = db.connect(options.db);
|
||||
|
||||
// persist resources in a store
|
||||
var resourceStore = this.resources = resources.build(this.createStore('resources'));
|
||||
|
||||
// use socket io for a session based realtime channel
|
||||
this.sockets = io.listen(this).sockets;
|
||||
|
||||
// persist sessions in a store
|
||||
var sessionStore = this.sessions = new SessionStore('sessions', this.db, this.sockets);
|
||||
|
||||
this.on('request', function (req, res) {
|
||||
// add utilites to req and res
|
||||
setupReqRes(req, res);
|
||||
|
||||
sessionStore.createSession(function(err, session) {
|
||||
if(err) {
|
||||
res.statusCode = 500;
|
||||
res.end('error when creating session ' + err);
|
||||
} else {
|
||||
// (re)set the session id
|
||||
req.cookies.set('sid', session.sid);
|
||||
|
||||
req.session = session;
|
||||
resourceStore.find(function (err, resources) {
|
||||
if(err) {
|
||||
res.statusCode = 500;
|
||||
res.end('error when finding resources ' + err);
|
||||
} else {
|
||||
var router = new Router(resources);
|
||||
router.route(req, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
util.inherits(Server, http.Server);
|
||||
|
||||
|
||||
/**
|
||||
* Start listening for incoming connections.
|
||||
*
|
||||
* @return {Server} for chaining
|
||||
*/
|
||||
|
||||
Server.prototype.listen = function(port, host) {
|
||||
return http.Server.prototype.listen.call(this, port || this.options.port, host || this.options.host);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new `Store` for persisting data using the database info that was passed to the server when it was created.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // Create a new server
|
||||
* var server = new Server({port: 3000, db: {host: 'localhost', port: 27015, name: 'my-db'}});
|
||||
*
|
||||
* // Attach a store to the server
|
||||
* var todos = server.createStore('todos');
|
||||
*
|
||||
* // Use the store to CRUD data
|
||||
* todos.insert({name: 'go to the store', done: true}, ...); // see `Store` for more info
|
||||
*
|
||||
* @param {String} namespace
|
||||
* @return {Store}
|
||||
*/
|
||||
|
||||
Server.prototype.createStore = function(namespace) {
|
||||
return (this.stores[namespace] = this.db.createStore(namespace));
|
||||
};
|
||||
|
||||
/**
|
||||
* Define or update a resource based on the given description.
|
||||
*
|
||||
*
|
||||
* @param {Object} description
|
||||
* @param {Function} callback(err)
|
||||
* @return {Resource}
|
||||
*/
|
||||
|
||||
Server.prototype.defineResource = function(description, fn) {
|
||||
var resources = this.resources;
|
||||
resources.first({path: description.path}, function (err, res) {
|
||||
if(err) return fn(err);
|
||||
if(res && res.id) {
|
||||
resources.update({id: description.id}, description, fn);
|
||||
} else {
|
||||
resources.insert(description, fn);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a single resource.
|
||||
*
|
||||
*
|
||||
* @param {String} path
|
||||
* @return {Resource}
|
||||
*/
|
||||
|
||||
Server.prototype.getResource = function(path, fn) {
|
||||
var resource;
|
||||
|
||||
this.resources.first({path: path}, fn);
|
||||
};
|
||||
|
||||
module.exports = Server;
|
||||
212
lib/session.js
212
lib/session.js
@@ -1,27 +1,199 @@
|
||||
var Store = require('./db').Store
|
||||
, util = require('util')
|
||||
, uuid = require('./util/uuid')
|
||||
, Cookies = require('cookies')
|
||||
, EventEmitter = require('events').EventEmitter;
|
||||
|
||||
/**
|
||||
* Initialize a connection with a client (or user) that can be established
|
||||
* and torn down at a later time. An established communication session may
|
||||
* involve more than one message in each direction. This is implemented in
|
||||
* both HTTP and WebSockets. Each WebSocket connection is bound to a session.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var session = new Session(connection);
|
||||
*
|
||||
* session.on('connected', function () {
|
||||
* session.emit('hello world');
|
||||
* });
|
||||
*
|
||||
* A store for persisting sessions inbetween connection / disconnection.
|
||||
* Automatically creates session IDs on inserted objects.
|
||||
*/
|
||||
|
||||
function Session(connection, sid, store) {
|
||||
function SessionStore(namespace, db, sockets) {
|
||||
this.sockets = sockets;
|
||||
|
||||
// socket queue
|
||||
var socketQueue = this.socketQueue = new EventEmitter()
|
||||
, socketIndex = this.socketIndex = {};
|
||||
|
||||
// TODO - sockets.on('connection', ...) - map to a session id based on socket.handshake.headers
|
||||
sockets && sockets.on('connection', function (socket) {
|
||||
// NOTE: do not use set here ever, the `Cookies` api is meant to get a req, res
|
||||
// but we are just using it for a cookie parser
|
||||
var cookies = new Cookies(socket.handshake)
|
||||
, sid = cookies.get('sid');
|
||||
|
||||
if(sid) {
|
||||
// index sockets against their session id
|
||||
socketIndex[sid] = socket;
|
||||
socketQueue.emit(sid, socket);
|
||||
}
|
||||
})
|
||||
|
||||
Store.apply(this, arguments);
|
||||
}
|
||||
util.inherits(SessionStore, Store);
|
||||
exports.SessionStore = SessionStore;
|
||||
|
||||
SessionStore.prototype.createUniqueIdentifier = function() {
|
||||
return uuid.create(128);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new `Session` based on an optional `sid` (session id).
|
||||
*
|
||||
* @param {String} sid
|
||||
* @param {Function} callback(err, session)
|
||||
*/
|
||||
|
||||
SessionStore.prototype.createSession = function(sid, fn) {
|
||||
var socketIndex = this.socketIndex;
|
||||
|
||||
if(typeof sid == 'function') {
|
||||
fn = sid;
|
||||
sid = undefined;
|
||||
}
|
||||
if(sid) {
|
||||
this.find({id: sid}, function(err, s) {
|
||||
if(err || !s) return fn(err);
|
||||
fn(err, new Session(s, this, socketIndex[sid]));
|
||||
});
|
||||
} else {
|
||||
sid = this.createUniqueIdentifier();
|
||||
fn(null, new Session({id: sid}, this, socketIndex[sid]));
|
||||
this.insert({id: sid}, function(err, s) {
|
||||
if(err) console.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An in memory representation of a client or user connection that can be saved to disk.
|
||||
* Data will be passed around via a `Context` to resources.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var session = new Session({id: 'my-sid', new SessionStore('sessions', db)});
|
||||
*
|
||||
* session.set({uid: 'my-uid'}).save();
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {Store} store
|
||||
* @param {Socket} socket
|
||||
*/
|
||||
|
||||
function Session(data, store, socket) {
|
||||
this.data = data;
|
||||
if(data && data.id) this.sid = data.id;
|
||||
this.store = store;
|
||||
|
||||
// create faux socket, to queue any events until
|
||||
// a real socket is available
|
||||
var socketWrapper = this.socket = {
|
||||
on: function () {
|
||||
// if we have a real socket, use it
|
||||
if(this._socket) {
|
||||
this._socket.apply(this._socket, arguments);
|
||||
} else {
|
||||
// otherwise add to bind queue
|
||||
var queue = this._bindQueue = this._bindQueue || [];
|
||||
queue.push(arguments);
|
||||
}
|
||||
},
|
||||
emit: function () {
|
||||
// if we have a real socket, use it
|
||||
if(this._socket) {
|
||||
this._socket.apply(this._socket, arguments);
|
||||
} else {
|
||||
// otherwise add to bind queue
|
||||
var queue = this._emitQueue = this._bindQueue || [];
|
||||
queue.push(arguments);
|
||||
}
|
||||
},
|
||||
_socket: socket
|
||||
}
|
||||
|
||||
// resolve queue once a socket is ready
|
||||
store.socketQueue.once(this.sid, function (socket) {
|
||||
socketWrapper._socket = socket;
|
||||
// drain bind queue
|
||||
if(socketWrapper._bindQueue && socketWrapper._bindQueue.length) {
|
||||
socketWrapper._bindQueue.forEach(function (args) {
|
||||
socket.on.apply(socket, args);
|
||||
})
|
||||
}
|
||||
// drain emit queue
|
||||
if(socketWrapper._emitQueue && socketWrapper._emitQueue.length) {
|
||||
socketWrapper._emitQueue.forEach(function (args) {
|
||||
socket.emit.apply(socket, args);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Session.prototype.remove = function (fn) {
|
||||
|
||||
}
|
||||
/**
|
||||
* Set properties on the in memory representation of a session.
|
||||
*
|
||||
* @param {Object} changes
|
||||
* @return {Session} this for chaining
|
||||
*/
|
||||
|
||||
Session.prototype.save = function (fn) {
|
||||
|
||||
}
|
||||
Session.prototype.set = function(object) {
|
||||
var session = this
|
||||
, data = session.data || (session.data = {});
|
||||
|
||||
|
||||
Object.keys(object).forEach(function(key) {
|
||||
data[key] = object[key];
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the in memory representation of a session to its store.
|
||||
*
|
||||
* @param {Function} callback(err, data)
|
||||
* @return {Session} this for chaining
|
||||
*/
|
||||
|
||||
Session.prototype.save = function(fn) {
|
||||
var session = this
|
||||
, data = this.data
|
||||
, query = {id: data.id};
|
||||
|
||||
session.remove(function (err) {
|
||||
if(err) return fn(err);
|
||||
session.store.insert(data, function (err, res) {
|
||||
fn(err, res);
|
||||
});
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the session using the data in its store.
|
||||
*
|
||||
* @param {Function} callback(err, data)
|
||||
* @return {Session} this for chaining
|
||||
*/
|
||||
|
||||
Session.prototype.fetch = function(fn) {
|
||||
var session = this;
|
||||
this.store.first({id: this.data.id}, function (err, data) {
|
||||
session.set(data);
|
||||
fn(err, data);
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the session.
|
||||
*
|
||||
* @param {Function} callback(err, data)
|
||||
* @return {Session} this for chaining
|
||||
*/
|
||||
|
||||
Session.prototype.remove = function(fn) {
|
||||
this.store.remove({id: this.data.id}, fn);
|
||||
return this;
|
||||
};
|
||||
9
lib/util/http.js
Normal file
9
lib/util/http.js
Normal file
@@ -0,0 +1,9 @@
|
||||
var Cookies = require('cookies');
|
||||
|
||||
/**
|
||||
* A utility for setting up a request and response.
|
||||
*/
|
||||
|
||||
exports.setup = function(req, res) {
|
||||
req.cookies = res.cookies = new Cookies(req, res);
|
||||
}
|
||||
111
lib/util/uuid.js
Normal file
111
lib/util/uuid.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright (C) 2010 by Johannes Baagøe <baagoe@baagoe.org>
|
||||
//
|
||||
// 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.
|
||||
|
||||
function Alea() {
|
||||
function Mash() {
|
||||
var n = 0xefc8249d;
|
||||
|
||||
var mash = function(data) {
|
||||
data = data.toString();
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
n += data.charCodeAt(i);
|
||||
var h = 0.02519603282416938 * n;
|
||||
n = h >>> 0;
|
||||
h -= n;
|
||||
h *= n;
|
||||
n = h >>> 0;
|
||||
h -= n;
|
||||
n += h * 0x100000000; // 2^32
|
||||
}
|
||||
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
|
||||
};
|
||||
|
||||
mash.version = 'Mash 0.9';
|
||||
return mash;
|
||||
}
|
||||
|
||||
return (function (args) {
|
||||
var s0 = 0;
|
||||
var s1 = 0;
|
||||
var s2 = 0;
|
||||
var c = 1;
|
||||
|
||||
if (args.length == 0) {
|
||||
args = [+new Date];
|
||||
}
|
||||
var mash = Mash();
|
||||
s0 = mash(' ');
|
||||
s1 = mash(' ');
|
||||
s2 = mash(' ');
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
s0 -= mash(args[i]);
|
||||
if (s0 < 0) {
|
||||
s0 += 1;
|
||||
}
|
||||
s1 -= mash(args[i]);
|
||||
if (s1 < 0) {
|
||||
s1 += 1;
|
||||
}
|
||||
s2 -= mash(args[i]);
|
||||
if (s2 < 0) {
|
||||
s2 += 1;
|
||||
}
|
||||
}
|
||||
mash = null;
|
||||
|
||||
var random = function() {
|
||||
var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
|
||||
s0 = s1;
|
||||
s1 = s2;
|
||||
return s2 = t - (c = t | 0);
|
||||
};
|
||||
random.uint32 = function() {
|
||||
return random() * 0x100000000; // 2^32
|
||||
};
|
||||
random.fract53 = function() {
|
||||
return random() +
|
||||
(random() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53
|
||||
};
|
||||
random.version = 'Alea 0.9';
|
||||
random.args = args;
|
||||
return random;
|
||||
|
||||
} (Array.prototype.slice.call(arguments)));
|
||||
}
|
||||
|
||||
// instantiate RNG. use the default seed, which is current time.
|
||||
var random = new Alea();
|
||||
|
||||
// Modified RFC 4122 v4 UUID
|
||||
exports.create = function (length) {
|
||||
length = length || 16;
|
||||
var s = [];
|
||||
var hexDigits = "0123456789abcdef";
|
||||
for (var i = 0; i < length; i++) {
|
||||
s[i] = hexDigits.substr(Math.floor(random() * 0x10), 1);
|
||||
}
|
||||
s[length - 3] = hexDigits.substr((s[length - 3] & 0x3) | 0x8, 1);
|
||||
|
||||
// return the uuid
|
||||
return s.join('');
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
"async-eval": "0.1.1",
|
||||
"mongodb": "1.0.2",
|
||||
"request": "2.x.x",
|
||||
"commander": "0.6.0"
|
||||
"commander": "0.6.0",
|
||||
"cookies": ">= 0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "*",
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('collection', function(){
|
||||
|
||||
it('should handle PUT', function(done) {
|
||||
var testData = [{test: true}, {test: false}];
|
||||
example('PUT', '/foo', {test: {type: 'boolean'}}, {test: false, _id: 7}, null,
|
||||
example('PUT', '/foo', {test: {type: 'boolean'}}, {test: false, id: 7}, null,
|
||||
function (req, res, method, path, properties, body) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
},
|
||||
@@ -149,7 +149,7 @@ describe('collection', function(){
|
||||
})
|
||||
|
||||
it('should handle DELETE', function(done) {
|
||||
example('DELETE', '/foo', {test: {type: 'boolean'}}, null, {_id: 7},
|
||||
example('DELETE', '/foo', {test: {type: 'boolean'}}, null, {id: 7},
|
||||
function (req, res, method, path, properties, body) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
},
|
||||
@@ -177,16 +177,16 @@ describe('collection', function(){
|
||||
var widgets = db.connect(TEST_DB).createStore('widgets');
|
||||
|
||||
var c = new Collection({
|
||||
onGet: 'var item = this; widgets.insert({foo:"bar"}, function(err, widget) { item._id = widget._id })',
|
||||
onGet: 'var item = this; widgets.insert({foo:"bar"}, function(err, widget) { item.id = widget.id })',
|
||||
resources: {
|
||||
widgets: widgets
|
||||
}
|
||||
});
|
||||
|
||||
var items = [{_id: 1}, {_id: 1}, {_id: 1}];
|
||||
var items = [{id: 1}, {id: 1}, {id: 1}];
|
||||
c.execListener('Get', {}, {}, items, function (err, result) {
|
||||
for(var i = 0; i < items.length; i++) {
|
||||
expect(result[i]._id).to.not.equal(1);
|
||||
expect(result[i].id).to.not.equal(1);
|
||||
}
|
||||
done(err);
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
var db = require('../lib/db')
|
||||
, TEST_DB = {name: 'test-db', host: 'localhost', port: 27017}
|
||||
, tester = db.connect(TEST_DB)
|
||||
, store = tester.createStore('test-store');
|
||||
, store = tester.createStore('test-store')
|
||||
, Store = require('../lib/db').Store;
|
||||
|
||||
beforeEach(function(done){
|
||||
store.remove(function () {
|
||||
@@ -25,8 +26,28 @@ describe('db', function(){
|
||||
})
|
||||
})
|
||||
|
||||
describe('db', function(){
|
||||
// TODO: this takes forever, move to integration?
|
||||
// describe('.drop(fn)', function(){
|
||||
// it('should drop the database', function(done) {
|
||||
// var tester = db.connect(TEST_DB);
|
||||
// tester.on('connected', function () {
|
||||
// store.insert({foo: 'bar'}, function () {
|
||||
// tester.drop(function (err) {
|
||||
// expect(err).to.not.exist;
|
||||
// store.find(function (err, res) {
|
||||
// expect(res).to.not.exist;
|
||||
// done(err);
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
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) {
|
||||
@@ -35,16 +56,33 @@ describe('store', function(){
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass the query to the underline database', function(done) {
|
||||
it('should pass the query to the underlying database', function(done) {
|
||||
store.insert([{i:1},{i:2},{i:3}], function () {
|
||||
store.find({i: {$lt: 3}}, function (err, result) {
|
||||
expect(result).to.exist;
|
||||
result.forEach(function (obj) {
|
||||
expect(obj.id).to.be.a('string');
|
||||
});
|
||||
expect(result).to.have.length(2);
|
||||
done(err);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.identify(object)', function() {
|
||||
it('should add an _id to the object', function() {
|
||||
var object = {};
|
||||
store.identify(object);
|
||||
expect(object._id.length).to.equal(16);
|
||||
})
|
||||
|
||||
it('should change _id to id', function() {
|
||||
var object = {_id: 'aaaaaaaabbbbbbbb'};
|
||||
store.identify(object);
|
||||
expect(object.id.length).to.equal(16);
|
||||
})
|
||||
})
|
||||
|
||||
describe('.remove(query, fn)', function(){
|
||||
it('should remove all the objects that match the query', function(done) {
|
||||
@@ -75,7 +113,7 @@ describe('store', function(){
|
||||
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.id).to.exist;
|
||||
expect(result.testing).to.equal(123);
|
||||
done();
|
||||
})
|
||||
@@ -84,9 +122,9 @@ describe('store', function(){
|
||||
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].id).to.exist;
|
||||
expect(result[0].a).to.equal(1);
|
||||
expect(result[1]._id).to.exist;
|
||||
expect(result[1].id).to.exist;
|
||||
expect(result[1].b).to.equal(2);
|
||||
expect(result).to.have.length(2);
|
||||
done(err);
|
||||
@@ -98,7 +136,7 @@ describe('store', 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};
|
||||
var query = {id: result.id};
|
||||
store.update(query, {foo: 'baz'}, function (err) {
|
||||
expect(err).to.not.exist;
|
||||
store.first(query, function (err, result) {
|
||||
|
||||
56
test/server.unit.js
Normal file
56
test/server.unit.js
Normal file
@@ -0,0 +1,56 @@
|
||||
var Server = require('../lib/server')
|
||||
, Db = require('../lib/db').Db
|
||||
, Store = require('../lib/db').Store
|
||||
, Router = require('../lib/router');
|
||||
|
||||
describe('Server', function() {
|
||||
describe('.listen()', function() {
|
||||
it('should start a new deployd server', function(done) {
|
||||
var server = new Server()
|
||||
, defaultOptions = {
|
||||
port: 2403,
|
||||
host: 'localhost',
|
||||
db: {
|
||||
name: 'deployd',
|
||||
port: 27017,
|
||||
host: '127.0.0.1'
|
||||
}
|
||||
};
|
||||
|
||||
server.listen();
|
||||
expect(server.db instanceof Db).to.equal(true);
|
||||
expect(server.options).to.eql(defaultOptions);
|
||||
server.on('listening', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.createStore(namespace)', function() {
|
||||
it('should create a store with the given name', function() {
|
||||
var server = new Server()
|
||||
, store = server.createStore('foo');
|
||||
|
||||
expect(store instanceof Store).to.equal(true);
|
||||
expect(server.stores.foo).to.equal(store);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.defineResource(resource)', function() {
|
||||
it('should create a resource based on its path', function(done) {
|
||||
var server = new Server();
|
||||
|
||||
server.defineResource({
|
||||
path: '/todos',
|
||||
type: 'Collection',
|
||||
properties: {
|
||||
title: {type: 'string'},
|
||||
order: {type: 'number'},
|
||||
done: {type: 'boolean'}
|
||||
}
|
||||
}, function (err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
var SessionStore = require('../lib/session').SessionStore
|
||||
, db = require('../lib/db')
|
||||
, EventEmitter = require('events').EventEmitter;
|
||||
|
||||
describe('SessionStore', function() {
|
||||
it('should bind sockets to sessions', function() {
|
||||
var sockets = new EventEmitter()
|
||||
, store = new SessionStore('sessions', db.connect(TEST_DB), sockets)
|
||||
, fauxSocket = {
|
||||
handshake: {headers: {cookie: 'name=value; name2=value2; sid=123'}}
|
||||
};
|
||||
|
||||
sockets.emit('connection', fauxSocket);
|
||||
|
||||
expect(store.socketIndex['123']).to.equal(fauxSocket);
|
||||
});
|
||||
|
||||
describe('.createUniqueIdentifier()', function() {
|
||||
it('should create a session id', function() {
|
||||
var store = new SessionStore()
|
||||
, sid = store.createUniqueIdentifier();
|
||||
|
||||
expect(sid.length).to.equal(128);
|
||||
})
|
||||
})
|
||||
|
||||
describe('.createSession(fn)', function() {
|
||||
it('should create a session', function(done) {
|
||||
var store = new SessionStore('sessions', db.connect(TEST_DB))
|
||||
, sid = store.createUniqueIdentifier();
|
||||
|
||||
store.createSession(function (err, session) {
|
||||
expect(session.sid).to.have.length(128);
|
||||
done(err);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Session', function() {
|
||||
function createSession(fn) {
|
||||
var store = new SessionStore('sessions', db.connect(TEST_DB));
|
||||
|
||||
store.createSession(function (err, session) {
|
||||
expect(session.sid).to.have.length(128);
|
||||
fn(err, session);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
it('should make the socket available from the session', function(done) {
|
||||
var sockets = new EventEmitter()
|
||||
, store = new SessionStore('sessions', db.connect(TEST_DB), sockets);
|
||||
|
||||
store.createSession(function (err, session) {
|
||||
var fauxSocket = {handshake: { headers: {cookie: 'name=value; name2=value2; sid=' + session.sid} } };
|
||||
sockets.emit('connection', fauxSocket);
|
||||
expect(session.socket._socket).to.equal(fauxSocket);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should make sockets available even before they exist', function(done) {
|
||||
this.timeout(100);
|
||||
|
||||
var sockets = new EventEmitter()
|
||||
, fauxSocket = new EventEmitter()
|
||||
, store = new SessionStore('sessions', db.connect(TEST_DB), sockets);
|
||||
|
||||
store.createSession(function (err, session) {
|
||||
// generate faux headers
|
||||
fauxSocket.handshake = { headers: {cookie: 'name=value; name2=value2; sid=' + session.sid} };
|
||||
|
||||
// bind to an event even before a connection has been made
|
||||
session.socket.on('test', function (data) {
|
||||
expect(data).to.equal(123);
|
||||
done();
|
||||
});
|
||||
|
||||
sockets.emit('connection', fauxSocket);
|
||||
|
||||
fauxSocket.emit('test', 123);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.set(changes)', function() {
|
||||
it('should set the changes to a sessions data', function(done) {
|
||||
createSession(function (err, session) {
|
||||
session.set({foo: 'bar'});
|
||||
expect(session.data).to.eql({id: session.sid, foo: 'bar'});
|
||||
done(err);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.save(fn)', function() {
|
||||
it('should persist the session data in the store', function(done) {
|
||||
createSession(function (err, session) {
|
||||
session.set({foo: 'bar'}).save(function (err, data) {
|
||||
session.store.first({id: session.sid}, function (err, sdata) {
|
||||
expect(sdata.foo).to.equal('bar');
|
||||
done(err);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.remove(fn)', function() {
|
||||
it('should remove the session data from the store', function(done) {
|
||||
createSession(function (err, session) {
|
||||
session.set({foo: 'bar'}).save(function (err, data) {
|
||||
session.store.first({id: session.sid}, function (err, sdata) {
|
||||
expect(sdata.foo).to.equal('bar');
|
||||
session.remove(function () {
|
||||
session.store.first({id: session.sid}, function (err, sdata) {
|
||||
expect(sdata).to.not.exist;
|
||||
done(err);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.fetch(fn)', function() {
|
||||
it('should fetch the session data from the store', function(done) {
|
||||
createSession(function (err, session) {
|
||||
session.set({foo: 'bar'}).save(function (err, data) {
|
||||
session.store.first({id: session.sid}, function (err, sdata) {
|
||||
session.data = {id: session.sid, foo: 'not-bar'};
|
||||
session.fetch(function (err) {
|
||||
expect(session.data).to.eql({id: session.sid, foo: 'bar'});
|
||||
done(err);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,7 @@ expect = require('chai').expect;
|
||||
request = require('request');
|
||||
http = require('http');
|
||||
TEST_DB = {name: 'test-db', host: 'localhost', port: 27017};
|
||||
mongodb = require('mongodb');
|
||||
|
||||
// request mock
|
||||
var port = 7000;
|
||||
@@ -30,4 +31,15 @@ freq = function(url, options, fn, callback) {
|
||||
.on('listening', function () {
|
||||
request(options);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// before(function (done) {
|
||||
// var mdb = new mongodb.Db(TEST_DB.name, new mongodb.Server(TEST_DB.host, TEST_DB.port));
|
||||
|
||||
// mdb.open(function (err) {
|
||||
// mdb.dropDatabase(function (err) {
|
||||
// done(err);
|
||||
// mdb.close();
|
||||
// });
|
||||
// })
|
||||
// })
|
||||
14
test/util.unit.js
Normal file
14
test/util.unit.js
Normal file
@@ -0,0 +1,14 @@
|
||||
describe('uuid', function() {
|
||||
describe('.create()', function() {
|
||||
var uuid = require('../lib/util/uuid');
|
||||
var used = {};
|
||||
// max number of objects that must not conflict
|
||||
// total of about 2 trillion possible combinations
|
||||
var i = 1000; // replace this with a larger number to really test
|
||||
while(i--) {
|
||||
var next = uuid.create();
|
||||
if(used[next]) throw 'already used'
|
||||
used[next] = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user