postgresqlrow-level-securitynode.js-typeormnestjs-configsupabase-database

How to set run time variable to postgresql in typeorm and nest js


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


Solution

  • 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 function 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 function 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 function 
          return this.userIdWrapper.userIdWrapper(findOne, userId);
        } catch (err) {
          console.log(err);
        }
      }
    }
    

    Don't forget to provide the custom class service inside the provider of user service.