Type-graphql: Circular ref in Union Type breaks it

Created on 25 Jun 2019  路  21Comments  路  Source: MichalLytek/type-graphql

Describe the bug

When I use union type composed of fields that have circular references to each other, the schema doesn't compile, throws TypeError: Cannot read property 'type' of undefined

To Reproduce

@ObjectType()
export class A {
  @Field(type => B)
  b: B;
}
@ObjectType()
export class B {
  @Field(type => A)
  a: A;
}



md5-63a0a67f7fe508b8ed6d0b9c3f95be54



export type ABUnionType = A | B;

export const ABUnion = createUnionType({
  name: 'Union',
  types: [A, B],
  resolveType: value => {
    return PageType;
  },
});



md5-63a0a67f7fe508b8ed6d0b9c3f95be54



@ObjectType()
export class C {
  @Field(type => ABUnionType)
  ab: ABUnion;
}



md5-44f72c280319ef4013bfbeb2d1803151



api_1       |     TypeError: Cannot read property 'type' of undefined
api_1       | 
api_1       |       at unionMetadata.classTypes.map.objectType (../node_modules/type-graphql/dist/schema/schema-generator.js:64:138)
api_1       |           at Array.map (<anonymous>)
api_1       |       at types (../node_modules/type-graphql/dist/schema/schema-generator.js:64:59)
api_1       |       at resolveThunk (../node_modules/graphql/type/definition.js:381:40)
api_1       |       at defineTypes (../node_modules/graphql/type/definition.js:761:15)
api_1       |       at GraphQLUnionType.getTypes (../node_modules/graphql/type/definition.js:731:26)
api_1       |       at typeMapReducer (../node_modules/graphql/type/schema.js:307:23)
api_1       |       at typeMapReducer (../node_modules/graphql/type/schema.js:295:12)
api_1       |       at typeMapReducer (../node_modules/graphql/type/schema.js:330:22)
api_1       |       at typeMapReducer (../node_modules/graphql/type/schema.js:295:12)

Enviorment (please complete the following information):

  • OS: Mac OS (running Node image in Docker container)
  • Node 10.15.0
  • Package version 0.17.3
  • TypeScript version 3.0.1

Additional context

I am doing this within NestJS, I am not sure if this problem isn't specific to it. I didn't test this without it.

It compiles successfully once I remove the union field from type C, or when I remove one of the references from one of the types A or B.

Bug Community Solved

All 21 comments

I haven't tough about that - it should be types: () => [A, B] to support lazy loading of classes and circular references.

What is your real world use case? It is really rare.

@19majkel94 I wouldn't say it has to be so rare...
My case:
I have a little facebook-like system and I have User, Page and Post types.
Post has author field of union type User | Page, depending on whether the post was published by a page managed by a user or by a regular user on his own.
Then User and Page have followedPages and followers fields respectively, which creates this circular ref.

I wouldn't say it is so rare...

You are the first one after 1,5 years 馃槈

The fix is easy but it's a breaking change, so it will need an update of deps from nestjs side too.

Thanks!

You are the first one after 1,5 years 馃槈

I think that's partially because most people might simply use page and user fields separately instead of union type for these cases and then deal with it on the frontend side, which is what we're going to do in the meantime so it's not that big of a deal, but IMHO using GraphQL union is the cleaner choice.

It's true that I've ran into this by a coincidence and took me a long time to find out where the problem actually is, but I believe as this takes on more people would run into this.

The cases I can think of:

  • My case
  • Tagging users, groups, pages in social systems that will have relations between each other
  • Not to mention even looking at the Union examples in official GraphQL docs themselves https://graphql.org/learn/schema/#union-types you can IMHO imagine how Human and Starship would have circular refs to each other (e. g. ownedShips, owner).
{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

I have come across a similar problem just today, I save notes which can be a child to a few different Entities. So my model field could be in this simple case a Contact or a User, but it seems I have no way to express that to Type-Graphql ?

      @ObjectType()
      export class Note extends Typegoose {

        @Field()
        @Property()
        body: string

        @Field()
        @Property({ required: true, refPath: 'modelRef' })
        model: Ref<Contact | User>

        @Field(type => String)
        @Property({required: true, enum: Object.values(ModelRef), index: true})
        modelRef: ModelRef

      }

@RyannGalea

export const NoteModelUnion = createUnionType({
  name: 'NoteModel',
  types: [Contact, User],
});

@ObjectType()
export class Note extends Typegoose {
  @Field()
  @Property()
  body: string;

  @Field(type => NoteModelUnion )
  @Property({ required: true, refPath: "modelRef" })
  model: Ref<typeof NoteModelUnion>;

  @Field(type => String)
  @Property({ required: true, enum: Object.values(ModelRef), index: true })
  modelRef: ModelRef;
}

I was trying that and getting this error @19majkel94

      (node:7904) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'type' of undefined
          at unionMetadata.classTypes.map.objectType (F:\prosperty\server\node_modules\type-graphql\dist\schema\schema-generator.js:64:138)
          at Array.map (<anonymous>)
          at types (F:\prosperty\server\node_modules\type-graphql\dist\schema\schema-generator.js:64:59)
          at resolveThunk (F:\prosperty\server\node_modules\graphql\type\definition.js:381:40)
          at defineTypes (F:\prosperty\server\node_modules\graphql\type\definition.js:761:15)
          at GraphQLUnionType.getTypes (F:\prosperty\server\node_modules\graphql\type\definition.js:731:26)
          at typeMapReducer (F:\prosperty\server\node_modules\graphql\type\schema.js:306:23)
          at typeMapReducer (F:\prosperty\server\node_modules\graphql\type\schema.js:329:22)
          at typeMapReducer (F:\prosperty\server\node_modules\graphql\type\schema.js:294:12)
          at typeMapReducer (F:\prosperty\server\node_modules\graphql\type\schema.js:329:22)
          at typeMapReducer (F:\prosperty\server\node_modules\graphql\type\schema.js:294:12)
          at typeMapReducer (F:\prosperty\server\node_modules\graphql\type\schema.js:294:12)
          at typeMapReducer (F:\prosperty\server\node_modules\graphql\type\schema.js:329:22)
          at Array.reduce (<anonymous>)
          at new GraphQLSchema (F:\prosperty\server\node_modules\graphql\type\schema.js:145:28)
          at Function.generateFromMetadataSync (F:\prosperty\server\node_modules\type-graphql\dist\schema\schema-generator.js:31:24)

@RyannGalea
Because "circular ref in Union Type breaks it". Wait for a proper fix or use a workaround.

Ok thanks, I understand this is the same issue now. Thank you @19majkel94

I am still facing this problem currently where I am getting an error of "Organization.members"'s Type is invalid! Type is: "undefined" after defining a circular union as follows:

import { createUnionType } from 'type-graphql';
import { User } from '../../../account/entities/user.schema';
import { Account } from '../../../account/entities/account.schema';
import { Organization } from '../../../account/entities/organization.schema';

export const OwnerUnion = createUnionType({
  name: 'Owner', // the name of the GraphQL union
  types: () => [Account, Organization, User], // function that returns array of object types classes
});

and inside the Organization class i have this:

@modelOptions({ schemaOptions: { timestamps: true } })
@ObjectType()
export class Organization {
.......
@Field(() => [User])
  @IsMongoId({
    each: true,
  })
  @arrayProp({
    itemsRef: User,
  })
  readonly members: Array<Ref<User>>;

.........
}

The error looks like this

[Nest] 31496   - 02/05/2020, 6:53:36 PM   [ExceptionHandler] "Organization.members"'s Type is invalid! Type is: "undefined" +2ms
Error: "Organization.members"'s Type is invalid! Type is: "undefined"
    at Object._buildPropMetadata (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\@typegoose\typegoose\lib\prop.js:162:19)
    at Object._buildSchema (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\@typegoose\typegoose\lib\internal\schema.js:36:20)
    at buildSchema (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\@typegoose\typegoose\lib\typegoose.js:149:20)
    at Object.getModelForClass (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\@typegoose\typegoose\lib\typegoose.js:91:39)
    at InstanceWrapper.useFactory [as metatype] (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\nestjs-typegoose\dist\typegoose.providers.js:21:49)
    at Injector.instantiateClass (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\@nestjs\core\injector\injector.js:291:55)
    at callback (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\@nestjs\core\injector\injector.js:75:41)
    at process._tickCallback (internal/process/next_tick.js:68:7)

My package.json contains the following related dependencies:

"@nestjs/common": "^6.7.2",
"@nestjs/core": "^6.7.2",
"@nestjs/graphql": "^6.5.3",
"type-graphql": "^0.17.6"

How can i fix this and was it patched?

@gimyboya , In my instance it was a node issue with circular dependency in the end. you may have a similar issue.

@RyannGalea Thanks for the fast reply but I don't really understand what you mean, sorry. How do I fix it?

@gimyboya I can only suggest searching up 'circular dependency in node' as it's very code specific.

@RyannGalea You still think it's anode issue even when the error don't appear only if i define the union?

I'm just letting you know what happened in my specific instance to maybe give you some ideas on what it could be, may not be the issue in your case. @gimyboya

Right, @MichalLytek Any comments on this? How can we move forward

@gimyboya The best would be to ask typegoose team about that error you get

@MichalLytek The Schemas definitions without type-graphql decorations are working fine. I faced this issue only when I started porting the back-end from REST to GraphQL using the decorators.

@gimyboya
The error comes from Object._buildPropMetadata (D:\Grainer\Ethis\Projects\dashboard-backend\node_modules\@typegoose\typegoose\lib\prop.js:162:19) so I can't do anything with that typegoose error.

I see. Thanks for pointing that out i will try to open an issue it typegoose repo

I use a union in schema as follows, and works.

export const MultiEnityUnion = createUnionType({
  name: 'MultiEnityUnion', 
  types: () => [Property, BusinessProperty, Supplier, User, File, Contact],
  resolveType: value => {
    if ('contactType'  in value) return Contact
    if ('buildingType' in value) return Property
    if ('supplierType' in value) return Supplier
    if ('mimetype'     in value) return File
    if ('businessRole' in value) return User
    if ('property' in value && 'addressString' in value) return BusinessProperty
    return undefined
  }
})
 ```

@ObjectType()
export class Note extends GlobalFields {

@Field()
@prop()
body: string

@Field()
@prop()
sticky: boolean

@Field(type => MultiEnityUnion, {nullable: true})
@prop({ required: true, refPath: 'entityRef' })
entity: Ref

@Field(type => EntitiesEnum, {nullable: true})
@prop({required: true, enum: Object.values(EntitiesEnum), index: true})
entityRef: EntitiesEnum

@Field(type => [FileRef])
@arrayProp({_id: false, items: FileRef})
files: FileRef[]

}

export const NoteModel = getModelForClass(Note)
```

Was this page helpful?
0 / 5 - 0 ratings