Using a non-standard type (BigNumber) with typegraphql

Viewed 25

I have an ethers.js BigNumber that I am storing in Postgresql DB as a numeric type using TypeORM. I am trying to make it work with Typegraphql but I am getting the error: CannotDetermineGraphQLTypeError: Cannot determine GraphQL output type for 'amount' of 'Transfer' class. Is the value, that is used as its TS type or explicit type, decorated with a proper decorator or is it a proper output value?

@ObjectType()
@Entity()
export class Transfer extends BaseEntity {
  @Field()
  @Column({
    type: "numeric",
    precision: 78,
    scale: 0,
    transformer: new BigNumberTransformer()
  })
  amount!: BigNumber
}

I created a BigNumberTransformer as was recommended in this medium post:

export class BigNumberTransformer implements ValueTransformer {
  to(num: BigNumber): string {
    return num.toString();
  }

  from(num: string): BigNumber {
    return BigNumber.from(num);
  }
}
1 Answers

You must create a custom scalar (BigNumber.ts):

import { Kind, GraphQLError, GraphQLScalarType } from "graphql";
import { BigNumber } from "ethers";

export const GraphQLBigNumber: GraphQLScalarType = new GraphQLScalarType({
  name: "BigNumber",

  description:
    "A field whose value is a valid BigNumber which safely allows mathematical operations on numbers of any magnitude",

  serialize(value) {
    if (value === null) return value;
    else return BigNumber.from(value.toString()).toString();
  },

  parseValue(value) {
    if (value === null) return value;
    else return BigNumber.from(value.toString());
  },

  parseLiteral(ast) {
    if (ast.kind !== Kind.STRING && ast.kind !== Kind.Int) {
      throw new GraphQLError(
        `Can only validate strings or ints as BigNumbers but got a: ${ast.kind}`
      );
    }

    if (ast.value === null) return ast.value;
    else return BigNumber.from(value.toString());
  },

  extensions: {
    codegenScalarType: "BigNumber | string | number",
  },
});

Then you can just use it in your field decorator:

// ...
import { GraphQLBigNumber } from "./BigNumber";

@ObjectType()
@Entity()
export class Transfer extends BaseEntity {
  @Field(() => GraphQLBigNumber) // Here!
  @Column({
    type: "numeric",
    precision: 78,
    scale: 0,
    transformer: new BigNumberTransformer(),
  })
  amount!: BigNumber;
}

Optionally, you can declare the association between the reflected property type and your scalar to automatically map it (no need for explicit type annotation!):

@ObjectType()
@Entity()
export class Transfer extends BaseEntity {
  @Field() // Magic goes here - no type annotation!
  @Column({
    type: "numeric",
    precision: 78,
    scale: 0,
    transformer: new BigNumberTransformer(),
  })
  amount!: BigNumber;
}

All you need to do is register the association map in the buildSchema options:

import { buildSchema } from "type-graphql";
import { BigNumber } from "ethers";
import { GraphQLBigNumber } from "./BigNumber";

const schema = await buildSchema({
  resolvers,
  scalarsMap: [{ type: BigNumber, scalar: GraphQLBigNumber }],
});

However, you must be aware that this will only work when the TypeScript reflection mechanism can handle it. So your class property type must be a class, not an enum, union or interface.

Related