From f4fcd647a280c34ed041b1ceb48ad08c660e7dfc Mon Sep 17 00:00:00 2001 From: kurt-west Date: Mon, 15 Jan 2024 02:50:23 -0800 Subject: [PATCH] feat(plugin) add support for @Expose() and @Exclude() decorators --- lib/plugin/merge-options.ts | 2 + lib/plugin/visitors/model-class.visitor.ts | 47 +++-- .../plugin/fixtures/create-cat-exclude.dto.ts | 187 ++++++++++++++++++ .../fixtures/create-cat-exclusive.dto.ts | 132 +++++++++++++ .../fixtures/create-cat-priority.dto.ts | 128 ++++++++++++ test/plugin/model-class-visitor.spec.ts | 148 ++++++++++++++ test/plugin/readonly-visitor.spec.ts | 5 +- 7 files changed, 634 insertions(+), 15 deletions(-) create mode 100644 test/plugin/fixtures/create-cat-exclude.dto.ts create mode 100644 test/plugin/fixtures/create-cat-exclusive.dto.ts create mode 100644 test/plugin/fixtures/create-cat-priority.dto.ts diff --git a/lib/plugin/merge-options.ts b/lib/plugin/merge-options.ts index b02386186..bf63ff262 100644 --- a/lib/plugin/merge-options.ts +++ b/lib/plugin/merge-options.ts @@ -4,6 +4,7 @@ export interface PluginOptions { dtoFileNameSuffix?: string | string[]; controllerFileNameSuffix?: string | string[]; classValidatorShim?: boolean; + classTransformerShim?: boolean | 'exclusive'; dtoKeyOfComment?: string; controllerKeyOfComment?: string; introspectComments?: boolean; @@ -17,6 +18,7 @@ const defaultOptions: PluginOptions = { dtoFileNameSuffix: ['.dto.ts', '.entity.ts'], controllerFileNameSuffix: ['.controller.ts'], classValidatorShim: true, + classTransformerShim: false, dtoKeyOfComment: 'description', controllerKeyOfComment: 'description', introspectComments: false, diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts index d1399ea06..a422d7889 100644 --- a/lib/plugin/visitors/model-class.visitor.ts +++ b/lib/plugin/visitors/model-class.visitor.ts @@ -2,7 +2,7 @@ import { compact, flatten, head } from 'lodash'; import { posix } from 'path'; import * as ts from 'typescript'; import { factory, PropertyAssignment } from 'typescript'; -import { ApiHideProperty } from '../../decorators'; +import { ApiHideProperty, ApiProperty } from '../../decorators'; import { PluginOptions } from '../merge-options'; import { METADATA_FACTORY_NAME } from '../plugin-constants'; import { pluginDebugLogger } from '../plugin-debug-logger'; @@ -155,23 +155,43 @@ export class ModelClassVisitor extends AbstractFileVisitor { sourceFile: ts.SourceFile, metadata: ClassMetadata ) { + const isPropertyStatic = (node.modifiers || []).some( + (modifier: ts.Modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword + ); + if (isPropertyStatic) { + return node; + } + const decorators = ts.canHaveDecorators(node) && ts.getDecorators(node); - const hidePropertyDecorator = getDecoratorOrUndefinedByNames( - [ApiHideProperty.name], + const classTransformerShim = options.classTransformerShim; + + const hidePropertyDecoratorExists = getDecoratorOrUndefinedByNames( + classTransformerShim + ? [ApiHideProperty.name, 'Exclude'] + : [ApiHideProperty.name], decorators, factory ); - if (hidePropertyDecorator) { - return node; - } - const isPropertyStatic = (node.modifiers || []).some( - (modifier: ts.Modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword + const annotatePropertyDecoratorExists = getDecoratorOrUndefinedByNames( + classTransformerShim ? [ApiProperty.name, 'Expose'] : [ApiProperty.name], + decorators, + factory ); - if (isPropertyStatic) { + + if ( + !annotatePropertyDecoratorExists && + (hidePropertyDecoratorExists || classTransformerShim === 'exclusive') + ) { + return node; + } else if (annotatePropertyDecoratorExists && hidePropertyDecoratorExists) { + pluginDebugLogger.debug( + `"${node.parent.name.getText()}->${node.name.getText()}" has conflicting decorators, excluding as @ApiHideProperty() takes priority.` + ); return node; } + try { this.inspectPropertyDeclaration( ctx.factory, @@ -695,7 +715,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { return result; } - const clonedMinLength = this.clonePrimitiveLiteral(factory, minLength) ?? minLength; + const clonedMinLength = + this.clonePrimitiveLiteral(factory, minLength) ?? minLength; if (clonedMinLength) { result.push( factory.createPropertyAssignment('minLength', clonedMinLength) @@ -707,10 +728,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { if (!canReferenceNode(maxLength, options)) { return result; } - const clonedMaxLength = this.clonePrimitiveLiteral( - factory, - maxLength - ) ?? maxLength; + const clonedMaxLength = + this.clonePrimitiveLiteral(factory, maxLength) ?? maxLength; if (clonedMaxLength) { result.push( factory.createPropertyAssignment('maxLength', clonedMaxLength) diff --git a/test/plugin/fixtures/create-cat-exclude.dto.ts b/test/plugin/fixtures/create-cat-exclude.dto.ts new file mode 100644 index 000000000..1e2dce52e --- /dev/null +++ b/test/plugin/fixtures/create-cat-exclude.dto.ts @@ -0,0 +1,187 @@ +export const createCatExcludeDtoText = ` +import { IsInt, IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; + +enum Status { + ENABLED, + DISABLED +} + +enum OneValueEnum { + ONE +} + +interface Node { + id: number; +} + +class OtherNode { + id: number; +} + +export class CreateCatDto { + @IsIn(['a', 'b']) + isIn: string; + @Matches(/^[+]?abc$/) + pattern: string; + name: string; + @Min(0) + @Max(10) + age: number = 3; + @IsPositive() + positive: number = 5; + @IsNegative() + negative: number = -1; + @Length(2) + lengthMin: string; + @Length(3, 5) + lengthMinMax: string; + tags: string[]; + status: Status = Status.ENABLED; + status2?: Status; + statusArr?: Status[]; + oneValueEnum?: OneValueEnum; + oneValueEnumArr?: OneValueEnum[]; + + /** this is breed im comment */ + @ApiProperty({ description: "this is breed", type: String }) + @IsString() + readonly breed?: string; + + nodes: Node[]; + optionalBoolean?: boolean; + date: Date; + + twoDimensionPrimitives: string[][]; + twoDimensionNodes: OtherNode[][]; + + @ApiHideProperty() + hidden: number; + + @Exclude() + excluded: number; + + static staticProperty: string; +} +`; + +export const createCatExcludeDtoTextTranspiled = `import * as openapi from "@nestjs/swagger"; +import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; +var Status; +(function (Status) { + Status[Status[\"ENABLED\"] = 0] = \"ENABLED\"; + Status[Status[\"DISABLED\"] = 1] = \"DISABLED\"; +})(Status || (Status = {})); +var OneValueEnum; +(function (OneValueEnum) { + OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\"; +})(OneValueEnum || (OneValueEnum = {})); +class OtherNode { + static _OPENAPI_METADATA_FACTORY() { + return { id: { required: true, type: () => Number } }; + } +} +export class CreateCatDto { + constructor() { + this.age = 3; + this.positive = 5; + this.negative = -1; + this.status = Status.ENABLED; + } + static _OPENAPI_METADATA_FACTORY() { + 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]] } }; + } +} +__decorate([ + IsIn(['a', 'b']) +], CreateCatDto.prototype, \"isIn\", void 0); +__decorate([ + Matches(/^[+]?abc$/) +], CreateCatDto.prototype, \"pattern\", void 0); +__decorate([ + Min(0), + Max(10) +], CreateCatDto.prototype, \"age\", void 0); +__decorate([ + IsPositive() +], CreateCatDto.prototype, \"positive\", void 0); +__decorate([ + IsNegative() +], CreateCatDto.prototype, \"negative\", void 0); +__decorate([ + Length(2) +], CreateCatDto.prototype, \"lengthMin\", void 0); +__decorate([ + Length(3, 5) +], CreateCatDto.prototype, \"lengthMinMax\", void 0); +__decorate([ + ApiProperty({ description: "this is breed", type: String }), + IsString() +], CreateCatDto.prototype, \"breed\", void 0); +__decorate([ + ApiHideProperty() +], CreateCatDto.prototype, \"hidden\", void 0); +__decorate([ + Exclude() +], CreateCatDto.prototype, "excluded", void 0); +`; + +export const createCatIgnoreExcludeDtoTextTranspiled = `import * as openapi from "@nestjs/swagger"; +import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; +var Status; +(function (Status) { + Status[Status[\"ENABLED\"] = 0] = \"ENABLED\"; + Status[Status[\"DISABLED\"] = 1] = \"DISABLED\"; +})(Status || (Status = {})); +var OneValueEnum; +(function (OneValueEnum) { + OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\"; +})(OneValueEnum || (OneValueEnum = {})); +class OtherNode { + static _OPENAPI_METADATA_FACTORY() { + return { id: { required: true, type: () => Number } }; + } +} +export class CreateCatDto { + constructor() { + this.age = 3; + this.positive = 5; + this.negative = -1; + this.status = Status.ENABLED; + } + static _OPENAPI_METADATA_FACTORY() { + 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 } }; + } +} +__decorate([ + IsIn(['a', 'b']) +], CreateCatDto.prototype, \"isIn\", void 0); +__decorate([ + Matches(/^[+]?abc$/) +], CreateCatDto.prototype, \"pattern\", void 0); +__decorate([ + Min(0), + Max(10) +], CreateCatDto.prototype, \"age\", void 0); +__decorate([ + IsPositive() +], CreateCatDto.prototype, \"positive\", void 0); +__decorate([ + IsNegative() +], CreateCatDto.prototype, \"negative\", void 0); +__decorate([ + Length(2) +], CreateCatDto.prototype, \"lengthMin\", void 0); +__decorate([ + Length(3, 5) +], CreateCatDto.prototype, \"lengthMinMax\", void 0); +__decorate([ + ApiProperty({ description: "this is breed", type: String }), + IsString() +], CreateCatDto.prototype, \"breed\", void 0); +__decorate([ + ApiHideProperty() +], CreateCatDto.prototype, \"hidden\", void 0); +__decorate([ + Exclude() +], CreateCatDto.prototype, "excluded", void 0); +`; diff --git a/test/plugin/fixtures/create-cat-exclusive.dto.ts b/test/plugin/fixtures/create-cat-exclusive.dto.ts new file mode 100644 index 000000000..b8a03a68b --- /dev/null +++ b/test/plugin/fixtures/create-cat-exclusive.dto.ts @@ -0,0 +1,132 @@ +export const createCatExclusiveDtoText = ` +import { IsInt, IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; + +enum Status { + ENABLED, + DISABLED +} + +enum OneValueEnum { + ONE +} + +interface Node { + id: number; +} + +class OtherNode { + id: number; +} + +export class CreateCatDto { + @IsIn(['a', 'b']) + isIn: string; + @Matches(/^[+]?abc$/) + pattern: string; + name: string; + @Min(0) + @Max(10) + age: number = 3; + @IsPositive() + positive: number = 5; + @IsNegative() + negative: number = -1; + @Length(2) + lengthMin: string; + @Length(3, 5) + lengthMinMax: string; + tags: string[]; + status: Status = Status.ENABLED; + status2?: Status; + statusArr?: Status[]; + oneValueEnum?: OneValueEnum; + oneValueEnumArr?: OneValueEnum[]; + + /** this is breed im comment */ + @ApiProperty({ description: "this is breed", type: String }) + @IsString() + readonly breed?: string; + + nodes: Node[]; + optionalBoolean?: boolean; + date: Date; + + twoDimensionPrimitives: string[][]; + twoDimensionNodes: OtherNode[][]; + + @ApiHideProperty() + hidden: number; + + @Exclude() + excluded: number; + + @Expose() + exposed: number; + + static staticProperty: string; +} +`; + +export const createCatExclusiveDtoTextTranspiled = `import * as openapi from "@nestjs/swagger"; +import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; +var Status; +(function (Status) { + Status[Status[\"ENABLED\"] = 0] = \"ENABLED\"; + Status[Status[\"DISABLED\"] = 1] = \"DISABLED\"; +})(Status || (Status = {})); +var OneValueEnum; +(function (OneValueEnum) { + OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\"; +})(OneValueEnum || (OneValueEnum = {})); +class OtherNode { + static _OPENAPI_METADATA_FACTORY() { + return {}; + } +} +export class CreateCatDto { + constructor() { + this.age = 3; + this.positive = 5; + this.negative = -1; + this.status = Status.ENABLED; + } + static _OPENAPI_METADATA_FACTORY() { + return { breed: { required: false, type: () => String, title: "this is breed im comment" }, exposed: { required: true, type: () => Number } }; + } +} +__decorate([ + IsIn(['a', 'b']) +], CreateCatDto.prototype, \"isIn\", void 0); +__decorate([ + Matches(/^[+]?abc$/) +], CreateCatDto.prototype, \"pattern\", void 0); +__decorate([ + Min(0), + Max(10) +], CreateCatDto.prototype, \"age\", void 0); +__decorate([ + IsPositive() +], CreateCatDto.prototype, \"positive\", void 0); +__decorate([ + IsNegative() +], CreateCatDto.prototype, \"negative\", void 0); +__decorate([ + Length(2) +], CreateCatDto.prototype, \"lengthMin\", void 0); +__decorate([ + Length(3, 5) +], CreateCatDto.prototype, \"lengthMinMax\", void 0); +__decorate([ + ApiProperty({ description: "this is breed", type: String }), + IsString() +], CreateCatDto.prototype, \"breed\", void 0); +__decorate([ + ApiHideProperty() +], CreateCatDto.prototype, \"hidden\", void 0); +__decorate([ + Exclude() +], CreateCatDto.prototype, "excluded", void 0); +__decorate([ + Expose() +], CreateCatDto.prototype, "exposed", void 0); +`; diff --git a/test/plugin/fixtures/create-cat-priority.dto.ts b/test/plugin/fixtures/create-cat-priority.dto.ts new file mode 100644 index 000000000..9757b6c50 --- /dev/null +++ b/test/plugin/fixtures/create-cat-priority.dto.ts @@ -0,0 +1,128 @@ +export const createCatPriorityDtoText = ` +import { IsInt, IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; + +enum Status { + ENABLED, + DISABLED +} + +enum OneValueEnum { + ONE +} + +interface Node { + id: number; +} + +class OtherNode { + id: number; +} + +export class CreateCatDto { + @IsIn(['a', 'b']) + isIn: string; + @Matches(/^[+]?abc$/) + pattern: string; + name: string; + @Min(0) + @Max(10) + age: number = 3; + @IsPositive() + positive: number = 5; + @IsNegative() + negative: number = -1; + @Length(2) + lengthMin: string; + @Length(3, 5) + lengthMinMax: string; + tags: string[]; + status: Status = Status.ENABLED; + status2?: Status; + statusArr?: Status[]; + oneValueEnum?: OneValueEnum; + oneValueEnumArr?: OneValueEnum[]; + + /** this is breed im comment */ + @ApiProperty({ description: "this is breed", type: String }) + @IsString() + readonly breed?: string; + + nodes: Node[]; + optionalBoolean?: boolean; + date: Date; + + twoDimensionPrimitives: string[][]; + twoDimensionNodes: OtherNode[][]; + + @Expose() + @ApiHideProperty() + hidden: number; + + @Exclude() + excluded: number; + + static staticProperty: string; +} +`; + +export const createCatPriorityDtoTextTranspiled = `import * as openapi from "@nestjs/swagger"; +import { IsString, IsPositive, IsNegative, Length, Matches, IsIn } from 'class-validator'; +var Status; +(function (Status) { + Status[Status[\"ENABLED\"] = 0] = \"ENABLED\"; + Status[Status[\"DISABLED\"] = 1] = \"DISABLED\"; +})(Status || (Status = {})); +var OneValueEnum; +(function (OneValueEnum) { + OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\"; +})(OneValueEnum || (OneValueEnum = {})); +class OtherNode { + static _OPENAPI_METADATA_FACTORY() { + return { id: { required: true, type: () => Number } }; + } +} +export class CreateCatDto { + constructor() { + this.age = 3; + this.positive = 5; + this.negative = -1; + this.status = Status.ENABLED; + } + static _OPENAPI_METADATA_FACTORY() { + 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]] } }; + } +} +__decorate([ + IsIn(['a', 'b']) +], CreateCatDto.prototype, \"isIn\", void 0); +__decorate([ + Matches(/^[+]?abc$/) +], CreateCatDto.prototype, \"pattern\", void 0); +__decorate([ + Min(0), + Max(10) +], CreateCatDto.prototype, \"age\", void 0); +__decorate([ + IsPositive() +], CreateCatDto.prototype, \"positive\", void 0); +__decorate([ + IsNegative() +], CreateCatDto.prototype, \"negative\", void 0); +__decorate([ + Length(2) +], CreateCatDto.prototype, \"lengthMin\", void 0); +__decorate([ + Length(3, 5) +], CreateCatDto.prototype, \"lengthMinMax\", void 0); +__decorate([ + ApiProperty({ description: "this is breed", type: String }), + IsString() +], CreateCatDto.prototype, \"breed\", void 0); +__decorate([ + Expose(), + ApiHideProperty() +], CreateCatDto.prototype, \"hidden\", void 0); +__decorate([ + Exclude() +], CreateCatDto.prototype, "excluded", void 0); +`; diff --git a/test/plugin/model-class-visitor.spec.ts b/test/plugin/model-class-visitor.spec.ts index d3c1d338c..9ea87c32b 100644 --- a/test/plugin/model-class-visitor.spec.ts +++ b/test/plugin/model-class-visitor.spec.ts @@ -34,6 +34,20 @@ import { parameterPropertyDtoText, parameterPropertyDtoTextTranspiled } from './fixtures/parameter-property.dto'; +import { + createCatExcludeDtoText, + createCatExcludeDtoTextTranspiled, + createCatIgnoreExcludeDtoTextTranspiled +} from './fixtures/create-cat-exclude.dto'; +import { + createCatExclusiveDtoText, + createCatExclusiveDtoTextTranspiled +} from './fixtures/create-cat-exclusive.dto'; +import { + createCatPriorityDtoText, + createCatPriorityDtoTextTranspiled +} from './fixtures/create-cat-priority.dto'; +import { pluginDebugLogger } from '../../lib/plugin/plugin-debug-logger'; describe('API model properties', () => { it('should add the metadata factory when no decorators exist, and generated propertyKey is title', () => { @@ -275,4 +289,138 @@ describe('API model properties', () => { }); expect(result.outputText).toEqual(parameterPropertyDtoTextTranspiled); }); + + it('should ignore Exclude decorator', () => { + const options: ts.CompilerOptions = { + module: ts.ModuleKind.ES2020, + target: ts.ScriptTarget.ES2020, + newLine: ts.NewLineKind.LineFeed, + noEmitHelpers: true, + experimentalDecorators: true, + strict: true + }; + const filename = 'create-cat-exclude.dto.ts'; + const fakeProgram = ts.createProgram([filename], options); + + const result = ts.transpileModule(createCatExcludeDtoText, { + compilerOptions: options, + fileName: filename, + transformers: { + before: [ + before( + { + classValidatorShim: true, + classTransformerShim: false, + dtoKeyOfComment: 'title', + introspectComments: true + }, + fakeProgram + ) + ] + } + }); + expect(result.outputText).toEqual(createCatIgnoreExcludeDtoTextTranspiled); + }); + + it('should hide properties decorated with the Exclude decorator', () => { + const options: ts.CompilerOptions = { + module: ts.ModuleKind.ES2020, + target: ts.ScriptTarget.ES2020, + newLine: ts.NewLineKind.LineFeed, + noEmitHelpers: true, + experimentalDecorators: true, + strict: true + }; + const filename = 'create-cat-exclude.dto.ts'; + const fakeProgram = ts.createProgram([filename], options); + + const result = ts.transpileModule(createCatExcludeDtoText, { + compilerOptions: options, + fileName: filename, + transformers: { + before: [ + before( + { + classValidatorShim: true, + classTransformerShim: true, + dtoKeyOfComment: 'title', + introspectComments: true + }, + fakeProgram + ) + ] + } + }); + expect(result.outputText).toEqual(createCatExcludeDtoTextTranspiled); + }); + + it('should hide a property with conflicting decorators', () => { + const options: ts.CompilerOptions = { + module: ts.ModuleKind.ES2020, + target: ts.ScriptTarget.ES2020, + newLine: ts.NewLineKind.LineFeed, + noEmitHelpers: true, + experimentalDecorators: true, + strict: true + }; + const filename = 'create-cat-priority.dto.ts'; + const fakeProgram = ts.createProgram([filename], options); + + const debugLoggerSpy = jest.spyOn(pluginDebugLogger, 'debug'); + + const result = ts.transpileModule(createCatPriorityDtoText, { + compilerOptions: options, + fileName: filename, + transformers: { + before: [ + before( + { + classValidatorShim: true, + classTransformerShim: true, + dtoKeyOfComment: 'title', + introspectComments: true, + debug: true + }, + fakeProgram + ) + ] + } + }); + expect(result.outputText).toEqual(createCatPriorityDtoTextTranspiled); + expect(debugLoggerSpy).toHaveBeenCalledWith( + '"CreateCatDto->hidden" has conflicting decorators, excluding as @ApiHideProperty() takes priority.' + ); + }); + + it('should add the metadata factory only when decorators exist', () => { + const options: ts.CompilerOptions = { + module: ts.ModuleKind.ES2020, + target: ts.ScriptTarget.ES2020, + newLine: ts.NewLineKind.LineFeed, + noEmitHelpers: true, + experimentalDecorators: true, + strict: true + }; + const filename = 'create-cat-exclusive.dto.ts'; + const fakeProgram = ts.createProgram([filename], options); + + const result = ts.transpileModule(createCatExclusiveDtoText, { + compilerOptions: options, + fileName: filename, + transformers: { + before: [ + before( + { + classValidatorShim: true, + classTransformerShim: 'exclusive', + dtoKeyOfComment: 'title', + introspectComments: true + }, + fakeProgram + ) + ] + } + }); + expect(result.outputText).toEqual(createCatExclusiveDtoTextTranspiled); + }); }); diff --git a/test/plugin/readonly-visitor.spec.ts b/test/plugin/readonly-visitor.spec.ts index daa14e99c..1331989c7 100644 --- a/test/plugin/readonly-visitor.spec.ts +++ b/test/plugin/readonly-visitor.spec.ts @@ -50,7 +50,10 @@ describe('Readonly visitor', () => { const expectedOutput = readFileSync( join(__dirname, 'fixtures', 'serialized-meta.fixture.ts'), 'utf-8' - ); + ) + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n'); + /** Normalize the file line endings to LF */ // writeFileSync( // join(__dirname, 'fixtures', 'serialized-meta.fixture.ts'),