Skip to content

Commit bde4830

Browse files
committed
fix(plugin): wrap enum property values in functions
- Check identifierText.includes('await ') instead of typeName.includes('import(') - Update test fixtures to conditionally add async based on esmCompatible - Add regression test for same-file array types (#3630)
1 parent 5f23060 commit bde4830

File tree

6 files changed

+144
-42
lines changed

6 files changed

+144
-42
lines changed

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,14 @@ export class ModelClassVisitor extends AbstractFileVisitor {
536536
this._typeImports
537537
);
538538

539+
// Only add async modifier if the identifier contains await (for ESM dynamic imports)
540+
const identifierText = identifier.text || '';
541+
const modifiers = identifierText.includes('await ')
542+
? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
543+
: undefined;
544+
539545
const initializer = factory.createArrowFunction(
540-
undefined,
546+
modifiers,
541547
undefined,
542548
[],
543549
undefined,
@@ -715,10 +721,6 @@ export class ModelClassVisitor extends AbstractFileVisitor {
715721

716722
const typeReferenceDescriptor = { typeName: getText(type, typeChecker) };
717723

718-
const modifiers = typeReferenceDescriptor.typeName.includes('import(')
719-
? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
720-
: undefined;
721-
722724
const enumIdentifier = typeReferenceToIdentifier(
723725
typeReferenceDescriptor,
724726
hostFilename,
@@ -728,6 +730,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
728730
this._typeImports
729731
);
730732

733+
// Only add async modifier if the identifier contains await (for ESM dynamic imports)
734+
const enumIdentifierText = enumIdentifier.text || '';
735+
const modifiers = enumIdentifierText.includes('await ')
736+
? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
737+
: undefined;
738+
731739
const initializer = factory.createArrowFunction(
732740
modifiers,
733741
undefined,

test/plugin/fixtures/parameter-property.dto.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class ParameterPropertyDto {
3737
this.protectedValue = protectedValue;
3838
}
3939
static _OPENAPI_METADATA_FACTORY() {
40-
return { readonlyValue: { required: false, type: () => String }, privateValue: { required: true, type: () => String, nullable: true }, publicValue: { required: true, type: () => [${fileImport}.ItemDto] }, protectedValue: { required: true, type: () => String, default: "1234" } };
40+
return { readonlyValue: { required: false, type: () => String }, privateValue: { required: true, type: () => String, nullable: true }, publicValue: { required: true, type: ${esmCompatible ? 'async ' : ''}() => [${fileImport}.ItemDto] }, protectedValue: { required: true, type: () => String, default: "1234" } };
4141
}
4242
}
4343
export var LettersEnum;
@@ -51,7 +51,7 @@ export class ItemDto {
5151
this.enumValue = enumValue;
5252
}
5353
static _OPENAPI_METADATA_FACTORY() {
54-
return { enumValue: { required: true, enum: async () => ${fileImport}.LettersEnum } };
54+
return { enumValue: { required: true, enum: ${esmCompatible ? 'async ' : ''}() => ${fileImport}.LettersEnum } };
5555
}
5656
}
5757
`;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Regression test for issue #3630
3+
* Same-file array types should not have async modifier added
4+
* https://github.com/nestjs/swagger/issues/3630
5+
*/
6+
7+
export const sameFileArrayDtoText = `
8+
export class App {
9+
/**
10+
* Name of the app.
11+
* @example "example-app"
12+
*/
13+
public name!: string;
14+
}
15+
16+
export class AppDataResult {
17+
/**
18+
* Returned rows.
19+
*/
20+
public items!: App[];
21+
22+
/**
23+
* Total count.
24+
* @example 100
25+
*/
26+
public total!: number;
27+
}
28+
`;
29+
30+
export const sameFileArrayDtoTextTranspiled = `import * as openapi from "@nestjs/swagger";
31+
export class App {
32+
static _OPENAPI_METADATA_FACTORY() {
33+
return { name: { required: true, type: () => String, title: "Name of the app.", example: "example-app" } };
34+
}
35+
}
36+
export class AppDataResult {
37+
static _OPENAPI_METADATA_FACTORY() {
38+
return { items: { required: true, type: () => [require("./same-file-array.dto").App], title: "Returned rows." }, total: { required: true, type: () => Number, title: "Total count.", example: 100 } };
39+
}
40+
}
41+
`;
42+
43+
// ESM version - should have async modifier because it uses await import()
44+
export const sameFileArrayDtoTextTranspiledEsm = `import * as openapi from "@nestjs/swagger";
45+
export class App {
46+
static _OPENAPI_METADATA_FACTORY() {
47+
return { name: { required: true, type: () => String, title: "Name of the app.", example: "example-app" } };
48+
}
49+
}
50+
export class AppDataResult {
51+
static _OPENAPI_METADATA_FACTORY() {
52+
return { items: { required: true, type: async () => [(await import("./same-file-array.dto.js")).App], title: "Returned rows." }, total: { required: true, type: () => Number, title: "Total count.", example: 100 } };
53+
}
54+
}
55+
`;

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

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,16 @@ export default async () => {
2323
constrainedLimit: { required: false, type: () => Number },
2424
enum: {
2525
required: true,
26-
enum: async () =>
27-
t['./cats/dto/pagination-query.dto.js'].LettersEnum
26+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum
2827
},
2928
enumArr: {
3029
required: true,
31-
enum: async () =>
32-
t['./cats/dto/pagination-query.dto.js'].LettersEnum,
30+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum,
3331
isArray: true
3432
},
3533
letters: {
3634
required: true,
37-
enum: async () =>
38-
t['./cats/dto/pagination-query.dto.js'].LettersEnum,
35+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum,
3936
isArray: true
4037
},
4138
beforeDate: { required: true, type: () => Date },
@@ -65,13 +62,11 @@ export default async () => {
6562
options: { required: false, type: () => [Object] },
6663
enum: {
6764
required: true,
68-
enum: async () =>
69-
t['./cats/dto/pagination-query.dto.js'].LettersEnum
65+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum
7066
},
7167
enumArr: {
7268
required: true,
73-
enum: async () =>
74-
t['./cats/dto/pagination-query.dto.js'].LettersEnum
69+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum
7570
},
7671
uppercaseString: { required: true, type: () => String },
7772
lowercaseString: { required: true, type: () => String },
@@ -144,25 +139,21 @@ export default async () => {
144139
options: { required: false, type: () => [Object] },
145140
enum: {
146141
required: true,
147-
enum: async () =>
148-
t['./cats/dto/pagination-query.dto.js'].LettersEnum
142+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum
149143
},
150144
state: {
151145
required: false,
152146
description: 'Available language in the application',
153147
example: 'FR',
154-
enum: async () =>
155-
t['./cats/dto/create-cat.dto.js'].CategoryState
148+
enum: () => t['./cats/dto/create-cat.dto.js'].CategoryState
156149
},
157150
enumArr: {
158151
required: true,
159-
enum: async () =>
160-
t['./cats/dto/pagination-query.dto.js'].LettersEnum
152+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum
161153
},
162154
enumArr2: {
163155
required: true,
164-
enum: async () =>
165-
t['./cats/dto/pagination-query.dto.js'].LettersEnum,
156+
enum: () => t['./cats/dto/pagination-query.dto.js'].LettersEnum,
166157
isArray: true
167158
},
168159
tag: {

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

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,16 @@ export default async () => {
2121
constrainedLimit: { required: false, type: () => Number },
2222
enum: {
2323
required: true,
24-
enum: async () =>
25-
t['./cats/dto/pagination-query.dto'].LettersEnum
24+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum
2625
},
2726
enumArr: {
2827
required: true,
29-
enum: async () =>
30-
t['./cats/dto/pagination-query.dto'].LettersEnum,
28+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum,
3129
isArray: true
3230
},
3331
letters: {
3432
required: true,
35-
enum: async () =>
36-
t['./cats/dto/pagination-query.dto'].LettersEnum,
33+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum,
3734
isArray: true
3835
},
3936
beforeDate: { required: true, type: () => Date },
@@ -63,13 +60,11 @@ export default async () => {
6360
options: { required: false, type: () => [Object] },
6461
enum: {
6562
required: true,
66-
enum: async () =>
67-
t['./cats/dto/pagination-query.dto'].LettersEnum
63+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum
6864
},
6965
enumArr: {
7066
required: true,
71-
enum: async () =>
72-
t['./cats/dto/pagination-query.dto'].LettersEnum
67+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum
7368
},
7469
uppercaseString: { required: true, type: () => String },
7570
lowercaseString: { required: true, type: () => String },
@@ -142,24 +137,21 @@ export default async () => {
142137
options: { required: false, type: () => [Object] },
143138
enum: {
144139
required: true,
145-
enum: async () =>
146-
t['./cats/dto/pagination-query.dto'].LettersEnum
140+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum
147141
},
148142
state: {
149143
required: false,
150144
description: 'Available language in the application',
151145
example: 'FR',
152-
enum: async () => t['./cats/dto/create-cat.dto'].CategoryState
146+
enum: () => t['./cats/dto/create-cat.dto'].CategoryState
153147
},
154148
enumArr: {
155149
required: true,
156-
enum: async () =>
157-
t['./cats/dto/pagination-query.dto'].LettersEnum
150+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum
158151
},
159152
enumArr2: {
160153
required: true,
161-
enum: async () =>
162-
t['./cats/dto/pagination-query.dto'].LettersEnum,
154+
enum: () => t['./cats/dto/pagination-query.dto'].LettersEnum,
163155
isArray: true
164156
},
165157
tag: {

test/plugin/model-class-visitor.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ import {
4848
stringLiteralDtoText,
4949
stringLiteralDtoTextTranspiled
5050
} from './fixtures/string-literal.dto';
51+
import {
52+
sameFileArrayDtoText,
53+
sameFileArrayDtoTextTranspiled,
54+
sameFileArrayDtoTextTranspiledEsm
55+
} from './fixtures/same-file-array.dto';
5156

5257
describe('API model properties', () => {
5358
it('should add the metadata factory when no decorators exist, and generated propertyKey is title', () => {
@@ -311,6 +316,57 @@ describe('API model properties', () => {
311316
);
312317
});
313318

319+
it('should not add async modifier for same-file array types (regression #3630)', () => {
320+
const options: ts.CompilerOptions = {
321+
module: ts.ModuleKind.ES2020,
322+
target: ts.ScriptTarget.ES2020,
323+
newLine: ts.NewLineKind.LineFeed,
324+
noEmitHelpers: true,
325+
experimentalDecorators: true,
326+
strict: true
327+
};
328+
const filename = 'same-file-array.dto.ts';
329+
const fakeProgram = ts.createProgram([filename], options);
330+
331+
const result = ts.transpileModule(sameFileArrayDtoText, {
332+
compilerOptions: options,
333+
fileName: filename,
334+
transformers: {
335+
before: [
336+
before(
337+
{
338+
introspectComments: true,
339+
classValidatorShim: true,
340+
dtoKeyOfComment: 'title'
341+
},
342+
fakeProgram
343+
)
344+
]
345+
}
346+
});
347+
expect(result.outputText).toEqual(sameFileArrayDtoTextTranspiled);
348+
349+
// ESM version - should have async modifier because it uses await import()
350+
const esmResult = ts.transpileModule(sameFileArrayDtoText, {
351+
compilerOptions: options,
352+
fileName: filename,
353+
transformers: {
354+
before: [
355+
before(
356+
{
357+
introspectComments: true,
358+
classValidatorShim: true,
359+
dtoKeyOfComment: 'title',
360+
esmCompatible: true
361+
},
362+
fakeProgram
363+
)
364+
]
365+
}
366+
});
367+
expect(esmResult.outputText).toEqual(sameFileArrayDtoTextTranspiledEsm);
368+
});
369+
314370
it('should ignore Exclude decorator', () => {
315371
const options: ts.CompilerOptions = {
316372
module: ts.ModuleKind.ES2020,

0 commit comments

Comments
 (0)