Skip to content

Commit fbf47a2

Browse files
feature(common) add class serializer interceptor
1 parent 643e84c commit fbf47a2

6 files changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Options to be passed during transformation.
3+
* @see https://github.com/typestack/class-transformer
4+
*/
5+
export interface ClassTransformOptions {
6+
/**
7+
* Exclusion strategy. By default exposeAll is used, which means that it will expose all properties are transformed
8+
* by default.
9+
*/
10+
strategy?: 'excludeAll' | 'exposeAll';
11+
/**
12+
* Only properties with given groups gonna be transformed.
13+
*/
14+
groups?: string[];
15+
/**
16+
* Only properties with "since" > version < "until" gonna be transformed.
17+
*/
18+
version?: number;
19+
/**
20+
* Excludes properties with the given prefixes. For example, if you mark your private properties with "_" and "__"
21+
* you can set this option's value to ["_", "__"] and all private properties will be skipped.
22+
* This works only for "exposeAll" strategy.
23+
*/
24+
excludePrefixes?: string[];
25+
/**
26+
* If set to true then class transformer will ignore all @Expose and @Exclude decorators and what inside them.
27+
* This option is useful if you want to kinda clone your object but do not apply decorators affects.
28+
*/
29+
ignoreDecorators?: boolean;
30+
/**
31+
* Target maps allows to set a Types of the transforming object without using @Type decorator.
32+
* This is useful when you are transforming external classes, or if you already have type metadata for
33+
* objects and you don't want to set it up again.
34+
*/
35+
targetMaps?: any[];
36+
/**
37+
* If set to true then class transformer will perform a circular check. (circular check is turned off by default)
38+
* This option is useful when you know for sure that your types might have a circular dependency.
39+
*/
40+
enableCircularCheck?: boolean;
41+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const CLASS_SERIALIZER_OPTIONS = 'class_serializer:options';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2+
import { Observable } from 'rxjs';
3+
import { map } from 'rxjs/operators';
4+
import { Inject } from '../decorators/core';
5+
import { ClassTransformOptions } from '../interfaces/external/class-transform-options.interface';
6+
import { loadPackage } from '../utils/load-package.util';
7+
import { isObject } from '../utils/shared.utils';
8+
import { CLASS_SERIALIZER_OPTIONS } from './class-serializer.constants';
9+
10+
let classTransformer: any = {};
11+
12+
export interface PlainLiteralObject {
13+
[key: string]: any;
14+
}
15+
16+
// NOTE (external)
17+
// We need to deduplicate them here due to the circular dependency
18+
// between core and common packages
19+
const REFLECTOR = 'Reflector';
20+
21+
@Injectable()
22+
export class ClassSerializerInterceptor implements NestInterceptor {
23+
constructor(@Inject(REFLECTOR) private readonly reflector: any) {
24+
const loadPkg = pkg => loadPackage(pkg, 'ClassSerializerInterceptor');
25+
classTransformer = loadPkg('class-transformer');
26+
}
27+
28+
intercept(
29+
context: ExecutionContext,
30+
call$: Observable<any>,
31+
): Observable<any> {
32+
const options = this.getContextOptions(context);
33+
return call$.pipe(
34+
map((res: PlainLiteralObject | Array<PlainLiteralObject>) =>
35+
this.serialize(res, options),
36+
),
37+
);
38+
}
39+
40+
serialize(
41+
response: PlainLiteralObject | Array<PlainLiteralObject>,
42+
options: ClassTransformOptions,
43+
): PlainLiteralObject | PlainLiteralObject[] {
44+
const isArray = Array.isArray(response);
45+
if (!isObject(response) && !isArray) {
46+
return response;
47+
}
48+
return isArray
49+
? (response as PlainLiteralObject[]).map(item =>
50+
this.transformToPlain(item, options),
51+
)
52+
: this.transformToPlain(response, options);
53+
}
54+
55+
transformToPlain(
56+
plainOrClass,
57+
options: ClassTransformOptions,
58+
): PlainLiteralObject {
59+
return plainOrClass && plainOrClass.constructor !== Object
60+
? classTransformer.classToPlain(plainOrClass, options)
61+
: plainOrClass;
62+
}
63+
64+
private getContextOptions(
65+
context: ExecutionContext,
66+
): ClassTransformOptions | undefined {
67+
return (
68+
this.reflectSerializeMetadata(context.getHandler()) ||
69+
this.reflectSerializeMetadata(context.getClass())
70+
);
71+
}
72+
73+
private reflectSerializeMetadata(obj): ClassTransformOptions | undefined {
74+
return this.reflector.get(CLASS_SERIALIZER_OPTIONS, obj);
75+
}
76+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './serialize-options.decorator';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ReflectMetadata } from '../../decorators';
2+
import { ClassTransformOptions } from '../../interfaces/external/class-transform-options.interface';
3+
import { CLASS_SERIALIZER_OPTIONS } from '../class-serializer.constants';
4+
5+
export const SerializeOptions = (options: ClassTransformOptions) =>
6+
ReflectMetadata(CLASS_SERIALIZER_OPTIONS, options);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './class-serializer.interceptors';
2+
export * from './decorators';

0 commit comments

Comments
 (0)