123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- import type { InferAttributes, InferCreationAttributes, ModelStatic } from '@sequelize/core';
- import { DataTypes, Model, QueryTypes } from '@sequelize/core';
- import type { ModelHooks } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-hooks.js';
- import { expect } from 'chai';
- import delay from 'delay';
- import sinon from 'sinon';
- import {
- beforeAll2,
- createMultiTransactionalTestSequelizeInstance,
- sequelize,
- setResetMode,
- } from './support';
- describe('AsyncLocalStorage (ContinuationLocalStorage) Transactions (CLS)', () => {
- if (!sequelize.dialect.supports.transactions) {
- return;
- }
- setResetMode('none');
- const vars = beforeAll2(async () => {
- const clsSequelize = await createMultiTransactionalTestSequelizeInstance(sequelize, {
- disableClsTransactions: false,
- });
- class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
- declare name: string | null;
- }
- User.init(
- {
- name: DataTypes.STRING,
- },
- { sequelize: clsSequelize },
- );
- await clsSequelize.sync({ force: true });
- return { clsSequelize, User };
- });
- after(async () => {
- return vars.clsSequelize.close();
- });
- describe('context', () => {
- it('does not use AsyncLocalStorage on manually managed transactions', async () => {
- const transaction = await vars.clsSequelize.startUnmanagedTransaction();
- try {
- expect(vars.clsSequelize.getCurrentClsTransaction()).to.equal(undefined);
- } finally {
- await transaction.rollback();
- }
- });
- // other tests for nested transaction are in sequelize/transaction.test.ts.
- it('supports nested transactions', async () => {
- await vars.clsSequelize.transaction(async () => {
- const transactionA = vars.clsSequelize.getCurrentClsTransaction();
- await vars.clsSequelize.transaction(async () => {
- const transactionB = vars.clsSequelize.getCurrentClsTransaction();
- expect(transactionA === transactionB).to.equal(true, 'transactions should be the same');
- });
- });
- });
- it('supports several concurrent transactions', async () => {
- let t1id;
- let t2id;
- await Promise.all([
- vars.clsSequelize.transaction(async () => {
- t1id = vars.clsSequelize.getCurrentClsTransaction()!.id;
- }),
- vars.clsSequelize.transaction(async () => {
- t2id = vars.clsSequelize.getCurrentClsTransaction()!.id;
- }),
- ]);
- expect(t1id).to.be.ok;
- expect(t2id).to.be.ok;
- expect(t1id).not.to.equal(t2id);
- });
- it('supports nested promise chains', async () => {
- await vars.clsSequelize.transaction(async () => {
- const tid = vars.clsSequelize.getCurrentClsTransaction()!.id;
- await vars.User.findAll();
- expect(vars.clsSequelize.getCurrentClsTransaction()!.id).to.be.ok;
- expect(vars.clsSequelize.getCurrentClsTransaction()!.id).to.equal(tid);
- });
- });
- it('does not leak variables to the outer scope', async () => {
- // 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.
- // 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;
- let transactionSetup = false;
- let transactionEnded = false;
- const clsTask = vars.clsSequelize.transaction(async () => {
- transactionSetup = true;
- await delay(500);
- expect(vars.clsSequelize.getCurrentClsTransaction()).to.be.ok;
- transactionEnded = true;
- });
- await new Promise<void>(resolve => {
- // Wait for the transaction to be setup
- const interval = setInterval(() => {
- if (transactionSetup) {
- clearInterval(interval);
- resolve();
- }
- }, 200);
- });
- expect(transactionEnded).not.to.be.ok;
- expect(vars.clsSequelize.getCurrentClsTransaction()).not.to.be.ok;
- // Just to make sure it didn't change between our last check and the assertion
- expect(transactionEnded).not.to.be.ok;
- await clsTask; // ensure we don't leak the promise
- });
- it('does not leak variables to the following promise chain', async () => {
- await vars.clsSequelize.transaction(() => {});
- expect(vars.clsSequelize.getCurrentClsTransaction()).not.to.be.ok;
- });
- it('does not leak outside findOrCreate', async () => {
- await vars.User.findOrCreate({
- where: {
- name: 'Kafka',
- },
- logging(sql) {
- if (sql.includes('default')) {
- throw new Error('The transaction was not properly assigned');
- }
- },
- });
- await vars.User.findAll();
- });
- });
- describe('sequelize.query', () => {
- beforeEach(async () => {
- await vars.User.truncate();
- });
- it('automatically uses the transaction in all calls', async () => {
- await vars.clsSequelize.transaction(async () => {
- await vars.User.create({ name: 'bob' });
- return Promise.all([
- expect(vars.User.findAll({ transaction: null })).to.eventually.have.length(0),
- expect(vars.User.findAll({})).to.eventually.have.length(1),
- ]);
- });
- });
- it('automagically uses the transaction in all calls with async/await', async () => {
- await vars.clsSequelize.transaction(async () => {
- await vars.User.create({ name: 'bob' });
- expect(await vars.User.findAll({ transaction: null })).to.have.length(0);
- expect(await vars.User.findAll({})).to.have.length(1);
- });
- });
- });
- it('promises returned by sequelize.query are correctly patched', async () => {
- await vars.clsSequelize.transaction(async t => {
- await vars.clsSequelize.query('select 1', { type: QueryTypes.SELECT });
- return expect(vars.clsSequelize.getCurrentClsTransaction()).to.equal(t);
- });
- });
- // reason for this test: https://github.com/sequelize/sequelize/issues/12973
- describe('Model Hook integration', () => {
- type Params<M extends Model> = {
- method: string;
- hooks: Array<keyof ModelHooks>;
- optionPos: number;
- execute(model: ModelStatic<M>): Promise<unknown>;
- getModel(): ModelStatic<M>;
- };
- function testHooks<T extends Model>({
- method,
- hooks: hookNames,
- optionPos,
- execute,
- getModel,
- }: Params<T>) {
- it(`passes the transaction to hooks {${hookNames.join(',')}} when calling ${method}`, async () => {
- await vars.clsSequelize.transaction(async transaction => {
- const hooks = Object.create(null);
- for (const hookName of hookNames) {
- hooks[hookName] = sinon.spy();
- }
- const User = getModel();
- for (const [hookName, spy] of Object.entries(hooks)) {
- User.hooks.addListener(hookName as keyof ModelHooks, spy as any);
- }
- await execute(User);
- const spyMatcher = [];
- // ignore all arguments until we get to the option bag.
- for (let i = 0; i < optionPos; i++) {
- spyMatcher.push(sinon.match.any);
- }
- // find the transaction in the option bag
- spyMatcher.push(sinon.match.has('transaction', transaction));
- for (const [hookName, spy] of Object.entries(hooks)) {
- expect(
- spy,
- `hook ${hookName} did not receive the transaction from AsyncLocalStorage.`,
- ).to.have.been.calledWith(...spyMatcher);
- }
- });
- });
- }
- testHooks({
- method: 'Model.bulkCreate',
- hooks: ['beforeBulkCreate', 'beforeCreate', 'afterCreate', 'afterBulkCreate'],
- optionPos: 1,
- async execute(User) {
- await User.bulkCreate([{ name: 'bob' }], { individualHooks: true });
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model.findAll',
- hooks: ['beforeFind', 'beforeFindAfterExpandIncludeAll', 'beforeFindAfterOptions'],
- optionPos: 0,
- async execute(User) {
- await User.findAll();
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model.findAll',
- hooks: ['afterFind'],
- optionPos: 1,
- async execute(User) {
- await User.findAll();
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model.count',
- hooks: ['beforeCount'],
- optionPos: 0,
- async execute(User) {
- await User.count();
- },
- getModel() {
- return vars.User;
- },
- });
- if (sequelize.dialect.supports.upserts) {
- testHooks({
- method: 'Model.upsert',
- hooks: ['beforeUpsert', 'afterUpsert'],
- optionPos: 1,
- async execute(User) {
- await User.upsert({
- id: 1,
- name: 'bob',
- });
- },
- getModel() {
- return vars.User;
- },
- });
- }
- testHooks({
- method: 'Model.destroy',
- hooks: ['beforeBulkDestroy', 'afterBulkDestroy'],
- optionPos: 0,
- async execute(User) {
- await User.destroy({ where: { name: 'bob' } });
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model.destroy with individualHooks',
- hooks: ['beforeDestroy', 'beforeDestroy'],
- optionPos: 1,
- async execute(User) {
- await User.create({ name: 'bob' });
- await User.destroy({ where: { name: 'bob' }, individualHooks: true });
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model#destroy',
- hooks: ['beforeDestroy', 'beforeDestroy'],
- optionPos: 1,
- async execute(User) {
- const user = await User.create({ name: 'bob' });
- await user.destroy();
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model.update',
- hooks: ['beforeBulkUpdate', 'afterBulkUpdate'],
- optionPos: 0,
- async execute(User) {
- await User.update({ name: 'alice' }, { where: { name: 'bob' } });
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model.update with individualHooks',
- hooks: ['beforeUpdate', 'afterUpdate'],
- optionPos: 1,
- async execute(User) {
- await User.create({ name: 'bob' });
- await User.update({ name: 'alice' }, { where: { name: 'bob' }, individualHooks: true });
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model#save (isNewRecord)',
- hooks: ['beforeCreate', 'afterCreate'],
- optionPos: 1,
- async execute(User: typeof vars.User) {
- const user = User.build({ name: 'bob' });
- user.name = 'alice';
- await user.save();
- },
- getModel() {
- return vars.User;
- },
- });
- testHooks({
- method: 'Model#save (!isNewRecord)',
- hooks: ['beforeUpdate', 'afterUpdate'],
- optionPos: 1,
- async execute(User: typeof vars.User) {
- const user = await User.create({ name: 'bob' });
- user.name = 'alice';
- await user.save();
- },
- getModel() {
- return vars.User;
- },
- });
- describe('paranoid restore', () => {
- const vars2 = beforeAll2(async () => {
- const ParanoidUser = vars.clsSequelize.define(
- 'ParanoidUser',
- {
- name: DataTypes.STRING,
- },
- { paranoid: true },
- );
- await ParanoidUser.sync({ force: true });
- return { ParanoidUser };
- });
- testHooks({
- method: 'Model.restore',
- hooks: ['beforeBulkRestore', 'afterBulkRestore'],
- optionPos: 0,
- async execute() {
- const User = vars2.ParanoidUser;
- await User.restore({ where: { name: 'bob' } });
- },
- getModel() {
- return vars2.ParanoidUser;
- },
- });
- testHooks({
- method: 'Model.restore with individualHooks',
- hooks: ['beforeRestore', 'afterRestore'],
- optionPos: 1,
- async execute() {
- const User = vars2.ParanoidUser;
- await User.create({ name: 'bob' });
- await User.destroy({ where: { name: 'bob' } });
- await User.restore({ where: { name: 'bob' }, individualHooks: true });
- },
- getModel() {
- return vars2.ParanoidUser;
- },
- });
- testHooks({
- method: 'Model#restore',
- hooks: ['beforeRestore', 'afterRestore'],
- optionPos: 1,
- async execute() {
- const User = vars2.ParanoidUser;
- const user = await User.create({ name: 'bob' });
- await user.destroy();
- await user.restore();
- },
- getModel() {
- return vars2.ParanoidUser;
- },
- });
- });
- });
- });
|