1

The Problem

Looking at this GraphQL query,

query {
    asset {
        name
        interfaces {
            created 
            ip_addresses {
                value
                network {
                    name
                }
            }
        }
    }
}

How do I define a resolver for just the network field on ip_addresses?

My First Thought

Reading docs the give examples of single nested queries, e.g

const resolverMap = {
  Query: {
    author(obj, args, context, info) {
      return find(authors, { id: args.id });
    },
  },
  Author: {
    posts(author) {
      return filter(posts, { authorId: author.id });
    },
  },
};

So I thought - why not just apply this pattern to nested properties?

const resolverMap = {  
  Query: {
      asset,
  },
  Asset: {
    interfaces: {
      ip_addresses: {
        network: () => console.log('network resolver called'),
      },
    },
  },
};

But this does not work, when I run the query - I do not see the console log.

Further Testing

I wanted to make sure that a resolver will always be called if its on root level of the query return type.

My hypothesis:

Asset: {
    properties: () => console.log('properties - will be called'), // This will get called
    interfaces: {
      created: () => console.log('created - wont be called'),
      ip_addresses: {
        network_id: () => console.log('network - wont be called'),
      },
    },

  },

And sure enough my console showed

properties - will be called

The confusing part

But somehow apollo is still using default resolvers for created and ip_addresses, as I can see the returned data in playground.

Workaround

I can implement "monolith" resolvers as follows:

Asset: {
    interfaces,
  },

Where the interfaces resolver does something like this:

export const interfaces = ({ interfaces }) =>
  interfaces.map(interfaceObj => ({ ...interfaceObj, ip_addresses: ip_addresses(interfaceObj) }));

export const ip_addresses = ({ ip_addresses }) =>
  ip_addresses.map(ipAddressObj => ({
    ...ipAddressObj,
    network: network(null, { id: ipAddressObj.network_id }),
  }));

But I feel that this should be handled by default resolvers, as these custom resolvers aren't actually doing anything, but passing data down to another resolver.

Daniel Cooke
  • 1,426
  • 12
  • 22

1 Answers1

2

The resolver map passed to the ApolloServer constructor is an object where each property is the name of a type in your schema. The value of this property is another object, wherein each property is a field for that type. Each of those properties then maps to a resolver function for that specified field.

You posted a query without posting your actual schema, so we don't know what any of your types are actually named, but assuming the network field is, for example, Network, your resolver map would need to look something like:

const resolver = {
  // ... other types like Query, IPAddress, etc. as needed
  Network: {
    name: () => 'My network name'
  } 
}

You can, of course, introduce a resolver for any field in the schema. If the field returns an object type, you return a JavaScript Object and can let the default resolver logic handle resolving "deeper" fields:

const resolvers = {
  IPAddress: {
    network: () => {
      return {
        name: 'My network name',
      }
    }
  }
}

Or...

const resolvers = {
  Interface: {
    ip_addresses: () => {
      return [
        { 
          value: 'Some value',
          network: {
            name: 'My network name',
          },
        },
      ]
    }
  }
}

Where you override the default resolver just depends at what point the data returned from your root-level field no longer matches your schema. For a more detailed explanation of the default resolver behavior, see this answer.

Daniel Rearden
  • 80,636
  • 11
  • 185
  • 183