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, Botlet.IO, 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.
:::note
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 exampleon 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_rls to on before 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 intoclscontext on each request from current user. -
PrismaClientis extended on$allOperationsto settenantIdinto db variabletenancy.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,
🎉 🎉 🎉

Top comments (0)