s.susini
s.susini

Reputation: 601

GraphQL nested resolvers

I'm new to GraphQL and I'm trying to set up a demo app to give a little introductory talk to my colleagues.

I'm working with NodeJS.

I've come up with the following schema:

type Query {
  author(id:Int!): Author
  song(id:Int!): Song
}
type Author {
  id:Int!
  name:String!
  songs(max:Int):[Song]!
}
type Song {
  id:Int!
  name:String!
  author:Author!
}

And this is the part of the resolvers relevant to the Song.author association:

[...]
Song: {
  author: ({ id }, args, context, info) => {
    return mergeInfo.delegate(
      'query',
      'author',
      { id },
      context,
      info,
    );
  }
}
[...]

So the problem with this approach is that I need to add the Song.id to the containing query to be able to include the Song.author in it:

{
  song(id: 1) {
    id
    author {
      name
    }
  }
}

The following would not work:

{
  song(id: 1) {
    author {
      name
    }
  }
}

Depending on the implementation it would give me an error or null (that would be even worse).

This forces who writes the query to know the implementation details of the back-end and this is obviously bad. :P

Does anyone have any solution to this issue? Is there anything that I overlooked?

I've tried using the info object, but this would solve the problem just because the id I need is part of the query, but I could come up with a scenario in which the param that I need is in the data available only in the back-end.

UPDATE:

As requested by Daniel (thanks), this is the whole test file that creates the schema including the stitching:

const { makeExecutableSchema, mergeSchemas } = require('graphql-tools');

const DATA = {
  authors: {
    1: { id: 1, name: 'John' },
    2: { id: 2, name: 'Paul' },
  },
  songs: {
    1: { id: 1, name: 'Love me do', authorId: 1 },
    2: { id: 2, name: 'I wanna be your man', authorId: 1 },
    3: { id: 3, name: 'I\'ll be back', authorId: 2 },
  }
};

const authorsTypes = `
  type Query {
    author(id:Int!): Author
  }
  type Author {
    id:Int!
    name:String!
  }
`;
const authorSchema = makeExecutableSchema({
  typeDefs: authorsTypes,
  resolvers: {
    Query: {
      author: (_, { id }) => DATA.authors[id],
    },
  },
});
const authorsLinksTypes = `
  extend type Author {
    songs(max:Int):[Song]!
  }
`;
const authorsLinksResolvers = mergeInfo => ({
  Author: {
    songs: ({ id }, args, context, info) => {
      return Object.values(DATA.songs).filter(it => it.authorId === id)
    }
  },
});

const songsTypes = `
  type Query {
    song(id:Int!): Song
  }
  type Song {
    id:Int!
    name:String!
  }
`;
const songsSchema = makeExecutableSchema({
  typeDefs: songsTypes,
  resolvers: {
    Query: {
      song: (_, { id }) => DATA.songs[id],
    },
  },
});
const songsLinksTypes = `
  extend type Song {
    author:Author!
  }
`;
const songsLinksResolvers = mergeInfo => ({
  Song: {
    author: ({ id }, args, context, info) => {
      return mergeInfo.delegate(
        'query',
        'author',
        { id },
        context,
        info,
      );
    }
  },
});

module.exports = mergeSchemas({
  schemas: [authorSchema, songsSchema, songsLinksTypes, authorsLinksTypes],
  resolvers: mergeInfo => ({
    ...songsLinksResolvers(mergeInfo),
    ...authorsLinksResolvers(mergeInfo),
  }),
});

Upvotes: 2

Views: 4894

Answers (1)

Daniel Rearden
Daniel Rearden

Reputation: 84657

The easiest way to handle this would be to utilize context to pass down the song id. That means you would need to modify the resolver for the song query like this:

Query: {
  song: (_, { id }, context) => {
    context.songId = id
    return DATA.songs[id]
  },
},

and then you can grab the id from the context inside your author resolver

Song: {
  author: (song, args, context, info) => {
    const id = song.id || context.songId
    return mergeInfo.delegate(
      'query',
      'author',
      { id },
      context,
      info,
    );
  }
},

Upvotes: 1

Related Questions