Reputation: 3714
In NestJS route, I want to serialize reponse based on request (user's role). This means that I need to pass option groups: []
into transformToPlain
method in ClassSerializerInterceptor
so class-transformer can properly return filtered format:
This is original interceptor source: https://github.com/nestjs/nest/blob/master/packages/common/serializer/class-serializer.interceptor.ts
I just extended this class and changed intercept
method to also include group
option based on request:
export class CustomClassSerializerInterceptor extends ClassSerializerInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// @ts-ignore
const contextOptions = this.getContextOptions(context);
const options = {
...this.defaultOptions,
...contextOptions,
groups: request.user.roles // Pseudo
};
return next
.handle()
.pipe(map((res: PlainLiteralObject | Array<PlainLiteralObject>) => this.serialize(res, options)));
}
}
My entity:
export class Content extends BaseDatabaseEntity {
@Column()
@Transform((type) => EContentType[type])
type: EContentType;
@Expose({ groups: ["MODERATOR", "ADMIN"] })
@Column({ default: "[]", type: "json" })
data: TContentDataColumn[];
}
Is this proper way to do it? For example this.getContextOptions
method is private in original source, so I need to do ts-ignore here, to override default intended privacy of class which seems to me as big no-no.
Am I even supposted to transform API response based on user's role, or it is anti-pattern?
Upvotes: 4
Views: 2615
Reputation: 183
Yes, extending the ClassSerializerInterceptor
is an adequate way to add more functionality into it.
As @Baterka have pointed out, this always worked but triggered compiler warnings.
Since then the source code has been changed so previously private fields like defaultOptions
and getContextOptions
are now protected instead, meaning they are available to derived classes.
Here is a working example that's fully compatible with NestJS version 8.0 or later:
roles-serializer.interceptor.ts
import { CallHandler, ClassSerializerInterceptor, ExecutionContext, Injectable, PlainLiteralObject } from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class RolesSerializerInterceptor extends ClassSerializerInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<any> {
const userRoles = context.switchToHttp().getRequest().user?.roles ?? [];
const contextOptions = this.getContextOptions(context);
const options = {
...this.defaultOptions,
...contextOptions,
groups: userRoles
};
return next
.handle()
.pipe(
map((res: PlainLiteralObject | PlainLiteralObject[]) =>
this.serialize(res, options)
)
);
}
}
Upvotes: 4
Reputation: 70490
You can make use of the @SerializationOptions()
decorator to pass extra options based on what route is being triggered. As it seems you need to do this dynamically, you could go so far as to use the Reflect
namespace and set the metadata each request. The metadata token is 'class_serializer:options'
, so you could do something like
Reflect.defineMetadata(
'class_serializer:options',
{ groups: req.user.roles },
Class.prototype,
'method'
);
It's not necessarily pretty, but it could work
Upvotes: 3