Skip to content

Commit

Permalink
Merge pull request #2777 from kurt-west/master
Browse files Browse the repository at this point in the history
feat(plugin) add support for @expose() and @exclude() decorators
  • Loading branch information
kamilmysliwiec authored Feb 7, 2024
2 parents 1d86dfd + f4fcd64 commit 876d17a
Show file tree
Hide file tree
Showing 7 changed files with 630 additions and 10 deletions.
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
38 changes: 29 additions & 9 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
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);
`;
132 changes: 132 additions & 0 deletions test/plugin/fixtures/create-cat-exclusive.dto.ts
Original file line number Diff line number Diff line change
@@ -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);
`;
Loading

0 comments on commit 876d17a

Please sign in to comment.