type-graphql
type-graphql copied to clipboard
Inherited nullable field of ObjectType becomes non-nullable in subclass.
I have an ObjectType declared with a nullable field. An ObjectType declared as a subclass of that ObjectType which defines no additional fields generates a schema in which that field is non-nullable.
@ObjectType('UserType')
export class UserType extends withBaseEntity(BaseUser) {
@Field({nullable: true})
fullName(): string {
return `${this.firstName} ${this.lastName}`
}
@Field(_type => CustomerType, {nullable: true})
customer?: CustomerType
@Field(type => [String], {nullable: 'items'})
roles: string[];
@Field(_type => CustomerType, {nullable: true})
gatekeeperFor?: CustomerType
gatekeeperForId?: string;
}
@ObjectType('ViewerType')
export class ViewerType extends UserType {
}
Expected Behavior In the generated schema, I would expect the gatekeeperFor field on ViewerType to be nullable, however it's non-nullable. Note also:
- The gatekeeperFor field on UserType is correctly generated as nullable.
- The customer field on both UserType and ViewerType (which is declared in the same way as gatekeeperFor) is correctly generated as nullable.
- Declaring the gatekeeperFor field in the ViewerType subclass works around this problem.
Environment (please complete the following information):
- OS: MacOS 11.3
- Node 14.17.3
- Package version 1.1.1
- TypeScript 4.4.2
@blevine Sorry but I'm unable to reproduce:
type ViewerType {
fullName: String
customer: CustomerType
roles: [String]!
gatekeeperFor: CustomerType
}
My reproduction:
// tslint:disable: member-ordering
import "reflect-metadata";
import { GraphQLSchema, IntrospectionObjectType, IntrospectionSchema, printType } from "graphql";
import { inspect } from "util";
import {
Field,
getMetadataStorage,
ObjectType,
Query,
registerEnumType,
Resolver,
} from "../../src";
import { getSchemaInfo } from "../helpers/getSchemaInfo";
describe("debug", () => {
let schema: GraphQLSchema;
let schemaIntrospection: IntrospectionSchema;
beforeAll(async () => {
getMetadataStorage().clear();
enum CustomerType {
NORMAL,
SPECIAL,
}
registerEnumType(CustomerType, { name: "CustomerType" });
@ObjectType("UserType")
class UserType {
@Field({ nullable: true })
fullName(): string {
return `fullname`;
}
@Field(_type => CustomerType, { nullable: true })
customer?: CustomerType;
@Field(type => [String], { nullable: "items" })
roles: string[];
@Field(_type => CustomerType, { nullable: true })
gatekeeperFor?: CustomerType;
gatekeeperForId?: string;
}
@ObjectType("ViewerType")
class ViewerType extends UserType {}
@Resolver()
class DebugResolver {
@Query()
debugQuery(): boolean {
return true;
}
}
({ schema, schemaIntrospection } = await getSchemaInfo({
resolvers: [DebugResolver],
orphanedTypes: [UserType, ViewerType],
}));
});
it("should properly emit inherited nullable field of ObjectType", async () => {
const viewerType = schema.getType("ViewerType")!;
console.log(printType(viewerType));
const introspectedViewerType = schemaIntrospection.types.find(
it => it.name === "ViewerType",
) as IntrospectionObjectType;
console.log(inspect(introspectedViewerType, { depth: null }));
});
});
Maybe your withBaseEntity
is faulty...
Here is the definition of BaseUser and withBaseEntity (apologies for not including it earlier). Do you see anything wrong there?
@InputType('UserInput', {description: 'New user data', isAbstract: true})
@ObjectType('UserType', {description: 'A User', isAbstract: true})
export default class BaseUser {
@Field({nullable: true})
firstName?: string;
@Field({nullable: true})
lastName?: string;
@Field({nullable: true})
userName?: string;
@Field({nullable: true})
email?: string;
}
// Mixin that adds base entity fields to the class
export default function withBaseEntity<TClassType extends ClassType>(BaseClass: TClassType) {
@ObjectType({ isAbstract: true })
@InputType({ isAbstract: true })
class BaseEntity extends BaseClass {
@Field(type => ID)
id: string;
@Field({nullable: true})
createdAt?: Date;
@Field({nullable: true})
updatedAt?: Date;
}
return BaseEntity;
}
Looks like this is still a problem. Have you had the opportunity to look at my withBaseEntity function to see if that's the culprit?