Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin) add support for @Expose() and @Exclude() decorators #2777

Merged
merged 1 commit into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/plugin/merge-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface PluginOptions {
dtoFileNameSuffix?: string | string[];
controllerFileNameSuffix?: string | string[];
classValidatorShim?: boolean;
classTransformerShim?: boolean | 'exclusive';
dtoKeyOfComment?: string;
controllerKeyOfComment?: string;
introspectComments?: boolean;
Expand All @@ -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,
Expand Down
47 changes: 33 additions & 14 deletions lib/plugin/visitors/model-class.visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
187 changes: 187 additions & 0 deletions test/plugin/fixtures/create-cat-exclude.dto.ts
Original file line number Diff line number Diff line change
@@ -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);
`;
Loading