Home > Software engineering >  How to set run time variable to postgresql in typeorm and nest js
How to set run time variable to postgresql in typeorm and nest js

Time:10-31

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....

CodePudding user response:

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.

CodePudding user response:

Who all are serching for this question . hear is the answer.. I spends 2 days for getting this stuff

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 funciton 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..

Thankyou for your support....

  • Related