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

Commit 60fe7918 by Mick Hansen

Merge pull request #4130 from sequelize/feat-count-hm-associations

feat(has-many): countAssociations
2 parents ccff0d8f 8c80535e
# Next # Next
- [FEATURE] Geometry support for postgres - [ADDED] `countAssociations` for hasMany and belongsToMany
- [BUG] Fix wrong count for `findAndCountAll` with required includes [#4016](https://github.com/sequelize/sequelize/pull/4016) - [ADDED] Geometry support for postgres
- [BUG] Fix problems related to parsing of unique constraint errors [#4017](https://github.com/sequelize/sequelize/issues/4017) and [#4012](https://github.com/sequelize/sequelize/issues/4012) - [FIXED] Fix wrong count for `findAndCountAll` with required includes [#4016](https://github.com/sequelize/sequelize/pull/4016)
- [BUG] Fix postgres path variable being surrounded by quotes to often in unique constraint errors [#4034](https://github.com/sequelize/sequelize/pull/4034) - [FIXED] Fix problems related to parsing of unique constraint errors [#4017](https://github.com/sequelize/sequelize/issues/4017) and [#4012](https://github.com/sequelize/sequelize/issues/4012)
- [BUG] Fix `removeAttributes(id)` not setting `this.primaryKeys` to null - [FIXED] Fix postgres path variable being surrounded by quotes to often in unique constraint errors [#4034](https://github.com/sequelize/sequelize/pull/4034)
- [BUG] Run validations on the through model during add, set and create for `belongsToMany` - [FIXED] Fix `removeAttributes(id)` not setting `this.primaryKeys` to null
- [FIXED] Run validations on the through model during add, set and create for `belongsToMany`
# 3.3.2 # 3.3.2
- [FIXED] upsert no longer updates with default values each time [#3994](https://github.com/sequelize/sequelize/pull/3994) - [FIXED] upsert no longer updates with default values each time [#3994](https://github.com/sequelize/sequelize/pull/3994)
...@@ -21,11 +22,11 @@ ...@@ -21,11 +22,11 @@
- [ADDED] Hook name can be passed via the direct method [#3901](https://github.com/sequelize/sequelize/issues/3901) - [ADDED] Hook name can be passed via the direct method [#3901](https://github.com/sequelize/sequelize/issues/3901)
# 3.2.0 # 3.2.0
- [FEATURE] Add support for new option `targetKey` in a belongs-to relationship for situations where the target key is not the id field. - [ADDED] Add support for new option `targetKey` in a belongs-to relationship for situations where the target key is not the id field.
- [FEATURE] Add support for keyword `after` in options of a field (useful for migrations), only for MySQL. [#3166](https://github.com/sequelize/sequelize/pull/3166) - [ADDED] Add support for keyword `after` in options of a field (useful for migrations), only for MySQL. [#3166](https://github.com/sequelize/sequelize/pull/3166)
- [FEATURE] There's a new sequelize.truncate function to truncate all tables defined through the sequelize models [#2671](https://github.com/sequelize/sequelize/pull/2671) - [ADDED] There's a new sequelize.truncate function to truncate all tables defined through the sequelize models [#2671](https://github.com/sequelize/sequelize/pull/2671)
- [FEATURE] Add support for MySQLs TINYTEXT, MEDIUMTEXT and LONGTEXT. [#3836](https://github.com/sequelize/sequelize/pull/3836) - [ADDED] Add support for MySQLs TINYTEXT, MEDIUMTEXT and LONGTEXT. [#3836](https://github.com/sequelize/sequelize/pull/3836)
- [FEATURE] Provide warnings if you misuse data types. [#3839](https://github.com/sequelize/sequelize/pull/3839) - [ADDED] Provide warnings if you misuse data types. [#3839](https://github.com/sequelize/sequelize/pull/3839)
- [FIXED] Fix a case where Postgres arrays containing JSONB type was being generated as JSON type. - [FIXED] Fix a case where Postgres arrays containing JSONB type was being generated as JSON type.
- [FIXED] Fix a case where `type` in `sequelize.query` was not being set to raw. [#3800](https://github.com/sequelize/sequelize/pull/3800) - [FIXED] Fix a case where `type` in `sequelize.query` was not being set to raw. [#3800](https://github.com/sequelize/sequelize/pull/3800)
- [FIXED] Fix an issue where include all was not being properly expanded for self-references [#3804](https://github.com/sequelize/sequelize/issues/3804) - [FIXED] Fix an issue where include all was not being properly expanded for self-references [#3804](https://github.com/sequelize/sequelize/issues/3804)
...@@ -72,13 +73,13 @@ ...@@ -72,13 +73,13 @@
- [REMOVED] `instance.increment/decrement` now longer takes a number as it's second argument. - [REMOVED] `instance.increment/decrement` now longer takes a number as it's second argument.
- [REMOVED/SECURITY] findOne no longer takes a string / integer / binary argument to represent a primaryKey. Use findById instead - [REMOVED/SECURITY] findOne no longer takes a string / integer / binary argument to represent a primaryKey. Use findById instead
- [REMOVED/SECURITY] `where: "raw query"` is no longer legal, you must now explicitely use `where: ["raw query", [replacements]]` - [REMOVED/SECURITY] `where: "raw query"` is no longer legal, you must now explicitely use `where: ["raw query", [replacements]]`
- [BUG] Fix showIndexQuery so appropriate indexes are returned when a schema is used - [FIXED] Fix showIndexQuery so appropriate indexes are returned when a schema is used
- [BUG] Fix addIndexQuery error when the model has a schema - [FIXED] Fix addIndexQuery error when the model has a schema
- [BUG] Fix app crash in sqlite while running in special unique constraint errors [#3730](https://github.com/sequelize/sequelize/pull/3730) - [FIXED] Fix app crash in sqlite while running in special unique constraint errors [#3730](https://github.com/sequelize/sequelize/pull/3730)
- [BUG] Fix bulkCreate: do not insert NULL for undefined values [#3729](https://github.com/sequelize/sequelize/pull/3729) - [FIXED] Fix bulkCreate: do not insert NULL for undefined values [#3729](https://github.com/sequelize/sequelize/pull/3729)
- [BUG] Fix trying to roll back a comitted transaction if an error occured while comitting resulting in an unhandled rejection [#3726](https://github.com/sequelize/sequelize/pull/3726) - [FIXED] Fix trying to roll back a comitted transaction if an error occured while comitting resulting in an unhandled rejection [#3726](https://github.com/sequelize/sequelize/pull/3726)
- [BUG] Fix regression in beforeUpdate hook where `instance.changed()` would always be false [#3727](https://github.com/sequelize/sequelize/pull/3727) - [FIXED] Fix regression in beforeUpdate hook where `instance.changed()` would always be false [#3727](https://github.com/sequelize/sequelize/pull/3727)
- [BUG] Fix trying to roll back a comitted transaction if an error occured while comitting - [FIXED] Fix trying to roll back a comitted transaction if an error occured while comitting
#### Backwards compatibility changes #### Backwards compatibility changes
- Most of the changes in 3.0.0 are BC breaking, read the changelog for 3.0.0 carefully. - Most of the changes in 3.0.0 are BC breaking, read the changelog for 3.0.0 carefully.
......
...@@ -156,11 +156,22 @@ var HasMany = function(source, target, options) { ...@@ -156,11 +156,22 @@ var HasMany = function(source, target, options) {
* @return {Promise} * @return {Promise}
* @method hasAssociations * @method hasAssociations
*/ */
hasAll: 'has' + plural hasAll: 'has' + plural,
/**
* Count everything currently associated with this, using an optional where clause
*
* @param {Object} [options]
* @param {Object} [options.where] An optional where clause to limit the associated models
* @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
* @return {Promise<Int>}
* @method countAssociations
*/
count: 'count' + plural
}; };
if (this.options.counterCache) { if (this.options.counterCache) {
new CounterCache(this, this.options.counterCache !== true ? this.options.counterCache : {}); new CounterCache(this, this.options.counterCache !== true ? this.options.counterCache : {});
delete this.accessors.count;
} }
}; };
...@@ -233,6 +244,24 @@ HasMany.prototype.injectGetter = function(obj) { ...@@ -233,6 +244,24 @@ HasMany.prototype.injectGetter = function(obj) {
return Model.all(options); return Model.all(options);
}; };
if (this.accessors.count) {
obj[this.accessors.count] = function(options) {
var model = association.target
, sequelize = model.sequelize;
options = association.target.__optClone(options) || {};
options.attributes = [
[sequelize.fn('COUNT', sequelize.col(model.primaryKeyAttribute)), 'count']
];
options.raw = true;
options.plain = true;
return obj[association.accessors.get].call(this, options).then(function (result) {
return parseInt(result.count, 10);
});
};
}
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) { obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
var where = {}; var where = {};
......
...@@ -415,11 +415,22 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -415,11 +415,22 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
beforeEach(function() { beforeEach(function() {
var self = this; var self = this;
this.User = this.sequelize.define('User', { username: DataTypes.STRING }); this.User = this.sequelize.define('User', {
this.Task = this.sequelize.define('Task', { title: DataTypes.STRING, active: DataTypes.BOOLEAN }); username: DataTypes.STRING
});
this.Task = this.sequelize.define('Task', {
title: DataTypes.STRING,
active: DataTypes.BOOLEAN
});
this.UserTask = this.sequelize.define('UserTask', {
started: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
});
this.User.belongsToMany(this.Task, { through: 'UserTasks' }); this.User.belongsToMany(this.Task, { through: this.UserTask });
this.Task.belongsToMany(this.User, { through: 'UserTasks' }); this.Task.belongsToMany(this.User, { through: this.UserTask });
return this.sequelize.sync({ force: true }).then(function() { return this.sequelize.sync({ force: true }).then(function() {
return Promise.all([ return Promise.all([
...@@ -449,7 +460,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -449,7 +460,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
it('should count scoped associations', function () { it('should count scoped associations', function () {
this.User.belongsToMany(this.Task, { this.User.belongsToMany(this.Task, {
as: 'activeTasks', as: 'activeTasks',
through: 'UserTasks', through: this.UserTask,
scope: { scope: {
active: true active: true
} }
...@@ -457,6 +468,35 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() { ...@@ -457,6 +468,35 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), function() {
return expect(this.user.countActiveTasks({})).to.eventually.equal(1); return expect(this.user.countActiveTasks({})).to.eventually.equal(1);
}); });
it('should count scoped through associations', function () {
var user = this.user;
this.User.belongsToMany(this.Task, {
as: 'startedTasks',
through: {
model: this.UserTask,
scope: {
started: true
}
}
});
return Promise.join(
this.Task.create().then(function (task) {
return user.addTask(task, {
started: true
});
}),
this.Task.create().then(function (task) {
return user.addTask(task, {
started: true
});
})
).then(function () {
return expect(user.countStartedTasks({})).to.eventually.equal(2);
});
});
}); });
describe('setAssociations', function() { describe('setAssociations', function() {
......
...@@ -634,6 +634,54 @@ describe(Support.getTestDialectTeaser('HasMany'), function() { ...@@ -634,6 +634,54 @@ describe(Support.getTestDialectTeaser('HasMany'), function() {
}); });
}); });
describe('countAssociations', function () {
beforeEach(function() {
var self = this;
this.User = this.sequelize.define('User', { username: DataTypes.STRING });
this.Task = this.sequelize.define('Task', { title: DataTypes.STRING, active: DataTypes.BOOLEAN });
this.User.hasMany(self.Task, {
foreignKey: 'userId'
});
return this.sequelize.sync({ force: true }).then(function() {
return Promise.all([
self.User.create({ username: 'John'}),
self.Task.create({ title: 'Get rich', active: true}),
self.Task.create({ title: 'Die trying', active: false})
]);
}).spread(function(john, task1, task2) {
self.user = john;
return john.setTasks([task1, task2]);
});
});
it('should count all associations', function () {
return expect(this.user.countTasks({})).to.eventually.equal(2);
});
it('should count filtered associations', function () {
return expect(this.user.countTasks({
where: {
active: true
}
})).to.eventually.equal(1);
});
it('should count scoped associations', function () {
this.User.hasMany(this.Task, {
foreignKey: 'userId',
as: 'activeTasks',
scope: {
active: true
}
});
return expect(this.user.countActiveTasks({})).to.eventually.equal(1);
});
});
describe('selfAssociations', function() { describe('selfAssociations', function() {
it('should work with alias', function() { it('should work with alias', function() {
var Person = this.sequelize.define('Group', {}); var Person = this.sequelize.define('Group', {});
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!