Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative grouping of field resolvers #193

Open
MichalLytek opened this issue Nov 9, 2018 · 6 comments
Open

Alternative grouping of field resolvers #193

MichalLytek opened this issue Nov 9, 2018 · 6 comments
Assignees
Labels
Enhancement 🆕 New feature or request
Milestone

Comments

@MichalLytek
Copy link
Owner

MichalLytek commented Nov 9, 2018

In GraphQL, our API behaves like a huge graph that describe all of the relations between our types, e.g. that the Post has an Author, the Comment has an Author and the Post has Comments.

In TypeGraphQL we resolve this relations using @FieldResolver. We can define them inline, as a getter or method of object type class. But the complex ones that needs interact with other services/api/db are placed in @Resolver()'s classes to leverage the dependency injection mechanism.

The first idea that Nest also follow is to group field resolvers together in the resolver of base class, eg.:

@Resolver(of => Post)
class PostResolver {
  @FieldResolver()
  author(@Root() post: Post) {
    // returning an author of the post
  }

  @FieldResolver()
  comments(@Root() post: Post) {
    // returning the list of comments for the post
  }
}

The problem with that design is that it's not a strict post resolver as it has to interact with comments and users repositories too:

@Resolver(of => Post)
class PostResolver {
  constructor(
    private postRepository: Repository<Post>,
    private userRepository: Repository<User>,
    private commentRepository: Repository<Comment>,
  ) {}
 
  @Query()
  posts() {
    return this.postRepository.findAll();
  }
  
  @FieldResolver()
  author(@Root() post: Post) {
     return this.userRepository.findById(post.authorId);
  }

  @FieldResolver()
  comments(@Root() post: Post) {
    return this.commentRepository.findAll({ postId: post.id });
  }
}

Also, in order to reuse that logic we need to extract it all into separate services:

@Resolver()
class PostResolver {
  constructor(
    private postService: PostService,
  ) {}
 
  @Query()
  posts() {
    return this.postService.getAll();
  }
}

@Resolver(of => User)
class UserResolver {
  constructor(
    private postService: PostService,
  ) {}
 
  @FieldResolver()
  posts(@Root() user: User) {
    return this.postService.getUsersPosts(user);
  }
}

As our resolvers belong to the infrastructure layer, this designs might sometimes force us to create e.g. PostResolverService that would allow to reuse the graphql-related logic, like complex pagination required by Relay. After a long discussion in #168, I've decided that this problem has to be solved by TypeGraphQL.

So in order to allow for better resolver's reuse and less boilerplate (creating additional services, injecting multiple things into constructor), it would be nice to introduce grouping field resolvers by a logical scope - returned type. In that case, base query resolver would act as a base resolver and all field resolvers would only translate the requirements from the root type into a payload of the base resolver:

@Resolver()
class PostResolver {
  constructor(
    private postRespository: MongoRepository<Post>
  ) {}

  @Query(returns => [Post])
  getPosts(@Args() { filter, skip, limit }: PostsArgs) {
    const criteria: any = {};
    if (filter) {
      if (filter.createdAtMin) criteria.createdAt = { $gt: filter.createdAtMin };
      if (filter.votesMin) criteria.votes = { $gt: filter.votesMin };
      if (filter.authorId) criteria.authorId = filter.authorId;
    }
    return this.postRepository.findAll(criteria)
      .limit(limit)
      .skip(skip);
  }

  @FieldResolver(of => Author, author => author.posts) 
  getAuthorPosts(@Root() author: Author, @Args() args: PostsArgs) {
    // prepare your args based on context
    args.filter.authorId = author.id;
    // delegate the logic to the base resolver
    return this.getPosts(args);
  }
}

The new syntax @FieldResolver(of => Author, author => author.posts) would allow to register a field resolver for other object type than the base query type. Thanks to that, we can delegate the work of translating our graphql's arg into service/repository args to the base resolver instead of doing it for each field resolver that returns that object types.

If you have any questions or comments, feel free to leave those below 😃

@tonyxiao
Copy link

This sounds great! Would also allow a class to be field resolver for more than one object which is super handy!

@tonyxiao
Copy link

tonyxiao commented Mar 30, 2019

@19majkel94 is this related to #183 the new schema generation pipeline or a completely separate enhancement?

@MichalLytek
Copy link
Owner Author

MichalLytek commented Mar 30, 2019

@tonyxiao It's not directly related but the current schema generation pipeline was not designed for this feature so it might be really complicated to implement it in a clean way without rewriting or dirty patching miscellaneous things.

I am delaying all new features like this until #183 as it only adds more work in rewriting which inevitable.

@tonyxiao
Copy link

Makes total sense. Excited for #183 to land 😍

@Jacse
Copy link

Jacse commented Nov 1, 2021

Any update on this?

@MichalLytek
Copy link
Owner Author

MichalLytek commented Nov 1, 2021

@Jacse I've made a PoC on my experimental repo and it works great. But this would be released as a part of v2.0 refactor, hence I don't plan to backport it into 1.x branch and old schema pipeline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement 🆕 New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants