Skip to content

Commit 266dc31

Browse files
committedAug 20, 2020
Merge branch 'tmtron-912_cli_nullable'
2 parents 912c215 + 9f2028b commit 266dc31

File tree

3 files changed

+128
-40
lines changed

3 files changed

+128
-40
lines changed
 

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

+82-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,107 @@ 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+
if (remainingTypes.length === 1) {
210+
const remainingTypesProperties = this.createTypePropertyAssignments(
211+
remainingTypes[0],
173212
typeChecker,
174213
existingProperties,
175-
{},
176214
hostFilename
177215
);
178-
return ts.createPropertyAssignment(
179-
ts.createIdentifier(member.name.getText()),
180-
literalExpr
216+
217+
const resultArray = new Array<ts.PropertyAssignment>(
218+
...remainingTypesProperties
181219
);
220+
if (isNullable) {
221+
const nullablePropertyAssignment = ts.createPropertyAssignment(
222+
'nullable',
223+
ts.createTrue()
224+
);
225+
resultArray.push(nullablePropertyAssignment);
226+
}
227+
return resultArray;
182228
}
183-
);
184-
return ts.createPropertyAssignment(
229+
}
230+
}
231+
232+
const type = typeChecker.getTypeAtLocation(node);
233+
if (!type) {
234+
return [];
235+
}
236+
let typeReference = getTypeReferenceAsString(type, typeChecker);
237+
if (!typeReference) {
238+
return [];
239+
}
240+
typeReference = replaceImportPath(typeReference, hostFilename);
241+
return [
242+
ts.createPropertyAssignment(
185243
key,
186244
ts.createArrowFunction(
187245
undefined,
188246
undefined,
189247
[],
190248
undefined,
191249
undefined,
192-
ts.createParen(ts.createObjectLiteral(propertyAssignments))
250+
ts.createIdentifier(typeReference)
193251
)
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)
210252
)
211-
);
253+
];
212254
}
213255

214256
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,6 +16,10 @@ 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', () => {
2125
it('should add the metadata factory when no decorators exist', () => {
@@ -101,4 +105,25 @@ describe('API model properties', () => {
101105
});
102106
expect(result.outputText).toEqual(es5CreateCatDtoTextTranspiled);
103107
});
108+
109+
it('should support & understand nullable type unions', () => {
110+
const options: ts.CompilerOptions = {
111+
module: ts.ModuleKind.ESNext,
112+
target: ts.ScriptTarget.ESNext,
113+
newLine: ts.NewLineKind.LineFeed,
114+
noEmitHelpers: true,
115+
strict: true
116+
};
117+
const filename = 'nullable.dto.ts';
118+
const fakeProgram = ts.createProgram([filename], options);
119+
120+
const result = ts.transpileModule(nullableDtoText, {
121+
compilerOptions: options,
122+
fileName: filename,
123+
transformers: {
124+
before: [before({ classValidatorShim: true }, fakeProgram)]
125+
}
126+
});
127+
expect(result.outputText).toEqual(nullableDtoTextTranspiled);
128+
});
104129
});

0 commit comments

Comments
 (0)
Please sign in to comment.