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

Nested Resolvers #475

Closed
j3bb9z opened this issue Nov 13, 2019 · 8 comments
Closed

Nested Resolvers #475

j3bb9z opened this issue Nov 13, 2019 · 8 comments

Comments

@j3bb9z
Copy link

j3bb9z commented Nov 13, 2019

I'm submitting a...


[ ] Regression 
[ ] Bug report
[x] Feature request
[x] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

No possibility to create nested resolvers (or no info in the docs).

Expected behavior

Possibility to make nested resolvers (and/or appropriate info in the docs).

Minimal reproduction of the problem with instructions

In the docs we have example how to make Posts property resolver on Author Resolver. But we don't have any info, how to make nested resolver on those posts. For example - we'd like to lazy fetch (only when someone requests the field) posts statistics. Seems like this isn't possible at the moment?

@Resolver(of => Author)
export class AuthorResolver {
  constructor(
    private readonly authorsService: AuthorsService,
    private readonly postsService: PostsService,
    private readonly statsService: StatsService, // hypothetical, low-performing service
  ) {}

  @Query(returns => Author)
  async author(@Args({ name: 'id', type: () => Int }) id: number) {
    return await this.authorsService.findOneById(id);
  }

  @ResolveProperty()
  async posts(@Parent() author) {
    const { id } = author;
    return await this.postsService.findAll({ authorId: id });
  }

  // This does NOT work. Nor is mentioned anywhere in the docs.
  @ResolveProperty()
  async stats(@Parent() post) {
    const { id } = post;
    return await this.statsService.getPostStatistics({ postId: id });
  }
}

So, is this possible in any way? If not, would a feature request like this be acceptable?

What is the motivation / use case for changing the behavior?

One of the biggest advantages of graphQL is lazy fetching nested fields (graph-like connections). Current implementation of graphQL decorators probably doesn't allow creating nested resolvers.

Edit: Found similar question here: #162, but no luck with an answer yet. :(

@krislefeber
Copy link

I'm not sure I completely follow, but how would it know where the @parent post is from? Why can't this be in its own Resolver class? It looks like you're trying to shove two entities in the same Resolver, when they should really be two separate classes

@j3bb9z
Copy link
Author

j3bb9z commented Nov 14, 2019

@krislefeber I just want to make possible query like this one:

{
    Level1(id: "someId") {
        level1Field
        Level2(optionalParam: "optional") {
            level2Field
            Level3(optionalParam: "optional") {
                level3Field
            }
        }
    }
}

It is perfectly fine, to have it in separate resolver classes... but is it possible to nest it like this?

@krislefeber
Copy link

krislefeber commented Nov 16, 2019

You would end up with something like this.

First define the classes. Notice that there are no decorators on the object references. We instead add the mappings to that object in the Resolver.

@ObjectType()
class Level1 {
    @Field(() => ID)
    level1Field: string;
    level2: Promise<Level2>;
}

@ObjectType()
class Level2 {
    @Field(() => ID)
    level2Field: string;
    level3: Promise<Level3>;
}

@ObjectType()
class Level3 {
    @Field(() => ID)
    level3Field: string;
} 

In the first resolver, add the initial query for a search by id. We also add the getLevel2Property function, which tells graphql how to connect the two objects.

@Resolver(Level1)
class Level1Resolver {

    @Query(() => Level1, {
        name: 'Level1',
    })
    public async getLevel1(@Args('id') id: string): Promise<Level1> {
        const level1 = new Level1();
        level1.level1Field = id;
        return level1;
    }

    @ResolveProperty(() => Level2, {
        name: 'Level2',
    })
    public async getLevel2Property(@Args('optionalParam') optionalParam: string, @Parent() parent: Level1): Promise<Level2> {
       // this can be whatever logic you have to connect the two entities
        const level2 = new Level2();
        if (optionalParam) {
            level2.level2Field = optionalParam;
        }
        return level2;
    }
}

In the level 2 resolver, we need to add the mapping to Level3. Once again, the code inside getLevel3Property can be whatever you want.

@Resolver(Level2)
class Level2Resolver {

    @ResolveProperty(() => Level3, {
        name: 'Level3',
    })
    public async getLevel3Property(@Args('optionalParam') optionalParam: string, @Parent() parent: Level2) {

        const level3 = new Level3();
        if (optionalParam) {
            level3.level3Field = optionalParam;
        }
        return level3;
    }
}

Let me know if that makes sense. Thanks

@j3bb9z
Copy link
Author

j3bb9z commented Nov 16, 2019

@krislefeber Thanks for help, that did work!

I actually tried similar approach before but had some errors, so I thought it won't work like that. Turns out it does, so I must have messed something up. 🤔

Anyway - thanks again, I appreciate it. 🙂

@j3bb9z j3bb9z closed this as completed Nov 16, 2019
@m7rlin
Copy link

m7rlin commented Jan 26, 2020

How to get Level1 input fields like (id: "someId") in Level2 resolver?

{
    Level1(id: "someId") {
        level1Field
        Level2 {
            level2Field
            Level3 {
                level3Field
            }
        }
    }
}

@krislefeber
Copy link

@m7rlin, couldn't you just pass that input in again?

@m7rlin
Copy link

m7rlin commented Jan 26, 2020

@krislefeber if I pass input again like so:

{
    Level1(id: "someId") {
        level1Field
        Level2(id: "someId") {
            level2Field
            Level3 {
                level3Field
            }
        }
    }
}

it will work but I don't want duplication and maybe later on I will add another input at Level2.
I created something like this:

@Resolver(Level1)
class Level1Resolver {

...

    @ResolveProperty(() => Level2, {
        name: 'Level2',
    })
    public async getLevel2Property(@Args('input') input: string, @Parent() parent: Level1) {
        return { _inputLevel1: input};
    }
}

On Level2 resolver I can access parent object. I don't know any other solution. It will be nice to have custom decorator which will achieve similar solution.

@lock
Copy link

lock bot commented Apr 25, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Apr 25, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants