I'm trying to use a Prisma transaction in a NestJS project and I can't figure out a clean way to accomplish the following:
Have a service that will call other services and have all of them bound to a transaction. Eg:
#Injectable()
export class OrdersService {
constructor(private prismaService: PrismaService, ...) {}
async someFn() {
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
await this.userService.update() // This will perform an update using prismaService internally
await this.otherService.delete() // Again, it'll use prismaService
}
}
}
In this case, both user and other services will use their own prisma service and won't be bound to the Transaction.
Is there a way to accomplish this without passing the prismaServiceBoundToTx to each method?
The main problem I had when finding a suitable solution was, that the prisma client in the lambda of an interactive transaction is not a fully fledged Client, but just Prisma.TransactionClient, which is missing $on, $connect, $disconnect, $use and the $transaction methods.
If prisma would provide a full Client at this place, all you could do to solve the problem was just doing transactions like this:
**THIS DOES NOT WORK BECAUSE prismaServiceBoundToTransaction IS JUST OF TYPE Prisma.TransactionClient!!!**
return await this.prismaService.$transaction(async (prismaServiceBoundToTransaction): Promise<any> => {
const userService = new UserService(prismaServiceBoundToTransaction)
const otherService = new OtherService(prismaServiceBoundToTransaction)
//Following calls will use prismaServiceBoundToTransaction internally
await userService.update()
await otherService.delete()
}
Of course above only works, if UserService and OtherService are stateless.
So for my solution I created a new Interface that will offer all methods of Prisma.TransactionClient, but also a custom method to create a transaction.
All of the services like your UserService will only retrieve this exact interface, so they can't call $transaction but only my interactiveTransaction method!
export interface PrismaClientWithCustomTransaction
extends Readonly<Prisma.TransactionClient> {
interactiveTransaction<F>(
fn: (prisma: Prisma.TransactionClient) => Promise<F>,
options?: {
maxWait?: number | undefined;
timeout?: number | undefined;
isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
}
): Promise<F>;
}
We then create a concrete class TransactionalPrismaClient that implements the mentioned interface and delivers it, by retrieving a Prisma.TransactionClient in it's constructor and forwarding all of its methods. Additionally we also just implement the interactiveTransaction method by executing the lambda method with the Prisma.TransactionClient
export class TransactionalPrismaClient<
T extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
U = 'log' extends keyof T
? T['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
? Prisma.GetEvents<T['log']>
: never
: never,
GlobalReject extends
| Prisma.RejectOnNotFound
| Prisma.RejectPerOperation
| false
| undefined = 'rejectOnNotFound' extends keyof T
? T['rejectOnNotFound']
: false
> implements PrismaClientWithCustomTransaction
{
constructor(private readonly transactionalClient: Prisma.TransactionClient) {}
$executeRaw<T = unknown>(
query: TemplateStringsArray | Prisma.Sql,
...values: any[]
): PrismaPromise<number> {
return this.transactionalClient.$executeRaw(query, ...values);
}
$executeRawUnsafe<T = unknown>(
query: string,
...values: any[]
): PrismaPromise<number> {
return this.transactionalClient.$executeRawUnsafe(query, ...values);
}
$queryRaw<T = unknown>(
query: TemplateStringsArray | Prisma.Sql,
...values: any[]
): PrismaPromise<T> {
return this.transactionalClient.$queryRaw(query, ...values);
}
$queryRawUnsafe<T = unknown>(
query: string,
...values: any[]
): PrismaPromise<T> {
return this.transactionalClient.$queryRawUnsafe(query, ...values);
}
get otherEntity(): Prisma.OtherEntityDelegate<GlobalReject> {
return this.transactionalClient.otherEntity;
}
get userEntity(): Prisma.UserEntityDelegate<GlobalReject> {
return this.transactionalClient.userEntity;
}
async interactiveTransaction<F>(
fn: (prisma: Prisma.TransactionClient) => Promise<F>,
options?: {
maxWait?: number | undefined;
timeout?: number | undefined;
isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
}
): Promise<F> {
return await fn(this.transactionalClient);
}
}
And in your PrismaService we also need to implement the interactiveTransaction method, so that it satifies our defined interface PrismaClientWithCustomTransaction.
#Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, PrismaClientWithCustomTransaction
{
private readonly logger = new ConsoleLogger(PrismaService.name);
async onModuleInit() {
this.logger.log('Trying to connect to db.');
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
interactiveTransaction<R>(
fn: (prisma: Prisma.TransactionClient) => Promise<R>,
options?: {
maxWait?: number | undefined;
timeout?: number | undefined;
isolationLevel?: Prisma.TransactionIsolationLevel | undefined;
},
numRetries = 1
): Promise<R> {
let result: Promise<R> | null = null;
for (let i = 0; i < numRetries; i++) {
try {
result = this.$transaction(fn, options);
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
//TODO?
} else {
throw e;
}
}
if (result != null) {
return result;
}
}
throw new Error(
'No result in transaction after maximum number of retries.'
);
}
}
Because in our services we now expect the PrismaClientWithCustomTransaction interface, the auto injecting of NestJs wont work anymore and we have to provide PrismaService using a token:
providers: [
{
provide: 'PRISMA_SERVICE_TOKEN',
useClass: PrismaService,
},
],
exportt class UserService{
constructor(#Inject('PRISMA_SERVICE_TOKEN') private readonly prisma: PrismaClientWithCustomTransaction){}
}
Alright so now we can do the following:
#Injectable()
export class OrdersService {
constructor( #Inject('PRISMA_SERVICE_TOKEN')
private readonly prisma: PrismaClientWithCustomTransaction, ...) {}
async someFn() {
return await this.prisma.interactiveTransaction(
async (client) => {
//You can still use client directly, if you dont need nested transaction logic
return client.userEntity.create(...)
//Or create services for nested usage
const transactionalClient = new TransactionalPrismaClient(client);
const userService = new UserService(transactionalClient);
return userService.createUser(...);
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead }
);
}
}
And if you need the $on, $connect, $disconnect, $use, you can of course still inject the original PrismaService with its regular interface.
Related
I have already a mock class implemented as a part of my previous work, which I want to provide as a part of my Jest Unit Testing.
Let me explain in code, that's better:
My Controller code:
export class VpcController {
constructor(private readonly vpcService: VpcService) {}
#Get
list() {
return this.vpcService.list();
}
}
My controller Jest Unit Test:
class VpcServiceMockFactory {
private list() {
return jest.fn().mockResolvedValue([TEMPLATE_VPC]);
}
getMock() {
const repoService: RepositoryService = new RepositoryService();
const vpsServiceMock: VpcServiceMock = new VpcServiceMock(repoService);
vpsServiceMock.create(TEMPLATE_VPC.name);
// This works
// return {
// list: this.list(),
// }
// This does not work
return {
list: vpsServiceMock.list(),
get: vpsServiceMock.get(TEMPLATE_VPC.id),
// create: vpsServiceMock.create('new-vpc'),
// update: vpsServiceMock.update(TEMPLATE_VPC.id, 'updated-name'),
// delete: vpsServiceMock.delete(TEMPLATE_VPC.id),
}
}
}
describe('VpcControllerTest', () => {
let controller: VpcController;
let spyService: VpcService;
beforeEach(async () => {
// Mock Services
const MockVpcServiceProvider = {
provide: VpcService,
useFactory: () => new VpcServiceMockFactory().getMock()
}
// Class-unter-test instantiation
const module: TestingModule = await Test.createTestingModule({
controllers: [VpcController],
providers: [VpcService, MockVpcServiceProvider],
}).compile()
// Get the instance handlers
controller = module.get<VpcController>(VpcController);
spyService = module.get<VpcService>(VpcService);
});
it('Get collection of VPCs', async () => {
// Execute the method
const result = await controller.list();
// Assertion
expect(spyService.list).toHaveBeenCalled();
expect(result.length).toBe(1);
expect(result[0].name).toBe('zopa');
});
}
My VpcServiceMock class:
export class VpcServiceMock {
constructor(private repository: RepositoryService) {}
list() {
return this.repository.list<VpcModel>(VPC);
}
}
My RepositoryService class:
async list<T>(type: VPC): Promise<T[]> {
return <T[]>this.aSimpleJsonObject[type];
}
However when I am running it, it is showing this error:
● VpcControllerTest › Test-2: Get collection of VPCs
TypeError: this.vpcService.list is not a function
38 | #ApiForbiddenResponse({ description: 'Unauthorized Request' })
39 | list() {
> 40 | return this.vpcService.list();
| ^
41 | }
42 |
43 | #Get(':id')
at VpcController.list (src/vpc/vpc.controller.ts:40:28)
at Object.<anonymous> (src/vpc/vpc.controller.spec.ts:87:37)
So the only way I can make it work:
If I provide a mock implementation in the Jest Unit test's VpcServiceMockFactory class (like I showed in the commented out code there as // This works).
Definitely I am missing something here, which I am not able to figure out.
Got my issue.
Basically I missed the jest.fn().mockResolvedValue.
Modified to make it work:
class VpcServiceMockFactory {
async getMock() {
const repoService: RepositoryService = new RepositoryService();
const vpcServiceMock: VpcServiceMock = new VpcServiceMock(repoService);
await vpcServiceMock.create(TEMPLATE_VPC.name); // repository store initialization
return {
list: jest.fn().mockResolvedValue(vpcServiceMock.list()),
get: jest.fn().mockResolvedValue(vpcServiceMock.get(TEMPLATE_VPC.id)),
create: jest.fn().mockResolvedValue(vpcServiceMock.create('new-vpc')),
update: jest.fn().mockResolvedValue(vpcServiceMock.update(TEMPLATE_VPC.id, 'updated-name')),
delete: jest.fn().mockResolvedValue(vpcServiceMock.delete(TEMPLATE_VPC.id)),
}
}
}
[SOLVED] I'm pretty new to NestJS and trying to get my head around durable providers but i can't get them to work.
My scenario is that i have a service with some logic and two providers that implement the same interface to get some data. Depending on a custom header value i want to use Provider1 or Provider2 and the service itself does not have to know about the existing provider implementations.
Since i'm in a request scoped scenario but i know there are only 2 possible dependency-subtrees i want to use durable providers that the dependencies are not newly initialised for each request but reused instead.
I set up the ContextIdStrategy as described in the official docs and it is executed on each request but i miss the part how to connect my provider implementations with the ContextSubtreeIds created in the ContextIdStrategy.
Interface:
export abstract class ITest {
abstract getData(): string;
}
Implementations:
export class Test1Provider implements ITest {
getData() {
return "TEST1";
}
}
export class Test2Provider implements ITest {
getData() {
return "TEST2";
}
}
Service:
#Injectable()
export class AppService {
constructor(private readonly testProvider: ITest) {}
getHello(): string {
return this.testProvider.getData();
}
}
Controller:
#Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
#Get()
getData(): string {
return this.appService.getData();
}
}
ContextIdStrategy:
const providers = new Map<string, ContextId>([
["provider1", ContextIdFactory.create()],
["provider2", ContextIdFactory.create()],
]);
export class AggregateByProviderContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const providerId = request.headers["x-provider-id"] as string;
let providerSubTreeId: ContextId;
if (providerId == "provider1") {
providerSubTreeId = providers["provider1"];
} else if (providerId == "provider2") {
providerSubTreeId = providers["provider2"];
} else {
throw Error(`x-provider-id ${providerId} not supported`);
}
// If tree is not durable, return the original "contextId" object
return (info: HostComponentInfo) =>
info.isTreeDurable ? providerSubTreeId : contextId;
}
}
Main:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
ContextIdFactory.apply(new AggregateByProviderContextIdStrategy());
await app.listen(3000);
}
bootstrap();
Module:
#Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: ITest,
useFactory: () => {
// THIS IS THE MISSING PIECE.
// Return either Test1Provider or Test2Provider based on the ContextSubtreeId
// which is created by the ContextIdStrategy
return new Test1Provider();
},
},
AppService,
],
})
export class AppModule {}
The missing part was a modification of the ContextIdStrategy return statement:
return {
resolve: (info: HostComponentInfo) => {
const context = info.isTreeDurable ? providerSubTreeId : contextId;
return context;
},
payload: { providerId },
}
after that change, the request object can be injected in the module and where it will only contain the providerId property and based on that, the useFactory statement can return different implementations
I'm trying to package my own AuthGuard for use in other projects and need to pass it a string before use.
Because I saw the Passport auth guard use a function that wrapped around a new class I've done the same...
export const AnchorAuthGuard = (rpc?: string): Type<CanActivate> => {
class AuthGuard implements CanActivate {
rpc = rpc || "https://eos.greymass.com";
async canActivate(context: ExecutionContext): Promise<boolean> {
const [req] = context.getArgs();
const { body } = req as { body: ProofPayload };
const proof = IdentityProof.from(body.proof);
const client = new APIClient({
provider: new AxiosAPIProvider(this.rpc),
});
const accountName = proof.signer.actor;
const account = await client.v1.chain.get_account(accountName);
const auth = account.getPermission(proof.signer.permission).required_auth;
const valid = proof.verify(auth, account.head_block_time);
if (valid) {
req.anchor = {
account: {
actor: proof.signer.actor.toString(),
permission: proof.signer.permission.toString(),
},
object: account.toJSON(),
};
return true;
} else {
return false;
}
}
}
return AuthGuard;
};
However, now that I've packaged this up and extending the Guard with extends for some more functionality in a projhect I'm consuming the library in I'm having trouble figuring out how to enter the rpc parameter via configService from the ConfigModule and now feel like I'm not using the best practices here and that there's a better way from the start.
Any ideas?
I am not sure if I understood you correctly but to modify AuthGuard you must extend AuthGuard class and write over canActivate method.
#Injectable()
export class LoginGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector, private config: ConfigService) {
super();
}
canActivate(context: ExecutionContext) {
return super.canActivate(context); // this is necessary due to possibly returning `boolean | Promise<boolean> | Observable<boolean>
}
}
I'm trying to pass my service to an instance of a class that I pass to a method decorator.
Here's the service:
#Injectable()
export class EntryService {
constructor(
#InjectRepository(EntryEntity)
private readonly entryRepository: Repository<EntryEntity>,
#InjectRepository(ImageEntity)
private readonly imageRepository: Repository<ImageEntity>,
private readonly awsService: AwsService,
private readonly connection: Connection,
private readonly categoriesService: CategoriesService,
private readonly cacheService: CacheService,
private readonly usersService: UserService,
private readonly imagesService: ImagesService,
private readonly notificationService: NotificationsService,
) {}
#RecordEntryOperation(new CreateOperation(this))
public async create(createEntryDto: CreateEntryBodyDto): Promise<Entry> {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.commitTransaction();
// more code
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
}
The thing here is that I need to use EntryService inside that class I pass to the RecordEntryOperation decorator.
The decorator (not fully implemented yet):
export const RecordEntryOperation = (operation: Operation) => {
return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => {
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
const response = await original.apply(this, args);
console.log(`operation.execute()`, await operation.execute());
return response;
};
};
};
The CreateOperation class looks like this (not fully implemented yet):
export class CreateOperation extends Operation {
constructor(public entryService: EntryService) { super(); }
public async execute(): Promise<any> {
return this.entryService.someEntryServiceOperation();
}
}
The error I'm getting reads as follows:
Argument of type 'typeof globalThis' is not assignable to parameter of type 'EntryService'.
Type 'typeof globalThis' is missing the following properties from type 'EntryService': entryRepository, imageRepository, awsService, and 53 more.
I don't fully understand what this error is about. I suspect that it means that the this passed to the CreateOperation class does not have all these dependencies injected into the service by the dependency injector.
I tried different things, but to no avail. Seems like I don't completely understand what is going on.
Any ideas?
What would be the right way to structure the code then?
The problem is the following line:
#RecordEntryOperation(new CreateOperation(this))
this does not refer to the instance of EntryService as you might expect, instead it refers to the globalThis (that this actually refers to the current module), thus the error. What you could do is to change your Operation-class a bit and pass the entryService to the execute method.
export class CreateOperation extends Operation {
constructor() { super(); }
public async execute(entryService: EntryService): Promise<any> {
return entryService.someEntryServiceOperation();
}
}
Then you can do the following in your decorator:
export const RecordEntryOperation = (OperationType: typeof CreateOperation) => {
return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => {
const operation = new OperationType();
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
const response = await original.apply(this, args);
console.log(`operation.execute()`, await operation.execute(this));
return response;
};
};
};
Then use it with:
#RecordEntryOperation(CreateOperation)
public async create(createEntryDto: CreateEntryBodyDto): Promise<Entry> { .. }
Bookstore exercise. A Book can be assigned a single Genre
Trying to assign a new book with the genreId supplied in the DTO.
BookRepository
#EntityRepository(Book)
export class BookRepository extends Repository<Book> {
constructor(
private genreService: GenresService,
) {
super();
}
async createBook(createBookDto: CreateBookDto): Promise<Book> {
const genre = await this.genreService.getOne(createBookDto.genreId);
const newBook = this.create({
...createBookDto,
genre,
});
return newBook.save();
}
}
GenreService
#Injectable()
export class GenresService {
constructor(
#InjectRepository(GenreRepository) private readonly genreRepository: GenreRepository,
) {}
async getOne(id: number): Promise<Genre> {
return this.getById(id);
}
private async getById(id: number): Promise<Genre> {
const found = await this.genreRepository.findOne(id);
if (!found) {
throw new NotFoundException(`Genre with id ${id} not found.`);
}
return found;
}
}
BookRepository and GenreRepository are imported together by the BookstoreModule, like so:
imports: [
TypeOrmModule.forFeature([
GenreRepository,
BookRepository,
AuthorRepository,
]),
// ...etc
},
NestJS spits out the following error:
[ExceptionsHandler] this.genreService.getOne is not a function +1045981ms
TypeError: this.genreService.getOne is not a function
at BookRepository.createBook (/Users/artur/Code/Sandbox/books-nest/dist/bookstore/books/book.repository.js:21:47)
at BooksService.createBook (/Users/artur/Code/Sandbox/books-nest/dist/bookstore/books/books.service.js:29:36)
at BooksController.create (/Users/artur/Code/Sandbox/books-nest/dist/bookstore/books/books.controller.js:31:33)
Tried to inject GenreRepository into theBookRepository`
constructor(
#InjectRepository(GenreRepository) private genreRepository: GenreRepository,
){
super();
}
and .findOne() from there, the error was:
No metadata for "2" was found. +26884ms
EntityMetadataNotFound: No metadata for "2" was found.
(2 being the id for genreId)
Not sure if my approach to saving is correct. Maybe the idea of finding the genre from within BookRepository is wrong and it should be solved in a different way. If so, how?
To me the createBook method should be lying within the BookService, BookService needs to inject GenreService and then you should call the genreService.getOne(createBookDto.genreId) from within the BookService.
It makes more sense to let the services handle the business logic / orchestration of the data flow imo.
I'd suggest to give it a try try with the following code:
BookService
#Injectable()
export class BookService {
constructor(
#InjectRepository(BookRepository) private readonly bookRepository: BookRepository,
private genreService: GenreService // <= here you go, you inject the genreService into the bookService to take advantage of its methods
) {}
async getOne(id: number): Promise<Genre> {
return this.getById(id);
}
async createBook(createBookDto: CreateBookDto): Promise<Book> {
const genre = await this.genreService.getOne(createBookDto.genreId);
const newBook = this.create({
...createBookDto,
genre,
});
return newBook.save();
}
private async getById(id: number): Promise<Genre> {
const found = await this.bookRepository.findOne(id);
if (!found) {
throw new NotFoundException(`Book with id ${id} not found.`);
}
return found;
}
}
BookRepository
#EntityRepository(Book)
export class BookRepository extends Repository<Book> {}
GenresService
#Injectable()
export class GenresService {
constructor(
#InjectRepository(GenreRepository) private readonly genreRepository: GenreRepository,
) {}
async getOne(id: number): Promise<Genre> {
return this.getById(id);
}
private async getById(id: number): Promise<Genre> {
const found = await this.genreRepository.findOne(id);
if (!found) {
throw new NotFoundException(`Genre with id ${id} not found.`);
}
return found;
}
}
GenreRepository
#EntityRepository(Genre)
export class GenreRepository extends Repository<Genre> {}
Edit DTO comment
CreateBookDto
import { Exclude, Expose } from 'class-transformer';
import { IsNumber, IsString, IsDate } from 'class-validator';
#Exclude()
export class CreateBookDto {
#Expose()
#IsNumber()
genreId: number;
#Expose()
#IsString()
title: string;
#Expose()
#IsString()
author: string;
#Expose()
#IsDate()
date_of_publication: Date;
// rest of informations....
}
Note that we're using class-transformer along with class-validator in order to instantiate and validate a proper CreateBookDto. CreateBookDto can be instantiated at the controller level if you're using validation pipe
Let me know if it helps :)