简体   繁体   中英

Resolve Custom Types at the root in GraphQL

I feel like I'm missing something obvious. I have IDs stored as [String] that I want to be able to resolve to the full objects they represent.

Background

This is what I want to enable. The missing ingredient is the resolvers:

const bookstore = `
  type Author {
    id: ID!
    books: [Book]
  }

  type Book {
    id: ID!
    title: String
  }

  type Query {
    getAuthor(id: ID!): Author
  }
`;

const my_query = `
  query {
    getAuthor(id: 1) {
      books { /* <-- should resolve bookIds to actual books I can query */
        title
      }
    }
  }
`;

const REAL_AUTHOR_DATA = [
  {
    id: 1,
    books: ['a', 'b'],
  },
];

const REAL_BOOK_DATA = [
  {
    id: 'a',
    title: 'First Book',
  },
  {
    id: 'b',
    title: 'Second Book',
  },
];


Desired result

I want to be able to drop a [Book] in the SCHEMA anywhere a [String] exists in the DATA and have Books load themselves from those Strings. Something like this:

const resolve = {
  Book: id => fetchToJson(`/some/external/api/${id}`),
};

What I've Tried

This resolver does nothing, the console.log doesn't even get called

const resolve = {
  Book(...args) {
    console.log(args);
  }
}

HOWEVER, this does get some results...

const resolve = {
  Book: {
    id(id) {
      console.log(id)
      return id;
    }
  }
}

Where the console.log does emit 'a' and 'b' . But I obviously can't scale that up to X number of fields and that'd be ridiculous.

What my team currently does is tackle it from the parent:

const resolve = {
  Author: {
    books: ({ books }) => books.map(id => fetchBookById(id)),
  }
}

This isn't ideal because maybe I have a type Publisher { books: [Book]} or a type User { favoriteBooks: [Book] } or a type Bookstore { newBooks: [Book] } . In each of these cases, the data under the hood is actually [String] and I do not want to have to repeat this code:

const resolve = {
  X: {
    books: ({ books }) => books.map(id => fetchBookById(id)),
  }
};

The fact that defining the Book.id resolver lead to console.log actually firing is making me think this should be possible, but I'm not finding my answer anywhere online and this seems like it'd be a pretty common use case, but I'm not finding implementation details anywhere.

What I've Investigated

  • Schema Directives seems like overkill to get what I want, and I just want to be able to plug [Books] anywhere a [String] actually exists in the data without having to do [Books] @rest('/external/api') in every single place.
  • Schema Delegation . In my use case, making Books publicly queryable isn't really appropriate and just clutters my Public schema with unused Queries.

Thanks for reading this far. Hopefully there's a simple solution I'm overlooking. If not, then GQL why are you like this...

If it helps, you can think of this way: types describe the kind of data returned in the response, while fields describe the actual value of the data. With this in mind, only a field can have a resolver (ie a function to tell it what kind of value to resolve to). A resolver for a type doesn't make sense in GraphQL.

So, you can either:

1. Deal with the repetition. Even if you have ten different types that all have a books field that needs to be resolved the same way, it doesn't have to be a big deal. Obviously in a production app, you wouldn't be storing your data in a variable and your code would be potentially more complex. However, the common logic can easily be extracted into a function that can be reused across multiple resolvers:

const mapIdsToBooks = ({ books }) => books.map(id => fetchBookById(id))

const resolvers = {
  Author: {
    books: mapIdsToBooks,
  },
  Library: {
    books: mapIdsToBooks,
  }
}

2. Fetch all the data at the root level instead. Rather than writing a separate resolver for the books field, you can return the author along with their books inside the getAuthor resolver:

function resolve(root, args) {
  const author = REAL_AUTHOR_DATA.find(row => row.id === args.id)
  if (!author) {
    return null
  }
  return {
    ...author,
    books: author.books.map(id => fetchBookById(id)),
  }
}

When dealing with databases, this is often the better approach anyway because it reduces the number of requests you make to the database. However, if you're wrapping an existing API (which is what it sounds like you're doing), you won't really gain anything by going this route.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM