Search code examples
typeormtypegraphql

When insing TypeORM and TypeGraphQL, Where do the ({nullable: true}) options go and what's the difference?


I'm working with a server app that uses both TypeORM and Type-GraphQL.

 @Field({ nullable: true })
    @Column({ nullable: true })
    url?: string; 

When I run GraphQL queries (using Apollo Studio) sometimes the query returns an error for having null values and other times the mutation fails when adding a new value.

I tested multiple variations, but I got inconsistent results will null getting rejected in weird ways. I looked into it found advice from deleting node_modules to dropping the database so I decided I should just figure out what null settings do in each decorator.

What's the difference between putting { nullable: true } in the @field() or the @column() and shouldn't simply having ? in the type definition work?

I also have separated my @InputType() and @Entity() classes into separate files so do I need to use the same values for @field() in the input as well?

Full example:

Entity

import { Field, ID, ObjectType } from "type-graphql";
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
@ObjectType()
export class Click extends BaseEntity{
    
    @Field(type => ID)
    @PrimaryGeneratedColumn("uuid")
    id: string;

    @Field(type => ID)
    @Column({ nullable: true })
    clickId?: string; 

    @Field({ nullable: true })
    @Column({ nullable: true })
    url?: string; 
}

Input

import { MaxLength } from "class-validator";
import { Field, ID, InputType } from "type-graphql";
import { Click } from '../entity/Click';

@InputType()
export class ClickInput implements Partial<Click> {

  //omitted the id field because it's generated on the server 
  
  @Field(type => ID)
  clickId: string; 

  @Field({ nullable: true })
  @MaxLength(256)  
  url: string; 

Resolver

import {
  Arg, Mutation,
  Query,
  Resolver
} from "type-graphql";
import { Service } from "typedi";
import { Click } from "../entity/Click";
import { ClickInput } from "../input/ClickInput";
import { ClickService } from "../service/ClickService";

@Service() 
@Resolver(of => Click)
export class ClickResolver {
  constructor(
    // constructor injection of a service
    private readonly clickService: ClickService, 
  ) {}   

  @Query( returns => [Click])
  async clicks() {    
    return this.clickService.list()
  }

  @Query( returns => Click, { nullable: true })  
  async click(@Arg("clickId") clickId: string) {
    return this.clickService.findById(clickId)
  }
  @Mutation( _type => Click )
  async createClick(@Arg("data") data: ClickInput): Promise<Click> {
    return this.clickService.create(data)
  }
  


 }

Service

import { Service } from "typedi";
import { DeleteResult } from "typeorm";
import { InjectRepository } from "typeorm-typedi-extensions";
import { Click } from "../entity/Click";
import { ClickRepository } from "../repo/ClickRepo";


@Service()
export class ClickService {
  constructor(
    @InjectRepository(Click)
    private readonly clickRepository: ClickRepository
  ) {}


  async findById(clickId: string, relations: string[] = []) {
    return this.clickRepository.findOne({
      where: {
        clickId,
      },
      relations: relations,
    });
  }

  async create(params: Partial<Click>): Promise<Click> {
    const u = this.clickRepository.create(params);
    return this.update(u);
  }

  async update(Click: Click): Promise<Click> {
    return this.clickRepository.save(Click);
  }

  async delete(Click: Click): Promise<DeleteResult> {
    return this.clickRepository.delete(Click)
  }

  async list() {
    return this.clickRepository.find()
  }
}

Solution

  • If you want a value to be null, you should put it on both.

    { nullable: true } in typeorm (@Entity) means the database is permitted to store null values. Otherwise, you won't be allowed to save a null value in that column in the database.

    { nullable: true} in type-graphql (@Field) means that the schema generated by TypeGraphQL will permit that field to be null. Otherwise, when querying, you would expect to potentially get an error of Cannot return null for non-nullable field EntityName.fieldName.


    To clarify, the GraphQL schema might look like:

    type Click {
        id: ID!
        clickId: ID!
        url: String
    }
    

    Notice how url doesn't have a ! so it's not required, but clickId does, so you're going to run into issues if you pass a null value.


    As for your input type, do you want to allow a null value for clickId to be passed? If so, you would also want it to be nullable: true in ClickInput. If, for example, you always required a clickId on creation but made it null later, then you could leave that out to require the input for clickId to be non-null.