Skip to content

Commit

Permalink
Merge pull request #2687 from devoto13/support-parameter-properties
Browse files Browse the repository at this point in the history
feat: Add basic support for parameter properties
  • Loading branch information
kamilmysliwiec authored Jan 15, 2024
2 parents 1d50385 + e3151bc commit 91d2035
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 10 deletions.
1 change: 1 addition & 0 deletions lib/plugin/merge-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface PluginOptions {
readonly?: boolean;
pathToSource?: string;
debug?: boolean;
parameterProperties?: boolean;
}

const defaultOptions: PluginOptions = {
Expand Down
84 changes: 74 additions & 10 deletions lib/plugin/visitors/model-class.visitor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { compact, flatten, head } from 'lodash';
import { posix } from 'path';
import * as ts from 'typescript';
import { PropertyAssignment, factory } from 'typescript';
import { factory, PropertyAssignment } from 'typescript';
import { ApiHideProperty } from '../../decorators';
import { PluginOptions } from '../merge-options';
import { METADATA_FACTORY_NAME } from '../plugin-constants';
Expand Down Expand Up @@ -78,6 +78,17 @@ export class ModelClassVisitor extends AbstractFileVisitor {
sourceFile,
metadata
);
} else if (
options.parameterProperties &&
ts.isConstructorDeclaration(node)
) {
this.visitConstructorDeclarationNode(
node,
typeChecker,
options,
sourceFile,
metadata
);
}
return node;
};
Expand Down Expand Up @@ -176,6 +187,41 @@ export class ModelClassVisitor extends AbstractFileVisitor {
}
}

visitConstructorDeclarationNode(
constructorNode: ts.ConstructorDeclaration,
typeChecker: ts.TypeChecker,
options: PluginOptions,
sourceFile: ts.SourceFile,
metadata: ClassMetadata
) {
constructorNode.forEachChild((node) => {
if (
ts.isParameter(node) &&
node.modifiers != null &&
node.modifiers.some(
(modifier: ts.Modifier) =>
modifier.kind === ts.SyntaxKind.ReadonlyKeyword ||
modifier.kind === ts.SyntaxKind.PrivateKeyword ||
modifier.kind === ts.SyntaxKind.PublicKeyword ||
modifier.kind === ts.SyntaxKind.ProtectedKeyword
)
) {
const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
factory,
node,
typeChecker,
factory.createNodeArray(),
options,
sourceFile.fileName,
sourceFile
);

const propertyName = node.name.getText();
metadata[propertyName] = objectLiteralExpr;
}
});
}

addMetadataFactory(
factory: ts.NodeFactory,
node: ts.ClassDeclaration,
Expand Down Expand Up @@ -254,7 +300,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {

createDecoratorObjectLiteralExpr(
factory: ts.NodeFactory,
node: ts.PropertyDeclaration | ts.PropertySignature,
node:
| ts.PropertyDeclaration
| ts.PropertySignature
| ts.ParameterDeclaration,
typeChecker: ts.TypeChecker,
existingProperties: ts.NodeArray<ts.PropertyAssignment> = factory.createNodeArray(),
options: PluginOptions = {},
Expand All @@ -278,7 +327,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
hostFilename,
options
),
...this.createDescriptionAndTsDocTagPropertyAssigments(
...this.createDescriptionAndTsDocTagPropertyAssignments(
factory,
node,
typeChecker,
Expand All @@ -301,7 +350,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {
options
)
];
if (options.classValidatorShim) {
if (
(ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) &&
options.classValidatorShim
) {
properties.push(
this.createValidationPropertyAssignments(factory, node, options)
);
Expand Down Expand Up @@ -445,7 +497,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {

createEnumPropertyAssignment(
factory: ts.NodeFactory,
node: ts.PropertyDeclaration | ts.PropertySignature,
node:
| ts.PropertyDeclaration
| ts.PropertySignature
| ts.ParameterDeclaration,
typeChecker: ts.TypeChecker,
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
hostFilename: string,
Expand Down Expand Up @@ -512,18 +567,24 @@ export class ModelClassVisitor extends AbstractFileVisitor {

createDefaultPropertyAssignment(
factory: ts.NodeFactory,
node: ts.PropertyDeclaration | ts.PropertySignature,
node:
| ts.PropertyDeclaration
| ts.PropertySignature
| ts.ParameterDeclaration,
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
options: PluginOptions
) {
const key = 'default';
if (hasPropertyKey(key, existingProperties)) {
return undefined;
}
let initializer = (node as ts.PropertyDeclaration).initializer;
if (!initializer) {
if (ts.isPropertySignature(node)) {
return undefined;
}
if (node.initializer == null) {
return undefined;
}
let initializer = node.initializer;
if (ts.isAsExpression(initializer)) {
initializer = initializer.expression;
}
Expand Down Expand Up @@ -745,9 +806,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
metadata[propertyName] = objectLiteral;
}

createDescriptionAndTsDocTagPropertyAssigments(
createDescriptionAndTsDocTagPropertyAssignments(
factory: ts.NodeFactory,
node: ts.PropertyDeclaration | ts.PropertySignature,
node:
| ts.PropertyDeclaration
| ts.PropertySignature
| ts.ParameterDeclaration,
typeChecker: ts.TypeChecker,
existingProperties: ts.NodeArray<ts.PropertyAssignment> = factory.createNodeArray(),
options: PluginOptions = {},
Expand Down
49 changes: 49 additions & 0 deletions test/plugin/fixtures/parameter-property.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export const parameterPropertyDtoText = `
export class ParameterPropertyDto {
constructor(
readonly readonlyValue?: string,
private privateValue: string | null,
public publicValue: ItemDto[],
regularParameter: string
protected protectedValue: string = '1234',
) {}
}
export enum LettersEnum {
A = 'A',
B = 'B',
C = 'C'
}
export class ItemDto {
constructor(readonly enumValue: LettersEnum) {}
}
`;

export const parameterPropertyDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
export class ParameterPropertyDto {
constructor(readonlyValue, privateValue, publicValue, regularParameter, protectedValue = '1234') {
this.readonlyValue = readonlyValue;
this.privateValue = privateValue;
this.publicValue = publicValue;
this.protectedValue = protectedValue;
}
static _OPENAPI_METADATA_FACTORY() {
return { readonlyValue: { required: false, type: () => String }, privateValue: { required: true, type: () => String, nullable: true }, publicValue: { required: true, type: () => [require("./parameter-property.dto").ItemDto] }, protectedValue: { required: true, type: () => String, default: "1234" } };
}
}
export var LettersEnum;
(function (LettersEnum) {
LettersEnum["A"] = "A";
LettersEnum["B"] = "B";
LettersEnum["C"] = "C";
})(LettersEnum || (LettersEnum = {}));
export class ItemDto {
constructor(enumValue) {
this.enumValue = enumValue;
}
static _OPENAPI_METADATA_FACTORY() {
return { enumValue: { required: true, enum: require("./parameter-property.dto").LettersEnum } };
}
}
`;
35 changes: 35 additions & 0 deletions test/plugin/model-class-visitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import {
stringLiteralDtoText,
stringLiteralDtoTextTranspiled
} from './fixtures/string-literal.dto';
import {
parameterPropertyDtoText,
parameterPropertyDtoTextTranspiled
} from './fixtures/parameter-property.dto';

describe('API model properties', () => {
it('should add the metadata factory when no decorators exist, and generated propertyKey is title', () => {
Expand Down Expand Up @@ -240,4 +244,35 @@ describe('API model properties', () => {
});
expect(result.outputText).toEqual(stringLiteralDtoTextTranspiled);
});

it('should support & understand parameter properties', () => {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.ES2020,
target: ts.ScriptTarget.ES2020,
newLine: ts.NewLineKind.LineFeed,
noEmitHelpers: true,
experimentalDecorators: true,
strict: true
};
const filename = 'parameter-property.dto.ts';
const fakeProgram = ts.createProgram([filename], options);

const result = ts.transpileModule(parameterPropertyDtoText, {
compilerOptions: options,
fileName: filename,
transformers: {
before: [
before(
{
introspectComments: true,
classValidatorShim: true,
parameterProperties: true
},
fakeProgram
)
]
}
});
expect(result.outputText).toEqual(parameterPropertyDtoTextTranspiled);
});
});

0 comments on commit 91d2035

Please sign in to comment.