tgriesser/checkit — pull request #19 by alkawryk (2014-08-09 20:30:23+00:00)
Title: Adding a 'context' property to the Checkit constructor options

This change introduces a `context` property on the options passed to the `Checkit` instance. The `context` property is supplied to any custom function-based validations as the third parameter (after the `params` parameter). The use-case this addresses in particular is Checkit-based model validation in BookshelfJS, where we may find ourselves needing more information than is available within the scope of the Checkit validation.

The issue I was running into was that transactions were causing my application/mocha tests to hang. The culprit was executing another query while a transaction was in progress. To get around this, the validation can pass the current transaction to its validation query - but I needed to be able to pass in more information to the validation to do this.

As a concrete, standalone example, see the following snippet:

```js
var Bookshelf = require('bookshelf'),
InMemoryDB = Bookshelf.initialize({
client: 'sqlite3',
connection: {
filename: ':memory:'
}
}),
Promise = require('bluebird'),
Checkit = require('checkit');

var Model = InMemoryDB.Model.extend({
initialize: function(){
this.on('creating', this.validateCreate, this);
this.on('updating', this.validateUpdate, this);
},

creationValidations: { },

updateValidations: { },

validateCreate: function(model, attrs, options){
return Checkit(this.creationValidations, {context: options})
.run(this.toJSON());
},

validateUpdate: function(model, attrs, options){
return Checkit(this.updateValidations, {context: options})
.run(this.toJSON());
}
});

var User = Model.extend({
tableName: 'users',
creationValidations : {
email: [{
rule: function(val, params, context){
var query = InMemoryDB.knex('users');
if (context && context.transacting){
query.transacting(context.transacting);
}

return query.where('email', '=', val)
.andWhere('id', '<>', this.target.id)
.then(function(resp){
if (resp.length > 0){
throw new Error('The email address is already in use.');
}
});
}
}]
}
});

var UserRelation = Model.extend({
tableName: 'user_relations'
});

var buildSchema = function(done){
InMemoryDB.knex.schema.createTable('users', function(table){
table.integer('id');
table.string('email');
})
.then(function(){
return InMemoryDB.knex.schema.createTable('user_relations', function(table){
table.integer('id');
table.integer('child_id');
table.integer('parent_id');
});
})
.then(function(){
done();
});
};

var destroySchema = function(done){
InMemoryDB.knex.schema.dropTable('users').then(function(res){
return InMemoryDB.knex.schema.dropTable('user_relations');
})
.then(function(result){
done();
});
};

beforeEach(buildSchema);

afterEach(destroySchema);

describe('transactions', function(){

it('should save everything together when saving in parallel', function(){
return InMemoryDB.transaction(function(t){
return Promise.all([
new User().save({id: 1, email: 'user1@gmail.com'}, {method: 'insert', transacting: t}),
new User().save({id: 2, email: 'user2@gmail.com'}, {method: 'insert', transacting: t}),
new UserRelation().save({child_id: 1, parent_id: 2}, {method: 'insert', transacting: t})
])
.then(function(){
console.log("committing...");
t.commit();
})
.caught(function(err){
console.log("error saving", err);
t.rollback();
})
});
});

it('should save everything together when saving in serial', function(){
return InMemoryDB.transaction(function(t){
return new User().save({id: 1, email: 'user1@gmail.com'}, {method: 'insert', transacting: t}).then(function(user1){
console.log("saved user 1");
return new User().save({id: 2, email: 'user2@gmail.com'}, {method: 'insert', transacting: t});
})
.then(function(user2){
console.log("saved user 2");
return new UserRelation().save({child_id: 1, parent_id: 2}, {method: 'insert', transacting: t});
})
.then(function(relation){
console.log("saved the relation");
t.commit();
})
.caught(function(err){
console.log("an error occurred", err);
t.rollback();
})
});
});

it('should still work fine without a transaction', function(){
return new User().save({id: 1, email: 'user1@gmail.com'}, {method: 'insert'}).then(function(user1){
console.log("saved user 1");
return new User().save({id: 2, email: 'user2@gmail.com'}, {method: 'insert'});
})
.then(function(user2){
console.log("saved user 2");
return new UserRelation().save({child_id: 1, parent_id: 2}, {method: 'insert'});
});
});

});
```

In this example, the tests try to insert some users within a transaction block. When the users are saved, their validation functions fire off a query (to check for duplicate emails). Without being able to pass the transaction object, these tests hang.

I've updated the documentation and included some tests. I also noticed the test-suite was missing test-coverage around function-based validation, so I've included a few of those tests as well.

Thanks for your consideration.

Alex