Skip to content

Instantly share code, notes, and snippets.

@reggi
Last active July 10, 2023 14:11
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save reggi/3d7db7693f2e4a327af59df61acb8a92 to your computer and use it in GitHub Desktop.
Save reggi/3d7db7693f2e4a327af59df61acb8a92 to your computer and use it in GitHub Desktop.
How do I have nested resolvers in nestjs / type-graphql?

I am trying to find a way that a resolver can essentially return another resolver using nest.js for creating Resolvers, and type-graphql to create object types.

Here's an example with the star-wars graphql api

{
  allVehicles(first: 1, last: 100) {
    vehicles {
      name
      id
      filmConnection(first: 1, last: 100) {
        films {
          title
          characterConnection(first: 1, last: 100) {
            characters {
              name
            }
          }
        }
      }
    }
  }
}

As you can see above the vehicles type returns a filmConnection which allows you to pass in arguments. In type-graphql, would this be apart of the @ObjectType would it be defined in the @Resolver? Or does it need to be defined in some other way?


Failed Attempts

I set up an example of a couple of nested objects.

Which should look something like this as a query:

Note there are no arguments to these types, that could be optional, but I'd like the resolvers to pass along parent information.

{
  human {
    head {
      eyes {
        color
      }
    }
    body {
      neck
    }
  }
}

Here are the @ObjectTypes

@ObjectType()
class Eyes {
    @Field(type => String)
    color: string
}

@ObjectType()
class Head {
    @Field(type => Eyes, { nullable: true })
    eyes?: Eyes
}

@ObjectType()
class Body {
    @Field(type => String)
    neck: string
}

@ObjectType()
class Human {
    @Field(type => String, { nullable: true })
    name: string

    @Field(type => Head, { nullable: true })
    head?: Head

    @Field(type => Body, { nullable: true })
    body?: Body
}

First Approach, multiple Resolvers

@Resolver(type => Human)
export class HumanResolver {

    @Query(type => Human)
    human(): Human {
        console.log('a')
        return { name: 'Greg' }
    }

}

@Resolver(type => Head)
export class HeadResovler {

    @Query(type => Head)
    head(): Head {
        console.log('B')
        return {}
    }

}

@Resolver(type => Body)
export class BodyResolver {

    @Query(type => Body)
    body(): Body {
        console.log('C')
        return { neck: '102' }
    }

}

@Resolver(type => Eyes)
export class EyesResolver {

    @Query(type => Eyes)
    eyes(): Eyes {
        console.log('d')
        return { color: 'brown' }
    }

}

First Approach, One Resolvers, using @ResolveProperty

Here's another way to do it possibly, with one top level resovler and the rest are properties.

@Resolver(type => Human)
export class BeingResolver {

    @Query(type => Human)
    human(): Human {
        console.log('a')
        return { name: 'Greg' }
    }

    @ResolveProperty(type => Head)
    head(): Head {
        console.log('b')
        return {}
    }

    @ResolveProperty(type => Body)
    body(): Body {
        console.log('c')
        return { neck: '102' }
    }

    @ResolveProperty(type => Eyes)
    eyes(): Eyes {
        console.log('d')
        return { color: 'brown' }
    }
}

The main issue I see here is that @ResolveProperty only goes one level deep.

@reggi
Copy link
Author

reggi commented Nov 27, 2019

Perhaps one solution found here: nestjs/graphql#475

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

@reggi
Copy link
Author

reggi commented Nov 28, 2019

Here's a working example:

@ObjectType()
class Level1 {
    @Field(() => String)
    id: string;

    @Field(() => Level2)
    getLevel2Property: Promise<Level2>;
}
@ObjectType()
class Level2 {
    @Field(() => String)
    id: string;

    @Field(() => Level3)
    getLevel3Property: Promise<Level3>;
}
@ObjectType()
class Level3 {
    @Field(() => String)
    id: string;
}
@Resolver(Level1)
export class Level1Resolver {

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

    @ResolveProperty(() => Level2, {
        name: 'Level2',
    })
    public async getLevel2Property(
        @Args('id') id: string,
        @Parent() parent: Level1
    ): Promise<Level2> {
        const level2 = new Level2();
        level2.id = id;
        return level2;
    }
}
@Resolver(Level2)
export class Level2Resolver {

    @ResolveProperty(() => Level3, {
        name: 'Level3',
    })
    public async getLevel3Property(
        @Args('id') id: string,
        @Parent() parent: Level2
    ) {
        const level3 = new Level3();
        level3.id = id;
        return level3;
    }
}
query Levels {
  Level1(id: "1") {
    id
    getLevel2Property(id: "2") {
      id
      getLevel3Property(id: "3") {
        id
      }
    }
  }
}

@shriomtri
Copy link

shriomtri commented Aug 19, 2022

This is not working as expected.

getLevel3Property throws error. Does not take any argument but in resolved still requires it.

@heji-staizen
Copy link

Thanks @reggi , If only I had seen this much earlier, it could have saved me a lot of time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment