Skip to content

Commit ec504c2

Browse files
committedJun 26, 2023
fix: several bug fixes for the readonly visitor
1 parent 0a2ad6a commit ec504c2

File tree

6 files changed

+198
-44
lines changed

6 files changed

+198
-44
lines changed
 

‎lib/plugin/utils/ast-utils.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,23 @@ export function createBooleanLiteral(
281281
return flag ? factory.createTrue() : factory.createFalse();
282282
}
283283

284-
export function createPrimitiveLiteral(factory: ts.NodeFactory, item: unknown) {
285-
const typeOfItem = typeof item;
286-
284+
export function createPrimitiveLiteral(
285+
factory: ts.NodeFactory,
286+
item: unknown,
287+
typeOfItem = typeof item
288+
) {
287289
switch (typeOfItem) {
288290
case 'boolean':
289291
return createBooleanLiteral(factory, item as boolean);
290-
case 'number':
292+
case 'number': {
293+
if ((item as number) < 0) {
294+
return factory.createPrefixUnaryExpression(
295+
SyntaxKind.MinusToken,
296+
factory.createNumericLiteral(Math.abs(item as number))
297+
);
298+
}
291299
return factory.createNumericLiteral(item as number);
300+
}
292301
case 'string':
293302
return factory.createStringLiteral(item as string);
294303
}

‎lib/plugin/utils/plugin-utils.ts

+15
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,18 @@ export function convertPath(windowsPath: string) {
340340
.replace(/\\/g, '/')
341341
.replace(/\/\/+/g, '/');
342342
}
343+
344+
/**
345+
* Checks if a node can be directly referenced.
346+
* In the readonly mode, only literals can be referenced directly.
347+
* Nodes like identifiers or call expressions are not available in the auto-generated code.
348+
*/
349+
export function canReferenceNode(node: ts.Node, options: PluginOptions) {
350+
if (!options.readonly) {
351+
return true;
352+
}
353+
if (ts.isIdentifier(node) || ts.isCallExpression(node)) {
354+
return false;
355+
}
356+
return true;
357+
}

‎lib/plugin/visitors/model-class.visitor.ts

+94-36
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isEnum
1717
} from '../utils/ast-utils';
1818
import {
19+
canReferenceNode,
1920
convertPath,
2021
extractTypeArgumentIfArray,
2122
getDecoratorOrUndefinedByNames,
@@ -248,7 +249,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
248249
): ts.ObjectLiteralExpression {
249250
const isRequired = !node.questionToken;
250251

251-
let properties = [
252+
const properties = [
252253
...existingProperties,
253254
!hasPropertyKey('required', existingProperties) &&
254255
factory.createPropertyAssignment(
@@ -271,7 +272,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
271272
options,
272273
sourceFile
273274
),
274-
this.createDefaultPropertyAssignment(factory, node, existingProperties),
275+
this.createDefaultPropertyAssignment(
276+
factory,
277+
node,
278+
existingProperties,
279+
options
280+
),
275281
this.createEnumPropertyAssignment(
276282
factory,
277283
node,
@@ -282,8 +288,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
282288
)
283289
];
284290
if (options.classValidatorShim) {
285-
properties = properties.concat(
286-
this.createValidationPropertyAssignments(factory, node)
291+
properties.push(
292+
this.createValidationPropertyAssignments(factory, node, options)
287293
);
288294
}
289295
return factory.createObjectLiteralExpression(compact(flatten(properties)));
@@ -471,7 +477,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
471477
createDefaultPropertyAssignment(
472478
factory: ts.NodeFactory,
473479
node: ts.PropertyDeclaration | ts.PropertySignature,
474-
existingProperties: ts.NodeArray<ts.PropertyAssignment>
480+
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
481+
options: PluginOptions
475482
) {
476483
const key = 'default';
477484
if (hasPropertyKey(key, existingProperties)) {
@@ -484,50 +491,65 @@ export class ModelClassVisitor extends AbstractFileVisitor {
484491
if (ts.isAsExpression(initializer)) {
485492
initializer = initializer.expression;
486493
}
494+
initializer =
495+
this.clonePrimitiveLiteral(factory, initializer) ?? initializer;
496+
497+
if (!canReferenceNode(initializer, options)) {
498+
return undefined;
499+
}
487500
return factory.createPropertyAssignment(key, initializer);
488501
}
489502

490503
createValidationPropertyAssignments(
491504
factory: ts.NodeFactory,
492-
node: ts.PropertyDeclaration | ts.PropertySignature
505+
node: ts.PropertyDeclaration | ts.PropertySignature,
506+
options: PluginOptions
493507
): ts.PropertyAssignment[] {
494508
const assignments = [];
495509
const decorators = ts.canHaveDecorators(node) && ts.getDecorators(node);
496510

497-
this.addPropertyByValidationDecorator(
498-
factory,
499-
'IsIn',
500-
'enum',
501-
decorators,
502-
assignments
503-
);
511+
if (!options.readonly) {
512+
// @IsIn() annotation is not supported in readonly mode
513+
this.addPropertyByValidationDecorator(
514+
factory,
515+
'IsIn',
516+
'enum',
517+
decorators,
518+
assignments,
519+
options
520+
);
521+
}
504522
this.addPropertyByValidationDecorator(
505523
factory,
506524
'Min',
507525
'minimum',
508526
decorators,
509-
assignments
527+
assignments,
528+
options
510529
);
511530
this.addPropertyByValidationDecorator(
512531
factory,
513532
'Max',
514533
'maximum',
515534
decorators,
516-
assignments
535+
assignments,
536+
options
517537
);
518538
this.addPropertyByValidationDecorator(
519539
factory,
520540
'MinLength',
521541
'minLength',
522542
decorators,
523-
assignments
543+
assignments,
544+
options
524545
);
525546
this.addPropertyByValidationDecorator(
526547
factory,
527548
'MaxLength',
528549
'maxLength',
529550
decorators,
530-
assignments
551+
assignments,
552+
options
531553
);
532554
this.addPropertiesByValidationDecorator(
533555
factory,
@@ -564,21 +586,36 @@ export class ModelClassVisitor extends AbstractFileVisitor {
564586
assignments,
565587
(decoratorRef: ts.Decorator) => {
566588
const decoratorArguments = getDecoratorArguments(decoratorRef);
567-
568589
const result = [];
569-
result.push(
570-
factory.createPropertyAssignment(
571-
'minLength',
572-
head(decoratorArguments)
573-
)
574-
);
575590

576-
if (decoratorArguments.length > 1) {
591+
const minLength = head(decoratorArguments);
592+
if (!canReferenceNode(minLength, options)) {
593+
return result;
594+
}
595+
596+
const clonedMinLength = this.clonePrimitiveLiteral(factory, minLength);
597+
if (clonedMinLength) {
577598
result.push(
578-
factory.createPropertyAssignment('maxLength', decoratorArguments[1])
599+
factory.createPropertyAssignment('minLength', clonedMinLength)
579600
);
580601
}
581602

603+
if (decoratorArguments.length > 1) {
604+
const maxLength = decoratorArguments[1];
605+
if (!canReferenceNode(maxLength, options)) {
606+
return result;
607+
}
608+
const clonedMaxLength = this.clonePrimitiveLiteral(
609+
factory,
610+
maxLength
611+
);
612+
if (clonedMaxLength) {
613+
result.push(
614+
factory.createPropertyAssignment('maxLength', clonedMaxLength)
615+
);
616+
}
617+
}
618+
582619
return result;
583620
}
584621
);
@@ -606,7 +643,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
606643
decoratorName: string,
607644
propertyKey: string,
608645
decorators: readonly ts.Decorator[],
609-
assignments: ts.PropertyAssignment[]
646+
assignments: ts.PropertyAssignment[],
647+
options: PluginOptions
610648
) {
611649
this.addPropertiesByValidationDecorator(
612650
factory,
@@ -617,16 +655,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
617655
const argument: ts.Expression = head(
618656
getDecoratorArguments(decoratorRef)
619657
);
620-
const assignment = ts.isNumericLiteral(argument)
621-
? ts.factory.createNumericLiteral(argument.text)
622-
: ts.isStringLiteral(argument)
623-
? ts.factory.createStringLiteral(argument.text)
624-
: argument;
625-
626-
if (assignment) {
627-
return [factory.createPropertyAssignment(propertyKey, assignment)];
658+
const assignment =
659+
this.clonePrimitiveLiteral(factory, argument) ?? argument;
660+
if (!canReferenceNode(assignment, options)) {
661+
return [];
628662
}
629-
return [];
663+
return [factory.createPropertyAssignment(propertyKey, assignment)];
630664
}
631665
);
632666
}
@@ -774,4 +808,28 @@ export class ModelClassVisitor extends AbstractFileVisitor {
774808
}
775809
return typeRef;
776810
}
811+
812+
private clonePrimitiveLiteral(factory: ts.NodeFactory, node: ts.Node) {
813+
const primitiveTypeName = this.getInitializerPrimitiveTypeName(node);
814+
if (!primitiveTypeName) {
815+
return undefined;
816+
}
817+
return createPrimitiveLiteral(factory, node.getText(), primitiveTypeName);
818+
}
819+
820+
private getInitializerPrimitiveTypeName(node: ts.Node) {
821+
if (
822+
ts.isIdentifier(node) &&
823+
(node.text === 'true' || node.text === 'false')
824+
) {
825+
return 'boolean';
826+
}
827+
if (ts.isNumericLiteral(node) || ts.isPrefixUnaryExpression(node)) {
828+
return 'number';
829+
}
830+
if (ts.isStringLiteral(node)) {
831+
return 'string';
832+
}
833+
return undefined;
834+
}
777835
}

‎test/plugin/fixtures/project/cats/dto/create-cat.dto.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
import {
2+
IsIn,
3+
IsNegative,
4+
IsPositive,
5+
Length,
6+
Matches,
7+
Max,
8+
Min
9+
} from 'class-validator';
10+
import { randomUUID } from 'node:crypto';
111
import { ApiExtraModels, ApiProperty } from '../../../../lib';
212
import { ExtraModel } from './extra-model.dto';
313
import { LettersEnum } from './pagination-query.dto';
@@ -8,13 +18,37 @@ export enum CategoryState {
818
DEPRECATED = 'DEPRECATED'
919
}
1020

21+
const MAX_AGE = 200;
22+
1123
@ApiExtraModels(ExtraModel)
1224
export class CreateCatDto {
25+
@IsIn(['a', 'b'])
26+
isIn: string;
27+
28+
@Matches(/^[+]?abc$/)
29+
pattern: string;
30+
31+
@IsPositive()
32+
positive: number = 5;
33+
34+
@IsNegative()
35+
negative: number = -1;
36+
37+
@Length(2)
38+
lengthMin: string;
39+
40+
@Length(3, 5)
41+
lengthMinMax: string;
42+
43+
date = new Date();
44+
1345
@ApiProperty()
14-
readonly name: string;
46+
readonly name: string = randomUUID();
1547

48+
@Min(1)
49+
@Max(MAX_AGE)
1650
@ApiProperty({ minimum: 1, maximum: 200 })
17-
readonly age: number;
51+
readonly age: number = 14;
1852

1953
@ApiProperty({ name: '_breed', type: String })
2054
readonly breed: string;

‎test/plugin/fixtures/serialized-meta.fixture.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,39 @@ export default async () => {
8484
import('./cats/dto/create-cat.dto'),
8585
{
8686
CreateCatDto: {
87+
isIn: { required: true, type: () => String },
88+
pattern: {
89+
required: true,
90+
type: () => String,
91+
pattern: '/^[+]?abc$/'
92+
},
93+
positive: {
94+
required: true,
95+
type: () => Number,
96+
default: 5,
97+
minimum: 1
98+
},
99+
negative: {
100+
required: true,
101+
type: () => Number,
102+
default: -1,
103+
maximum: -1
104+
},
105+
lengthMin: { required: true, type: () => String, minLength: 2 },
106+
lengthMinMax: {
107+
required: true,
108+
type: () => String,
109+
minLength: 3,
110+
maxLength: 5
111+
},
112+
date: { required: true, type: () => Object, default: new Date() },
87113
name: { required: true, type: () => String },
88-
age: { required: true, type: () => Number },
114+
age: {
115+
required: true,
116+
type: () => Number,
117+
default: 14,
118+
minimum: 1
119+
},
89120
breed: { required: true, type: () => String },
90121
tags: { required: false, type: () => [String] },
91122
createdAt: { required: true, type: () => Date },

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ describe('Readonly visitor', () => {
1919
const visitor = new ReadonlyVisitor({
2020
pathToSource: join(__dirname, 'fixtures', 'project'),
2121
introspectComments: true,
22-
dtoFileNameSuffix: ['.dto.ts', '.model.ts', '.class.ts']
22+
dtoFileNameSuffix: ['.dto.ts', '.model.ts', '.class.ts'],
23+
classValidatorShim: true
2324
});
2425
const metadataPrinter = new PluginMetadataPrinter();
2526

@@ -50,6 +51,12 @@ describe('Readonly visitor', () => {
5051
'utf-8'
5152
);
5253

54+
// writeFileSync(
55+
// join(__dirname, 'fixtures', 'serialized-meta.fixture.ts'),
56+
// result,
57+
// 'utf-8'
58+
// );
59+
5360
expect(result).toEqual(expectedOutput);
5461
});
5562
});

0 commit comments

Comments
 (0)
Please sign in to comment.