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

Commit 3ff27d10 by Felix Becker Committed by Jan Aagaard Meier

ES6 refactor: associations (#6050)

* Make BelongsTo association an ES6 class

* ES6 refactor of belongs-to.js

* Make BelongsToMany an ES6 class

* ES6 refactor of BelongsToMany

* Make HasMany an ES6 class

* ES6 refactor of HasMany

* Make HasOne an ES6 class

* ES6 refactor of HasOne

* ES6 refactor of association helpers

* ES6 refactor of associations/index.js

* ES6 refactor of association mixin
1 parent 5d7b26c2
'use strict'; 'use strict';
var Utils = require('./../utils') const Utils = require('./../utils');
, Helpers = require('./helpers') const Helpers = require('./helpers');
, _ = require('lodash') const _ = require('lodash');
, Association = require('./base') const Association = require('./base');
, BelongsTo = require('./belongs-to') const BelongsTo = require('./belongs-to');
, HasMany = require('./has-many') const HasMany = require('./has-many');
, HasOne = require('./has-one') const HasOne = require('./has-one');
, util = require('util');
/** /**
* Many-to-many association with a join table. * Many-to-many association with a join table.
...@@ -37,738 +36,706 @@ var Utils = require('./../utils') ...@@ -37,738 +36,706 @@ var Utils = require('./../utils')
* *
* @mixin BelongsToMany * @mixin BelongsToMany
*/ */
var BelongsToMany = function(source, target, options) { class BelongsToMany extends Association {
Association.call(this); constructor(source, target, options) {
super();
options = options || {}; options = options || {};
if (options.through === undefined || options.through === true || options.through === null) {
throw new Error('belongsToMany must be given a through option, either a string or a model');
}
if (!options.through.model) {
options.through = {
model: options.through
};
}
this.associationType = 'BelongsToMany';
this.source = source;
this.target = target;
this.targetAssociation = null;
this.options = options;
this.sequelize = source.modelManager.sequelize;
this.through = _.assign({}, options.through);
this.scope = options.scope;
this.isMultiAssociation = true;
this.isSelfAssociation = this.source === this.target;
this.doubleLinked = false;
this.as = this.options.as;
if (!this.as && this.isSelfAssociation) {
throw new Error('\'as\' must be defined for many-to-many self-associations');
}
if (this.as) { if (options.through === undefined || options.through === true || options.through === null) {
this.isAliased = true; throw new Error('belongsToMany must be given a through option, either a string or a model');
}
if (Utils._.isPlainObject(this.as)) { if (!options.through.model) {
this.options.name = this.as; options.through = {
this.as = this.as.plural; model: options.through
} else {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
}; };
} }
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.combinedTableName = Utils.combineTableNames( this.associationType = 'BelongsToMany';
this.source.tableName, this.source = source;
this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName this.target = target;
); this.targetAssociation = null;
this.options = options;
this.sequelize = source.modelManager.sequelize;
this.through = _.assign({}, options.through);
this.scope = options.scope;
this.isMultiAssociation = true;
this.isSelfAssociation = this.source === this.target;
this.doubleLinked = false;
this.as = this.options.as;
if (!this.as && this.isSelfAssociation) {
throw new Error('\'as\' must be defined for many-to-many self-associations');
}
/* if (this.as) {
* If self association, this is the target association - Unless we find a pairing association this.isAliased = true;
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
/* if (Utils._.isPlainObject(this.as)) {
* Default/generated foreign/other keys this.options.name = this.as;
*/ this.as = this.as.plural;
if (_.isObject(this.options.foreignKey)) { } else {
this.foreignKeyAttribute = this.options.foreignKey; this.options.name = {
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; plural: this.as,
} else { singular: Utils.singularize(this.as)
if (!this.options.foreignKey) { };
this.foreignKeyDefault = true; }
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
} }
this.foreignKeyAttribute = {}; this.combinedTableName = Utils.combineTableNames(
this.foreignKey = this.options.foreignKey || Utils.camelizeIf( this.source.tableName,
[ this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
Utils.underscoredIf(this.options.as || this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
); );
}
if (_.isObject(this.options.otherKey)) { /*
this.otherKeyAttribute = this.options.otherKey; * If self association, this is the target association - Unless we find a pairing association
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName; */
} else { if (this.isSelfAssociation) {
if (!this.options.otherKey) { this.targetAssociation = this;
this.otherKeyDefault = true;
} }
this.otherKeyAttribute = {}; /*
this.otherKey = this.options.otherKey || Utils.camelizeIf( * Default/generated foreign/other keys
[ */
Utils.underscoredIf( if (_.isObject(this.options.foreignKey)) {
this.isSelfAssociation ? this.foreignKeyAttribute = this.options.foreignKey;
Utils.singularize(this.as) : this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
this.target.options.name.singular, } else {
this.target.options.underscored if (!this.options.foreignKey) {
), this.foreignKeyDefault = true;
this.target.primaryKeyAttribute }
].join('_'),
!this.target.options.underscored
);
}
/*
* Find paired association (if exists)
*/
_.each(this.target.associations, function(association) {
if (association.associationType !== 'BelongsToMany') return;
if (association.target !== this.source) return;
if (this.options.through.model === association.options.through.model) { this.foreignKeyAttribute = {};
this.paired = association; this.foreignKey = this.options.foreignKey || Utils.camelizeIf(
association.paired = this; [
Utils.underscoredIf(this.options.as || this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
} }
}.bind(this));
if (_.isObject(this.options.otherKey)) {
if (typeof this.through.model === 'string') { this.otherKeyAttribute = this.options.otherKey;
if (!this.sequelize.isDefined(this.through.model)) { this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through.model,
indexes: [], //we don't want indexes here (as referenced in #2416)
paranoid: false, // A paranoid join table does not make sense
validate: {} // Don't propagate model-level validations
}));
} else { } else {
this.through.model = this.sequelize.model(this.through.model); if (!this.options.otherKey) {
} this.otherKeyDefault = true;
} }
if (this.paired) { this.otherKeyAttribute = {};
if (this.otherKeyDefault) { this.otherKey = this.options.otherKey || Utils.camelizeIf(
this.otherKey = this.paired.foreignKey; [
Utils.underscoredIf(
this.isSelfAssociation ?
Utils.singularize(this.as) :
this.target.options.name.singular,
this.target.options.underscored
),
this.target.primaryKeyAttribute
].join('_'),
!this.target.options.underscored
);
} }
if (this.paired.otherKeyDefault) {
// If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey /*
if (this.paired.otherKey !== this.foreignKey) { * Find paired association (if exists)
delete this.through.model.rawAttributes[this.paired.otherKey]; */
_.each(this.target.associations, association => {
if (association.associationType !== 'BelongsToMany') return;
if (association.target !== this.source) return;
if (this.options.through.model === association.options.through.model) {
this.paired = association;
association.paired = this;
} }
this.paired.otherKey = this.foreignKey; });
this.paired.foreignIdentifier = this.foreignKey;
delete this.paired.foreignIdentifierField;
}
}
if (this.through) { if (typeof this.through.model === 'string') {
this.throughModel = this.through.model; if (!this.sequelize.isDefined(this.through.model)) {
} this.through.model = this.sequelize.define(this.through.model, {}, _.extend(this.options, {
tableName: this.through.model,
indexes: [], //we don't want indexes here (as referenced in #2416)
paranoid: false, // A paranoid join table does not make sense
validate: {} // Don't propagate model-level validations
}));
} else {
this.through.model = this.sequelize.model(this.through.model);
}
}
this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model); if (this.paired) {
if (this.otherKeyDefault) {
this.associationAccessor = this.as; this.otherKey = this.paired.foreignKey;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
var plural = Utils.uppercaseFirst(this.options.name.plural)
, singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
/**
* Get 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
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findAll} for a full explanation of options
* @return {Promise<Array<Instance>>}
* @method getAssociations
*/
get: 'get' + plural,
/**
* Set the associated models by passing an array of instances or their primary keys. Everything that it not in the passed array will be un-associated.
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`. Can also hold additional attributes for the join table
* @param {Object} [options.validate] Run validation for the join model
* @return {Promise}
* @method setAssociations
*/
set: 'set' + plural,
/**
* Associate several persisted instances with this.
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociations
*/
addMultiple: 'add' + plural,
/**
* Associate a persisted instance with this.
*
* @param {Instance|String|Number} [newAssociation] A persisted instance or primary key of instance to associate with this.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociation
*/
add: 'add' + singular,
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Object} [values]
* @param {Object} [options] Options passed to create and add. Can also hold additional attributes for the join table
* @return {Promise}
* @method createAssociation
*/
create: 'create' + singular,
/**
* Un-associate the instance.
*
* @param {Instance|String|Number} [oldAssociated] Can be an Instance or its primary key
* @param {Object} [options] Options passed to `through.destroy`
* @return {Promise}
* @method removeAssociation
*/
remove: 'remove' + singular,
/**
* Un-associate several instances.
*
* @param {Array<Instance|String|Number>} [oldAssociated] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to `through.destroy`
* @return {Promise}
* @method removeAssociations
*/
removeMultiple: 'remove' + plural,
/**
* Check if an instance is associated with this.
*
* @param {Instance|String|Number} [instance] Can be an Instance or its primary key
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociation
*/
hasSingle: 'has' + singular,
/**
* Check if all instances are associated with this.
*
* @param {Array<Instance|String|Number>} [instances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociations
*/
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
};
};
util.inherits(BelongsToMany, Association);
// the id is in the target table
// or in an extra table which connects two tables
BelongsToMany.prototype.injectAttributes = function() {
var self = this;
this.identifier = this.foreignKey;
this.foreignIdentifier = this.otherKey;
// remove any PKs previously defined by sequelize
// but ignore any keys that are part of this association (#5865)
_.each(this.through.model.rawAttributes, function(attribute, attributeName) {
if (attribute.primaryKey === true && attribute._autoGenerated === true) {
if (attributeName === self.foreignKey || attributeName === self.otherKey) {
// this key is still needed as it's part of the association
// so just set primaryKey to false
attribute.primaryKey = false;
} }
else { if (this.paired.otherKeyDefault) {
delete self.through.model.rawAttributes[attributeName]; // If paired otherKey was inferred we should make sure to clean it up before adding a new one that matches the foreignKey
if (this.paired.otherKey !== this.foreignKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
}
this.paired.otherKey = this.foreignKey;
this.paired.foreignIdentifier = this.foreignKey;
delete this.paired.foreignIdentifierField;
} }
self.primaryKeyDeleted = true;
} }
});
var sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute]
, sourceKeyType = sourceKey.type
, sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute
, targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute]
, targetKeyType = targetKey.type
, targetKeyField = targetKey.field || this.target.primaryKeyAttribute
, sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType })
, targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });
if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) {
var uniqueKey = [this.through.model.tableName, this.foreignKey, this.otherKey, 'unique'].join('_');
targetAttribute.unique = sourceAttribute.unique = uniqueKey;
}
if (!this.through.model.rawAttributes[this.foreignKey]) { if (this.through) {
this.through.model.rawAttributes[this.foreignKey] = { this.throughModel = this.through.model;
_autoGenerated: true }
};
}
if (!this.through.model.rawAttributes[this.otherKey]) { this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model);
this.through.model.rawAttributes[this.otherKey] = {
_autoGenerated: true this.associationAccessor = this.as;
// Get singular and plural names, trying 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);
this.accessors = {
/**
* Get 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
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findAll} for a full explanation of options
* @return {Promise<Array<Instance>>}
* @method getAssociations
*/
get: 'get' + plural,
/**
* Set the associated models by passing an array of instances or their primary keys. Everything that it not in the passed array will be un-associated.
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`. Can also hold additional attributes for the join table
* @param {Object} [options.validate] Run validation for the join model
* @return {Promise}
* @method setAssociations
*/
set: 'set' + plural,
/**
* Associate several persisted instances with this.
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociations
*/
addMultiple: 'add' + plural,
/**
* Associate a persisted instance with this.
*
* @param {Instance|String|Number} [newAssociation] A persisted instance or primary key of instance to associate with this.
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`. Can also hold additional attributes for the join table.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociation
*/
add: 'add' + singular,
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Object} [values]
* @param {Object} [options] Options passed to create and add. Can also hold additional attributes for the join table
* @return {Promise}
* @method createAssociation
*/
create: 'create' + singular,
/**
* Un-associate the instance.
*
* @param {Instance|String|Number} [oldAssociated] Can be an Instance or its primary key
* @param {Object} [options] Options passed to `through.destroy`
* @return {Promise}
* @method removeAssociation
*/
remove: 'remove' + singular,
/**
* Un-associate several instances.
*
* @param {Array<Instance|String|Number>} [oldAssociated] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to `through.destroy`
* @return {Promise}
* @method removeAssociations
*/
removeMultiple: 'remove' + plural,
/**
* Check if an instance is associated with this.
*
* @param {Instance|String|Number} [instance] Can be an Instance or its primary key
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociation
*/
hasSingle: 'has' + singular,
/**
* Check if all instances are associated with this.
*
* @param {Array<Instance|String|Number>} [instances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociations
*/
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.constraints !== false) { // the id is in the target table
sourceAttribute.references = { // or in an extra table which connects two tables
model: this.source.getTableName(), injectAttributes() {
key: sourceKeyField
}; this.identifier = this.foreignKey;
// For the source attribute the passed option is the priority this.foreignIdentifier = this.otherKey;
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.foreignKey].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.foreignKey].onUpdate; // remove any PKs previously defined by sequelize
// but ignore any keys that are part of this association (#5865)
_.each(this.through.model.rawAttributes, (attribute, attributeName) => {
if (attribute.primaryKey === true && attribute._autoGenerated === true) {
if (attributeName === this.foreignKey || attributeName === this.otherKey) {
// this key is still needed as it's part of the association
// so just set primaryKey to false
attribute.primaryKey = false;
}
else {
delete this.through.model.rawAttributes[attributeName];
}
this.primaryKeyDeleted = true;
}
});
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE'; const sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute];
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE'; const sourceKeyType = sourceKey.type;
const sourceKeyField = sourceKey.field || this.source.primaryKeyAttribute;
const targetKey = this.target.rawAttributes[this.target.primaryKeyAttribute];
const targetKeyType = targetKey.type;
const targetKeyField = targetKey.field || this.target.primaryKeyAttribute;
const sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType });
const targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });
if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) {
const uniqueKey = [this.through.model.tableName, this.foreignKey, this.otherKey, 'unique'].join('_');
targetAttribute.unique = sourceAttribute.unique = uniqueKey;
}
targetAttribute.references = { if (!this.through.model.rawAttributes[this.foreignKey]) {
model: this.target.getTableName(), this.through.model.rawAttributes[this.foreignKey] = {
key: targetKeyField _autoGenerated: true
}; };
// But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call) }
targetAttribute.onDelete = this.through.model.rawAttributes[this.otherKey].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.otherKey].onUpdate || this.options.onUpdate;
if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE'; if (!this.through.model.rawAttributes[this.otherKey]) {
if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE'; this.through.model.rawAttributes[this.otherKey] = {
} _autoGenerated: true
};
}
this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute); if (this.options.constraints !== false) {
this.through.model.rawAttributes[this.otherKey] = _.extend(this.through.model.rawAttributes[this.otherKey], targetAttribute); sourceAttribute.references = {
model: this.source.getTableName(),
key: sourceKeyField
};
// For the source attribute the passed option is the priority
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.foreignKey].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.foreignKey].onUpdate;
this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey; if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey; if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
if (this.paired && !this.paired.foreignIdentifierField) { targetAttribute.references = {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey; model: this.target.getTableName(),
} key: targetKeyField
};
// But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call)
targetAttribute.onDelete = this.through.model.rawAttributes[this.otherKey].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.otherKey].onUpdate || this.options.onUpdate;
if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE';
if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
}
this.through.model.rawAttributes[this.foreignKey] = _.extend(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
this.through.model.rawAttributes[this.otherKey] = _.extend(this.through.model.rawAttributes[this.otherKey], targetAttribute);
this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
this.through.model.refreshAttributes(); if (this.paired && !this.paired.foreignIdentifierField) {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
this.toSource = new BelongsTo(this.through.model, this.source, { }
foreignKey: this.foreignKey
}); this.through.model.refreshAttributes();
this.manyFromSource = new HasMany(this.source, this.through.model, {
foreignKey: this.foreignKey this.toSource = new BelongsTo(this.through.model, this.source, {
}); foreignKey: this.foreignKey
this.oneFromSource = new HasOne(this.source, this.through.model, { });
foreignKey: this.foreignKey, this.manyFromSource = new HasMany(this.source, this.through.model, {
as: this.through.model.name foreignKey: this.foreignKey
}); });
this.oneFromSource = new HasOne(this.source, this.through.model, {
this.toTarget = new BelongsTo(this.through.model, this.target, { foreignKey: this.foreignKey,
foreignKey: this.otherKey as: this.through.model.name
});
this.manyFromTarget = new HasMany(this.target, this.through.model, {
foreignKey: this.otherKey
});
this.oneFromTarget = new HasOne(this.target, this.through.model, {
foreignKey: this.otherKey,
as: this.through.model.name
});
if (this.paired && this.paired.otherKeyDefault) {
this.paired.toTarget = new BelongsTo(this.paired.through.model, this.paired.target, {
foreignKey: this.paired.otherKey
}); });
this.paired.oneFromTarget = new HasOne(this.paired.target, this.paired.through.model, { this.toTarget = new BelongsTo(this.through.model, this.target, {
foreignKey: this.paired.otherKey, foreignKey: this.otherKey
as: this.paired.through.model.name
}); });
this.manyFromTarget = new HasMany(this.target, this.through.model, {
foreignKey: this.otherKey
});
this.oneFromTarget = new HasOne(this.target, this.through.model, {
foreignKey: this.otherKey,
as: this.through.model.name
});
if (this.paired && this.paired.otherKeyDefault) {
this.paired.toTarget = new BelongsTo(this.paired.through.model, this.paired.target, {
foreignKey: this.paired.otherKey
});
this.paired.oneFromTarget = new HasOne(this.paired.target, this.paired.through.model, {
foreignKey: this.paired.otherKey,
as: this.paired.through.model.name
});
}
Helpers.checkNamingCollision(this);
return this;
} }
Helpers.checkNamingCollision(this); injectGetter(obj) {
const association = this;
return this; obj[this.accessors.get] = function(options) {
}; options = Utils.cloneDeep(options) || {};
BelongsToMany.prototype.injectGetter = function(obj) { const through = association.through;
var association = this; let scopeWhere;
let throughWhere;
obj[this.accessors.get] = function(options) { if (association.scope) {
options = Utils.cloneDeep(options) || {}; scopeWhere = _.clone(association.scope);
}
var instance = this options.where = {
, through = association.through $and: [
, scopeWhere scopeWhere,
, throughWhere; options.where
]
};
if (association.scope) { if (Object(through.model) === through.model) {
scopeWhere = _.clone(association.scope); throughWhere = {};
} throughWhere[association.foreignKey] = this.get(association.source.primaryKeyAttribute);
options.where = { if (through.scope) {
$and: [ _.assign(throughWhere, through.scope);
scopeWhere, }
options.where
]
};
if (Object(through.model) === through.model) { //If a user pass a where on the options through options, make an "and" with the current throughWhere
throughWhere = {}; if (options.through && options.through.where) {
throughWhere[association.foreignKey] = instance.get(association.source.primaryKeyAttribute); throughWhere = {
$and: [throughWhere, options.through.where]
};
}
if (through.scope) { options.include = options.include || [];
_.assign(throughWhere, through.scope); options.include.push({
association: association.oneFromTarget,
attributes: options.joinTableAttributes,
required: true,
where: throughWhere
});
} }
//If a user pass a where on the options through options, make an "and" with the current throughWhere let model = association.target;
if (options.through && options.through.where) { if (options.hasOwnProperty('scope')) {
throughWhere = { if (!options.scope) {
$and: [throughWhere, options.through.where] model = model.unscoped();
}; } else {
model = model.scope(options.scope);
}
} }
options.include = options.include || []; if (options.hasOwnProperty('schema')) {
options.include.push({ model = model.schema(options.schema, options.schemaDelimiter);
association: association.oneFromTarget,
attributes: options.joinTableAttributes,
required: true,
where: throughWhere
});
}
var model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
} }
}
if (options.hasOwnProperty('schema')) { return model.findAll(options);
model = model.schema(options.schema, options.schemaDelimiter); };
}
return model.findAll(options); obj[this.accessors.count] = function(options) {
}; const model = association.target;
const sequelize = model.sequelize;
obj[this.accessors.count] = function(options) { options = Utils.cloneDeep(options);
var model = association.target options.attributes = [
, sequelize = model.sequelize; [sequelize.fn('COUNT', sequelize.col([association.target.name, model.primaryKeyAttribute].join('.'))), 'count']
];
options.joinTableAttributes = [];
options.raw = true;
options.plain = true;
options = Utils.cloneDeep(options); return obj[association.accessors.get].call(this, options).then(result => parseInt(result.count, 10));
options.attributes = [ };
[sequelize.fn('COUNT', sequelize.col([association.target.name, model.primaryKeyAttribute].join('.'))), 'count']
];
options.joinTableAttributes = [];
options.raw = true;
options.plain = true;
return obj[association.accessors.get].call(this, options).then(function (result) { obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
return parseInt(result.count, 10); const where = {};
});
};
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) { if (!Array.isArray(instances)) {
var where = {}; instances = [instances];
}
if (!Array.isArray(instances)) { options = _.assign({
instances = [instances]; raw: true
} }, options, {
scope: false
});
options = _.assign({ where.$or = instances.map(instance => {
raw: true if (instance instanceof association.target) {
}, options, { return instance.where();
scope: false } else {
}); const $where = {};
$where[association.target.primaryKeyAttribute] = instance;
return $where;
}
});
where.$or = instances.map(function (instance) { options.where = {
if (instance instanceof association.target) { $and: [
return instance.where(); where,
} else { options.where
var $where = {}; ]
$where[association.target.primaryKeyAttribute] = instance; };
return $where;
}
});
options.where = { return this[association.accessors.get](options).then(associatedObjects => associatedObjects.length === instances.length);
$and: [
where,
options.where
]
}; };
return this[association.accessors.get](options).then(function(associatedObjects) { return this;
return associatedObjects.length === instances.length; }
});
};
return this; injectSetter(obj) {
}; const association = this;
BelongsToMany.prototype.injectSetter = function(obj) { obj[this.accessors.set] = function(newAssociatedObjects, options) {
var association = this; options = options || {};
const sourceKey = association.source.primaryKeyAttribute;
const targetKey = association.target.primaryKeyAttribute;
const identifier = association.identifier;
const foreignIdentifier = association.foreignIdentifier;
const where = {};
obj[this.accessors.set] = function(newAssociatedObjects, options) { if (newAssociatedObjects === null) {
options = options || {}; newAssociatedObjects = [];
var instance = this } else {
, sourceKey = association.source.primaryKeyAttribute newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
, targetKey = association.target.primaryKeyAttribute }
, identifier = association.identifier
, foreignIdentifier = association.foreignIdentifier
, where = {};
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
} else {
newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
}
where[identifier] = this.get(sourceKey); where[identifier] = this.get(sourceKey);
return association.through.model.findAll(_.defaults({ return association.through.model.findAll(_.defaults({where, raw: true}, options)).then(currentRows => {
where: where, const obsoleteAssociations = [];
raw: true, const promises = [];
}, options)).then(function (currentRows) { let defaultAttributes = options;
var obsoleteAssociations = []
, defaultAttributes = options
, promises = []
, unassociatedObjects;
// Don't try to insert the transaction as an attribute in the through table
defaultAttributes = _.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
unassociatedObjects = newAssociatedObjects.filter(function(obj) {
return !_.find(currentRows, function(currentRow) {
return currentRow[foreignIdentifier] === obj.get(targetKey);
});
});
currentRows.forEach(function(currentRow) { // Don't try to insert the transaction as an attribute in the through table
var newObj = _.find(newAssociatedObjects, function(obj) { defaultAttributes = _.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
return currentRow[foreignIdentifier] === obj.get(targetKey);
});
if (!newObj) { const unassociatedObjects = newAssociatedObjects.filter(obj =>
obsoleteAssociations.push(currentRow); !_.find(currentRows, currentRow => currentRow[foreignIdentifier] === obj.get(targetKey))
} else { );
var throughAttributes = newObj[association.through.model.name];
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object) for (const currentRow of currentRows) {
if (throughAttributes instanceof association.through.model) { const newObj = _.find(newAssociatedObjects, obj => currentRow[foreignIdentifier] === obj.get(targetKey));
throughAttributes = {};
}
var where = {} if (!newObj) {
, attributes = _.defaults({}, throughAttributes, defaultAttributes); obsoleteAssociations.push(currentRow);
} else {
let throughAttributes = newObj[association.through.model.name];
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof association.through.model) {
throughAttributes = {};
}
where[identifier] = instance.get(sourceKey); const where = {};
where[foreignIdentifier] = newObj.get(targetKey); const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (Object.keys(attributes).length) { where[identifier] = this.get(sourceKey);
promises.push(association.through.model.update(attributes, _.extend(options, { where[foreignIdentifier] = newObj.get(targetKey);
where: where
}))); if (Object.keys(attributes).length) {
promises.push(association.through.model.update(attributes, _.extend(options, {where})));
}
} }
} }
});
if (obsoleteAssociations.length > 0) { if (obsoleteAssociations.length > 0) {
var where = {}; const where = {};
where[identifier] = instance.get(sourceKey); where[identifier] = this.get(sourceKey);
where[foreignIdentifier] = obsoleteAssociations.map(function(obsoleteAssociation) { where[foreignIdentifier] = obsoleteAssociations.map(obsoleteAssociation => obsoleteAssociation[foreignIdentifier]);
return obsoleteAssociation[foreignIdentifier];
});
promises.push(association.through.model.destroy(_.defaults({
where: where
}, options)));
}
if (unassociatedObjects.length > 0) { promises.push(association.through.model.destroy(_.defaults({where}, options)));
var bulk = unassociatedObjects.map(function(unassociatedObject) { }
var attributes = {};
attributes[identifier] = instance.get(sourceKey); if (unassociatedObjects.length > 0) {
attributes[foreignIdentifier] = unassociatedObject.get(targetKey); const bulk = unassociatedObjects.map(unassociatedObject => {
let attributes = {};
attributes = _.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes); attributes[identifier] = this.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
_.assign(attributes, association.through.scope); attributes = _.defaults(attributes, unassociatedObject[association.through.model.name], defaultAttributes);
return attributes; _.assign(attributes, association.through.scope);
}.bind(this));
promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options))); return attributes;
} });
return Utils.Promise.all(promises); promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options)));
}); }
};
obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstances, additionalAttributes) { return Utils.Promise.all(promises);
// If newInstances is null or undefined, no-op });
if (!newInstances) return Utils.Promise.resolve(); };
additionalAttributes = _.clone(additionalAttributes) || {}; obj[this.accessors.addMultiple] = obj[this.accessors.add] = function(newInstances, additionalAttributes) {
// If newInstances is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve();
var instance = this additionalAttributes = _.clone(additionalAttributes) || {};
, defaultAttributes = _.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging'])
, sourceKey = association.source.primaryKeyAttribute
, targetKey = association.target.primaryKeyAttribute
, identifier = association.identifier
, foreignIdentifier = association.foreignIdentifier
, options = additionalAttributes;
newInstances = association.toInstanceArray(newInstances); const defaultAttributes = _.omit(additionalAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
const sourceKey = association.source.primaryKeyAttribute;
const targetKey = association.target.primaryKeyAttribute;
const identifier = association.identifier;
const foreignIdentifier = association.foreignIdentifier;
const options = additionalAttributes;
var where = {}; newInstances = association.toInstanceArray(newInstances);
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = newInstances.map(function (newInstance) { return newInstance.get(targetKey); });
_.assign(where, association.through.scope); const where = {};
where[identifier] = this.get(sourceKey);
where[foreignIdentifier] = newInstances.map(newInstance => newInstance.get(targetKey));
return association.through.model.findAll(_.defaults({ _.assign(where, association.through.scope);
where: where,
raw: true,
}, options)).then(function (currentRows) {
var promises = [];
var unassociatedObjects = [], changedAssociations = []; return association.through.model.findAll(_.defaults({where, raw: true}, options)).then(currentRows => {
newInstances.forEach(function(obj) { const promises = [];
var existingAssociation = _.find(currentRows, function(current) { const unassociatedObjects = [];
return current[foreignIdentifier] === obj.get(targetKey); const changedAssociations = [];
}); for (const obj of newInstances) {
const existingAssociation = _.find(currentRows, current => current[foreignIdentifier] === obj.get(targetKey));
if (!existingAssociation) { if (!existingAssociation) {
unassociatedObjects.push(obj); unassociatedObjects.push(obj);
} else { } else {
var throughAttributes = obj[association.through.model.name] const throughAttributes = obj[association.through.model.name];
, attributes = _.defaults({}, throughAttributes, defaultAttributes); const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (_.some(Object.keys(attributes), function (attribute) { if (_.some(Object.keys(attributes), attribute => attributes[attribute] !== existingAssociation[attribute])) {
return attributes[attribute] !== existingAssociation[attribute]; changedAssociations.push(obj);
})) { }
changedAssociations.push(obj);
} }
} }
});
if (unassociatedObjects.length > 0) {
var bulk = unassociatedObjects.map(function(unassociatedObject) {
var throughAttributes = unassociatedObject[association.through.model.name]
, attributes = _.defaults({}, throughAttributes, defaultAttributes);
attributes[identifier] = instance.get(sourceKey); if (unassociatedObjects.length > 0) {
attributes[foreignIdentifier] = unassociatedObject.get(targetKey); const bulk = unassociatedObjects.map(unassociatedObject => {
const throughAttributes = unassociatedObject[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
_.assign(attributes, association.through.scope); attributes[identifier] = this.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
return attributes; _.assign(attributes, association.through.scope);
}.bind(this));
promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options))); return attributes;
} });
changedAssociations.forEach(function(assoc) { promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options)));
var throughAttributes = assoc[association.through.model.name]
, attributes = _.defaults({}, throughAttributes, defaultAttributes)
, where = {};
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof association.through.model) {
throughAttributes = {};
} }
where[identifier] = instance.get(sourceKey); for (const assoc of changedAssociations) {
where[foreignIdentifier] = assoc.get(targetKey); let throughAttributes = assoc[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
const where = {};
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof association.through.model) {
throughAttributes = {};
}
promises.push(association.through.model.update(attributes, _.extend(options, { where[identifier] = this.get(sourceKey);
where: where where[foreignIdentifier] = assoc.get(targetKey);
})));
});
return Utils.Promise.all(promises); promises.push(association.through.model.update(attributes, _.extend(options, {where})));
}); }
};
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) { return Utils.Promise.all(promises);
options = options || {}; });
};
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects); obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) {
options = options || {};
var where = {}; oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
where[association.identifier] = this.get(association.source.primaryKeyAttribute);
where[association.foreignIdentifier] = oldAssociatedObjects.map(function (newInstance) { return newInstance.get(association.target.primaryKeyAttribute); });
return association.through.model.destroy(_.defaults({ const where = {};
where: where where[association.identifier] = this.get(association.source.primaryKeyAttribute);
}, options)); where[association.foreignIdentifier] = oldAssociatedObjects.map(newInstance => newInstance.get(association.target.primaryKeyAttribute));
};
return this; return association.through.model.destroy(_.defaults({where}, options));
}; };
BelongsToMany.prototype.injectCreator = function(obj) { return this;
var association = this; }
obj[this.accessors.create] = function(values, options) { injectCreator(obj) {
var instance = this; const association = this;
options = options || {};
values = values || {};
if (Array.isArray(options)) { obj[this.accessors.create] = function(values, options) {
options = { options = options || {};
fields: options values = values || {};
};
}
if (association.scope) { if (Array.isArray(options)) {
_.assign(values, association.scope); options = {
if (options.fields) { fields: options
options.fields = options.fields.concat(Object.keys(association.scope)); };
} }
}
// Create the related model instance if (association.scope) {
return association.target.create(values, options).then(function(newAssociatedObject) { _.assign(values, association.scope);
return instance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject); if (options.fields) {
}); options.fields = options.fields.concat(Object.keys(association.scope));
}; }
}
// Create the related model instance
return association.target.create(values, options).then(newAssociatedObject =>
this[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject)
);
};
return this; return this;
}; }
}
module.exports = BelongsToMany; module.exports = BelongsToMany;
module.exports.BelongsToMany = BelongsToMany;
module.exports.default = BelongsToMany;
'use strict'; 'use strict';
var Utils = require('./../utils') const Utils = require('./../utils');
, Helpers = require('./helpers') const Helpers = require('./helpers');
, _ = require('lodash') const _ = require('lodash');
, Transaction = require('../transaction') const Transaction = require('../transaction');
, Association = require('./base') const Association = require('./base');
, util = require('util');
/** /**
* One-to-one association * One-to-one association
...@@ -14,248 +13,247 @@ var Utils = require('./../utils') ...@@ -14,248 +13,247 @@ var Utils = require('./../utils')
* *
* @mixin BelongsTo * @mixin BelongsTo
*/ */
var BelongsTo = function(source, target, options) { class BelongsTo extends Association {
Association.call(this); constructor(source, target, options) {
super();
this.associationType = 'BelongsTo';
this.source = source; this.associationType = 'BelongsTo';
this.target = target; this.source = source;
this.options = options; this.target = target;
this.scope = options.scope; this.options = options;
this.isSingleAssociation = true; this.scope = options.scope;
this.isSelfAssociation = (this.source === this.target); this.isSingleAssociation = true;
this.as = this.options.as; this.isSelfAssociation = (this.source === this.target);
this.foreignKeyAttribute = {}; this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true; if (this.as) {
this.options.name = { this.isAliased = true;
singular: this.as this.options.name = {
}; singular: this.as
} else { };
this.as = this.target.options.name.singular; } else {
this.options.name = this.target.options.name; this.as = this.target.options.name.singular;
} this.options.name = this.target.options.name;
}
if (_.isObject(this.options.foreignKey)) { if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey; this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) { } else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey; this.foreignKey = this.options.foreignKey;
} }
if (!this.foreignKey) { if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf( this.foreignKey = Utils.camelizeIf(
[ [
Utils.underscoredIf(this.options.as || this.as, this.source.options.underscored), Utils.underscoredIf(this.options.as || this.as, this.source.options.underscored),
this.target.primaryKeyAttribute this.target.primaryKeyAttribute
].join('_'), ].join('_'),
!this.source.options.underscored !this.source.options.underscored
); );
} }
this.identifier = this.foreignKey; this.identifier = this.foreignKey;
if (this.source.rawAttributes[this.identifier]) { if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier; this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
} }
this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute; this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey; this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute; this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute;
this.targetIdentifier = this.targetKey; this.targetIdentifier = this.targetKey;
this.associationAccessor = this.as; this.associationAccessor = this.as;
this.options.useHooks = options.useHooks; this.options.useHooks = options.useHooks;
// Get singular name, trying to uppercase the first letter, unless the model forbids it // Get singular name, trying to uppercase the first letter, unless the model forbids it
var singular = Utils.uppercaseFirst(this.options.name.singular); const singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = { this.accessors = {
/** /**
* Get the associated instance. * Get the associated instance.
* *
* @param {Object} [options] * @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|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 {String} [options.schema] Apply a schema on the related model
* @see {Model#findOne} for a full explanation of options * @see {Model#findOne} for a full explanation of options
* @return {Promise<Instance>} * @return {Promise<Instance>}
* @method getAssociation * @method getAssociation
*/ */
get: 'get' + singular, get: 'get' + singular,
/** /**
* Set the associated model. * Set the associated model.
* *
* @param {Instance|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 {Instance|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 {Object} [options] Options passed to `this.save`
* @param {Boolean} [options.save=true] Skip saving this after setting the foreign key if false. * @param {Boolean} [options.save=true] Skip saving this after setting the foreign key if false.
* @return {Promise} * @return {Promise}
* @method setAssociation * @method setAssociation
*/ */
set: 'set' + singular, set: 'set' + singular,
/** /**
* Create a new instance of the associated model and associate it with this. * Create a new instance of the associated model and associate it with this.
* *
* @param {Object} [values] * @param {Object} [values]
* @param {Object} [options] Options passed to `target.create` and setAssociation. * @param {Object} [options] Options passed to `target.create` and setAssociation.
* @see {Model#create} for a full explanation of options * @see {Model#create} for a full explanation of options
* @return {Promise} * @return {Promise}
* @method createAssociation * @method createAssociation
*/ */
create: 'create' + singular create: 'create' + singular
}; };
};
util.inherits(BelongsTo, Association);
// the id is in the source table
BelongsTo.prototype.injectAttributes = function() {
var newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.target.rawAttributes[this.targetKey].type,
allowNull : true
});
if (this.options.constraints !== false) {
var source = this.source.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
this.options.onDelete = this.options.onDelete || (source.allowNull ? 'SET NULL' : 'NO ACTION');
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField); // the id is in the source table
Utils.mergeDefaults(this.source.rawAttributes, newAttributes); injectAttributes() {
const newAttributes = {};
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey; newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.target.rawAttributes[this.targetKey].type,
allowNull : true
});
this.source.refreshAttributes(); if (this.options.constraints !== false) {
const source = this.source.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
this.options.onDelete = this.options.onDelete || (source.allowNull ? 'SET NULL' : 'NO ACTION');
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
}
Helpers.checkNamingCollision(this); Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
return this; this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
};
BelongsTo.prototype.mixin = function(obj) { this.source.refreshAttributes();
var association = this;
obj[this.accessors.get] = function(options) { Helpers.checkNamingCollision(this);
return association.get(this, options);
};
association.injectSetter(obj); return this;
association.injectCreator(obj); }
};
BelongsTo.prototype.get = function(instances, options) { mixin(obj) {
var association = this const association = this;
, Target = association.target
, instance
, where = {};
options = Utils.cloneDeep(options); obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
if (options.hasOwnProperty('scope')) { association.injectSetter(obj);
if (!options.scope) { association.injectCreator(obj);
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
} }
if (options.hasOwnProperty('schema')) { get(instances, options) {
Target = Target.schema(options.schema, options.schemaDelimiter); const association = this;
} const where = {};
let Target = association.target;
let instance;
if (!Array.isArray(instances)) { options = Utils.cloneDeep(options);
instance = instances;
instances = undefined;
}
if (instances) { if (options.hasOwnProperty('scope')) {
where[association.targetKey] = { if (!options.scope) {
$in: instances.map(function (instance) { Target = Target.unscoped();
return instance.get(association.foreignKey); } else {
}) Target = Target.scope(options.scope);
}; }
} else { }
if (association.targetKeyIsPrimary && !options.where) {
return Target.findById(instance.get(association.foreignKey), options); if (options.hasOwnProperty('schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
}
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
if (instances) {
where[association.targetKey] = {
$in: instances.map(instance => instance.get(association.foreignKey))
};
} else { } else {
where[association.targetKey] = instance.get(association.foreignKey); if (association.targetKeyIsPrimary && !options.where) {
options.limit = null; return Target.findById(instance.get(association.foreignKey), options);
} else {
where[association.targetKey] = instance.get(association.foreignKey);
options.limit = null;
}
} }
}
options.where = options.where ? options.where = options.where ?
{$and: [where, options.where]} : {$and: [where, options.where]} :
where; where;
if (instances) { if (instances) {
return Target.findAll(options).then(function (results) { return Target.findAll(options).then(results => {
var result = {}; const result = {};
instances.forEach(function (instance) { for (const instance of instances) {
result[instance.get(association.foreignKey, {raw: true})] = null; result[instance.get(association.foreignKey, {raw: true})] = null;
}); }
results.forEach(function (instance) { for (const instance of results) {
result[instance.get(association.targetKey, {raw: true})] = instance; result[instance.get(association.targetKey, {raw: true})] = instance;
}); }
return result; return result;
}); });
}
return Target.findOne(options);
} }
return Target.findOne(options);
};
// Add setAssociaton method to the prototype of the model instance // Add setAssociaton method to the prototype of the model instance
BelongsTo.prototype.injectSetter = function(instancePrototype) { injectSetter(instancePrototype) {
var association = this; const association = this;
instancePrototype[this.accessors.set] = function(associatedInstance, options) { instancePrototype[this.accessors.set] = function(associatedInstance, options) {
options = options || {}; options = options || {};
var value = associatedInstance; let value = associatedInstance;
if (associatedInstance instanceof association.target) { if (associatedInstance instanceof association.target) {
value = associatedInstance[association.targetKey]; value = associatedInstance[association.targetKey];
} }
this.set(association.foreignKey, value); this.set(association.foreignKey, value);
if (options.save === false) return; if (options.save === false) return;
options = _.extend({ options = _.extend({
fields: [association.foreignKey], fields: [association.foreignKey],
allowNull: [association.foreignKey], allowNull: [association.foreignKey],
association: true association: true
}, options); }, options);
// passes the changed field to save, so only that field get updated. // passes the changed field to save, so only that field get updated.
return this.save(options); return this.save(options);
}; };
return this; return this;
}; }
// Add createAssociation method to the prototype of the model instance // Add createAssociation method to the prototype of the model instance
BelongsTo.prototype.injectCreator = function(instancePrototype) { injectCreator(instancePrototype) {
var association = this; const association = this;
instancePrototype[this.accessors.create] = function(values, fieldsOrOptions) { instancePrototype[this.accessors.create] = function(values, fieldsOrOptions) {
var instance = this const options = {};
, options = {};
if ((fieldsOrOptions || {}).transaction instanceof Transaction) { if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
options.transaction = fieldsOrOptions.transaction; options.transaction = fieldsOrOptions.transaction;
} }
options.logging = (fieldsOrOptions || {}).logging; options.logging = (fieldsOrOptions || {}).logging;
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) { return association.target.create(values, fieldsOrOptions).then(newAssociatedObject =>
return instance[association.accessors.set](newAssociatedObject, options); this[association.accessors.set](newAssociatedObject, options)
}); );
}; };
return this; return this;
}; }
}
module.exports = BelongsTo; module.exports = BelongsTo;
module.exports.BelongsTo = BelongsTo;
module.exports.default = BelongsTo;
'use strict'; 'use strict';
var Utils = require('./../utils') const Utils = require('./../utils');
, Helpers = require('./helpers') const Helpers = require('./helpers');
, _ = require('lodash') const _ = require('lodash');
, Association = require('./base') const Association = require('./base');
, util = require('util');
/** /**
* One-to-many association * One-to-many association
...@@ -13,520 +12,500 @@ var Utils = require('./../utils') ...@@ -13,520 +12,500 @@ var Utils = require('./../utils')
* *
* @mixin HasMany * @mixin HasMany
*/ */
var HasMany = function(source, target, options) { class HasMany extends Association {
Association.call(this); constructor(source, target, options) {
super();
this.associationType = 'HasMany';
this.source = source; this.associationType = 'HasMany';
this.target = target; this.source = source;
this.targetAssociation = null; this.target = target;
this.options = options || {}; this.targetAssociation = null;
this.sequelize = source.modelManager.sequelize; this.options = options || {};
this.through = options.through; this.sequelize = source.modelManager.sequelize;
this.scope = options.scope; this.through = options.through;
this.isMultiAssociation = true; this.scope = options.scope;
this.isSelfAssociation = this.source === this.target; this.isMultiAssociation = true;
this.as = this.options.as; this.isSelfAssociation = this.source === this.target;
this.foreignKeyAttribute = {}; this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.options.through) {
throw new Error('N:M associations are not supported with hasMany. Use belongsToMany instead'); if (this.options.through) {
} throw new Error('N:M associations are not supported with hasMany. Use belongsToMany instead');
}
/* /*
* If self association, this is the target association * If self association, this is the target association
*/ */
if (this.isSelfAssociation) { if (this.isSelfAssociation) {
this.targetAssociation = this; this.targetAssociation = this;
} }
if (this.as) {
this.isAliased = true;
if (_.isPlainObject(this.as)) { if (this.as) {
this.options.name = this.as; this.isAliased = true;
this.as = this.as.plural;
if (_.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
} else {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
};
}
} else { } else {
this.options.name = { this.as = this.target.options.name.plural;
plural: this.as, this.options.name = this.target.options.name;
singular: Utils.singularize(this.as)
};
} }
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
/* /*
* Foreign key setup * Foreign key setup
*/ */
if (_.isObject(this.options.foreignKey)) { if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey; this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) { } else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey; this.foreignKey = this.options.foreignKey;
} }
if (!this.foreignKey) { if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf( this.foreignKey = Utils.camelizeIf(
[ [
Utils.underscoredIf(this.options.as || this.source.options.name.singular, this.source.options.underscored), Utils.underscoredIf(this.options.as || this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute this.source.primaryKeyAttribute
].join('_'), ].join('_'),
!this.source.options.underscored !this.source.options.underscored
); );
}
if (this.target.rawAttributes[this.foreignKey]) {
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
this.associationAccessor = this.as;
// Get singular and plural names, trying 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);
this.accessors = {
/**
* Get 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
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findAll} for a full explanation of options
* @return {Promise<Array<Instance>>}
* @method getAssociations
*/
get: 'get' + plural,
/**
* Set the associated models by passing an array of persisted instances or their primary keys. Everything that is not in the passed array will be un-associated
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations.
* @param {Object} [options] Options passed to `target.findAll` and `update`.
* @param {Object} [options.validate] Run validation for the join model
* @return {Promise}
* @method setAssociations
*/
set: 'set' + plural,
/**
* Associate several persisted instances with this.
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this.
* @param {Object} [options] Options passed to `target.update`.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociations
*/
addMultiple: 'add' + plural,
/**
* Associate a persisted instance with this.
*
* @param {Instance|String|Number} [newAssociation] A persisted instance or primary key of instance to associate with this.
* @param {Object} [options] Options passed to `target.update`.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociation
*/
add: 'add' + singular,
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Object} [values]
* @param {Object} [options] Options passed to `target.create`.
* @return {Promise}
* @method createAssociation
*/
create: 'create' + singular,
/**
* Un-associate the instance.
*
* @param {Instance|String|Number} [oldAssociated] Can be an Instance or its primary key
* @param {Object} [options] Options passed to `target.update`
* @return {Promise}
* @method removeAssociation
*/
remove: 'remove' + singular,
/**
* Un-associate several instances.
*
* @param {Array<Instance|String|Number>} [oldAssociatedArray] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to `through.destroy`
* @return {Promise}
* @method removeAssociations
*/
removeMultiple: 'remove' + plural,
/**
* Check if an instance is associated with this.
*
* @param {Instance|String|Number} [instance] Can be an Instance or its primary key
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociation
*/
hasSingle: 'has' + singular,
/**
* Check if all instances are associated with this.
*
* @param {Array<Instance|String|Number>} [instances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociations
*/
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.target.rawAttributes[this.foreignKey]) { // the id is in the target table
// or in an extra table which connects two tables
injectAttributes() {
const newAttributes = {};
const constraintOptions = _.clone(this.options); // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.source.primaryKeyAttribute].type,
allowNull : true
});
if (this.options.constraints !== false) {
const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
this.associationAccessor = this.as; this.target.refreshAttributes();
this.source.refreshAttributes();
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
var plural = Utils.uppercaseFirst(this.options.name.plural) Helpers.checkNamingCollision(this);
, singular = Utils.uppercaseFirst(this.options.name.singular);
return this;
this.accessors = {
/**
* Get 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
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findAll} for a full explanation of options
* @return {Promise<Array<Instance>>}
* @method getAssociations
*/
get: 'get' + plural,
/**
* Set the associated models by passing an array of persisted instances or their primary keys. Everything that is not in the passed array will be un-associated
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations.
* @param {Object} [options] Options passed to `target.findAll` and `update`.
* @param {Object} [options.validate] Run validation for the join model
* @return {Promise}
* @method setAssociations
*/
set: 'set' + plural,
/**
* Associate several persisted instances with this.
*
* @param {Array<Instance|String|Number>} [newAssociations] An array of persisted instances or primary key of instances to associate with this.
* @param {Object} [options] Options passed to `target.update`.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociations
*/
addMultiple: 'add' + plural,
/**
* Associate a persisted instance with this.
*
* @param {Instance|String|Number} [newAssociation] A persisted instance or primary key of instance to associate with this.
* @param {Object} [options] Options passed to `target.update`.
* @param {Object} [options.validate] Run validation for the join model.
* @return {Promise}
* @method addAssociation
*/
add: 'add' + singular,
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Object} [values]
* @param {Object} [options] Options passed to `target.create`.
* @return {Promise}
* @method createAssociation
*/
create: 'create' + singular,
/**
* Un-associate the instance.
*
* @param {Instance|String|Number} [oldAssociated] Can be an Instance or its primary key
* @param {Object} [options] Options passed to `target.update`
* @return {Promise}
* @method removeAssociation
*/
remove: 'remove' + singular,
/**
* Un-associate several instances.
*
* @param {Array<Instance|String|Number>} [oldAssociatedArray] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to `through.destroy`
* @return {Promise}
* @method removeAssociations
*/
removeMultiple: 'remove' + plural,
/**
* Check if an instance is associated with this.
*
* @param {Instance|String|Number} [instance] Can be an Instance or its primary key
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociation
*/
hasSingle: 'has' + singular,
/**
* Check if all instances are associated with this.
*
* @param {Array<Instance|String|Number>} [instances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
* @return {Promise}
* @method hasAssociations
*/
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
};
};
util.inherits(HasMany, Association);
// the id is in the target table
// or in an extra table which connects two tables
HasMany.prototype.injectAttributes = function() {
var newAttributes = {};
var constraintOptions = _.clone(this.options); // Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.source.primaryKeyAttribute].type,
allowNull : true
});
if (this.options.constraints !== false) {
var target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; mixin(obj) {
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; const association = this;
this.target.refreshAttributes(); obj[this.accessors.get] = function(options) {
this.source.refreshAttributes(); return association.get(this, options);
};
Helpers.checkNamingCollision(this); if (this.accessors.count) {
obj[this.accessors.count] = function(options) {
return association.count(this, options);
};
}
return this; obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
}; return association.has(this, instances, options);
};
HasMany.prototype.mixin = function(obj) { obj[this.accessors.set] = function(instances, options) {
var association = this; return association.set(this, instances, options);
};
obj[this.accessors.get] = function(options) { obj[this.accessors.add] = obj[this.accessors.addMultiple] = function(instances, options) {
return association.get(this, options); return association.add(this, instances, options);
}; };
if (this.accessors.count) { obj[this.accessors.remove] = obj[this.accessors.removeMultiple] = function(instances, options) {
obj[this.accessors.count] = function(options) { return association.remove(this, instances, options);
return association.count(this, options);
}; };
}
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) { obj[this.accessors.create] = function(values, options) {
return association.has(this, instances, options); return association.create(this, values, options);
}; };
obj[this.accessors.set] = function(instances, options) {
return association.set(this, instances, options);
};
obj[this.accessors.add] = obj[this.accessors.addMultiple] = function(instances, options) {
return association.add(this, instances, options);
};
obj[this.accessors.remove] = obj[this.accessors.removeMultiple] = function(instances, options) {
return association.remove(this, instances, options);
};
obj[this.accessors.create] = function(values, options) {
return association.create(this, values, options);
};
};
HasMany.prototype.get = function(instances, options) {
var association = this
, where = {}
, Model = association.target
, instance
, values;
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
} }
options = Utils.cloneDeep(options) || {}; get(instances, options) {
const association = this;
const where = {};
let Model = association.target;
let instance;
let values;
if (association.scope) { if (!Array.isArray(instances)) {
_.assign(where, association.scope); instance = instances;
} instances = undefined;
}
if (instances) { options = Utils.cloneDeep(options) || {};
values = instances.map(function (instance) {
return instance.get(association.source.primaryKeyAttribute, {raw: true});
});
if (options.limit && instances.length > 1) { if (association.scope) {
options.groupedLimit = { _.assign(where, association.scope);
limit: options.limit, }
on: association.foreignKeyField,
values: values
};
delete options.limit; if (instances) {
values = instances.map(instance => instance.get(association.source.primaryKeyAttribute, {raw: true}));
if (options.limit && instances.length > 1) {
options.groupedLimit = {
limit: options.limit,
on: association.foreignKeyField,
values
};
delete options.limit;
} else {
where[association.foreignKey] = {
$in: values
};
delete options.groupedLimit;
}
} else { } else {
where[association.foreignKey] = { where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true});
$in: values
};
delete options.groupedLimit;
} }
} else {
where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true});
}
options.where = options.where ? options.where = options.where ?
{$and: [where, options.where]} : {$and: [where, options.where]} :
where; where;
if (options.hasOwnProperty('scope')) { if (options.hasOwnProperty('scope')) {
if (!options.scope) { if (!options.scope) {
Model = Model.unscoped(); Model = Model.unscoped();
} else { } else {
Model = Model.scope(options.scope); Model = Model.scope(options.scope);
}
} }
}
if (options.hasOwnProperty('schema')) { if (options.hasOwnProperty('schema')) {
Model = Model.schema(options.schema, options.schemaDelimiter); Model = Model.schema(options.schema, options.schemaDelimiter);
} }
return Model.findAll(options).then(function (results) { return Model.findAll(options).then(results => {
if (instance) return results; if (instance) return results;
var result = {}; const result = {};
instances.forEach(function (instance) { for (const instance of instances) {
result[instance.get(association.source.primaryKeyAttribute, {raw: true})] = []; result[instance.get(association.source.primaryKeyAttribute, {raw: true})] = [];
}); }
results.forEach(function (instance) { for (const instance of results) {
result[instance.get(association.foreignKey, {raw: true})].push(instance); result[instance.get(association.foreignKey, {raw: true})].push(instance);
}); }
return result; return result;
}); });
};
HasMany.prototype.count = function(instance, options) {
var association = this
, model = association.target
, sequelize = model.sequelize;
options = Utils.cloneDeep(options);
options.attributes = [
[sequelize.fn('COUNT', sequelize.col(model.primaryKeyField)), 'count']
];
options.raw = true;
options.plain = true;
return this.get(instance, options).then(function (result) {
return parseInt(result.count, 10);
});
};
HasMany.prototype.has = function(sourceInstance, targetInstances, options) {
var association = this
, where = {};
if (!Array.isArray(targetInstances)) {
targetInstances = [targetInstances];
} }
options = _.assign({}, options, { count(instance, options) {
scope: false, const association = this;
raw: true const model = association.target;
}); const sequelize = model.sequelize;
where.$or = targetInstances.map(function (instance) { options = Utils.cloneDeep(options);
if (instance instanceof association.target) { options.attributes = [
return instance.where(); [sequelize.fn('COUNT', sequelize.col(model.primaryKeyField)), 'count']
} else { ];
var _where = {}; options.raw = true;
_where[association.target.primaryKeyAttribute] = instance; options.plain = true;
return _where;
} return this.get(instance, options).then(result => parseInt(result.count, 10));
});
options.where = {
$and: [
where,
options.where
]
};
return this.get(
sourceInstance,
options
).then(function(associatedObjects) {
return associatedObjects.length === targetInstances.length;
});
};
HasMany.prototype.set = function(sourceInstance, targetInstances, options) {
var association = this;
if (targetInstances === null) {
targetInstances = [];
} else {
targetInstances = association.toInstanceArray(targetInstances);
} }
return association.get(sourceInstance, _.defaults({ has(sourceInstance, targetInstances, options) {
scope: false, const association = this;
raw: true const where = {};
}, options)).then(function(oldAssociations) {
var promises = [] if (!Array.isArray(targetInstances)) {
, obsoleteAssociations = oldAssociations.filter(function(old) { targetInstances = [targetInstances];
return !_.find(targetInstances, function(obj) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, unassociatedObjects = targetInstances.filter(function(obj) {
return !_.find(oldAssociations, function(old) {
return obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute];
});
})
, updateWhere
, update;
if (obsoleteAssociations.length > 0) {
update = {};
update[association.foreignKey] = null;
updateWhere = {};
updateWhere[association.target.primaryKeyAttribute] = obsoleteAssociations.map(function(associatedObject) {
return associatedObject[association.target.primaryKeyAttribute];
});
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
} }
if (unassociatedObjects.length > 0) { options = _.assign({}, options, {
updateWhere = {}; scope: false,
raw: true
});
update = {}; where.$or = targetInstances.map(instance => {
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute); if (instance instanceof association.target) {
return instance.where();
} else {
const _where = {};
_where[association.target.primaryKeyAttribute] = instance;
return _where;
}
});
_.assign(update, association.scope); options.where = {
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) { $and: [
return unassociatedObject[association.target.primaryKeyAttribute]; where,
}); options.where
]
};
promises.push(association.target.unscoped().update( return this.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length);
update, }
_.defaults({
where: updateWhere set(sourceInstance, targetInstances, options) {
}, options) const association = this;
));
if (targetInstances === null) {
targetInstances = [];
} else {
targetInstances = association.toInstanceArray(targetInstances);
} }
return Utils.Promise.all(promises).return(sourceInstance); return association.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]
)
);
const unassociatedObjects = targetInstances.filter(obj =>
!_.find(oldAssociations, old =>
obj[association.target.primaryKeyAttribute] === old[association.target.primaryKeyAttribute]
)
);
let updateWhere;
let update;
if (obsoleteAssociations.length > 0) {
update = {};
update[association.foreignKey] = null;
updateWhere = {};
updateWhere[association.target.primaryKeyAttribute] = obsoleteAssociations.map(associatedObject =>
associatedObject[association.target.primaryKeyAttribute]
);
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
if (unassociatedObjects.length > 0) {
updateWhere = {};
update = {};
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(unassociatedObject =>
unassociatedObject[association.target.primaryKeyAttribute]
);
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
return Utils.Promise.all(promises).return(sourceInstance);
});
}
HasMany.prototype.add = function(sourceInstance, targetInstances, options) { add(sourceInstance, targetInstances, options) {
if (!targetInstances) return Utils.Promise.resolve(); if (!targetInstances) return Utils.Promise.resolve();
var association = this const association = this;
, update = {} const update = {};
, where = {}; const where = {};
options = options || {}; options = options || {};
targetInstances = association.toInstanceArray(targetInstances); targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute); update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope); _.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (unassociatedObject) { where[association.target.primaryKeyAttribute] = targetInstances.map(unassociatedObject =>
return unassociatedObject.get(association.target.primaryKeyAttribute); unassociatedObject.get(association.target.primaryKeyAttribute)
}); );
return association.target.unscoped().update( return association.target.unscoped().update(update, _.defaults({where}, options)).return(sourceInstance);
update, }
_.defaults({
where: where
}, options)
).return(sourceInstance);
};
HasMany.prototype.remove = function(sourceInstance, targetInstances, options) { remove(sourceInstance, targetInstances, options) {
var association = this const association = this;
, update = {} const update = {};
, where = {}; const where = {};
options = options || {}; options = options || {};
targetInstances = association.toInstanceArray(targetInstances); targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = null; update[association.foreignKey] = null;
where[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute); where[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (targetInstance) { where[association.target.primaryKeyAttribute] = targetInstances.map(targetInstance =>
return targetInstance.get(association.target.primaryKeyAttribute); targetInstance.get(association.target.primaryKeyAttribute)
}); );
return association.target.unscoped().update( return association.target.unscoped().update(update, _.defaults({where}, options)).return(this);
update, }
_.defaults({
where: where
}, options)
).return(this);
};
HasMany.prototype.create = function(sourceInstance, values, options) { create(sourceInstance, values, options) {
var association = this; const association = this;
options = options || {}; options = options || {};
if (Array.isArray(options)) { if (Array.isArray(options)) {
options = { options = {
fields: options fields: options
}; };
} }
if (values === undefined) { if (values === undefined) {
values = {}; values = {};
} }
if (association.scope) { if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) { for (const attribute of Object.keys(association.scope)) {
values[attribute] = association.scope[attribute]; values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute); if (options.fields) options.fields.push(attribute);
}); }
} }
values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute); values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.foreignKey); if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options); return association.target.create(values, options);
}; }
}
module.exports = HasMany; module.exports = HasMany;
module.exports.HasMany = HasMany;
module.exports.default = HasMany;
'use strict'; 'use strict';
var Utils = require('./../utils') const Utils = require('./../utils');
, Helpers = require('./helpers') const Helpers = require('./helpers');
, _ = require('lodash') const _ = require('lodash');
, Association = require('./base') const Association = require('./base');
, util = require('util');
/** /**
* One-to-one association * One-to-one association
...@@ -14,263 +13,259 @@ var Utils = require('./../utils') ...@@ -14,263 +13,259 @@ var Utils = require('./../utils')
* *
* @mixin HasOne * @mixin HasOne
*/ */
var HasOne = function(srcModel, targetModel, options) { class HasOne extends Association {
Association.call(this); constructor(srcModel, targetModel, options) {
super();
this.associationType = 'HasOne';
this.source = srcModel; this.associationType = 'HasOne';
this.target = targetModel; this.source = srcModel;
this.options = options; this.target = targetModel;
this.scope = options.scope; this.options = options;
this.isSingleAssociation = true; this.scope = options.scope;
this.isSelfAssociation = (this.source === this.target); this.isSingleAssociation = true;
this.as = this.options.as; this.isSelfAssociation = (this.source === this.target);
this.foreignKeyAttribute = {}; this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true; if (this.as) {
this.options.name = { this.isAliased = true;
singular: this.as this.options.name = {
}; singular: this.as
} else { };
this.as = this.target.options.name.singular; } else {
this.options.name = this.target.options.name; this.as = this.target.options.name.singular;
} this.options.name = this.target.options.name;
}
if (_.isObject(this.options.foreignKey)) { if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey; this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName; this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) { } else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey; this.foreignKey = this.options.foreignKey;
} }
if (!this.foreignKey) { if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf( this.foreignKey = Utils.camelizeIf(
[ [
Utils.underscoredIf(Utils.singularize(this.options.as || this.source.name), this.target.options.underscored), Utils.underscoredIf(Utils.singularize(this.options.as || this.source.name), this.target.options.underscored),
this.source.primaryKeyAttribute this.source.primaryKeyAttribute
].join('_'), ].join('_'),
!this.source.options.underscored !this.source.options.underscored
); );
} }
this.sourceIdentifier = this.source.primaryKeyAttribute; this.sourceIdentifier = this.source.primaryKeyAttribute;
this.sourceKey = this.source.primaryKeyAttribute; this.sourceKey = this.source.primaryKeyAttribute;
this.sourceKeyIsPrimary = this.sourceKey === this.source.primaryKeyAttribute; this.sourceKeyIsPrimary = this.sourceKey === this.source.primaryKeyAttribute;
this.associationAccessor = this.as; this.associationAccessor = this.as;
this.options.useHooks = options.useHooks; this.options.useHooks = options.useHooks;
if (this.target.rawAttributes[this.foreignKey]) { if (this.target.rawAttributes[this.foreignKey]) {
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey; this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
} }
// Get singular name, trying to uppercase the first letter, unless the model forbids it // Get singular name, trying to uppercase the first letter, unless the model forbids it
var singular = Utils.uppercaseFirst(this.options.name.singular); const singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = { this.accessors = {
/** /**
* Get the associated instance. * Get the associated instance.
* *
* @param {Object} [options] * @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|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 {String} [options.schema] Apply a schema on the related model
* @see {Model#findOne} for a full explanation of options * @see {Model#findOne} for a full explanation of options
* @return {Promise<Instance>} * @return {Promise<Instance>}
* @method getAssociation * @method getAssociation
*/ */
get: 'get' + singular, get: 'get' + singular,
/** /**
* Set the associated model. * Set the associated model.
* *
* @param {Instance|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 {Instance|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 {Object} [options] Options passed to getAssociation and `target.save`
* @return {Promise} * @return {Promise}
* @method setAssociation * @method setAssociation
*/ */
set: 'set' + singular, set: 'set' + singular,
/** /**
* Create a new instance of the associated model and associate it with this. * Create a new instance of the associated model and associate it with this.
* *
* @param {Object} [values] * @param {Object} [values]
* @param {Object} [options] Options passed to `target.create` and setAssociation. * @param {Object} [options] Options passed to `target.create` and setAssociation.
* @see {Model#create} for a full explanation of options * @see {Model#create} for a full explanation of options
* @return {Promise} * @return {Promise}
* @method createAssociation * @method createAssociation
*/ */
create: 'create' + singular create: 'create' + singular
}; };
};
util.inherits(HasOne, Association);
// the id is in the target table
HasOne.prototype.injectAttributes = function() {
var newAttributes = {}
, keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type;
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || keyType,
allowNull : true
});
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
if (this.options.constraints !== false) {
var target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
this.options.onDelete = this.options.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
} }
Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.foreignKey], this.source, this.target, this.options); // the id is in the target table
injectAttributes() {
const newAttributes = {};
const keyType = this.source.rawAttributes[this.source.primaryKeyAttribute].type;
// Sync attributes and setters/getters to Model prototype newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
this.target.refreshAttributes(); type: this.options.keyType || keyType,
allowNull : true
});
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
Helpers.checkNamingCollision(this); this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
return this; if (this.options.constraints !== false) {
}; const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
this.options.onDelete = this.options.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
}
HasOne.prototype.mixin = function(obj) { Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.foreignKey], this.source, this.target, this.options);
var association = this;
obj[this.accessors.get] = function(options) { // Sync attributes and setters/getters to Model prototype
return association.get(this, options); this.target.refreshAttributes();
};
association.injectSetter(obj); Helpers.checkNamingCollision(this);
association.injectCreator(obj);
};
HasOne.prototype.get = function(instances, options) { return this;
var association = this }
, Target = association.target
, instance
, where = {};
options = Utils.cloneDeep(options); mixin(obj) {
const association = this;
if (options.hasOwnProperty('scope')) { obj[this.accessors.get] = function(options) {
if (!options.scope) { return association.get(this, options);
Target = Target.unscoped(); };
} else {
Target = Target.scope(options.scope);
}
}
if (options.hasOwnProperty('schema')) { association.injectSetter(obj);
Target = Target.schema(options.schema, options.schemaDelimiter); association.injectCreator(obj);
} }
if (!Array.isArray(instances)) { get(instances, options) {
instance = instances; const association = this;
instances = undefined; const where = {};
} let Target = association.target;
let instance;
if (instances) { options = Utils.cloneDeep(options);
where[association.foreignKey] = {
$in: instances.map(function (instance) {
return instance.get(association.sourceKey);
})
};
} else {
where[association.foreignKey] = instance.get(association.sourceKey);
}
if (association.scope) { if (options.hasOwnProperty('scope')) {
_.assign(where, association.scope); if (!options.scope) {
} Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
options.where = options.where ? if (options.hasOwnProperty('schema')) {
{$and: [where, options.where]} : Target = Target.schema(options.schema, options.schemaDelimiter);
where; }
if (instances) { if (!Array.isArray(instances)) {
return Target.findAll(options).then(function (results) { instance = instances;
var result = {}; instances = undefined;
instances.forEach(function (instance) { }
result[instance.get(association.sourceKey, {raw: true})] = null;
});
results.forEach(function (instance) { if (instances) {
result[instance.get(association.foreignKey, {raw: true})] = instance; where[association.foreignKey] = {
}); $in: instances.map(instance => instance.get(association.sourceKey))
};
} else {
where[association.foreignKey] = instance.get(association.sourceKey);
}
return result; if (association.scope) {
}); _.assign(where, association.scope);
} }
return Target.findOne(options);
};
HasOne.prototype.injectSetter = function(instancePrototype) { options.where = options.where ?
var association = this; {$and: [where, options.where]} :
where;
instancePrototype[this.accessors.set] = function(associatedInstance, options) { if (instances) {
var instance = this, return Target.findAll(options).then(results => {
alreadyAssociated; const result = {};
for (const instance of instances) {
result[instance.get(association.sourceKey, {raw: true})] = null;
}
options = _.assign({}, options, { for (const instance of results) {
scope: false result[instance.get(association.foreignKey, {raw: true})] = instance;
}); }
return instance[association.accessors.get](options).then(function(oldInstance) {
// TODO Use equals method once #5605 is resolved return result;
alreadyAssociated = oldInstance && associatedInstance && _.every(association.target.primaryKeyAttributes, function(attribute) {
return oldInstance.get(attribute, {raw: true}) === associatedInstance.get(attribute, {raw: true});
}); });
}
return Target.findOne(options);
}
if (oldInstance && !alreadyAssociated) { injectSetter(instancePrototype) {
oldInstance[association.foreignKey] = null; const association = this;
return oldInstance.save(_.extend({}, options, {
fields: [association.foreignKey],
allowNull: [association.foreignKey],
association: true
}));
}
}).then(function() {
if (associatedInstance && !alreadyAssociated) {
if (!(associatedInstance instanceof association.target)) {
var tmpInstance = {};
tmpInstance[association.target.primaryKeyAttribute] = associatedInstance;
associatedInstance = association.target.build(tmpInstance, {
isNewRecord: false
});
}
_.assign(associatedInstance, association.scope); instancePrototype[this.accessors.set] = function(associatedInstance, options) {
associatedInstance.set(association.foreignKey, instance.get(association.sourceIdentifier)); let alreadyAssociated;
return associatedInstance.save(options); options = _.assign({}, options, {
} scope: false
return null; });
}); return this[association.accessors.get](options).then(oldInstance => {
}; // TODO Use equals method once #5605 is resolved
alreadyAssociated = oldInstance && associatedInstance && _.every(association.target.primaryKeyAttributes, attribute =>
oldInstance.get(attribute, {raw: true}) === associatedInstance.get(attribute, {raw: true})
);
if (oldInstance && !alreadyAssociated) {
oldInstance[association.foreignKey] = null;
return oldInstance.save(_.extend({}, options, {
fields: [association.foreignKey],
allowNull: [association.foreignKey],
association: true
}));
}
}).then(() => {
if (associatedInstance && !alreadyAssociated) {
if (!(associatedInstance instanceof association.target)) {
const tmpInstance = {};
tmpInstance[association.target.primaryKeyAttribute] = associatedInstance;
associatedInstance = association.target.build(tmpInstance, {
isNewRecord: false
});
}
_.assign(associatedInstance, association.scope);
associatedInstance.set(association.foreignKey, this.get(association.sourceIdentifier));
return associatedInstance.save(options);
}
return null;
});
};
return this; return this;
}; }
HasOne.prototype.injectCreator = function(instancePrototype) { injectCreator(instancePrototype) {
var association = this; const association = this;
instancePrototype[this.accessors.create] = function(values, options) { instancePrototype[this.accessors.create] = function(values, options) {
var instance = this; values = values || {};
values = values || {}; options = options || {};
options = options || {};
if (association.scope) { if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) { for (const attribute of Object.keys(association.scope)) {
values[attribute] = association.scope[attribute]; values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute); if (options.fields) options.fields.push(attribute);
}); }
} }
values[association.foreignKey] = instance.get(association.sourceIdentifier); values[association.foreignKey] = this.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.foreignKey); if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options); return association.target.create(values, options);
}; };
return this; return this;
}; }
}
module.exports = HasOne; module.exports = HasOne;
'use strict'; 'use strict';
var Utils = require('./../utils'); const Utils = require('./../utils');
function checkNamingCollision (association) { function checkNamingCollision (association) {
if (association.source.rawAttributes.hasOwnProperty(association.as)) { if (association.source.rawAttributes.hasOwnProperty(association.as)) {
...@@ -11,6 +11,7 @@ function checkNamingCollision (association) { ...@@ -11,6 +11,7 @@ function checkNamingCollision (association) {
); );
} }
} }
exports.checkNamingCollision = checkNamingCollision;
function addForeignKeyConstraints (newAttribute, source, target, options, key) { function addForeignKeyConstraints (newAttribute, source, target, options, key) {
// FK constraints are opt-in: users must either set `foreignKeyConstraints` // FK constraints are opt-in: users must either set `foreignKeyConstraints`
...@@ -19,9 +20,9 @@ function addForeignKeyConstraints (newAttribute, source, target, options, key) { ...@@ -19,9 +20,9 @@ function addForeignKeyConstraints (newAttribute, source, target, options, key) {
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) { if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach // Find primary keys: composite keys not supported with this approach
var primaryKeys = Utils._.chain(source.rawAttributes).keys() const primaryKeys = Utils._.chain(source.rawAttributes).keys()
.filter(function($key) { return source.rawAttributes[$key].primaryKey; }) .filter($key => source.rawAttributes[$key].primaryKey)
.map(function($key) { return source.rawAttributes[$key].field || $key; }).value(); .map($key => source.rawAttributes[$key].field || $key).value();
if (primaryKeys.length === 1) { if (primaryKeys.length === 1) {
if (!!source.$schema) { if (!!source.$schema) {
...@@ -42,8 +43,4 @@ function addForeignKeyConstraints (newAttribute, source, target, options, key) { ...@@ -42,8 +43,4 @@ function addForeignKeyConstraints (newAttribute, source, target, options, key) {
} }
} }
} }
exports.addForeignKeyConstraints = addForeignKeyConstraints;
module.exports = {
checkNamingCollision: checkNamingCollision,
addForeignKeyConstraints: addForeignKeyConstraints
};
'use strict'; 'use strict';
var Association = require('./base'); const Association = require('./base');
Association.BelongsTo = require('./belongs-to'); Association.BelongsTo = require('./belongs-to');
Association.HasOne = require('./has-one'); Association.HasOne = require('./has-one');
Association.HasMany = require('./has-many'); Association.HasMany = require('./has-many');
Association.BelongsToMany = require('./belongs-to-many'); Association.BelongsToMany = require('./belongs-to-many');
module.exports = Association; module.exports = Association;
module.exports.default = Association;
module.exports.Association = Association;
'use strict'; 'use strict';
var Utils = require('./../utils') const Utils = require('./../utils');
, _ = require('lodash') const _ = require('lodash');
, HasOne = require('./has-one') const HasOne = require('./has-one');
, HasMany = require('./has-many') const HasMany = require('./has-many');
, BelongsToMany = require('./belongs-to-many') const BelongsToMany = require('./belongs-to-many');
, BelongsTo = require('./belongs-to'); const BelongsTo = require('./belongs-to');
/** /**
* Creating associations in sequelize is done by calling one of the belongsTo / hasOne / hasMany / belongsToMany functions on a model (the source), and providing another model as the first argument to the function (the target). * Creating associations in sequelize is done by calling one of the belongsTo / hasOne / hasMany / belongsToMany functions on a model (the source), and providing another model as the first argument to the function (the target).
...@@ -86,24 +86,157 @@ var Utils = require('./../utils') ...@@ -86,24 +86,157 @@ var Utils = require('./../utils')
* @mixin Associations * @mixin Associations
* @name Associations * @name Associations
*/ */
var Mixin = module.exports = function() {}; const Mixin = {
/**
* Creates a 1:m association between this (the source) and the provided target. The foreign key is added on the target.
*
* Example: `User.hasMany(Profile)`. This will add userId to the profile table.
*
* @param {Model} target
* @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {string|object} [options.as] The alias of this model. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the association, you should provide the same alias when eager loading and when getting associated models. Defaults to the pluralized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the target table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of source + primary key of source
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target. (sqlite not supported for N:M)
* @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] SET NULL if foreignKey allows nulls, CASCADE if otherwise
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/
hasMany(target, options) { // testhint options:none
if (!target.prototype || !(target.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not a subclass of Sequelize.Model');
}
const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options = _.extend(options, _.omit(source.options, ['hooks']));
// the id is in the foreign table or in a connecting table
const association = new HasMany(source, target, options);
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association.mixin(source.prototype);
return association;
},
/**
* Create an N:M association with a join table.
*
* ```js
* User.belongsToMany(Project, { through: 'UserProjects' })
* Project.belongsToMany(User, { through: 'UserProjects' })
* ```
* Defining `through` is required. Sequelize would previously attempt to auto generate names but that would not always lead to the most logical setups.
*
* If you define a through model with custom attributes, these attributes can be set when adding / setting new associations in two ways. Consider users and projects from before with a join table that stores whether the project has been started yet:
* ```js
* let UserProjects = sequelize.define('UserProjects', {
* started: Sequelize.BOOLEAN
* })
* User.belongsToMany(Project, { through: UserProjects })
* Project.belongsToMany(User, { through: UserProjects })
* ```
* ```js
* jan.addProject(homework, { started: false }) // The homework project is not started yet
* jan.setProjects([makedinner, doshopping], { started: true}) // Both shopping and dinner has been started
* ```
*
* If you want to set several target instances, but with different attributes you have to set the attributes on the instance, using a property with the name of the through model:
*
* ```js
* p1.UserProjects = {
* started: true
* }
* user.setProjects([p1, p2], {started: false}) // The default value is false, but p1 overrides that.
* ```
*
* Similarly, when fetching through a join table with custom attributes, these attributes will be available as an object with the name of the through model.
* ```js
* user.getProjects().then(function (projects) {
* let p1 = projects[0]
* p1.UserProjects.started // Is this project started yet?
* })
* ```
*
* @param {Model} target
* @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {Model|string|object} [options.through] The name of the table that is used to join source and target in n:m associations. Can also be a sequelize model if you want to define the junction table yourself and add extra attributes to it.
* @param {Model} [options.through.model] The model used to join both sides of the N:M association.
* @param {object} [options.through.scope] A key/value set that will be used for association create and find defaults on the through model. (Remember to add the attributes to the through model)
* @param {boolean} [options.through.unique=true] If true a unique key will be generated from the foreign keys used (might want to turn this off and create specific unique keys when using scopes)
* @param {string|object} [options.as] The alias of this association. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the association, you should provide the same alias when eager loading and when getting associated models. Defaults to the pluralized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the join table (representing the source model) or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of source + primary key of source
* @param {string|object} [options.otherKey] The name of the foreign key in the join table (representing the target model) or an object representing the type definition for the other column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of target + primary key of target
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target. (sqlite not supported for N:M)
* @param {boolean} [options.timestamps=sequelize.options.timestamps] Should the join model have timestamps
* @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] Cascade if this is a n:m, and set null if it is a 1:m
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/
belongsToMany(targetModel, options) { // testhint options:none
if (!targetModel.prototype || !(targetModel.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.belongsToMany called with something that\'s not a subclass of Sequelize.Model');
}
const sourceModel = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options.timestamps = options.timestamps === undefined ? this.sequelize.options.timestamps : options.timestamps;
options = _.extend(options, _.omit(sourceModel.options, ['hooks', 'timestamps', 'scopes', 'defaultScope']));
// the id is in the foreign table or in a connecting table
const association = new BelongsToMany(sourceModel, targetModel, options);
sourceModel.associations[association.associationAccessor] = association.injectAttributes();
association.injectGetter(sourceModel.prototype);
association.injectSetter(sourceModel.prototype);
association.injectCreator(sourceModel.prototype);
return association;
},
getAssociation(target, alias) {
for (const associationName in this.associations) {
if (this.associations.hasOwnProperty(associationName)) {
const association = this.associations[associationName];
if (association.target.name === target.name && (alias === undefined ? !association.isAliased : association.as === alias)) {
return association;
}
}
}
return null;
}
};
// The logic for hasOne and belongsTo is exactly the same // The logic for hasOne and belongsTo is exactly the same
var singleLinked = function (Type) { function singleLinked(Type) {
return function(target, options) { // testhint options:none return function(target, options) { // testhint options:none
if (!target.prototype || !(target.prototype instanceof this.sequelize.Model)) { if (!target.prototype || !(target.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.' + Utils.lowercaseFirst(Type.toString()) + ' called with something that\'s not a subclass of Sequelize.Model'); throw new Error(this.name + '.' + Utils.lowercaseFirst(Type.toString()) + ' called with something that\'s not a subclass of Sequelize.Model');
} }
var source = this; const source = this;
// Since this is a mixin, we'll need a unique variable name for hooks (since Model will override our hooks option) // Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options = options || {}; options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks); options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks; options.useHooks = options.hooks;
// the id is in the foreign table // the id is in the foreign table
var association = new Type(source, target, _.extend(options, source.options)); const association = new Type(source, target, _.extend(options, source.options));
source.associations[association.associationAccessor] = association.injectAttributes(); source.associations[association.associationAccessor] = association.injectAttributes();
if (association.mixin) { if (association.mixin) {
...@@ -116,7 +249,8 @@ var singleLinked = function (Type) { ...@@ -116,7 +249,8 @@ var singleLinked = function (Type) {
return association; return association;
}; };
}; }
/** /**
* Creates an association between this (the source) and the provided target. The foreign key is added on the target. * Creates an association between this (the source) and the provided target. The foreign key is added on the target.
* *
...@@ -152,135 +286,6 @@ Mixin.hasOne = singleLinked(HasOne); ...@@ -152,135 +286,6 @@ Mixin.hasOne = singleLinked(HasOne);
*/ */
Mixin.belongsTo = singleLinked(BelongsTo); Mixin.belongsTo = singleLinked(BelongsTo);
/** module.exports = Mixin;
* Creates a 1:m association between this (the source) and the provided target. The foreign key is added on the target. module.exports.Mixin = Mixin;
* module.exports.default = Mixin;
* Example: `User.hasMany(Profile)`. This will add userId to the profile table.
*
* @param {Model} target
* @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {string|object} [options.as] The alias of this model. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the association, you should provide the same alias when eager loading and when getting associated models. Defaults to the pluralized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the target table or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of source + primary key of source
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target. (sqlite not supported for N:M)
* @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] SET NULL if foreignKey allows nulls, CASCADE if otherwise
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/
Mixin.hasMany = function(target, options) { // testhint options:none
if (!target.prototype || !(target.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.hasMany called with something that\'s not a subclass of Sequelize.Model');
}
var source = this;
// Since this is a mixin, we'll need a unique variable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options = _.extend(options, _.omit(source.options, ['hooks']));
// the id is in the foreign table or in a connecting table
var association = new HasMany(source, target, options);
source.associations[association.associationAccessor] = association;
association.injectAttributes();
association.mixin(source.prototype);
return association;
};
/**
* Create an N:M association with a join table.
*
* ```js
* User.belongsToMany(Project, { through: 'UserProjects' })
* Project.belongsToMany(User, { through: 'UserProjects' })
* ```
* Defining `through` is required. Sequelize would previously attempt to auto generate names but that would not always lead to the most logical setups.
*
* If you define a through model with custom attributes, these attributes can be set when adding / setting new associations in two ways. Consider users and projects from before with a join table that stores whether the project has been started yet:
* ```js
* var UserProjects = sequelize.define('UserProjects', {
* started: Sequelize.BOOLEAN
* })
* User.belongsToMany(Project, { through: UserProjects })
* Project.belongsToMany(User, { through: UserProjects })
* ```
* ```js
* jan.addProject(homework, { started: false }) // The homework project is not started yet
* jan.setProjects([makedinner, doshopping], { started: true}) // Both shopping and dinner has been started
* ```
*
* If you want to set several target instances, but with different attributes you have to set the attributes on the instance, using a property with the name of the through model:
*
* ```js
* p1.UserProjects = {
* started: true
* }
* user.setProjects([p1, p2], {started: false}) // The default value is false, but p1 overrides that.
* ```
*
* Similarly, when fetching through a join table with custom attributes, these attributes will be available as an object with the name of the through model.
* ```js
* user.getProjects().then(function (projects) {
* var p1 = projects[0]
* p1.UserProjects.started // Is this project started yet?
* })
* ```
*
* @param {Model} target
* @param {object} [options]
* @param {boolean} [options.hooks=false] Set to true to run before-/afterDestroy hooks when an associated model is deleted because of a cascade. For example if `User.hasOne(Profile, {onDelete: 'cascade', hooks:true})`, the before-/afterDestroy hooks for profile will be called when a user is deleted. Otherwise the profile will be deleted without invoking any hooks
* @param {Model|string|object} [options.through] The name of the table that is used to join source and target in n:m associations. Can also be a sequelize model if you want to define the junction table yourself and add extra attributes to it.
* @param {Model} [options.through.model] The model used to join both sides of the N:M association.
* @param {object} [options.through.scope] A key/value set that will be used for association create and find defaults on the through model. (Remember to add the attributes to the through model)
* @param {boolean} [options.through.unique=true] If true a unique key will be generated from the foreign keys used (might want to turn this off and create specific unique keys when using scopes)
* @param {string|object} [options.as] The alias of this association. If you provide a string, it should be plural, and will be singularized using node.inflection. If you want to control the singular version yourself, provide an object with `plural` and `singular` keys. See also the `name` option passed to `sequelize.define`. If you create multiple associations between the same tables, you should provide an alias to be able to distinguish between them. If you provide an alias when creating the association, you should provide the same alias when eager loading and when getting associated models. Defaults to the pluralized name of target
* @param {string|object} [options.foreignKey] The name of the foreign key in the join table (representing the source model) or an object representing the type definition for the foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of source + primary key of source
* @param {string|object} [options.otherKey] The name of the foreign key in the join table (representing the target model) or an object representing the type definition for the other column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property to set the name of the column. Defaults to the name of target + primary key of target
* @param {object} [options.scope] A key/value set that will be used for association create and find defaults on the target. (sqlite not supported for N:M)
* @param {boolean} [options.timestamps=sequelize.options.timestamps] Should the join model have timestamps
* @param {string} [options.onDelete='SET&nbsp;NULL|CASCADE'] Cascade if this is a n:m, and set null if it is a 1:m
* @param {string} [options.onUpdate='CASCADE']
* @param {boolean} [options.constraints=true] Should on update and on delete constraints be enabled on the foreign key.
*/
Mixin.belongsToMany = function(targetModel, options) { // testhint options:none
if (!targetModel.prototype || !(targetModel.prototype instanceof this.sequelize.Model)) {
throw new Error(this.name + '.belongsToMany called with something that\'s not a subclass of Sequelize.Model');
}
var sourceModel = this;
// Since this is a mixin, we'll need a unique variable name for hooks (since Model will override our hooks option)
options = options || {};
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options.timestamps = options.timestamps === undefined ? this.sequelize.options.timestamps : options.timestamps;
options = _.extend(options, _.omit(sourceModel.options, ['hooks', 'timestamps', 'scopes', 'defaultScope']));
// the id is in the foreign table or in a connecting table
var association = new BelongsToMany(sourceModel, targetModel, options);
sourceModel.associations[association.associationAccessor] = association.injectAttributes();
association.injectGetter(sourceModel.prototype);
association.injectSetter(sourceModel.prototype);
association.injectCreator(sourceModel.prototype);
return association;
};
Mixin.getAssociation = function(target, alias) {
for (var associationName in this.associations) {
if (this.associations.hasOwnProperty(associationName)) {
var association = this.associations[associationName];
if (association.target.name === target.name && (alias === undefined ? !association.isAliased : association.as === alias)) {
return association;
}
}
}
return null;
};
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!