Skip to content

Commit

Permalink
feat(plugin): Update plugin with null union support
Browse files Browse the repository at this point in the history
Prior to these changes, the GraphQL plugin only supported decorating field properties when the
type of a class property was optional. However, sometimes typescript users like to use union
types that include null instead to indicate a nullable field. This change makes it possible to
support nullable fields when the union includes a null type.
  • Loading branch information
vinnymac committed Apr 3, 2021
1 parent 0b90900 commit 40a5589
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 2 deletions.
8 changes: 8 additions & 0 deletions lib/plugin/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ export function isEnumLiteral(type: Type) {
return hasFlag(type, TypeFlags.EnumLiteral) && !type.isUnion();
}

export function isNull(type: Type) {
if (type.isUnion()) {
return Boolean(type.types.find((t) => hasFlag(t, TypeFlags.Null)));
} else {
return hasFlag(type, TypeFlags.Null);
}
}

export function hasFlag(type: Type, flag: TypeFlags) {
return (type.flags & flag) === flag;
}
Expand Down
5 changes: 3 additions & 2 deletions lib/plugin/visitors/model-class.visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as ts from 'typescript';
import { HideField } from '../../decorators';
import { PluginOptions } from '../merge-options';
import { METADATA_FACTORY_NAME } from '../plugin-constants';
import { getDescriptionOfNode } from '../utils/ast-utils';
import { getDescriptionOfNode, isNull } from '../utils/ast-utils';
import {
getDecoratorOrUndefinedByNames,
getTypeReferenceAsString,
Expand Down Expand Up @@ -134,7 +134,8 @@ export class ModelClassVisitor {
sourceFile: ts.SourceFile,
pluginOptions: PluginOptions,
): ts.ObjectLiteralExpression {
const isRequired = !node.questionToken;
const type = typeChecker.getTypeAtLocation(node);
const isRequired = !node.questionToken && !isNull(type);

const properties = [
...existingProperties,
Expand Down
55 changes: 55 additions & 0 deletions tests/plugin/fixtures/create-null-cat.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export const createNullCatDtoText = `
enum Status {
ENABLED,
DISABLED
}
enum OneValueEnum {
ONE
}
interface Node {
id: number;
}
export class CreateCatDto {
name: string | null;
age: number = 3;
tags: string[];
status: Status = Status.ENABLED;
status2?: Status | null;
statusArr?: Status[];
readonly breed?: string;
nodes: Node[];
date: Date;
nil: null;
@HideField()
hidden: number;
static staticProperty: string;
}
`;

export const createNullCatDtoTextTranspiled = `var Status;
(function (Status) {
Status[Status[\"ENABLED\"] = 0] = \"ENABLED\";
Status[Status[\"DISABLED\"] = 1] = \"DISABLED\";
})(Status || (Status = {}));
var OneValueEnum;
(function (OneValueEnum) {
OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\";
})(OneValueEnum || (OneValueEnum = {}));
export class CreateCatDto {
constructor() {
this.age = 3;
this.status = Status.ENABLED;
}
static _GRAPHQL_METADATA_FACTORY() {
return { name: { nullable: true, type: () => Object }, age: { nullable: false, type: () => Number }, tags: { nullable: false, type: () => [String] }, status: { nullable: false, type: () => Status }, status2: { nullable: true, type: () => Object }, statusArr: { nullable: true, type: () => [Status] }, breed: { nullable: true, type: () => String }, nodes: { nullable: false, type: () => [Object] }, date: { nullable: false, type: () => Date }, nil: { nullable: true, type: () => Object } };
}
}
__decorate([
HideField()
], CreateCatDto.prototype, \"hidden\", void 0);
`
25 changes: 25 additions & 0 deletions tests/plugin/model-class-visitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
es5CreateCatDtoText,
es5CreateCatDtoTextTranspiled,
} from './fixtures/es5-class.dto';
import {
createNullCatDtoText,
createNullCatDtoTextTranspiled
} from './fixtures/create-null-cat.dto';

describe('API model properties', () => {
it('should add the metadata factory when no decorators exist', () => {
Expand Down Expand Up @@ -84,4 +88,25 @@ describe('API model properties', () => {
});
expect(result.outputText).toEqual(es5CreateCatDtoTextTranspiled);
});

it('should add the decorator when union type includes null', () => {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
newLine: ts.NewLineKind.LineFeed,
noEmitHelpers: true,
strict: true,
};
const filename = 'create-cat.input.ts';
const fakeProgram = ts.createProgram([filename], options);

const result = ts.transpileModule(createNullCatDtoText, {
compilerOptions: options,
fileName: filename,
transformers: {
before: [before({}, fakeProgram)],
},
});
expect(result.outputText).toEqual(createNullCatDtoTextTranspiled);
});
});

0 comments on commit 40a5589

Please sign in to comment.