🔧 Nestjs + Prisma, transaction propagation & test rollback & multi-tenancy
Prisma interactive transaction
Prisma is great. Yet it's support for interactive transaction is currently limited (Up to v5+),
// interactive transaction
prisma.$transaction([array_of_operations,]);
prisma.$transaction(async (tx) => {
    // dependent operations
});
Container managed transactions
As we integrate Prisma to NestJs, we want cross Services transaction propagation, container managed tx is even better. After some investigation, we come to @nestjs-cls/transactional,
@Injectable()
export class MyService {
  constructor(
    private readonly txHost: TransactionHost<TransactionalAdapterPrisma>,
    private readonly anotherService: AnotherService,
  ) {}
@Transactional()
  async myMethod(){
    const prisma = this.txHost.tx as PrismaClient;
    // db operations1
    await this.anotherService.anotherMethod(); // db operations2
    // db operations3
  }
}
Just decorate your Service method with @Transactional(), it will automatically propagate the transaction across service calls. Under the hood, it uses cls (continuation-local storage) to manage the transaction context.
Rollback is triggered by just throwing an exception.
Setup the module
    ClsModule.forRoot({
      plugins: [
        new ClsPluginTransactional({
          imports: [PrismaModule], // comment this line, if you have existing PrismaModule
          middleware: { mount: true }, // mount plugin as nestjs middleware
          adapter: new TransactionalAdapterPrisma({ // Prisma adaptor
            // the original PrismaService is wrapped into the adaptor
            prismaInjectionToken: PrismaService,
          }),
        }),
      ],
    }),
More complicated use cases, please read it's documentation.
Transactions in e2e test cases
After each test case, we need to rollback the transaction, preventing side-effects. Thankfully again, it comes to @chax-at/transactional-prisma-testing,
// the PrismaService proxy
let prismaService;
// This function must be called before every test
async function beforeEachFn(): Promise<void> {
  if(prismaTestingHelper == null) {
    prismaTestingHelper = new PrismaTestingHelper(app.get(PrismaService));
    prismaService = prismaTestingHelper.getProxyClient();
  }
  await prismaTestingHelper.startNewTransaction();
}
// This function must be called after every test
function afterEachFn(): void {
  prismaTestingHelper?.rollbackCurrentTransaction();
}
Integrated with @nestjs-cls/transactional
We need this orderly wrapping,
@nestjs-cls/transactional
  └─@chax-at/transactional-prisma-testing
      └─original-PrismaService
So we override PrismaService with testing proxy in beforeAllFn,
import { Test } from '@nestjs/testing';
let prismaTestingHelper, prismaServiceForTest;
const beforeAllFn = () => {
  const moduleFixtureForTest = await Test.createTestingModule({ imports: [AppModule] })
      .overrideProvider(PrismaService)
      .useFactory({ factory: (originalPrisma: PrismaService) => {
          if (prismaTestingHelper == null) {
            prismaTestingHelper = new PrismaTestingHelper(originalPrisma);
            prismaServiceForTest = prismaTestingHelper.getProxyClient();
          }
          return prismaServiceForTest;
        },
        inject: [PrismaService],
      })
      .compile();
  const app: NestFastifyApplication =
    await moduleFixtureForTest.createNestApplication(new FastifyAdapter(), {
      abortOnError: false,
      bufferLogs: false,
    });
  // ... ...
}
Don't forget to call these method around each test,
describe('AppController (e2e)', () => {
  beforeAll(beforeAllFn);
  afterAll(afterAllFn);
  beforeEach(beforeEachFn);
  afterEach(afterEachFn);
  it('/api (GET)', () => {
    // db operations are rollbacked after each test automatically.
  });
🎉 🎉 🎉
Transparent multi-tenancy based on postgreSQL
We have a multi-tenant application, Callgent, tenants data are isolated based on the tenant_id column in db tables.
By design, we want tenant isolation transparently implemented, so we don't need to add tenant_id to every query.
  // by default, we want the below query to be tenant isolated,
  // only users in context tenant are returned.
  prisma.$queryRaw`SELECT * FROM users`
We utilize the postgreSQL row level security(RLS), following this article to achieve transparent tenant isolation.
RLS is only applicable in postgreSQL
How to implement
- 
in schema.prismafile, set db columntenant_iddefault value to context variabletenancy.tenantId,tenant_id Int @default(dbgenerated("(current_setting('tenancy.tenantId'))::int"))
- 
enable postgres row level security(RLS), so that we can filter data by tenant_idautomatically: write SQL in prisma/migrations/01_row_level_security/migration.sql, please refer this real example
- 
on each request, preset tenant_idintoclscontext, refer to auth-logined.listener.ts,cls.set('TENANT_ID', curentUser.tenantId);
- 
extend PrismaClientto settenant_idbefore any query, refer to prisma-tenancy.provider.ts,SELECT set_config('tenancy.tenantId', cls.get('TENANT_ID') ...
- 
if you want to bypass rls, for example, by admin, or looking up the logon user to determine their tenant ID: CREATE POLICY bypass_rls_policy ON "User" USING (current_setting('tenancy.bypass_rls', TRUE)::text = 'on');then when you want to bypass rls, you must set tenancy.bypass_rlstoonbefore running the query, refer to prisma-tenancy.service.ts,await prisma.$executeRaw`SELECT set_config('tenancy.bypass_rls', 'on', TRUE)`;
how it works
- tenantIdis set into- clscontext on each request from current user.
- PrismaClientis extended on- $allOperationsto set- tenantIdinto db variable- tenancy.tenantIdbefore any query.
- postgreSQL RLS is enabled, so that all queries will be filtered by tenancy.tenantIdautomatically.
- on db insertoperation,tenancy.tenantIdis set intotenant_idcolumn as default value.
Integrate them all
As we extended PrismaClient to enable transparent multi-tenancy, now the wrapping order is changed to,
@nestjs-cls/transactional
  └─@chax-at/transactional-prisma-testing
      └─**tenancy-extended-prisma**
        └─original-PrismaService
- 
extend PrismaClientas nameTENANTED_PRISMA_SERVICE@Global()
 @Module({
 imports: [PrismaModule], // original PrismaModule
 providers: [ {
 provide: TENANTED_PRISMA_SERVICE,
 // ...
 } ],
 exports: [TENANTED_PRISMA_SERVICE], // extended PrismaClient
 })
 export class PrismaTenancyOnPgModule {}
- 
replace PrismaServicewithTENANTED_PRISMA_SERVICEinprismaInjectionToken,
- 
replace PrismaServicewithTENANTED_PRISMA_SERVICEinbeforeAllFn,
🎉 🎉 🎉
