Search code examples
graphqltypegraphql

GraphQL - defining types


I'm trying to follow Ben Awad's lireddit tutorial.

At the time he made the tutorial, there may have been different inferences about Field types.

I'm trying to add a Field to my relationship attribute (adding creator Field to a Post), so that I can then access creator attributes on that post record.

Ben does this as follows:

@Field()
  @ManyToOne(() => User, (user) => user.posts)
  creator: User;

That worked for him. When I try this, I get an error that says:

 throw new errors_1.NoExplicitTypeError(prototype.constructor.name, propertyKey,

parameterIndex, argName);

NoExplicitTypeError: Unable to infer GraphQL type from TypeScript reflection system. You need to provide explicit type for 'creator' of 'Post' class.

When I look at the GraphQL docs for how to provide an explicit type for creator, I can't find a similar example (simple enough for me to decipher a principle that I can apply).

I'm confused by the docs, because the have the following example:

Can anyone see what I need to do to ask for the field to be recognised as an object that I can read from?

@ObjectType()
class Rate {
  @Field(type => Int)
  value: number;

  @Field()
  date: Date;

  user: User;
}

I think they use user: User the same way I use creator: User. Is there a reason that Field() can't have the same thing as ObjectType()?

I tried:

@Field(() => [User])
  @ManyToOne(() => User, (user) => user.posts)
  creator: User;

This doesn't give any errors (neither does the code the way Ben has it), until I get to the playground, in which case, I can't return the user data - so clearly it's wrong. It also isn't clear whether the array means the array of attributes on the user object, or an array of users (which would also be wrong). I can see from the GraphQL docs that it should be possible to define a field attribute as an object type, but I can't find an example showing how to do that.

I have seen this post, which looks like a similar problem, but I can't see from the suggested answers, how to apply those ideas to this problem.

I have seen this post, which has a similar problem, and is answered with a reference to an example that shows how to write resolvers that find relations, but my resolver already worked to find the creatorId, so I think maybe I'm not looking in the right place for an answer.

In my post resolver, I have:

import {
  Resolver,
  Query,
  Arg,
  Mutation,
  InputType,
  Field,
  Ctx,
  UseMiddleware,
  Int,
  FieldResolver,
  Root,
  ObjectType,
} from "type-graphql";
import { Post } from "../entities/Post";
import { MyContext } from "../types";
import { isAuth } from "../middleware/isAuth";
import { getConnection } from "typeorm";

@InputType()
class PostInput {
  @Field()
  title: string;
  @Field()
  text: string;
}

@ObjectType()
class PaginatedPosts {
  @Field(() => [Post])
  posts: Post[];
  @Field()
  hasMore: boolean;
}

@Resolver(Post)
export class PostResolver {
  @FieldResolver(() => String)
  textSnippet(@Root() post: Post) {
    return post.text.slice(0, 50);
  }

  @Query(() => PaginatedPosts)
  async posts(
    @Arg("limit", () => Int) limit: number,
    @Arg("cursor", () => String, { nullable: true }) cursor: string | null
  ): Promise<PaginatedPosts> {
    // 20 -> 21
    const realLimit = Math.min(50, limit);
    const reaLimitPlusOne = realLimit + 1;
    const qb = getConnection()
      .getRepository(Post)
      .createQueryBuilder("p")
      .orderBy('"createdAt"', "DESC")
      .take(reaLimitPlusOne);

    if (cursor) {
      qb.where('"createdAt" < :cursor', {
        cursor: new Date(parseInt(cursor)),
      });
    }

    const posts = await qb.getMany();

    return {
      posts: posts.slice(0, realLimit),
      hasMore: posts.length === reaLimitPlusOne,
    };
  }

  @Query(() => Post, { nullable: true })
  post(@Arg("id") id: number): Promise<Post | undefined> {
    return Post.findOne(id);
  }

  @Mutation(() => Post)
  @UseMiddleware(isAuth)
  async createPost(
    @Arg("input") input: PostInput,
    @Ctx() { req }: MyContext
  ): Promise<Post> {
    return Post.create({
      ...input,
      creatorId: req.session.userId,
    }).save();
  }

  @Mutation(() => Post, { nullable: true })
  async updatePost(
    @Arg("id") id: number,
    @Arg("title", () => String, { nullable: true }) title: string
  ): Promise<Post | null> {
    const post = await Post.findOne(id);
    if (!post) {
      return null;
    }
    if (typeof title !== "undefined") {
      await Post.update({ id }, { title });
    }
    return post;
  }

  @Mutation(() => Boolean)
  async deletePost(@Arg("id") id: number): Promise<boolean> {
    await Post.delete(id);
    return true;
  }
}

Solution

  • First of all, creator should be of a User type, and not a list of users, i.e.

    @Field(() => User)
    @ManyToOne(() => User, (user) => user.posts)
    creator: User;
    

    When you are retrieving a post, you should include a relation in your query, so the User entity is also loaded:

    @Query(() => Post, { nullable: true })
    post(@Arg("id") id: number): Promise<Post | undefined> {
      return Post.findOne(id, { relations: "creator" });
    }
    

    Also, when you're using query builder to fetch posts, you should add User entities:

    const qb = getConnection()
      .getRepository(Post)
      .createQueryBuilder("p")
      .leftJoinAndSelect("p.creator", "p_creator")
      .orderBy('"createdAt"', "DESC")
      .take(reaLimitPlusOne);
    

    Bonus note: There's a common problem of over-fetching the data in GraphQL, so queries can become slow with time. In that manner, you could also consider moving the creator field to FieldResolver, so it's retrieved from the database only if it's requested. In case you do that, one other good practice with ManyToOne relations is to use a dataloader, so if you, for example, load 10 posts from the same creator, you'll end up with only one fetching operation of that creator instead of 10 requests to the database. There's a great tutorial and explanation provided by Ben Awad too: https://www.youtube.com/watch?v=uCbFMZYQbxE.

    This isn't necessary for this tutorial in particular, but it's a must-know if you're building some serious app.