Type-graphql: Enum validation inconsitency

Created on 6 Dec 2019  路  7Comments  路  Source: MichalLytek/type-graphql

Describe the bug
Currently the way type-graphql, graphql and class-validator treats enums leads to some confusing behavior:
GraphQL only recognizes one value in enums, not a key-value pair like TypeScript does. type-graphql chose to use the key of an enum in the graphql representation. The validation logic of class-validator validates for the value, though. This leads to the situation that if you request the value of an enum from graphql in a query and then send it back in a mutation, it will fail validation.

To Reproduce
Server side

enum ExampleEnum {
  Foo = '1'
  Bar = '2'
}

registerEnumType(ExampleEnum), {
  name: 'ExampleEnum'
})

@ObjectType()
export class ExampleType {
  @Field(type => ExampleEnum)
  @IsEnum(ExampleEnum)
  public fooOrBar: ExampleEnum
}

@Resolver(ExampleType)
class ExampleResolver {
  @Mutation(type => ExampleType)
  public async updateExampleType (@Arg('fooOrBar', type => ExampleEnum) fooOrBar: ExampleEnum) {
    return { fooOrBar }
  }
}

Connect this to e. g. Apollo and then the following test

const query = `mutation { updateExampleType(fooOrBar: ${ExampleEnum.Foo}) { fooOrBar } }`
server.post('/graphql')
        .set('Accept', 'application/json')
        .send({ query })
        .expect(200)

will fail with a validation error because it sends in fooOrBar: 1 but expects fooOrBar: Foo.

Expected behavior
Behavior between validation and the rest of the code should be consistent. The easiest way would be to use enum values instead of enum labels so that fooOrBar: 1 would be the expected value in the code above. This would be a breaking change, so it should be hidden behind a flag, ideally on registerEnumType.

Enviorment (please complete the following information):

  • OS: Windows
  • Node 10.17.0
  • Package version 0.17.5
  • TypeScript version 3.7.2
Documentation Question Solved

Most helpful comment

All 7 comments

i have the same problem

My first and most important question is: why the hell you have @IsEnum(ExampleEnum) on the enum field of GraphQL ObjectType? You don't trust graphql-js and you think that one of 1 million times it will pass an incorrect value? 馃槃

The mechanism of enums in TypeGraphQL is quite simple. GraphQL SDL doesn't have any mapping capabilities, so we describe enum like this:

enum RGB {
  RED
  BLUE
  GREEN
}

So you will receive "RED" string in JSON while querying as output data. When you provide the value as input, you have arg: RED syntax without the quotes and "RED" string as a variable in JSON.

But sometimes to have an idiomatic GraphQL API you need to map from the UPPERCASE enum field names of GraphQL into numbers or other strings, so you declare them in this way in TS code

enum RGB {
  RED = "#FF0000"
  BLUE = "#0000FF"
  GREEN = "#00FF00"
}

This way in your code on runtime you will have the values on the right that correspond to your database values or something.

The easiest way would be to use enum values instead of enum labels

This is against GraphQL Spec:

Enums are not references for a numeric value, but are unique values in their own right. They may serialize as a string: the name of the represented value.

So you will receive "1" string instead of 1 number.

This leads to the situation that if you request the value of an enum from graphql in a query and then send it back in a mutation, it will fail validation.

No, it's not true - you will return "RED" as response and you may pass "RED" as a value in variables JSON.

The validation logic of class-validator validates for the value, though.

So just use Object.keys(ExampleEnum) to have an array of GraphQL enum labels 馃槈

My first and most important question is: why the hell you have @IsEnum(ExampleEnum) on the enum field of GraphQL ObjectType? You don't trust graphql-js and you think that one of 1 million times it will pass an incorrect value? 馃槃

Two reasons:

  1. It is hard to remember which decorators I am allowed to use and which not - I would prefer for them to just all work.
  2. The main benefit of TypeScript and the decorators for me is that I can define the main data types once and reuse everywhere. So if I have a REST endpoint for the same data in the same service, I can use the same data type for validation. And, even more importantly: I can import it in the frontend and use the exact same validators for validating user input. We all remember the countless times when backend and frontend didn't match up and e. g. an email that passed validation in the frontend then was rejected my the backend. By just using the same code I didn't have this happen to me for quite some time now.

The easiest way would be to use enum values instead of enum labels

This is against GraphQL Spec:

Enums are not references for a numeric value, but are unique values in their own right. They may serialize as a string: the name of the represented value.

So you will receive "1" string instead of 1 number.

Good point, I didn't think about that. This makes the situation even more complicated :-/

This leads to the situation that if you request the value of an enum from graphql in a query and then send it back in a mutation, it will fail validation.
No, it's not true - you will return "RED" as response and you may pass "RED" as a value in variables JSON.

Ok, right again - the problem only appears if the frontend tries to create a new value with the same type that the backend uses.

So just use Object.keys(ExampleEnum) to have an array of GraphQL enum labels 馃槈

Then the type becomes incompatible with the backend, so this only moves the problem.

So basically, to sum up the enums rules:

  • you can't use the TS enums on client side, if you use mappings feature, as they are clearly a backend thing
  • on client use the generated string literal union as a passed values (e.g. "RED" | "GREEN" | "BLUE")
  • if you don't need a mapping, you can have 1:1 enums:
enum RGB {
  RED = "RED"
  BLUE = "BLUE"
  GREEN = "GREEN"
}

Then you can use them both on server and client side 馃槈

Ok, makes sense. Would you be interested in a PR adding this to the docs? :)

How can I set column values to be of an enum? Let say there is a field with the name saftey_features and it has values from enum.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Asim13se picture Asim13se  路  3Comments

Tybot204 picture Tybot204  路  3Comments

robertchung97 picture robertchung97  路  3Comments

MichalLytek picture MichalLytek  路  4Comments

avkonst picture avkonst  路  3Comments