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):
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.
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:
{
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)
```