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

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';
var Utils = require('./../utils')
, Helpers = require('./helpers')
, _ = require('lodash')
, Association = require('./base')
, BelongsTo = require('./belongs-to')
, HasMany = require('./has-many')
, HasOne = require('./has-one')
, util = require('util');
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const BelongsTo = require('./belongs-to');
const HasMany = require('./has-many');
const HasOne = require('./has-one');
/**
* Many-to-many association with a join table.
......@@ -37,738 +36,706 @@ var Utils = require('./../utils')
*
* @mixin BelongsToMany
*/
var BelongsToMany = function(source, target, options) {
Association.call(this);
class BelongsToMany extends Association {
constructor(source, target, options) {
super();
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');
}
options = options || {};
if (this.as) {
this.isAliased = true;
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 (Utils._.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)
if (!options.through.model) {
options.through = {
model: options.through
};
}
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.combinedTableName = Utils.combineTableNames(
this.source.tableName,
this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
);
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 self association, this is the target association - Unless we find a pairing association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
if (this.as) {
this.isAliased = true;
/*
* Default/generated foreign/other keys
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
if (!this.options.foreignKey) {
this.foreignKeyDefault = true;
if (Utils._.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 {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelizeIf(
[
Utils.underscoredIf(this.options.as || this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
this.combinedTableName = Utils.combineTableNames(
this.source.tableName,
this.isSelfAssociation ? (this.as || this.target.tableName) : this.target.tableName
);
}
if (_.isObject(this.options.otherKey)) {
this.otherKeyAttribute = this.options.otherKey;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
} else {
if (!this.options.otherKey) {
this.otherKeyDefault = true;
/*
* If self association, this is the target association - Unless we find a pairing association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelizeIf(
[
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
);
}
/*
* Find paired association (if exists)
*/
_.each(this.target.associations, function(association) {
if (association.associationType !== 'BelongsToMany') return;
if (association.target !== this.source) return;
/*
* Default/generated foreign/other keys
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
if (!this.options.foreignKey) {
this.foreignKeyDefault = true;
}
if (this.options.through.model === association.options.through.model) {
this.paired = association;
association.paired = this;
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelizeIf(
[
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 (typeof this.through.model === 'string') {
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
}));
if (_.isObject(this.options.otherKey)) {
this.otherKeyAttribute = this.options.otherKey;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
} else {
this.through.model = this.sequelize.model(this.through.model);
}
}
if (!this.options.otherKey) {
this.otherKeyDefault = true;
}
if (this.paired) {
if (this.otherKeyDefault) {
this.otherKey = this.paired.foreignKey;
this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelizeIf(
[
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) {
delete this.through.model.rawAttributes[this.paired.otherKey];
/*
* Find paired association (if exists)
*/
_.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) {
this.throughModel = this.through.model;
}
if (typeof this.through.model === 'string') {
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);
this.associationAccessor = this.as;
// 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;
if (this.paired) {
if (this.otherKeyDefault) {
this.otherKey = this.paired.foreignKey;
}
else {
delete self.through.model.rawAttributes[attributeName];
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) {
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]) {
this.through.model.rawAttributes[this.foreignKey] = {
_autoGenerated: true
};
}
if (this.through) {
this.throughModel = this.through.model;
}
if (!this.through.model.rawAttributes[this.otherKey]) {
this.through.model.rawAttributes[this.otherKey] = {
_autoGenerated: true
this.options.tableName = this.combinedName = (this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model);
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) {
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;
// the id is in the target table
// or in an extra table which connects two tables
injectAttributes() {
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, (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';
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
const sourceKey = this.source.rawAttributes[this.source.primaryKeyAttribute];
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 = {
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 (!this.through.model.rawAttributes[this.foreignKey]) {
this.through.model.rawAttributes[this.foreignKey] = {
_autoGenerated: true
};
}
if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE';
if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
}
if (!this.through.model.rawAttributes[this.otherKey]) {
this.through.model.rawAttributes[this.otherKey] = {
_autoGenerated: true
};
}
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);
if (this.options.constraints !== false) {
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;
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
if (this.paired && !this.paired.foreignIdentifierField) {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
}
targetAttribute.references = {
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();
this.toSource = new BelongsTo(this.through.model, this.source, {
foreignKey: this.foreignKey
});
this.manyFromSource = new HasMany(this.source, this.through.model, {
foreignKey: this.foreignKey
});
this.oneFromSource = new HasOne(this.source, this.through.model, {
foreignKey: this.foreignKey,
as: this.through.model.name
});
this.toTarget = new BelongsTo(this.through.model, this.target, {
foreignKey: this.otherKey
});
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
if (this.paired && !this.paired.foreignIdentifierField) {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
}
this.through.model.refreshAttributes();
this.toSource = new BelongsTo(this.through.model, this.source, {
foreignKey: this.foreignKey
});
this.manyFromSource = new HasMany(this.source, this.through.model, {
foreignKey: this.foreignKey
});
this.oneFromSource = new HasOne(this.source, this.through.model, {
foreignKey: this.foreignKey,
as: this.through.model.name
});
this.paired.oneFromTarget = new HasOne(this.paired.target, this.paired.through.model, {
foreignKey: this.paired.otherKey,
as: this.paired.through.model.name
this.toTarget = new BelongsTo(this.through.model, this.target, {
foreignKey: this.otherKey
});
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) {
var association = this;
const through = association.through;
let scopeWhere;
let throughWhere;
obj[this.accessors.get] = function(options) {
options = Utils.cloneDeep(options) || {};
if (association.scope) {
scopeWhere = _.clone(association.scope);
}
var instance = this
, through = association.through
, scopeWhere
, throughWhere;
options.where = {
$and: [
scopeWhere,
options.where
]
};
if (association.scope) {
scopeWhere = _.clone(association.scope);
}
if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[association.foreignKey] = this.get(association.source.primaryKeyAttribute);
options.where = {
$and: [
scopeWhere,
options.where
]
};
if (through.scope) {
_.assign(throughWhere, through.scope);
}
if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[association.foreignKey] = instance.get(association.source.primaryKeyAttribute);
//If a user pass a where on the options through options, make an "and" with the current throughWhere
if (options.through && options.through.where) {
throughWhere = {
$and: [throughWhere, options.through.where]
};
}
if (through.scope) {
_.assign(throughWhere, through.scope);
options.include = options.include || [];
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
if (options.through && options.through.where) {
throughWhere = {
$and: [throughWhere, options.through.where]
};
let model = association.target;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
options.include = options.include || [];
options.include.push({
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')) {
model = model.schema(options.schema, options.schemaDelimiter);
}
}
if (options.hasOwnProperty('schema')) {
model = model.schema(options.schema, options.schemaDelimiter);
}
return model.findAll(options);
};
return model.findAll(options);
};
obj[this.accessors.count] = function(options) {
const model = association.target;
const sequelize = model.sequelize;
obj[this.accessors.count] = function(options) {
var model = association.target
, sequelize = model.sequelize;
options = Utils.cloneDeep(options);
options.attributes = [
[sequelize.fn('COUNT', sequelize.col([association.target.name, model.primaryKeyAttribute].join('.'))), 'count']
];
options.joinTableAttributes = [];
options.raw = true;
options.plain = true;
options = Utils.cloneDeep(options);
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(result => parseInt(result.count, 10));
};
return obj[association.accessors.get].call(this, options).then(function (result) {
return parseInt(result.count, 10);
});
};
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
const where = {};
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
var where = {};
if (!Array.isArray(instances)) {
instances = [instances];
}
if (!Array.isArray(instances)) {
instances = [instances];
}
options = _.assign({
raw: true
}, options, {
scope: false
});
options = _.assign({
raw: true
}, options, {
scope: false
});
where.$or = instances.map(instance => {
if (instance instanceof association.target) {
return instance.where();
} else {
const $where = {};
$where[association.target.primaryKeyAttribute] = instance;
return $where;
}
});
where.$or = instances.map(function (instance) {
if (instance instanceof association.target) {
return instance.where();
} else {
var $where = {};
$where[association.target.primaryKeyAttribute] = instance;
return $where;
}
});
options.where = {
$and: [
where,
options.where
]
};
options.where = {
$and: [
where,
options.where
]
return this[association.accessors.get](options).then(associatedObjects => associatedObjects.length === instances.length);
};
return this[association.accessors.get](options).then(function(associatedObjects) {
return associatedObjects.length === instances.length;
});
};
return this;
}
return this;
};
injectSetter(obj) {
const association = this;
BelongsToMany.prototype.injectSetter = function(obj) {
var association = this;
obj[this.accessors.set] = function(newAssociatedObjects, options) {
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) {
options = options || {};
var instance = this
, sourceKey = association.source.primaryKeyAttribute
, targetKey = association.target.primaryKeyAttribute
, identifier = association.identifier
, foreignIdentifier = association.foreignIdentifier
, where = {};
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
} else {
newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
}
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
} else {
newAssociatedObjects = association.toInstanceArray(newAssociatedObjects);
}
where[identifier] = this.get(sourceKey);
return association.through.model.findAll(_.defaults({
where: where,
raw: true,
}, options)).then(function (currentRows) {
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);
});
});
where[identifier] = this.get(sourceKey);
return association.through.model.findAll(_.defaults({where, raw: true}, options)).then(currentRows => {
const obsoleteAssociations = [];
const promises = [];
let defaultAttributes = options;
currentRows.forEach(function(currentRow) {
var newObj = _.find(newAssociatedObjects, function(obj) {
return currentRow[foreignIdentifier] === obj.get(targetKey);
});
// Don't try to insert the transaction as an attribute in the through table
defaultAttributes = _.omit(defaultAttributes, ['transaction', 'hooks', 'individualHooks', 'ignoreDuplicates', 'validate', 'fields', 'logging']);
if (!newObj) {
obsoleteAssociations.push(currentRow);
} 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)
if (throughAttributes instanceof association.through.model) {
throughAttributes = {};
}
const unassociatedObjects = newAssociatedObjects.filter(obj =>
!_.find(currentRows, currentRow => currentRow[foreignIdentifier] === obj.get(targetKey))
);
for (const currentRow of currentRows) {
const newObj = _.find(newAssociatedObjects, obj => currentRow[foreignIdentifier] === obj.get(targetKey));
var where = {}
, attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (!newObj) {
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);
where[foreignIdentifier] = newObj.get(targetKey);
const where = {};
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (Object.keys(attributes).length) {
promises.push(association.through.model.update(attributes, _.extend(options, {
where: where
})));
where[identifier] = this.get(sourceKey);
where[foreignIdentifier] = newObj.get(targetKey);
if (Object.keys(attributes).length) {
promises.push(association.through.model.update(attributes, _.extend(options, {where})));
}
}
}
});
if (obsoleteAssociations.length > 0) {
var where = {};
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = obsoleteAssociations.map(function(obsoleteAssociation) {
return obsoleteAssociation[foreignIdentifier];
});
promises.push(association.through.model.destroy(_.defaults({
where: where
}, options)));
}
if (obsoleteAssociations.length > 0) {
const where = {};
where[identifier] = this.get(sourceKey);
where[foreignIdentifier] = obsoleteAssociations.map(obsoleteAssociation => obsoleteAssociation[foreignIdentifier]);
if (unassociatedObjects.length > 0) {
var bulk = unassociatedObjects.map(function(unassociatedObject) {
var attributes = {};
promises.push(association.through.model.destroy(_.defaults({where}, options)));
}
attributes[identifier] = instance.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
if (unassociatedObjects.length > 0) {
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;
}.bind(this));
_.assign(attributes, association.through.scope);
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) {
// If newInstances is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve();
return Utils.Promise.all(promises);
});
};
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
, 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;
additionalAttributes = _.clone(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 = {};
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = newInstances.map(function (newInstance) { return newInstance.get(targetKey); });
newInstances = association.toInstanceArray(newInstances);
_.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({
where: where,
raw: true,
}, options)).then(function (currentRows) {
var promises = [];
_.assign(where, association.through.scope);
var unassociatedObjects = [], changedAssociations = [];
newInstances.forEach(function(obj) {
var existingAssociation = _.find(currentRows, function(current) {
return current[foreignIdentifier] === obj.get(targetKey);
});
return association.through.model.findAll(_.defaults({where, raw: true}, options)).then(currentRows => {
const promises = [];
const unassociatedObjects = [];
const changedAssociations = [];
for (const obj of newInstances) {
const existingAssociation = _.find(currentRows, current => current[foreignIdentifier] === obj.get(targetKey));
if (!existingAssociation) {
unassociatedObjects.push(obj);
} else {
var throughAttributes = obj[association.through.model.name]
, attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (!existingAssociation) {
unassociatedObjects.push(obj);
} else {
const throughAttributes = obj[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (_.some(Object.keys(attributes), function (attribute) {
return attributes[attribute] !== existingAssociation[attribute];
})) {
changedAssociations.push(obj);
if (_.some(Object.keys(attributes), attribute => attributes[attribute] !== existingAssociation[attribute])) {
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);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
if (unassociatedObjects.length > 0) {
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;
}.bind(this));
_.assign(attributes, association.through.scope);
promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options)));
}
return attributes;
});
changedAssociations.forEach(function(assoc) {
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 = {};
promises.push(association.through.model.bulkCreate(bulk, _.assign({ validate: true }, options)));
}
where[identifier] = instance.get(sourceKey);
where[foreignIdentifier] = assoc.get(targetKey);
for (const assoc of changedAssociations) {
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: where
})));
});
where[identifier] = this.get(sourceKey);
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) {
options = options || {};
return Utils.Promise.all(promises);
});
};
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
obj[this.accessors.removeMultiple] = obj[this.accessors.remove] = function(oldAssociatedObjects, options) {
options = options || {};
var where = {};
where[association.identifier] = this.get(association.source.primaryKeyAttribute);
where[association.foreignIdentifier] = oldAssociatedObjects.map(function (newInstance) { return newInstance.get(association.target.primaryKeyAttribute); });
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
return association.through.model.destroy(_.defaults({
where: where
}, options));
};
const where = {};
where[association.identifier] = this.get(association.source.primaryKeyAttribute);
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) {
var association = this;
return this;
}
obj[this.accessors.create] = function(values, options) {
var instance = this;
options = options || {};
values = values || {};
injectCreator(obj) {
const association = this;
if (Array.isArray(options)) {
options = {
fields: options
};
}
obj[this.accessors.create] = function(values, options) {
options = options || {};
values = values || {};
if (association.scope) {
_.assign(values, association.scope);
if (options.fields) {
options.fields = options.fields.concat(Object.keys(association.scope));
if (Array.isArray(options)) {
options = {
fields: options
};
}
}
// Create the related model instance
return association.target.create(values, options).then(function(newAssociatedObject) {
return instance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject);
});
};
if (association.scope) {
_.assign(values, association.scope);
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 = BelongsToMany;
module.exports.default = BelongsToMany;
'use strict';
var Utils = require('./../utils')
, Helpers = require('./helpers')
, _ = require('lodash')
, Transaction = require('../transaction')
, Association = require('./base')
, util = require('util');
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Transaction = require('../transaction');
const Association = require('./base');
/**
* One-to-one association
......@@ -14,248 +13,247 @@ var Utils = require('./../utils')
*
* @mixin BelongsTo
*/
var BelongsTo = function(source, target, options) {
Association.call(this);
this.associationType = 'BelongsTo';
this.source = source;
this.target = target;
this.options = options;
this.scope = options.scope;
this.isSingleAssociation = true;
this.isSelfAssociation = (this.source === this.target);
this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
this.options.name = {
singular: this.as
};
} else {
this.as = this.target.options.name.singular;
this.options.name = this.target.options.name;
}
class BelongsTo extends Association {
constructor(source, target, options) {
super();
this.associationType = 'BelongsTo';
this.source = source;
this.target = target;
this.options = options;
this.scope = options.scope;
this.isSingleAssociation = true;
this.isSelfAssociation = (this.source === this.target);
this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
this.options.name = {
singular: this.as
};
} else {
this.as = this.target.options.name.singular;
this.options.name = this.target.options.name;
}
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
[
Utils.underscoredIf(this.options.as || this.as, this.source.options.underscored),
this.target.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
[
Utils.underscoredIf(this.options.as || this.as, this.source.options.underscored),
this.target.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
this.identifier = this.foreignKey;
this.identifier = this.foreignKey;
if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute;
this.targetIdentifier = this.targetKey;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
// Get singular name, trying to uppercase the first letter, unless the model forbids it
var singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
/**
* Get the associated instance.
*
* @param {Object} [options]
* @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false.
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findOne} for a full explanation of options
* @return {Promise<Instance>}
* @method getAssociation
*/
get: 'get' + singular,
/**
* 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 {Object} [options] Options passed to `this.save`
* @param {Boolean} [options.save=true] Skip saving this after setting the foreign key if false.
* @return {Promise}
* @method setAssociation
*/
set: 'set' + 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` and setAssociation.
* @see {Model#create} for a full explanation of options
* @return {Promise}
* @method createAssociation
*/
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';
this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute;
this.targetIdentifier = this.targetKey;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
// Get singular name, trying to uppercase the first letter, unless the model forbids it
const singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
/**
* Get the associated instance.
*
* @param {Object} [options]
* @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false.
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findOne} for a full explanation of options
* @return {Promise<Instance>}
* @method getAssociation
*/
get: 'get' + singular,
/**
* 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 {Object} [options] Options passed to `this.save`
* @param {Boolean} [options.save=true] Skip saving this after setting the foreign key if false.
* @return {Promise}
* @method setAssociation
*/
set: 'set' + 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` and setAssociation.
* @see {Model#create} for a full explanation of options
* @return {Promise}
* @method createAssociation
*/
create: 'create' + singular
};
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
// the id is in the source table
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) {
var association = this;
this.source.refreshAttributes();
obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
Helpers.checkNamingCollision(this);
association.injectSetter(obj);
association.injectCreator(obj);
};
return this;
}
BelongsTo.prototype.get = function(instances, options) {
var association = this
, Target = association.target
, instance
, where = {};
mixin(obj) {
const association = this;
options = Utils.cloneDeep(options);
obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
association.injectSetter(obj);
association.injectCreator(obj);
}
if (options.hasOwnProperty('schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
}
get(instances, options) {
const association = this;
const where = {};
let Target = association.target;
let instance;
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
options = Utils.cloneDeep(options);
if (instances) {
where[association.targetKey] = {
$in: instances.map(function (instance) {
return instance.get(association.foreignKey);
})
};
} else {
if (association.targetKeyIsPrimary && !options.where) {
return Target.findById(instance.get(association.foreignKey), options);
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
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 {
where[association.targetKey] = instance.get(association.foreignKey);
options.limit = null;
if (association.targetKeyIsPrimary && !options.where) {
return Target.findById(instance.get(association.foreignKey), options);
} else {
where[association.targetKey] = instance.get(association.foreignKey);
options.limit = null;
}
}
}
options.where = options.where ?
{$and: [where, options.where]} :
where;
options.where = options.where ?
{$and: [where, options.where]} :
where;
if (instances) {
return Target.findAll(options).then(function (results) {
var result = {};
instances.forEach(function (instance) {
result[instance.get(association.foreignKey, {raw: true})] = null;
});
if (instances) {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(association.foreignKey, {raw: true})] = null;
}
results.forEach(function (instance) {
result[instance.get(association.targetKey, {raw: true})] = instance;
});
for (const instance of results) {
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
BelongsTo.prototype.injectSetter = function(instancePrototype) {
var association = this;
// Add setAssociaton method to the prototype of the model instance
injectSetter(instancePrototype) {
const association = this;
instancePrototype[this.accessors.set] = function(associatedInstance, options) {
options = options || {};
instancePrototype[this.accessors.set] = function(associatedInstance, options) {
options = options || {};
var value = associatedInstance;
if (associatedInstance instanceof association.target) {
value = associatedInstance[association.targetKey];
}
let value = associatedInstance;
if (associatedInstance instanceof association.target) {
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({
fields: [association.foreignKey],
allowNull: [association.foreignKey],
association: true
}, options);
options = _.extend({
fields: [association.foreignKey],
allowNull: [association.foreignKey],
association: true
}, options);
// passes the changed field to save, so only that field get updated.
return this.save(options);
};
// passes the changed field to save, so only that field get updated.
return this.save(options);
};
return this;
};
return this;
}
// Add createAssociation method to the prototype of the model instance
BelongsTo.prototype.injectCreator = function(instancePrototype) {
var association = this;
// Add createAssociation method to the prototype of the model instance
injectCreator(instancePrototype) {
const association = this;
instancePrototype[this.accessors.create] = function(values, fieldsOrOptions) {
var instance = this
, options = {};
instancePrototype[this.accessors.create] = function(values, fieldsOrOptions) {
const options = {};
if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
options.transaction = fieldsOrOptions.transaction;
}
options.logging = (fieldsOrOptions || {}).logging;
if ((fieldsOrOptions || {}).transaction instanceof Transaction) {
options.transaction = fieldsOrOptions.transaction;
}
options.logging = (fieldsOrOptions || {}).logging;
return association.target.create(values, fieldsOrOptions).then(function(newAssociatedObject) {
return instance[association.accessors.set](newAssociatedObject, options);
});
};
return association.target.create(values, fieldsOrOptions).then(newAssociatedObject =>
this[association.accessors.set](newAssociatedObject, options)
);
};
return this;
};
return this;
}
}
module.exports = BelongsTo;
module.exports.BelongsTo = BelongsTo;
module.exports.default = BelongsTo;
'use strict';
var Utils = require('./../utils')
, Helpers = require('./helpers')
, _ = require('lodash')
, Association = require('./base')
, util = require('util');
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
/**
* One-to-many association
......@@ -13,520 +12,500 @@ var Utils = require('./../utils')
*
* @mixin HasMany
*/
var HasMany = function(source, target, options) {
Association.call(this);
this.associationType = 'HasMany';
this.source = source;
this.target = target;
this.targetAssociation = null;
this.options = options || {};
this.sequelize = source.modelManager.sequelize;
this.through = options.through;
this.scope = options.scope;
this.isMultiAssociation = true;
this.isSelfAssociation = this.source === this.target;
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');
}
class HasMany extends Association {
constructor(source, target, options) {
super();
this.associationType = 'HasMany';
this.source = source;
this.target = target;
this.targetAssociation = null;
this.options = options || {};
this.sequelize = source.modelManager.sequelize;
this.through = options.through;
this.scope = options.scope;
this.isMultiAssociation = true;
this.isSelfAssociation = this.source === this.target;
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 self association, this is the target association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
if (this.as) {
this.isAliased = true;
/*
* If self association, this is the target association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
if (_.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
if (this.as) {
this.isAliased = true;
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 {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
};
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
/*
* Foreign key setup
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
/*
* Foreign key setup
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
[
Utils.underscoredIf(this.options.as || this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
[
Utils.underscoredIf(this.options.as || this.source.options.name.singular, this.source.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!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.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
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 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';
this.target.refreshAttributes();
this.source.refreshAttributes();
Helpers.checkNamingCollision(this);
return this;
}
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.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
mixin(obj) {
const association = this;
this.target.refreshAttributes();
this.source.refreshAttributes();
obj[this.accessors.get] = function(options) {
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) {
var association = this;
obj[this.accessors.set] = function(instances, options) {
return association.set(this, instances, options);
};
obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
obj[this.accessors.add] = obj[this.accessors.addMultiple] = function(instances, options) {
return association.add(this, instances, options);
};
if (this.accessors.count) {
obj[this.accessors.count] = function(options) {
return association.count(this, options);
obj[this.accessors.remove] = obj[this.accessors.removeMultiple] = function(instances, options) {
return association.remove(this, instances, options);
};
}
obj[this.accessors.hasSingle] = obj[this.accessors.hasAll] = function(instances, options) {
return association.has(this, instances, 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;
obj[this.accessors.create] = function(values, options) {
return association.create(this, values, options);
};
}
options = Utils.cloneDeep(options) || {};
get(instances, options) {
const association = this;
const where = {};
let Model = association.target;
let instance;
let values;
if (association.scope) {
_.assign(where, association.scope);
}
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
if (instances) {
values = instances.map(function (instance) {
return instance.get(association.source.primaryKeyAttribute, {raw: true});
});
options = Utils.cloneDeep(options) || {};
if (options.limit && instances.length > 1) {
options.groupedLimit = {
limit: options.limit,
on: association.foreignKeyField,
values: values
};
if (association.scope) {
_.assign(where, association.scope);
}
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 {
where[association.foreignKey] = {
$in: values
};
delete options.groupedLimit;
where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true});
}
} else {
where[association.foreignKey] = instance.get(association.source.primaryKeyAttribute, {raw: true});
}
options.where = options.where ?
{$and: [where, options.where]} :
where;
options.where = options.where ?
{$and: [where, options.where]} :
where;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
}
}
}
if (options.hasOwnProperty('schema')) {
Model = Model.schema(options.schema, options.schemaDelimiter);
}
if (options.hasOwnProperty('schema')) {
Model = Model.schema(options.schema, options.schemaDelimiter);
}
return Model.findAll(options).then(function (results) {
if (instance) return results;
return Model.findAll(options).then(results => {
if (instance) return results;
var result = {};
instances.forEach(function (instance) {
result[instance.get(association.source.primaryKeyAttribute, {raw: true})] = [];
});
const result = {};
for (const instance of instances) {
result[instance.get(association.source.primaryKeyAttribute, {raw: true})] = [];
}
results.forEach(function (instance) {
result[instance.get(association.foreignKey, {raw: true})].push(instance);
});
for (const instance of results) {
result[instance.get(association.foreignKey, {raw: true})].push(instance);
}
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];
return result;
});
}
options = _.assign({}, options, {
scope: false,
raw: true
});
count(instance, options) {
const association = this;
const model = association.target;
const sequelize = model.sequelize;
where.$or = targetInstances.map(function (instance) {
if (instance instanceof association.target) {
return instance.where();
} else {
var _where = {};
_where[association.target.primaryKeyAttribute] = instance;
return _where;
}
});
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);
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(result => parseInt(result.count, 10));
}
return association.get(sourceInstance, _.defaults({
scope: false,
raw: true
}, options)).then(function(oldAssociations) {
var promises = []
, obsoleteAssociations = oldAssociations.filter(function(old) {
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)
));
has(sourceInstance, targetInstances, options) {
const association = this;
const where = {};
if (!Array.isArray(targetInstances)) {
targetInstances = [targetInstances];
}
if (unassociatedObjects.length > 0) {
updateWhere = {};
options = _.assign({}, options, {
scope: false,
raw: true
});
update = {};
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
where.$or = targetInstances.map(instance => {
if (instance instanceof association.target) {
return instance.where();
} else {
const _where = {};
_where[association.target.primaryKeyAttribute] = instance;
return _where;
}
});
_.assign(update, association.scope);
updateWhere[association.target.primaryKeyAttribute] = unassociatedObjects.map(function(unassociatedObject) {
return unassociatedObject[association.target.primaryKeyAttribute];
});
options.where = {
$and: [
where,
options.where
]
};
promises.push(association.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
return this.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length);
}
set(sourceInstance, targetInstances, 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) {
if (!targetInstances) return Utils.Promise.resolve();
add(sourceInstance, targetInstances, options) {
if (!targetInstances) return Utils.Promise.resolve();
var association = this
, update = {}
, where = {};
const association = this;
const update = {};
const where = {};
options = options || {};
options = options || {};
targetInstances = association.toInstanceArray(targetInstances);
targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
update[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
_.assign(update, association.scope);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (unassociatedObject) {
return unassociatedObject.get(association.target.primaryKeyAttribute);
});
where[association.target.primaryKeyAttribute] = targetInstances.map(unassociatedObject =>
unassociatedObject.get(association.target.primaryKeyAttribute)
);
return association.target.unscoped().update(
update,
_.defaults({
where: where
}, options)
).return(sourceInstance);
};
return association.target.unscoped().update(update, _.defaults({where}, options)).return(sourceInstance);
}
HasMany.prototype.remove = function(sourceInstance, targetInstances, options) {
var association = this
, update = {}
, where = {};
remove(sourceInstance, targetInstances, options) {
const association = this;
const update = {};
const where = {};
options = options || {};
targetInstances = association.toInstanceArray(targetInstances);
options = options || {};
targetInstances = association.toInstanceArray(targetInstances);
update[association.foreignKey] = null;
update[association.foreignKey] = null;
where[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = targetInstances.map(function (targetInstance) {
return targetInstance.get(association.target.primaryKeyAttribute);
});
where[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
where[association.target.primaryKeyAttribute] = targetInstances.map(targetInstance =>
targetInstance.get(association.target.primaryKeyAttribute)
);
return association.target.unscoped().update(
update,
_.defaults({
where: where
}, options)
).return(this);
};
return association.target.unscoped().update(update, _.defaults({where}, options)).return(this);
}
HasMany.prototype.create = function(sourceInstance, values, options) {
var association = this;
create(sourceInstance, values, options) {
const association = this;
options = options || {};
options = options || {};
if (Array.isArray(options)) {
options = {
fields: options
};
}
if (Array.isArray(options)) {
options = {
fields: options
};
}
if (values === undefined) {
values = {};
}
if (values === undefined) {
values = {};
}
if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute);
});
}
if (association.scope) {
for (const attribute of Object.keys(association.scope)) {
values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute);
}
}
values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
};
values[association.foreignKey] = sourceInstance.get(association.source.primaryKeyAttribute);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
}
}
module.exports = HasMany;
module.exports.HasMany = HasMany;
module.exports.default = HasMany;
'use strict';
var Utils = require('./../utils')
, Helpers = require('./helpers')
, _ = require('lodash')
, Association = require('./base')
, util = require('util');
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
/**
* One-to-one association
......@@ -14,263 +13,259 @@ var Utils = require('./../utils')
*
* @mixin HasOne
*/
var HasOne = function(srcModel, targetModel, options) {
Association.call(this);
this.associationType = 'HasOne';
this.source = srcModel;
this.target = targetModel;
this.options = options;
this.scope = options.scope;
this.isSingleAssociation = true;
this.isSelfAssociation = (this.source === this.target);
this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
this.options.name = {
singular: this.as
};
} else {
this.as = this.target.options.name.singular;
this.options.name = this.target.options.name;
}
class HasOne extends Association {
constructor(srcModel, targetModel, options) {
super();
this.associationType = 'HasOne';
this.source = srcModel;
this.target = targetModel;
this.options = options;
this.scope = options.scope;
this.isSingleAssociation = true;
this.isSelfAssociation = (this.source === this.target);
this.as = this.options.as;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
this.options.name = {
singular: this.as
};
} else {
this.as = this.target.options.name.singular;
this.options.name = this.target.options.name;
}
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
[
Utils.underscoredIf(Utils.singularize(this.options.as || this.source.name), this.target.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelizeIf(
[
Utils.underscoredIf(Utils.singularize(this.options.as || this.source.name), this.target.options.underscored),
this.source.primaryKeyAttribute
].join('_'),
!this.source.options.underscored
);
}
this.sourceIdentifier = this.source.primaryKeyAttribute;
this.sourceKey = this.source.primaryKeyAttribute;
this.sourceKeyIsPrimary = this.sourceKey === this.source.primaryKeyAttribute;
this.sourceIdentifier = this.source.primaryKeyAttribute;
this.sourceKey = this.source.primaryKeyAttribute;
this.sourceKeyIsPrimary = this.sourceKey === this.source.primaryKeyAttribute;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
if (this.target.rawAttributes[this.foreignKey]) {
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
if (this.target.rawAttributes[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
var singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
/**
* Get the associated instance.
*
* @param {Object} [options]
* @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findOne} for a full explanation of options
* @return {Promise<Instance>}
* @method getAssociation
*/
get: 'get' + singular,
/**
* 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 {Object} [options] Options passed to getAssociation and `target.save`
* @return {Promise}
* @method setAssociation
*/
set: 'set' + 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` and setAssociation.
* @see {Model#create} for a full explanation of options
* @return {Promise}
* @method createAssociation
*/
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';
// Get singular name, trying to uppercase the first letter, unless the model forbids it
const singular = Utils.uppercaseFirst(this.options.name.singular);
this.accessors = {
/**
* Get the associated instance.
*
* @param {Object} [options]
* @param {String|Boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
* @param {String} [options.schema] Apply a schema on the related model
* @see {Model#findOne} for a full explanation of options
* @return {Promise<Instance>}
* @method getAssociation
*/
get: 'get' + singular,
/**
* 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 {Object} [options] Options passed to getAssociation and `target.save`
* @return {Promise}
* @method setAssociation
*/
set: 'set' + 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` and setAssociation.
* @see {Model#create} for a full explanation of options
* @return {Promise}
* @method createAssociation
*/
create: 'create' + singular
};
}
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
this.target.refreshAttributes();
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
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) {
var association = this;
Helpers.addForeignKeyConstraints(this.target.rawAttributes[this.foreignKey], this.source, this.target, this.options);
obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
// Sync attributes and setters/getters to Model prototype
this.target.refreshAttributes();
association.injectSetter(obj);
association.injectCreator(obj);
};
Helpers.checkNamingCollision(this);
HasOne.prototype.get = function(instances, options) {
var association = this
, Target = association.target
, instance
, where = {};
return this;
}
options = Utils.cloneDeep(options);
mixin(obj) {
const association = this;
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
obj[this.accessors.get] = function(options) {
return association.get(this, options);
};
if (options.hasOwnProperty('schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
association.injectSetter(obj);
association.injectCreator(obj);
}
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
get(instances, options) {
const association = this;
const where = {};
let Target = association.target;
let instance;
if (instances) {
where[association.foreignKey] = {
$in: instances.map(function (instance) {
return instance.get(association.sourceKey);
})
};
} else {
where[association.foreignKey] = instance.get(association.sourceKey);
}
options = Utils.cloneDeep(options);
if (association.scope) {
_.assign(where, association.scope);
}
if (options.hasOwnProperty('scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
options.where = options.where ?
{$and: [where, options.where]} :
where;
if (options.hasOwnProperty('schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
}
if (instances) {
return Target.findAll(options).then(function (results) {
var result = {};
instances.forEach(function (instance) {
result[instance.get(association.sourceKey, {raw: true})] = null;
});
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
results.forEach(function (instance) {
result[instance.get(association.foreignKey, {raw: true})] = instance;
});
if (instances) {
where[association.foreignKey] = {
$in: instances.map(instance => instance.get(association.sourceKey))
};
} else {
where[association.foreignKey] = instance.get(association.sourceKey);
}
return result;
});
}
return Target.findOne(options);
};
if (association.scope) {
_.assign(where, association.scope);
}
HasOne.prototype.injectSetter = function(instancePrototype) {
var association = this;
options.where = options.where ?
{$and: [where, options.where]} :
where;
instancePrototype[this.accessors.set] = function(associatedInstance, options) {
var instance = this,
alreadyAssociated;
if (instances) {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(association.sourceKey, {raw: true})] = null;
}
options = _.assign({}, options, {
scope: false
});
return instance[association.accessors.get](options).then(function(oldInstance) {
// TODO Use equals method once #5605 is resolved
alreadyAssociated = oldInstance && associatedInstance && _.every(association.target.primaryKeyAttributes, function(attribute) {
return oldInstance.get(attribute, {raw: true}) === associatedInstance.get(attribute, {raw: true});
for (const instance of results) {
result[instance.get(association.foreignKey, {raw: true})] = instance;
}
return result;
});
}
return Target.findOne(options);
}
if (oldInstance && !alreadyAssociated) {
oldInstance[association.foreignKey] = null;
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
});
}
injectSetter(instancePrototype) {
const association = this;
_.assign(associatedInstance, association.scope);
associatedInstance.set(association.foreignKey, instance.get(association.sourceIdentifier));
instancePrototype[this.accessors.set] = function(associatedInstance, options) {
let alreadyAssociated;
return associatedInstance.save(options);
}
return null;
});
};
options = _.assign({}, options, {
scope: false
});
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) {
var association = this;
injectCreator(instancePrototype) {
const association = this;
instancePrototype[this.accessors.create] = function(values, options) {
var instance = this;
values = values || {};
options = options || {};
instancePrototype[this.accessors.create] = function(values, options) {
values = values || {};
options = options || {};
if (association.scope) {
Object.keys(association.scope).forEach(function (attribute) {
values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute);
});
}
if (association.scope) {
for (const attribute of Object.keys(association.scope)) {
values[attribute] = association.scope[attribute];
if (options.fields) options.fields.push(attribute);
}
}
values[association.foreignKey] = instance.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
};
values[association.foreignKey] = this.get(association.sourceIdentifier);
if (options.fields) options.fields.push(association.foreignKey);
return association.target.create(values, options);
};
return this;
};
return this;
}
}
module.exports = HasOne;
'use strict';
var Utils = require('./../utils');
const Utils = require('./../utils');
function checkNamingCollision (association) {
if (association.source.rawAttributes.hasOwnProperty(association.as)) {
......@@ -11,6 +11,7 @@ function checkNamingCollision (association) {
);
}
}
exports.checkNamingCollision = checkNamingCollision;
function addForeignKeyConstraints (newAttribute, source, target, options, key) {
// FK constraints are opt-in: users must either set `foreignKeyConstraints`
......@@ -19,9 +20,9 @@ function addForeignKeyConstraints (newAttribute, source, target, options, key) {
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach
var primaryKeys = Utils._.chain(source.rawAttributes).keys()
.filter(function($key) { return source.rawAttributes[$key].primaryKey; })
.map(function($key) { return source.rawAttributes[$key].field || $key; }).value();
const primaryKeys = Utils._.chain(source.rawAttributes).keys()
.filter($key => source.rawAttributes[$key].primaryKey)
.map($key => source.rawAttributes[$key].field || $key).value();
if (primaryKeys.length === 1) {
if (!!source.$schema) {
......@@ -42,8 +43,4 @@ function addForeignKeyConstraints (newAttribute, source, target, options, key) {
}
}
}
module.exports = {
checkNamingCollision: checkNamingCollision,
addForeignKeyConstraints: addForeignKeyConstraints
};
exports.addForeignKeyConstraints = addForeignKeyConstraints;
'use strict';
var Association = require('./base');
const Association = require('./base');
Association.BelongsTo = require('./belongs-to');
Association.HasOne = require('./has-one');
Association.HasMany = require('./has-many');
Association.BelongsToMany = require('./belongs-to-many');
module.exports = Association;
module.exports.default = Association;
module.exports.Association = Association;
'use strict';
var Utils = require('./../utils')
, _ = require('lodash')
, HasOne = require('./has-one')
, HasMany = require('./has-many')
, BelongsToMany = require('./belongs-to-many')
, BelongsTo = require('./belongs-to');
const Utils = require('./../utils');
const _ = require('lodash');
const HasOne = require('./has-one');
const HasMany = require('./has-many');
const BelongsToMany = require('./belongs-to-many');
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).
......@@ -86,24 +86,157 @@ var Utils = require('./../utils')
* @mixin 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
var singleLinked = function (Type) {
function singleLinked(Type) {
return function(target, options) { // testhint options:none
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');
}
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.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
// 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();
if (association.mixin) {
......@@ -116,7 +249,8 @@ var singleLinked = function (Type) {
return association;
};
};
}
/**
* 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);
*/
Mixin.belongsTo = singleLinked(BelongsTo);
/**
* 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.
*/
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;
};
module.exports = Mixin;
module.exports.Mixin = Mixin;
module.exports.default = Mixin;
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!