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()
}
}
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.