Aliheym / typeorm-transactional

A Transactional Method Decorator for TypeORM that uses Async Local Storage or cls-hooked to handle and propagate transactions between different repositories and service methods.
MIT License
213 stars 28 forks source link

[Question/Feature Request] Execute class-validation inside transaction #1

Closed Migushthe2nd closed 2 years ago

Migushthe2nd commented 2 years ago

I use this module in combination with NestJS. I have implemented custom validators to validate e.g. parameters (@Params) in a request supplied by the user. This in order to check whether entities exist before continuing running the body of a controller method. The issue I have is that this interaction by the user is executed in two transactions: one is started and committed by the class validator, the other is started and committed after this class validator upon running the service method (Controller -> Service -> update). This could result in a situation:

  1. entity exists during execution of the class validator
  2. enity gets deleted by someone elses
  3. service cannot find entity and throws exception

Would it somehow be possible to have the validator and service share the same transaction?

The structure is as follows.

Class Validator entity-exists.validator.ts

@ValidatorConstraint({name: "entityExists", async: true})
@Injectable()
export class EntityExistsRule implements ValidatorConstraintInterface {
    constructor(
        private service: EntityService,
    ) {
    }

    // <-- tried adding @Transactional here
    async validate(id: number, args: ValidationArguments) {
        return this.service.exists(id); // <-- this exists call has a @Transactional decorator
    }

    defaultMessage(args: ValidationArguments) {
        return `Entity${args.value} not found`;
    }
}

export function EntityExists(validationOptions?: ValidationOptions) {
    return function (object: any, propertyName: string) {
        registerDecorator({
            name: "EntityExists",
            target: object.constructor,
            propertyName: propertyName,
            options: validationOptions,
            validator: EntityExistsRule,
        });
    };
}

Params get-entity-by-id.params.ts

export class GetEntityById {
    @EntityExists()
    @IsInt()
    id: number;
}

Controller entity.controller.ts

@Controller("entities/:id")
export class EntityController {
    constructor(
        private readonly entityService: EntityService,
    ) {
    }

    @Patch()
    // <-- tried adding @Transactional here
    async updateEntity(
        @Param() {houseId}: GetEntityByIdParams,
        @Body() body: UpdateEntityDto,
    ) {
        return this.entityService.update(id, body);
    }
}

Service entity.service.ts

export class EntityService {
    constructor(
        @InjectRepository(Entity) private repository: Repository<Entity>,
    ) {
    }

    @Transactional()
    exists(
        id: number
        updatedEntity: Partial<Entity>,
    ) {
        // this query is executed in the first transaction
        return this.repository.exists({
            where: {
                id,
            }
        });
    }

    @Transactional()
    async update(
        id: number
        updatedEntity: Partial<Entity>,
    ) {
        // this query is executed in a second transaction
        const entity = await this.repository.findOne({
            where: {
                id,
            }
        });

        // this could cause an exception, because entity could have been deleted between the Validator transaction and this transaction
        entity.field = "ok"

        // ...
    }
}

What I have considered

Thank you for updating the module for the latest TypeORM version by the way!

Aliheym commented 2 years ago

@Migushthe2nd Sorry for the late reply.

With the current implementation of this library and pipes handling (ValidationPipe handles class-validator) in Nest.js it seems impossible to me to implement this feature.

The problem is that pipes are executed separately and before the controller code is executed:

https://github.com/nestjs/nest/blob/master/packages/core/router/router-execution-context.ts#L139-L173

Roughly it looks like this:

() => {
  const pipes = executePipes();

  const response = entityController.updateEntity();
}

With @Transactional() decorator for controller method it looks like:

() => {
  const pipes = executePipes();

  await runInTransaction(async () =>
    const response = entityController.updateEntity();
  }
}

To work with pipes in same transactional context we need to wrap code that execute both pipes and controller code inside runInTransaction.

We need something like that:

() => {
  await runInTransaction(async () =>
     const pipes = executePipes();

     const response = entityController.updateEntity();
  }
}

But so far I don't see any solution other than manually handling ValidationPipe inside controller method. It seems Nest.js doesn't offer anything to handle it.