Skip to content

Commit

Permalink
Support for relationships.
Browse files Browse the repository at this point in the history
  • Loading branch information
wbyoung committed May 27, 2015
1 parent 576f0df commit 34886ff
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ using [Azul.js][azul] with Express. For more information, see the
```js
app.post('/articles', at.route(function(req, res, next, Article, Author) {
Author.objects.findOrCreate({ name: req.body.author }).then(function(author) {
return Article.create({ author: author, title: req.body.title }).save();
return author.createArticle({ title: req.body.title }).save();
})
.then(function(article) {
res.send({ article: article.json });
Expand Down
54 changes: 50 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
var _ = require('lodash');
var BPromise = require('bluebird');

// TODO: this does not currently work across relationships

var setupRequest = function(db, req) {
if (req.azul && req.azul.transaction) { return; } // already set up

Expand Down Expand Up @@ -120,6 +118,54 @@ var makeExpressErrorRoute = function(fn) {
};
};

/**
* Create a model class binder function.
*
* The resulting function should be called with the name of a model to bind. A
* bound model will be created from that name. All relationships on that model
* will also be bound properly. The result is a model that you can safely use
* that has been bound to the query/transaction.
*
* @param {Database} db
* @param {Request} req
* @return {Function}
*/
var modelBinder = function(db, req) {
var query = req.azul.query;
var bound = {};
var bind = function(/*name*/) {
var name = arguments[0].toLowerCase();
if (!bound[name]) {
var subclass = bound[name] = db.model(name).extend();
var prototype = subclass.__class__.prototype;

// create an override of each relation defined on the model with the
// relation's model classes swapped out for bound models. note that no
// re-configuration will occur for the relation objects. they'll simply
// use a different model class when creating or accessing instances.
_.keysIn(prototype).filter(function(key) {
return key.match(/Relation$/);
})
.forEach(function(key) {
// TODO: we're accessing protected variables on the relation here. it
// would be a good idea to expose a tested method in the main azul
// project that we're sure will exist.
var relation = Object.create(prototype[key]); // copy relation
relation._modelClass = bind(relation._modelClass.__name__);
relation._relatedModel = bind(relation._relatedModel.__name__);
Object.defineProperty(prototype, key, { // redefine property
enumerable: true, get: function() { return relation; },
});
});

// redefine the query object on this model class
subclass.reopenClass({ query: query });
}
return bound[name];
};
return bind;
};

/**
* A wrapper for Express routes that binds queries & model classes to the
* transaction.
Expand Down Expand Up @@ -173,9 +219,9 @@ var route = function(db, fn) {

// setup the azul argument, binding queries and model classes
var query = req.azul.query;
var binder = modelBinder(db, req);
var azulArgs = azulParams.map(function(arg) {
return arg === 'query' ? query :
db.model(arg).extend({}, { query: query });
return arg === 'query' ? query : binder(arg);
});

// combine args & bind function we're wrapping
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
"lodash": "^3.6.0"
},
"peerDependencies": {
"azul": "^0.0.1-alpha.13"
"azul": "^0.0.1-alpha.14"
},
"devDependencies": {
"azul": "^0.0.1-alpha.13",
"azul": "^0.0.1-alpha.14",
"chai": "^2.3.0",
"coveralls": "^2.11.2",
"istanbul": "^0.3.13",
Expand Down
43 changes: 43 additions & 0 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,49 @@ describe('azul-transaction', function() {

});

it('works with relationships', function(done) {
db.model('article').reopen({
author: db.belongsTo(),
comments: db.hasMany(),
});
db.model('author', { articles: db.hasMany(), });
db.model('comment', { article: db.belongsTo() });
adapter.respond(/select \* from "authors"/i, [{ id: 5 }]);
adapter.respond(/select \* from "articles"/i,
[{ id: 1, 'author_id': 5 }]);

BPromise.bind().then(function() {
var route = at.route(function(req, res, query, Article) {
Article.objects.find(1).tap(function(article) {
return article.fetchAuthor();
})
.tap(function(article) {
return article.commentObjects.fetch();
})
.then(function() {
res.end();
})
.catch(done);
});
return route(req, res, next); // invoke route
})
.then(function() {
expect(adapter.executed).to.eql(['BEGIN']);
return res._end.wait;
})
.then(function() {
expect(adapter.clients.length).to.eql(1);
expect(adapter.executed).to.eql([
'BEGIN',
['SELECT * FROM "articles" WHERE "id" = ? LIMIT 1', [1]],
['SELECT * FROM "authors" WHERE "id" = ? LIMIT 1', [5]],
['SELECT * FROM "comments" WHERE "article_id" = ?', [1]],
'COMMIT'
]);
})
.then(done, done);
});

});

describe('test setup', function() {
Expand Down

0 comments on commit 34886ff

Please sign in to comment.