destroy.test.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import type { InferAttributes, InferCreationAttributes } from '@sequelize/core';
  2. import { DataTypes, Model } from '@sequelize/core';
  3. import { Attribute, PrimaryKey, Table, Version } from '@sequelize/core/decorators-legacy';
  4. import { expect } from 'chai';
  5. import sinon from 'sinon';
  6. import { beforeAll2, expectsql, sequelize } from '../../support';
  7. describe('ModelRepository#destroy', () => {
  8. const vars = beforeAll2(() => {
  9. @Table({
  10. noPrimaryKey: true,
  11. })
  12. class NoPk extends Model<InferAttributes<NoPk>, InferCreationAttributes<NoPk>> {}
  13. class SimpleId extends Model<InferAttributes<SimpleId>, InferCreationAttributes<SimpleId>> {
  14. declare id: number;
  15. }
  16. class CompositePk extends Model<
  17. InferAttributes<CompositePk>,
  18. InferCreationAttributes<CompositePk>
  19. > {
  20. @PrimaryKey
  21. @Attribute(DataTypes.INTEGER)
  22. declare id1: number;
  23. @PrimaryKey
  24. @Attribute(DataTypes.INTEGER)
  25. declare id2: number;
  26. }
  27. class VersionedSimpleId extends Model<
  28. InferAttributes<VersionedSimpleId>,
  29. InferCreationAttributes<VersionedSimpleId>
  30. > {
  31. declare id: number;
  32. @Version
  33. declare version: number;
  34. }
  35. sequelize.addModels([SimpleId, CompositePk, VersionedSimpleId, NoPk]);
  36. return { SimpleId, CompositePk, VersionedSimpleId, NoPk };
  37. });
  38. afterEach(() => {
  39. sinon.restore();
  40. vars.SimpleId.hooks.removeAllListeners();
  41. });
  42. it('throw an error if the model has no primary key', async () => {
  43. const { NoPk } = vars;
  44. const repository = NoPk.modelRepository;
  45. const instance = NoPk.build();
  46. await expect(repository._UNSTABLE_destroy(instance)).to.be.rejectedWith(
  47. 'does not have a primary key attribute definition',
  48. );
  49. });
  50. it(`throws an error if the model's PK is not loaded`, async () => {
  51. const { SimpleId } = vars;
  52. const repository = SimpleId.modelRepository;
  53. const instance = SimpleId.build();
  54. await expect(repository._UNSTABLE_destroy(instance)).to.be.rejectedWith(
  55. 'missing the value of its primary key',
  56. );
  57. });
  58. it('creates an optimized query for single-entity deletions', async () => {
  59. const stub = sinon.stub(sequelize, 'queryRaw');
  60. const { SimpleId } = vars;
  61. const repository = SimpleId.modelRepository;
  62. const instance = SimpleId.build({ id: 1 });
  63. await repository._UNSTABLE_destroy(instance);
  64. expect(stub.callCount).to.eq(1);
  65. const firstCall = stub.getCall(0);
  66. expectsql(firstCall.args[0], {
  67. default: `DELETE FROM [SimpleIds] WHERE [id] = 1`,
  68. mssql: `DELETE FROM [SimpleIds] WHERE [id] = 1; SELECT @@ROWCOUNT AS AFFECTEDROWS;`,
  69. });
  70. });
  71. it('creates an optimized query for non-composite PKs with no version', async () => {
  72. const stub = sinon.stub(sequelize, 'queryRaw');
  73. const { SimpleId } = vars;
  74. const repository = SimpleId.modelRepository;
  75. const instance1 = SimpleId.build({ id: 1 });
  76. const instance2 = SimpleId.build({ id: 2 });
  77. await repository._UNSTABLE_destroy([instance1, instance2]);
  78. expect(stub.callCount).to.eq(1);
  79. const firstCall = stub.getCall(0);
  80. expectsql(firstCall.args[0], {
  81. default: `DELETE FROM [SimpleIds] WHERE [id] IN (1, 2)`,
  82. mssql: `DELETE FROM [SimpleIds] WHERE [id] IN (1, 2); SELECT @@ROWCOUNT AS AFFECTEDROWS;`,
  83. });
  84. });
  85. it('creates a deoptimized query for composite PKs', async () => {
  86. const stub = sinon.stub(sequelize, 'queryRaw');
  87. const { CompositePk } = vars;
  88. const repository = CompositePk.modelRepository;
  89. const instance1 = CompositePk.build({ id1: 1, id2: 2 });
  90. const instance2 = CompositePk.build({ id1: 3, id2: 4 });
  91. await repository._UNSTABLE_destroy([instance1, instance2]);
  92. expect(stub.callCount).to.eq(1);
  93. const firstCall = stub.getCall(0);
  94. expectsql(firstCall.args[0], {
  95. default: `DELETE FROM [CompositePks] WHERE ([id1] = 1 AND [id2] = 2) OR ([id1] = 3 AND [id2] = 4)`,
  96. mssql: `DELETE FROM [CompositePks] WHERE ([id1] = 1 AND [id2] = 2) OR ([id1] = 3 AND [id2] = 4); SELECT @@ROWCOUNT AS AFFECTEDROWS;`,
  97. });
  98. });
  99. it('creates a deoptimized query if the model is versioned', async () => {
  100. const stub = sinon.stub(sequelize, 'queryRaw');
  101. const { VersionedSimpleId } = vars;
  102. const repository = VersionedSimpleId.modelRepository;
  103. const instance1 = VersionedSimpleId.build({ id: 1, version: 2 });
  104. const instance2 = VersionedSimpleId.build({ id: 3, version: 4 });
  105. await repository._UNSTABLE_destroy([instance1, instance2]);
  106. expect(stub.callCount).to.eq(1);
  107. const firstCall = stub.getCall(0);
  108. expectsql(firstCall.args[0], {
  109. default: `DELETE FROM [VersionedSimpleIds] WHERE ([id] = 1 AND [version] = 2) OR ([id] = 3 AND [version] = 4)`,
  110. mssql: `DELETE FROM [VersionedSimpleIds] WHERE ([id] = 1 AND [version] = 2) OR ([id] = 3 AND [version] = 4); SELECT @@ROWCOUNT AS AFFECTEDROWS;`,
  111. });
  112. });
  113. it('supports beforeDestroyMany/afterDestroyMany hooks', async () => {
  114. sinon.stub(sequelize, 'queryRaw');
  115. const { SimpleId } = vars;
  116. const repository = SimpleId.modelRepository;
  117. const instance1 = SimpleId.build({ id: 1 });
  118. const instance2 = SimpleId.build({ id: 2 });
  119. const beforeDestroyManySpy = sinon.spy();
  120. const afterDestroyManySpy = sinon.spy();
  121. SimpleId.hooks.addListener('beforeDestroyMany', beforeDestroyManySpy);
  122. SimpleId.hooks.addListener('afterDestroyMany', afterDestroyManySpy);
  123. await repository._UNSTABLE_destroy([instance1, instance2]);
  124. expect(beforeDestroyManySpy.callCount).to.eq(1);
  125. expect(beforeDestroyManySpy.getCall(0).args).to.deep.eq([
  126. [instance1, instance2],
  127. { manualOnDelete: 'paranoid' },
  128. ]);
  129. expect(afterDestroyManySpy.callCount).to.eq(1);
  130. expect(afterDestroyManySpy.getCall(0).args).to.deep.eq([
  131. [instance1, instance2],
  132. { manualOnDelete: 'paranoid' },
  133. undefined, // returned from queryRaw stub
  134. ]);
  135. });
  136. it('skips beforeDestroyMany/afterDestroyMany hooks if noHooks is passed', async () => {
  137. sinon.stub(sequelize, 'queryRaw');
  138. const { SimpleId } = vars;
  139. const repository = SimpleId.modelRepository;
  140. const instance1 = SimpleId.build({ id: 1 });
  141. const instance2 = SimpleId.build({ id: 2 });
  142. const beforeDestroyManySpy = sinon.spy();
  143. const afterDestroyManySpy = sinon.spy();
  144. SimpleId.hooks.addListener('beforeDestroyMany', beforeDestroyManySpy);
  145. SimpleId.hooks.addListener('afterDestroyMany', afterDestroyManySpy);
  146. await repository._UNSTABLE_destroy([instance1, instance2], { noHooks: true });
  147. expect(beforeDestroyManySpy.callCount).to.eq(0);
  148. expect(afterDestroyManySpy.callCount).to.eq(0);
  149. });
  150. it('allows modifying the list of instances in beforeDestroyMany', async () => {
  151. const stub = sinon.stub(sequelize, 'queryRaw');
  152. const { SimpleId } = vars;
  153. const repository = SimpleId.modelRepository;
  154. const instance1 = SimpleId.build({ id: 1 });
  155. const instance2 = SimpleId.build({ id: 2 });
  156. const instance3 = SimpleId.build({ id: 3 });
  157. SimpleId.hooks.addListener('beforeDestroyMany', instances => {
  158. instances.push(instance3);
  159. });
  160. await repository._UNSTABLE_destroy([instance1, instance2]);
  161. expect(stub.callCount).to.eq(1);
  162. const firstCall = stub.getCall(0);
  163. expectsql(firstCall.args[0], {
  164. default: `DELETE FROM [SimpleIds] WHERE [id] IN (1, 2, 3)`,
  165. mssql: `DELETE FROM [SimpleIds] WHERE [id] IN (1, 2, 3); SELECT @@ROWCOUNT AS AFFECTEDROWS;`,
  166. });
  167. });
  168. it('aborts if beforeDestroyMany removes all instances', async () => {
  169. const stub = sinon.stub(sequelize, 'queryRaw');
  170. const { SimpleId } = vars;
  171. const repository = SimpleId.modelRepository;
  172. const instance1 = SimpleId.build({ id: 1 });
  173. SimpleId.hooks.addListener('beforeDestroyMany', instances => {
  174. // remove all instances
  175. instances.splice(0, instances.length);
  176. });
  177. await repository._UNSTABLE_destroy([instance1]);
  178. expect(stub.callCount).to.eq(0);
  179. });
  180. });