Permalink
Browse files

Many-to-many relation

- hasMany {through: Class}
- hasAndBelongsToMany
- some specs in relations.test.js
  • Loading branch information...
1 parent 9cb7051 commit edaef1f885029418558c9b1f2884bea7606c2551 @1602 committed Apr 12, 2013
Showing with 188 additions and 19 deletions.
  1. +6 −2 lib/model.js
  2. +106 −12 lib/relations.js
  3. +8 −1 lib/scope.js
  4. +67 −2 test/relations.test.js
  5. +1 −2 test/scope.test.js
View
@@ -389,11 +389,15 @@ AbstractClass.all = function all(params, cb) {
}
var constr = this;
this.schema.adapter.all(this.modelName, params, function (err, data) {
- if (data && data.map) {
+ if (data && data.forEach) {
data.forEach(function (d, i) {
var obj = new constr;
obj._initProperties(d, false);
- data[i] = obj;
+ if (params && params.include && params.collect) {
+ data[i] = obj.__cachedRelations[params.collect];
+ } else {
+ data[i] = obj;
+ }
});
if (data && data.countBeforeLimit) {
data.countBeforeLimit = data.countBeforeLimit;
View
@@ -7,16 +7,16 @@ var defineScope = require('./scope.js').defineScope;
/**
* Relations mixins for ./model.js
*/
-var AbstractClass = require('./model.js');
+var Model = require('./model.js');
/**
* Declare hasMany relation
*
- * @param {Class} anotherClass - class to has many
+ * @param {Model} anotherClass - class to has many
* @param {Object} params - configuration {as:, foreignKey:}
* @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});`
*/
-AbstractClass.hasMany = function hasMany(anotherClass, params) {
+Model.hasMany = function hasMany(anotherClass, params) {
var thisClass = this, thisClassName = this.modelName;
params = params || {};
if (typeof anotherClass === 'string') {
@@ -46,14 +46,65 @@ AbstractClass.hasMany = function hasMany(anotherClass, params) {
// each instance of this class should have method named
// pluralize(anotherClass.modelName)
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
- defineScope(this.prototype, anotherClass, methodName, function () {
- var x = {};
- x[fk] = this.id;
- return {where: x};
- }, {
+ var scopeMethods = {
find: find,
destroy: destroy
- });
+ };
+ if (params.through) {
+ var fk2 = i8n.camelize(anotherClass.modelName + '_id', true);
+ scopeMethods.create = function create(data, done) {
+ if (typeof data !== 'object') {
+ done = data;
+ data = {};
+ }
+ var id = this.id;
+ anotherClass.create(data, function(err, ac) {
+ if (err) return done(err, ac);
+ var d = {};
+ d[fk] = id;
+ d[fk2] = ac.id;
+ params.through.create(d, function(e) {
+ if (e) {
+ ac.destroy(function() {
+ done(e);
+ });
+ } else {
+ done(err, ac);
+ }
+ });
+ });
+ };
+ scopeMethods.add = function(acInst, done) {
+ var data = {};
+ data[fk] = this.id;
+ data[fk2] = acInst.id || acInst;
+ params.through.findOrCreate({where: data}, done);
+ };
+ scopeMethods.remove = function(acInst, done) {
+ var self = this;
+ var q = {};
+ q[fk2] = acInst.id || acInst;
+ params.through.findOne({where: q}, function(err, d) {
+ if (err) {
+ return done(err);
+ }
+ if (!d) {
+ return done();
+ }
+ d.destroy(done);
+ });
+ };
+ }
+ defineScope(this.prototype, params.through || anotherClass, methodName, function () {
+ var filter = {};
+ filter.where = {};
+ filter.where[fk] = this.id;
+ if (params.through) {
+ filter.collect = i8n.camelize(anotherClass.modelName, true);
+ filter.include = filter.collect;
+ }
+ return filter;
+ }, scopeMethods);
// obviously, anotherClass should have attribute called `fk`
anotherClass.schema.defineForeignKey(anotherClass.modelName, fk);
@@ -106,7 +157,7 @@ AbstractClass.hasMany = function hasMany(anotherClass, params) {
*
* This optional parameter default value is false, so the related object will be loaded from cache if available.
*/
-AbstractClass.belongsTo = function (anotherClass, params) {
+Model.belongsTo = function (anotherClass, params) {
params = params || {};
if ('string' === typeof anotherClass) {
params.as = anotherClass;
@@ -124,7 +175,7 @@ AbstractClass.belongsTo = function (anotherClass, params) {
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
var fk = params.foreignKey || methodName + 'Id';
- this.relations[params['as']] = {
+ this.relations[methodName] = {
type: 'belongsTo',
keyFrom: fk,
keyTo: 'id',
@@ -163,7 +214,7 @@ AbstractClass.belongsTo = function (anotherClass, params) {
if (!refresh && this.__cachedRelations && (typeof this.__cachedRelations[methodName] !== 'undefined')) {
cachedValue = this.__cachedRelations[methodName];
}
- if (p instanceof AbstractClass) { // acts as setter
+ if (p instanceof Model) { // acts as setter
this[fk] = p.id;
this.__cachedRelations[methodName] = p;
} else if (typeof p === 'function') { // acts as async getter
@@ -189,3 +240,46 @@ AbstractClass.belongsTo = function (anotherClass, params) {
};
+/**
+ * Many-to-many relation
+ *
+ * Post.hasAndBelongsToMany('tags'); creates connection model 'PostTag'
+ */
+Model.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) {
+ params = params || {};
+ var models = this.schema.models;
+
+ if ('string' === typeof anotherClass) {
+ params.as = anotherClass;
+ if (params.model) {
+ anotherClass = params.model;
+ } else {
+ anotherClass = lookupModel(i8n.singularize(anotherClass)) ||
+ anotherClass;
+ }
+ if (typeof anotherClass === 'string') {
+ throw new Error('Could not find "' + anotherClass + '" relation for ' + this.modelName);
+ }
+ }
+
+ if (!params.through) {
+ var name1 = this.modelName + anotherClass.modelName;
+ var name2 = anotherClass.modelName + this.modelName;
+ params.through = lookupModel(name1) || lookupModel(name2) ||
+ this.schema.define(name1);
+ }
+ params.through.belongsTo(this);
+ params.through.belongsTo(anotherClass);
+
+ this.hasMany(anotherClass, {as: params.as, through: params.through});
+
+ function lookupModel(modelName) {
+ var lookupClassName = modelName.toLowerCase();
+ for (var name in models) {
+ if (name.toLowerCase() === lookupClassName) {
+ return models[name];
+ }
+ }
+ }
+
+};
View
@@ -57,7 +57,8 @@ function defineScope(cls, targetClass, name, params, methods) {
if (!this.__cachedRelations || (typeof this.__cachedRelations[name] == 'undefined') || actualRefresh) {
var self = this;
- return targetClass.all(mergeParams(actualCond, caller._scope), function(err, data) {
+ var params = mergeParams(actualCond, caller._scope);
+ return targetClass.all(params, function(err, data) {
if (!err && saveOnCache) {
if (!self.__cachedRelations) {
self.__cachedRelations = {};
@@ -134,6 +135,12 @@ function defineScope(cls, targetClass, name, params, methods) {
if (update.where) {
base.where = merge(base.where, update.where);
}
+ if (update.include) {
+ base.include = update.include;
+ }
+ if (update.collect) {
+ base.collect = update.collect;
+ }
// overwrite order
if (update.order) {
View
@@ -159,13 +159,78 @@ describe('relations', function() {
});
});
- describe.skip('hasAndBelongsToMany', function() {
- var Article, Tag;
+ describe('hasAndBelongsToMany', function() {
+ var Article, Tag, ArticleTag;
it('can be declared', function(done) {
Article = db.define('Article', {title: String});
Tag = db.define('Tag', {name: String});
Article.hasAndBelongsToMany('tags');
+ ArticleTag = db.models.ArticleTag;
+ db.automigrate(function() {
+ Article.destroyAll(function() {
+ Tag.destroyAll(function() {
+ ArticleTag.destroyAll(done)
+ });
+ });
+ });
});
+
+ it('should allow to create instances on scope', function(done) {
+ Article.create(function(e, article) {
+ article.tags.create({name: 'popular'}, function(e, t) {
+ t.should.be.an.instanceOf(Tag);
+ ArticleTag.findOne(function(e, at) {
+ should.exist(at);
+ at.tagId.should.equal(t.id);
+ at.articleId.should.equal(article.id);
+ done();
+ });
+ });
+ });
+ });
+
+ it('should allow to fetch scoped instances', function(done) {
+ Article.findOne(function(e, article) {
+ article.tags(function(e, tags) {
+ should.not.exist(e);
+ should.exist(tags);
+ done();
+ });
+ });
+ });
+
+ it('should allow to add connection with instance', function(done) {
+ Article.findOne(function(e, article) {
+ Tag.create({name: 'awesome'}, function(e, tag) {
+ article.tags.add(tag, function(e, at) {
+ should.not.exist(e);
+ should.exist(at);
+ at.should.be.an.instanceOf(ArticleTag);
+ at.tagId.should.equal(tag.id);
+ at.articleId.should.equal(article.id);
+ done();
+ });
+ });
+ });
+ });
+
+ it('should allow to remove connection with instance', function(done) {
+ Article.findOne(function(e, article) {
+ article.tags(function(e, tags) {
+ var len = tags.length;
+ article.tags.remove(tags[0], function(e, at) {
+ should.not.exist(e);
+ article.tags(true, function(e, tags) {
+ tags.should.have.lengthOf(len - 1);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('should allow to destroy instance and connection');
+
});
});
View
@@ -4,6 +4,7 @@ var should = require('./init.js');
var db, Railway, Station;
describe('sc0pe', function() {
+
before(function() {
db = getSchema();
Railway = db.define('Railway', {
@@ -16,7 +17,6 @@ describe('sc0pe', function() {
isActive: {type: Boolean, index: true},
isUndeground: {type: Boolean, index: true}
});
-
});
beforeEach(function(done) {
@@ -27,7 +27,6 @@ describe('sc0pe', function() {
it('should define scope with query', function(done) {
Station.scope('active', {where: {isActive: true}});
-
Station.active.create(function(err, station) {
should.not.exist(err);
should.exist(station);

0 comments on commit edaef1f

Please sign in to comment.