UNPKG

sequelize

Version:

Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift and Snowflake’s Data Cloud. It features solid transaction support, relations, eager and lazy loading, read replication and more.

567 lines (566 loc) 23.1 kB
"use strict"; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); 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"); class BelongsToMany extends Association { constructor(source, target, options) { super(source, target, options); if (this.options.through === void 0 || 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 = __spreadValues({}, 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 (this.isSelfAssociation) { this.targetAssociation = this; } _.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.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: [], paranoid: this.through.paranoid ? this.through.paranoid : false, validate: {} })); } else { this.through.model = this.sequelize.model(this.through.model); } } 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) { 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 (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; 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() { 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("_")); } } _injectAttributes() { this.identifier = this.foreignKey; this.foreignIdentifier = this.otherKey; _.each(this.through.model.rawAttributes, (attribute, attributeName) => { if (attribute.primaryKey === true && attribute._autoGenerated === true) { if ([this.foreignKey, this.otherKey].includes(attributeName)) { 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 = __spreadValues({ type: sourceKeyType }, this.foreignKeyAttribute); const targetAttribute = __spreadValues({ type: targetKeyType }, this.otherKeyAttribute); 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 }; 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 }; 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"; } Object.assign(this.through.model.rawAttributes[this.foreignKey], sourceAttribute); 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.options.sequelize.options.dialect === "db2" && this.source.rawAttributes[this.sourceKey].primaryKey !== true) { this.source.rawAttributes[this.sourceKey].unique = true; } 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, sourceKey: this.sourceKey, 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, sourceKey: this.targetKey, 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, sourceKey: this.paired.targetKey, 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); } async get(instance, options) { options = Utils.cloneDeep(options) || {}; const through = this.through; let scopeWhere; let throughWhere; if (this.scope) { scopeWhere = __spreadValues({}, 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 (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, paranoid: _.get(options.through, "paranoid", 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); } async 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; const result = await this.get(instance, options); return parseInt(result.count, 10); } async has(sourceInstance, instances, options) { if (!Array.isArray(instances)) { instances = [instances]; } options = __spreadProps(__spreadValues({ 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 ] }; const associatedObjects = await this.get(sourceInstance, options); return _.differenceWith(instancePrimaryKeys, associatedObjects, (a, b) => _.isEqual(a[this.targetKey], b[this.targetKey])).length === 0; } async set(sourceInstance, newAssociatedObjects, options) { options = options || {}; const sourceKey = this.sourceKey; const targetKey = this.targetKey; const identifier = this.identifier; const foreignIdentifier = this.foreignIdentifier; if (newAssociatedObjects === null) { newAssociatedObjects = []; } else { newAssociatedObjects = this.toInstanceArray(newAssociatedObjects); } const where = __spreadValues({ [identifier]: sourceInstance.get(sourceKey) }, 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]; if (throughAttributes instanceof this.through.model) { throughAttributes = {}; } const attributes = __spreadValues(__spreadValues({}, defaultAttributes), throughAttributes); 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) { promises.push(this.through.model.destroy(__spreadProps(__spreadValues({}, options), { where: __spreadValues({ [identifier]: sourceInstance.get(sourceKey), [foreignIdentifier]: obsoleteAssociations.map((obsoleteAssociation) => obsoleteAssociation[foreignIdentifier]) }, this.through.scope) }))); } if (unassociatedObjects.length > 0) { const bulk = unassociatedObjects.map((unassociatedObject) => { return __spreadValues(__spreadProps(__spreadValues(__spreadValues({}, defaultAttributes), unassociatedObject[this.through.model.name]), { [identifier]: sourceInstance.get(sourceKey), [foreignIdentifier]: unassociatedObject.get(targetKey) }), this.through.scope); }); promises.push(this.through.model.bulkCreate(bulk, __spreadValues({ validate: true }, options))); } return Promise.all(promises); }; try { const currentRows = await this.through.model.findAll(__spreadProps(__spreadValues({}, options), { where, raw: true })); return await updateAssociations(currentRows); } catch (error) { if (error instanceof EmptyResultError) return updateAssociations([]); throw error; } } async add(sourceInstance, newInstances, options) { if (!newInstances) return Promise.resolve(); options = __spreadValues({}, 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 = __spreadValues({ [identifier]: sourceInstance.get(sourceKey), [foreignIdentifier]: newInstances.map((newInstance) => newInstance.get(targetKey)) }, 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 = __spreadValues(__spreadValues({}, defaultAttributes), throughAttributes); 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 = __spreadValues(__spreadValues({}, defaultAttributes), throughAttributes); 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, __spreadValues({ validate: true }, options))); } for (const assoc of changedAssociations) { let throughAttributes = assoc[association.through.model.name]; const attributes = __spreadValues(__spreadValues({}, defaultAttributes), throughAttributes); if (throughAttributes instanceof association.through.model) { throughAttributes = {}; } promises.push(association.through.model.update(attributes, Object.assign(options, { where: { [identifier]: sourceInstance.get(sourceKey), [foreignIdentifier]: assoc.get(targetKey) } }))); } return Promise.all(promises); }; try { const currentRows = await association.through.model.findAll(__spreadProps(__spreadValues({}, options), { where, raw: true })); const [associations] = await updateAssociations(currentRows); return associations; } catch (error) { if (error instanceof EmptyResultError) return updateAssociations(); throw error; } } 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(__spreadProps(__spreadValues({}, options), { where })); } async 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)); } } const newAssociatedObject = await association.target.create(values, options); await 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; //# sourceMappingURL=belongs-to-many.js.map