has-one-mixins.test.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import type {
  2. CreationOptional,
  3. HasOneSetAssociationMixin,
  4. InferAttributes,
  5. InferCreationAttributes,
  6. } from '@sequelize/core';
  7. import { DataTypes, Model } from '@sequelize/core';
  8. import { AllowNull, Attribute, HasOne, NotNull } from '@sequelize/core/decorators-legacy';
  9. import { expect } from 'chai';
  10. import {
  11. beforeAll2,
  12. createMultiTransactionalTestSequelizeInstance,
  13. sequelize,
  14. setResetMode,
  15. } from '../support';
  16. const dialect = sequelize.dialect;
  17. describe('hasOne Mixins', () => {
  18. setResetMode('destroy');
  19. const vars = beforeAll2(async () => {
  20. class Article extends Model<InferAttributes<Article>, InferCreationAttributes<Article>> {
  21. declare id: CreationOptional<number>;
  22. @HasOne(() => Label, 'articleId')
  23. declare label?: Label;
  24. declare setLabel: HasOneSetAssociationMixin<Label, Label['id']>;
  25. @HasOne(() => NonNullLabel, 'articleId')
  26. declare nonNullLabel?: NonNullLabel;
  27. declare setNonNullLabel: HasOneSetAssociationMixin<NonNullLabel, NonNullLabel['id']>;
  28. }
  29. class Label extends Model<InferAttributes<Label>, InferCreationAttributes<Label>> {
  30. declare id: CreationOptional<number>;
  31. @AllowNull
  32. @Attribute(DataTypes.INTEGER)
  33. declare articleId: number | null;
  34. }
  35. class NonNullLabel extends Model<
  36. InferAttributes<NonNullLabel>,
  37. InferCreationAttributes<NonNullLabel>
  38. > {
  39. declare id: CreationOptional<number>;
  40. @NotNull
  41. @Attribute(DataTypes.INTEGER)
  42. declare articleId: number;
  43. }
  44. sequelize.addModels([Article, Label, NonNullLabel]);
  45. await sequelize.sync({ force: true });
  46. return { Article, Label, NonNullLabel };
  47. });
  48. describe('setAssociation', () => {
  49. it('associates target model to the source model', async () => {
  50. const { Label, Article } = vars;
  51. const [article, label] = await Promise.all([Article.create(), Label.create()]);
  52. // TODO: this should be null - https://github.com/sequelize/sequelize/issues/14671
  53. expect(label.articleId).to.beNullish();
  54. await article.setLabel(label);
  55. await label.reload();
  56. expect(label.articleId).to.equal(article.id);
  57. });
  58. it('unlinks the previous association', async () => {
  59. const { Label, Article } = vars;
  60. const article = await Article.create();
  61. const label1 = await Label.create({ articleId: article.id });
  62. const label2 = await Label.create();
  63. expect(label1.articleId).to.equal(article.id);
  64. // TODO: this should be null - https://github.com/sequelize/sequelize/issues/14671
  65. expect(label2.articleId).to.beNullish();
  66. await article.setLabel(label2);
  67. await Promise.all([label1.reload(), label2.reload()]);
  68. expect(label1.articleId).to.equal(null);
  69. expect(label2.articleId).to.equal(article.id);
  70. });
  71. it('clears associations when the parameter is null', async () => {
  72. const { Label, Article } = vars;
  73. const article = await Article.create();
  74. const label = await Label.create({ articleId: article.id });
  75. expect(label.articleId).to.equal(article.id);
  76. await article.setLabel(null);
  77. await label.reload();
  78. expect(label.articleId).to.equal(null);
  79. });
  80. it('destroys the previous associations if `destroyPrevious` is true', async () => {
  81. const { Label, Article } = vars;
  82. const article = await Article.create();
  83. await Label.create({ articleId: article.id });
  84. await article.setLabel(null, { destroyPrevious: true });
  85. expect(await Label.count()).to.equal(0);
  86. });
  87. it('destroys the previous associations if the foreign key is not nullable', async () => {
  88. const { NonNullLabel, Article } = vars;
  89. const article = await Article.create();
  90. await NonNullLabel.create({ articleId: article.id });
  91. await article.setNonNullLabel(null);
  92. expect(await NonNullLabel.count()).to.equal(0);
  93. });
  94. it('supports passing the primary key instead of an object', async () => {
  95. const { Label, Article } = vars;
  96. const [article, label] = await Promise.all([Article.create(), Label.create()]);
  97. await article.setLabel(label.id);
  98. await label.reload();
  99. expect(label.articleId).to.equal(article.id);
  100. });
  101. it('supports setting same association twice', async () => {
  102. const { Label, Article } = vars;
  103. const [article, label] = await Promise.all([Article.create(), Label.create()]);
  104. await article.setLabel(label);
  105. await article.setLabel(label);
  106. await label.reload();
  107. expect(label.articleId).to.equal(article.id);
  108. });
  109. });
  110. });
  111. describe('hasOne Mixins + transaction', () => {
  112. if (!dialect.supports.transactions) {
  113. return;
  114. }
  115. setResetMode('destroy');
  116. const vars = beforeAll2(async () => {
  117. class Article extends Model<InferAttributes<Article>, InferCreationAttributes<Article>> {
  118. declare id: CreationOptional<number>;
  119. @HasOne(() => Label, 'articleId')
  120. declare label?: Label;
  121. declare setLabel: HasOneSetAssociationMixin<Label, Label['id']>;
  122. }
  123. class Label extends Model<InferAttributes<Label>, InferCreationAttributes<Label>> {
  124. declare id: CreationOptional<number>;
  125. @AllowNull
  126. @Attribute(DataTypes.INTEGER)
  127. declare articleId: number | null;
  128. }
  129. const transactionSequelize = await createMultiTransactionalTestSequelizeInstance(sequelize);
  130. transactionSequelize.addModels([Article, Label]);
  131. await transactionSequelize.sync({ force: true });
  132. return { Article, Label, transactionSequelize };
  133. });
  134. after(async () => {
  135. return vars.transactionSequelize.close();
  136. });
  137. describe('setAssociations', () => {
  138. it('supports transactions', async () => {
  139. const { Label, Article, transactionSequelize } = vars;
  140. const [article, label] = await Promise.all([Article.create(), Label.create()]);
  141. await transactionSequelize.transaction(async transaction => {
  142. await article.setLabel(label, { transaction });
  143. const labels0 = await Label.findAll({
  144. where: { articleId: article.id },
  145. transaction: null,
  146. });
  147. expect(labels0.length).to.equal(0);
  148. const labels = await Label.findAll({ where: { articleId: article.id }, transaction });
  149. expect(labels.length).to.equal(1);
  150. });
  151. });
  152. it('uses the transaction when destroying the previous associations', async () => {
  153. const { Label, Article, transactionSequelize } = vars;
  154. const [article, t] = await Promise.all([
  155. Article.create(),
  156. transactionSequelize.startUnmanagedTransaction(),
  157. ]);
  158. await Label.create({ articleId: article.id });
  159. await article.setLabel(null, { destroyPrevious: true, transaction: t });
  160. expect(await Label.count({ transaction: null })).to.equal(1);
  161. expect(await Label.count({ transaction: t })).to.equal(0);
  162. await t.rollback();
  163. });
  164. it('uses the transaction when unsetting the previous associations', async () => {
  165. const { Label, Article, transactionSequelize } = vars;
  166. const [article, t] = await Promise.all([
  167. Article.create(),
  168. transactionSequelize.startUnmanagedTransaction(),
  169. ]);
  170. await Label.create({ articleId: article.id });
  171. await article.setLabel(null, { transaction: t });
  172. expect((await Label.findOne({ rejectOnEmpty: true, transaction: null })).articleId).to.equal(
  173. article.id,
  174. );
  175. expect((await Label.findOne({ rejectOnEmpty: true, transaction: t })).articleId).to.equal(
  176. null,
  177. );
  178. await t.rollback();
  179. });
  180. });
  181. });