Why each GraphQL Object type has both "Resolver" and "RelationResolver" types?

For each GraphQL object that’s defined, there are corresponding [ObjectName]Resolver
and [ObjectName]RelationResolver that are generated.

Resolver fields are wrapped with OptArgsResolverFn
and RelationResolver fields are wrapped with RequiredResolverFn.

What is the reason behind having both these types defined?

Thank you

2 Likes

I’m curious if this is answered elsewhere? I have a similar question and am not understanding the behavior of these two objects as it relates to a gql query in a Cell.

In my case, I have a new Resolver that I created to handle the inclusion of related models in a return value:

export const technologyModelsWithRelations: QueryResolvers['technologyModelsWithRelations'] =
  ({ ids }) => {
    const technologyModels = db.technologyModel.findMany({
      where: {
        id: {
          in: ids,
        },
      },
      include: {
        technologyModelVersions: {
          orderBy: {
            version: 'desc',
          },
          take: 1,
        },
      },
    })
    return technologyModels
  }

The autogenerated RelationResolver is:

export const TechnologyModel: TechnologyModelRelationResolvers = {
  technologyModelVersions: (_obj, { root }) => {
    return db.technologyModel
      .findUnique({ where: { id: root?.id } })
      .technologyModelVersions()
  },
}

When I run the following in a Cell:

gql`
  query TechnologyModelsWithRelationsQuery($technology_model_ids: [Int!]!) {
    technologyModelsWithRelations(ids: $technology_model_ids) {
      id
      status
      clusterId
      technologyModelVersions {
        id
        version
        name
        description
      }
    }
  }
`
  • the return value includes all of the technologyModelVersions instead of being limited to a single result as defined in technologyModelsWithRelations.
  • If I update the TechnologyModelRelationResolvers definition to take: 1 then it “works” but it is unclear to me how I could then manage a separate query in which I do want to return all of the technologyModelVersions instead of just one?

Many thanks for any additional insight into this or places for me to read up!

Hi @gcallsen and glad to see you trying out RedwoodJS!

You’ve stumbled upon a topic that we should definitely clarify in the docs and that’s the way GraphQL plays with the services.

In Redwood, a service method (e.g. technologyModelsWithRelations) gets automatically mapped to a GraphQL resolver of the same name – and which you had defined in one of your sdl files. Redwood also assembles all the sdl (aka types), the services (aka resolvers), directives, and other things like subscription and such to make a large schema. We let you work on the schema in pieces and “merge:” everything that the GraphQL server function needs.

GraphQL sees queries like

post {
 id
 title  
 authors {
   id
   name
  }
}

as:

  • get me the post
  • get me the post’s authors

It will use the author resolver and the author’s books resolver to fetch that. And yes, this could be two separate queries.

Now, even if in your Prisma query as part of the post services you eager load and include the authors and even sort the authors by name, GraphQL won’t use that info but go back the the post’s authors resolver.

So, an example we can see if that books may not be sorted by title even though the author service is doing that.

Of course, you could replicate the sort logic in the post’s authors resolver … but there’s a better way.

If you eager load and include and sort the authors in your post service, you can “shortcut” in the posts’ authors resolver to ask – do I already have authors populated on my root object (ie, did post eager load and sort the authors I wanted already)? if, so no more work is needed and you can return the root as it doesn’t need to make another db call to fetch the post’s authors.

export const Post: PostRelationResolvers = {
  authors: (_obj, { root }) => {
   // shortcut and see if the root Post already has authors populated, and if so return them
   // if the post service eager loaded and included and sorted, they will retain that filtering
    if (root.authors) {
      return root.authors
   } 
   // otherwise you need to fetch them from the db
    return db.post.findUnique({ where: { id: root?.id } }).authors()
  },
}

We’ve been considering changing this in the default CRUD scaffolded services – and should document better.

However, with RSC and direct db fetching, this behavior will go away since what the service returns is what is returned.

Hope that helps.

I think you could try:

export const TechnologyModel: TechnologyModelRelationResolvers = {
  technologyModelVersions: (_obj, { root }) => {

    if (root.technologyModelVersions) {
      return root.technologyModelVersions
   } 

    return db.technologyModel
      .findUnique({ where: { id: root?.id } })
      .technologyModelVersions()
  },
}

and let your service of the take 1 etc.

Let me know if that works – and if not, we can try something else.

2 Likes

Amazing, thank you so much for the detailed/quick feedback! Yes, that does exactly what I expect with your suggested snippet. Lots of concepts to commit to memory but that’s always the case - I am loving working with Redwood.

1 Like

@gcallsen Glad it’s working. It’s a concept I said we haven’t documented or added to the generators well enough. It can help reduce N=1 queries, too, if you eager load common queries.

I often tend to write specific services w/out relation resolvers when I know I’ll have a specific return shape.

@dthyresson This was really useful knowledge. We were able to speed up some of our queries because we had already done a lot of include: { ... } and weren’t taking advantage of eager loading.

I did run into a type problem while adding if (root.model) return root.model to various relation resolvers:

“Type instantiation is excessively deep and possibly infinite. ts(2589)”

billede

Is that something you have seen before and is there a fix for it?

This error “only” occurs on 10 out of 90 relation resolvers, but I don’t see any pattern as to why it happens on some models and not others. It happens on a model with a single one-to-one relationship too.

Thanks - glad it helped.

I often don’t use relation resolvers and simply make specific services like “MembersWithOrganizations” or such and rely on Prisma to optimally include and eager load the data needed for the return type SDL. Even Wirth optimizing the relation resolver, it still has to be called by GraphQL even if it returns without database access so can be fmore efficient.

As for this, I have not but some Googling says that can “happen when TS decides that types become too complex to compute”. I don’t know your types, but it could be too recursive or too many keys or namespaces (again more Googling).

See Version 3.4-dev breaks recursive types · Issue #30188 · microsoft/TypeScript · GitHub

Or you may be ok just telling the linter to ignore that TS warning.