From 34886ff2f5d0ed075196ceb067d9d0620688aacf Mon Sep 17 00:00:00 2001 From: Whitney Young Date: Wed, 27 May 2015 14:59:56 -0700 Subject: [PATCH] Support for relationships. --- README.md | 2 +- index.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++---- package.json | 4 ++-- test/tests.js | 43 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0674196..e5bd91d 100644 --- a/README.md +++ b/README.md @@ -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 }); diff --git a/index.js b/index.js index 1d61f38..e39e4d1 100644 --- a/index.js +++ b/index.js @@ -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 @@ -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. @@ -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 diff --git a/package.json b/package.json index 64f27c3..161204c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/tests.js b/test/tests.js index 52fd89f..e0499fc 100644 --- a/test/tests.js +++ b/test/tests.js @@ -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() {