Skip to content

Instantly share code, notes, and snippets.

@jsocol
Created May 2, 2012 07:53
Show Gist options
  • Select an option

  • Save jsocol/2574884 to your computer and use it in GitHub Desktop.

Select an option

Save jsocol/2574884 to your computer and use it in GitHub Desktop.
Mongolian + Deferred = Tolerable Mongo in Node
/**
* Imagine, if you will, a simple task. You have a blog written in Node, with
* its content stored in Mongo, and you want to display a single post (for
* simplicity, let's pretend the URL contains the ObjectID) and its comments.
*
* Normally, you'd have to fall into callback/scope hell, and do something
* along the lines of this, already in an onRequest handler:
*/
function displayPost(request, response) {
// We'll look up the post and its comments.
var id = request.get('id'); // Or something similarly easy.
posts.findOne({"_id": id}, function(err, post) {
if (err) throw Http404();
// Mongolian seems to lack DBRefs right now.
comments.find({"post_id": id}).toArray(function(err, comms) {
if (err) throw Http500();
response.write(someTemplate.render(post, comms));
response.end();
});
});
}
/**
* I have utterly glossed over error handling. It's wrong, I don't care, it gets
* the point across. I have seen good developers write exactly this kind of mess.
*
* Because you need both the post and the comments (comms) to render the page,
* the naive solution is to do two asynchronous look-ups in *serial* rather than
* in parallel, and then deeply couple/nest the actual rendering and sending the
* response within the look-up code. That's insane.
*
* First, both lookups are asynchronous. Even though they are related, they don't
* (have to) depend on each other, so we should really run them in parallel.
* Sometimes they'll need to be serial--and in those cases we can still use a
* better method, because...
*
* Second, we need to separate them. Getting the comments right now needs the whole
* post in scope--even if it *does* depend on the post object and not just the id,
* that should be passed in as an argument, not in the scope. That makes it possible
* to test "getting the comments", a much smaller unit. It also lets us test "getting
* the post" without having to worry about the comments. Smaller units: more testable,
* more comprehensible, more good.
*
* Third, we're mixing concerns like a novice PHP developer. Having a template render
* in the middle of a database query is just as bad as
*
* <h1><?= mysql_query($my, "SELECT title FROM posts WHERE..."); ?></h1>
*
* The template render, again, ends up depending on scope rather than passing values,
* which makes it nearly impossible to test on its own, and extremely deeply nested.
* Just imagine dealing with errors at any level in this.
*
* But there's a better way!
*
*/
var Mongolian = require('mongolian'),
Deferred = require('Deferred');
var server = new Mongolain;
var db = server.db('blog');
var posts = db.collection('posts');
var comments = db.collection('comments');
/* For example...
posts.insert({
"title": "f1rst",
"content": "the first post!"
});
posts.insert({
"title": "second",
"content": "the second post!"
});
*/
// These two functions are totally independent, testable, and do not in any
// way depend on scope. Everything necessary is passed in via an argument.
function getPost(o, postsColl) {
var query = new Deferred();
postsColl.findOne(o || {}, function(err, post) {
if (err)
return query.reject(err);
return query.resolve(post);
});
return query.promise();
}
function getComments(o, commentsColl) {
var query = new Deferred();
commentsColl.find(o || {}).toArray(function(err, comms) {
if (err)
return query.reject(err);
return query.resolve(comms);
});
return query.promise();
}
// This is a kind of nonsensical, pseudo-way to do this:
function displayPost(id, template, stream) {
var lookup = Deferred.when(
getPost({"_id":id}, posts),
getComments({"post":id}, comments)
);
lookup.then(
function(post, comments) {
stream.write(template.render(post, comments));
},
function(postErr, commentsErr) {
stream.write(errorTemplate.render([postErr, commentsErr]);
}
);
}
/**
* If template compiling/rendering is slow, we can make that a deferred, too!
* This function is, again, totally testable and isolated. Everything necessary
* is passed into it via arguments. Any test suite capable of handling async
* code can test this with:
*
* renderTemplate('foo', {}).then(test.pass, test.fail);
*
* Or something more complicated, that makes real assertions.
*/
function renderTemplate(template, data) {
var render = new Deferred();
// Maybe 'template' is a filename and we have to read it off the
// filesystem, then do the parsing and rendering. Here let's just
// pretend it's generically async.
template.render(data, function(err, html) {
if (err)
return render.reject(err);
return render.resolve(html);
});
return render.promise();
}
/**
* This is more of a legitimate view from something like Express.
* I've still glossed over some things.
* Look! Look how little nesting! How things are atomic and testable!
* I still glossed over some things like status codes and not-founds
* vs legit query errors.
* It depends a _little_ on scope because I get the mongo collections
* from the global scope, but that's easily fixable, if it needs to
* be fixed at all.
*/
function displayPost(request, response) {
var id = request.get('id');
var lookup = Deferred.when(
getPost({"_id":id}, posts),
getComments({"post":id}, comments)
);
lookup.done(function(post, comments) {
renderTemplate(
'post.ejs',
{'post': post, 'comments': comments}
).done(response.end);
}).fail(function(postErr, commentsErr) {
renderTemplate(
'error.ejs',
{'errors': [postErr, commentsErr]}
).done(response.end);
});
}
@MJefferson

Copy link
Copy Markdown

This is awesome man, good work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment