Skip to content

Commit 0038627

Browse files
committedAug 19, 2020
feat(@nestjs/swagger): CLI plugin no supports nullable
`nullableValOpt?: string | null;` now results in `type`=`string`, `nullable`=`true` before this change, the type was object this fixes #912
1 parent c417059 commit 0038627

File tree

3 files changed

+129
-40
lines changed

3 files changed

+129
-40
lines changed
 

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

+83-40
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
123123
hostFilename = ''
124124
): ts.ObjectLiteralExpression {
125125
const isRequired = !node.questionToken;
126-
127126
let properties = [
128127
...existingProperties,
129128
!hasPropertyKey('required', existingProperties) &&
130129
ts.createPropertyAssignment('required', ts.createLiteral(isRequired)),
131-
this.createTypePropertyAssignment(
132-
node,
130+
...this.createTypePropertyAssignments(
131+
node.type,
133132
typeChecker,
134133
existingProperties,
135134
hostFilename
@@ -151,64 +150,108 @@ export class ModelClassVisitor extends AbstractFileVisitor {
151150
return objectLiteral;
152151
}
153152

154-
createTypePropertyAssignment(
155-
node: ts.PropertyDeclaration | ts.PropertySignature,
153+
/**
154+
* The function returns an array with 0, 1 or 2 PropertyAssignments.
155+
* Possible keys:
156+
* - 'type'
157+
* - 'nullable'
158+
*/
159+
private createTypePropertyAssignments(
160+
node: ts.TypeNode,
156161
typeChecker: ts.TypeChecker,
157162
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
158163
hostFilename: string
159-
) {
164+
): ts.PropertyAssignment[] {
160165
const key = 'type';
161166
if (hasPropertyKey(key, existingProperties)) {
162-
return undefined;
163-
}
164-
const type = typeChecker.getTypeAtLocation(node);
165-
if (!type) {
166-
return undefined;
167+
return [];
167168
}
168-
if (node.type && ts.isTypeLiteralNode(node.type)) {
169-
const propertyAssignments = Array.from(node.type.members || []).map(
170-
(member) => {
171-
const literalExpr = this.createDecoratorObjectLiteralExpr(
172-
member as ts.PropertySignature,
169+
if (node) {
170+
if (ts.isTypeLiteralNode(node)) {
171+
const propertyAssignments = Array.from(node.members || []).map(
172+
(member) => {
173+
const literalExpr = this.createDecoratorObjectLiteralExpr(
174+
member as ts.PropertySignature,
175+
typeChecker,
176+
existingProperties,
177+
{},
178+
hostFilename
179+
);
180+
return ts.createPropertyAssignment(
181+
ts.createIdentifier(member.name.getText()),
182+
literalExpr
183+
);
184+
}
185+
);
186+
return [
187+
ts.createPropertyAssignment(
188+
key,
189+
ts.createArrowFunction(
190+
undefined,
191+
undefined,
192+
[],
193+
undefined,
194+
undefined,
195+
ts.createParen(ts.createObjectLiteral(propertyAssignments))
196+
)
197+
)
198+
];
199+
} else if (ts.isUnionTypeNode(node)) {
200+
const nullableType = node.types.find(
201+
(type) => type.kind === ts.SyntaxKind.NullKeyword
202+
);
203+
const isNullable = !!nullableType;
204+
const remainingTypes = node.types.filter(
205+
(item) => item !== nullableType
206+
);
207+
/**
208+
* when we have more than 1 type left, we could use oneOf
209+
*/
210+
if (remainingTypes.length === 1) {
211+
const remainingTypesProperties = this.createTypePropertyAssignments(
212+
remainingTypes[0],
173213
typeChecker,
174214
existingProperties,
175-
{},
176215
hostFilename
177216
);
178-
return ts.createPropertyAssignment(
179-
ts.createIdentifier(member.name.getText()),
180-
literalExpr
217+
218+
const resultArray = new Array<ts.PropertyAssignment>(
219+
...remainingTypesProperties
181220
);
221+
if (isNullable) {
222+
const nullablePropertyAssignment = ts.createPropertyAssignment(
223+
'nullable',
224+
ts.createTrue()
225+
);
226+
resultArray.push(nullablePropertyAssignment);
227+
}
228+
return resultArray;
182229
}
183-
);
184-
return ts.createPropertyAssignment(
230+
}
231+
}
232+
233+
const type = typeChecker.getTypeAtLocation(node);
234+
if (!type) {
235+
return [];
236+
}
237+
let typeReference = getTypeReferenceAsString(type, typeChecker);
238+
if (!typeReference) {
239+
return [];
240+
}
241+
typeReference = replaceImportPath(typeReference, hostFilename);
242+
return [
243+
ts.createPropertyAssignment(
185244
key,
186245
ts.createArrowFunction(
187246
undefined,
188247
undefined,
189248
[],
190249
undefined,
191250
undefined,
192-
ts.createParen(ts.createObjectLiteral(propertyAssignments))
251+
ts.createIdentifier(typeReference)
193252
)
194-
);
195-
}
196-
let typeReference = getTypeReferenceAsString(type, typeChecker);
197-
if (!typeReference) {
198-
return undefined;
199-
}
200-
typeReference = replaceImportPath(typeReference, hostFilename);
201-
return ts.createPropertyAssignment(
202-
key,
203-
ts.createArrowFunction(
204-
undefined,
205-
undefined,
206-
[],
207-
undefined,
208-
undefined,
209-
ts.createIdentifier(typeReference)
210253
)
211-
);
254+
];
212255
}
213256

214257
createEnumPropertyAssignment(

‎test/plugin/fixtures/nullable.dto.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const nullableDtoText = `
2+
export class NullableDto {
3+
@ApiProperty()
4+
stringValue: string | null;
5+
@ApiProperty()
6+
stringArr: string[] | null;
7+
}
8+
`;
9+
10+
export const nullableDtoTextTranspiled = `export class NullableDto {
11+
static _OPENAPI_METADATA_FACTORY() {
12+
return { stringValue: { required: true, type: () => String, nullable: true }, stringArr: { required: true, type: () => [String], nullable: true } };
13+
}
14+
}
15+
__decorate([
16+
ApiProperty()
17+
], NullableDto.prototype, "stringValue", void 0);
18+
__decorate([
19+
ApiProperty()
20+
], NullableDto.prototype, "stringArr", void 0);
21+
`;

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

+25
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,33 @@ import {
1616
es5CreateCatDtoText,
1717
es5CreateCatDtoTextTranspiled
1818
} from './fixtures/es5-class.dto';
19+
import {
20+
nullableDtoText,
21+
nullableDtoTextTranspiled
22+
} from './fixtures/nullable.dto';
1923

2024
describe('API model properties', () => {
25+
it('should understand nullable', () => {
26+
const options: ts.CompilerOptions = {
27+
module: ts.ModuleKind.ESNext,
28+
target: ts.ScriptTarget.ESNext,
29+
newLine: ts.NewLineKind.LineFeed,
30+
noEmitHelpers: true,
31+
strict: true
32+
};
33+
const filename = 'nullable.dto.ts';
34+
const fakeProgram = ts.createProgram([filename], options);
35+
36+
const result = ts.transpileModule(nullableDtoText, {
37+
compilerOptions: options,
38+
fileName: filename,
39+
transformers: {
40+
before: [before({ classValidatorShim: true }, fakeProgram)]
41+
}
42+
});
43+
expect(result.outputText).toEqual(nullableDtoTextTranspiled);
44+
});
45+
2146
it('should add the metadata factory when no decorators exist', () => {
2247
const options: ts.CompilerOptions = {
2348
module: ts.ModuleKind.ESNext,

0 commit comments

Comments
 (0)
Please sign in to comment.