init source

This commit is contained in:
Le Viet
2022-03-07 22:07:57 +07:00
parent e4376f3777
commit 8aba590a8d
11240 changed files with 1012977 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
'use strict';
const { AssociationError } = require('./../errors');
/**
* 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).
*
* * hasOne - adds a foreign key to the target and singular association mixins to the source.
* * belongsTo - add a foreign key and singular association mixins to the source.
* * hasMany - adds a foreign key to target and plural association mixins to the source.
* * belongsToMany - creates an N:M association with a join table and adds plural association mixins to the source. The junction table is created with sourceId and targetId.
*
* Creating an association will add a foreign key constraint to the attributes. All associations use `CASCADE` on update and `SET NULL` on delete, except for n:m, which also uses `CASCADE` on delete.
*
* When creating associations, you can provide an alias, via the `as` option. This is useful if the same model is associated twice, or you want your association to be called something other than the name of the target model.
*
* As an example, consider the case where users have many pictures, one of which is their profile picture. All pictures have a `userId`, but in addition the user model also has a `profilePictureId`, to be able to easily load the user's profile picture.
*
* ```js
* User.hasMany(Picture)
* User.belongsTo(Picture, { as: 'ProfilePicture', constraints: false })
*
* user.getPictures() // gets you all pictures
* user.getProfilePicture() // gets you only the profile picture
*
* User.findAll({
* where: ...,
* include: [
* { model: Picture }, // load all pictures
* { model: Picture, as: 'ProfilePicture' }, // load the profile picture.
* // Notice that the spelling must be the exact same as the one in the association
* ]
* })
* ```
* To get full control over the foreign key column added by sequelize, you can use the `foreignKey` option. It can either be a string, that specifies the name, or and object type definition,
* equivalent to those passed to `sequelize.define`.
*
* ```js
* User.hasMany(Picture, { foreignKey: 'uid' })
* ```
*
* The foreign key column in Picture will now be called `uid` instead of the default `userId`.
*
* ```js
* User.hasMany(Picture, {
* foreignKey: {
* name: 'uid',
* allowNull: false
* }
* })
* ```
*
* This specifies that the `uid` column cannot be null. In most cases this will already be covered by the foreign key constraints, which sequelize creates automatically, but can be useful in case where the foreign keys are disabled, e.g. due to circular references (see `constraints: false` below).
*
* When fetching associated models, you can limit your query to only load some models. These queries are written in the same way as queries to `find`/`findAll`. To only get pictures in JPG, you can do:
*
* ```js
* user.getPictures({
* where: {
* format: 'jpg'
* }
* })
* ```
*
* There are several ways to update and add new associations. Continuing with our example of users and pictures:
* ```js
* user.addPicture(p) // Add a single picture
* user.setPictures([p1, p2]) // Associate user with ONLY these two picture, all other associations will be deleted
* user.addPictures([p1, p2]) // Associate user with these two pictures, but don't touch any current associations
* ```
*
* You don't have to pass in a complete object to the association functions, if your associated model has a single primary key:
*
* ```js
* user.addPicture(req.query.pid) // Here pid is just an integer, representing the primary key of the picture
* ```
*
* In the example above we have specified that a user belongs to his profile picture. Conceptually, this might not make sense, but since we want to add the foreign key to the user model this is the way to do it.
*
* Note how we also specified `constraints: false` for profile picture. This is because we add a foreign key from user to picture (profilePictureId), and from picture to user (userId). If we were to add foreign keys to both, it would create a cyclic dependency, and sequelize would not know which table to create first, since user depends on picture, and picture depends on user. These kinds of problems are detected by sequelize before the models are synced to the database, and you will get an error along the lines of `Error: Cyclic dependency found. 'users' is dependent of itself`. If you encounter this, you should either disable some constraints, or rethink your associations completely.
*/
class Association {
constructor(source, target, options = {}) {
/**
* @type {Model}
*/
this.source = source;
/**
* @type {Model}
*/
this.target = target;
this.options = options;
this.scope = options.scope;
this.isSelfAssociation = this.source === this.target;
this.as = options.as;
/**
* The type of the association. One of `HasMany`, `BelongsTo`, `HasOne`, `BelongsToMany`
* @type {string}
*/
this.associationType = '';
if (source.hasAlias(options.as)) {
throw new AssociationError(`You have used the alias ${options.as} in two separate associations. ` +
'Aliased associations must have unique aliases.'
);
}
}
/**
* Normalize input
*
* @param {Array|string} input it may be array or single obj, instance or primary key
*
* @private
* @returns {Array} built objects
*/
toInstanceArray(input) {
if (!Array.isArray(input)) {
input = [input];
}
return input.map(element => {
if (element instanceof this.target) return element;
const tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = element;
return this.target.build(tmpInstance, { isNewRecord: false });
});
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return this.as;
}
inspect() {
return this.as;
}
}
module.exports = Association;
+818
View File
@@ -0,0 +1,818 @@
'use strict';
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');
const AssociationError = require('../errors').AssociationError;
const EmptyResultError = require('../errors').EmptyResultError;
const Op = require('../operators');
/**
* Many-to-many association with a join table.
*
* When the join table has additional attributes, these can be passed in the options object:
*
* ```js
* UserProject = sequelize.define('user_project', {
* role: Sequelize.STRING
* });
* User.belongsToMany(Project, { through: UserProject });
* Project.belongsToMany(User, { through: UserProject });
* // through is required!
*
* user.addProject(project, { through: { role: 'manager' }});
* ```
*
* All methods allow you to pass either a persisted instance, its primary key, or a mixture:
*
* ```js
* Project.create({ id: 11 }).then(project => {
* user.addProjects([project, 12]);
* });
* ```
*
* 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], { through: { 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(projects => {
* let p1 = projects[0]
* p1.UserProjects.started // Is this project started yet?
* })
* ```
*
* In the API reference below, add the name of the association to the method, e.g. for `User.belongsToMany(Project)` the getter will be `user.getProjects()`.
*
* @see {@link Model.belongsToMany}
*/
class BelongsToMany extends Association {
constructor(source, target, options) {
super(source, target, options);
if (this.options.through === undefined || this.options.through === true || this.options.through === null) {
throw new AssociationError(`${source.name}.belongsToMany(${target.name}) requires through option, pass either a string or a model`);
}
if (!this.options.through.model) {
this.options.through = {
model: options.through
};
}
this.associationType = 'BelongsToMany';
this.targetAssociation = null;
this.sequelize = source.sequelize;
this.through = Object.assign({}, this.options.through);
this.isMultiAssociation = true;
this.doubleLinked = false;
if (!this.as && this.isSelfAssociation) {
throw new AssociationError('\'as\' must be defined for many-to-many self-associations');
}
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.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
);
/*
* If self association, this is the target association - Unless we find a pairing association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
/*
* 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;
}
});
/*
* Default/generated source/target keys
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
if (this.options.targetKey) {
this.targetKey = this.options.targetKey;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
} else {
this.targetKeyDefault = true;
this.targetKey = this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
}
this._createForeignAndOtherKeys();
if (typeof this.through.model === 'string') {
if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, Object.assign(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 = Object.assign(this.options, _.pick(this.through.model.options, [
'timestamps', 'createdAt', 'updatedAt', 'deletedAt', 'paranoid'
]));
if (this.paired) {
let needInjectPaired = false;
if (this.targetKeyDefault) {
this.targetKey = this.paired.sourceKey;
this.targetKeyField = this.paired.sourceKeyField;
this._createForeignAndOtherKeys();
}
if (this.paired.targetKeyDefault) {
// in this case paired.otherKey depends on paired.targetKey,
// so cleanup previously wrong generated otherKey
if (this.paired.targetKey !== this.sourceKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.targetKey = this.sourceKey;
this.paired.targetKeyField = this.sourceKeyField;
this.paired._createForeignAndOtherKeys();
needInjectPaired = true;
}
}
if (this.otherKeyDefault) {
this.otherKey = this.paired.foreignKey;
}
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;
needInjectPaired = true;
}
}
if (needInjectPaired) {
this.paired._injectAttributes();
}
}
if (this.through) {
this.throughModel = 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
const plural = _.upperFirst(this.options.name.plural);
const singular = _.upperFirst(this.options.name.singular);
this.accessors = {
get: `get${plural}`,
set: `set${plural}`,
addMultiple: `add${plural}`,
add: `add${singular}`,
create: `create${singular}`,
remove: `remove${singular}`,
removeMultiple: `remove${plural}`,
hasSingle: `has${singular}`,
hasAll: `has${plural}`,
count: `count${plural}`
};
}
_createForeignAndOtherKeys() {
/*
* Default/generated foreign/other keys
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelize(
[
this.source.options.name.singular,
this.sourceKey
].join('_')
);
}
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;
}
this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelize(
[
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
this.targetKey
].join('_')
);
}
}
// 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;
}
});
const sourceKey = this.source.rawAttributes[this.sourceKey];
const sourceKeyType = sourceKey.type;
const sourceKeyField = this.sourceKeyField;
const targetKey = this.target.rawAttributes[this.targetKey];
const targetKeyType = targetKey.type;
const targetKeyField = this.targetKeyField;
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) {
let uniqueKey;
if (typeof this.options.uniqueKey === 'string' && this.options.uniqueKey !== '') {
uniqueKey = this.options.uniqueKey;
} else {
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.model.rawAttributes[this.otherKey]) {
this.through.model.rawAttributes[this.otherKey] = {
_autoGenerated: true
};
}
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;
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
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] = Object.assign(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
this.through.model.rawAttributes[this.otherKey] = Object.assign(this.through.model.rawAttributes[this.otherKey], targetAttribute);
this.through.model.refreshAttributes();
this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
if (this.paired && !this.paired.foreignIdentifierField) {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
}
this.toSource = new BelongsTo(this.through.model, this.source, {
foreignKey: this.foreignKey
});
this.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
});
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;
}
mixin(obj) {
const methods = ['get', 'count', 'hasSingle', 'hasAll', 'set', 'add', 'addMultiple', 'remove', 'removeMultiple', 'create'];
const aliases = {
hasSingle: 'has',
hasAll: 'has',
addMultiple: 'add',
removeMultiple: 'remove'
};
Helpers.mixinMethods(this, obj, methods, aliases);
}
/**
* Get everything currently associated with this, using an optional where clause.
*
* @see
* {@link Model} for a full explanation of options
*
* @param {Model} instance instance
* @param {Object} [options] find 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
*
* @returns {Promise<Array<Model>>}
*/
get(instance, options) {
options = Utils.cloneDeep(options) || {};
const through = this.through;
let scopeWhere;
let throughWhere;
if (this.scope) {
scopeWhere = _.clone(this.scope);
}
options.where = {
[Op.and]: [
scopeWhere,
options.where
]
};
if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[this.foreignKey] = instance.get(this.sourceKey);
if (through.scope) {
Object.assign(throughWhere, through.scope);
}
//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 = {
[Op.and]: [throughWhere, options.through.where]
};
}
options.include = options.include || [];
options.include.push({
association: this.oneFromTarget,
attributes: options.joinTableAttributes,
required: true,
where: throughWhere
});
}
let model = this.target;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
model = model.schema(options.schema, options.schemaDelimiter);
}
return model.findAll(options);
}
/**
* Count everything currently associated with this, using an optional where clause.
*
* @param {Model} instance instance
* @param {Object} [options] find 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
*
* @returns {Promise<number>}
*/
count(instance, options) {
const sequelize = this.target.sequelize;
options = Utils.cloneDeep(options);
options.attributes = [
[sequelize.fn('COUNT', sequelize.col([this.target.name, this.targetKeyField].join('.'))), 'count']
];
options.joinTableAttributes = [];
options.raw = true;
options.plain = true;
return this.get(instance, options).then(result => parseInt(result.count, 10));
}
/**
* Check if one or more instance(s) are associated with this. If a list of instances is passed, the function returns true if _all_ instances are associated
*
* @param {Model} sourceInstance source instance to check for an association with
* @param {Model|Model[]|string[]|string|number[]|number} [instances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
*
* @returns {Promise<boolean>}
*/
has(sourceInstance, instances, options) {
if (!Array.isArray(instances)) {
instances = [instances];
}
options = Object.assign({
raw: true
}, options, {
scope: false,
attributes: [this.targetKey],
joinTableAttributes: []
});
const instancePrimaryKeys = instances.map(instance => {
if (instance instanceof this.target) {
return instance.where();
}
return {
[this.targetKey]: instance
};
});
options.where = {
[Op.and]: [
{ [Op.or]: instancePrimaryKeys },
options.where
]
};
return this.get(sourceInstance, options).then(associatedObjects =>
_.differenceBy(instancePrimaryKeys, associatedObjects, this.targetKey).length === 0
);
}
/**
* 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 {Model} sourceInstance source instance to associate new instances with
* @param {Model|Model[]|string[]|string|number[]|number} [newAssociatedObjects] A single instance or primary key, or a mixed array of persisted instances or primary keys
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`
* @param {Object} [options.validate] Run validation for the join model
* @param {Object} [options.through] Additional attributes for the join table.
*
* @returns {Promise}
*/
set(sourceInstance, newAssociatedObjects, options) {
options = options || {};
const sourceKey = this.sourceKey;
const targetKey = this.targetKey;
const identifier = this.identifier;
const foreignIdentifier = this.foreignIdentifier;
let where = {};
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
} else {
newAssociatedObjects = this.toInstanceArray(newAssociatedObjects);
}
where[identifier] = sourceInstance.get(sourceKey);
where = Object.assign(where, this.through.scope);
const updateAssociations = currentRows => {
const obsoleteAssociations = [];
const promises = [];
const defaultAttributes = options.through || {};
const unassociatedObjects = newAssociatedObjects.filter(obj =>
!currentRows.some(currentRow => currentRow[foreignIdentifier] === obj.get(targetKey))
);
for (const currentRow of currentRows) {
const newObj = newAssociatedObjects.find(obj => currentRow[foreignIdentifier] === obj.get(targetKey));
if (!newObj) {
obsoleteAssociations.push(currentRow);
} else {
let throughAttributes = newObj[this.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 this.through.model) {
throughAttributes = {};
}
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (Object.keys(attributes).length) {
promises.push(
this.through.model.update(attributes, Object.assign(options, {
where: {
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: newObj.get(targetKey)
}
}
))
);
}
}
}
if (obsoleteAssociations.length > 0) {
const where = Object.assign({
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: obsoleteAssociations.map(obsoleteAssociation => obsoleteAssociation[foreignIdentifier])
}, this.through.scope);
promises.push(
this.through.model.destroy(_.defaults({
where
}, options))
);
}
if (unassociatedObjects.length > 0) {
const bulk = unassociatedObjects.map(unassociatedObject => {
let attributes = {};
attributes[identifier] = sourceInstance.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
attributes = _.defaults(attributes, unassociatedObject[this.through.model.name], defaultAttributes);
Object.assign(attributes, this.through.scope);
attributes = Object.assign(attributes, this.through.scope);
return attributes;
});
promises.push(this.through.model.bulkCreate(bulk, Object.assign({ validate: true }, options)));
}
return Utils.Promise.all(promises);
};
return this.through.model.findAll(_.defaults({ where, raw: true }, options))
.then(currentRows => updateAssociations(currentRows))
.catch(error => {
if (error instanceof EmptyResultError) return updateAssociations([]);
throw error;
});
}
/**
* Associate one or several rows with source instance. It will not un-associate any already associated instance
* that may be missing from `newInstances`.
*
* @param {Model} sourceInstance source instance to associate new instances with
* @param {Model|Model[]|string[]|string|number[]|number} [newInstances] A single instance or primary key, or a mixed array of persisted instances or primary keys
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`
* @param {Object} [options.validate] Run validation for the join model.
* @param {Object} [options.through] Additional attributes for the join table.
*
* @returns {Promise}
*/
add(sourceInstance, newInstances, options) {
// If newInstances is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve();
options = _.clone(options) || {};
const association = this;
const sourceKey = association.sourceKey;
const targetKey = association.targetKey;
const identifier = association.identifier;
const foreignIdentifier = association.foreignIdentifier;
const defaultAttributes = options.through || {};
newInstances = association.toInstanceArray(newInstances);
const where = {
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: newInstances.map(newInstance => newInstance.get(targetKey))
};
Object.assign(where, association.through.scope);
const updateAssociations = currentRows => {
const promises = [];
const unassociatedObjects = [];
const changedAssociations = [];
for (const obj of newInstances) {
const existingAssociation = currentRows && currentRows.find(current => current[foreignIdentifier] === obj.get(targetKey));
if (!existingAssociation) {
unassociatedObjects.push(obj);
} else {
const throughAttributes = obj[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (Object.keys(attributes).some(attribute => attributes[attribute] !== existingAssociation[attribute])) {
changedAssociations.push(obj);
}
}
}
if (unassociatedObjects.length > 0) {
const bulk = unassociatedObjects.map(unassociatedObject => {
const throughAttributes = unassociatedObject[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
attributes[identifier] = sourceInstance.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
Object.assign(attributes, association.through.scope);
return attributes;
});
promises.push(association.through.model.bulkCreate(bulk, Object.assign({ validate: true }, options)));
}
for (const assoc of changedAssociations) {
let throughAttributes = assoc[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
// 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 where = {
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: assoc.get(targetKey)
};
promises.push(association.through.model.update(attributes, Object.assign(options, { where })));
}
return Utils.Promise.all(promises);
};
return association.through.model.findAll(_.defaults({ where, raw: true }, options))
.then(currentRows => updateAssociations(currentRows))
.then(([associations]) => associations)
.catch(error => {
if (error instanceof EmptyResultError) return updateAssociations();
throw error;
});
}
/**
* Un-associate one or more instance(s).
*
* @param {Model} sourceInstance instance to un associate instances with
* @param {Model|Model[]|string|string[]|number|number[]} [oldAssociatedObjects] Can be an Instance or its primary key, or a mixed array of instances and primary keys
* @param {Object} [options] Options passed to `through.destroy`
*
* @returns {Promise}
*/
remove(sourceInstance, oldAssociatedObjects, options) {
const association = this;
options = options || {};
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
const where = {
[association.identifier]: sourceInstance.get(association.sourceKey),
[association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.targetKey))
};
return association.through.model.destroy(_.defaults({ where }, options));
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance source instance
* @param {Object} [values] values for target model
* @param {Object} [options] Options passed to create and add
* @param {Object} [options.through] Additional attributes for the join table
*
* @returns {Promise}
*/
create(sourceInstance, values, options) {
const association = this;
options = options || {};
values = values || {};
if (Array.isArray(options)) {
options = {
fields: options
};
}
if (association.scope) {
Object.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 =>
sourceInstance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject)
);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.plural) {
return this.as === alias.plural;
}
return !this.isAliased;
}
}
module.exports = BelongsToMany;
module.exports.BelongsToMany = BelongsToMany;
module.exports.default = BelongsToMany;
+252
View File
@@ -0,0 +1,252 @@
'use strict';
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-one association
*
* In the API reference below, add the name of the association to the method, e.g. for `User.belongsTo(Project)` the getter will be `user.getProject()`.
*
* @see {@link Model.belongsTo}
*/
class BelongsTo extends Association {
constructor(source, target, options) {
super(source, target, options);
this.associationType = 'BelongsTo';
this.isSingleAssociation = true;
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 (!this.foreignKey) {
this.foreignKey = Utils.camelize(
[
this.as,
this.target.primaryKeyAttribute
].join('_')
);
}
this.identifier = this.foreignKey;
if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
if (
this.options.targetKey
&& !this.target.rawAttributes[this.options.targetKey]
) {
throw new Error(`Unknown attribute "${this.options.targetKey}" passed as targetKey, define this attribute on model "${this.target.name}" first`);
}
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 = _.upperFirst(this.options.name.singular);
this.accessors = {
get: `get${singular}`,
set: `set${singular}`,
create: `create${singular}`
};
}
// the id is in the source table
_injectAttributes() {
const newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.target.rawAttributes[this.targetKey].type,
allowNull: true
});
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.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
this.source.refreshAttributes();
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
}
mixin(obj) {
const methods = ['get', 'set', 'create'];
Helpers.mixinMethods(this, obj, methods);
}
/**
* Get the associated instance.
*
* @param {Model|Array<Model>} instances source instances
* @param {Object} [options] find options
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false.
* @param {string} [options.schema] Apply a schema on the related model
*
* @see
* {@link Model.findOne} for a full explanation of options
*
* @returns {Promise<Model>}
*/
get(instances, options) {
const where = {};
let Target = this.target;
let instance;
options = Utils.cloneDeep(options);
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
}
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
if (instances) {
where[this.targetKey] = {
[Op.in]: instances.map(instance => instance.get(this.foreignKey))
};
} else {
if (this.targetKeyIsPrimary && !options.where) {
return Target.findByPk(instance.get(this.foreignKey), options);
}
where[this.targetKey] = instance.get(this.foreignKey);
options.limit = null;
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (instances) {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(this.foreignKey, { raw: true })] = null;
}
for (const instance of results) {
result[instance.get(this.targetKey, { raw: true })] = instance;
}
return result;
});
}
return Target.findOne(options);
}
/**
* Set the associated model.
*
* @param {Model} sourceInstance the source instance
* @param {?<Model>|string|number} [associatedInstance] 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.
*
* @returns {Promise}
*/
set(sourceInstance, associatedInstance, options = {}) {
let value = associatedInstance;
if (associatedInstance instanceof this.target) {
value = associatedInstance[this.targetKey];
}
sourceInstance.set(this.foreignKey, value);
if (options.save === false) return;
options = Object.assign({
fields: [this.foreignKey],
allowNull: [this.foreignKey],
association: true
}, options);
// passes the changed field to save, so only that field get updated.
return sourceInstance.save(options);
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance the source instance
* @param {Object} [values={}] values to create associated model instance with
* @param {Object} [options={}] Options passed to `target.create` and setAssociation.
*
* @see
* {@link Model#create} for a full explanation of options
*
* @returns {Promise<Model>} The created target model
*/
create(sourceInstance, values, options) {
values = values || {};
options = options || {};
return this.target.create(values, options)
.then(newAssociatedObject => sourceInstance[this.accessors.set](newAssociatedObject, options)
.then(() => newAssociatedObject)
);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.singular) {
return this.as === alias.singular;
}
return !this.isAliased;
}
}
module.exports = BelongsTo;
module.exports.BelongsTo = BelongsTo;
module.exports.default = BelongsTo;
+487
View File
@@ -0,0 +1,487 @@
'use strict';
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-many association
*
* In the API reference below, add the name of the association to the method, e.g. for `User.hasMany(Project)` the getter will be `user.getProjects()`.
* If the association is aliased, use the alias instead, e.g. `User.hasMany(Project, { as: 'jobs' })` will be `user.getJobs()`.
*
* @see {@link Model.hasMany}
*/
class HasMany extends Association {
constructor(source, target, options) {
super(source, target, options);
this.associationType = 'HasMany';
this.targetAssociation = null;
this.sequelize = source.sequelize;
this.isMultiAssociation = true;
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 (_.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;
}
/*
* 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.camelize(
[
this.source.options.name.singular,
this.source.primaryKeyAttribute
].join('_')
);
}
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;
}
/*
* Source key setup
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
if (this.source.rawAttributes[this.sourceKey]) {
this.sourceKeyAttribute = this.sourceKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else {
this.sourceKeyAttribute = this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.primaryKeyField;
}
// Get singular and plural names
// try to uppercase the first letter, unless the model forbids it
const plural = _.upperFirst(this.options.name.plural);
const singular = _.upperFirst(this.options.name.singular);
this.associationAccessor = this.as;
this.accessors = {
get: `get${plural}`,
set: `set${plural}`,
addMultiple: `add${plural}`,
add: `add${singular}`,
create: `create${singular}`,
remove: `remove${singular}`,
removeMultiple: `remove${plural}`,
hasSingle: `has${singular}`,
hasAll: `has${plural}`,
count: `count${plural}`
};
}
// the id is in the target table
// or in an extra table which connects two tables
_injectAttributes() {
const newAttributes = {};
// Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
const constraintOptions = _.clone(this.options);
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.sourceKeyAttribute].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, this.sourceKeyField);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.target.refreshAttributes();
this.source.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
Helpers.checkNamingCollision(this);
return this;
}
mixin(obj) {
const methods = ['get', 'count', 'hasSingle', 'hasAll', 'set', 'add', 'addMultiple', 'remove', 'removeMultiple', 'create'];
const aliases = {
hasSingle: 'has',
hasAll: 'has',
addMultiple: 'add',
removeMultiple: 'remove'
};
Helpers.mixinMethods(this, obj, methods, aliases);
}
/**
* Get everything currently associated with this, using an optional where clause.
*
* @param {Model|Array<Model>} instances source instances
* @param {Object} [options] find 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
* {@link Model.findAll} for a full explanation of options
*
* @returns {Promise<Array<Model>>}
*/
get(instances, options = {}) {
const where = {};
let Model = this.target;
let instance;
let values;
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
options = Object.assign({}, options);
if (this.scope) {
Object.assign(where, this.scope);
}
if (instances) {
values = instances.map(instance => instance.get(this.sourceKey, { raw: true }));
if (options.limit && instances.length > 1) {
options.groupedLimit = {
limit: options.limit,
on: this, // association
values
};
delete options.limit;
} else {
where[this.foreignKey] = {
[Op.in]: values
};
delete options.groupedLimit;
}
} else {
where[this.foreignKey] = instance.get(this.sourceKey, { raw: true });
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Model = Model.schema(options.schema, options.schemaDelimiter);
}
return Model.findAll(options).then(results => {
if (instance) return results;
const result = {};
for (const instance of instances) {
result[instance.get(this.sourceKey, { raw: true })] = [];
}
for (const instance of results) {
result[instance.get(this.foreignKey, { raw: true })].push(instance);
}
return result;
});
}
/**
* Count everything currently associated with this, using an optional where clause.
*
* @param {Model} instance the source instance
* @param {Object} [options] find & count 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
*
* @returns {Promise<number>}
*/
count(instance, options) {
options = Utils.cloneDeep(options);
options.attributes = [
[
this.sequelize.fn(
'COUNT',
this.sequelize.col(`${this.target.name}.${this.target.primaryKeyField}`)
),
'count'
]
];
options.raw = true;
options.plain = true;
return this.get(instance, options).then(result => parseInt(result.count, 10));
}
/**
* Check if one or more rows are associated with `this`.
*
* @param {Model} sourceInstance the source instance
* @param {Model|Model[]|string[]|string|number[]|number} [targetInstances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
*
* @returns {Promise}
*/
has(sourceInstance, targetInstances, options) {
const where = {};
if (!Array.isArray(targetInstances)) {
targetInstances = [targetInstances];
}
options = Object.assign({}, options, {
scope: false,
attributes: [this.target.primaryKeyAttribute],
raw: true
});
where[Op.or] = targetInstances.map(instance => {
if (instance instanceof this.target) {
return instance.where();
}
return {
[this.target.primaryKeyAttribute]: instance
};
});
options.where = {
[Op.and]: [
where,
options.where
]
};
return this.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length);
}
/**
* 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 {Model} sourceInstance source instance to associate new instances with
* @param {Model|Model[]|string[]|string|number[]|number} [targetInstances] 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
*
* @returns {Promise}
*/
set(sourceInstance, targetInstances, options) {
if (targetInstances === null) {
targetInstances = [];
} else {
targetInstances = this.toInstanceArray(targetInstances);
}
return this.get(sourceInstance, _.defaults({ scope: false, raw: true }, options)).then(oldAssociations => {
const promises = [];
const obsoleteAssociations = oldAssociations.filter(old =>
!targetInstances.find(obj =>
obj[this.target.primaryKeyAttribute] === old[this.target.primaryKeyAttribute]
)
);
const unassociatedObjects = targetInstances.filter(obj =>
!oldAssociations.find(old =>
obj[this.target.primaryKeyAttribute] === old[this.target.primaryKeyAttribute]
)
);
let updateWhere;
let update;
if (obsoleteAssociations.length > 0) {
update = {};
update[this.foreignKey] = null;
updateWhere = {
[this.target.primaryKeyAttribute]: obsoleteAssociations.map(associatedObject =>
associatedObject[this.target.primaryKeyAttribute]
)
};
promises.push(this.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
if (unassociatedObjects.length > 0) {
updateWhere = {};
update = {};
update[this.foreignKey] = sourceInstance.get(this.sourceKey);
Object.assign(update, this.scope);
updateWhere[this.target.primaryKeyAttribute] = unassociatedObjects.map(unassociatedObject =>
unassociatedObject[this.target.primaryKeyAttribute]
);
promises.push(this.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
return Utils.Promise.all(promises).return(sourceInstance);
});
}
/**
* Associate one or more target rows with `this`. This method accepts a Model / string / number to associate a single row,
* or a mixed array of Model / string / numbers to associate multiple rows.
*
* @param {Model} sourceInstance the source instance
* @param {Model|Model[]|string[]|string|number[]|number} [targetInstances] A single instance or primary key, or a mixed array of persisted instances or primary keys
* @param {Object} [options] Options passed to `target.update`.
*
* @returns {Promise}
*/
add(sourceInstance, targetInstances, options = {}) {
if (!targetInstances) return Utils.Promise.resolve();
const update = {};
targetInstances = this.toInstanceArray(targetInstances);
update[this.foreignKey] = sourceInstance.get(this.sourceKey);
Object.assign(update, this.scope);
const where = {
[this.target.primaryKeyAttribute]: targetInstances.map(unassociatedObject =>
unassociatedObject.get(this.target.primaryKeyAttribute)
)
};
return this.target.unscoped().update(update, _.defaults({ where }, options)).return(sourceInstance);
}
/**
* Un-associate one or several target rows.
*
* @param {Model} sourceInstance instance to un associate instances with
* @param {Model|Model[]|string|string[]|number|number[]} [targetInstances] Can be an Instance or its primary key, or a mixed array of instances and primary keys
* @param {Object} [options] Options passed to `target.update`
*
* @returns {Promise}
*/
remove(sourceInstance, targetInstances, options = {}) {
const update = {
[this.foreignKey]: null
};
targetInstances = this.toInstanceArray(targetInstances);
const where = {
[this.foreignKey]: sourceInstance.get(this.sourceKey),
[this.target.primaryKeyAttribute]: targetInstances.map(targetInstance =>
targetInstance.get(this.target.primaryKeyAttribute)
)
};
return this.target.unscoped().update(update, _.defaults({ where }, options)).return(this);
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance source instance
* @param {Object} [values] values for target model instance
* @param {Object} [options] Options passed to `target.create`
*
* @returns {Promise}
*/
create(sourceInstance, values, options = {}) {
if (Array.isArray(options)) {
options = {
fields: options
};
}
if (values === undefined) {
values = {};
}
if (this.scope) {
for (const attribute of Object.keys(this.scope)) {
values[attribute] = this.scope[attribute];
if (options.fields) options.fields.push(attribute);
}
}
values[this.foreignKey] = sourceInstance.get(this.sourceKey);
if (options.fields) options.fields.push(this.foreignKey);
return this.target.create(values, options);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.plural) {
return this.as === alias.plural;
}
return !this.isAliased;
}
}
module.exports = HasMany;
module.exports.HasMany = HasMany;
module.exports.default = HasMany;
+280
View File
@@ -0,0 +1,280 @@
'use strict';
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-one association
*
* In the API reference below, add the name of the association to the method, e.g. for `User.hasOne(Project)` the getter will be `user.getProject()`.
* This is almost the same as `belongsTo` with one exception - The foreign key will be defined on the target model.
*
* @see {@link Model.hasOne}
*/
class HasOne extends Association {
constructor(source, target, options) {
super(source, target, options);
this.associationType = 'HasOne';
this.isSingleAssociation = true;
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 (!this.foreignKey) {
this.foreignKey = Utils.camelize(
[
Utils.singularize(this.options.as || this.source.name),
this.source.primaryKeyAttribute
].join('_')
);
}
if (
this.options.sourceKey
&& !this.source.rawAttributes[this.options.sourceKey]
) {
throw new Error(`Unknown attribute "${this.options.sourceKey}" passed as sourceKey, define this attribute on model "${this.source.name}" first`);
}
this.sourceKey = this.sourceKeyAttribute = this.options.sourceKey || this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
this.sourceKeyIsPrimary = this.sourceKey === this.source.primaryKeyAttribute;
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;
}
// Get singular name, trying to uppercase the first letter, unless the model forbids it
const singular = _.upperFirst(this.options.name.singular);
this.accessors = {
get: `get${singular}`,
set: `set${singular}`,
create: `create${singular}`
};
}
// the id is in the target table
_injectAttributes() {
const newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.sourceKey].type,
allowNull: true
});
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';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, this.options, this.sourceKeyField);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.target.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
}
mixin(obj) {
const methods = ['get', 'set', 'create'];
Helpers.mixinMethods(this, obj, methods);
}
/**
* Get the associated instance.
*
* @param {Model|Array<Model>} instances source instances
* @param {Object} [options] find options
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
* @param {string} [options.schema] Apply a schema on the related model
*
* @see
* {@link Model.findOne} for a full explanation of options
*
* @returns {Promise<Model>}
*/
get(instances, options) {
const where = {};
let Target = this.target;
let instance;
options = Utils.cloneDeep(options);
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
}
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
if (instances) {
where[this.foreignKey] = {
[Op.in]: instances.map(instance => instance.get(this.sourceKey))
};
} else {
where[this.foreignKey] = instance.get(this.sourceKey);
}
if (this.scope) {
Object.assign(where, this.scope);
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (instances) {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(this.sourceKey, { raw: true })] = null;
}
for (const instance of results) {
result[instance.get(this.foreignKey, { raw: true })] = instance;
}
return result;
});
}
return Target.findOne(options);
}
/**
* Set the associated model.
*
* @param {Model} sourceInstance the source instance
* @param {?<Model>|string|number} [associatedInstance] 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 getAssociation and `target.save`
*
* @returns {Promise}
*/
set(sourceInstance, associatedInstance, options) {
let alreadyAssociated;
options = Object.assign({}, options, {
scope: false
});
return sourceInstance[this.accessors.get](options).then(oldInstance => {
// TODO Use equals method once #5605 is resolved
alreadyAssociated = oldInstance && associatedInstance && this.target.primaryKeyAttributes.every(attribute =>
oldInstance.get(attribute, { raw: true }) === (associatedInstance.get ? associatedInstance.get(attribute, { raw: true }) : associatedInstance)
);
if (oldInstance && !alreadyAssociated) {
oldInstance[this.foreignKey] = null;
return oldInstance.save(Object.assign({}, options, {
fields: [this.foreignKey],
allowNull: [this.foreignKey],
association: true
}));
}
}).then(() => {
if (associatedInstance && !alreadyAssociated) {
if (!(associatedInstance instanceof this.target)) {
const tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = associatedInstance;
associatedInstance = this.target.build(tmpInstance, {
isNewRecord: false
});
}
Object.assign(associatedInstance, this.scope);
associatedInstance.set(this.foreignKey, sourceInstance.get(this.sourceKeyAttribute));
return associatedInstance.save(options);
}
return null;
});
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance the source instance
* @param {Object} [values={}] values to create associated model instance with
* @param {Object} [options] Options passed to `target.create` and setAssociation.
*
* @see
* {@link Model#create} for a full explanation of options
*
* @returns {Promise<Model>} The created target model
*/
create(sourceInstance, values, options) {
values = values || {};
options = options || {};
if (this.scope) {
for (const attribute of Object.keys(this.scope)) {
values[attribute] = this.scope[attribute];
if (options.fields) {
options.fields.push(attribute);
}
}
}
values[this.foreignKey] = sourceInstance.get(this.sourceKeyAttribute);
if (options.fields) {
options.fields.push(this.foreignKey);
}
return this.target.create(values, options);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.singular) {
return this.as === alias.singular;
}
return !this.isAliased;
}
}
module.exports = HasOne;
+69
View File
@@ -0,0 +1,69 @@
'use strict';
function checkNamingCollision(association) {
if (Object.prototype.hasOwnProperty.call(association.source.rawAttributes, association.as)) {
throw new Error(
`Naming collision between attribute '${association.as}'` +
` and association '${association.as}' on model ${association.source.name}` +
'. To remedy this, change either foreignKey or as in your association definition'
);
}
}
exports.checkNamingCollision = checkNamingCollision;
function addForeignKeyConstraints(newAttribute, source, target, options, key) {
// FK constraints are opt-in: users must either set `foreignKeyConstraints`
// on the association, or request an `onDelete` or `onUpdate` behavior
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach
const primaryKeys = Object.keys(source.primaryKeys)
.map(primaryKeyAttribute => source.rawAttributes[primaryKeyAttribute].field || primaryKeyAttribute);
if (primaryKeys.length === 1 || !primaryKeys.includes(key)) {
if (source._schema) {
newAttribute.references = {
model: source.sequelize.getQueryInterface().QueryGenerator.addSchema({
tableName: source.tableName,
_schema: source._schema,
_schemaDelimiter: source._schemaDelimiter
})
};
} else {
newAttribute.references = { model: source.tableName };
}
newAttribute.references.key = key || primaryKeys[0];
newAttribute.onDelete = options.onDelete;
newAttribute.onUpdate = options.onUpdate;
}
}
}
exports.addForeignKeyConstraints = addForeignKeyConstraints;
/**
* Mixin (inject) association methods to model prototype
*
* @private
*
* @param {Object} association instance
* @param {Object} obj Model prototype
* @param {Array} methods Method names to inject
* @param {Object} aliases Mapping between model and association method names
*
*/
function mixinMethods(association, obj, methods, aliases) {
aliases = aliases || {};
for (const method of methods) {
// don't override custom methods
if (!Object.prototype.hasOwnProperty.call(obj, association.accessors[method])) {
const realMethod = aliases[method] || method;
obj[association.accessors[method]] = function() {
return association[realMethod](this, ...Array.from(arguments));
};
}
}
}
exports.mixinMethods = mixinMethods;
+12
View File
@@ -0,0 +1,12 @@
'use strict';
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;
+124
View File
@@ -0,0 +1,124 @@
'use strict';
const _ = require('lodash');
const HasOne = require('./has-one');
const HasMany = require('./has-many');
const BelongsToMany = require('./belongs-to-many');
const BelongsTo = require('./belongs-to');
function isModel(model, sequelize) {
return model
&& model.prototype
&& model.prototype instanceof sequelize.Sequelize.Model;
}
const Mixin = {
hasMany(target, options = {}) {
if (!isModel(target, this.sequelize)) {
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.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options = Object.assign(options, _.omit(source.options, ['hooks']));
if (options.useHooks) {
this.runHooks('beforeAssociate', { source, target, type: HasMany }, options);
}
// 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);
if (options.useHooks) {
this.runHooks('afterAssociate', { source, target, type: HasMany, association }, options);
}
return association;
},
belongsToMany(target, options = {}) {
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.belongsToMany 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.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options.timestamps = options.timestamps === undefined ? this.sequelize.options.timestamps : options.timestamps;
options = Object.assign(options, _.omit(source.options, ['hooks', 'timestamps', 'scopes', 'defaultScope']));
if (options.useHooks) {
this.runHooks('beforeAssociate', { source, target, type: BelongsToMany }, options);
}
// the id is in the foreign table or in a connecting table
const association = new BelongsToMany(source, target, options);
source.associations[association.associationAccessor] = association;
association._injectAttributes();
association.mixin(source.prototype);
if (options.useHooks) {
this.runHooks('afterAssociate', { source, target, type: BelongsToMany, association }, options);
}
return association;
},
getAssociations(target) {
return _.values(this.associations).filter(association => association.target.name === target.name);
},
getAssociationForAlias(target, alias) {
// Two associations cannot have the same alias, so we can use find instead of filter
return this.getAssociations(target).find(association => association.verifyAssociationAlias(alias)) || null;
}
};
// The logic for hasOne and belongsTo is exactly the same
function singleLinked(Type) {
return function(target, options = {}) {
// eslint-disable-next-line no-invalid-this
const source = this;
if (!isModel(target, source.sequelize)) {
throw new Error(`${source.name}.${_.lowerFirst(Type.name)} called with something that's not a subclass of Sequelize.Model`);
}
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
if (options.useHooks) {
source.runHooks('beforeAssociate', { source, target, type: Type }, options);
}
// the id is in the foreign table
const association = new Type(source, target, Object.assign(options, source.options));
source.associations[association.associationAccessor] = association;
association._injectAttributes();
association.mixin(source.prototype);
if (options.useHooks) {
source.runHooks('afterAssociate', { source, target, type: Type, association }, options);
}
return association;
};
}
Mixin.hasOne = singleLinked(HasOne);
Mixin.belongsTo = singleLinked(BelongsTo);
module.exports = Mixin;
module.exports.Mixin = Mixin;
module.exports.default = Mixin;