From 8e22cdc1c1bf2fb0fdd8bdc8bcf68f0134a362dd Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 3 Oct 2012 15:19:25 -0700 Subject: [PATCH 01/17] updated history --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index 3a9f7db..16bddfb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,7 @@ ## 0.6.6 + - Added CORS support - Exposed the server object to modules as `process.server` - Fixed a rare bug where the first request after a login would not be authenticated - Fixed minor bug when loading only node modules From 02898ef5cca127fb77630c4e1929450101059e5f Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 3 Oct 2012 15:22:00 -0700 Subject: [PATCH 02/17] Release v0.6.6 --- docs/about.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/about.md b/docs/about.md index de42047..517f343 100644 --- a/docs/about.md +++ b/docs/about.md @@ -1,4 +1,4 @@ -# About Deployd Server +# About Deployd We call Deployd a **resource server**. A resource server is not a library, but a complete server that works out of the box, and can be customized to fit the needs of your app by adding resources. Resources are ready-made components that live at a URL and provide functionality to your client app. From 048e1ffd3099f4b1605d0a715aca173c3c04781e Mon Sep 17 00:00:00 2001 From: Boussekeyt Jules Date: Thu, 11 Oct 2012 19:36:47 +0300 Subject: [PATCH 03/17] Update docs/deploy.md --- docs/deploy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy.md b/docs/deploy.md index 461a3e5..5c5ef3e 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -26,7 +26,7 @@ To provide additional collaborators access to push new versions and access the d You can also deploy your app on your server, or on a cloud hosting service such as EC2 or Heroku. The server must support [Node.js](http://nodejs.org/). -Deployd also requires a [MongoDB] (http://www.mongodb.org/) database, which can be hosted on the same server or externally. +Deployd also requires a [MongoDB](http://www.mongodb.org/) database, which can be hosted on the same server or externally. If you have root shell access on the deployment server, you can install Deployd on it using the command `npm install -g deployd`. Otherwise, you will need to install Deployd as a dependency of your app itself using `npm install deployd` in the root directory of your app. From 121636d0d14ed6fec37c4b0991000e4339355b22 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 10:57:22 -0700 Subject: [PATCH 04/17] fixed internal-client documentation --- docs/module-api/internal-client.md | 4 ++-- lib/router.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/module-api/internal-client.md b/docs/module-api/internal-client.md index c52853a..d28428d 100644 --- a/docs/module-api/internal-client.md +++ b/docs/module-api/internal-client.md @@ -6,8 +6,8 @@ The `internal-client` module is responsible for building a server-side version o ## internalClient.build(server, [session], [stack]) - var internalClient = require('deployd/lib/internalClient'); - var dpd = internalClient.build(server, req.session, req.stack); + var internalClient = require('deployd/lib/internal-client'); + var dpd = internalClient.build(server); dpd.todos.get(function(data, err) { // Do something... diff --git a/lib/router.js b/lib/router.js index f81100c..c63b7b4 100644 --- a/lib/router.js +++ b/lib/router.js @@ -83,7 +83,7 @@ Router.prototype.route = function (req, res) { } else { debug('404 %s', req.url); res.statusCode = 404; - res.domain.emit('error', new Error('Resource not found')); + server.handleError(new Error('Resource not found'), req, res); } }); }); From 6dec442356e0a4a86f2e49caf2209529d4e0a36e Mon Sep 17 00:00:00 2001 From: Dallon Feldner Date: Fri, 19 Oct 2012 11:25:34 -0700 Subject: [PATCH 05/17] Changed root behavior --- lib/resources/collection/index.js | 24 +++---- lib/script.js | 7 +- test-app/public/test/collection.test.js | 85 ++++++++++++++++++++++++- test-app/resources/todos/post.js | 4 ++ 4 files changed, 100 insertions(+), 20 deletions(-) diff --git a/lib/resources/collection/index.js b/lib/resources/collection/index.js index 8af0b2a..988de7a 100644 --- a/lib/resources/collection/index.js +++ b/lib/resources/collection/index.js @@ -143,8 +143,9 @@ Collection.prototype.sanitizeQuery = function (query) { // skip properties that do not exist, but allow $ queries and id if(!prop && key.indexOf('$') !== 0 && key !== 'id') return; - // hack - $limitRecursion is not a mongo property so we'll get rid of it, too + // hack - $limitRecursion and $skipEvents are not mongo properties so we'll get rid of them, too if (key === '$limitRecursion') return; + if (key === '$skipEvents') return; if(expected == 'number' && actual == 'string') { sanitized[key] = parseFloat(val); @@ -316,9 +317,7 @@ Collection.prototype.find = function (ctx, fn) { errors[key] = val || true; }, hide: function(property) { - if (!session.isRoot) { - delete data[property]; - } + delete data[property]; }, 'this': data, data: data @@ -350,9 +349,7 @@ Collection.prototype.find = function (ctx, fn) { errors[key] = val || true; }, hide: function(property) { - if (!session.isRoot) { - delete data[property]; - } + delete data[property]; }, 'this': data, data: data @@ -453,6 +450,7 @@ Collection.prototype.save = function (ctx, fn) { function done(err, item) { errors = errors && {errors: errors}; + debug('errors: %j', err); fn(errors || err, item); } @@ -463,14 +461,10 @@ Collection.prototype.save = function (ctx, fn) { errors[key] = val || true; }, hide: function(property) { - if (!session.isRoot) { - delete item[property]; - } + delete item[property]; }, protect: function(property) { - if (!session.isRoot) { - delete item[property]; - } + delete item[property]; }, 'this': item, data: item @@ -671,8 +665,8 @@ Collection.prototype.execCommands = function (type, obj, commands) { }; Collection.prototype.shouldRunEvent = function(ev, ctx) { - var runEvents = ctx && ((ctx.body && ctx.body.$runEvents) || (ctx.query && ctx.query.$runEevents)) - , rootPrevent = ctx && ctx.session && ctx.session.isRoot && !runEvents; + var skipEvents = ctx && ((ctx.body && ctx.body.$skipEvents) || (ctx.query && ctx.query.$skipEvents)) + , rootPrevent = ctx && ctx.session && ctx.session.isRoot && skipEvents; return !rootPrevent && ev; }; diff --git a/lib/script.js b/lib/script.js index f52a260..4140edb 100644 --- a/lib/script.js +++ b/lib/script.js @@ -37,15 +37,14 @@ Script.prototype.run = function (ctx, domain, fn) { var scriptContext = { 'this': {}, cancel: function(msg, status) { - if (!req.isRoot) { - var err = {message: msg, statusCode: status}; - throw err; - } + var err = {message: msg, statusCode: status}; + throw err; }, me: session && session.user, console: console, query: ctx.query, internal: req && req.internal, + isRoot: req && req.session && req.session.isRoot, emit: function(collection, query, event, data) { if(arguments.length === 4) { session.emitToUsers(collection, query, event, data); diff --git a/test-app/public/test/collection.test.js b/test-app/public/test/collection.test.js index 79142a8..ddf7a26 100644 --- a/test-app/public/test/collection.test.js +++ b/test-app/public/test/collection.test.js @@ -1,3 +1,4 @@ +/*global _dpd:false */ describe('Collection', function() { describe('dpd.todos', function() { it('should exist', function() { @@ -67,6 +68,18 @@ describe('Collection', function() { done(); }); }); + it('should create a todo that exists in the store', function(done) { + dpd.todos.post({title: 'faux'}, function (todo, err) { + expect(todo.id.length).to.equal(16); + expect(todo.title).to.equal('faux'); + expect(err).to.not.exist; + dpd.todos.get(todo.id, function(res, err) { + if (err) return done(err); + expect(res.title).to.equal('faux'); + done(); + }); + }); + }); }); describe('.post({title: "notvalid"}, fn)', function() { @@ -381,7 +394,6 @@ describe('Collection', function() { todoId = res.id; dpd.todos.put(todoId, {message: "notvalidput"}, next); }).chain(function(next, res, err) { - console.log(res, err); expect(err).to.exist; expect(err.errors).to.exist; expect(err.errors.message).to.equal("message should not be notvalidput"); @@ -592,6 +604,77 @@ describe('Collection', function() { }); }); + describe('root', function() { + afterEach(function(done) { + _dpd.ajax.headers = {}; + cleanCollection(dpd.todos, done); + }); + + describe('dpd-ssh-key', function() { + beforeEach(function() { + _dpd.ajax.headers = { + 'dpd-ssh-key': true + }; + }); + + it('should detect root', function(done) { + chain(function(next) { + dpd.todos.post({title: 'valid'}, next); + }).chain(function(next, res, err) { + if (err) return done(err); + expect(res.isRoot).to.equal(true); + done(); + }); + }); + + it('should allow skipping events', function(done) { + chain(function(next) { + dpd.todos.post({title: 'notvalid', $skipEvents: true}, next); + }).chain(function(next, res, err) { + if (err) return done(err); + expect(res.title).to.equal('notvalid'); + done(); + }); + }); + + it('should allow skipping events on get', function(done) { + var id; + chain(function(next) { + dpd.todos.post({title: '$GET_CANCEL'}, next); + }).chain(function(next, res, err) { + if (err) return done(err); + id = res.id; + dpd.todos.get(id, {$skipEvents: true}, next); + }).chain(function(next, res, err) { + if (err) return done(err); + expect(res.title).to.equal("$GET_CANCEL"); + done(); + }); + }); + }); + + it('should not allow skipping events', function(done) { + chain(function(next) { + dpd.todos.post({title: 'notvalid', $skipEvents: true}, next); + }).chain(function(next, res, err) { + expect(err).to.exist; + expect(err.errors).to.exist; + done(); + }); + }); + + it('should not detect root', function(done) { + chain(function(next) { + dpd.todos.post({title: 'valid'}, next); + }).chain(function(next, res, err) { + if (err) return done(err); + expect(res.isRoot).to.not.exist; + done(); + }); + }); + + }); + describe('dpd.recursive', function() { beforeEach(function(done) { dpd.recursive.post({name: "dataception"}, function(res) { diff --git a/test-app/resources/todos/post.js b/test-app/resources/todos/post.js index d3db4f6..f864703 100644 --- a/test-app/resources/todos/post.js +++ b/test-app/resources/todos/post.js @@ -35,4 +35,8 @@ if (this.title === "$CANCEL_TEST") { if (this.title === "$INTERNAL_CANCEL_TEST") { if (!internal) cancel('internal cancel'); +} + +if (isRoot) { + this.isRoot = true; } \ No newline at end of file From 42280a8eb6b2397b53bc7cc4404b28de35be01fb Mon Sep 17 00:00:00 2001 From: Dallon Feldner Date: Fri, 19 Oct 2012 11:52:09 -0700 Subject: [PATCH 06/17] Updated documentation for new root behavior --- HISTORY.md | 1 + docs/module-api/script.md | 6 +++--- docs/reference/collection-events.md | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 4ec4338..f344da9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,7 @@ - Added new data editor - Fixed major bug where calling error() would not always cancel the request - Fixed bug where PUT would fail without an error if you provided a query + - Changed root behavior - no longer ignores cancel() in events ## 0.6.6 diff --git a/docs/module-api/script.md b/docs/module-api/script.md index c4c07b1..8912b77 100644 --- a/docs/module-api/script.md +++ b/docs/module-api/script.md @@ -61,7 +61,7 @@ If a callback is provided the script will be run in **async mode**. The callback * fn(err, script) -Load a new `script` at the given file `path`. Callback with an error if one occured or a new `Script` loaded from the contents of the file. +Load a new `script` at the given file `path`. Callback with an error if one occured or a new `Script` loaded from the contents of the file. ## Default Domain @@ -71,8 +71,6 @@ Scripts are executed with a default sandbox and set of domain functions. These a Throws an error that immediately stops the execution of a context and calls the callback passed to `script.run()` passing the error as the first argument. -`cancel()` does not have an effect if the current `Context.isRoot` or `Context.internal` is true. - ### emit([collection], [query], event, data) Stability: will change in 0.7 @@ -85,5 +83,7 @@ The default sandbox or global object in a `Script` comes with several other prop - `me` - the current user if one exists on the `Context` - `this` - an empty object if not overridden by the `domain` + - `internal` - a boolean property, true if this request has been initiated by another script + - `isRoot` - a boolean property, true if this request is authenticated as root (from the dashboard or a custom script) - `query` - the current `Context`'s query - `console` - support for `console.log()` and other `console` methods diff --git a/docs/reference/collection-events.md b/docs/reference/collection-events.md index a69ad8a..033d24c 100644 --- a/docs/reference/collection-events.md +++ b/docs/reference/collection-events.md @@ -161,6 +161,29 @@ Dpd.js will prevent recursive queries. This works by returning `null` from a `dp ] } + +### internal + +Equal to true if this request has been sent by another script. + + // Example: On GET /posts + // Posts with a parent are invisible, but are counted by their parent + if (this.parentId && !internal) cancel(); + + dpd.posts.get({parentId: this.id}, function(posts) { + this.childPosts = posts.length; + }); + +### isRoot + +Equal to true if this request has been authenticated as root (has the `dpd-ssh-key` header with the appropriate key) + + // Example: On PUT /users + // Protect reputation property - should only be calculated by a custom script. + + if (!isRoot) protect('reputation'); + + ### console.log() console.log([arguments]...) From ae3f4b1de6f540cfea4aa23608839b5f2186a423 Mon Sep 17 00:00:00 2001 From: Dallon Feldner Date: Fri, 19 Oct 2012 11:52:37 -0700 Subject: [PATCH 07/17] Updated data editor for new root behavior --- lib/resources/collection/dashboard/js/data.js | 29 ++++++++++++------- lib/resources/collection/index.js | 13 +++++---- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/lib/resources/collection/dashboard/js/data.js b/lib/resources/collection/dashboard/js/data.js index 181ecc6..3ceb66d 100644 --- a/lib/resources/collection/dashboard/js/data.js +++ b/lib/resources/collection/dashboard/js/data.js @@ -418,7 +418,7 @@ rowVm._deleteRow = function() { if (rowVm.id()) { var index = vm.data.indexOf(rowVm); - dpd(resource).del(rowVm.id(), function(res, err) { + dpd(resource).del(rowVm.id(), {$skipEvents: true}, function(res, err) { if (err) return showError("Error deleting row", err); vm.data.remove(rowVm); var data = vm.data(); @@ -462,15 +462,23 @@ rowVm[name](value); if (rowVm.id()) { - if (!options.dontNotify) { - createUndo("Changed " + vm.selectedProp().name, function() { - rowVm._saveProp(prop, lastValue, {dontNotify: true}); - vm.selectedProp(prop); - vm.selectedRow(rowVm); - }); - } + body[name] = value; - dpd(resource).put(rowVm.id(), body); + body.$skipEvents = true; + dpd(resource).put(rowVm.id(), body, function(res, err) { + if (err) { + showError("Error updating row", err); + rowVm[name](lastValue); + } + if (!options.dontNotify) { + createUndo("Changed " + vm.selectedProp().name, function() { + rowVm._saveProp(prop, lastValue, {dontNotify: true}); + vm.selectedProp(prop); + vm.selectedRow(rowVm); + }); + } + + }); } else if (!options.dontSave) { rowVm._save(); } @@ -516,6 +524,7 @@ } function postRow(data, fn) { + data.$skipEvents = true; dpd(resource).post(data, function(res, err) { if (err) return fn(null, err); vm.fadeInRows.push(getRowById(res.id)); //In case it's already there @@ -793,7 +802,7 @@ var begin = page - 1; begin = Math.max(0, Math.min(begin, getLastPage() - 1)); - dpd(resource).get({$skip: begin*PAGE_SIZE, $limit: PAGE_SIZE*3}, function(res, err) { + dpd(resource).get({$skip: begin*PAGE_SIZE, $limit: PAGE_SIZE*3, $skipEvents: true}, function(res, err) { if (err) return; vm.currentPage(page); ko.mapping.fromJS({data: res}, rowMapping, vm); diff --git a/lib/resources/collection/index.js b/lib/resources/collection/index.js index 988de7a..45003c7 100644 --- a/lib/resources/collection/index.js +++ b/lib/resources/collection/index.js @@ -378,17 +378,18 @@ Collection.prototype.remove = function (ctx, fn) { , store = this.store , session = ctx.session , query = ctx.query + , sanitizedQuery = this.sanitizeQuery(query) , errors; 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, result) { + store.find(sanitizedQuery, function (err, result) { if(err) { return fn(err); } function done(err) { if(err) return fn(err); - store.remove(query, fn); + store.remove(sanitizedQuery, fn); if(session.emitToAll) session.emitToAll(collection.name + ':changed'); } @@ -421,6 +422,7 @@ Collection.prototype.save = function (ctx, fn) { , store = this.store , session = ctx.session , item = ctx.body + , query = ctx.query || {} , client = ctx.dpd , errors; @@ -471,10 +473,11 @@ Collection.prototype.save = function (ctx, fn) { }; function put() { - var id = query.id; - store.first(query, function(err, obj) { + var id = query.id + , sanitizedQuery = collection.sanitizeQuery(query); + store.first(sanitizedQuery, function(err, obj) { if(!obj) { - if (Object.keys(query) === 1) { + if (Object.keys(sanitizedQuery) === 1) { return done(new Error("No object exists with that id")); } else { return done(new Error("No object exists that matches that query")); From bd6f5978f540ecdddcd2ae9869763c18eeda5f89 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 11:57:16 -0700 Subject: [PATCH 08/17] Release v0.6.7 --- ABOUT.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ABOUT.md b/ABOUT.md index dbb130b..b871c98 100644 --- a/ABOUT.md +++ b/ABOUT.md @@ -31,13 +31,6 @@ realtime resource server 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` - - changed `object._id` to `object.id` on all stored objects - ## license Copyright 2012 deployd, llc From 56a9348ad300e396cc29b57c76e874531498b124 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 12:08:32 -0700 Subject: [PATCH 09/17] reverted handleError --- lib/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/router.js b/lib/router.js index c63b7b4..f81100c 100644 --- a/lib/router.js +++ b/lib/router.js @@ -83,7 +83,7 @@ Router.prototype.route = function (req, res) { } else { debug('404 %s', req.url); res.statusCode = 404; - server.handleError(new Error('Resource not found'), req, res); + res.domain.emit('error', new Error('Resource not found')); } }); }); From 3b8e7e3bee2a0e78e4ca6544c18344ff6adb9403 Mon Sep 17 00:00:00 2001 From: Dallon Feldner Date: Fri, 19 Oct 2012 12:11:50 -0700 Subject: [PATCH 10/17] Disabled typeahead --- lib/resources/collection/dashboard/js/util/knockout-util.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/resources/collection/dashboard/js/util/knockout-util.js b/lib/resources/collection/dashboard/js/util/knockout-util.js index cdf89ad..40c0357 100644 --- a/lib/resources/collection/dashboard/js/util/knockout-util.js +++ b/lib/resources/collection/dashboard/js/util/knockout-util.js @@ -400,7 +400,11 @@ ko.bindingHandlers.aceEditorOptions = { ko.bindingHandlers.typeahead = { update: function(element, valueAccessor) { var value = ko.utils.unwrapObservable(valueAccessor()); - $(element).typeahead({source: value}); + if (window.typeahead) { + $(element).typeahead({source: value}); + } else { + $(element).typeahead({source: []}); + } } }; From 514a42f1364167271dac0cd858a5e07f7baa2d54 Mon Sep 17 00:00:00 2001 From: Dallon Feldner Date: Fri, 19 Oct 2012 12:22:50 -0700 Subject: [PATCH 11/17] Fixed data editor bug where ctrl-enter would edit a number or boolean in a modal. --- lib/resources/collection/dashboard/js/data.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/resources/collection/dashboard/js/data.js b/lib/resources/collection/dashboard/js/data.js index 3ceb66d..b4c3b2d 100644 --- a/lib/resources/collection/dashboard/js/data.js +++ b/lib/resources/collection/dashboard/js/data.js @@ -213,6 +213,11 @@ return true; }; + vm.edit.isEditableModal = function() { + var type = vm.selectedProp().type; + return vm.edit.isJson() || type === 'string'; + }; + vm.edit.hasChanged = ko.observable(false); vm.edit.editValue.subscribe(function(newVal) { vm.edit.hasChanged(true); @@ -656,7 +661,7 @@ return false; case 13: //enter - if (e.ctrlKey) { + if (e.ctrlKey && vm.edit.isEditableModal()) { vm.selectedRow()._editProp(vm.selectedProp(), {modal: true}); } else { vm.selectedRow()._editProp(vm.selectedProp()); From 1d259b71eb4f79399deb48053a1518fe8d191d98 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 17:14:42 -0700 Subject: [PATCH 12/17] fixed bugs preventing events from being emitted to users in certain connection states --- lib/session.js | 49 ++++++++++++++++++++++--------------------- test/sessions.unit.js | 13 ------------ 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/lib/session.js b/lib/session.js index 40f3063..9fb99b3 100644 --- a/lib/session.js +++ b/lib/session.js @@ -24,19 +24,20 @@ function SessionStore(namespace, db, sockets) { 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(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); - } - }); + if(sid) { + // index sockets against their session id + socketIndex[sid] = socket; + socketQueue.emit(sid, socket); + } + }); + } Store.apply(this, arguments); } @@ -104,43 +105,44 @@ SessionStore.prototype.createSession = function(sid, fn) { */ function Session(data, store, sockets, rawSockets) { + var sid; this.data = data; - if(data && data.id) this.sid = data.id; + if(data && data.id) this.sid = 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 () { + var s = sockets[sid]; // if we have a real socket, use it - if(this._socket) { - this._socket.apply(this._socket, arguments); + if(s) { + s.on.apply(s, arguments); } else { // otherwise add to bind queue var queue = this._bindQueue = this._bindQueue || []; queue.push(arguments); } }, - emit: function () { + emit: function (ev) { + var s = sockets[sid]; + // if we have a real socket, use it - if(this._socket) { - this._socket.emit.apply(this._socket, arguments); + if(s) { + s.emit.apply(s, arguments); } else { // otherwise add to bind queue var queue = this._emitQueue = this._bindQueue || []; queue.push(arguments); } - }, - _socket: sockets[this.sid] + } }; this.emitToUsers = function(collection, query, event, data) { collection.get(query, function(users) { var userSession; - // TODO: arguments in weird order if(users && users.id) { - userSession = userSessionIndex[err.id]; - console.info(userSession); + userSession = userSessionIndex[users.id]; if(userSession && userSession.socket) { userSession.socket.emit(event, data); } @@ -163,7 +165,6 @@ function Session(data, store, sockets, rawSockets) { // 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) { diff --git a/test/sessions.unit.js b/test/sessions.unit.js index 0dcb264..9c9ec36 100644 --- a/test/sessions.unit.js +++ b/test/sessions.unit.js @@ -47,19 +47,6 @@ describe('Session', function() { }); } - - it('should make the socket available from the session', function(done) { - var sockets = new EventEmitter() - , store = new SessionStore('sessions', db.create(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); From 07a891af4b30ba4b62f78eb0df89f6da87afaddb Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 17:20:22 -0700 Subject: [PATCH 13/17] updated history --- HISTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.md b/HISTORY.md index 18e9a77..e944888 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,7 @@ - Fixed major bug where calling error() would not always cancel the request - Fixed bug where PUT would fail without an error if you provided a query - Changed root behavior - no longer ignores cancel() in events + - Fixed bugs preventing events from being `emit()`ed to users in certain connection states ## 0.6.6 From 4589333a2dee109aacea0e05211aa2dcf8cbcacb Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 18:40:17 -0700 Subject: [PATCH 14/17] added more intelligent JSON parsing --- lib/util/http.js | 69 ++++++++++++++++++++++++++++++----------------- test/util.unit.js | 11 ++++++++ 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/lib/util/http.js b/lib/util/http.js index d2964a6..a1a9c98 100644 --- a/lib/util/http.js +++ b/lib/util/http.js @@ -14,33 +14,41 @@ exports.setup = function(req, res, next) { , handler = corser.create({supportsCredentials: true, methods: ALLOWED_METHODS, origins: origins}); handler(req, res, function () { - if (req.method === "OPTIONS") { - // End CORS preflight request. - res.writeHead(204); - res.end(); - } else { - var mime = req.headers['content-type'] || ''; - mime = mime.split(';')[0]; //Just in case there's multiple mime types, pick the first + req.cookies = res.cookies = new Cookies(req, res); + + if(~req.url.indexOf('?')) { + try { + req.query = parseQuery(req.url); + } catch (ex) { + res.setHeader('Content-Type', 'text/plain'); + res.statusCode = 400; + res.end('Failed to parse querystring: ' + ex); + return; + } + } + + switch(req.method) { + case 'OPTIONS': + // End CORS preflight request. + res.writeHead(204); + res.end(); + break; + case 'POST': + case 'PUT': + case 'DELETE': + var mime = req.headers['content-type'] || 'application/json'; + mime = mime.split(';')[0]; //Just in case there's multiple mime types, pick the first - req.cookies = res.cookies = new Cookies(req, res); - - if(~req.url.indexOf('?')) { - try { - req.query = parseQuery(req.url); - } catch (ex) { - res.setHeader('Content-Type', 'text/plain'); - res.statusCode = 400; - res.end('Failed to parse querystring: ' + ex); - return; + if(autoParse[mime]) { + autoParse[mime](req, res, mime, next); + } else { + if(req.headers['content-length']) req.pause(); + next(); } - } - - if(autoParse[mime]) { - autoParse[mime](req, res, mime, next); - } else { - if(req.headers['content-length']) req.pause(); + break; + default: next(); - } + break; } }); }; @@ -68,7 +76,18 @@ var parseBody = exports.parseBody = function(req, res, mime, callback) { } try { - req.body = parser.parse(buf); + if(buf.length) { + if(mime === 'application/json' && '{' != buf[0] && '[' != buf[0]) { + res.setHeader('Content-Type', 'text/plain'); + res.statusCode = 400; + res.end('Could not parse invalid JSON'); + return; + } + + req.body = parser.parse(buf); + } else { + req.body = {}; + } callback(); } catch (ex) { res.setHeader('Content-Type', 'text/plain'); diff --git a/test/util.unit.js b/test/util.unit.js index 8898dc6..bcbc44a 100644 --- a/test/util.unit.js +++ b/test/util.unit.js @@ -83,6 +83,17 @@ describe('.parseBody()', function() { req.emit('data', value); req.emit('end'); }); + + it('should interpret an empty body as an empty object', function(done) { + var req = new Stream(); + + http.parseBody(req, this.res, 'application/json', function(err) { + expect(err).to.not.exist; + expect(req.body).to.eql({}); + done(); + }); + req.emit('end'); + }); }); }); From fc87ede63e0cae79681ed1f44d8b79862f3863f2 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 18:59:55 -0700 Subject: [PATCH 15/17] fixed missing sanitization of boolean query values --- lib/resources/collection/index.js | 4 +++- test-app/public/index.html | 1 + test-app/public/test/collection.test.js | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/resources/collection/index.js b/lib/resources/collection/index.js index 45003c7..1f24227 100644 --- a/lib/resources/collection/index.js +++ b/lib/resources/collection/index.js @@ -149,11 +149,13 @@ Collection.prototype.sanitizeQuery = function (query) { if(expected == 'number' && actual == 'string') { sanitized[key] = parseFloat(val); + } else if(expected == 'boolean' && actual != 'boolean') { + sanitized[key] = (val === 'true') ? true : false; } else if (typeof val !== 'undefined') { sanitized[key] = val; } }); - + return sanitized; }; diff --git a/test-app/public/index.html b/test-app/public/index.html index 188d10d..bf18e6f 100644 --- a/test-app/public/index.html +++ b/test-app/public/index.html @@ -5,6 +5,7 @@ Mocha Tests + diff --git a/test-app/public/test/collection.test.js b/test-app/public/test/collection.test.js index ddf7a26..e36a383 100644 --- a/test-app/public/test/collection.test.js +++ b/test-app/public/test/collection.test.js @@ -224,6 +224,29 @@ describe('Collection', function() { }); }); }); + + describe('GET /full?boolean=true', function () { + it('should filter boolean properties by query string', function(done) { + dpd.full.post({boolean: true}, function (full) { + dpd.full.post({boolean: false}, function(full){ + $.ajax({ + type: "GET", + url: "/full?boolean=true", + success: function (res) { + expect(res.length).to.be.greaterThan(0); + res.forEach(function(obj){ + expect(obj.boolean).to.equal(true); + }); + done(); + }, + error: function (e) { + done(e); + } + }); + }); + }); + }); + }); describe('.get({id: "non existent"}, fn)', function() { it('should return a 404', function(done) { From ff25430d6342acdee5990fc0a650ada4620e8b5a Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Fri, 19 Oct 2012 19:05:45 -0700 Subject: [PATCH 16/17] updated history --- HISTORY.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index e944888..2cb1fe0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,10 @@ - Fixed bug where PUT would fail without an error if you provided a query - Changed root behavior - no longer ignores cancel() in events - Fixed bugs preventing events from being `emit()`ed to users in certain connection states + - Fixed bug where boolean query values (?bool=true) were not treated as booleans + - Fixed unnecessary error when parsing JSON body + - Added more intelegent body parsing + ## 0.6.6 From 124b79756e424eae16ebda48c0359e25a80d195d Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Sun, 21 Oct 2012 15:16:32 -0700 Subject: [PATCH 17/17] fixes #84 --- bin/dpd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/dpd b/bin/dpd index 8ffffdd..1d433dd 100755 --- a/bin/dpd +++ b/bin/dpd @@ -33,7 +33,7 @@ program .option('-d, --dashboard', 'start the dashboard immediately') .option('-o, --open', 'open in a browser') .option('-e, --environment [env]', 'defaults to development') - .option('-h, --host [host]', 'specify host for mongo server') + .option('-H, --host [host]', 'specify host for mongo server') .option('-P, --mongoPort [mongoPort]', 'mongodb port to connect to') .option('-n, --dbname [dbname]', 'name of the mongo database') .option('-a, --auth', 'prompts for mongo server credentials')