API Reference Source

lib/associations/belongs-to-many.js

  1. 'use strict';
  2.  
  3. const Utils = require('./../utils');
  4. const Helpers = require('./helpers');
  5. const _ = require('lodash');
  6. const Association = require('./base');
  7. const BelongsTo = require('./belongs-to');
  8. const HasMany = require('./has-many');
  9. const HasOne = require('./has-one');
  10. const AssociationError = require('../errors').AssociationError;
  11. const EmptyResultError = require('../errors').EmptyResultError;
  12. const Op = require('../operators');
  13.  
  14. /**
  15. * Many-to-many association with a join table.
  16. *
  17. * When the join table has additional attributes, these can be passed in the options object:
  18. *
  19. * ```js
  20. * UserProject = sequelize.define('user_project', {
  21. * role: Sequelize.STRING
  22. * });
  23. * User.belongsToMany(Project, { through: UserProject });
  24. * Project.belongsToMany(User, { through: UserProject });
  25. * // through is required!
  26. *
  27. * user.addProject(project, { through: { role: 'manager' }});
  28. * ```
  29. *
  30. * All methods allow you to pass either a persisted instance, its primary key, or a mixture:
  31. *
  32. * ```js
  33. * Project.create({ id: 11 }).then(project => {
  34. * user.addProjects([project, 12]);
  35. * });
  36. * ```
  37. *
  38. * 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:
  39. *
  40. * ```js
  41. * p1.UserProjects = {
  42. * started: true
  43. * }
  44. * user.setProjects([p1, p2], { through: { started: false }}) // The default value is false, but p1 overrides that.
  45. * ```
  46. *
  47. * 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.
  48. * ```js
  49. * user.getProjects().then(projects => {
  50. * let p1 = projects[0]
  51. * p1.UserProjects.started // Is this project started yet?
  52. * })
  53. * ```
  54. *
  55. * 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()`.
  56. *
  57. * @see {@link Model.belongsToMany}
  58. */
  59. class BelongsToMany extends Association {
  60. constructor(source, target, options) {
  61. super(source, target, options);
  62.  
  63. if (this.options.through === undefined || this.options.through === true || this.options.through === null) {
  64. throw new AssociationError(`${source.name}.belongsToMany(${target.name}) requires through option, pass either a string or a model`);
  65. }
  66.  
  67. if (!this.options.through.model) {
  68. this.options.through = {
  69. model: options.through
  70. };
  71. }
  72.  
  73. this.associationType = 'BelongsToMany';
  74. this.targetAssociation = null;
  75. this.sequelize = source.sequelize;
  76. this.through = Object.assign({}, this.options.through);
  77. this.isMultiAssociation = true;
  78. this.doubleLinked = false;
  79.  
  80. if (!this.as && this.isSelfAssociation) {
  81. throw new AssociationError('\'as\' must be defined for many-to-many self-associations');
  82. }
  83.  
  84. if (this.as) {
  85. this.isAliased = true;
  86.  
  87. if (_.isPlainObject(this.as)) {
  88. this.options.name = this.as;
  89. this.as = this.as.plural;
  90. } else {
  91. this.options.name = {
  92. plural: this.as,
  93. singular: Utils.singularize(this.as)
  94. };
  95. }
  96. } else {
  97. this.as = this.target.options.name.plural;
  98. this.options.name = this.target.options.name;
  99. }
  100.  
  101. this.combinedTableName = Utils.combineTableNames(
  102. this.source.tableName,
  103. this.isSelfAssociation ? this.as || this.target.tableName : this.target.tableName
  104. );
  105.  
  106. /*
  107. * If self association, this is the target association - Unless we find a pairing association
  108. */
  109. if (this.isSelfAssociation) {
  110. this.targetAssociation = this;
  111. }
  112.  
  113. /*
  114. * Find paired association (if exists)
  115. */
  116. _.each(this.target.associations, association => {
  117. if (association.associationType !== 'BelongsToMany') return;
  118. if (association.target !== this.source) return;
  119.  
  120. if (this.options.through.model === association.options.through.model) {
  121. this.paired = association;
  122. association.paired = this;
  123. }
  124. });
  125.  
  126. /*
  127. * Default/generated source/target keys
  128. */
  129. this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
  130. this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
  131.  
  132. if (this.options.targetKey) {
  133. this.targetKey = this.options.targetKey;
  134. this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
  135. } else {
  136. this.targetKeyDefault = true;
  137. this.targetKey = this.target.primaryKeyAttribute;
  138. this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
  139. }
  140.  
  141. this._createForeignAndOtherKeys();
  142.  
  143. if (typeof this.through.model === 'string') {
  144. if (!this.sequelize.isDefined(this.through.model)) {
  145. this.through.model = this.sequelize.define(this.through.model, {}, Object.assign(this.options, {
  146. tableName: this.through.model,
  147. indexes: [], //we don't want indexes here (as referenced in #2416)
  148. paranoid: false, // A paranoid join table does not make sense
  149. validate: {} // Don't propagate model-level validations
  150. }));
  151. } else {
  152. this.through.model = this.sequelize.model(this.through.model);
  153. }
  154. }
  155.  
  156. this.options = Object.assign(this.options, _.pick(this.through.model.options, [
  157. 'timestamps', 'createdAt', 'updatedAt', 'deletedAt', 'paranoid'
  158. ]));
  159.  
  160. if (this.paired) {
  161. let needInjectPaired = false;
  162.  
  163. if (this.targetKeyDefault) {
  164. this.targetKey = this.paired.sourceKey;
  165. this.targetKeyField = this.paired.sourceKeyField;
  166. this._createForeignAndOtherKeys();
  167. }
  168. if (this.paired.targetKeyDefault) {
  169. // in this case paired.otherKey depends on paired.targetKey,
  170. // so cleanup previously wrong generated otherKey
  171. if (this.paired.targetKey !== this.sourceKey) {
  172. delete this.through.model.rawAttributes[this.paired.otherKey];
  173. this.paired.targetKey = this.sourceKey;
  174. this.paired.targetKeyField = this.sourceKeyField;
  175. this.paired._createForeignAndOtherKeys();
  176. needInjectPaired = true;
  177. }
  178. }
  179.  
  180. if (this.otherKeyDefault) {
  181. this.otherKey = this.paired.foreignKey;
  182. }
  183. if (this.paired.otherKeyDefault) {
  184. // If paired otherKey was inferred we should make sure to clean it up
  185. // before adding a new one that matches the foreignKey
  186. if (this.paired.otherKey !== this.foreignKey) {
  187. delete this.through.model.rawAttributes[this.paired.otherKey];
  188. this.paired.otherKey = this.foreignKey;
  189. needInjectPaired = true;
  190. }
  191. }
  192.  
  193. if (needInjectPaired) {
  194. this.paired._injectAttributes();
  195. }
  196. }
  197.  
  198. if (this.through) {
  199. this.throughModel = this.through.model;
  200. }
  201.  
  202. this.options.tableName = this.combinedName = this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model;
  203.  
  204. this.associationAccessor = this.as;
  205.  
  206. // Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
  207. const plural = _.upperFirst(this.options.name.plural);
  208. const singular = _.upperFirst(this.options.name.singular);
  209.  
  210. this.accessors = {
  211. get: `get${plural}`,
  212. set: `set${plural}`,
  213. addMultiple: `add${plural}`,
  214. add: `add${singular}`,
  215. create: `create${singular}`,
  216. remove: `remove${singular}`,
  217. removeMultiple: `remove${plural}`,
  218. hasSingle: `has${singular}`,
  219. hasAll: `has${plural}`,
  220. count: `count${plural}`
  221. };
  222. }
  223.  
  224. _createForeignAndOtherKeys() {
  225. /*
  226. * Default/generated foreign/other keys
  227. */
  228. if (_.isObject(this.options.foreignKey)) {
  229. this.foreignKeyAttribute = this.options.foreignKey;
  230. this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
  231. } else {
  232. this.foreignKeyAttribute = {};
  233. this.foreignKey = this.options.foreignKey || Utils.camelize(
  234. [
  235. this.source.options.name.singular,
  236. this.sourceKey
  237. ].join('_')
  238. );
  239. }
  240.  
  241. if (_.isObject(this.options.otherKey)) {
  242. this.otherKeyAttribute = this.options.otherKey;
  243. this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
  244. } else {
  245. if (!this.options.otherKey) {
  246. this.otherKeyDefault = true;
  247. }
  248.  
  249. this.otherKeyAttribute = {};
  250. this.otherKey = this.options.otherKey || Utils.camelize(
  251. [
  252. this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
  253. this.targetKey
  254. ].join('_')
  255. );
  256. }
  257. }
  258.  
  259. // the id is in the target table
  260. // or in an extra table which connects two tables
  261. _injectAttributes() {
  262. this.identifier = this.foreignKey;
  263. this.foreignIdentifier = this.otherKey;
  264.  
  265. // remove any PKs previously defined by sequelize
  266. // but ignore any keys that are part of this association (#5865)
  267. _.each(this.through.model.rawAttributes, (attribute, attributeName) => {
  268. if (attribute.primaryKey === true && attribute._autoGenerated === true) {
  269. if (attributeName === this.foreignKey || attributeName === this.otherKey) {
  270. // this key is still needed as it's part of the association
  271. // so just set primaryKey to false
  272. attribute.primaryKey = false;
  273. }
  274. else {
  275. delete this.through.model.rawAttributes[attributeName];
  276. }
  277. this.primaryKeyDeleted = true;
  278. }
  279. });
  280.  
  281. const sourceKey = this.source.rawAttributes[this.sourceKey];
  282. const sourceKeyType = sourceKey.type;
  283. const sourceKeyField = this.sourceKeyField;
  284. const targetKey = this.target.rawAttributes[this.targetKey];
  285. const targetKeyType = targetKey.type;
  286. const targetKeyField = this.targetKeyField;
  287. const sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType });
  288. const targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });
  289.  
  290. if (this.primaryKeyDeleted === true) {
  291. targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
  292. } else if (this.through.unique !== false) {
  293. let uniqueKey;
  294. if (typeof this.options.uniqueKey === 'string' && this.options.uniqueKey !== '') {
  295. uniqueKey = this.options.uniqueKey;
  296. } else {
  297. uniqueKey = [this.through.model.tableName, this.foreignKey, this.otherKey, 'unique'].join('_');
  298. }
  299. targetAttribute.unique = sourceAttribute.unique = uniqueKey;
  300. }
  301.  
  302. if (!this.through.model.rawAttributes[this.foreignKey]) {
  303. this.through.model.rawAttributes[this.foreignKey] = {
  304. _autoGenerated: true
  305. };
  306. }
  307.  
  308. if (!this.through.model.rawAttributes[this.otherKey]) {
  309. this.through.model.rawAttributes[this.otherKey] = {
  310. _autoGenerated: true
  311. };
  312. }
  313.  
  314. if (this.options.constraints !== false) {
  315. sourceAttribute.references = {
  316. model: this.source.getTableName(),
  317. key: sourceKeyField
  318. };
  319. // For the source attribute the passed option is the priority
  320. sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.foreignKey].onDelete;
  321. sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.foreignKey].onUpdate;
  322.  
  323. if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
  324. if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
  325.  
  326. targetAttribute.references = {
  327. model: this.target.getTableName(),
  328. key: targetKeyField
  329. };
  330. // But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call)
  331. targetAttribute.onDelete = this.through.model.rawAttributes[this.otherKey].onDelete || this.options.onDelete;
  332. targetAttribute.onUpdate = this.through.model.rawAttributes[this.otherKey].onUpdate || this.options.onUpdate;
  333.  
  334. if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE';
  335. if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
  336. }
  337.  
  338. this.through.model.rawAttributes[this.foreignKey] = Object.assign(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
  339. this.through.model.rawAttributes[this.otherKey] = Object.assign(this.through.model.rawAttributes[this.otherKey], targetAttribute);
  340.  
  341. this.through.model.refreshAttributes();
  342.  
  343. this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
  344. this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
  345.  
  346. if (this.paired && !this.paired.foreignIdentifierField) {
  347. this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
  348. }
  349.  
  350. this.toSource = new BelongsTo(this.through.model, this.source, {
  351. foreignKey: this.foreignKey
  352. });
  353. this.manyFromSource = new HasMany(this.source, this.through.model, {
  354. foreignKey: this.foreignKey
  355. });
  356. this.oneFromSource = new HasOne(this.source, this.through.model, {
  357. foreignKey: this.foreignKey,
  358. as: this.through.model.name
  359. });
  360.  
  361. this.toTarget = new BelongsTo(this.through.model, this.target, {
  362. foreignKey: this.otherKey
  363. });
  364. this.manyFromTarget = new HasMany(this.target, this.through.model, {
  365. foreignKey: this.otherKey
  366. });
  367. this.oneFromTarget = new HasOne(this.target, this.through.model, {
  368. foreignKey: this.otherKey,
  369. as: this.through.model.name
  370. });
  371.  
  372. if (this.paired && this.paired.otherKeyDefault) {
  373. this.paired.toTarget = new BelongsTo(this.paired.through.model, this.paired.target, {
  374. foreignKey: this.paired.otherKey
  375. });
  376.  
  377. this.paired.oneFromTarget = new HasOne(this.paired.target, this.paired.through.model, {
  378. foreignKey: this.paired.otherKey,
  379. as: this.paired.through.model.name
  380. });
  381. }
  382.  
  383. Helpers.checkNamingCollision(this);
  384.  
  385. return this;
  386. }
  387.  
  388. mixin(obj) {
  389. const methods = ['get', 'count', 'hasSingle', 'hasAll', 'set', 'add', 'addMultiple', 'remove', 'removeMultiple', 'create'];
  390. const aliases = {
  391. hasSingle: 'has',
  392. hasAll: 'has',
  393. addMultiple: 'add',
  394. removeMultiple: 'remove'
  395. };
  396.  
  397. Helpers.mixinMethods(this, obj, methods, aliases);
  398. }
  399.  
  400. /**
  401. * Get everything currently associated with this, using an optional where clause.
  402. *
  403. * @see
  404. * {@link Model} for a full explanation of options
  405. *
  406. * @param {Model} instance instance
  407. * @param {Object} [options] find options
  408. * @param {Object} [options.where] An optional where clause to limit the associated models
  409. * @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
  410. * @param {string} [options.schema] Apply a schema on the related model
  411. *
  412. * @returns {Promise<Array<Model>>}
  413. */
  414. get(instance, options) {
  415. options = Utils.cloneDeep(options) || {};
  416.  
  417. const through = this.through;
  418. let scopeWhere;
  419. let throughWhere;
  420.  
  421. if (this.scope) {
  422. scopeWhere = _.clone(this.scope);
  423. }
  424.  
  425. options.where = {
  426. [Op.and]: [
  427. scopeWhere,
  428. options.where
  429. ]
  430. };
  431.  
  432. if (Object(through.model) === through.model) {
  433. throughWhere = {};
  434. throughWhere[this.foreignKey] = instance.get(this.sourceKey);
  435.  
  436. if (through.scope) {
  437. Object.assign(throughWhere, through.scope);
  438. }
  439.  
  440. //If a user pass a where on the options through options, make an "and" with the current throughWhere
  441. if (options.through && options.through.where) {
  442. throughWhere = {
  443. [Op.and]: [throughWhere, options.through.where]
  444. };
  445. }
  446.  
  447. options.include = options.include || [];
  448. options.include.push({
  449. association: this.oneFromTarget,
  450. attributes: options.joinTableAttributes,
  451. required: true,
  452. where: throughWhere
  453. });
  454. }
  455.  
  456. let model = this.target;
  457. if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
  458. if (!options.scope) {
  459. model = model.unscoped();
  460. } else {
  461. model = model.scope(options.scope);
  462. }
  463. }
  464.  
  465. if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
  466. model = model.schema(options.schema, options.schemaDelimiter);
  467. }
  468.  
  469. return model.findAll(options);
  470. }
  471.  
  472. /**
  473. * Count everything currently associated with this, using an optional where clause.
  474. *
  475. * @param {Model} instance instance
  476. * @param {Object} [options] find options
  477. * @param {Object} [options.where] An optional where clause to limit the associated models
  478. * @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
  479. *
  480. * @returns {Promise<number>}
  481. */
  482. count(instance, options) {
  483. const sequelize = this.target.sequelize;
  484.  
  485. options = Utils.cloneDeep(options);
  486. options.attributes = [
  487. [sequelize.fn('COUNT', sequelize.col([this.target.name, this.targetKeyField].join('.'))), 'count']
  488. ];
  489. options.joinTableAttributes = [];
  490. options.raw = true;
  491. options.plain = true;
  492.  
  493. return this.get(instance, options).then(result => parseInt(result.count, 10));
  494. }
  495.  
  496. /**
  497. * 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
  498. *
  499. * @param {Model} sourceInstance source instance to check for an association with
  500. * @param {Model|Model[]|string[]|string|number[]|number} [instances] Can be an array of instances or their primary keys
  501. * @param {Object} [options] Options passed to getAssociations
  502. *
  503. * @returns {Promise<boolean>}
  504. */
  505. has(sourceInstance, instances, options) {
  506. if (!Array.isArray(instances)) {
  507. instances = [instances];
  508. }
  509.  
  510. options = Object.assign({
  511. raw: true
  512. }, options, {
  513. scope: false,
  514. attributes: [this.targetKey],
  515. joinTableAttributes: []
  516. });
  517.  
  518. const instancePrimaryKeys = instances.map(instance => {
  519. if (instance instanceof this.target) {
  520. return instance.where();
  521. }
  522. return {
  523. [this.targetKey]: instance
  524. };
  525. });
  526.  
  527. options.where = {
  528. [Op.and]: [
  529. { [Op.or]: instancePrimaryKeys },
  530. options.where
  531. ]
  532. };
  533.  
  534. return this.get(sourceInstance, options).then(associatedObjects =>
  535. _.differenceWith(instancePrimaryKeys, associatedObjects,
  536. (a, b) => _.isEqual(a[this.targetKey], b[this.targetKey])).length === 0
  537. );
  538. }
  539.  
  540. /**
  541. * Set the associated models by passing an array of instances or their primary keys.
  542. * Everything that it not in the passed array will be un-associated.
  543. *
  544. * @param {Model} sourceInstance source instance to associate new instances with
  545. * @param {Model|Model[]|string[]|string|number[]|number} [newAssociatedObjects] A single instance or primary key, or a mixed array of persisted instances or primary keys
  546. * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`
  547. * @param {Object} [options.validate] Run validation for the join model
  548. * @param {Object} [options.through] Additional attributes for the join table.
  549. *
  550. * @returns {Promise}
  551. */
  552. set(sourceInstance, newAssociatedObjects, options) {
  553. options = options || {};
  554.  
  555. const sourceKey = this.sourceKey;
  556. const targetKey = this.targetKey;
  557. const identifier = this.identifier;
  558. const foreignIdentifier = this.foreignIdentifier;
  559. let where = {};
  560.  
  561. if (newAssociatedObjects === null) {
  562. newAssociatedObjects = [];
  563. } else {
  564. newAssociatedObjects = this.toInstanceArray(newAssociatedObjects);
  565. }
  566.  
  567. where[identifier] = sourceInstance.get(sourceKey);
  568. where = Object.assign(where, this.through.scope);
  569.  
  570. const updateAssociations = currentRows => {
  571. const obsoleteAssociations = [];
  572. const promises = [];
  573. const defaultAttributes = options.through || {};
  574.  
  575. const unassociatedObjects = newAssociatedObjects.filter(obj =>
  576. !currentRows.some(currentRow => currentRow[foreignIdentifier] === obj.get(targetKey))
  577. );
  578.  
  579. for (const currentRow of currentRows) {
  580. const newObj = newAssociatedObjects.find(obj => currentRow[foreignIdentifier] === obj.get(targetKey));
  581.  
  582. if (!newObj) {
  583. obsoleteAssociations.push(currentRow);
  584. } else {
  585. let throughAttributes = newObj[this.through.model.name];
  586. // Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
  587. if (throughAttributes instanceof this.through.model) {
  588. throughAttributes = {};
  589. }
  590.  
  591. const attributes = _.defaults({}, throughAttributes, defaultAttributes);
  592.  
  593. if (Object.keys(attributes).length) {
  594. promises.push(
  595. this.through.model.update(attributes, Object.assign(options, {
  596. where: {
  597. [identifier]: sourceInstance.get(sourceKey),
  598. [foreignIdentifier]: newObj.get(targetKey)
  599. }
  600. }
  601. ))
  602. );
  603. }
  604. }
  605. }
  606.  
  607. if (obsoleteAssociations.length > 0) {
  608. const where = Object.assign({
  609. [identifier]: sourceInstance.get(sourceKey),
  610. [foreignIdentifier]: obsoleteAssociations.map(obsoleteAssociation => obsoleteAssociation[foreignIdentifier])
  611. }, this.through.scope);
  612. promises.push(
  613. this.through.model.destroy(_.defaults({
  614. where
  615. }, options))
  616. );
  617. }
  618.  
  619. if (unassociatedObjects.length > 0) {
  620. const bulk = unassociatedObjects.map(unassociatedObject => {
  621. let attributes = {};
  622.  
  623. attributes[identifier] = sourceInstance.get(sourceKey);
  624. attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
  625.  
  626. attributes = _.defaults(attributes, unassociatedObject[this.through.model.name], defaultAttributes);
  627.  
  628. Object.assign(attributes, this.through.scope);
  629. attributes = Object.assign(attributes, this.through.scope);
  630.  
  631. return attributes;
  632. });
  633.  
  634. promises.push(this.through.model.bulkCreate(bulk, Object.assign({ validate: true }, options)));
  635. }
  636.  
  637. return Utils.Promise.all(promises);
  638. };
  639.  
  640. return this.through.model.findAll(_.defaults({ where, raw: true }, options))
  641. .then(currentRows => updateAssociations(currentRows))
  642. .catch(error => {
  643. if (error instanceof EmptyResultError) return updateAssociations([]);
  644. throw error;
  645. });
  646. }
  647.  
  648. /**
  649. * Associate one or several rows with source instance. It will not un-associate any already associated instance
  650. * that may be missing from `newInstances`.
  651. *
  652. * @param {Model} sourceInstance source instance to associate new instances with
  653. * @param {Model|Model[]|string[]|string|number[]|number} [newInstances] A single instance or primary key, or a mixed array of persisted instances or primary keys
  654. * @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`
  655. * @param {Object} [options.validate] Run validation for the join model.
  656. * @param {Object} [options.through] Additional attributes for the join table.
  657. *
  658. * @returns {Promise}
  659. */
  660. add(sourceInstance, newInstances, options) {
  661. // If newInstances is null or undefined, no-op
  662. if (!newInstances) return Utils.Promise.resolve();
  663.  
  664. options = _.clone(options) || {};
  665.  
  666. const association = this;
  667. const sourceKey = association.sourceKey;
  668. const targetKey = association.targetKey;
  669. const identifier = association.identifier;
  670. const foreignIdentifier = association.foreignIdentifier;
  671. const defaultAttributes = options.through || {};
  672.  
  673. newInstances = association.toInstanceArray(newInstances);
  674.  
  675. const where = {
  676. [identifier]: sourceInstance.get(sourceKey),
  677. [foreignIdentifier]: newInstances.map(newInstance => newInstance.get(targetKey))
  678. };
  679.  
  680. Object.assign(where, association.through.scope);
  681.  
  682. const updateAssociations = currentRows => {
  683. const promises = [];
  684. const unassociatedObjects = [];
  685. const changedAssociations = [];
  686. for (const obj of newInstances) {
  687. const existingAssociation = currentRows && currentRows.find(current => current[foreignIdentifier] === obj.get(targetKey));
  688.  
  689. if (!existingAssociation) {
  690. unassociatedObjects.push(obj);
  691. } else {
  692. const throughAttributes = obj[association.through.model.name];
  693. const attributes = _.defaults({}, throughAttributes, defaultAttributes);
  694.  
  695. if (Object.keys(attributes).some(attribute => attributes[attribute] !== existingAssociation[attribute])) {
  696. changedAssociations.push(obj);
  697. }
  698. }
  699. }
  700.  
  701. if (unassociatedObjects.length > 0) {
  702. const bulk = unassociatedObjects.map(unassociatedObject => {
  703. const throughAttributes = unassociatedObject[association.through.model.name];
  704. const attributes = _.defaults({}, throughAttributes, defaultAttributes);
  705.  
  706. attributes[identifier] = sourceInstance.get(sourceKey);
  707. attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
  708.  
  709. Object.assign(attributes, association.through.scope);
  710.  
  711. return attributes;
  712. });
  713.  
  714. promises.push(association.through.model.bulkCreate(bulk, Object.assign({ validate: true }, options)));
  715. }
  716.  
  717. for (const assoc of changedAssociations) {
  718. let throughAttributes = assoc[association.through.model.name];
  719. const attributes = _.defaults({}, throughAttributes, defaultAttributes);
  720. // Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
  721. if (throughAttributes instanceof association.through.model) {
  722. throughAttributes = {};
  723. }
  724. const where = {
  725. [identifier]: sourceInstance.get(sourceKey),
  726. [foreignIdentifier]: assoc.get(targetKey)
  727. };
  728.  
  729.  
  730. promises.push(association.through.model.update(attributes, Object.assign(options, { where })));
  731. }
  732.  
  733. return Utils.Promise.all(promises);
  734. };
  735.  
  736. return association.through.model.findAll(_.defaults({ where, raw: true }, options))
  737. .then(currentRows => updateAssociations(currentRows))
  738. .then(([associations]) => associations)
  739. .catch(error => {
  740. if (error instanceof EmptyResultError) return updateAssociations();
  741. throw error;
  742. });
  743. }
  744.  
  745. /**
  746. * Un-associate one or more instance(s).
  747. *
  748. * @param {Model} sourceInstance instance to un associate instances with
  749. * @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
  750. * @param {Object} [options] Options passed to `through.destroy`
  751. *
  752. * @returns {Promise}
  753. */
  754. remove(sourceInstance, oldAssociatedObjects, options) {
  755. const association = this;
  756.  
  757. options = options || {};
  758.  
  759. oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
  760.  
  761. const where = {
  762. [association.identifier]: sourceInstance.get(association.sourceKey),
  763. [association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.targetKey))
  764. };
  765.  
  766. return association.through.model.destroy(_.defaults({ where }, options));
  767. }
  768.  
  769. /**
  770. * Create a new instance of the associated model and associate it with this.
  771. *
  772. * @param {Model} sourceInstance source instance
  773. * @param {Object} [values] values for target model
  774. * @param {Object} [options] Options passed to create and add
  775. * @param {Object} [options.through] Additional attributes for the join table
  776. *
  777. * @returns {Promise}
  778. */
  779. create(sourceInstance, values, options) {
  780. const association = this;
  781.  
  782. options = options || {};
  783. values = values || {};
  784.  
  785. if (Array.isArray(options)) {
  786. options = {
  787. fields: options
  788. };
  789. }
  790.  
  791. if (association.scope) {
  792. Object.assign(values, association.scope);
  793. if (options.fields) {
  794. options.fields = options.fields.concat(Object.keys(association.scope));
  795. }
  796. }
  797.  
  798. // Create the related model instance
  799. return association.target.create(values, options).then(newAssociatedObject =>
  800. sourceInstance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject)
  801. );
  802. }
  803.  
  804. verifyAssociationAlias(alias) {
  805. if (typeof alias === 'string') {
  806. return this.as === alias;
  807. }
  808.  
  809. if (alias && alias.plural) {
  810. return this.as === alias.plural;
  811. }
  812.  
  813. return !this.isAliased;
  814. }
  815. }
  816.  
  817. module.exports = BelongsToMany;
  818. module.exports.BelongsToMany = BelongsToMany;
  819. module.exports.default = BelongsToMany;