nestjs / graphql

GraphQL (TypeScript) module for Nest framework (node.js) 🍷
https://docs.nestjs.com/graphql/quick-start
MIT License
1.45k stars 391 forks source link

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

Open Gamote opened 2 years ago

Gamote commented 2 years ago

Is there an existing issue for this?

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?

Other

rmagon commented 2 years ago

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

c1ba commented 1 year ago

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
}
rmagon commented 1 year ago

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!

c1ba commented 1 year ago

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

ggepenyan commented 1 year ago

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 commented 1 year ago

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.

xde013 commented 1 year ago

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.

ahmad66617 commented 1 year ago

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-(((

moloti commented 9 months ago

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

B0ySetsF1re commented 7 months ago

@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! :)

DeepaPrasanna commented 5 months ago

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