API Reference Source

lib/associations/belongs-to.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 Op = require('../operators');
  8.  
  9. /**
  10. * One-to-one association
  11. *
  12. * 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()`.
  13. *
  14. * @see {@link Model.belongsTo}
  15. */
  16. class BelongsTo extends Association {
  17. constructor(source, target, options) {
  18. super(source, target, options);
  19.  
  20. this.associationType = 'BelongsTo';
  21. this.isSingleAssociation = true;
  22. this.foreignKeyAttribute = {};
  23.  
  24. if (this.as) {
  25. this.isAliased = true;
  26. this.options.name = {
  27. singular: this.as
  28. };
  29. } else {
  30. this.as = this.target.options.name.singular;
  31. this.options.name = this.target.options.name;
  32. }
  33.  
  34. if (_.isObject(this.options.foreignKey)) {
  35. this.foreignKeyAttribute = this.options.foreignKey;
  36. this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
  37. } else if (this.options.foreignKey) {
  38. this.foreignKey = this.options.foreignKey;
  39. }
  40.  
  41. if (!this.foreignKey) {
  42. this.foreignKey = Utils.camelize(
  43. [
  44. this.as,
  45. this.target.primaryKeyAttribute
  46. ].join('_')
  47. );
  48. }
  49.  
  50. this.identifier = this.foreignKey;
  51. if (this.source.rawAttributes[this.identifier]) {
  52. this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
  53. }
  54.  
  55. if (
  56. this.options.targetKey
  57. && !this.target.rawAttributes[this.options.targetKey]
  58. ) {
  59. throw new Error(`Unknown attribute "${this.options.targetKey}" passed as targetKey, define this attribute on model "${this.target.name}" first`);
  60. }
  61.  
  62. this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
  63. this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
  64. this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute;
  65. this.targetIdentifier = this.targetKey;
  66.  
  67. this.associationAccessor = this.as;
  68. this.options.useHooks = options.useHooks;
  69.  
  70. // Get singular name, trying to uppercase the first letter, unless the model forbids it
  71. const singular = _.upperFirst(this.options.name.singular);
  72.  
  73. this.accessors = {
  74. get: `get${singular}`,
  75. set: `set${singular}`,
  76. create: `create${singular}`
  77. };
  78. }
  79.  
  80. // the id is in the source table
  81. _injectAttributes() {
  82. const newAttributes = {};
  83.  
  84. newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
  85. type: this.options.keyType || this.target.rawAttributes[this.targetKey].type,
  86. allowNull: true
  87. });
  88.  
  89. if (this.options.constraints !== false) {
  90. const source = this.source.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
  91. this.options.onDelete = this.options.onDelete || (source.allowNull ? 'SET NULL' : 'NO ACTION');
  92. this.options.onUpdate = this.options.onUpdate || 'CASCADE';
  93. }
  94.  
  95. Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
  96. Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
  97.  
  98. this.source.refreshAttributes();
  99.  
  100. this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
  101.  
  102. Helpers.checkNamingCollision(this);
  103.  
  104. return this;
  105. }
  106.  
  107. mixin(obj) {
  108. const methods = ['get', 'set', 'create'];
  109.  
  110. Helpers.mixinMethods(this, obj, methods);
  111. }
  112.  
  113. /**
  114. * Get the associated instance.
  115. *
  116. * @param {Model|Array<Model>} instances source instances
  117. * @param {Object} [options] find options
  118. * @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false.
  119. * @param {string} [options.schema] Apply a schema on the related model
  120. *
  121. * @see
  122. * {@link Model.findOne} for a full explanation of options
  123. *
  124. * @returns {Promise<Model>}
  125. */
  126. get(instances, options) {
  127. const where = {};
  128. let Target = this.target;
  129. let instance;
  130.  
  131. options = Utils.cloneDeep(options);
  132.  
  133. if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
  134. if (!options.scope) {
  135. Target = Target.unscoped();
  136. } else {
  137. Target = Target.scope(options.scope);
  138. }
  139. }
  140.  
  141. if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
  142. Target = Target.schema(options.schema, options.schemaDelimiter);
  143. }
  144.  
  145. if (!Array.isArray(instances)) {
  146. instance = instances;
  147. instances = undefined;
  148. }
  149.  
  150. if (instances) {
  151. where[this.targetKey] = {
  152. [Op.in]: instances.map(instance => instance.get(this.foreignKey))
  153. };
  154. } else {
  155. if (this.targetKeyIsPrimary && !options.where) {
  156. return Target.findByPk(instance.get(this.foreignKey), options);
  157. }
  158. where[this.targetKey] = instance.get(this.foreignKey);
  159. options.limit = null;
  160. }
  161.  
  162. options.where = options.where ?
  163. { [Op.and]: [where, options.where] } :
  164. where;
  165.  
  166. if (instances) {
  167. return Target.findAll(options).then(results => {
  168. const result = {};
  169. for (const instance of instances) {
  170. result[instance.get(this.foreignKey, { raw: true })] = null;
  171. }
  172.  
  173. for (const instance of results) {
  174. result[instance.get(this.targetKey, { raw: true })] = instance;
  175. }
  176.  
  177. return result;
  178. });
  179. }
  180.  
  181. return Target.findOne(options);
  182. }
  183.  
  184. /**
  185. * Set the associated model.
  186. *
  187. * @param {Model} sourceInstance the source instance
  188. * @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.
  189. * @param {Object} [options={}] options passed to `this.save`
  190. * @param {boolean} [options.save=true] Skip saving this after setting the foreign key if false.
  191. *
  192. * @returns {Promise}
  193. */
  194. set(sourceInstance, associatedInstance, options = {}) {
  195. let value = associatedInstance;
  196.  
  197. if (associatedInstance instanceof this.target) {
  198. value = associatedInstance[this.targetKey];
  199. }
  200.  
  201. sourceInstance.set(this.foreignKey, value);
  202.  
  203. if (options.save === false) return;
  204.  
  205. options = Object.assign({
  206. fields: [this.foreignKey],
  207. allowNull: [this.foreignKey],
  208. association: true
  209. }, options);
  210.  
  211. // passes the changed field to save, so only that field get updated.
  212. return sourceInstance.save(options);
  213. }
  214.  
  215. /**
  216. * Create a new instance of the associated model and associate it with this.
  217. *
  218. * @param {Model} sourceInstance the source instance
  219. * @param {Object} [values={}] values to create associated model instance with
  220. * @param {Object} [options={}] Options passed to `target.create` and setAssociation.
  221. *
  222. * @see
  223. * {@link Model#create} for a full explanation of options
  224. *
  225. * @returns {Promise<Model>} The created target model
  226. */
  227. create(sourceInstance, values, options) {
  228. values = values || {};
  229. options = options || {};
  230.  
  231. return this.target.create(values, options)
  232. .then(newAssociatedObject => sourceInstance[this.accessors.set](newAssociatedObject, options)
  233. .then(() => newAssociatedObject)
  234. );
  235. }
  236.  
  237. verifyAssociationAlias(alias) {
  238. if (typeof alias === 'string') {
  239. return this.as === alias;
  240. }
  241.  
  242. if (alias && alias.singular) {
  243. return this.as === alias.singular;
  244. }
  245.  
  246. return !this.isAliased;
  247. }
  248. }
  249.  
  250. module.exports = BelongsTo;
  251. module.exports.BelongsTo = BelongsTo;
  252. module.exports.default = BelongsTo;