Search code examples
javascriptnode.jsbluebirdbookshelf.js

How to process and update all models in a collection, in a single transaction, with Bookshelf?


I need to iterate over all the models in a Bookshelf collection, compute some information, then store that information back in each model. It's important that I do this in a single transaction, for rollbacks on errors.

The problem I am running into is the only way I can really think of to do this is with Promise.map (Bluebird), but a bookshelf collection can't be passed to map. For example, this does not work (Thing is a Model, Promise is a bluebird promise):

Bookshelf.transaction(function (t) {
    return Thing.fetchAll({transacting:t}).then(function (things) {
        return Promise.map(things, function (thing) {
            return thing.save({
                value: computeSomeValueSync(thing)
            }, {
                transacting: t
            });
        });
    });
}).tap(function () {
    console.log("update complete");
});

Because things can't be passed to Promise.map, and there doesn't seem to be anything in the Bookshelf API that can obtain an array of models from a collection...

How can I do this?


Solution

  • All right, I found a solution, at least.

    First step is to write a function that computes and saves the value, and make it be a member of the bookshelf model. So, for the example from my post, I'd define the following function in Thing when extending the model:

    ... = bookshelf.Model.extend({
    
        ...
    
        updateSomeValue: function (options) {
            return this.save({ 
                value: computeSomeValueSync(this)
            }, options);
        }
    
    });
    

    Where options is the options to pass to save, which we can use to pass the transaction through. Easy enough. Then, we can do the equivalent of Promise.map with Collection#invokeThen, like this:

    Bookshelf.transaction(function (t) {
        return Thing.fetchAll({transacting:t}).then(function (things) {
            return things.invokeThen("updateSomeValue", {transacting:t});
        });        
    }).tap(function () {
        console.log("update complete");
    });
    

    There, invokeThen essentially does what I intended to do with Promise.map -- returns a promise that becomes fulfilled once all the promises returned by Thing#updateSomeValue are fulfilled.

    It's only mildly inconvenient in that I have to add the model method, but it does make a bit of sense at least. The interface is a little weird because the docs are tough to piece together. But, at least it's possible.

    Still open to other ideas.