belongs-to.test.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. import type { CreationOptional, ModelStatic, NonAttribute } from '@sequelize/core';
  2. import { DataTypes, Deferrable, Model } from '@sequelize/core';
  3. import { BelongsTo } from '@sequelize/core/decorators-legacy';
  4. import { expect } from 'chai';
  5. import each from 'lodash/each';
  6. import sinon from 'sinon';
  7. import { getTestDialectTeaser, sequelize } from '../../support';
  8. describe(getTestDialectTeaser('belongsTo'), () => {
  9. it('throws when invalid model is passed', () => {
  10. const User = sequelize.define('User');
  11. expect(() => {
  12. // @ts-expect-error -- testing that invalid input results in error
  13. User.belongsTo();
  14. }).to.throw(
  15. `User.belongsTo was called with undefined as the target model, but it is not a subclass of Sequelize's Model class`,
  16. );
  17. });
  18. it('warn on invalid options', () => {
  19. const User = sequelize.define('User', {});
  20. const Task = sequelize.define('Task', {});
  21. expect(() => {
  22. User.belongsTo(Task, { targetKey: 'wowow' });
  23. }).to.throwWithCause(
  24. 'Unknown attribute "wowow" passed as targetKey, define this attribute on model "Task" first',
  25. );
  26. });
  27. it('should not override custom methods with association mixin', () => {
  28. const methods = {
  29. getTask: 'get',
  30. setTask: 'set',
  31. createTask: 'create',
  32. };
  33. const User = sequelize.define('User');
  34. const Task = sequelize.define('Task');
  35. const initialMethod = function wrapper() {};
  36. each(methods, (alias, method) => {
  37. // TODO: remove this eslint-disable once we drop support for TypeScript <= 5.3
  38. // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
  39. // @ts-ignore -- This only became invalid starting with TS 5.4
  40. User.prototype[method] = initialMethod;
  41. });
  42. User.belongsTo(Task, { as: 'task' });
  43. const user = User.build();
  44. each(methods, (alias, method) => {
  45. // @ts-expect-error -- dynamic type, not worth typing
  46. expect(user[method]).to.eq(initialMethod);
  47. });
  48. });
  49. it('should throw an error if "foreignKey" and "as" result in a name clash', () => {
  50. const Person = sequelize.define('person', {});
  51. const Car = sequelize.define('car', {});
  52. expect(() => Car.belongsTo(Person, { foreignKey: 'person' })).to.throw(
  53. "Naming collision between attribute 'person' and association 'person' on model car. To remedy this, change the \"as\" options in your association definition",
  54. );
  55. });
  56. it('should throw an error if an association clashes with the name of an already defined attribute', () => {
  57. const Person = sequelize.define('person', {});
  58. const Car = sequelize.define('car', {
  59. person: DataTypes.INTEGER,
  60. });
  61. expect(() => Car.belongsTo(Person, { as: 'person' })).to.throw(
  62. "Naming collision between attribute 'person' and association 'person' on model car. To remedy this, change the \"as\" options in your association definition",
  63. );
  64. });
  65. it('generates a default association name', () => {
  66. const User = sequelize.define('User', {});
  67. const Task = sequelize.define('Task', {});
  68. Task.belongsTo(User);
  69. expect(Object.keys(Task.associations)).to.deep.eq(['user']);
  70. expect(Object.keys(User.associations)).to.deep.eq([]);
  71. });
  72. it('should add a nullable foreign key by default', () => {
  73. const BarUser = sequelize.define('user');
  74. const BarProject = sequelize.define('project');
  75. BarProject.belongsTo(BarUser, { foreignKey: 'userId' });
  76. expect(BarProject.getAttributes().userId.allowNull).to.eq(
  77. undefined,
  78. 'allowNull should be undefined',
  79. );
  80. });
  81. it('sets the foreign key default onDelete to CASCADE if allowNull: false', async () => {
  82. const Task = sequelize.define('Task', { title: DataTypes.STRING });
  83. const User = sequelize.define('User', { username: DataTypes.STRING });
  84. Task.belongsTo(User, { foreignKey: { allowNull: false } });
  85. expect(Task.getAttributes().userId.onDelete).to.eq('CASCADE');
  86. });
  87. it(`does not overwrite the 'deferrable' option set in Model.init`, () => {
  88. const A = sequelize.define('A', {
  89. bId: {
  90. type: DataTypes.INTEGER,
  91. references: {
  92. deferrable: Deferrable.INITIALLY_IMMEDIATE,
  93. },
  94. },
  95. });
  96. const B = sequelize.define('B');
  97. A.belongsTo(B);
  98. expect(A.getAttributes().bId.references?.deferrable).to.equal(Deferrable.INITIALLY_IMMEDIATE);
  99. });
  100. // See https://github.com/sequelize/sequelize/issues/15625 for more details
  101. it('should be possible to define two belongsTo associations with the same target #15625', () => {
  102. class Post extends Model {
  103. declare id: CreationOptional<number>;
  104. @BelongsTo(() => Author, {
  105. foreignKey: 'authorId',
  106. targetKey: 'id',
  107. inverse: { as: 'myBooks', type: 'hasMany' },
  108. })
  109. declare author: NonAttribute<Author>;
  110. declare authorId: number;
  111. @BelongsTo(() => Author, {
  112. foreignKey: 'coAuthorId',
  113. targetKey: 'id',
  114. inverse: { as: 'notMyBooks', type: 'hasMany' },
  115. })
  116. declare coAuthor: NonAttribute<Author>;
  117. declare coAuthorId: number;
  118. }
  119. class Author extends Model {
  120. declare id: number;
  121. }
  122. // This would previously fail because the BelongsTo association would create an hasMany association which would
  123. // then try to create a redundant belongsTo association
  124. sequelize.addModels([Post, Author]);
  125. });
  126. it(`uses the model's singular name to generate the foreign key name`, () => {
  127. const Book = sequelize.define(
  128. 'Book',
  129. {},
  130. {
  131. name: {
  132. singular: 'Singular',
  133. plural: 'Plural',
  134. },
  135. },
  136. );
  137. const Log = sequelize.define('Log', {}, {});
  138. Log.belongsTo(Book);
  139. expect(Log.getAttributes().pluralId).to.not.exist;
  140. expect(Log.getAttributes().singularId).to.exist;
  141. });
  142. describe('association hooks', () => {
  143. let Projects: ModelStatic<any>;
  144. let Tasks: ModelStatic<any>;
  145. beforeEach(() => {
  146. Projects = sequelize.define('Project', { title: DataTypes.STRING });
  147. Tasks = sequelize.define('Task', { title: DataTypes.STRING });
  148. });
  149. describe('beforeAssociate', () => {
  150. it('should trigger', () => {
  151. const beforeAssociate = sinon.spy();
  152. Projects.beforeAssociate(beforeAssociate);
  153. Projects.belongsTo(Tasks, { hooks: true });
  154. const beforeAssociateArgs = beforeAssociate.getCall(0).args;
  155. expect(beforeAssociate).to.have.been.called;
  156. expect(beforeAssociateArgs.length).to.equal(2);
  157. const firstArg = beforeAssociateArgs[0];
  158. expect(Object.keys(firstArg).join(',')).to.equal('source,target,type,sequelize');
  159. expect(firstArg.source).to.equal(Projects);
  160. expect(firstArg.target).to.equal(Tasks);
  161. expect(firstArg.type.name).to.equal('BelongsTo');
  162. expect(firstArg.sequelize.constructor.name).to.equal('Sequelize');
  163. });
  164. it('should not trigger association hooks', () => {
  165. const beforeAssociate = sinon.spy();
  166. Projects.beforeAssociate(beforeAssociate);
  167. Projects.belongsTo(Tasks, { hooks: false });
  168. expect(beforeAssociate).to.not.have.been.called;
  169. });
  170. });
  171. describe('afterAssociate', () => {
  172. it('should trigger', () => {
  173. const afterAssociate = sinon.spy();
  174. Projects.afterAssociate(afterAssociate);
  175. Projects.belongsTo(Tasks, { hooks: true });
  176. const afterAssociateArgs = afterAssociate.getCall(0).args;
  177. expect(afterAssociate).to.have.been.called;
  178. expect(afterAssociateArgs.length).to.equal(2);
  179. const firstArg = afterAssociateArgs[0];
  180. expect(Object.keys(firstArg).join(',')).to.equal(
  181. 'source,target,type,association,sequelize',
  182. );
  183. expect(firstArg.source).to.equal(Projects);
  184. expect(firstArg.target).to.equal(Tasks);
  185. expect(firstArg.type.name).to.equal('BelongsTo');
  186. expect(firstArg.association.constructor.name).to.equal('BelongsTo');
  187. expect(firstArg.sequelize.constructor.name).to.equal('Sequelize');
  188. });
  189. it('should not trigger association hooks', () => {
  190. const afterAssociate = sinon.spy();
  191. Projects.afterAssociate(afterAssociate);
  192. Projects.belongsTo(Tasks, { hooks: false });
  193. expect(afterAssociate).to.not.have.been.called;
  194. });
  195. });
  196. });
  197. });