Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Node.js promises with mongoskin

I'm trying to avoid using callbacks when making mongodb queries. I'm using mongoskin to make calls like so:

req.db.collection('users').find().toArray(function (err, doc) {
  res.json(doc);
});

In many cases I need to make multiple queries so I want to use Node.js promise library but I'm not sure how to wrap these functions as promises. Most of the examples I see are trivial for things like readFile, I'm guessing in this case I would need to wrap toArray somehow? Can this be done or would have to be something implemented by mongoskin?

An example could be any set of callbacks, find/insert, find/find/insert, find/update:

req.db.collection('users').find().toArray(function (err, doc) {
  if (doc) {
    req.db.collection('users').find().toArray(function (err, doc) {
      // etc...
    });
  }
  else {
    // err
  }
});
like image 618
Rob Avatar asked May 15 '14 19:05

Rob


3 Answers

You can promisify the entire module like so with bluebird:

var Promise = require("bluebird");
var mongoskin = require("mongoskin");
Object.keys(mongoskin).forEach(function(key) {
  var value = mongoskin[key];
  if (typeof value === "function") {
    Promise.promisifyAll(value);
    Promise.promisifyAll(value.prototype);
  }
});
Promise.promisifyAll(mongoskin);

This only needs to be done in one place for one time in your application, not anywhere in your application code.

After that you just use methods normally except with the Async suffix and don't pass callbacks:

req.db.collection('users').find().toArrayAsync()
  .then(function(doc) {
    if (doc) {
      return req.db.collection('users').find().toArrayAsync();
    }
  })
  .then(function(doc) {
    if (doc) {
      return req.db.collection('users').find().toArrayAsync();
    }
  })
  .then(function(doc) {
    if (doc) {
      return req.db.collection('users').find().toArrayAsync();
    }
  });

So again, if you call a function like

foo(a, b, c, function(err, result) {
    if (err) return console.log(err);
    //Code
});

The promise-returning version is called like:

fooAsync(a, b, c).then(...)

(Uncaught errors are automatically logged so you don't need to check for them if you are only going to log it)

like image 130
Esailija Avatar answered Oct 13 '22 15:10

Esailija


Just stumbled here with the same question and didn't love "promisfying" mongoskin so did a bit more digging and found monk. It's built on top of mongoskin, tidies up the API and returns promises for all async calls. Probably worth a peek to anyone else who lands here.

like image 2
matt.kauffman23 Avatar answered Oct 13 '22 15:10

matt.kauffman23


Esailija's answer may work, but its not super efficient since you have to run db.collection on every single db call. I don't know exactly how expensive that is, but looking at the code in mongoskin, its non-trivial. Not only that, but it's globally modifying prototypes, which isn't very safe.

The way I do this with fibers futures is:

  1. wrap the collection methods for each collection
  2. on receiving the result, for methods that return a Cursor wrap the toArray method, call it and return the resulting future (for methods that don't return a cursor, you don't need to do anything else).
  3. use the future as normal

like this:

var Future = require("fibers/future")

// note: when i originally wrote this answer fibers/futures didn't have a good/intuitive wrapping function; but as of 2014-08-18, it does have one
function futureWrap() {
    // function
    if(arguments.length === 1) {
        var fn = arguments[0]
        var object = undefined

    // object, methodName
    } else {
        var object = arguments[0]
        var fn = object[arguments[1]]
    }

    return function() {
        var args = Array.prototype.slice.call(arguments)
        var future = new Future
        args.push(future.resolver())
        var me = this
        if(object) me = object
        fn.apply(me, args)
        return future
    }
}

var methodsYouWantToHave = ['findOne', 'find', 'update', 'insert', 'remove', 'findAndModify']
var methods = {}
methodsYouWantToHave.forEach(function(method) {
    internalMethods[method] = futureWrap(this.collection, method)
}.bind(this))

// use them
var document = methods.findOne({_id: 'a3jf938fj98j'}, {}).wait()
var documents = futureWrap(methods.find({x: 'whatever'}, {}).wait(), 'toArray')().wait()

If you don't want to use fibers, I'd recommend using the async-future module, which has a good wrap function built in too.

like image 1
B T Avatar answered Oct 13 '22 15:10

B T