Enums not working with nestjs and graphql
Asked Answered
I

4

12

I have recently moved from using typegraphql and typeorm directly to using them with nestjs. Mostly this has been a straightforward experience. I however have one issue with respect to enums.

I have a set of custom decorators that I have combined together so that I can easily decorate up my models without having both typeorm, typegraphql and class validator decorators. This worked great before and works fine now in all cases other than enums.

As an example here is an @OptionalDecimal decorator:

import { IsNumber } from 'class-validator'
import { Field, Float } from 'type-graphql'
import { Column } from 'typeorm'

export function OptionalDecimal() {
    const typeDecorator = IsNumber()
    const fieldDecorator = Field(type => Float, { nullable: true })
    const columnDecorator = Column('decimal', { nullable: true })

    return (target: any, key: string) => {
        typeDecorator(target, key)
        fieldDecorator(target, key)
        columnDecorator(target, key)
    }
}

My @Enum decorator is as so:

import { IsEnum } from 'class-validator'
import { Field } from 'type-graphql'
import { Column } from 'typeorm'
import { IEnumOptions } from './IEnumOptions'

export function Enum(
    typeFunction: (type?: any) => object,
    options: IEnumOptions = {}
) {
    const isEnumDecorator = IsEnum(typeFunction())
    const fieldDecorator = Field(typeFunction)
    const columnDecorator = Column({
        default: options.default,
        enum: typeFunction(),
        type: 'enum',
    })

    return (target: any, key: string) => {
        isEnumDecorator(target, key)
        fieldDecorator(target, key)
        columnDecorator(target, key)
    }
}

I define my enums in separate files like so:

import { registerEnumType } from 'type-graphql'

export enum AccountState {
  ACTIVE,
  SUSPENDED,
  CLOSED,
}

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

And is used thusly:

@EntityType()
export class Member extends VersionedEntity {
  @IdentifierNewGuid()
  public readonly id: string

  @Enum(type => AccountState, { default: AccountState.ACTIVE })
  public accountState: AccountState
...

My database is returning numeric ids for the enumerations and the field type in the database (mysql) is enum. As an example where my database is returning 1 for accountState which should be SUSPENDED I receive a graphql error:

  "errors": [
    {
      "message": "Expected a value of type \"AccountState\" but received: 1",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "searchMembers",
        0,
        "accountState"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Expected a value of type \"AccountState\" but received: 1",
            "    at completeLeafValue

So to recap this approach worked fine with typeorm and typegraphql directly but sadly fails to work now. All the other decorators I have appear to work fine (50+) so it's just something that's up specifically with enums.

This is a major blocker for me and any help would be greatly appreciated as I am currently out of ideas.

Edit - In response to Shusson, when I add the decorators manually it also does not work actually:

@Column({
    default: AccountState.ACTIVE,
    enum: AccountState,
    type: 'enum',
  })
@Field(type => AccountState)
public accountState: AccountState

Cheers, Mark

Inspired answered 9/4, 2019 at 17:28 Comment(2)
Have you tried replacing the custom enum decorator with the standard typeorm declaration?Padlock
I have updated my question, thanks for your response @PadlockInspired
I
14

In the end I fixed this by making the enums equal their string equivalents rather than default numerical values when defining them like so:

export enum AccountState {
  ACTIVE='ACTIVE',
  SUSPENDED='SUSPENDED',
  CLOSED='CLOSED',
}

This results in storing the string value in the database instead which plays well with graphql. This definitely worked before, I am not sure how :) I would prefer to use numeric values from a db design perspective, so any ideas would still be appreciated.

Inspired answered 10/4, 2019 at 19:56 Comment(3)
You're awesome man.. I just missed the registerEnumType(), I was using the string Enums, however it was not working, I saw your code and BAM.... I got the hint... In my case enum with Numeric value also working.. Just declare mongoose document using interface with int enum.Impatient
Thanks, glad I could be of help :)Inspired
i'm using the @Field decorator in nestjs/graphql pkg. Tried setting the defaultValue as an array containing an enum member...graphql playground threw an error stating "each value in ... must be a valid enum value". It was only after setting the enum member to equal its string equivalent was there no longer an error... Your solution helped solve my problem. Thank you.Bibeau
H
1

Since this is still an issue in 2022, here is a solution I came up with that does not require you to change your integer enums to string ones with the keys exactly matching the values. You can use the @Transform decorator from the class-transformer package.

@ArgsType()
export class SomeArgs {
  @Field(() => SomeEnum, {
    nullable: true,
    defaultValue: SomeEnum.SOME_KEY,
  })
  @Transform(({ value }) => 
    typeof value === 'string' ? SomeEnum[value] : value
  )
  someEnumProp?: SomeEnum = SomeEnum.SOME_KEY;
}

Where SomeEnum is like:

export enum SomeEnum {
  SOME_KEY = 1, 
  SOME_OTHER_KEY = 2, 
}

registerEnumType(SomeEnum, {name: 'SomeEnum'})
Huggermugger answered 20/9, 2022 at 11:7 Comment(0)
S
0

You could try a custom a resolver

export enum AccountState {
  ACTIVE,
  SUSPENDED,
  CLOSED,
}

// Make to register before creating resolver
registerEnumType(AccountState, { name: 'AccountState' })

export const accountStateResolver: Record<keyof typeof AccountState, any> = {
  ACTIVE: 0,
  SUSPENDED: 1,
  CLOSED: 2
};

Then in your app.module.ts file or wherever you imported your graphql module root.

@Module({
  ...
  imports:[
    ...
    GraphQLModule.forRoot({
      ...
      resolvers: {
        AccountState: accountStateResolver,
      }
    }),
    ...
  ]
  ...
})
Speaker answered 16/10, 2022 at 4:6 Comment(0)
D
0

I found a solution that only requires pretty minimal changes. First:

export function createGQLEnumType<T>(enumType: T): { [key: string]: string } {
  const result = {} as { [key: string]: string }
  for (const key in enumType) {
    if (Object.prototype.hasOwnProperty.call(enumType, key)) {
      const value = enumType[key as keyof T] as string
      result[value] = value
    }
  }
  return result
}

Then in your type definitions:

const AccountStateGQLEnum = createGQLEnumType(AccountState)
registerEnumType(AccountStateGQLEnum, { name: 'AccountState' })

@InputType({ description: 'Sample input type' })
export class SampleInput {

  @Field(() => AccountStateGQLEnum!)
  accountState: AccountState
}
Dextrogyrate answered 9/5, 2024 at 8:28 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.