Skip to content

Commit f4fcd64

Browse files
committed
feat(plugin) add support for @expose() and @exclude() decorators
1 parent 40042d4 commit f4fcd64

7 files changed

+634
-15
lines changed

lib/plugin/merge-options.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface PluginOptions {
44
dtoFileNameSuffix?: string | string[];
55
controllerFileNameSuffix?: string | string[];
66
classValidatorShim?: boolean;
7+
classTransformerShim?: boolean | 'exclusive';
78
dtoKeyOfComment?: string;
89
controllerKeyOfComment?: string;
910
introspectComments?: boolean;
@@ -17,6 +18,7 @@ const defaultOptions: PluginOptions = {
1718
dtoFileNameSuffix: ['.dto.ts', '.entity.ts'],
1819
controllerFileNameSuffix: ['.controller.ts'],
1920
classValidatorShim: true,
21+
classTransformerShim: false,
2022
dtoKeyOfComment: 'description',
2123
controllerKeyOfComment: 'description',
2224
introspectComments: false,

lib/plugin/visitors/model-class.visitor.ts

+33-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { compact, flatten, head } from 'lodash';
22
import { posix } from 'path';
33
import * as ts from 'typescript';
44
import { factory, PropertyAssignment } from 'typescript';
5-
import { ApiHideProperty } from '../../decorators';
5+
import { ApiHideProperty, ApiProperty } from '../../decorators';
66
import { PluginOptions } from '../merge-options';
77
import { METADATA_FACTORY_NAME } from '../plugin-constants';
88
import { pluginDebugLogger } from '../plugin-debug-logger';
@@ -155,23 +155,43 @@ export class ModelClassVisitor extends AbstractFileVisitor {
155155
sourceFile: ts.SourceFile,
156156
metadata: ClassMetadata
157157
) {
158+
const isPropertyStatic = (node.modifiers || []).some(
159+
(modifier: ts.Modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword
160+
);
161+
if (isPropertyStatic) {
162+
return node;
163+
}
164+
158165
const decorators = ts.canHaveDecorators(node) && ts.getDecorators(node);
159166

160-
const hidePropertyDecorator = getDecoratorOrUndefinedByNames(
161-
[ApiHideProperty.name],
167+
const classTransformerShim = options.classTransformerShim;
168+
169+
const hidePropertyDecoratorExists = getDecoratorOrUndefinedByNames(
170+
classTransformerShim
171+
? [ApiHideProperty.name, 'Exclude']
172+
: [ApiHideProperty.name],
162173
decorators,
163174
factory
164175
);
165-
if (hidePropertyDecorator) {
166-
return node;
167-
}
168176

169-
const isPropertyStatic = (node.modifiers || []).some(
170-
(modifier: ts.Modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword
177+
const annotatePropertyDecoratorExists = getDecoratorOrUndefinedByNames(
178+
classTransformerShim ? [ApiProperty.name, 'Expose'] : [ApiProperty.name],
179+
decorators,
180+
factory
171181
);
172-
if (isPropertyStatic) {
182+
183+
if (
184+
!annotatePropertyDecoratorExists &&
185+
(hidePropertyDecoratorExists || classTransformerShim === 'exclusive')
186+
) {
187+
return node;
188+
} else if (annotatePropertyDecoratorExists && hidePropertyDecoratorExists) {
189+
pluginDebugLogger.debug(
190+
`"${node.parent.name.getText()}->${node.name.getText()}" has conflicting decorators, excluding as @ApiHideProperty() takes priority.`
191+
);
173192
return node;
174193
}
194+
175195
try {
176196
this.inspectPropertyDeclaration(
177197
ctx.factory,
@@ -695,7 +715,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
695715
return result;
696716
}
697717

698-
const clonedMinLength = this.clonePrimitiveLiteral(factory, minLength) ?? minLength;
718+
const clonedMinLength =
719+
this.clonePrimitiveLiteral(factory, minLength) ?? minLength;
699720
if (clonedMinLength) {
700721
result.push(
701722
factory.createPropertyAssignment('minLength', clonedMinLength)
@@ -707,10 +728,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
707728
if (!canReferenceNode(maxLength, options)) {
708729
return result;
709730
}
710-
const clonedMaxLength = this.clonePrimitiveLiteral(
711-
factory,
712-
maxLength
713-
) ?? maxLength;
731+
const clonedMaxLength =
732+
this.clonePrimitiveLiteral(factory, maxLength) ?? maxLength;
714733
if (clonedMaxLength) {
715734
result.push(
716735
factory.createPropertyAssignment('maxLength', clonedMaxLength)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
export const createCatExcludeDtoText = `
2+
import { IsInt, IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator';
3+
4+
enum Status {
5+
ENABLED,
6+
DISABLED
7+
}
8+
9+
enum OneValueEnum {
10+
ONE
11+
}
12+
13+
interface Node {
14+
id: number;
15+
}
16+
17+
class OtherNode {
18+
id: number;
19+
}
20+
21+
export class CreateCatDto {
22+
@IsIn(['a', 'b'])
23+
isIn: string;
24+
@Matches(/^[+]?abc$/)
25+
pattern: string;
26+
name: string;
27+
@Min(0)
28+
@Max(10)
29+
age: number = 3;
30+
@IsPositive()
31+
positive: number = 5;
32+
@IsNegative()
33+
negative: number = -1;
34+
@Length(2)
35+
lengthMin: string;
36+
@Length(3, 5)
37+
lengthMinMax: string;
38+
tags: string[];
39+
status: Status = Status.ENABLED;
40+
status2?: Status;
41+
statusArr?: Status[];
42+
oneValueEnum?: OneValueEnum;
43+
oneValueEnumArr?: OneValueEnum[];
44+
45+
/** this is breed im comment */
46+
@ApiProperty({ description: "this is breed", type: String })
47+
@IsString()
48+
readonly breed?: string;
49+
50+
nodes: Node[];
51+
optionalBoolean?: boolean;
52+
date: Date;
53+
54+
twoDimensionPrimitives: string[][];
55+
twoDimensionNodes: OtherNode[][];
56+
57+
@ApiHideProperty()
58+
hidden: number;
59+
60+
@Exclude()
61+
excluded: number;
62+
63+
static staticProperty: string;
64+
}
65+
`;
66+
67+
export const createCatExcludeDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
68+
import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator';
69+
var Status;
70+
(function (Status) {
71+
Status[Status[\"ENABLED\"] = 0] = \"ENABLED\";
72+
Status[Status[\"DISABLED\"] = 1] = \"DISABLED\";
73+
})(Status || (Status = {}));
74+
var OneValueEnum;
75+
(function (OneValueEnum) {
76+
OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\";
77+
})(OneValueEnum || (OneValueEnum = {}));
78+
class OtherNode {
79+
static _OPENAPI_METADATA_FACTORY() {
80+
return { id: { required: true, type: () => Number } };
81+
}
82+
}
83+
export class CreateCatDto {
84+
constructor() {
85+
this.age = 3;
86+
this.positive = 5;
87+
this.negative = -1;
88+
this.status = Status.ENABLED;
89+
}
90+
static _OPENAPI_METADATA_FACTORY() {
91+
return { isIn: { required: true, type: () => String, enum: ['a', 'b'] }, pattern: { required: true, type: () => String, pattern: "/^[+]?abc$/" }, name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, positive: { required: true, type: () => Number, default: 5, minimum: 1 }, negative: { required: true, type: () => Number, default: -1, maximum: -1 }, lengthMin: { required: true, type: () => String, minLength: 2 }, lengthMinMax: { required: true, type: () => String, minLength: 3, maxLength: 5 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { required: false, type: () => String, title: "this is breed im comment" }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date }, twoDimensionPrimitives: { required: true, type: () => [[String]] }, twoDimensionNodes: { required: true, type: () => [[OtherNode]] } };
92+
}
93+
}
94+
__decorate([
95+
IsIn(['a', 'b'])
96+
], CreateCatDto.prototype, \"isIn\", void 0);
97+
__decorate([
98+
Matches(/^[+]?abc$/)
99+
], CreateCatDto.prototype, \"pattern\", void 0);
100+
__decorate([
101+
Min(0),
102+
Max(10)
103+
], CreateCatDto.prototype, \"age\", void 0);
104+
__decorate([
105+
IsPositive()
106+
], CreateCatDto.prototype, \"positive\", void 0);
107+
__decorate([
108+
IsNegative()
109+
], CreateCatDto.prototype, \"negative\", void 0);
110+
__decorate([
111+
Length(2)
112+
], CreateCatDto.prototype, \"lengthMin\", void 0);
113+
__decorate([
114+
Length(3, 5)
115+
], CreateCatDto.prototype, \"lengthMinMax\", void 0);
116+
__decorate([
117+
ApiProperty({ description: "this is breed", type: String }),
118+
IsString()
119+
], CreateCatDto.prototype, \"breed\", void 0);
120+
__decorate([
121+
ApiHideProperty()
122+
], CreateCatDto.prototype, \"hidden\", void 0);
123+
__decorate([
124+
Exclude()
125+
], CreateCatDto.prototype, "excluded", void 0);
126+
`;
127+
128+
export const createCatIgnoreExcludeDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
129+
import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator';
130+
var Status;
131+
(function (Status) {
132+
Status[Status[\"ENABLED\"] = 0] = \"ENABLED\";
133+
Status[Status[\"DISABLED\"] = 1] = \"DISABLED\";
134+
})(Status || (Status = {}));
135+
var OneValueEnum;
136+
(function (OneValueEnum) {
137+
OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\";
138+
})(OneValueEnum || (OneValueEnum = {}));
139+
class OtherNode {
140+
static _OPENAPI_METADATA_FACTORY() {
141+
return { id: { required: true, type: () => Number } };
142+
}
143+
}
144+
export class CreateCatDto {
145+
constructor() {
146+
this.age = 3;
147+
this.positive = 5;
148+
this.negative = -1;
149+
this.status = Status.ENABLED;
150+
}
151+
static _OPENAPI_METADATA_FACTORY() {
152+
return { isIn: { required: true, type: () => String, enum: ['a', 'b'] }, pattern: { required: true, type: () => String, pattern: "/^[+]?abc$/" }, name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, positive: { required: true, type: () => Number, default: 5, minimum: 1 }, negative: { required: true, type: () => Number, default: -1, maximum: -1 }, lengthMin: { required: true, type: () => String, minLength: 2 }, lengthMinMax: { required: true, type: () => String, minLength: 3, maxLength: 5 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { required: false, type: () => String, title: "this is breed im comment" }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date }, twoDimensionPrimitives: { required: true, type: () => [[String]] }, twoDimensionNodes: { required: true, type: () => [[OtherNode]] }, excluded: { required: true, type: () => Number } };
153+
}
154+
}
155+
__decorate([
156+
IsIn(['a', 'b'])
157+
], CreateCatDto.prototype, \"isIn\", void 0);
158+
__decorate([
159+
Matches(/^[+]?abc$/)
160+
], CreateCatDto.prototype, \"pattern\", void 0);
161+
__decorate([
162+
Min(0),
163+
Max(10)
164+
], CreateCatDto.prototype, \"age\", void 0);
165+
__decorate([
166+
IsPositive()
167+
], CreateCatDto.prototype, \"positive\", void 0);
168+
__decorate([
169+
IsNegative()
170+
], CreateCatDto.prototype, \"negative\", void 0);
171+
__decorate([
172+
Length(2)
173+
], CreateCatDto.prototype, \"lengthMin\", void 0);
174+
__decorate([
175+
Length(3, 5)
176+
], CreateCatDto.prototype, \"lengthMinMax\", void 0);
177+
__decorate([
178+
ApiProperty({ description: "this is breed", type: String }),
179+
IsString()
180+
], CreateCatDto.prototype, \"breed\", void 0);
181+
__decorate([
182+
ApiHideProperty()
183+
], CreateCatDto.prototype, \"hidden\", void 0);
184+
__decorate([
185+
Exclude()
186+
], CreateCatDto.prototype, "excluded", void 0);
187+
`;

0 commit comments

Comments
 (0)