skip to Main Content

Iam using the row level security in supabase with nest.js, So how can I set runtime variables safely to the DB so that I can be sure that the variables sync with each app user (due to the http request triggered the execution)?

I saw that it is possible to set local variables in a transaction but I wouldn’t like to wrap all the queries with transactions.

Thanks & Regards

I tried to execute this with subscribers in nestjs it working fine . but it wont have a function like beforeSelect or beforeLoad , so i drop it

import { Inject, Injectable, Scope } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { ContextService } from 'src/context/context.service';
import { DataSource, EntityManager, LoadEvent, RecoverEvent, TransactionRollbackEvent, TransactionStartEvent } from 'typeorm';
import {
  EventSubscriber,
  EntitySubscriberInterface,
  InsertEvent,
  UpdateEvent,
  RemoveEvent,
} from 'typeorm';

@Injectable()
@EventSubscriber()
export class CurrentUserSubscriber implements EntitySubscriberInterface {
  constructor(
    @InjectDataSource() dataSource: DataSource,
    private context: ContextService,
  ) {
    dataSource.subscribers.push(this);
  }

  async setUserId(mng: EntityManager, userId: string) {
    await mng.query(
      `SELECT set_config('request.jwt.claim.sub', '${userId}', true);`,
    );
  }
  async beforeInsert(event: InsertEvent<any>) {
    try {
      const userId = this.context.getRequest();
      await this.setUserId(event.manager, userId);
    } catch (err) {
      console.log(err);
    }
  }

  async beforeTransactionRollback(event: TransactionRollbackEvent) {
    console.log('hello')
    try {
      const userId = this.context.getRequest();
      await this.setUserId(event.manager, userId);
    } catch (err) {
      console.log(err);
    }
  }

  async beforeUpdate(event: UpdateEvent<any>) {
    try {
      const userId = this.context.getRequest();
      await this.setUserId(event.manager, userId);
    } catch (err) {
      console.log(err);
    }
  }
  async beforeRemove(event: RemoveEvent<any>) {
    try {
      const userId = this.context.getRequest();
      await this.setUserId(event.manager, userId);
    } catch (err) {
      console.log(err);
    }
  }
}

After i get to know that we can use query runner instead of subscriber . but its not working ,
also i need a common method to use all the queries

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Users } from 'src/common/entities';
import { DataSource, EntityManager, Repository } from 'typeorm';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(Users) private userRepository: Repository<Users>,
    private dataSource: DataSource,
    private em: EntityManager,
  ) {}
  getAllUsers(userId: string) {
    const queryRunner = this.dataSource.createQueryRunner();

    return new Promise(async (resolve, reject) => {
      let res: any;
      try {
        await queryRunner.connect();
        await queryRunner.manager.query(
          // like this we can set the variable
          `SELECT set_config('request.jwt.claim.sub', '${userId}', true);`,
        );
        // after setting config variable the query should return only one user by userId
        res = await queryRunner.query('SELECT * FROM users');
        //  but it reurns every user
      } catch (err) {
        reject(err);
      } finally {
        await queryRunner.manager.query(`RESET request.jwt.claim.sub`);
        await queryRunner.release();
        resolve(res);
      }
    });
  }
}

Thanks in advance….

2

Answers


  1. Chosen as BEST ANSWER

    First you have to create a custom class for wrapping your userId or any stuff

    custome_service.ts ==>

    @Injectable()
    export class UserIdWrapper {
      constructor(private dataSource: DataSource) {}
    
      userIdWrapper = (callback: (mng: QueryRunner) => Promise<any>, userId: string) => {
        const queryRunner = this.dataSource.createQueryRunner();
        return new Promise(async (resolve, reject) => {
          let res: any;
          try {
            await queryRunner.connect();
            await queryRunner.manager.query(
              `SELECT set_config('your_variable_name', '${userId}', false)`,
            );
            //here is your funciton your calling in the service
            res = await callback(queryRunner);
          } catch (err) {
            reject(err);
          } finally {
            await queryRunner.manager.query(`RESET your_variable_name`);
            await queryRunner.release();
            resolve(res);
          }
        });
      };
    }
    

    Now here you have to call the function inside user service

    user.service.ts ==>

    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Users } from 'src/common/entities';
    import { UserIdWrapper } from 'src/common/local-settup/userId_wrapper';
    import { DataSource, EntityManager, QueryRunner, Repository } from 'typeorm';
    
    @Injectable()
    export class UsersService {
      constructor(
        @InjectRepository(Users) private userRepository: Repository<Users>,
        private dataSource: DataSource,
        private userIdWrapper: UserIdWrapper
      ) {}
    
      async getAllUsers(userId: string) {
        //This is your call back funciton that have to pass
        const findOne = async (queryRunner: QueryRunner) => {
          const res = await queryRunner.query('SELECT * FROM public.users');
          return res;
        };
        try {
          //hear we are passing the function in to the class funciton 
          return this.userIdWrapper.userIdWrapper(findOne, userId);
        } catch (err) {
          console.log(err);
        }
      }
    }
    

    Dont forgot to provide the custom class service inside the provider of user service.


  2. Sorry to say, bro. But in currently state of development TypeORM does not have a feature that let us set conection variables. The roundabout for your problem is to do something like this.

    /**
     * Note: Set current_tenant session var and executes a query on repository.
     * Usage:
     * const itens = = await tenantTransactionWrapper( manager => {
     *     return manager.getRepository(Entity).find();
     *  });
     *
     * @param {function} callback - a function thar receives an Entity Manager and returns a method to be executed by tenantTransactionWrapper
     * @param {string} providedTenantId - optional tenantId, otherwise tenant will be taken from localStorage
     */
    async function tenantWrapper<R>(
      callback: (manager: EntityManager) => Promise<R>,
      providedTenantId?: string,
    ) {
      const tenantId = providedTenantId || tenantStorage.get();
      let response: R;
      await AppDataSource.transaction(async manager => {
        await manager.query(`SET LOCAL smsystem.current_tenant='${tenantId}';`);
        response = await callback(manager);
      });
    
      return response;
    }
    

    Then create a custom repository to make use of the wraper a little bit simple.

    const customRepository = <T>(entity: EntityTarget<T>) => ({
      find: (options?: FindManyOptions<T>) =>
        tenantTransactionWrapper(mng => mng.getRepository(entity).find(options))(),
      findAndCount: (options?: FindManyOptions<T>) =>
        tenantTransactionWrapper(mng =>
          mng.getRepository(entity).findAndCount(options),
        )(),
      save: (entities: DeepPartial<T>[], options?: SaveOptions) =>
        tenantTransactionWrapper(mng =>
          mng.getRepository(entity).save(entities, options),
        )(),
      findOne: (options: FindOneOptions<T>) =>
        tenantTransactionWrapper(async mng =>
          mng.getRepository(entity).findOne(options),
        )(),
      remove: (entities: T[], options?: RemoveOptions) =>
        tenantTransactionWrapper(mng =>
          mng.getRepository(entity).remove(entities, options),
        )(),
      createQueryBuilder: () => {
        throw new Error(
          'Cannot create queryBuilder for that repository type, instead use: tenantWrapper',
        );
      },
      tenantTransactionWrapper,
    });
    

    And finally use our customRepository :

    class PersonsRepository implements IPersonsRepository {
      private ormRepository: Repository<Person>;
    
      constructor() {
        this.ormRepository = AppDataSource.getRepository<Person>(Person).extend(
          customRepository(Person),
        );
      }
    
      public async create(data: ICreatePersonDTO): Promise<Person> {
        const newPerson = this.ormRepository.create(data);
    
        await this.ormRepository.save(newPerson);
    
        return newPerson;
      }
    
      public async getAll(relations: string[] = []): Promise<Person[]> {
        return this.ormRepository.find({ relations });
      }
    

    I hope this may help someone and will be very glad if someone provides a better solution.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search