Nestjs extend/combine decorators? - node.js

I have simple custom decorator:
export const User: () => ParameterDecorator = createParamDecorator(
(data: any, req): UserIdentity => {
const user = getUser(req);
return user;
},
);
And now, I need to validate if we have email in user object.
The problem is that I can't update my current decorator.
Could I extend my current decorator?
Create a new decorator based on the previous one or create a new decorator and combine it?

Yes, you can do "decorator composition" with Nest, but this might not be a perfect solution for your case, depending on what you intend to do when user has no email property.
As per the example from the documentation:
import { applyDecorators } from '#nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized"' }),
);
}
In this example, Auth is a decorator that can be used to combine all the one passed in applyDecorators.
Thus, I'd recommend extending your decorator using a pipe.
As stated by the documentation:
Nest treats custom param decorators in the same fashion as the built-in ones (#Body(), #Param() and #Query()). This means that pipes are executed for the custom annotated parameters as well (in our examples, the user argument). Moreover, you can apply the pipe directly to the custom decorator:
#Get()
async findOne(#User(new ValidationPipe()) user: UserEntity) {
console.log(user);
}
In this example, User is a custom parameter decorator. And ValidationPipe is passed, but you can imagine passing any pipe.

Related

How to get method and its metadata from NestJS provider?

I'm making a NestJS wrapper for Typegoose because the existing one is complete deprecated and has one critical drawback that I want to fix in my implementation.
Problem: there is #EventTrackerFor(schema: AnyClass) that takes Typegoose class. It's implemented like this:
export const EventTrackerFor = (schema: AnyClass) =>
applyDecorators(Injectable, SetMetadata('tracker-for', schema.name));
Also, there are #Pre(eventName: PreEvents) and Post(eventName: PostEvents) decorators:
export const Post = (eventName: PreEvents) => SetMetadata('post', eventName);
export const Pre = (eventName: PostEvents) => SetMetadata('pre', eventName);
And as a result, library user will do it like that:
#EventTrackerFor(User)
class UserEventTracker {
constructor(private readonly anyService: AnyService) {}
#Pre(PreEvents.SAVE)
#Post(PostEvents.SAVE)
logOnAndAfterCreate() {
console.log('user created')
}
}
// ------------------------ Any module
#Module({
imports: [MyModule.forFeature([ {schema: User} ])],
providers: [UserEventTracker]
})
class AnyModule {}
I need to get value from #EventTrackerFor somehow and methods of the provider, which are decorated with #Pre() and #Post decorators, including values that passed inside them.
I was looking for a clue in different packages like #nestjs/bull, which has similar logics inside, but they have so much code, so I could not understand how do they do it.
Project repository: https://github.com/GrapeoffJS/kindagoose

Custom param decorator which transform param to database entity

In Laravel (php) has route /article/:article, and in controller method I get the model:
function getArticle(ArticleModel $article) {...}
How to make this in NestJS?
My controller:
#Controller('/articles')
export class ArticlesController {
#Get('/:article/edit')
editArticle(#Param('article') articleId: number) {...}
}
How to transform #Param('article') to custom decorator #ArticleParam() which will return my Article entity by id in request?
You can implement a custom pipe that injects a TypeORM repository and returns the database entity when prompted with an ID, something like this:
#Injectable()
export class ArticlePipe implements PipeTransform {
constructor(#InjectRepository(Article) private repository: Repository<Article>) {}
transform(value: id, metadata: ArgumentsMetadata): Promise<Article|null> {
return this.repository.findOneBy({ id });
}
}
Then use it like
#Get('/article/:id')
getArticle(#Param('id', ArticlePipe) article: Article) { ... }
You just need to make sure to use the pipe only on modules that provide the Article EntityRepository.
Then, if you need the specific #ArticleParam, it should be like this:
export function ArticleParam = () => applyDecorators(
Param(ArticlePipe)
)

NestJS: wrap 3rd party decorator & setMetaData in order to use Reflector to check annotation

I am using NestJS to implement a project with TypeScript.
I am using a 3rd party library which provide a decorator called Protected, I can use the decorator to annotate my controller method:
#Protected()
myFunc(){
...
}
I have a Guard, in which I want to check whether the annotation is there inside my MyGuard class. I tried:
#Injectable()
export class MyGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): Promise<boolean> {
const value = this.reflector.get('Protected', context.getHandler());
console.log(`VALUE: ${metaValue}`);
...
}
}
The log message shows me VALUE: undefined. I read the NestJS doc, there is an example of using setMetadata() and then in Guard method use the metadata key to retrieve metadata to check if the annotation is there. However, this is a 3rd party decorator, there is no information for me whether they use any metadata key.
So, I come up with a workaround, I create my own custom decorator which wraps the 3rd party Protected decorator & use my decorator instead on controller method:
import {Protected} from '3rd-party-lib'
import { SetMetadata } from '#nestjs/common';
export const MyProtected = () => {
Protected();
SetMetadata(IS_PROTECTED, true);
}
But now, the annotation on controller method raises error:
/**
ERROR: Unable to resolve signature of method decorator when called as an expression.
This expression is not callable.
Type 'void' has no call signatures.ts(1241)
**/
#MyProtected()
myFunc(){
...
}
My questions:
Is there a way to use Reflector to check whether the 3rd party annotation is presented in the controller method inside MyGuard ?
If there is no way without using setMetadata & since I don't know what metadata key to check due to 3rd party library. How can I setMetadata myself in my custom decorator in order to achieve what I need?
Why not use Nest's applyDecorators so you can compose the decorator out of several decorators?
export const MyProtected = () => applyDecorators(
Protected(),
SetMetadata(IS_PROTECTED, true)
);

How to globally apply Swagger UI's #ApiBearerAuth() instead of applying for each controller

I am working on a nestJS based api, and I am using Swagger UI documentation. I want keep the functionality of #ApiBearerAuth() for all my controllers, however I wish to have this as a global feature. That way every time I create new routes or endpoints it will already be covered.
From the https://docs.nestjs.com/openapi/security documentation:
#ApiBearerAuth()
#Controller('cats')
export class CatsController {}
This is what I am following now, but is there a way to set this globally?
Yes, in your DocumentBuilder you may add:
.addSecurity('ApiKeyAuth', {
type: 'apiKey',
in: 'header',
name: 'Authorization',
})
.addSecurityRequirements('ApiKeyAuth')
In the source code of #nestjs/swagger its visible that addBearerAuth() sets the security name to "bearer" so it is possible to use addBearerAuth() and addSecurityRequirements('bearer'):
const config = new DocumentBuilder()
...
.addBearerAuth()
.addSecurityRequirements('bearer')
.build();
This is a little hack, because I use ApiTags in every Controller so that I create a new decorator
import { applyDecorators } from '#nestjs/common';
export function ApiTagsAndBearer(...tags: string[]) {
return applyDecorators(
ApiBearerAuth(), //
ApiTags(...tags),
);
}

Automatically parse query parameter to object when defined in NestJS

I am writing a NestJS application. Some of the endpoints support sorting e.g. http://127.0.0.1:3000/api/v1/members?sort=-id&take=100 Which means sort by id descending.
This parameter arrives as a #Query parameter and is passed to my service. This service transforms it into an object which is used by TypeORM:
{
id: 'DESC'
}
I don't want to call this conversion method manually every time I need sorting.
I've tried an intercepter but this one could not easily change the request parameters into the desired object.
A pipe worked but then I still need to add #Query(new SortPipe()) for every endpoint definition.
Another option is in the repository itself. The NestJS documentation is very well written, but misses guidance in where to put what.
Is there someone who had a similar issue with converting Query parameters before they are used in NestJS, and can explain what approach is the best within NestJS?
This question might look like an opinion based question, however I am looking for the way it is supposed to be done with the NestJS philosophy in mind.
Pipes are probably the easiest way to accomplish this. Instead of adding your pipe for every endpoint definition you can add a global pipe that will be called on every endpoint. In your main.ts:
async function bootstrap() {
...
app.useGlobalPipes(new SortPipe());
...
}
You can then create a pipe like this:
import { PipeTransform, Injectable, ArgumentMetadata } from '#nestjs/common';
#Injectable()
export class SortPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const { type } = metadata;
// Make sure to only run your logic on queries
if (type === 'query') return this.transformQuery(value);
return value;
}
transformQuery(query: any) {
if (typeof query !== 'object' || !value) return query;
const { sort } = query;
if (sort) query.sort = convertForTypeOrm(sort);
return query;
}
}
If you do not want sort value on ALL endpoints to be automatically converted, you can pass custom parameter to #Query(), for example #Query('sort'). And then:
transform(value: any, metadata: ArgumentMetadata) {
const { type, data } = metadata;
// Make sure to only run your logic on queries when 'sort' is supplied
if (type === 'query' && data === 'sort') return this.transformQuery(value);
return value;
}

Resources