Skip to content

Commit 876d17a

Browse files
authoredFeb 7, 2024
Merge pull request #2777 from kurt-west/master
feat(plugin) add support for @expose() and @exclude() decorators
2 parents 1d86dfd + f4fcd64 commit 876d17a

7 files changed

+630
-10
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

+29-9
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,
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+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
export const createCatExclusiveDtoText = `
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+
@Expose()
64+
exposed: number;
65+
66+
static staticProperty: string;
67+
}
68+
`;
69+
70+
export const createCatExclusiveDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
71+
import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator';
72+
var Status;
73+
(function (Status) {
74+
Status[Status[\"ENABLED\"] = 0] = \"ENABLED\";
75+
Status[Status[\"DISABLED\"] = 1] = \"DISABLED\";
76+
})(Status || (Status = {}));
77+
var OneValueEnum;
78+
(function (OneValueEnum) {
79+
OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\";
80+
})(OneValueEnum || (OneValueEnum = {}));
81+
class OtherNode {
82+
static _OPENAPI_METADATA_FACTORY() {
83+
return {};
84+
}
85+
}
86+
export class CreateCatDto {
87+
constructor() {
88+
this.age = 3;
89+
this.positive = 5;
90+
this.negative = -1;
91+
this.status = Status.ENABLED;
92+
}
93+
static _OPENAPI_METADATA_FACTORY() {
94+
return { breed: { required: false, type: () => String, title: "this is breed im comment" }, exposed: { required: true, type: () => Number } };
95+
}
96+
}
97+
__decorate([
98+
IsIn(['a', 'b'])
99+
], CreateCatDto.prototype, \"isIn\", void 0);
100+
__decorate([
101+
Matches(/^[+]?abc$/)
102+
], CreateCatDto.prototype, \"pattern\", void 0);
103+
__decorate([
104+
Min(0),
105+
Max(10)
106+
], CreateCatDto.prototype, \"age\", void 0);
107+
__decorate([
108+
IsPositive()
109+
], CreateCatDto.prototype, \"positive\", void 0);
110+
__decorate([
111+
IsNegative()
112+
], CreateCatDto.prototype, \"negative\", void 0);
113+
__decorate([
114+
Length(2)
115+
], CreateCatDto.prototype, \"lengthMin\", void 0);
116+
__decorate([
117+
Length(3, 5)
118+
], CreateCatDto.prototype, \"lengthMinMax\", void 0);
119+
__decorate([
120+
ApiProperty({ description: "this is breed", type: String }),
121+
IsString()
122+
], CreateCatDto.prototype, \"breed\", void 0);
123+
__decorate([
124+
ApiHideProperty()
125+
], CreateCatDto.prototype, \"hidden\", void 0);
126+
__decorate([
127+
Exclude()
128+
], CreateCatDto.prototype, "excluded", void 0);
129+
__decorate([
130+
Expose()
131+
], CreateCatDto.prototype, "exposed", void 0);
132+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
export const createCatPriorityDtoText = `
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+
@Expose()
58+
@ApiHideProperty()
59+
hidden: number;
60+
61+
@Exclude()
62+
excluded: number;
63+
64+
static staticProperty: string;
65+
}
66+
`;
67+
68+
export const createCatPriorityDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
69+
import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator';
70+
var Status;
71+
(function (Status) {
72+
Status[Status[\"ENABLED\"] = 0] = \"ENABLED\";
73+
Status[Status[\"DISABLED\"] = 1] = \"DISABLED\";
74+
})(Status || (Status = {}));
75+
var OneValueEnum;
76+
(function (OneValueEnum) {
77+
OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\";
78+
})(OneValueEnum || (OneValueEnum = {}));
79+
class OtherNode {
80+
static _OPENAPI_METADATA_FACTORY() {
81+
return { id: { required: true, type: () => Number } };
82+
}
83+
}
84+
export class CreateCatDto {
85+
constructor() {
86+
this.age = 3;
87+
this.positive = 5;
88+
this.negative = -1;
89+
this.status = Status.ENABLED;
90+
}
91+
static _OPENAPI_METADATA_FACTORY() {
92+
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]] } };
93+
}
94+
}
95+
__decorate([
96+
IsIn(['a', 'b'])
97+
], CreateCatDto.prototype, \"isIn\", void 0);
98+
__decorate([
99+
Matches(/^[+]?abc$/)
100+
], CreateCatDto.prototype, \"pattern\", void 0);
101+
__decorate([
102+
Min(0),
103+
Max(10)
104+
], CreateCatDto.prototype, \"age\", void 0);
105+
__decorate([
106+
IsPositive()
107+
], CreateCatDto.prototype, \"positive\", void 0);
108+
__decorate([
109+
IsNegative()
110+
], CreateCatDto.prototype, \"negative\", void 0);
111+
__decorate([
112+
Length(2)
113+
], CreateCatDto.prototype, \"lengthMin\", void 0);
114+
__decorate([
115+
Length(3, 5)
116+
], CreateCatDto.prototype, \"lengthMinMax\", void 0);
117+
__decorate([
118+
ApiProperty({ description: "this is breed", type: String }),
119+
IsString()
120+
], CreateCatDto.prototype, \"breed\", void 0);
121+
__decorate([
122+
Expose(),
123+
ApiHideProperty()
124+
], CreateCatDto.prototype, \"hidden\", void 0);
125+
__decorate([
126+
Exclude()
127+
], CreateCatDto.prototype, "excluded", void 0);
128+
`;

‎test/plugin/model-class-visitor.spec.ts

+148
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ import {
3434
parameterPropertyDtoText,
3535
parameterPropertyDtoTextTranspiled
3636
} from './fixtures/parameter-property.dto';
37+
import {
38+
createCatExcludeDtoText,
39+
createCatExcludeDtoTextTranspiled,
40+
createCatIgnoreExcludeDtoTextTranspiled
41+
} from './fixtures/create-cat-exclude.dto';
42+
import {
43+
createCatExclusiveDtoText,
44+
createCatExclusiveDtoTextTranspiled
45+
} from './fixtures/create-cat-exclusive.dto';
46+
import {
47+
createCatPriorityDtoText,
48+
createCatPriorityDtoTextTranspiled
49+
} from './fixtures/create-cat-priority.dto';
50+
import { pluginDebugLogger } from '../../lib/plugin/plugin-debug-logger';
3751

3852
describe('API model properties', () => {
3953
it('should add the metadata factory when no decorators exist, and generated propertyKey is title', () => {
@@ -275,4 +289,138 @@ describe('API model properties', () => {
275289
});
276290
expect(result.outputText).toEqual(parameterPropertyDtoTextTranspiled);
277291
});
292+
293+
it('should ignore Exclude decorator', () => {
294+
const options: ts.CompilerOptions = {
295+
module: ts.ModuleKind.ES2020,
296+
target: ts.ScriptTarget.ES2020,
297+
newLine: ts.NewLineKind.LineFeed,
298+
noEmitHelpers: true,
299+
experimentalDecorators: true,
300+
strict: true
301+
};
302+
const filename = 'create-cat-exclude.dto.ts';
303+
const fakeProgram = ts.createProgram([filename], options);
304+
305+
const result = ts.transpileModule(createCatExcludeDtoText, {
306+
compilerOptions: options,
307+
fileName: filename,
308+
transformers: {
309+
before: [
310+
before(
311+
{
312+
classValidatorShim: true,
313+
classTransformerShim: false,
314+
dtoKeyOfComment: 'title',
315+
introspectComments: true
316+
},
317+
fakeProgram
318+
)
319+
]
320+
}
321+
});
322+
expect(result.outputText).toEqual(createCatIgnoreExcludeDtoTextTranspiled);
323+
});
324+
325+
it('should hide properties decorated with the Exclude decorator', () => {
326+
const options: ts.CompilerOptions = {
327+
module: ts.ModuleKind.ES2020,
328+
target: ts.ScriptTarget.ES2020,
329+
newLine: ts.NewLineKind.LineFeed,
330+
noEmitHelpers: true,
331+
experimentalDecorators: true,
332+
strict: true
333+
};
334+
const filename = 'create-cat-exclude.dto.ts';
335+
const fakeProgram = ts.createProgram([filename], options);
336+
337+
const result = ts.transpileModule(createCatExcludeDtoText, {
338+
compilerOptions: options,
339+
fileName: filename,
340+
transformers: {
341+
before: [
342+
before(
343+
{
344+
classValidatorShim: true,
345+
classTransformerShim: true,
346+
dtoKeyOfComment: 'title',
347+
introspectComments: true
348+
},
349+
fakeProgram
350+
)
351+
]
352+
}
353+
});
354+
expect(result.outputText).toEqual(createCatExcludeDtoTextTranspiled);
355+
});
356+
357+
it('should hide a property with conflicting decorators', () => {
358+
const options: ts.CompilerOptions = {
359+
module: ts.ModuleKind.ES2020,
360+
target: ts.ScriptTarget.ES2020,
361+
newLine: ts.NewLineKind.LineFeed,
362+
noEmitHelpers: true,
363+
experimentalDecorators: true,
364+
strict: true
365+
};
366+
const filename = 'create-cat-priority.dto.ts';
367+
const fakeProgram = ts.createProgram([filename], options);
368+
369+
const debugLoggerSpy = jest.spyOn(pluginDebugLogger, 'debug');
370+
371+
const result = ts.transpileModule(createCatPriorityDtoText, {
372+
compilerOptions: options,
373+
fileName: filename,
374+
transformers: {
375+
before: [
376+
before(
377+
{
378+
classValidatorShim: true,
379+
classTransformerShim: true,
380+
dtoKeyOfComment: 'title',
381+
introspectComments: true,
382+
debug: true
383+
},
384+
fakeProgram
385+
)
386+
]
387+
}
388+
});
389+
expect(result.outputText).toEqual(createCatPriorityDtoTextTranspiled);
390+
expect(debugLoggerSpy).toHaveBeenCalledWith(
391+
'"CreateCatDto->hidden" has conflicting decorators, excluding as @ApiHideProperty() takes priority.'
392+
);
393+
});
394+
395+
it('should add the metadata factory only when decorators exist', () => {
396+
const options: ts.CompilerOptions = {
397+
module: ts.ModuleKind.ES2020,
398+
target: ts.ScriptTarget.ES2020,
399+
newLine: ts.NewLineKind.LineFeed,
400+
noEmitHelpers: true,
401+
experimentalDecorators: true,
402+
strict: true
403+
};
404+
const filename = 'create-cat-exclusive.dto.ts';
405+
const fakeProgram = ts.createProgram([filename], options);
406+
407+
const result = ts.transpileModule(createCatExclusiveDtoText, {
408+
compilerOptions: options,
409+
fileName: filename,
410+
transformers: {
411+
before: [
412+
before(
413+
{
414+
classValidatorShim: true,
415+
classTransformerShim: 'exclusive',
416+
dtoKeyOfComment: 'title',
417+
introspectComments: true
418+
},
419+
fakeProgram
420+
)
421+
]
422+
}
423+
});
424+
expect(result.outputText).toEqual(createCatExclusiveDtoTextTranspiled);
425+
});
278426
});

‎test/plugin/readonly-visitor.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ describe('Readonly visitor', () => {
5050
const expectedOutput = readFileSync(
5151
join(__dirname, 'fixtures', 'serialized-meta.fixture.ts'),
5252
'utf-8'
53-
);
53+
)
54+
.replace(/\r\n/g, '\n')
55+
.replace(/\r/g, '\n');
56+
/** Normalize the file line endings to LF */
5457

5558
// writeFileSync(
5659
// join(__dirname, 'fixtures', 'serialized-meta.fixture.ts'),

0 commit comments

Comments
 (0)
Please sign in to comment.