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

Commit a71a019f by Sushant Committed by GitHub

fix: inject foreignKey when using separate:true (#9396)

1 parent d6cd7534
......@@ -7,7 +7,6 @@ const Transaction = require('../transaction');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-one association
*
......@@ -115,16 +114,17 @@ class BelongsTo extends Association {
/**
* Get the associated instance.
*
* @param {Object} [options]
* @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false.
* @param {String} [options.schema] Apply a schema on the related model
* @param {Object} [options]
* @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false.
* @param {String} [options.schema] Apply a schema on the related model
*
* @see {@link Model.findOne} for a full explanation of options
*
* @return {Promise<Model>}
*/
get(instances, options) {
const association = this;
const where = {};
let Target = association.target;
let Target = this.target;
let instance;
options = Utils.cloneDeep(options);
......@@ -147,14 +147,14 @@ class BelongsTo extends Association {
}
if (instances) {
where[association.targetKey] = {
[Op.in]: instances.map(instance => instance.get(association.foreignKey))
where[this.targetKey] = {
[Op.in]: instances.map(instance => instance.get(this.foreignKey))
};
} else {
if (association.targetKeyIsPrimary && !options.where) {
return Target.findById(instance.get(association.foreignKey), options);
if (this.targetKeyIsPrimary && !options.where) {
return Target.findById(instance.get(this.foreignKey), options);
} else {
where[association.targetKey] = instance.get(association.foreignKey);
where[this.targetKey] = instance.get(this.foreignKey);
options.limit = null;
}
}
......@@ -167,11 +167,11 @@ class BelongsTo extends Association {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(association.foreignKey, {raw: true})] = null;
result[instance.get(this.foreignKey, {raw: true})] = null;
}
for (const instance of results) {
result[instance.get(association.targetKey, {raw: true})] = instance;
result[instance.get(this.targetKey, {raw: true})] = instance;
}
return result;
......@@ -184,28 +184,26 @@ class BelongsTo extends Association {
/**
* Set the associated model.
*
* @param {Model|String|Number} [newAssociation] An persisted instance or the primary key of an instance to associate with this. Pass `null` or `undefined` to remove the association.
* @param {Object} [options] Options passed to `this.save`
* @param {Boolean} [options.save=true] Skip saving this after setting the foreign key if false.
* @return {Promise}
* @param {Model|String|Number} [newAssociation] An persisted instance or the primary key of an instance to associate with this. Pass `null` or `undefined` to remove the association.
* @param {Object} [options] Options passed to `this.save`
* @param {Boolean} [options.save=true] Skip saving this after setting the foreign key if false.
*
* @return {Promise}
*/
set(sourceInstance, associatedInstance, options) {
const association = this;
options = options || {};
set(sourceInstance, associatedInstance, options = {}) {
let value = associatedInstance;
if (associatedInstance instanceof association.target) {
value = associatedInstance[association.targetKey];
if (associatedInstance instanceof this.target) {
value = associatedInstance[this.targetKey];
}
sourceInstance.set(association.foreignKey, value);
sourceInstance.set(this.foreignKey, value);
if (options.save === false) return;
options = _.extend({
fields: [association.foreignKey],
allowNull: [association.foreignKey],
fields: [this.foreignKey],
allowNull: [this.foreignKey],
association: true
}, options);
......@@ -218,22 +216,24 @@ class BelongsTo extends Association {
*
* @param {Object} [values]
* @param {Object} [options] Options passed to `target.create` and setAssociation.
*
* @see {@link Model#create} for a full explanation of options
*
* @return {Promise}
*/
create(sourceInstance, values, fieldsOrOptions) {
const association = this;
const options = {};
options.logging = (fieldsOrOptions || {}).logging;
if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
options.transaction = fieldsOrOptions.transaction;
}
options.logging = (fieldsOrOptions || {}).logging;
return association.target.create(values, fieldsOrOptions).then(newAssociatedObject =>
sourceInstance[association.accessors.set](newAssociatedObject, options)
);
return this.target.create(values, fieldsOrOptions)
.then(newAssociatedObject =>
sourceInstance[this.accessors.set](newAssociatedObject, options)
);
}
}
......
......@@ -21,7 +21,6 @@ class HasMany extends Association {
this.associationType = 'HasMany';
this.targetAssociation = null;
this.sequelize = source.sequelize;
this.through = options.through;
this.isMultiAssociation = true;
this.foreignKeyAttribute = {};
......@@ -91,7 +90,8 @@ class HasMany extends Association {
this.sourceKeyField = this.source.primaryKeyField;
}
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
// Get singular and plural names
// try to uppercase the first letter, unless the model forbids it
const plural = Utils.uppercaseFirst(this.options.name.plural);
const singular = Utils.uppercaseFirst(this.options.name.singular);
......@@ -165,10 +165,10 @@ class HasMany extends Association {
* @see {@link Model.findAll} for a full explanation of options
* @return {Promise<Array<Model>>}
*/
get(instances, options) {
const association = this;
get(instances, options = {}) {
const where = {};
let Model = association.target;
let Model = this.target;
let instance;
let values;
......@@ -177,36 +177,35 @@ class HasMany extends Association {
instances = undefined;
}
options = Utils.cloneDeep(options) || {};
options = Utils.cloneDeep(options);
if (association.scope) {
_.assign(where, association.scope);
if (this.scope) {
_.assign(where, this.scope);
}
if (instances) {
values = instances.map(instance => instance.get(association.sourceKey, {raw: true}));
values = instances.map(instance => instance.get(this.sourceKey, { raw: true }));
if (options.limit && instances.length > 1) {
options.groupedLimit = {
limit: options.limit,
on: association,
on: this, // association
values
};
delete options.limit;
} else {
where[association.foreignKey] = {
where[this.foreignKey] = {
[Op.in]: values
};
delete options.groupedLimit;
}
} else {
where[association.foreignKey] = instance.get(association.sourceKey, {raw: true});
where[this.foreignKey] = instance.get(this.sourceKey, { raw: true });
}
options.where = options.where ?
{[Op.and]: [where, options.where]} :
{ [Op.and]: [where, options.where] } :
where;
if (options.hasOwnProperty('scope')) {
......@@ -221,17 +220,16 @@ class HasMany extends Association {
Model = Model.schema(options.schema, options.schemaDelimiter);
}
return Model.findAll(options).then(results => {
if (instance) return results;
const result = {};
for (const instance of instances) {
result[instance.get(association.sourceKey, {raw: true})] = [];
result[instance.get(this.sourceKey, { raw: true })] = [];
}
for (const instance of results) {
result[instance.get(association.foreignKey, {raw: true})].push(instance);
result[instance.get(this.foreignKey, { raw: true })].push(instance);
}
return result;
......@@ -241,24 +239,28 @@ class HasMany extends Association {
/**
* 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 {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<Integer>}
*/
count(instance, options) {
const association = this;
const model = association.target;
const sequelize = model.sequelize;
options = Utils.cloneDeep(options);
options.attributes = [
[sequelize.fn('COUNT', sequelize.col(model.name.concat('.', model.primaryKeyField))), 'count']
[
this.sequelize.fn(
'COUNT',
this.sequelize.col(this.target.name.concat('.', this.target.primaryKeyField))
),
'count'
]
];
options.raw = true;
options.plain = true;
return association.get(instance, options).then(result => parseInt(result.count, 10));
return this.get(instance, options).then(result => parseInt(result.count, 10));
}
/**
......@@ -266,10 +268,10 @@ class HasMany extends Association {
*
* @param {Model[]|Model|string[]|String|number[]|Number} [instance(s)]
* @param {Object} [options] Options passed to getAssociations
*
* @return {Promise}
*/
has(sourceInstance, targetInstances, options) {
const association = this;
const where = {};
if (!Array.isArray(targetInstances)) {
......@@ -282,11 +284,11 @@ class HasMany extends Association {
});
where[Op.or] = targetInstances.map(instance => {
if (instance instanceof association.target) {
if (instance instanceof this.target) {
return instance.where();
} else {
const _where = {};
_where[association.target.primaryKeyAttribute] = instance;
_where[this.target.primaryKeyAttribute] = instance;
return _where;
}
});
......@@ -298,7 +300,7 @@ class HasMany extends Association {
]
};
return association.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length);
return this.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length);
}
/**
......@@ -310,24 +312,22 @@ class HasMany extends Association {
* @return {Promise}
*/
set(sourceInstance, targetInstances, options) {
const association = this;
if (targetInstances === null) {
targetInstances = [];
} else {
targetInstances = association.toInstanceArray(targetInstances);
targetInstances = this.toInstanceArray(targetInstances);
}
return association.get(sourceInstance, _.defaults({scope: false, raw: true}, options)).then(oldAssociations => {
return this.get(sourceInstance, _.defaults({scope: false, raw: true}, options)).then(oldAssociations => {
const promises = [];
const obsoleteAssociations = oldAssociations.filter(old =>
!_.find(targetInstances, obj =>
obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute]
obj[this.target.primaryKeyAttribute] === old[this.target.primaryKeyAttribute]
)
);
const unassociatedObjects = targetInstances.filter(obj =>
!_.find(oldAssociations, old =>
obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute]
obj[this.target.primaryKeyAttribute] === old[this.target.primaryKeyAttribute]
)
);
let updateWhere;
......@@ -335,15 +335,15 @@ class HasMany extends Association {
if (obsoleteAssociations.length > 0) {
update = {};
update[association.foreignKey] = null;
update[this.foreignKey] = null;
updateWhere = {};
updateWhere[association.target.primaryKeyAttribute] = obsoleteAssociations.map(associatedObject =>
associatedObject[association.target.primaryKeyAttribute]
updateWhere[this.target.primaryKeyAttribute] = obsoleteAssociations.map(associatedObject =>
associatedObject[this.target.primaryKeyAttribute]
);
promises.push(association.target.unscoped().update(
promises.push(this.target.unscoped().update(
update,
_.defaults({
where: updateWhere
......@@ -355,14 +355,14 @@ class HasMany extends Association {
updateWhere = {};
update = {};
update[association.foreignKey] = sourceInstance.get(association.sourceKey);
update[this.foreignKey] = sourceInstance.get(this.sourceKey);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(unassociatedObject =>
unassociatedObject[association.target.primaryKeyAttribute]
_.assign(update, this.scope);
updateWhere[this.target.primaryKeyAttribute] = unassociatedObjects.map(unassociatedObject =>
unassociatedObject[this.target.primaryKeyAttribute]
);
promises.push(association.target.unscoped().update(
promises.push(this.target.unscoped().update(
update,
_.defaults({
where: updateWhere
......@@ -382,25 +382,22 @@ class HasMany extends Association {
* @param {Object} [options] Options passed to `target.update`.
* @return {Promise}
*/
add(sourceInstance, targetInstances, options) {
add(sourceInstance, targetInstances, options = {}) {
if (!targetInstances) return Utils.Promise.resolve();
const association = this;
const update = {};
const where = {};
options = options || {};
targetInstances = this.toInstanceArray(targetInstances);
targetInstances = association.toInstanceArray(targetInstances);
update[this.foreignKey] = sourceInstance.get(this.sourceKey);
_.assign(update, this.scope);
update[association.foreignKey] = sourceInstance.get(association.sourceKey);
_.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = targetInstances.map(unassociatedObject =>
unassociatedObject.get(association.target.primaryKeyAttribute)
where[this.target.primaryKeyAttribute] = targetInstances.map(unassociatedObject =>
unassociatedObject.get(this.target.primaryKeyAttribute)
);
return association.target.unscoped().update(update, _.defaults({where}, options)).return(sourceInstance);
return this.target.unscoped().update(update, _.defaults({where}, options)).return(sourceInstance);
}
/**
......@@ -410,22 +407,20 @@ class HasMany extends Association {
* @param {Object} [options] Options passed to `target.update`
* @return {Promise}
*/
remove(sourceInstance, targetInstances, options) {
const association = this;
remove(sourceInstance, targetInstances, options = {}) {
const update = {};
const where = {};
options = options || {};
targetInstances = association.toInstanceArray(targetInstances);
targetInstances = this.toInstanceArray(targetInstances);
update[association.foreignKey] = null;
update[this.foreignKey] = null;
where[association.foreignKey] = sourceInstance.get(association.sourceKey);
where[association.target.primaryKeyAttribute] = targetInstances.map(targetInstance =>
targetInstance.get(association.target.primaryKeyAttribute)
where[this.foreignKey] = sourceInstance.get(this.sourceKey);
where[this.target.primaryKeyAttribute] = targetInstances.map(targetInstance =>
targetInstance.get(this.target.primaryKeyAttribute)
);
return association.target.unscoped().update(update, _.defaults({where}, options)).return(this);
return this.target.unscoped().update(update, _.defaults({where}, options)).return(this);
}
/**
......@@ -435,11 +430,7 @@ class HasMany extends Association {
* @param {Object} [options] Options passed to `target.create`.
* @return {Promise}
*/
create(sourceInstance, values, options) {
const association = this;
options = options || {};
create(sourceInstance, values, options = {}) {
if (Array.isArray(options)) {
options = {
fields: options
......@@ -450,16 +441,16 @@ class HasMany extends Association {
values = {};
}
if (association.scope) {
for (const attribute of Object.keys(association.scope)) {
values[attribute] = association.scope[attribute];
if (this.scope) {
for (const attribute of Object.keys(this.scope)) {
values[attribute] = this.scope[attribute];
if (options.fields) options.fields.push(attribute);
}
}
values[association.foreignKey] = sourceInstance.get(association.sourceKey);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
values[this.foreignKey] = sourceInstance.get(this.sourceKey);
if (options.fields) options.fields.push(this.foreignKey);
return this.target.create(values, options);
}
}
......
......@@ -120,9 +120,9 @@ class HasOne extends Association {
* @return {Promise<Model>}
*/
get(instances, options) {
const association = this;
const where = {};
let Target = association.target;
let Target = this.target;
let instance;
options = Utils.cloneDeep(options);
......@@ -145,15 +145,15 @@ class HasOne extends Association {
}
if (instances) {
where[association.foreignKey] = {
[Op.in]: instances.map(instance => instance.get(association.sourceKey))
where[this.foreignKey] = {
[Op.in]: instances.map(instance => instance.get(this.sourceKey))
};
} else {
where[association.foreignKey] = instance.get(association.sourceKey);
where[this.foreignKey] = instance.get(this.sourceKey);
}
if (association.scope) {
_.assign(where, association.scope);
if (this.scope) {
_.assign(where, this.scope);
}
options.where = options.where ?
......@@ -164,61 +164,61 @@ class HasOne extends Association {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(association.sourceKey, {raw: true})] = null;
result[instance.get(this.sourceKey, {raw: true})] = null;
}
for (const instance of results) {
result[instance.get(association.foreignKey, {raw: true})] = instance;
result[instance.get(this.foreignKey, {raw: true})] = instance;
}
return result;
});
}
return Target.findOne(options);
}
/**
* Set the associated model.
*
* @param {Model|String|Number} [newAssociation] An persisted instance or the primary key of a persisted instance to associate with this. Pass `null` or `undefined` to remove the association.
* @param {Object} [options] Options passed to getAssociation and `target.save`
* @param {Model|String|Number} [associatedInstance] An persisted instance or the primary key of a persisted instance to associate with this. Pass `null` or `undefined` to remove the association.
* @param {Object} [options] Options passed to getAssociation and `target.save`
*
* @return {Promise}
*/
set(sourceInstance, associatedInstance, options) {
const association = this;
let alreadyAssociated;
options = _.assign({}, options, {
scope: false
});
return sourceInstance[association.accessors.get](options).then(oldInstance => {
return sourceInstance[this.accessors.get](options).then(oldInstance => {
// TODO Use equals method once #5605 is resolved
alreadyAssociated = oldInstance && associatedInstance && _.every(association.target.primaryKeyAttributes, attribute =>
alreadyAssociated = oldInstance && associatedInstance && _.every(this.target.primaryKeyAttributes, attribute =>
oldInstance.get(attribute, {raw: true}) === (associatedInstance.get ? associatedInstance.get(attribute, {raw: true}) : associatedInstance)
);
if (oldInstance && !alreadyAssociated) {
oldInstance[association.foreignKey] = null;
oldInstance[this.foreignKey] = null;
return oldInstance.save(_.extend({}, options, {
fields: [association.foreignKey],
allowNull: [association.foreignKey],
fields: [this.foreignKey],
allowNull: [this.foreignKey],
association: true
}));
}
}).then(() => {
if (associatedInstance && !alreadyAssociated) {
if (!(associatedInstance instanceof association.target)) {
if (!(associatedInstance instanceof this.target)) {
const tmpInstance = {};
tmpInstance[association.target.primaryKeyAttribute] = associatedInstance;
associatedInstance = association.target.build(tmpInstance, {
tmpInstance[this.target.primaryKeyAttribute] = associatedInstance;
associatedInstance = this.target.build(tmpInstance, {
isNewRecord: false
});
}
_.assign(associatedInstance, association.scope);
associatedInstance.set(association.foreignKey, sourceInstance.get(association.sourceIdentifier));
_.assign(associatedInstance, this.scope);
associatedInstance.set(this.foreignKey, sourceInstance.get(this.sourceIdentifier));
return associatedInstance.save(options);
}
......@@ -232,30 +232,30 @@ class HasOne extends Association {
*
* @param {Object} [values]
* @param {Object} [options] Options passed to `target.create` and setAssociation.
*
* @see {@link Model#create} for a full explanation of options
*
* @return {Promise}
*/
create(sourceInstance, values, options) {
const association = this;
values = values || {};
options = options || {};
if (association.scope) {
for (const attribute of Object.keys(association.scope)) {
values[attribute] = association.scope[attribute];
if (this.scope) {
for (const attribute of Object.keys(this.scope)) {
values[attribute] = this.scope[attribute];
if (options.fields) {
options.fields.push(attribute);
}
}
}
values[association.foreignKey] = sourceInstance.get(association.sourceIdentifier);
values[this.foreignKey] = sourceInstance.get(this.sourceIdentifier);
if (options.fields) {
options.fields.push(association.foreignKey);
options.fields.push(this.foreignKey);
}
return association.target.create(values, options);
return this.target.create(values, options);
}
}
......
......@@ -655,16 +655,28 @@ class Model {
include.separate = true;
}
if (include.separate === true && !(include.association instanceof HasMany)) {
throw new Error('Only HasMany associations support include.separate');
}
if (include.separate === true) {
if (!(include.association instanceof HasMany)) {
throw new Error('Only HasMany associations support include.separate');
}
include.duplicating = false;
}
if (include.separate === true && options.attributes && options.attributes.length && !_.includes(options.attributes, association.source.primaryKeyAttribute)) {
options.attributes.push(association.source.primaryKeyAttribute);
if (
options.attributes
&& options.attributes.length
&& !_.includes(_.flattenDepth(options.attributes, 2), association.sourceKey)
) {
options.attributes.push(association.sourceKey);
}
if (
include.attributes
&& include.attributes.length
&& !_.includes(_.flattenDepth(include.attributes, 2), association.foreignKey)
) {
include.attributes.push(association.foreignKey);
}
}
// Validate child includes
......@@ -1769,16 +1781,14 @@ class Model {
return include.association.get(results, _.assign(
{},
_.omit(options, 'include', 'attributes', 'order', 'where', 'limit', 'offset', 'plain'),
_.omit(include, 'parent', 'association', 'as')
_.omit(options, ['include', 'attributes', 'originalAttributes', 'order', 'where', 'limit', 'offset', 'plain']),
_.omit(include, ['parent', 'association', 'as', 'originalAttributes'])
)).then(map => {
for (const result of results) {
result.set(
include.association.as,
map[result.get(include.association.source.primaryKeyAttribute)],
{
raw: true
}
map[result.get(include.association.sourceKey)],
{ raw: true }
);
}
});
......
......@@ -103,6 +103,65 @@ if (current.dialect.supports.groupedLimit) {
});
});
it('should work even if include does not specify foreign key attribute with custom sourceKey', function() {
const User = this.sequelize.define('User', {
name: DataTypes.STRING,
userExtraId: {
type: DataTypes.INTEGER,
unique: true
}
});
const Task = this.sequelize.define('Task', {
title: DataTypes.STRING
});
const sqlSpy = sinon.spy();
User.Tasks = User.hasMany(Task, {
as: 'tasks',
foreignKey: 'userId',
sourceKey: 'userExtraId'
});
return this.sequelize
.sync({force: true})
.then(() => {
return User.create({
id: 1,
userExtraId: 222,
tasks: [
{},
{},
{}
]
}, {
include: [User.Tasks]
});
})
.then(() => {
return User.findAll({
attributes: ['name'],
include: [
{
attributes: [
'title'
],
association: User.Tasks,
separate: true
}
],
order: [
['id', 'ASC']
],
logging: sqlSpy
});
})
.then(users => {
expect(users[0].get('tasks')).to.be.ok;
expect(users[0].get('tasks').length).to.equal(3);
expect(sqlSpy).to.have.been.calledTwice;
});
});
it('should not break a nested include with null values', function() {
const User = this.sequelize.define('User', {}),
Team = this.sequelize.define('Team', {}),
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!