不要怂,就是干,撸起袖子干!

Commit d206cde1 by Mick Hansen

feat(associations/scope): add support for scope on N:M through model

1 parent e3e4b31f
...@@ -15,65 +15,51 @@ module.exports = (function() { ...@@ -15,65 +15,51 @@ module.exports = (function() {
HasManyDoubleLinked.prototype.injectGetter = function(options, queryOptions) { HasManyDoubleLinked.prototype.injectGetter = function(options, queryOptions) {
var self = this var self = this
, through = self.association.through , through = self.association.through
, targetAssociation = self.association.targetAssociation; , scopeWhere = null
, throughScopeWhere = null;
//fully qualify
var instancePrimaryKey = self.instance.Model.primaryKeyAttribute
, foreignPrimaryKey = self.association.target.primaryKeyAttribute;
var scopeWhere = this.association.scope ? {} : null;
if (this.association.scope) { if (this.association.scope) {
scopeWhere = {};
Object.keys(this.association.scope).forEach(function (attribute) { Object.keys(this.association.scope).forEach(function (attribute) {
scopeWhere[attribute] = this.association.scope[attribute]; scopeWhere[attribute] = this.association.scope[attribute];
}.bind(this)); }.bind(this));
} }
options.where = new Utils.and([ options.where = new Utils.and([
new Utils.where(
through.model.rawAttributes[self.association.identifier],
self.instance[instancePrimaryKey]
),
new Utils.where(
through.model.rawAttributes[self.association.foreignIdentifier],
{
join: new Utils.literal([
self.QueryInterface.quoteTable(self.association.target.name),
self.QueryInterface.quoteIdentifier(foreignPrimaryKey)
].join('.'))
}
),
scopeWhere, scopeWhere,
options.where options.where
]); ]);
if (Object(targetAssociation.through.model) === targetAssociation.through.model) { if (Object(through.model) === through.model) {
queryOptions.hasJoinTableModel = true; if (through && through.scope) {
queryOptions.joinTableModel = through.model; throughScopeWhere = {};
Object.keys(through.scope).forEach(function (attribute) {
if (!options.attributes) { throughScopeWhere[attribute] = through.scope[attribute];
options.attributes = [ }.bind(this));
self.QueryInterface.quoteTable(self.association.target.name) + '.*'
];
} }
if (options.joinTableAttributes) { options.include = options.include || [];
options.joinTableAttributes.forEach(function(elem) { options.include.push({
options.attributes.push( model: through.model,
self.QueryInterface.quoteTable(through.model.name) + '.' + self.QueryInterface.quoteIdentifier(elem) + ' as ' + as: Utils.singularize(through.model.tableName),
self.QueryInterface.quoteIdentifier(through.model.name + '.' + elem, true) association: {
); isSingleAssociation: true,
}); source: self.association.source,
} else { target: self.association.target,
Utils._.forOwn(through.model.rawAttributes, function(elem, key) { identifier: self.association.foreignIdentifier
options.attributes.push( },
self.QueryInterface.quoteTable(through.model.name) + '.' + self.QueryInterface.quoteIdentifier(key) + ' as ' + required: true,
self.QueryInterface.quoteIdentifier(through.model.name + '.' + key, true) where: new Utils.and([
); new Utils.where(
through.model.rawAttributes[self.association.identifier],
self.instance.get(self.association.source.primaryKeyAttribute)
),
throughScopeWhere
]),
_pseudo: true
}); });
} }
} return self.association.target.findAll(options, queryOptions);
return self.association.target.findAllJoin([through.model.getTableName(), through.model.name], options, queryOptions);
}; };
HasManyDoubleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) { HasManyDoubleLinked.prototype.injectSetter = function(oldAssociations, newAssociations, defaultAttributes) {
...@@ -150,8 +136,14 @@ module.exports = (function() { ...@@ -150,8 +136,14 @@ module.exports = (function() {
attributes = Utils._.defaults(attributes, unassociatedObject[targetAssociation.through.model.name], defaultAttributes); attributes = Utils._.defaults(attributes, unassociatedObject[targetAssociation.through.model.name], defaultAttributes);
} }
if (this.association.through.scope) {
Object.keys(this.association.through.scope).forEach(function (attribute) {
attributes[attribute] = this.association.through.scope[attribute];
}.bind(this));
}
return attributes; return attributes;
}); }.bind(this));
promises.push(self.association.through.model.bulkCreate(bulk, options)); promises.push(self.association.through.model.bulkCreate(bulk, options));
} }
...@@ -192,6 +184,11 @@ module.exports = (function() { ...@@ -192,6 +184,11 @@ module.exports = (function() {
} }
} else { } else {
attributes = Utils._.defaults(attributes, newAssociation[targetAssociation.through.model.name], additionalAttributes); attributes = Utils._.defaults(attributes, newAssociation[targetAssociation.through.model.name], additionalAttributes);
if (this.association.through.scope) {
Object.keys(this.association.through.scope).forEach(function (attribute) {
attributes[attribute] = this.association.through.scope[attribute];
}.bind(this));
}
return this.association.through.model.create(attributes, options); return this.association.through.model.create(attributes, options);
} }
......
...@@ -406,6 +406,13 @@ module.exports = (function() { ...@@ -406,6 +406,13 @@ module.exports = (function() {
var instance = this var instance = this
, primaryKeyAttribute = association.target.primaryKeyAttribute; , primaryKeyAttribute = association.target.primaryKeyAttribute;
additionalAttributes = additionalAttributes || {};
if (association.through && association.through.scope) {
Object.keys(association.through.scope).forEach(function (attribute) {
additionalAttributes[attribute] = association.through.scope[attribute];
});
}
if (Array.isArray(newInstance)) { if (Array.isArray(newInstance)) {
var newInstances = newInstance.map(function(newInstance) { var newInstances = newInstance.map(function(newInstance) {
if (!(newInstance instanceof association.target.Instance)) { if (!(newInstance instanceof association.target.Instance)) {
......
...@@ -910,8 +910,8 @@ module.exports = (function() { ...@@ -910,8 +910,8 @@ module.exports = (function() {
} }
} }
} else { } else {
var left = association.associationType === 'BelongsTo' ? association.target : include.association.source var left = association.associationType === 'BelongsTo' ? association.target : association.source
, primaryKeysLeft = association.associationType === 'BelongsTo' ? left.primaryKeyAttributes : left.primaryKeyAttributes , primaryKeysLeft = left.primaryKeyAttributes
, tableLeft = association.associationType === 'BelongsTo' ? as : parentTable , tableLeft = association.associationType === 'BelongsTo' ? as : parentTable
, attrLeft = primaryKeysLeft[0] , attrLeft = primaryKeysLeft[0]
, tableRight = association.associationType === 'BelongsTo' ? parentTable : as , tableRight = association.associationType === 'BelongsTo' ? parentTable : as
......
...@@ -711,12 +711,12 @@ module.exports = (function() { ...@@ -711,12 +711,12 @@ module.exports = (function() {
//right now, the caller (has-many-double-linked) is in charge of the where clause //right now, the caller (has-many-double-linked) is in charge of the where clause
Model.prototype.findAllJoin = function(joinTableName, options, queryOptions) { Model.prototype.findAllJoin = function(joinTableName, options, queryOptions) {
var optcpy = Utils._.clone(options); options = optClone(options || {});
optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteTable(this.name) + '.*']; options.attributes = options.attributes || [this.QueryInterface.quoteTable(this.name) + '.*'];
// whereCollection is used for non-primary key updates // whereCollection is used for non-primary key updates
this.options.whereCollection = optcpy.where || null; this.options.whereCollection = options.where || null;
return this.QueryInterface.select(this, [[this.getTableName(), this.name], joinTableName], optcpy, Utils._.defaults({ return this.QueryInterface.select(this, [[this.getTableName(), this.name], joinTableName], options, Utils._.defaults({
type: QueryTypes.SELECT type: QueryTypes.SELECT
}, queryOptions, { transaction: (options || {}).transaction })); }, queryOptions, { transaction: (options || {}).transaction }));
}; };
......
...@@ -28,11 +28,6 @@ describe(Support.getTestDialectTeaser("associations"), function() { ...@@ -28,11 +28,6 @@ describe(Support.getTestDialectTeaser("associations"), function() {
} }
} }
}); });
this.Tag = this.sequelize.define('tag', {
name: Sequelize.STRING,
taggable: Sequelize.STRING,
taggable_id: Sequelize.INTEGER
});
this.Post.hasMany(this.Comment, { this.Post.hasMany(this.Comment, {
foreignKey: 'commentable_id', foreignKey: 'commentable_id',
...@@ -42,10 +37,7 @@ describe(Support.getTestDialectTeaser("associations"), function() { ...@@ -42,10 +37,7 @@ describe(Support.getTestDialectTeaser("associations"), function() {
}); });
this.Comment.belongsTo(this.Post, { this.Comment.belongsTo(this.Post, {
foreignKey: 'commentable_id', foreignKey: 'commentable_id',
as: 'post', as: 'post'
scope: {
commentable: 'post'
}
}); });
this.Image.hasMany(this.Comment, { this.Image.hasMany(this.Comment, {
...@@ -56,10 +48,7 @@ describe(Support.getTestDialectTeaser("associations"), function() { ...@@ -56,10 +48,7 @@ describe(Support.getTestDialectTeaser("associations"), function() {
}); });
this.Comment.belongsTo(this.Image, { this.Comment.belongsTo(this.Image, {
foreignKey: 'commentable_id', foreignKey: 'commentable_id',
as: 'image', as: 'image'
scope: {
commentable: 'image'
}
}); });
this.Question.hasMany(this.Comment, { this.Question.hasMany(this.Comment, {
...@@ -70,10 +59,7 @@ describe(Support.getTestDialectTeaser("associations"), function() { ...@@ -70,10 +59,7 @@ describe(Support.getTestDialectTeaser("associations"), function() {
}); });
this.Comment.belongsTo(this.Question, { this.Comment.belongsTo(this.Question, {
foreignKey: 'commentable_id', foreignKey: 'commentable_id',
as: 'question', as: 'question'
scope: {
commentable: 'question'
}
}); });
}); });
...@@ -258,7 +244,6 @@ describe(Support.getTestDialectTeaser("associations"), function() { ...@@ -258,7 +244,6 @@ describe(Support.getTestDialectTeaser("associations"), function() {
}) })
); );
}).spread(function (postA, postB, postC) { }).spread(function (postA, postB, postC) {
//console.log(postA.get('categories'));
expect(postA.get('categories').length).to.equal(1); expect(postA.get('categories').length).to.equal(1);
expect(postA.get('tags').length).to.equal(1); expect(postA.get('tags').length).to.equal(1);
expect(postB.get('categories').length).to.equal(1); expect(postB.get('categories').length).to.equal(1);
...@@ -276,9 +261,158 @@ describe(Support.getTestDialectTeaser("associations"), function() { ...@@ -276,9 +261,158 @@ describe(Support.getTestDialectTeaser("associations"), function() {
}); });
}); });
describe.skip('on the through model', function () { describe('on the through model', function () {
beforeEach(function () {
this.Post = this.sequelize.define('post', {});
this.Image = this.sequelize.define('image', {});
this.Question = this.sequelize.define('question', {});
this.ItemTag = this.sequelize.define('item_tag', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
tag_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable'
},
taggable: {
type: DataTypes.STRING,
unique: 'item_tag_taggable'
},
taggable_id: {
type: DataTypes.INTEGER,
unique: 'item_tag_taggable',
references: null
}
});
this.Tag = this.sequelize.define('tag', {
name: DataTypes.STRING
});
this.Post.hasMany(this.Tag, {
through: {
model: this.ItemTag,
unique: false,
scope: {
taggable: 'post'
}
},
foreignKey: 'taggable_id',
constraints: false
});
this.Tag.hasMany(this.Post, {
through: {
model: this.ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
this.Image.hasMany(this.Tag, {
through: {
model: this.ItemTag,
unique: false,
scope: {
taggable: 'image'
}
},
foreignKey: 'taggable_id',
constraints: false
});
this.Tag.hasMany(this.Image, {
through: {
model: this.ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
this.Question.hasMany(this.Tag, {
through: {
model: this.ItemTag,
unique: false,
scope: {
taggable: 'question'
}
},
foreignKey: 'taggable_id',
constraints: false
});
this.Tag.hasMany(this.Question, {
through: {
model: this.ItemTag,
unique: false
},
foreignKey: 'tag_id'
});
});
it('should create, find and include associations with scope values', function () { it('should create, find and include associations with scope values', function () {
var self = this;
return Promise.join(
this.Post.sync({force: true}),
this.Image.sync({force: true}),
this.Question.sync({force: true}),
this.Tag.sync({force: true})
).bind(this).then(function () {
return this.ItemTag.sync({force: true});
}).then(function () {
return Promise.join(
this.Post.create(),
this.Image.create(),
this.Question.create(),
this.Tag.create({name: 'tagA'}),
this.Tag.create({name: 'tagB'}),
this.Tag.create({name: 'tagC'})
);
}).spread(function (post, image, question, tagA, tagB, tagC) {
this.post = post;
this.image = image;
this.question = question;
return Promise.join(
post.setTags([tagA]).then(function () {
return Promise.join(
post.createTag({name: 'postTag'}),
post.addTag(tagB)
);
}),
image.setTags([tagB]).then(function () {
return Promise.join(
image.createTag({name: 'imageTag'}),
image.addTag(tagC)
);
}),
question.setTags([tagC]).then(function () {
return Promise.join(
question.createTag({name: 'questionTag'}),
question.addTag(tagA)
);
})
);
}).then(function () {
return Promise.join(
this.post.getTags(),
this.image.getTags(),
this.question.getTags()
).spread(function (postTags, imageTags, questionTags) {
expect(postTags.length).to.equal(3);
expect(imageTags.length).to.equal(3);
expect(questionTags.length).to.equal(3);
expect(postTags.map(function (tag) {
return tag.name;
}).sort()).to.deep.equal(['postTag', 'tagA', 'tagB']);
expect(imageTags.map(function (tag) {
return tag.name;
}).sort()).to.deep.equal(['imageTag', 'tagB', 'tagC']);
expect(questionTags.map(function (tag) {
return tag.name;
}).sort()).to.deep.equal(['questionTag', 'tagA', 'tagC']);
});
});
}); });
}); });
}); });
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!