graphql icon indicating copy to clipboard operation
graphql copied to clipboard

Type references in `union` doesn't get resolved when used as return type for an Object field

Open Gamote opened this issue 2 years ago • 11 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Current behavior

References part of a union type can't be resolved by the GraphQL Gateway (federation) when is used as a type for a field. If the union type is used as return type for a Query or Mutation everything works as expected.

Minimum reproduction code

https://github.com/Gamote/nestjs-graphql-union-mrr

Steps to reproduce

Please check the minimum reproduction code for more info on the types.

When FavoriteItemUnion is used as return type for a field in an Object type.

// Field definition in the `users` service
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(() => Int)
  id: number;

  @Field()
  firstName: string;

  @Field()
  lastName: string;
  
  // This field returns a union type
  @Field(() => FavoriteItemUnion)
  favoriteItem: typeof FavoriteItemUnion;
}
// Query resolver defined in the `users` service
@Query(() => User)
async getUserById(@Args('id') id: number) {
  return this.usersService.getById(id);
  /*
    ^ For `userId=1` will return the following:
    {
      "id": 1,
      "firstName": "John",
      "lastName": "Doe",
      "favoriteItem": {
        "__typename": "Song",
        "id": 1
      }
    }
  */
}
# Query to retrieve the user + his favorite item
query GetUserById {
  getUserById(id: 1) {
    id
    firstName
    lastName
    favoriteItem {
      ... on Song {
        id
        title
      }
    }
  }
}
{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Song.title.",
      "locations": [
        {
          "line": 9,
          "column": 9
        }
      ],
      "path": [
        "getUserById",
        "favoriteItem",
        "title"
      ]
    }
  ],
  "data": null
}

In this case, if we remove the title from the query, we will get a response as GraphQL doesn't need to resolve any more data.

Expected behavior

I expect for this query:

query GetUserById {
  getUserById(id: 1) {
    id
    firstName
    lastName
    favoriteItem {
      ... on Song {
        id
        title
      }
    }
  }
}

to return this data:

{
  "data": {
    "getUserById": {
      "id": 1,
      "firstName": "John",
      "lastName": "Doe",
      "favoriteItem": {
        "id": 1,
        "title": "Song 1"
      }
    }
  }
}

Package version

10.0.10

Graphql version

graphql: 16.4.0 @nestjs/mercurius: 10.0.9 @nestjs/platform-fastify: 8.4.4 mercurius: v9.4.0

NestJS version

8.0.0

Node.js version

v16.14.2

In which operating systems have you tested?

  • [X] macOS
  • [ ] Windows
  • [ ] Linux

Other

  • I have tried to identify the problem in the source code but without success
  • I have used the plain GraphQL and the query was successful so the functionality it is supported by GraphQL

Gamote avatar May 04 '22 07:05 Gamote

In addition to this, I have also experienced that it is not possible to make the union fields as nullable. They are always resolved as mandatory fields.

  // This field returns a union type
  @Field(() => FavoriteItemUnion, { nullalbe: true})
  favoriteItem: typeof FavoriteItemUnion;

This makes favoruteItem as a mandatory field and not nullable

rmagon avatar Sep 05 '22 20:09 rmagon

I have the same issue as well still today.

The object type in question:

export const Rol = createUnionType({
    name: 'Rol',
    types: ()=> [Student, Profesor] as const,
    resolveType(value) {
        if (value.an) {
            return 'Student';
        }
        return 'Profesor';
    }
});

export type RolCreereInputType = StudentCreereInput | ProfesorCreereInput;

@Schema()
@ObjectType()
export class User extends Document {

    @Prop()
    @Field(()=> ID)
    _id: string;

    @Prop({required: true})
    @Field(()=> String)
    nume: string;

    @Prop({required: true})
    @Field(()=> String)
    eMail: string;

    @Prop({required: true})
    @Field(()=> String)
    numarTelefon: string;

    @Prop({type: S.Types.ObjectId, refPath: 'onModel'})
    @Field(()=> Rol)
    rol: typeof Rol;
}

The query:

query GasireUser($gasireUserId: String!) {
  gasireUser(id: $gasireUserId) {
    nume
    numarTelefon
    eMail
    rol {
      ... on Profesor {
        _id
        persoana {
          nume
        }
      }
    }
  }
}

Query resolver:

@Query(()=> User)
    async gasireUser(@Args("id") id: string) {
        return await this.userService.gasireUser(id);
    }

The result:

{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field Profesor.persoana.",
      "locations": [
        {
          "line": 9,
          "column": 9
        }
      ],
      "path": [
        "gasireUser",
        "rol",
        "persoana"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Cannot return null for non-nullable field Profesor.persoana.",
            "    at completeValue (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:594:13)",
            "    at executeField (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:489:19)",
            "    at executeFields (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:413:20)",
            "    at completeObjectValue (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:914:10)",
            "    at completeAbstractValue (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:795:10)",
            "    at completeValue (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:624:12)",
            "    at completeValue (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:584:23)",
            "    at executeField (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:489:19)",
            "    at executeFields (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:413:20)",
            "    at completeObjectValue (D:\\Facultate\\Licenta\\utm-prezenta-api\\node_modules\\graphql\\execution\\execute.js:914:10)"
          ]
        }
      }
    }
  ],
  "data": null
}

c1ba avatar Nov 01 '22 07:11 c1ba

My workaround was to create a separate Nullable type, smth like this:

@InputType('NullableDataInput')
@ObjectType()
export class NullableData {
  @Field({ nullable: true })
  isNull?: boolean;
}

and then use it in my union type, when we do resolveType, we always default to the NullableType.

export const FavouriteItemUnion = createUnionType({
  name: 'FavouriteItemUnion',
  types: () =>
    [
      Banana,
      Apple,
      NullableData,
    ] as const,
  resolveType: (value: any) => {
    switch (value.xxx) {
      case 'banana':
        return Banana;
      case 'apple':
        return Apple;
     ...
      default:
        return NullableData;
    }
  },
});

Hope it helps!

rmagon avatar Nov 01 '22 08:11 rmagon

I didn't totally helped me, but at least it helped me for debugging. I also work with mongoose. If I am to return a value like a string for the object I see that it has no problems, but I now see that it crashes when I am to return an object that needs populated. Still, thanks

c1ba avatar Nov 03 '22 06:11 c1ba

Let's say I have this:

export const UserUnion = createUnionType({
  name: 'OpponentUnion',
  types: () => [Number, User],
});

where User is ObjectType. and later I do:

@Field(() => UserUnion)
defender: number | User;

Which I'm not able to do because of an error (const getObjectType = (item) => this.typeDefinitionsStorage.getObjectTypeByTarget(item).type;). Even if I use a custom Scalar for the Number.

ggepenyan avatar Apr 21 '23 11:04 ggepenyan

I'm able to do this without an error: @Field((type) => [Int, User]) But later in the schema.gql I get this: defender: [Int!]! Which is not what expected.

ggepenyan avatar Apr 21 '23 11:04 ggepenyan

Having the same issue here, for example:

import { InputType, Field } from '@nestjs/graphql';
import { createUnionType } from '@nestjs/graphql';

const StringOrArrayOfStringType = createUnionType({
  name: 'StringOrArrayOfString',
  types: () => [String],
  resolveType(value) {
    if (Array.isArray(value)) {
      return [String];
    }
    return String;
  },
});

@InputType()
export class MyInput {
  @Field(() => StringOrArrayOfStringType)
  myField: string | string[];
}

Will result to en error: Error: Cannot determine a GraphQL input type null for the "name". Make sure your class is decorated with an appropriate decorator.

xde013 avatar Apr 28 '23 00:04 xde013

Having the same issue here, for example:

import { InputType, Field } from '@nestjs/graphql';
import { createUnionType } from '@nestjs/graphql';

const StringOrArrayOfStringType = createUnionType({
  name: 'StringOrArrayOfString',
  types: () => [String],
  resolveType(value) {
    if (Array.isArray(value)) {
      return [String];
    }
    return String;
  },
});

@InputType()
export class MyInput {
  @Field(() => StringOrArrayOfStringType)
  myField: string | string[];
}

Will result to en error: Error: Cannot determine a GraphQL input type null for the "name". Make sure your class is decorated with an appropriate decorator.

The same problem-(((

ahmad66617 avatar Jun 22 '23 21:06 ahmad66617

@kamilmysliwiec is there any update on this issue? This problem still exists. Unions would be super helpful to simplify API design, but with this bug there is very limited use of the union type. I understand that unions don't work as an input type (still not implemented in the standard), but a fix for this bug would be amazing.

moloti avatar Dec 21 '23 14:12 moloti

@kamilmysliwiec Looks like unions doesn't work at all, I tried mostly the same as guys did above and it doesn't working. Is it planned to be fixed any time soon? Thanks! :)

B0ySetsF1re avatar Feb 09 '24 17:02 B0ySetsF1re

Having the same issue here, for example:

import { InputType, Field } from '@nestjs/graphql';
import { createUnionType } from '@nestjs/graphql';

const StringOrArrayOfStringType = createUnionType({
  name: 'StringOrArrayOfString',
  types: () => [String],
  resolveType(value) {
    if (Array.isArray(value)) {
      return [String];
    }
    return String;
  },
});

@InputType()
export class MyInput {
  @Field(() => StringOrArrayOfStringType)
  myField: string | string[];
}

Will result to en error: Error: Cannot determine a GraphQL input type null for the "name". Make sure your class is decorated with an appropriate decorator.

I am also facing the same issue. I wanted object or array of objects

DeepaPrasanna avatar Apr 09 '24 05:04 DeepaPrasanna