type-graphql icon indicating copy to clipboard operation
type-graphql copied to clipboard

Inherited nullable field of ObjectType becomes non-nullable in subclass.

Open blevine opened this issue 3 years ago • 3 comments

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:

  1. The gatekeeperFor field on UserType is correctly generated as nullable.
  2. The customer field on both UserType and ViewerType (which is declared in the same way as gatekeeperFor) is correctly generated as nullable.
  3. 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 avatar Sep 22 '21 15:09 blevine

@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...

MichalLytek avatar Nov 07 '21 14:11 MichalLytek

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;
}

blevine avatar Nov 07 '21 15:11 blevine

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?

blevine avatar Feb 08 '22 20:02 blevine