cls.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import type { InferAttributes, InferCreationAttributes, ModelStatic } from '@sequelize/core';
  2. import { DataTypes, Model, QueryTypes } from '@sequelize/core';
  3. import type { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-hooks.js';
  4. import { expect } from 'chai';
  5. import delay from 'delay';
  6. import sinon from 'sinon';
  7. import {
  8. beforeAll2,
  9. createMultiTransactionalTestSequelizeInstance,
  10. sequelize,
  11. setResetMode,
  12. } from './support';
  13. describe('AsyncLocalStorage (ContinuationLocalStorage) Transactions (CLS)', () => {
  14. if (!sequelize.dialect.supports.transactions) {
  15. return;
  16. }
  17. setResetMode('none');
  18. const vars = beforeAll2(async () => {
  19. const clsSequelize = await createMultiTransactionalTestSequelizeInstance(sequelize, {
  20. disableClsTransactions: false,
  21. });
  22. class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
  23. declare name: string | null;
  24. }
  25. User.init(
  26. {
  27. name: DataTypes.STRING,
  28. },
  29. { sequelize: clsSequelize },
  30. );
  31. await clsSequelize.sync({ force: true });
  32. return { clsSequelize, User };
  33. });
  34. after(async () => {
  35. return vars.clsSequelize.close();
  36. });
  37. describe('context', () => {
  38. it('does not use AsyncLocalStorage on manually managed transactions', async () => {
  39. const transaction = await vars.clsSequelize.startUnmanagedTransaction();
  40. try {
  41. expect(vars.clsSequelize.getCurrentClsTransaction()).to.equal(undefined);
  42. } finally {
  43. await transaction.rollback();
  44. }
  45. });
  46. // other tests for nested transaction are in sequelize/transaction.test.ts.
  47. it('supports nested transactions', async () => {
  48. await vars.clsSequelize.transaction(async () => {
  49. const transactionA = vars.clsSequelize.getCurrentClsTransaction();
  50. await vars.clsSequelize.transaction(async () => {
  51. const transactionB = vars.clsSequelize.getCurrentClsTransaction();
  52. expect(transactionA === transactionB).to.equal(true, 'transactions should be the same');
  53. });
  54. });
  55. });
  56. it('supports several concurrent transactions', async () => {
  57. let t1id;
  58. let t2id;
  59. await Promise.all([
  60. vars.clsSequelize.transaction(async () => {
  61. t1id = vars.clsSequelize.getCurrentClsTransaction()!.id;
  62. }),
  63. vars.clsSequelize.transaction(async () => {
  64. t2id = vars.clsSequelize.getCurrentClsTransaction()!.id;
  65. }),
  66. ]);
  67. expect(t1id).to.be.ok;
  68. expect(t2id).to.be.ok;
  69. expect(t1id).not.to.equal(t2id);
  70. });
  71. it('supports nested promise chains', async () => {
  72. await vars.clsSequelize.transaction(async () => {
  73. const tid = vars.clsSequelize.getCurrentClsTransaction()!.id;
  74. await vars.User.findAll();
  75. expect(vars.clsSequelize.getCurrentClsTransaction()!.id).to.be.ok;
  76. expect(vars.clsSequelize.getCurrentClsTransaction()!.id).to.equal(tid);
  77. });
  78. });
  79. it('does not leak variables to the outer scope', async () => {
  80. // This is a little tricky. We want to check the values in the outer scope, when the transaction has been successfully set up, but before it has been comitted.
  81. // We can't just call another function from inside that transaction, since that would transfer the context to that function - exactly what we are trying to prevent;
  82. let transactionSetup = false;
  83. let transactionEnded = false;
  84. const clsTask = vars.clsSequelize.transaction(async () => {
  85. transactionSetup = true;
  86. await delay(500);
  87. expect(vars.clsSequelize.getCurrentClsTransaction()).to.be.ok;
  88. transactionEnded = true;
  89. });
  90. await new Promise<void>(resolve => {
  91. // Wait for the transaction to be setup
  92. const interval = setInterval(() => {
  93. if (transactionSetup) {
  94. clearInterval(interval);
  95. resolve();
  96. }
  97. }, 200);
  98. });
  99. expect(transactionEnded).not.to.be.ok;
  100. expect(vars.clsSequelize.getCurrentClsTransaction()).not.to.be.ok;
  101. // Just to make sure it didn't change between our last check and the assertion
  102. expect(transactionEnded).not.to.be.ok;
  103. await clsTask; // ensure we don't leak the promise
  104. });
  105. it('does not leak variables to the following promise chain', async () => {
  106. await vars.clsSequelize.transaction(() => {});
  107. expect(vars.clsSequelize.getCurrentClsTransaction()).not.to.be.ok;
  108. });
  109. it('does not leak outside findOrCreate', async () => {
  110. await vars.User.findOrCreate({
  111. where: {
  112. name: 'Kafka',
  113. },
  114. logging(sql) {
  115. if (sql.includes('default')) {
  116. throw new Error('The transaction was not properly assigned');
  117. }
  118. },
  119. });
  120. await vars.User.findAll();
  121. });
  122. });
  123. describe('sequelize.query', () => {
  124. beforeEach(async () => {
  125. await vars.User.truncate();
  126. });
  127. it('automatically uses the transaction in all calls', async () => {
  128. await vars.clsSequelize.transaction(async () => {
  129. await vars.User.create({ name: 'bob' });
  130. return Promise.all([
  131. expect(vars.User.findAll({ transaction: null })).to.eventually.have.length(0),
  132. expect(vars.User.findAll({})).to.eventually.have.length(1),
  133. ]);
  134. });
  135. });
  136. it('automagically uses the transaction in all calls with async/await', async () => {
  137. await vars.clsSequelize.transaction(async () => {
  138. await vars.User.create({ name: 'bob' });
  139. expect(await vars.User.findAll({ transaction: null })).to.have.length(0);
  140. expect(await vars.User.findAll({})).to.have.length(1);
  141. });
  142. });
  143. });
  144. it('promises returned by sequelize.query are correctly patched', async () => {
  145. await vars.clsSequelize.transaction(async t => {
  146. await vars.clsSequelize.query('select 1', { type: QueryTypes.SELECT });
  147. return expect(vars.clsSequelize.getCurrentClsTransaction()).to.equal(t);
  148. });
  149. });
  150. // reason for this test: https://github.com/sequelize/sequelize/issues/12973
  151. describe('Model Hook integration', () => {
  152. type Params<M extends Model> = {
  153. method: string;
  154. hooks: Array<keyof ModelHooks>;
  155. optionPos: number;
  156. execute(model: ModelStatic<M>): Promise<unknown>;
  157. getModel(): ModelStatic<M>;
  158. };
  159. function testHooks<T extends Model>({
  160. method,
  161. hooks: hookNames,
  162. optionPos,
  163. execute,
  164. getModel,
  165. }: Params<T>) {
  166. it(`passes the transaction to hooks {${hookNames.join(',')}} when calling ${method}`, async () => {
  167. await vars.clsSequelize.transaction(async transaction => {
  168. const hooks = Object.create(null);
  169. for (const hookName of hookNames) {
  170. hooks[hookName] = sinon.spy();
  171. }
  172. const User = getModel();
  173. for (const [hookName, spy] of Object.entries(hooks)) {
  174. User.hooks.addListener(hookName as keyof ModelHooks, spy as any);
  175. }
  176. await execute(User);
  177. const spyMatcher = [];
  178. // ignore all arguments until we get to the option bag.
  179. for (let i = 0; i < optionPos; i++) {
  180. spyMatcher.push(sinon.match.any);
  181. }
  182. // find the transaction in the option bag
  183. spyMatcher.push(sinon.match.has('transaction', transaction));
  184. for (const [hookName, spy] of Object.entries(hooks)) {
  185. expect(
  186. spy,
  187. `hook ${hookName} did not receive the transaction from AsyncLocalStorage.`,
  188. ).to.have.been.calledWith(...spyMatcher);
  189. }
  190. });
  191. });
  192. }
  193. testHooks({
  194. method: 'Model.bulkCreate',
  195. hooks: ['beforeBulkCreate', 'beforeCreate', 'afterCreate', 'afterBulkCreate'],
  196. optionPos: 1,
  197. async execute(User) {
  198. await User.bulkCreate([{ name: 'bob' }], { individualHooks: true });
  199. },
  200. getModel() {
  201. return vars.User;
  202. },
  203. });
  204. testHooks({
  205. method: 'Model.findAll',
  206. hooks: ['beforeFind', 'beforeFindAfterExpandIncludeAll', 'beforeFindAfterOptions'],
  207. optionPos: 0,
  208. async execute(User) {
  209. await User.findAll();
  210. },
  211. getModel() {
  212. return vars.User;
  213. },
  214. });
  215. testHooks({
  216. method: 'Model.findAll',
  217. hooks: ['afterFind'],
  218. optionPos: 1,
  219. async execute(User) {
  220. await User.findAll();
  221. },
  222. getModel() {
  223. return vars.User;
  224. },
  225. });
  226. testHooks({
  227. method: 'Model.count',
  228. hooks: ['beforeCount'],
  229. optionPos: 0,
  230. async execute(User) {
  231. await User.count();
  232. },
  233. getModel() {
  234. return vars.User;
  235. },
  236. });
  237. if (sequelize.dialect.supports.upserts) {
  238. testHooks({
  239. method: 'Model.upsert',
  240. hooks: ['beforeUpsert', 'afterUpsert'],
  241. optionPos: 1,
  242. async execute(User) {
  243. await User.upsert({
  244. id: 1,
  245. name: 'bob',
  246. });
  247. },
  248. getModel() {
  249. return vars.User;
  250. },
  251. });
  252. }
  253. testHooks({
  254. method: 'Model.destroy',
  255. hooks: ['beforeBulkDestroy', 'afterBulkDestroy'],
  256. optionPos: 0,
  257. async execute(User) {
  258. await User.destroy({ where: { name: 'bob' } });
  259. },
  260. getModel() {
  261. return vars.User;
  262. },
  263. });
  264. testHooks({
  265. method: 'Model.destroy with individualHooks',
  266. hooks: ['beforeDestroy', 'beforeDestroy'],
  267. optionPos: 1,
  268. async execute(User) {
  269. await User.create({ name: 'bob' });
  270. await User.destroy({ where: { name: 'bob' }, individualHooks: true });
  271. },
  272. getModel() {
  273. return vars.User;
  274. },
  275. });
  276. testHooks({
  277. method: 'Model#destroy',
  278. hooks: ['beforeDestroy', 'beforeDestroy'],
  279. optionPos: 1,
  280. async execute(User) {
  281. const user = await User.create({ name: 'bob' });
  282. await user.destroy();
  283. },
  284. getModel() {
  285. return vars.User;
  286. },
  287. });
  288. testHooks({
  289. method: 'Model.update',
  290. hooks: ['beforeBulkUpdate', 'afterBulkUpdate'],
  291. optionPos: 0,
  292. async execute(User) {
  293. await User.update({ name: 'alice' }, { where: { name: 'bob' } });
  294. },
  295. getModel() {
  296. return vars.User;
  297. },
  298. });
  299. testHooks({
  300. method: 'Model.update with individualHooks',
  301. hooks: ['beforeUpdate', 'afterUpdate'],
  302. optionPos: 1,
  303. async execute(User) {
  304. await User.create({ name: 'bob' });
  305. await User.update({ name: 'alice' }, { where: { name: 'bob' }, individualHooks: true });
  306. },
  307. getModel() {
  308. return vars.User;
  309. },
  310. });
  311. testHooks({
  312. method: 'Model#save (isNewRecord)',
  313. hooks: ['beforeCreate', 'afterCreate'],
  314. optionPos: 1,
  315. async execute(User: typeof vars.User) {
  316. const user = User.build({ name: 'bob' });
  317. user.name = 'alice';
  318. await user.save();
  319. },
  320. getModel() {
  321. return vars.User;
  322. },
  323. });
  324. testHooks({
  325. method: 'Model#save (!isNewRecord)',
  326. hooks: ['beforeUpdate', 'afterUpdate'],
  327. optionPos: 1,
  328. async execute(User: typeof vars.User) {
  329. const user = await User.create({ name: 'bob' });
  330. user.name = 'alice';
  331. await user.save();
  332. },
  333. getModel() {
  334. return vars.User;
  335. },
  336. });
  337. describe('paranoid restore', () => {
  338. const vars2 = beforeAll2(async () => {
  339. const ParanoidUser = vars.clsSequelize.define(
  340. 'ParanoidUser',
  341. {
  342. name: DataTypes.STRING,
  343. },
  344. { paranoid: true },
  345. );
  346. await ParanoidUser.sync({ force: true });
  347. return { ParanoidUser };
  348. });
  349. testHooks({
  350. method: 'Model.restore',
  351. hooks: ['beforeBulkRestore', 'afterBulkRestore'],
  352. optionPos: 0,
  353. async execute() {
  354. const User = vars2.ParanoidUser;
  355. await User.restore({ where: { name: 'bob' } });
  356. },
  357. getModel() {
  358. return vars2.ParanoidUser;
  359. },
  360. });
  361. testHooks({
  362. method: 'Model.restore with individualHooks',
  363. hooks: ['beforeRestore', 'afterRestore'],
  364. optionPos: 1,
  365. async execute() {
  366. const User = vars2.ParanoidUser;
  367. await User.create({ name: 'bob' });
  368. await User.destroy({ where: { name: 'bob' } });
  369. await User.restore({ where: { name: 'bob' }, individualHooks: true });
  370. },
  371. getModel() {
  372. return vars2.ParanoidUser;
  373. },
  374. });
  375. testHooks({
  376. method: 'Model#restore',
  377. hooks: ['beforeRestore', 'afterRestore'],
  378. optionPos: 1,
  379. async execute() {
  380. const User = vars2.ParanoidUser;
  381. const user = await User.create({ name: 'bob' });
  382. await user.destroy();
  383. await user.restore();
  384. },
  385. getModel() {
  386. return vars2.ParanoidUser;
  387. },
  388. });
  389. });
  390. });
  391. });