belongs-to-many.test.ts 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380
  1. import type {
  2. BelongsToGetAssociationMixin,
  3. BelongsToManyAssociation,
  4. BelongsToManySetAssociationsMixin,
  5. CreationOptional,
  6. InferAttributes,
  7. InferCreationAttributes,
  8. ModelStatic,
  9. } from '@sequelize/core';
  10. import {
  11. AssociationError,
  12. BelongsToAssociation,
  13. DataTypes,
  14. HasManyAssociation,
  15. HasOneAssociation,
  16. Model,
  17. } from '@sequelize/core';
  18. import { expect } from 'chai';
  19. import each from 'lodash/each';
  20. import type { SinonStub } from 'sinon';
  21. import sinon from 'sinon';
  22. import {
  23. beforeEach2,
  24. createSequelizeInstance,
  25. getTestDialectTeaser,
  26. resetSequelizeInstance,
  27. sequelize,
  28. } from '../../support';
  29. describe(getTestDialectTeaser('belongsToMany'), () => {
  30. beforeEach(() => {
  31. resetSequelizeInstance();
  32. });
  33. it('throws when invalid model is passed', () => {
  34. const User = sequelize.define('User');
  35. expect(() => {
  36. // @ts-expect-error -- testing that invalid input results in error
  37. User.belongsToMany();
  38. }).to.throw(
  39. `User.belongsToMany was called with undefined as the target model, but it is not a subclass of Sequelize's Model class`,
  40. );
  41. });
  42. it('creates the join table when through is a string', async () => {
  43. const User = sequelize.define('User');
  44. const Group = sequelize.define('Group');
  45. User.belongsToMany(Group, { as: 'MyGroups', through: 'GroupUser' });
  46. Group.belongsToMany(User, { as: 'MyUsers', through: 'GroupUser' });
  47. expect(sequelize.models.getOrThrow('GroupUser')).to.exist;
  48. });
  49. it('should not inherit scopes from parent to join table', () => {
  50. const A = sequelize.define('a');
  51. const B = sequelize.define(
  52. 'b',
  53. {},
  54. {
  55. defaultScope: {
  56. where: {
  57. foo: 'bar',
  58. },
  59. },
  60. scopes: {
  61. baz: {
  62. where: {
  63. fooz: 'zab',
  64. },
  65. },
  66. },
  67. },
  68. );
  69. B.belongsToMany(A, { through: 'AB' });
  70. const AB = sequelize.models.getOrThrow('AB');
  71. expect(AB.options.defaultScope).to.deep.equal({});
  72. expect(AB.options.scopes).to.deep.equal({});
  73. });
  74. it('should not inherit validations from parent to join table', () => {
  75. const A = sequelize.define('a');
  76. const B = sequelize.define(
  77. 'b',
  78. {},
  79. {
  80. validate: {
  81. validateModel() {
  82. return true;
  83. },
  84. },
  85. },
  86. );
  87. B.belongsToMany(A, { through: 'AB' });
  88. const AB = sequelize.models.getOrThrow('AB');
  89. expect(AB.options.validate).to.deep.equal({});
  90. });
  91. it('should not override custom methods with association mixin', () => {
  92. const methods = {
  93. getTasks: 'get',
  94. countTasks: 'count',
  95. hasTask: 'has',
  96. hasTasks: 'has',
  97. setTasks: 'set',
  98. addTask: 'add',
  99. addTasks: 'add',
  100. removeTask: 'remove',
  101. removeTasks: 'remove',
  102. createTask: 'create',
  103. };
  104. const User = sequelize.define('User');
  105. const Task = sequelize.define('Task');
  106. function originalMethod() {}
  107. each(methods, (alias, method) => {
  108. // TODO: remove this eslint-disable once we drop support for TypeScript <= 5.3
  109. // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
  110. // @ts-ignore -- This only became invalid starting with TS 5.4
  111. User.prototype[method] = originalMethod;
  112. });
  113. User.belongsToMany(Task, { through: 'UserTasks', as: 'task' });
  114. const user = User.build();
  115. each(methods, (alias, method) => {
  116. // @ts-expect-error -- dynamic type, not worth typing
  117. expect(user[method]).to.eq(originalMethod);
  118. });
  119. });
  120. it('allows customizing the inverse association name (long form)', () => {
  121. const User = sequelize.define('User');
  122. const Task = sequelize.define('Task');
  123. User.belongsToMany(Task, { through: 'UserTask', as: 'tasks', inverse: { as: 'users' } });
  124. expect(Task.associations.users).to.be.ok;
  125. expect(User.associations.tasks).to.be.ok;
  126. });
  127. it('allows customizing the inverse association name (shorthand)', () => {
  128. const User = sequelize.define('User');
  129. const Task = sequelize.define('Task');
  130. User.belongsToMany(Task, { through: 'UserTask', as: 'tasks', inverse: 'users' });
  131. expect(Task.associations.users).to.be.ok;
  132. expect(User.associations.tasks).to.be.ok;
  133. });
  134. it('allows defining two associations with the same through, but with a different scope on the through table', () => {
  135. const User = sequelize.define('User');
  136. const Post = sequelize.define('Post', { editing: DataTypes.BOOLEAN });
  137. User.belongsToMany(Post, { through: 'UserPost' });
  138. Post.belongsToMany(User, { through: 'UserPost' });
  139. User.belongsToMany(Post, {
  140. as: 'editingPosts',
  141. inverse: {
  142. as: 'editingUsers',
  143. },
  144. through: {
  145. model: 'UserPost',
  146. scope: {
  147. editing: true,
  148. },
  149. },
  150. });
  151. });
  152. it('allows defining two associations with the same inverse association', () => {
  153. const User = sequelize.define('User');
  154. const Post = sequelize.define('Post');
  155. const Association1 = Post.belongsToMany(User, {
  156. through: { model: 'UserPost' },
  157. as: 'categories',
  158. scope: { type: 'category' },
  159. });
  160. const Association2 = Post.belongsToMany(User, {
  161. through: { model: 'UserPost' },
  162. as: 'tags',
  163. scope: { type: 'tag' },
  164. });
  165. // This means Association1.pairedWith.pairedWith is not always Association1
  166. // This may be an issue
  167. expect(Association1.pairedWith).to.eq(Association2.pairedWith);
  168. });
  169. it('lets you customize the name of the intermediate associations', () => {
  170. const User = sequelize.define('User');
  171. const Group = sequelize.define('Group');
  172. const GroupUser = sequelize.define('GroupUser');
  173. User.belongsToMany(Group, {
  174. through: GroupUser,
  175. as: 'groups',
  176. throughAssociations: {
  177. toSource: 'toSource',
  178. toTarget: 'toTarget',
  179. fromSource: 'fromSources',
  180. fromTarget: 'fromTargets',
  181. },
  182. inverse: {
  183. as: 'members',
  184. },
  185. });
  186. expect(Object.keys(User.associations).sort()).to.deep.eq([
  187. 'fromSource',
  188. 'fromSources',
  189. 'groups',
  190. ]);
  191. expect(Object.keys(Group.associations).sort()).to.deep.eq([
  192. 'fromTarget',
  193. 'fromTargets',
  194. 'members',
  195. ]);
  196. expect(Object.keys(GroupUser.associations).sort()).to.deep.eq(['toSource', 'toTarget']);
  197. });
  198. it('errors when trying to define similar associations with incompatible inverse associations', () => {
  199. const User = sequelize.define('User');
  200. const Post = sequelize.define('Post');
  201. Post.belongsToMany(User, {
  202. through: { model: 'UserPost' },
  203. as: 'categories',
  204. scope: { type: 'category' },
  205. });
  206. expect(() => {
  207. Post.belongsToMany(User, {
  208. through: { model: 'UserPost' },
  209. as: 'tags',
  210. scope: { type: 'tag' },
  211. otherKey: {
  212. onUpdate: 'NO ACTION',
  213. },
  214. });
  215. }).to.throw('Defining BelongsToMany association "tags" from Post to User failed');
  216. });
  217. it('errors when trying to define the same association', () => {
  218. const User = sequelize.define('User');
  219. const Post = sequelize.define('Post');
  220. Post.belongsToMany(User, {
  221. through: { model: 'UserPost' },
  222. });
  223. expect(() => {
  224. Post.belongsToMany(User, { through: { model: 'UserPost' } });
  225. }).to.throw(
  226. 'You have defined two associations with the same name "users" on the model "Post". Use another alias using the "as" parameter',
  227. );
  228. });
  229. it('generates a default association name', () => {
  230. const User = sequelize.define('User', {});
  231. const Task = sequelize.define('Task', {});
  232. User.belongsToMany(Task, { through: 'UserTask' });
  233. expect(Object.keys(Task.associations)).to.deep.eq(['users', 'usersTasks', 'userTask']);
  234. expect(Object.keys(User.associations)).to.deep.eq(['tasks', 'tasksUsers', 'taskUser']);
  235. });
  236. describe('proper syntax', () => {
  237. it('throws an AssociationError if the through option is undefined, true, or null', () => {
  238. const User = sequelize.define('User', {});
  239. const Task = sequelize.define('Task', {});
  240. // @ts-expect-error -- we're testing that these do throw
  241. const errorFunction1 = () => User.belongsToMany(Task, { through: true });
  242. // @ts-expect-error -- see above
  243. const errorFunction2 = () => User.belongsToMany(Task, { through: undefined });
  244. // @ts-expect-error -- see above
  245. const errorFunction3 = () => User.belongsToMany(Task, { through: null });
  246. for (const errorFunction of [errorFunction1, errorFunction2, errorFunction3]) {
  247. expect(errorFunction).to.throwWithCause(
  248. AssociationError,
  249. `${User.name}.belongsToMany(${Task.name}) requires a through model, set the "through", or "through.model" options to either a string or a model`,
  250. );
  251. }
  252. });
  253. it('throws an AssociationError for a self-association defined without an alias', () => {
  254. const User = sequelize.define('User', {});
  255. const errorFunction = User.belongsToMany.bind(User, User, { through: 'jointable' });
  256. expect(errorFunction).to.throwWithCause(
  257. AssociationError,
  258. 'Both options "as" and "inverse.as" must be defined for belongsToMany self-associations, and their value must be different.',
  259. );
  260. });
  261. });
  262. describe('timestamps', () => {
  263. it('follows the global timestamps true option', () => {
  264. const User = sequelize.define('User', {});
  265. const Task = sequelize.define('Task', {});
  266. User.belongsToMany(Task, { through: 'user_task1' });
  267. expect(sequelize.models.getOrThrow('user_task1').getAttributes()).to.contain.all.keys([
  268. 'createdAt',
  269. 'updatedAt',
  270. ]);
  271. });
  272. it('allows me to override the global timestamps option', () => {
  273. const User = sequelize.define('User', {});
  274. const Task = sequelize.define('Task', {});
  275. User.belongsToMany(Task, { through: { model: 'user_task2', timestamps: false } });
  276. expect(sequelize.models.getOrThrow('user_task2').getAttributes()).not.to.contain.any.keys([
  277. 'createdAt',
  278. 'updatedAt',
  279. ]);
  280. });
  281. it('follows the global timestamps false option', () => {
  282. const sequelize2 = createSequelizeInstance({
  283. define: {
  284. timestamps: false,
  285. },
  286. });
  287. const User = sequelize2.define('User', {});
  288. const Task = sequelize2.define('Task', {});
  289. User.belongsToMany(Task, { through: 'user_task3' });
  290. expect(sequelize2.models.getOrThrow('user_task3').getAttributes()).not.to.contain.any.keys([
  291. 'createdAt',
  292. 'updatedAt',
  293. ]);
  294. });
  295. });
  296. describe('optimizations using bulk create, destroy and update', () => {
  297. function getEntities() {
  298. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  299. declare id: CreationOptional<number>;
  300. declare username: string | null;
  301. declare setTasks: BelongsToManySetAssociationsMixin<Task, number>;
  302. }
  303. User.init(
  304. {
  305. id: {
  306. type: DataTypes.INTEGER,
  307. primaryKey: true,
  308. autoIncrement: true,
  309. },
  310. username: DataTypes.STRING,
  311. },
  312. { sequelize },
  313. );
  314. class Task extends Model<InferAttributes<Task>> {
  315. declare id: CreationOptional<number>;
  316. declare title: string | null;
  317. }
  318. Task.init(
  319. {
  320. id: {
  321. type: DataTypes.INTEGER,
  322. primaryKey: true,
  323. autoIncrement: true,
  324. },
  325. title: DataTypes.STRING,
  326. },
  327. { sequelize },
  328. );
  329. const UserTasks = sequelize.define('UserTasks', {});
  330. User.belongsToMany(Task, { through: UserTasks });
  331. Task.belongsToMany(User, { through: UserTasks });
  332. const user = User.build({
  333. id: 42,
  334. });
  335. const task1 = Task.build({
  336. id: 15,
  337. });
  338. const task2 = Task.build({
  339. id: 16,
  340. });
  341. sinon.stub(UserTasks, 'findAll').resolves([]);
  342. sinon.stub(UserTasks, 'bulkCreate').resolves([]);
  343. sinon.stub(UserTasks, 'destroy').resolves(0);
  344. return { user, task1, task2, UserTasks };
  345. }
  346. afterEach(() => {
  347. sinon.restore();
  348. });
  349. it('uses one insert into statement', async () => {
  350. const { user, task1, task2, UserTasks } = getEntities();
  351. await user.setTasks([task1, task2]);
  352. expect(UserTasks.findAll).to.have.been.calledOnce;
  353. expect(UserTasks.bulkCreate).to.have.been.calledOnce;
  354. });
  355. it('uses one delete from statement', async () => {
  356. const { user, task1, task2, UserTasks } = getEntities();
  357. (UserTasks.findAll as SinonStub)
  358. .onFirstCall()
  359. .resolves([])
  360. .onSecondCall()
  361. .resolves([
  362. { userId: 42, taskId: 15 },
  363. { userId: 42, taskId: 16 },
  364. ]);
  365. await user.setTasks([task1, task2]);
  366. await user.setTasks([]);
  367. expect(UserTasks.findAll).to.have.been.calledTwice;
  368. expect(UserTasks.destroy).to.have.been.calledOnce;
  369. });
  370. });
  371. describe('foreign keys', () => {
  372. it('should infer otherKey from paired BTM relationship with a through string defined', () => {
  373. const User = sequelize.define('User', {});
  374. const Place = sequelize.define('Place', {});
  375. const Places = User.belongsToMany(Place, {
  376. through: 'user_places',
  377. foreignKey: 'user_id',
  378. otherKey: 'place_id',
  379. });
  380. const Users = Place.getAssociation('users') as BelongsToManyAssociation;
  381. expect(Places.pairedWith).to.equal(Users);
  382. expect(Users.pairedWith).to.equal(Places);
  383. expect(Places.foreignKey).to.equal('user_id');
  384. expect(Users.foreignKey).to.equal('place_id');
  385. expect(Places.otherKey).to.equal('place_id');
  386. expect(Users.otherKey).to.equal('user_id');
  387. });
  388. it('should infer otherKey from paired BTM relationship with a through model defined', () => {
  389. const User = sequelize.define('User', {});
  390. const Place = sequelize.define('Place', {});
  391. const UserPlace = sequelize.define(
  392. 'UserPlace',
  393. {
  394. id: {
  395. primaryKey: true,
  396. type: DataTypes.INTEGER,
  397. autoIncrement: true,
  398. },
  399. },
  400. { timestamps: false },
  401. );
  402. const Places = User.belongsToMany(Place, {
  403. through: UserPlace,
  404. foreignKey: 'user_id',
  405. otherKey: 'place_id',
  406. });
  407. const Users = Place.getAssociation('users') as BelongsToManyAssociation;
  408. expect(Places.pairedWith).to.equal(Users);
  409. expect(Users.pairedWith).to.equal(Places);
  410. expect(Places.foreignKey).to.equal('user_id');
  411. expect(Users.foreignKey).to.equal('place_id');
  412. expect(Places.otherKey).to.equal('place_id');
  413. expect(Users.otherKey).to.equal('user_id');
  414. expect(Object.keys(UserPlace.getAttributes()).length).to.equal(3); // Defined primary key and two foreign keys
  415. });
  416. it('should infer foreign keys (camelCase)', () => {
  417. const Person = sequelize.define('Person');
  418. const PersonChildren = sequelize.define('PersonChildren');
  419. const Children = Person.belongsToMany(Person, {
  420. as: 'Children',
  421. through: PersonChildren,
  422. inverse: { as: 'Parents' },
  423. });
  424. expect(Children.foreignKey).to.equal('parentId');
  425. expect(Children.otherKey).to.equal('childId');
  426. expect(PersonChildren.getAttributes()[Children.foreignKey]).to.be.ok;
  427. expect(PersonChildren.getAttributes()[Children.otherKey]).to.be.ok;
  428. });
  429. it('should infer foreign keys (snake_case)', () => {
  430. const Person = sequelize.define('Person', {}, { underscored: true });
  431. const PersonChildren = sequelize.define('PersonChildren', {}, { underscored: true });
  432. const Children = Person.belongsToMany(Person, {
  433. as: 'Children',
  434. through: PersonChildren,
  435. inverse: { as: 'Parents' },
  436. });
  437. expect(Children.foreignKey).to.equal('parentId');
  438. expect(Children.otherKey).to.equal('childId');
  439. expect(PersonChildren.getAttributes()[Children.foreignKey]).to.be.ok;
  440. expect(PersonChildren.getAttributes()[Children.otherKey]).to.be.ok;
  441. expect(PersonChildren.getAttributes()[Children.foreignKey].columnName).to.equal('parent_id');
  442. expect(PersonChildren.getAttributes()[Children.otherKey].columnName).to.equal('child_id');
  443. });
  444. it('should create non-null foreign keys by default', () => {
  445. const A = sequelize.define('A');
  446. const B = sequelize.define('B');
  447. const association = A.belongsToMany(B, { through: 'AB' });
  448. const attributes = association.throughModel.getAttributes();
  449. expect(attributes.aId.allowNull).to.be.false;
  450. expect(attributes.bId.allowNull).to.be.false;
  451. });
  452. it('allows creating nullable FKs', () => {
  453. const A = sequelize.define('A');
  454. const B = sequelize.define('B');
  455. const association = A.belongsToMany(B, {
  456. through: 'AB',
  457. foreignKey: { allowNull: true },
  458. otherKey: { allowNull: true },
  459. });
  460. const attributes = association.throughModel.getAttributes();
  461. expect(attributes.aId.allowNull).to.be.true;
  462. expect(attributes.bId.allowNull).to.be.true;
  463. });
  464. it('should add FKs with onDelete=cascade by default', () => {
  465. const A = sequelize.define('A');
  466. const B = sequelize.define('B');
  467. const association = A.belongsToMany(B, { through: 'AB', foreignKey: {} });
  468. const attributes = association.throughModel.getAttributes();
  469. expect(attributes.aId.onDelete).to.eq('CASCADE');
  470. expect(attributes.bId.onDelete).to.eq('CASCADE');
  471. });
  472. });
  473. describe('source/target keys', () => {
  474. it('should infer targetKey from paired BTM relationship with a through string defined', () => {
  475. const User = sequelize.define('User', { user_id: DataTypes.UUID });
  476. const Place = sequelize.define('Place', { place_id: DataTypes.UUID });
  477. const Places = User.belongsToMany(Place, {
  478. through: 'user_places',
  479. sourceKey: 'user_id',
  480. targetKey: 'place_id',
  481. });
  482. const Users = Place.getAssociation('users') as BelongsToManyAssociation;
  483. expect(Places.pairedWith).to.equal(Users);
  484. expect(Users.pairedWith).to.equal(Places);
  485. expect(Places.sourceKey).to.equal('user_id');
  486. expect(Users.sourceKey).to.equal('place_id');
  487. expect(Places.targetKey).to.equal('place_id');
  488. expect(Users.targetKey).to.equal('user_id');
  489. });
  490. it('should infer targetKey from paired BTM relationship with a through model defined', () => {
  491. const User = sequelize.define('User', { user_id: DataTypes.UUID });
  492. const Place = sequelize.define('Place', { place_id: DataTypes.UUID });
  493. const UserPlace = sequelize.define(
  494. 'UserPlace',
  495. {
  496. id: {
  497. primaryKey: true,
  498. type: DataTypes.INTEGER,
  499. autoIncrement: true,
  500. },
  501. },
  502. { timestamps: false },
  503. );
  504. const Places = User.belongsToMany(Place, {
  505. through: UserPlace,
  506. sourceKey: 'user_id',
  507. targetKey: 'place_id',
  508. });
  509. const Users = Place.getAssociation('users') as BelongsToManyAssociation;
  510. expect(Places.pairedWith).to.equal(Users);
  511. expect(Users.pairedWith).to.equal(Places);
  512. expect(Places.sourceKey).to.equal('user_id');
  513. expect(Users.sourceKey).to.equal('place_id', 'Users.sourceKey is invalid');
  514. expect(Places.targetKey).to.equal('place_id');
  515. expect(Users.targetKey).to.equal('user_id', 'Users.targetKey is invalid');
  516. expect(Object.keys(UserPlace.getAttributes()).length).to.equal(3); // Defined primary key and two foreign keys
  517. });
  518. });
  519. describe('pseudo associations', () => {
  520. it('should setup belongsTo relations to source and target from join model with defined foreign/other keys', () => {
  521. const Product = sequelize.define('Product', {
  522. title: DataTypes.STRING,
  523. });
  524. const Tag = sequelize.define('Tag', {
  525. name: DataTypes.STRING,
  526. });
  527. const ProductTag = sequelize.define(
  528. 'ProductTag',
  529. {
  530. id: {
  531. primaryKey: true,
  532. type: DataTypes.INTEGER,
  533. autoIncrement: true,
  534. },
  535. priority: DataTypes.INTEGER,
  536. },
  537. {
  538. timestamps: false,
  539. },
  540. );
  541. const ProductTags = Product.belongsToMany(Tag, {
  542. through: ProductTag,
  543. foreignKey: 'productId',
  544. otherKey: 'tagId',
  545. });
  546. const TagProducts = Tag.belongsToMany(Product, {
  547. through: ProductTag,
  548. foreignKey: 'tagId',
  549. otherKey: 'productId',
  550. });
  551. expect(ProductTags.fromThroughToSource).to.be.an.instanceOf(BelongsToAssociation);
  552. expect(ProductTags.fromThroughToTarget).to.be.an.instanceOf(BelongsToAssociation);
  553. expect(TagProducts.fromThroughToSource).to.be.an.instanceOf(BelongsToAssociation);
  554. expect(TagProducts.fromThroughToTarget).to.be.an.instanceOf(BelongsToAssociation);
  555. expect(ProductTags.fromThroughToSource.foreignKey).to.equal(ProductTags.foreignKey);
  556. expect(ProductTags.fromThroughToTarget.foreignKey).to.equal(ProductTags.otherKey);
  557. expect(TagProducts.fromThroughToSource.foreignKey).to.equal(TagProducts.foreignKey);
  558. expect(TagProducts.fromThroughToTarget.foreignKey).to.equal(TagProducts.otherKey);
  559. expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4);
  560. expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(
  561. ['id', 'priority', 'productId', 'tagId'].sort(),
  562. );
  563. });
  564. it('should setup hasMany relations to source and target from join model with defined foreign/other keys', () => {
  565. const Product = sequelize.define('Product', {
  566. title: DataTypes.STRING,
  567. });
  568. const Tag = sequelize.define('Tag', {
  569. name: DataTypes.STRING,
  570. });
  571. const ProductTag = sequelize.define(
  572. 'ProductTag',
  573. {
  574. id: {
  575. primaryKey: true,
  576. type: DataTypes.INTEGER,
  577. autoIncrement: true,
  578. },
  579. priority: DataTypes.INTEGER,
  580. },
  581. {
  582. timestamps: false,
  583. },
  584. );
  585. const ProductTags = Product.belongsToMany(Tag, {
  586. through: ProductTag,
  587. foreignKey: 'productId',
  588. otherKey: 'tagId',
  589. });
  590. const TagProducts = Tag.belongsToMany(Product, {
  591. through: ProductTag,
  592. foreignKey: 'tagId',
  593. otherKey: 'productId',
  594. });
  595. expect(ProductTags.fromSourceToThrough).to.be.an.instanceOf(HasManyAssociation);
  596. expect(ProductTags.fromTargetToThrough).to.be.an.instanceOf(HasManyAssociation);
  597. expect(TagProducts.fromSourceToThrough).to.be.an.instanceOf(HasManyAssociation);
  598. expect(TagProducts.fromTargetToThrough).to.be.an.instanceOf(HasManyAssociation);
  599. expect(ProductTags.fromSourceToThrough.foreignKey).to.equal(ProductTags.foreignKey);
  600. expect(ProductTags.fromTargetToThrough.foreignKey).to.equal(ProductTags.otherKey);
  601. expect(TagProducts.fromSourceToThrough.foreignKey).to.equal(TagProducts.foreignKey);
  602. expect(TagProducts.fromTargetToThrough.foreignKey).to.equal(TagProducts.otherKey);
  603. expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4);
  604. expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(
  605. ['id', 'priority', 'tagId', 'productId'].sort(),
  606. );
  607. });
  608. it('should setup hasOne relations to source and target from join model with defined foreign/other keys', () => {
  609. const Product = sequelize.define('Product', {
  610. title: DataTypes.STRING,
  611. });
  612. const Tag = sequelize.define('Tag', {
  613. name: DataTypes.STRING,
  614. });
  615. const ProductTag = sequelize.define(
  616. 'ProductTag',
  617. {
  618. id: {
  619. primaryKey: true,
  620. type: DataTypes.INTEGER,
  621. autoIncrement: true,
  622. },
  623. priority: DataTypes.INTEGER,
  624. },
  625. {
  626. timestamps: false,
  627. },
  628. );
  629. const ProductTags = Product.belongsToMany(Tag, {
  630. through: ProductTag,
  631. foreignKey: 'productId',
  632. otherKey: 'tagId',
  633. });
  634. const TagProducts = Tag.belongsToMany(Product, {
  635. through: ProductTag,
  636. foreignKey: 'tagId',
  637. otherKey: 'productId',
  638. });
  639. expect(ProductTags.fromSourceToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  640. expect(ProductTags.fromTargetToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  641. expect(TagProducts.fromSourceToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  642. expect(TagProducts.fromTargetToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  643. expect(ProductTags.fromSourceToThroughOne.foreignKey).to.equal(ProductTags.foreignKey);
  644. expect(ProductTags.fromTargetToThroughOne.foreignKey).to.equal(ProductTags.otherKey);
  645. expect(TagProducts.fromSourceToThroughOne.foreignKey).to.equal(TagProducts.foreignKey);
  646. expect(TagProducts.fromTargetToThroughOne.foreignKey).to.equal(TagProducts.otherKey);
  647. expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4);
  648. expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(
  649. ['id', 'priority', 'productId', 'tagId'].sort(),
  650. );
  651. });
  652. it('should setup hasOne relations to source and target from join model with defined source keys', () => {
  653. const Product = sequelize.define('Product', {
  654. title: DataTypes.STRING,
  655. productSecondaryId: DataTypes.STRING,
  656. });
  657. const Tag = sequelize.define('Tag', {
  658. name: DataTypes.STRING,
  659. tagSecondaryId: DataTypes.STRING,
  660. });
  661. const ProductTag = sequelize.define(
  662. 'ProductTag',
  663. {
  664. id: {
  665. primaryKey: true,
  666. type: DataTypes.INTEGER,
  667. autoIncrement: true,
  668. },
  669. priority: DataTypes.INTEGER,
  670. },
  671. {
  672. timestamps: false,
  673. },
  674. );
  675. const ProductTags = Product.belongsToMany(Tag, {
  676. through: ProductTag,
  677. sourceKey: 'productSecondaryId',
  678. targetKey: 'tagSecondaryId',
  679. });
  680. const TagProducts = Tag.getAssociation('products') as BelongsToManyAssociation;
  681. expect(ProductTags.foreignKey).to.equal(
  682. 'productProductSecondaryId',
  683. 'generated foreign key for source name (product) + source key (productSecondaryId) should result in productProductSecondaryId',
  684. );
  685. expect(TagProducts.foreignKey).to.equal('tagTagSecondaryId');
  686. expect(ProductTags.fromSourceToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  687. expect(ProductTags.fromTargetToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  688. expect(TagProducts.fromSourceToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  689. expect(TagProducts.fromTargetToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  690. expect(TagProducts.fromSourceToThroughOne.sourceKey).to.equal(TagProducts.sourceKey);
  691. expect(TagProducts.fromTargetToThroughOne.sourceKey).to.equal(TagProducts.targetKey);
  692. expect(ProductTags.fromSourceToThroughOne.sourceKey).to.equal(ProductTags.sourceKey);
  693. expect(ProductTags.fromTargetToThroughOne.sourceKey).to.equal(ProductTags.targetKey);
  694. expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4);
  695. expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(
  696. ['id', 'priority', 'productProductSecondaryId', 'tagTagSecondaryId'].sort(),
  697. );
  698. });
  699. it('should setup belongsTo relations to source and target from join model with only foreign keys defined', () => {
  700. const Product = sequelize.define('Product', {
  701. title: DataTypes.STRING,
  702. });
  703. const Tag = sequelize.define('Tag', {
  704. name: DataTypes.STRING,
  705. });
  706. const ProductTag = sequelize.define(
  707. 'ProductTag',
  708. {
  709. id: {
  710. primaryKey: true,
  711. type: DataTypes.INTEGER,
  712. autoIncrement: true,
  713. },
  714. priority: DataTypes.INTEGER,
  715. },
  716. {
  717. timestamps: false,
  718. },
  719. );
  720. const ProductTags = Product.belongsToMany(Tag, {
  721. through: ProductTag,
  722. foreignKey: 'product_ID',
  723. otherKey: 'tag_ID',
  724. });
  725. const TagProducts = Tag.getAssociation('products') as BelongsToManyAssociation;
  726. expect(ProductTags.fromThroughToSource).to.be.ok;
  727. expect(ProductTags.fromThroughToTarget).to.be.ok;
  728. expect(TagProducts.fromThroughToSource).to.be.ok;
  729. expect(TagProducts.fromThroughToTarget).to.be.ok;
  730. expect(ProductTags.fromThroughToSource.foreignKey).to.equal(ProductTags.foreignKey);
  731. expect(ProductTags.fromThroughToTarget.foreignKey).to.equal(ProductTags.otherKey);
  732. expect(TagProducts.fromThroughToSource.foreignKey).to.equal(TagProducts.foreignKey);
  733. expect(TagProducts.fromThroughToTarget.foreignKey).to.equal(TagProducts.otherKey);
  734. expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4);
  735. expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(
  736. ['id', 'priority', 'product_ID', 'tag_ID'].sort(),
  737. );
  738. });
  739. it('should setup hasOne relations to source and target from join model with only foreign keys defined', () => {
  740. const Product = sequelize.define('Product', {
  741. title: DataTypes.STRING,
  742. });
  743. const Tag = sequelize.define('Tag', {
  744. name: DataTypes.STRING,
  745. });
  746. const ProductTag = sequelize.define(
  747. 'ProductTag',
  748. {
  749. id: {
  750. primaryKey: true,
  751. type: DataTypes.INTEGER,
  752. autoIncrement: true,
  753. },
  754. priority: DataTypes.INTEGER,
  755. },
  756. {
  757. timestamps: false,
  758. },
  759. );
  760. const ProductTags = Product.belongsToMany(Tag, {
  761. through: ProductTag,
  762. foreignKey: 'product_ID',
  763. otherKey: 'tag_ID',
  764. });
  765. const TagProducts = Tag.getAssociation('products') as BelongsToManyAssociation;
  766. expect(ProductTags.fromSourceToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  767. expect(ProductTags.fromTargetToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  768. expect(TagProducts.fromSourceToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  769. expect(TagProducts.fromTargetToThroughOne).to.be.an.instanceOf(HasOneAssociation);
  770. expect(ProductTags.fromSourceToThroughOne.foreignKey).to.equal(ProductTags.foreignKey);
  771. expect(ProductTags.fromTargetToThroughOne.foreignKey).to.equal(ProductTags.otherKey);
  772. expect(TagProducts.fromSourceToThroughOne.foreignKey).to.equal(TagProducts.foreignKey);
  773. expect(TagProducts.fromTargetToThroughOne.foreignKey).to.equal(TagProducts.otherKey);
  774. expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4);
  775. expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(
  776. ['id', 'priority', 'product_ID', 'tag_ID'].sort(),
  777. );
  778. });
  779. it('should setup belongsTo relations to source and target from join model with no foreign keys defined', () => {
  780. const Product = sequelize.define('Product', {
  781. title: DataTypes.STRING,
  782. });
  783. const Tag = sequelize.define('Tag', {
  784. name: DataTypes.STRING,
  785. });
  786. const ProductTag = sequelize.define(
  787. 'ProductTag',
  788. {
  789. id: {
  790. primaryKey: true,
  791. type: DataTypes.INTEGER,
  792. autoIncrement: true,
  793. },
  794. priority: DataTypes.INTEGER,
  795. },
  796. {
  797. timestamps: false,
  798. },
  799. );
  800. const ProductTags = Product.belongsToMany(Tag, { through: ProductTag });
  801. const TagProducts = Tag.belongsToMany(Product, { through: ProductTag });
  802. expect(ProductTags.fromThroughToSource).to.be.ok;
  803. expect(ProductTags.fromThroughToTarget).to.be.ok;
  804. expect(TagProducts.fromThroughToSource).to.be.ok;
  805. expect(TagProducts.fromThroughToTarget).to.be.ok;
  806. expect(ProductTags.fromThroughToSource.foreignKey).to.equal(ProductTags.foreignKey);
  807. expect(ProductTags.fromThroughToTarget.foreignKey).to.equal(ProductTags.otherKey);
  808. expect(TagProducts.fromThroughToSource.foreignKey).to.equal(TagProducts.foreignKey);
  809. expect(TagProducts.fromThroughToTarget.foreignKey).to.equal(TagProducts.otherKey);
  810. expect(Object.keys(ProductTag.getAttributes()).length).to.equal(4);
  811. expect(Object.keys(ProductTag.getAttributes()).sort()).to.deep.equal(
  812. ['id', 'priority', 'productId', 'tagId'].sort(),
  813. );
  814. });
  815. });
  816. describe('associations on the join table', () => {
  817. const vars = beforeEach2(() => {
  818. class User extends Model {}
  819. class Project extends Model {}
  820. class UserProject extends Model {
  821. declare getUser: BelongsToGetAssociationMixin<User>;
  822. declare getProject: BelongsToGetAssociationMixin<Project>;
  823. }
  824. sequelize.addModels([User, Project, UserProject]);
  825. User.belongsToMany(Project, { through: UserProject });
  826. Project.belongsToMany(User, { through: UserProject });
  827. return { User, Project, UserProject };
  828. });
  829. it('should work for belongsTo associations defined before belongsToMany', () => {
  830. expect(vars.UserProject.prototype.getUser).to.be.ok;
  831. });
  832. it('should work for belongsTo associations defined after belongsToMany', () => {
  833. expect(vars.UserProject.prototype.getProject).to.be.ok;
  834. });
  835. });
  836. describe('self-associations', () => {
  837. it('does not pair multiple self associations with different through arguments', () => {
  838. const User = sequelize.define('user', {});
  839. const UserFollower = sequelize.define('userFollowers', {});
  840. const Invite = sequelize.define('invite', {});
  841. const UserFollowers = User.belongsToMany(User, {
  842. as: 'Followers',
  843. inverse: {
  844. as: 'Followings',
  845. },
  846. through: UserFollower,
  847. });
  848. const UserInvites = User.belongsToMany(User, {
  849. as: 'Invites',
  850. inverse: {
  851. as: 'Inviters',
  852. },
  853. foreignKey: 'InviteeId',
  854. through: Invite,
  855. });
  856. expect(UserFollowers.pairedWith).not.to.eq(UserInvites);
  857. expect(UserInvites.pairedWith).not.to.be.eq(UserFollowers);
  858. expect(UserFollowers.otherKey).not.to.equal(UserInvites.foreignKey);
  859. });
  860. it('correctly generates a foreign/other key when none are defined', () => {
  861. const User = sequelize.define('user', {});
  862. const UserFollower = sequelize.define(
  863. 'userFollowers',
  864. {
  865. id: {
  866. type: DataTypes.INTEGER,
  867. primaryKey: true,
  868. autoIncrement: true,
  869. },
  870. },
  871. {
  872. timestamps: false,
  873. },
  874. );
  875. const UserFollowers = User.belongsToMany(User, {
  876. as: 'Followers',
  877. inverse: {
  878. as: 'Followings',
  879. },
  880. through: UserFollower,
  881. });
  882. expect(UserFollowers.foreignKey).to.eq('followingId');
  883. expect(UserFollowers.otherKey).to.eq('followerId');
  884. expect(Object.keys(UserFollower.getAttributes()).length).to.equal(3);
  885. });
  886. it('works with singular and plural name for self-associations', () => {
  887. // Models taken from https://github.com/sequelize/sequelize/issues/3796
  888. const Service = sequelize.define('service', {});
  889. Service.belongsToMany(Service, {
  890. through: 'Supplements',
  891. as: 'supplements',
  892. inverse: {
  893. as: { singular: 'supplemented', plural: 'supplemented' },
  894. },
  895. });
  896. expect(Service.prototype).to.have.ownProperty('getSupplements').to.be.a('function');
  897. expect(Service.prototype).to.have.ownProperty('addSupplement').to.be.a('function');
  898. expect(Service.prototype).to.have.ownProperty('addSupplements').to.be.a('function');
  899. expect(Service.prototype).to.have.ownProperty('getSupplemented').to.be.a('function');
  900. expect(Service.prototype).not.to.have.ownProperty('getSupplementeds').to.be.a('function');
  901. expect(Service.prototype).to.have.ownProperty('addSupplemented').to.be.a('function');
  902. expect(Service.prototype).not.to.have.ownProperty('addSupplementeds').to.be.a('function');
  903. });
  904. });
  905. describe('constraints', () => {
  906. it('work properly when through is a string', () => {
  907. const User = sequelize.define('User', {});
  908. const Group = sequelize.define('Group', {});
  909. User.belongsToMany(Group, {
  910. as: 'MyGroups',
  911. through: 'group_user',
  912. foreignKey: {
  913. onUpdate: 'RESTRICT',
  914. onDelete: 'SET NULL',
  915. },
  916. otherKey: {
  917. onUpdate: 'SET NULL',
  918. onDelete: 'RESTRICT',
  919. },
  920. inverse: {
  921. as: 'MyUsers',
  922. },
  923. });
  924. const MyUsers = Group.associations.MyUsers as BelongsToManyAssociation;
  925. const MyGroups = User.associations.MyGroups as BelongsToManyAssociation;
  926. const throughModel = MyUsers.through.model;
  927. expect(Object.keys(throughModel.getAttributes()).sort()).to.deep.equal(
  928. ['userId', 'groupId', 'createdAt', 'updatedAt'].sort(),
  929. );
  930. expect(throughModel === MyGroups.through.model);
  931. expect(throughModel.getAttributes().userId.onUpdate).to.equal('RESTRICT');
  932. expect(throughModel.getAttributes().userId.onDelete).to.equal('SET NULL');
  933. expect(throughModel.getAttributes().groupId.onUpdate).to.equal('SET NULL');
  934. expect(throughModel.getAttributes().groupId.onDelete).to.equal('RESTRICT');
  935. });
  936. it('work properly when through is a model', () => {
  937. const User = sequelize.define('User', {});
  938. const Group = sequelize.define('Group', {});
  939. const UserGroup = sequelize.define('GroupUser', {}, { tableName: 'user_groups' });
  940. User.belongsToMany(Group, {
  941. as: 'MyGroups',
  942. through: UserGroup,
  943. foreignKey: {
  944. onUpdate: 'RESTRICT',
  945. onDelete: 'SET NULL',
  946. },
  947. otherKey: {
  948. onUpdate: 'SET NULL',
  949. onDelete: 'RESTRICT',
  950. },
  951. inverse: {
  952. as: 'MyUsers',
  953. },
  954. });
  955. const MyUsers = Group.associations.MyUsers as BelongsToManyAssociation;
  956. const MyGroups = User.associations.MyGroups as BelongsToManyAssociation;
  957. expect(MyUsers.through.model === MyGroups.through.model);
  958. const Through = MyUsers.through.model;
  959. expect(Object.keys(Through.getAttributes()).sort()).to.deep.equal(
  960. ['userId', 'groupId', 'createdAt', 'updatedAt'].sort(),
  961. );
  962. expect(Through.getAttributes().userId.onUpdate).to.equal(
  963. 'RESTRICT',
  964. 'UserId.onUpdate should have been RESTRICT',
  965. );
  966. expect(Through.getAttributes().userId.onDelete).to.equal(
  967. 'SET NULL',
  968. 'UserId.onDelete should have been SET NULL',
  969. );
  970. expect(Through.getAttributes().groupId.onUpdate).to.equal(
  971. 'SET NULL',
  972. 'GroupId.OnUpdate should have been SET NULL',
  973. );
  974. expect(Through.getAttributes().groupId.onDelete).to.equal(
  975. 'RESTRICT',
  976. 'GroupId.onDelete should have been RESTRICT',
  977. );
  978. });
  979. it('makes the foreign keys primary keys', () => {
  980. const User = sequelize.define('User', {});
  981. const Group = sequelize.define('Group', {});
  982. const association = User.belongsToMany(Group, {
  983. as: 'MyGroups',
  984. through: 'GroupUser',
  985. inverse: {
  986. as: 'MyUsers',
  987. },
  988. });
  989. const Through = association.throughModel;
  990. expect(Object.keys(Through.getAttributes()).sort()).to.deep.equal(
  991. ['createdAt', 'updatedAt', 'groupId', 'userId'].sort(),
  992. );
  993. expect(Through.getAttributes().userId.primaryKey).to.be.true;
  994. expect(Through.getAttributes().groupId.primaryKey).to.be.true;
  995. // @ts-expect-error -- this property does not exist after normalization
  996. expect(Through.getAttributes().userId.unique).to.be.undefined;
  997. // @ts-expect-error -- this property does not exist after normalization
  998. expect(Through.getAttributes().groupId.unique).to.be.undefined;
  999. });
  1000. it('generates unique identifier with very long length', () => {
  1001. const User = sequelize.define('User', {}, { tableName: 'table_user_with_very_long_name' });
  1002. const Group = sequelize.define('Group', {}, { tableName: 'table_group_with_very_long_name' });
  1003. const UserGroup = sequelize.define(
  1004. 'GroupUser',
  1005. {
  1006. id: {
  1007. type: DataTypes.INTEGER,
  1008. primaryKey: true,
  1009. },
  1010. id_user_very_long_field: {
  1011. type: DataTypes.INTEGER(1),
  1012. },
  1013. id_group_very_long_field: {
  1014. type: DataTypes.INTEGER(1),
  1015. },
  1016. },
  1017. { tableName: 'table_user_group_with_very_long_name' },
  1018. );
  1019. User.belongsToMany(Group, {
  1020. as: 'MyGroups',
  1021. through: UserGroup,
  1022. foreignKey: 'id_user_very_long_field',
  1023. otherKey: 'id_group_very_long_field',
  1024. inverse: {
  1025. as: 'MyUsers',
  1026. },
  1027. });
  1028. const MyUsers = Group.associations.MyUsers as BelongsToManyAssociation;
  1029. const MyGroups = User.associations.MyGroups as BelongsToManyAssociation;
  1030. const Through = MyUsers.through.model;
  1031. expect(Through === MyGroups.through.model);
  1032. expect(Object.keys(Through.getAttributes()).sort()).to.deep.equal(
  1033. [
  1034. 'id',
  1035. 'createdAt',
  1036. 'updatedAt',
  1037. 'id_user_very_long_field',
  1038. 'id_group_very_long_field',
  1039. ].sort(),
  1040. );
  1041. expect(Through.getIndexes()).to.deep.equal([
  1042. {
  1043. name: 'table_user_group_with_very_long_name_id_group_very_long_field_id_user_very_long_field_unique',
  1044. unique: true,
  1045. fields: ['id_user_very_long_field', 'id_group_very_long_field'],
  1046. column: 'id_user_very_long_field',
  1047. },
  1048. ]);
  1049. // @ts-expect-error -- this property does not exist after normalization
  1050. expect(Through.getAttributes().id_user_very_long_field.unique).to.be.undefined;
  1051. // @ts-expect-error -- this property does not exist after normalization
  1052. expect(Through.getAttributes().id_group_very_long_field.unique).to.be.undefined;
  1053. });
  1054. it('generates unique identifier with custom name', () => {
  1055. const User = sequelize.define('User', {}, { tableName: 'table_user_with_very_long_name' });
  1056. const Group = sequelize.define('Group', {}, { tableName: 'table_group_with_very_long_name' });
  1057. const UserGroup = sequelize.define(
  1058. 'GroupUser',
  1059. {
  1060. id: {
  1061. type: DataTypes.INTEGER,
  1062. primaryKey: true,
  1063. },
  1064. id_user_very_long_field: {
  1065. type: DataTypes.INTEGER(1),
  1066. },
  1067. id_group_very_long_field: {
  1068. type: DataTypes.INTEGER(1),
  1069. },
  1070. },
  1071. { tableName: 'table_user_group_with_very_long_name' },
  1072. );
  1073. User.belongsToMany(Group, {
  1074. as: 'MyGroups',
  1075. through: {
  1076. model: UserGroup,
  1077. unique: 'custom_user_group_unique',
  1078. },
  1079. foreignKey: 'id_user_very_long_field',
  1080. otherKey: 'id_group_very_long_field',
  1081. inverse: {
  1082. as: 'MyUsers',
  1083. },
  1084. });
  1085. const MyUsers = Group.associations.MyUsers as BelongsToManyAssociation;
  1086. const MyGroups = User.associations.MyGroups as BelongsToManyAssociation;
  1087. expect(MyUsers.through.model === UserGroup);
  1088. expect(MyGroups.through.model === UserGroup);
  1089. expect(UserGroup.getIndexes()).to.deep.equal([
  1090. {
  1091. name: 'custom_user_group_unique',
  1092. unique: true,
  1093. fields: ['id_user_very_long_field', 'id_group_very_long_field'],
  1094. column: 'id_user_very_long_field',
  1095. },
  1096. ]);
  1097. // @ts-expect-error -- this property does not exist after normalization
  1098. expect(UserGroup.getAttributes().id_user_very_long_field.unique).to.be.undefined;
  1099. // @ts-expect-error -- this property does not exist after normalization
  1100. expect(UserGroup.getAttributes().id_group_very_long_field.unique).to.be.undefined;
  1101. });
  1102. });
  1103. describe('association hooks', () => {
  1104. let Project: ModelStatic<any>;
  1105. let Task: ModelStatic<any>;
  1106. beforeEach(() => {
  1107. Project = sequelize.define('Project', { title: DataTypes.STRING });
  1108. Task = sequelize.define('Task', { title: DataTypes.STRING });
  1109. });
  1110. describe('beforeBelongsToManyAssociate', () => {
  1111. it('should trigger', () => {
  1112. const beforeAssociate = sinon.spy();
  1113. Project.beforeAssociate(beforeAssociate);
  1114. Project.belongsToMany(Task, { through: 'projects_and_tasks', hooks: true });
  1115. const beforeAssociateArgs = beforeAssociate.getCall(0).args;
  1116. expect(beforeAssociate).to.have.been.called;
  1117. expect(beforeAssociateArgs.length).to.equal(2);
  1118. const firstArg = beforeAssociateArgs[0];
  1119. expect(Object.keys(firstArg).join(',')).to.equal('source,target,type,sequelize');
  1120. expect(firstArg.source).to.equal(Project);
  1121. expect(firstArg.target).to.equal(Task);
  1122. expect(firstArg.type.name).to.equal('BelongsToMany');
  1123. expect(firstArg.sequelize.constructor.name).to.equal('Sequelize');
  1124. });
  1125. it('should not trigger association hooks', () => {
  1126. const beforeAssociate = sinon.spy();
  1127. Project.beforeAssociate(beforeAssociate);
  1128. Project.belongsToMany(Task, { through: 'projects_and_tasks', hooks: false });
  1129. expect(beforeAssociate).to.not.have.been.called;
  1130. });
  1131. });
  1132. describe('afterBelongsToManyAssociate', () => {
  1133. it('should trigger', () => {
  1134. const afterAssociate = sinon.spy();
  1135. Project.afterAssociate(afterAssociate);
  1136. Project.belongsToMany(Task, { through: 'projects_and_tasks', hooks: true });
  1137. const afterAssociateArgs = afterAssociate.getCalls()[afterAssociate.callCount - 1].args;
  1138. expect(afterAssociate).to.have.been.called;
  1139. expect(afterAssociateArgs.length).to.equal(2);
  1140. const firstArg = afterAssociateArgs[0];
  1141. expect(Object.keys(firstArg).join(',')).to.equal(
  1142. 'source,target,type,association,sequelize',
  1143. );
  1144. expect(firstArg.source).to.equal(Project);
  1145. expect(firstArg.target).to.equal(Task);
  1146. expect(firstArg.type.name).to.equal('BelongsToMany');
  1147. expect(firstArg.association.constructor.name).to.equal('BelongsToMany');
  1148. expect(firstArg.sequelize.constructor.name).to.equal('Sequelize');
  1149. });
  1150. it('should not trigger association hooks', () => {
  1151. const afterAssociate = sinon.spy();
  1152. Project.afterAssociate(afterAssociate);
  1153. Project.belongsToMany(Task, { through: 'projects_and_tasks', hooks: false });
  1154. expect(afterAssociate).to.not.have.been.called;
  1155. });
  1156. });
  1157. });
  1158. });