graphql with typescript - #FieldResolver not working in compiled-to-js type-graphql - graphql

I a newbie with type-graphql and I have this weird issue of field resolvers not working.
I'm using a stack of:
typescript(3.9.5)
type-graphql(1.0.0)
typeorm(0.2.25)
apollo(2.16.0).
Whenever I run my typescript (using ts-node) everything works well, but when I use the compiled js version with express, the queries work only with simple db fields, but fieldResolvers don't work at all and in the end I get "Cannot return null for non-nullable field".
The funny thing is that the field resolvers are sibling of query resolvers in the same #Resolver class. the queries get fired (placed a console log) but the field resolvers never do. anyone have a clue?
here are some snippets from the code:
#Entity()
#ObjectType()
export class Member extends BaseEntity {
#Field(() => ID)
#PrimaryGeneratedColumn()
id: string;
#UseForSearch()
#UseAsTitle()
#Field(() => String)
#IsEmail()
#Column({ type: 'varchar', unique: true })
email: string;
#Field(() => [Item])
#OneToMany(type => Item, item => item.member)
items?: Item[];
#OneToMany(type => Review, review => review.member)
reviews?: Review[];
#Field(() => Number)
itemsCount: number;
}
#Entity()
#ObjectType()
export class Item extends BaseEntity {
#Field(() => ID)
#PrimaryGeneratedColumn()
id: string;
#ManyToOne(type => Category, category => category.items)
#JoinColumn({ name: 'categoryId' })
#Field(() => Category)
category?: Category;
#Field(() => String, { nullable: true })
#Column({
nullable: true,
})
name: string;
#ManyToOne(type => Member, member => member.items)
#JoinColumn({ name: 'memberId' })
#Field(() => Member)
member?: Member;
#Field(() => ID)
#Column('int', { nullable: true })
public memberId: number | null;
}
import { Resolver, Query, FieldResolver, Root, Ctx, Arg } from 'type-graphql';
import { getRepository, Repository } from 'typeorm';
import { Item } from '../entity';
import { Member } from '../entity/member';
#Resolver(of => Member)
export class MemberResolver {
constructor(private itemsRepository: Repository<Item>) {
this.itemsRepository = getRepository(Item);
}
#Query(() => [Member])
members() {
console.log('in members query');
return Member.find();
}
#FieldResolver()
async items(#Root() member: Member) {
return await this.itemsRepository.find({ where: { memberId: member.id } });
}
#FieldResolver()
async itemsCount(#Root() member: Member) {
console.log('in itemsCount resolver');
return this.itemsRepository.count({ where: { memberId: member.id } });
}
query {
members {
email
itemsCount
}
}
{
"errors": [
{
"message": "Cannot return null for non-nullable field Member.itemsCount.",
"locations": [
{
"line": 4,
"column": 5
}
],
"path": [
"members",
0,
"itemsCount"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: Cannot return null for non-nullable field Member.itemsCount.",
" at completeValue (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:599:13)",
" at completeValueCatchingError (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:534:19)",
" at resolveField (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:465:10)",
" at executeFields (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:297:18)",
" at collectAndExecuteSubfields (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:752:10)",
" at completeObjectValue (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:742:10)",
" at completeValue (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:630:12)",
" at completeValue (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:596:21)",
" at completeValueCatchingError (/Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:534:19)",
" at /Users/admin/tackar/clampa/clampa-server/node_modules/graphql/execution/execute.js:655:25"
]
}
}
}
],
"data": null
}

There is an issue with your compile step (TS to JS) - if it doesn't emit ES6 classes and arrow functions, it will treat of => Member as a function with prototype, so it gonna think it's the object type class.

The problem is not the compiling. The problem is your data
the message you are seeing is Cannot return null for non-nullable field Member.itemsCount
Well, either you add a value in itemsCount, or mark the field as nullable. And the problem is you are missing an await before the call to members count
Just add the await
#FieldResolver()
async itemsCount(#Root() member: Member) {
console.log('in itemsCount resolver');
return await this.itemsRepository.count({ where: { memberId: member.id } });
}

Related

Cannot return null for non-nullable field - Typegoose and TypeGraphQL

I have this Album model set up using Typegoose and TypeGraphQL, which contains multiple songs from the Song model using the AlbumSong ObjectType:
import {
prop as Property,
getModelForClass,
modelOptions,
Ref,
} from "#typegoose/typegoose";
import { Field, ObjectType, ID } from "type-graphql";
import { AlbumCategory, } from "./albumCategory.model";
import { Song } from "./song.model";
#ObjectType()
export class AlbumSong {
#Field(() => ID)
#Property({ required: true })
id!: string;
#Field(() => Song)
#Property({ type: () => Song, ref: () => Song, required: true })
song!: Song;
}
#ObjectType({ description: "The Album Model" })
#modelOptions({ schemaOptions: { collection: "albums", timestamps: true } })
export class Album {
#Field(() => ID)
id: string;
#Field()
#Property({ type: () => String })
title: string;
#Field(() => [AlbumSong])
#Property({ type: AlbumSong })
albumSongs!: Partial<AlbumSong>[];
#Field()
#Property({ required: true, default: Date.now })
createdAt: Date;
#Field()
#Property({ required: true, default: Date.now })
updatedAt: Date;
}
export const AlbumModel = getModelForClass(Album);
When trying to query the album using:
#Query((_returns) => Album, { nullable: false, name: "album" })
async getAlbumById(#Arg("id") id: string) {
return await AlbumModel.findById({ _id: id });
}
With the following GraphQL:
query Album($albumId: String!) {
album(id: $albumId) {
id
albumSongs {
id
song {
id
}
}
}
}
I get: "Cannot return null for non-nullable field AlbumSong.song."
To me it seems like the reference is not working, when i only query the albumSong's id it returns just fine...
Setup a FieldResolver to resolve the song within an AlbumSong
#Resolver(() => AlbumSong)
export class AlbumSongFieldResolver {
#FieldResolver(() => Song)
async song(#Root() parent: AlbumSong): Promise<Song> {
return Song.findOne(parent.song);
}
}

TypeGraphQL - Not able to match all the interfaces of a union

Summary
The goal is to declare the return type of a mutation using a union in order to express multiple states: Success and user errors
Being able to select concrete types according to the use cases:
mutation($data: CreateUserInput!) {
createUser(data: $data){
... on CreateUserSuccess {
user {
id
}
}
... on EmailTakenError {
emailWasTaken
}
... on UserError {
code
message
}
}
}
Implementation using TypeGraphQ:
#ObjectType()
class CreateUserSuccess {
#Field(() => User)
user: User
}
#ObjectType()
class EmailTakenError {
#Field()
emailWasTaken: boolean
}
const mapMutationValueKeyToObjectType = {
user: CreateUserSuccess,
code: UserError,
emailWasTaken: EmailTakenError
}
const CreateUserPayload = createUnionType({
name: 'CreateUserPayload',
types: () => [CreateUserSuccess, EmailTakenError, UserError] as const,
resolveType: mutationValue => {
const mapperKeys = Object.keys(mapMutationValueKeyToObjectType)
const mutationValueKey = mapperKeys.find((key) => key in mutationValue)
return mapMutationValueKeyToObjectType[mutationValueKey]
}
})
#InputType()
class CreateUserInput implements Partial<User> {
#Field()
name: string
#Field()
email: string
}
#Resolver(User)
export class UserResolver {
#Mutation(() => CreateUserPayload)
createUser (#Arg('data', {
description: 'Represents the input data needed to create a new user'
}) createUserInput: CreateUserInput) {
const { name, email } = createUserInput
return createUser({ name, email })
}
}
Data layer
export const createUser = async ({
name, email
}: { name: string; email: string; }) => {
const existingUser = await dbClient.user.findUnique({
where: {
email
}
})
if (existingUser) {
return {
code: ErrorCode.DUPLICATE_ENTRY,
message: "There's an existing user with the provided email.",
emailWasTaken: true
}
}
return dbClient.user.create({
data: {
name,
email
}
})
}
Issue
The response doesn't resolve all of the selected fields according to their unions, even by returning fields that are related to different types
if (existingUser) {
return {
code: ErrorCode.DUPLICATE_ENTRY,
message: "There's an existing user with the provided email.",
emailWasTaken: true
}
}
My doubt is this case is, why emailWasTaken is not being returned within the response if the EmailTakenError type is being selected?
This was an interpretation mistake on my part
The reasoning is that resolvers with a union type as the return definition should indeed just return one of those, in the case above, UserError and EmailTakenError wouldn't be returned on the same response
More info on this GitHub discussion

Nestjs Graphql the mutation/query input dto is empty

I don't know what I miss but the input for the mutation or the queries is always empty
when I debug seems that the input is empty object
This is my code:
resolver
#Query(() => String)
async testMutation(#Args('args') args: UpvotePostInput) {
return args.postId;
}
the dto
import { InputType, Field } from '#nestjs/graphql';
#InputType()
export class UpvotePostInput {
#Field(() => String)
postId: string;
}
the empty object
the error
{
"errors": [
{
"message": "Cannot return null for non-nullable field Query.testMutation.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"testMutation"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: Cannot return null for non-nullable field Query.testMutation.",
" at completeValue (/....js:559:13)",
" at /.....xecute.js:469:16",
" at processTicksAndRejections (internal/process/task_queues.js:97:5)",
" at async Promise.all (index 0)"
]
}
}
}
],
"data": null
}
The solution is to add #IsNotEmpty() or #IsOptional() decorator to the property:
#InputType()
export class UpdateCarInput extends PartialType(CreateCarInput) {
#Field(() => Int)
#IsNotEmpty()
id: number;
}
weird, but this resolve the problem.
I investigated this strange behaviour of nestjs and find out that this happens when use use global validation pipe with whitelist property set to true.
I tested this global validation pipe
app.useGlobalPipes(
new ValidationPipe({
// whitelist: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
for this resolver method
#Mutation()
createPost(#Args('input') input: CreatePostInput) {
return this.postsService.create(input);
}
CreatePostInput class is generated with nestjs graphql schema first approach
//graphql.ts file
export class CreatePostInput {
title: string;
authorId: number;
}
nestjs-cli.json file is configured to use #nestjs/graphql plugin
"compilerOptions": {
"plugins": [
{
"name": "#nestjs/graphql",
"options": {
"introspectComments": true
}
}
]
}
The input object is not an empty object
Add deep partial
https://gist.github.com/navix/6c25c15e0a2d3cd0e5bce999e0086fc9
#Query(() => String)
async testMutation(#Args('args') args: DeepPartial<UpvotePostInput>) {
return args.postId;
}

Schema must contain uniquely named types named "Project"

I am creating a Apollo Graphql backend using type-orm. I create an entity called Project:
import { Field, ObjectType } from "type-graphql";
import { BaseEntity, Column, Entity, ObjectID, ObjectIdColumn } from "typeorm";
#ObjectType()
#Entity()
export class Project extends BaseEntity {
#Field(() => String)
#ObjectIdColumn()
id: ObjectID;
#Field()
#Column({ unique: true })
name!: string;
#Field()
#Column()
startDate!: Date;
#Field()
#Column({nullable: true})
endDate!: Date
#Field()
#Column({unique:true})
githubUrl: string;
}
and the resolver project:
import { Arg, Mutation, Query, Resolver } from 'type-graphql'
import {Project} from '../entities/project'
import {ProjectInput, ProjectResponse} from '../types/ProjectTypes'
#Resolver()
export class ProjectResolver {
#Query(() => [Project])
async getProjects(): Promise<Project[] | null> {
let projects = await Project.getRepository().find();
return projects;
}
#Mutation(() => ProjectResponse)
async createProject(
#Arg("input") input: ProjectInput
): Promise<ProjectResponse>{
let project : Project;
if(input.name == ""){
throw Error("Invalid input")
}
try{
project = await Project.create({
name: input.name,
startDate: input.startDate,
}).save();
}catch (error) {
if (error.code === 11000) {
return {
errors: [
{
field: "project",
message: "The project name is already in use",
},
],
};
} else return error;
}
return {project: project};
}
#Mutation(() => ProjectResponse)
async setProjectEndDate(
#Arg("projectId") projectId: string,
#Arg("endDate") endDate: Date
): Promise<ProjectResponse>{
let project = await Project.getRepository().findOne(projectId)
if(project){
if(project?.startDate > endDate){
return {
errors:[{
field:"EndDate",
message:"The end date must be a date after the start date of a project."
}]
}
}
project.endDate = endDate;
project.save();
}
return {
errors:[{
field:"Project",
message:"Project could not be found."
}]
}
}
}
this is the code of the 2 auxiliary classes for the input and response of the resolver:
#InputType()
export class ProjectInput{
#Field()
name: string
#Field()
startDate: Date
#Field(()=> Date,{nullable:true})
endDate?: Date | null
#Field(()=> String, {nullable:true})
githubUrl?: string
}
#ObjectType()
export class ProjectResponse{
#Field(() => [FieldError], { nullable: true })
errors?: FieldError[]
#Field(() => Project, { nullable: true })
project?: Project | null
}
this is the code I use to create the ApolloServer object:
const apolloServer = new ApolloServer({
introspection: true,
playground: true,
schema: await buildSchema({
resolvers: [ProjectResolver],
validate: false, // Disable default GraphQL errors
}),
context: ({ req, res }) => ({ req, res}), // Enables use of context (with request) in resolvers
})
And the error I get is the following:
Error: Schema must contain uniquely named types but contains multiple types named "Project".
at new GraphQLSchema (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\graphql\type\schema.js:194:15)
at Function.generateFromMetadataSync (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\type-graphql\dist\schema\schema-generator.js:31:32)
at Function.generateFromMetadata (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\type-graphql\dist\schema\schema-generator.js:16:29)
at Object.buildSchema (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\node_modules\type-graphql\dist\utils\buildSchema.js:10:61)
at C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\dist\index.js:42:38
at Generator.next ()
at fulfilled (C:\Users\User\Desktop\UPV\Proyectos\Cv web\myweb-backend\dist\index.js:5:58)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
I don't know what the problem is so I would appreciate your help
I have managed to solve the problem by changing the import of the class 'Project' made in the 'ProjectResolver' class.
Instead of:
import {Project} from '../entities/project'
Now looks like this:
import {Project} from '../../src/entities/project'

TypeORM GraphQL nested query returns null

I am trying to use a GraphQL nested query to get information about project a and customer. I can get the information about project but I am unable to get the customer info.
How do I properly get the nested information?
Project Model
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, ManyToOne} from "typeorm";
import { ObjectType, Field, ID } from "type-graphql";
import {Customer} from "./Customer";
#Entity('project')
#ObjectType()
export class Project extends BaseEntity {
#Field(() => ID)
#PrimaryGeneratedColumn()
id: number;
#Field(() => String)
#Column()
name: string;
#Field(() => String)
#Column()
short_name: string;
#Field(() => String)
#Column()
description: string;
#Field(() => Customer, { nullable: true })
#ManyToOne(type => Customer, customer => customer.projects)
customer: Customer;
}
User Model
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column, OneToMany, JoinTable } from "typeorm";
import { ObjectType, Field, ID } from "type-graphql";
import {Project} from "./Project";
import {ProjectUser} from "./Project_handled_by_user";
#Entity('customer')
#ObjectType()
export class Customer extends BaseEntity {
#Field(() => ID)
#PrimaryGeneratedColumn()
id: number;
#Field(() => String)
#Column()
name: string;
#Field(() => String)
#Column()
short_name: string;
#Field(() => String)
#Column()
active: string;
#Field(() => [Project], { nullable: true })
#OneToMany(type => Project, project => project.customer)
projects: Project[];
}
Project Resolver
import { Resolver, Query, Mutation, Arg } from "type-graphql";
import { Project } from "../models/Project";
#Resolver()
export class ProjectResolver {
#Query(() => [Project])
projects() {
return Project.find();
}
#Query(() => Project)
project(#Arg("id") id: string) {
return Project.findOne({ where: { id } });
}
}
For example, my query is:
{
project(id: "1"){
id
name
customer{
id
name
}
}
}
The query returns:
{
"data": {
"project": {
"id": "1",
"name": "KFS 5.1",
"customer": null
}
}
}
Thanks for help

Resources