GithubHelp home page GithubHelp logo

odavid / typeorm-transactional-cls-hooked Goto Github PK

View Code? Open in Web Editor NEW
519.0 14.0 83.0 376 KB

A Transactional Method Decorator for typeorm that uses cls-hooked to handle and propagate transactions between different repositories and service methods. Inpired by Spring Trasnactional Annotation and Sequelize CLS

License: MIT License

TypeScript 99.69% JavaScript 0.31%
typeorm transaction nestjs typescript decorator

typeorm-transactional-cls-hooked's Introduction

typeorm-transactional-cls-hooked

npm version

A Transactional Method Decorator for typeorm that uses cls-hooked to handle and propagate transactions between different repositories and service methods.

Inspired by Spring Transactional Annotation and Sequelize CLS

See Changelog

Installation

npm install --save typeorm-transactional-cls-hooked
## Needed dependencies
npm install --save typeorm reflect-metadata

Or

yarn add typeorm-transactional-cls-hooked
## Needed dependencies
yarn add typeorm reflect-metadata

Note: You will need to import reflect-metadata somewhere in the global place of your app - https://github.com/typeorm/typeorm#installation

Initialization

In order to use it, you will first need to initialize the cls-hooked namespace before your application is started

import { initializeTransactionalContext } from 'typeorm-transactional-cls-hooked';

initializeTransactionalContext() // Initialize cls-hooked
...
app = express()
...

BaseRepository

Since this is an external library, all your typeorm repositories will need to be a custom repository extending either the BaseRepository (when using TypeORM's Entity) or the BaseTreeRepository class (when using TypeORM's TreeEntity).

// Post.entity.ts
@Entity()
export class Post{
  @PrimaryGeneratedColumn()
  id: number

  @Column
  message: string
  ...
}

// Post.repository.ts
import { EntityRepository } from 'typeorm';
import { BaseRepository } from 'typeorm-transactional-cls-hooked';

@EntityRepository(Post)
export class PostRepository extends BaseRepository<Post> {}

The only purpose of the BaseRepository class is to make sure the manager property of the repository will always be the right one. In cases where inheritance is not possible, you can always Patch the Repository/TreeRepository to enable the same functionality as the BaseRepository

Patching TypeORM Repository

Sometimes there is a need to keep using the TypeORM Repository instead of using the BaseRepository. For this cases, you will need to "mixin/patch" the original Repository with the BaseRepository. By doing so, you will be able to use the original Repository and not change the code or use BaseRepository.

This method was taken from https://gist.github.com/Diluka/87efbd9169cae96a012a43d1e5695667 (Thanks @Diluka)

In order to do that, the following should be done during initialization:

import { initializeTransactionalContext, patchTypeORMRepositoryWithBaseRepository } from 'typeorm-transactional-cls-hooked';

initializeTransactionalContext() // Initialize cls-hooked
patchTypeORMRepositoryWithBaseRepository() // patch Repository with BaseRepository.

If there is a need to keep using the TypeORM TreeRepository instead of using BaseTreeRepository, use patchTypeORMTreeRepositoryWithBaseTreeRepository.


IMPORTANT NOTE

Calling initializeTransactionalContext and patchTypeORMRepositoryWithBaseRepository must happen BEFORE any application context is initialized!


Using Transactional Decorator

  • Every service method that needs to be transactional, need to use the @Transactional() decorator
  • The decorator can take a connectionName as argument (by default it is default)
    • In some cases, where the connectionName should be dynamically evaluated, the value of connectionName can be a function that returns a string.
  • The decorator can take an optional propagation as argument to define the propagation behaviour
  • The decorator can take an optional isolationLevel as argument to define the isolation level (by default it will use your database driver's default isolation level.)
export class PostService {
  constructor(readonly repository: PostRepository)

  @Transactional() // Will open a transaction if one doesn't already exist
  async createPost(id, message): Promise<Post> {
    const post = this.repository.create({ id, message })
    return this.repository.save(post)
  }
}

Transaction Propagation

The following propagation options can be specified:

  • MANDATORY - Support a current transaction, throw an exception if none exists.
  • NESTED - Execute within a nested transaction if a current transaction exists, behave like REQUIRED else.
  • NEVER - Execute non-transactionally, throw an exception if a transaction exists.
  • NOT_SUPPORTED - Execute non-transactionally, suspend the current transaction if one exists.
  • REQUIRED (default behaviour) - Support a current transaction, create a new one if none exists.
  • REQUIRES_NEW - Create a new transaction, and suspend the current transaction if one exists.
  • SUPPORTS - Support a current transaction, execute non-transactionally if none exists.

Isolation Levels

The following isolation level options can be specified:

  • READ_UNCOMMITTED - A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur.
  • READ_COMMITTED - A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur.
  • REPEATABLE_READ - A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur.
  • SERIALIZABLE = A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented.

NOTE: If a transaction already exist and a method is decorated with @Transactional and propagation does not equal to REQUIRES_NEW, then the declared isolationLevel value will not be taken into account.

Hooks

Because you hand over control of the transaction creation to this library, there is no way for you to know whether or not the current transaction was sucessfully persisted to the database.

To circumvent that, we expose three helper methods that allow you to hook into the transaction lifecycle and take appropriate action after a commit/rollback.

  • runOnTransactionCommit(cb) takes a callback to be executed after the current transaction was sucessfully committed
  • runOnTransactionRollback(cb) takes a callback to be executed after the current transaction rolls back. The callback gets the error that initiated the roolback as a parameter.
  • runOnTransactionComplete(cb) takes a callback to be executed at the completion of the current transactional context. If there was an error, it gets passed as an argument.
export class PostService {
    constructor(readonly repository: PostRepository, readonly events: EventService) {}

    @Transactional()
    async createPost(id, message): Promise<Post> {
        const post = this.repository.create({ id, message })
        const result = await this.repository.save(post)
        runOnTransactionCommit(() => this.events.emit('post created'))
        return result
    }
}

Unit Test Mocking

@Transactional and BaseRepository can be mocked to prevent running any of the transactional code in unit tests.

This can be accomplished in Jest with:

jest.mock('typeorm-transactional-cls-hooked', () => ({
  Transactional: () => () => ({}),
  BaseRepository: class {},
}));

Repositories, services, etc. can be mocked as usual.

Logging / Debug

The Transactional uses the Typeorm Connection logger to emit log messages.

In order to enable logs, you should set logging: ["log"] or logging: ["all"] to your typeorm logging configuration.

The Transactional log message structure looks as follows:

Transactional@UNIQ_ID|CONNECTION_NAME|METHOD_NAME|ISOLATION|PROPAGATION - MESSAGE
  • UNIQ_ID - a timestamp taken at the begining of the Transactional call
  • CONNECTION_NAME - The typeorm connection name passed to the Transactional decorator
  • METHOD_NAME - The decorated method in action
  • ISOLATION - The Isolation Level passed to the Transactional decorator
  • PROPAGATION - The Propagation value passed to the Transactional decorator

During initialization and patching repositories, the Typeorm Connection logger is not available yet. For this reason, the console.log() is being used, but only if TRANSACTIONAL_CONSOLE_DEBUG environment variable is defined.

typeorm-transactional-cls-hooked's People

Contributors

ashleyw avatar boy51 avatar daffodil11 avatar dependabot[bot] avatar holm avatar labibramadhan avatar lytc avatar matomesc avatar nathan815 avatar odavid avatar ohaddavid-pecan avatar sunjoong85 avatar svvac avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

typeorm-transactional-cls-hooked's Issues

ROUTE_ARGS_METADATA missing after using @Transactional decorator

When @Transactional is being used on a method, the method decorator @Body(), @Param(), and @Query() documentations will be gone later on the Swagger docs.

Below is my inspections:

Here is @nestjs/common injecting ROUTE_ARGS_METADATA to controller methods by using its method name as the key, the ROUTE_ARGS_METADATA itself is mainly stored on the target.constructor which is the class where the method is defined, this is going to be used later for @nestjs/swagger to explore available methods/endpoints and its object structure as well that can be found here

The problem is that @Transactional is overwriting the method without preserving the method name, this makes the createPipesRouteParamDecorator() function of @nestjs/swagger will not find any kind of ROUTE_ARGS_METADATA that have been previously stored by @nestjs/common, because the key itself to retrieve ROUTE_ARGS_METADATA stored on the class is the method name

what is the default Propagation?

I use default Propagation which pass nothing to @Transactional(). I consider it is REQUIRED. But when I use a decorated method to call another decorated method cause a deadlock. TypeORM logs two START TRANSACTION. it means the default Propagation is REQUIRES_NEW? Or it is a bug?

Testing Error initializeTransactionalContext()

So I'm using Jest and we are strictly doing unit tests -

I used the example

jest.mock('typeorm-transactional-cls-hooked', () => ({
            Transactional: () => () => ({}),
            BaseRepository: class {},
        }));

But it errors on initializeTransactionalContext(). Where would initializeTransactionalContext be used within a jest unit test ?

Does this library work with both InjectRepository and createQueryBuilder

Hi:

Nice library you got here! On one of my project, we use an encapsulation pattern where each Repo class gets the typeorm repository injected in the constructor.

@Injectable()
export class UserRepo {
constructor(
@InjectRepository(User) private readonly repository: Repository
) {}
}

Then, somewhere below, we have a method like this:

@transactional()
async lockUserById(id: string): Promise<User[]> {
const users = await this.repository
.createQueryBuilder('user')
.select(user.id)
.where(user.id = :userId, { id })
.setLock('pessimistic_write')
.getMany()
return alerts
}

When multiple execution happens, it seems they are all able to pass lockUserById and go on to insert user records instead of waiting.

Then, all but one of them will rollback and retry again.

Does this mean transaction is applied correctly to the injected Repository?

We do use:
initializeTransactionalContext() // Initialize cls-hooked
patchTypeORMRepositoryWithBaseRepository() // patch Repository with BaseRepository.

At the beginning.

And this is MYSQL

Thanks!

cls-hooked should be a dependency

Hey there, great library!

I think that cls-hooked should be a Dependency (right now it's a peerDependency). This is because the library will not work if cls-hooked is not installed.

Issue with not rolled back transactions

Hello,

First of all thanks for your work.

I have an issue with not rolled back transactions in my repository, even I can clearly see that in DB logs transaction is starting and rolling back. This issue occurs in my mocha tests ( data are not cleared between tests ).

I've following configuration steps but the issue still occurs. Any hints regarding what I'm doing wrong?

To reproduce just update line 115 in Equipment.spec.ts file to test equipment

Link to repository

https://github.com/Melzar/onion-architecture-boilerplate

Thanks in advance for any help and your time.

EntityManager is Undefined

I setup per the documentation, but when this is called entitymanager is undefined:

save<T extends DeepPartial<Entity>>(entityOrEntities: T|T[], options?: SaveOptions): Promise<T|T[]> {
        return this.manager.save<T>(this.metadata.target as any, entityOrEntities as any, options);
    }

my index:

initializeTransactionalContext();
serve();

my respository:

@EntityRepository(User)
export class UserRepository extends BaseRepository<User> {}

my service injecting it

export class UserService {
    constructor(
        private readonly userRepository: UserRepository
    ) {}

lastly the call to save

return await this.userRepository.save(user);

Feature request: make NAMESPACE_NAME exported

I'd like to know whether some code is running as a result of a call to a method that was decorated with @Transactional or not, to make sure all DB updates are done only inside transactions. Now to get context I have to hard-code value of NAMESPACE_NAME (from src/common.ts) in my code, being able to import it would be helpful.

typeorm-transactional-cls-hooked initialization

Hi, @odavid !

This is not an issue, its just a warning to devs community.

My app is spread between several modules, and in order to 'reuse' code, I did something like this:

import {
  initializeTransactionalContext,
  patchTypeORMRepositoryWithBaseRepository,
} from 'typeorm-transactional-cls-hooked';

export const initializeTransactions = () => {
  initializeTransactionalContext(); 
  patchTypeORMRepositoryWithBaseRepository(); 
};

And ...

import { initializeTransactions } from 'mylib';

async function bootstrap() {
  initializeTransactions();
...
...

Code above makes transactions dont work ! Transaction initialization must be in the SAME module that it will used. I spend 3 hours researching why library was not working ... haha !

Best, and my warm congrats for this esplendid library !

Log statements don't respect `logging` option from TypeORM

This bug was added in this commit:

ee15911

console.log(`Transactional@initializeTransactionalContext\`)

console.log(`Transactional@patchRepositoryManager repositoryType: ${repositoryType?.constructor?.name}`)

These console.log statements don't respect the logging option from TypeORM. This results in log statements even when you have logging set to false.

Importing reflect metadata

Thanks for the amazing work with this package.

One quick question, I realized at the index:
import 'reflect-metadata'

This may cause issues if it is included twice, and the official typeorm doc recommends importing it.

I was wondering if importing reflect-metadata should be kept as consumer responsibility, or alternatively could the doc be updated to point out the fact that it is included already ?

use @Transactional() with @Res of nestjs transaction won't close

Hello everyone,

I'm using @Transactional() decorator in controller methods but with problem when using @Res decorator to inject express's response object into the method.

I am serving raw file content with some controller method so that I inject express response object and pipe the file read stream to client.

Before that, I update my file entity but the request stuck at typeorm repository save method and the transaction won't close.

The workaround for me is remove @Transactional() from the controller method.

The call is something like the following:

// request comes in
//  |
//  V
// controller method getFile
@Get(':id')
@Transactional() // originally has @Transactional(), but removed
public async getFile(@Param('id') fileId: string, @Res() res: Response): Promise<void> {
    // update file entity
    await this.fileService.update(/* some params */);
    // get file readable stream
    // and then
    stream.pipe(res);
}
//  |
//  V
// file service update method (has @Transactional())
@Injectable()
export class FileService {
    @Transactional()
    public async update(file: FileEntity): Promise<FileEntity> {
        // update file
        return this.fileRepo.save( // stuck at here if add @Transactional() decorator to controller method
            this.fileRepo.create({
                ...file,
                updatedAt: new Date(),
            }),
        );
    }
}
nestjs: v7.6.5
typeorm-transactional-cls-hooked: v0.1.19

Not working with Nestjs 6

With Nestjs version 6 it is not working.

When I can use repository in service:

constructor(
    private readonly userRepository: UserRepository
  ) {}

I got error that It cannot resolve dependencies...

Transactional not applying on whole method ?

When trying this library, I realize that the transaction is committed after the first "await this.service1.create" while I would expect the transaction to be committed at the end of the method.

@Transactional()
    async myMethod(id, message): Promise<Post> {
        const service1Result = await this.service1.create({ id, message })  // which makes a repository1.save()
        const service1Result = await this.service2.create({ id, message })  //  which makes a repository2.save()
        runOnTransactionCommit(() => this.events.emit('post created'))
        return result
}

Hello, could i ask u some question about cls's problem?

I'm using sequelize be the orm of my project, and i has implemented a Transactional decorator for sequelize with using cls which has similar function like yours.
But in my using cases, i found some problems caused by cls.
Something like this:
image

Actually, if you use cls, use ns.run (function of cls), when you use [await async func()] in the callback of ns.run, there will be ineffective after retured from await.

i want to get some helps about how you resolve the problem from you if you has encoutered it.
Any help will be appriciated!

Transactions not working with multiple DBs (connections)

I am using the typeorm-test-transactions package to run transactional tests. As the package is essentially just a wrapper for this one, I assume I should post here. If not please correct me.

We have 2 connections to a postgres and a timescale DB in our app and those are initialized in one 'root' test as follows:

var connections = await createConnections([
		{
			type: 'postgres',
			name: 'default',
			host: process.env.TESTDB_HOST,
			port: +process.env.TESTDB_PORT,
			username: process.env.TESTDB_USER,
			password: process.env.TESTDB_PASS,
			database: process.env.TESTDB_DB,
			entities: ['./src/shared/models/default/**/*.entity.ts'],
			migrations: ['./src/shared/migrations/default/*.ts'],
			logging: ['error', 'warn'],
			synchronize: false,
			cli: {
				migrationsDir: './src/shared/migrations/default'
			}
		},
		{
			type: 'postgres',
			name: 'timescale',
			host: process.env.TESTDB_TIMESCALE_HOST,
			port: +process.env.TESTDB_TIMESCALE_PORT,
			username: process.env.TESTDB_TIMESCALE_USER,
			password: process.env.TESTDB_TIMESCALE_PASS,
			database: process.env.TESTDB_TIMESCALE_DB,
			entities: ['./src/shared/models/timescale/**/*.entity.ts'],
			migrations: ['./src/shared/migrations/timescale/*.ts'],
			logging: ['error', 'warn'],
			synchronize: false,
			cli: {
				migrationsDir: './src/shared/migrations/timescale'
			}
		}
	]);

In one of our tests, we access a repository like so:
repository = getConnection('timescale').getCustomRepository(CandleRepository);

The problem is that, when getting a non-default connection, the transactions don't seem to work. If I delete the default connection, rename the timescale one and re-run the tests, everything works as expected, i.e. the tests don't affect each other.

Could this be an issue with your project and how could we solve it? I'd be prepared to do a PR if it's not too complicated to get into the project's code.

transaction is not working

The first save is success. the second save is no working. But I mock try throw exception. The create method should not working. Now the first save is success.

     @Transactional()
     public async create(entity: T): Promise<T> {
        await this.save(this.toEntity(entity), 'add');
        throw new Warning('测试事务操作');
        entity.id+='6666';
        return await this.save(this.toEntity(entity), 'add');
    }
protected async save(entity: T, method: string): Promise<T> {
        if (!entity || typeof entity !== 'object') {
            throw new Warning('不是有效对象');
        }
        try {
            if (method === 'add') {
                entity = this.initCreationAudited(entity);
            }
            entity = this.initModificationAudited(entity);
            return await this.repository.save(entity as any);
        } catch (err) {
            throw new Warning(err.message);
        }
    }

Transactional doesn't work well with Promise.all

I double checked #58 and despite the fact, that I still belive the code was not written correctly, there actually is a problem with Promise.all calls if there are many nested calls to methods decorated as Transactional.

Reproduction code (needs sql.js library installed):

// test-entity.ts
// tslint:disable:max-classes-per-file

import { Service } from 'typedi';
import { Column, Entity, PrimaryGeneratedColumn, Repository } from 'typeorm';
import { Transactional } from 'typeorm-transactional-cls-hooked';
import { InjectRepository } from 'typeorm-typedi-extensions';

@Entity({ name: 'entity' })
export class TestEntity {
  @PrimaryGeneratedColumn({ name: 'id' })
  public id: number;

  @Column({ name: 'name' })
  public name: string;
}

@Service()
export class TestService {
  @InjectRepository(TestEntity)
  public repository: Repository<TestEntity>;

  @Transactional()
  public reprodA(ids: number[]): Promise<any> {
    console.log('reprodA start');
    return Promise.all(
      ids.map((id, index) => {
        const prevId = index === 0 ? ids[ids.length - 1] : ids[index - 1];
        return this.reprodB(id, prevId);
      })
    );
  }

  @Transactional()
  public workaround(ids: number[]): Promise<any> {
    console.log('workaround start');
    let error;
    return Promise.all(
      ids.map((id, index) => {
        const prevId = index === 0 ? ids[ids.length - 1] : ids[index - 1];
        return this.reprodB(id, prevId).catch(err => (error = error || err));
      })
    ).then(() => {
      if (error) {
        throw error;
      }
    });
  }

  @Transactional()
  public async reprodB(id: number, prevId: number): Promise<any> {
    if (id === 2) {
      throw new Error('Mock error');
    } else {
      await this.timeConsumingOperation();
    }
    await this.reprodC(id);
  }

  @Transactional()
  public async reprodC(id: number): Promise<any> {
    let entity = await this.repository.findOne(id);
    if (!entity) {
      entity = this.repository.create();
    }
    entity.name = 'Test';
    await this.repository.save(entity);
  }

  public timeConsumingOperation() {
    return new Promise(resolve => setTimeout(() => resolve(true), 2000));
  }
}
// promise-all.test.ts
import { Container } from 'typedi';
import * as TypeORM from 'typeorm';
import {
  initializeTransactionalContext,
  patchTypeORMRepositoryWithBaseRepository
} from 'typeorm-transactional-cls-hooked';

import { initializeCls } from '../src/server/initializeCls';
import { TestEntity, TestService } from './helpers/testEntity';

describe('Promise.all', () => {
  beforeAll(() => {
    initializeTransactionalContext();
    patchTypeORMRepositoryWithBaseRepository();
    initializeCls();
    TypeORM.useContainer(Container);
    return TypeORM.createConnection({
      cache: false,
      dropSchema: true,
      entities: [TestEntity],
      logger: 'advanced-console',
      logging: 'all',
      synchronize: true,
      type: 'sqljs'
    });
  });

  it('will commit some inner transactions on error', async () => {
    const service = Container.get(TestService);
    const testEntitiesCountBefore = await service.repository.count();
    await expect(service.reprodA([1, 2, 3, 4])).rejects.toThrow();
    await service.timeConsumingOperation();
    const testEntitiesCountAfter = await service.repository.count();
    expect(testEntitiesCountAfter).toBeGreaterThan(testEntitiesCountBefore);
  });

  it('should not commit any inner transactions on error', async () => {
    const service = Container.get(TestService);
    const testEntitiesCountBefore = await service.repository.count();
    await expect(service.workaround([5, 2, 6, 7])).rejects.toThrow();
    await service.timeConsumingOperation();
    const testEntitiesCountAfter = await service.repository.count();
    expect(testEntitiesCountAfter).toEqual(testEntitiesCountBefore);
  });
});

I'm not really sure what can you do with that in your library, as there is no way of canceling a promise.

A workaround I found is to catch the errors, let all the promises continue, and then throw the caught error when they're all finished, It is used in the workaround method of TestService.

However it is ugly and consumes time/server power for no reason, and would be great if there was a way of solving this issue in the library. Maybe it would be possible to mark the transaction as failed in CLS and just throw errors on any repository call if it's marked, I'll try to look into it.

Anyway, untill it's resolved I'll leave this issue here with a workaround.

When i use Propagation.NESTED, serializable not work

I write a method A with transaction SERIALIZABLE in my code, and it works as my expect. However, When I add another method B with transaction Propagation.NESTED, and A calls B. I found the transaction A not work like SERIALIZABLE.
This is my code.

 @Transactional({ isolationLevel: IsolationLevel.SERIALIZABLE })
  async addUserScoreAuto(user: AuthUser) {
    await this.service.findOrCreate({ userId: user.id });
  }
 @Transactional({ propagation: Propagation.NESTED })
  async findOrCreate(info: DeepPartial<T>): Promise<[T, boolean]> {
    const entity = await this.repo.findOne(info);
    if (!entity) {
      return [await this.repo.save(info), true];
    }
    return [entity, false];
  }

Error when using repository methods outside of a request context

On my open source project, we have a in memory queue, where we have some functions regards gamification stuff. So, those functions run asynchronously

Before version 0.1.17, everything worked fine, but now we are constantly getting the error below

image

As far as I can tell, it seems that when not bound to a http request context, the manager is null, and then everything stops working.

image

Suggestion: clarity in documentation

I appreciate the project and am trying to use it for something slightly outside its main use case: wrap an entire Express request-response in a transaction (as in Django) and I'm not clear how to do so...

Is the Transactional annotation required in all cases or will a BaseEntity use a CLS-transaction if it's available? I guess the question is: how is a CLS-txn started?; what uses a CLS-txn?

How to test that method uses Transactional decorator?

Hi,

thanks for this awesome lib!

is there a way to verify in jest that a method is decorated with @Transactional()?

@Transactional()
something() {
 // ...
}
it("runs in a transaction", () => {
 // ???
})

Any help is much appreciated :-)

Any way mocking @Transactional()

Problem

I got this message in my test codes
No CLS namespace defined in your app ... please call initializeTransactionalContext() before application start.

Question

Are there any ways to mock @transactional() in my test codes?

My test codes are below.

describe('Approval', () => {
    test('Approve booking.', async () => {
      /**
       * Given
       * orderId
       */
      const orderId = '1234';

      const spy_OrderRepository_findOrder = jest.spyOn(orderRepository, 'findOrder');
      const spy_OrderRepository_saveOrder = jest.spyOn(orderRepository, 'saveOrder');

      /**
       * When
       * Host confirms booking request.
       */
     //confirmRequest is wrapped by @Transactional()
      await hostOrderService.confirmRequest(orderId');

     /**
     * Then
     *  Success
     */

Furthermore, is it possible to manage transactions in a service layer without this hook? Currently, NestJS integration is not possible to control typeorm transaction features among services, not repositories.

Great job! Thanks!

library architecture

please remove types from dist folder, if we are not importing from dist types are not working

image

image

Custom Repositories without BaseRepository or Repository

Hi there!

In my project, I don't extend by BaseRepository or Repository. In my repository class, I use @InjectRepisitoryManager() to use EntityManager. Eg.

@Injectable() export class TestRepository { constructor(@InjectEntityManager() private readonly entityManager: EntityManager) {}

In this case, a Transactional() seems to not working properly. I saw in logs START TRANSACTION and ROLLBACK on an error but still, others (correct ones) operations are executed on the database. Is there a way to fix this?

The @Transactional() decorator doesn't want to open a new transaction

index.ts

export class Main {
     constructor(
        private readonly service: Service // Are injected from DI system
    ) {} 

     public load() {
          setTimeout(() => this.doJob(), 5000)
     }

     private async doJob() {
          await this.service.makeMeACake()
     }
}

service.ts

export class Service {
    constructor(readonly cakeRepository: CakeRepository) {}

    @Transactional()
    public async makeMeACake() {
        let cake = await this.cakeRepository.findOneOrFail(50, {
            lock: { mode: 'pessimistic_write' }
        })

        cake.status = ECakeStatus.CREATING
        await this.cakeRepository.update(cake.id, { status: cake.status })
    }
}

And here's what I caught when launching the application:
image

I don't use http server in this application. I have several functions, timer and service. Unfortunately for some reason the @transactional() decorator doesn't want to open a new transaction in my case.
In the original, the code has more methods and is more complicated but the basic principle of calling functions is exactly the same as I had described. Where can I find the error? How to debug?

suggest to turn this lib into a patch

for (const p of Object.getOwnPropertyNames(BaseRepository.prototype)) {
  Object.defineProperty(Repository.prototype, p, Object.getOwnPropertyDescriptor(BaseRepository.prototype, p));
}

If modified Repository, it can also support ActivieRecord!

How to use Hook?

Hi @odavid!

First, I wanted to say thanks for the awesome library. Solve most of my use cases really well. Right now I am trying to figure out how to do a retry if a certain Transaction failed due to for example the Isolation Level of Postgres.

Let's say I have some service that does the following:

class Handler {
  @Transactional()
  async handle(data: any): Promise<void> {
    return new Promise(async (resolve, reject) => {
      let result;
      runOnTransactionEnd(async (err: Error | null) => {
        if (err) {
          // Retry
          try {
            result = await this._prepareData(data);
          } catch (err) {
            reject(err);
          }
        }
        await this._broadcastEvent(new OrderCreated(result));
        resolve(result);
      });
      result = await this._prepareData(data);
    });
  }
  
  async _prepareData(data: any) {
    const user = await this.userService.retrieveUser(data.userId);
    if (!user) {
      await this.userService.createUser(data.userId);
    }
    try {
      const order = await this.orderService.makeOrder(user, data);
      return { user, order };
    } catch (err) {
      this.logger.error(err);
      throw new Error('Something wrong, rollback!!');
    }
  }  
}

Let's assume I will only retry once, so if a certain error happens on my subsequent queries, due to for example concurrent update, I want to retry the query again with the updated result.

If no error, then I want to proceed to the broadcastEvent. But it seems that my Promise will be unhandled when I don't wrap the result with a try/catch, but if I wrapped it with a try/catch, the runOnTransactionEnd does not seem to be executed. Could you provide an example, on how for example you can achieve a retry?

No context available. ns.run() or ns.bind() must be called first

{ "status": 500, "errstr": "No context available. ns.run() or ns.bind() must be called first.", "data": "{\"errorMsg\":\"No context available. ns.run() or ns.bind() must be called first.\",\"stack\":\"Error: No context available. ns.run() or ns.bind() must be called first. at Namespace.set (/xxx/node_modules/cls-hooked/context.js:36:11) at Object.exports.setEntityManagerForConnection (/xxx/node_modules/typeorm-transactional-cls-hooked/src/common.ts:40:14) at Task.<anonymous> (/xxx/node_modules/typeorm-transactional-cls-hooked/src/Transactional.ts:46:11) at step (/xxx/node_modules/typeorm-transactional-cls-hooked/dist/Transactional.js:32:23) at Object.next (/xxx/node_modules/typeorm-transactional-cls-hooked/dist/Transactional.js:13:53) at /xxx/node_modules/typeorm-transactional-cls-hooked/dist/Transactional.js:7:71 at Promise._execute (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/debuggability.js:272:9) at Promise._resolveFromExecutor (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/promise.js:473:18) at new Promise (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/promise.js:77:14) at __awaiter (/xxx/node_modules/typeorm-transactional-cls-hooked/dist/Transactional.js:3:12) at transactionCallback (/xxx/node_modules/typeorm-transactional-cls-hooked/src/Transactional.ts:45:72) at EntityManager.<anonymous> (/xxx/src/entity-manager/EntityManager.ts:138:34) at step (/xxx/node_modules/tslib/tslib.js:133:27) at Object.next (/xxx/node_modules/tslib/tslib.js:114:57) at fulfilled (/xxx/node_modules/tslib/tslib.js:104:62) at tryCatcher (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/util.js:16:23) at Promise._settlePromiseFromHandler (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/promise.js:502:31) at Promise._settlePromise (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/promise.js:559:18) at Promise._settlePromise0 (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/promise.js:604:10) at Promise._settlePromises (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/promise.js:683:18) at Async._drainQueue (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/async.js:138:16) at Async._drainQueues (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/async.js:148:10) at Immediate.Async.drainQueues [as _onImmediate] (/xxx/node_modules/thinkjs/node_modules/bluebird/js/release/async.js:17:14) at processImmediate (timers.js:637:19)\"}" }

Is there someone can help me?

QueryRunnerAlreadyReleasedError after runOnTransactionRollback

I have a @Transactional() function with runOnTransactionRollback(() => this.someRepository.save()) in it.
So, when the function caught error and rejects with rollback, the this.someRepository.save() have called and then I receive the QueryRunnerAlreadyReleasedError error.

The RelationLoader ignore the CLS transaction

Hey,

how do you manage to use the transactional entity manager for the relation loader?

When I take a look at the code, I can see that it's use

     this.connection.createQueryBuilder(queryRunner)

I try to do :

             await candidateFilesService.getRepo().
             createQueryBuilder()
            .useTransaction(true)
            .relation(CandidateEntity, 'candidateFile')
            .of(c1).loadOne()

Latest update breaks MongoRepository

Hi,

I'm using both SQL and noSQL in my application, both wrapped with TypeORM.

I'm using this plugin, with patchTypeORMRepositoryWithBaseRepository, since I'm using some entities from old external packages.

Everything worked fine, untill latest update, where getEntityManagerOrTransactionManager got ability to return undefined

It seems that because of that, TypeORM's MongoRepository always gets undefined as it's entityManager, when it's called in a transaction. Before the update, it would just return the _entityManager passed from repository object, but now it's unable to.

Workaround is to redefine MongoRepository manager as a "normal" property, after patchTypeORMRepositoryWithBaseRepository was called, so I suggest adding it to the function itself.

Object.defineProperty(MongoRepository.prototype, 'manager', { configurable: true, writable: true });

How to commit the transaction?

Hi there. Thx for this awesome library, but i have one question about the transactions.
How to commit a transaction when using the `@Transactional () ' decorator?

@Transactional()
async myAwesomeMethod(): Promise<void> {
  await this.repository.save(...);

  // <-- how can i commit a transaction before an exception occurs? 

  throw new Error();
}

Problem with repository injection

Hi,
I'm trying out your package. It seems that for @transactional to work all database access should be done using repositories (and not via EntityManager, correct me if I'm wrong).
I had a bunch of repositories doing operations on other database entities, using their local manager.
So I replaced those with repositories injection (each repository gets injected with any other repository it needs).
But this didn't work. Looking at the value of the injected repository in a debugger it is not of the repository type at all, but rather of type EntityManager. I guess I'm doing something wrong here but I don't know what.
Thanks

A way to support transactions started without this library

To ease the migration to this library (which is great and should make my life much better, hauling the entity manager around is a terrible experience) it would be nice if it could support transactions started in a traditional way. i.e:

await getConnection().transaction(async transactionalEntityManager => {
   this.create()
});

 @Transactional({propagation: Propagation.MANDATORY})
  async create() {
    const newThing = this.repository.create()
    return this.repository.save(newThing)
  }

Right now this will fail, but it would be great if it could work. Unfortunately I don't have enough know-how to implement it myself. If you think it's possible and will nudge me in the right direction, I'd be willing to try, though.

Question: Transaction per request

Hey,

I use Nestjs with Typeorm. I'm not sure it has a point when transaction comes into it.
If node is fully non blocking what's the point of having a single connection but cannot have multiple transactions?

I see that only one transaction is allowed per connection but maybe a transaction queue could solve this problem.

So when two requests hit the same controller method and both requires a transaction the second request will fail since there's another active transaction.

Is this package able to solve this problem?

dist folder missing in package version 0.1.9

I've just upgraded to the latest version and I think the build step was missed when publishing, because he dist folder appears to be missing, which prevents the module from working.

Typo

See victoriqueko/typeorm-transactional-cls-hooked@ccd41860be824cdfba1e53efb609d69215695880

repository extending BaseRepository<Entity> doesn't work with NESTJS

Hi,

I'm currently using typeorm-transactional-cls-hooked with my project with TypeORM, express and it works fine.

then i try to using NESTJS with typeorm�� data mapper pattern, with repositories and it doesn't work

detail codes below

import { BaseRepository } from 'typeorm-transactional-cls-hooked';
import { EntityRepository } from 'typeorm';
import { Member, MemberType, MemberStatus } from '../models/members';

@EntityRepository(Member)
export class MemberHandler extends BaseRepository<Member> {
  async findMemberById(member_id: number): Promise<Member> {
    const member: Member = await this.findOne(member_id);
    return member;
  }
}

and bootstrap codes below

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { initializeTransactionalContext } from 'typeorm-transactional-cls-hooked';
import 'reflect-metadata';
import { createConnection } from 'typeorm';
import 'dotenv/config';

async function bootstrap() {
  createConnection('default')
    .then(async (connection) => {
      /* typeorm transactional initialize cls-hooked */
      initializeTransactionalContext();

      /* app bootstrap */
      const app = await NestFactory.create(AppModule);
      await app.listen(process.env.APP_PORT);
    })
    .catch((err) => {
      console.log(err);
    });
}
bootstrap();

I can't find any differences between previous project and new one

here's compile error
스크린샷 2020-11-27 오후 12 47 54

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.