Search code examples
graphqlgraphql-js

Different field types depending on args


My database is structured as following:

There is a Product table:

id (integer)
manufacture_id (integer)
state (boolean)

And there is a Product_translations table

product_id (integer)
language_id (integer)
name (string)
description (string)

When querying for a product I would like to be able to receive a name and description directly if I provide a language id as an argument, or receive a list of translations with all the language id's and name/description instead if I don't provide a language id.

Is there a way to achieve this without creating two different Types and two different Queries?


Solution

  • Yes and no.

    When you specify the return type for your query (let's call it getProduct), you can only specify one type (or a union or interface... more on that later). That type (Product) will have an immutable list of fields. When you make a request to your server, you will have to identify a subset of those fields to have the server return. With this in mind, it's not possible (at least natively) to send a query have the server return a different subset of fields depending on those arguments.

    That said, what you can do is define a type that includes all the possible fields, like this:

    type Product {
      id: ID!
      name: String
      description: String
      translations: [Translation!]!
    }
    

    Then within your resolver for getProduct, you can fetch the product from the table and then check whether language was provided as an argument. If it wasn't, fetch the list of translations and set your product's translations property to it. If language was provided, fetch just that translation, use it to populate the name and description properties of the product, and set translations to an empty array.

    In this way, depending on whether language is passed in as an argument, your returned Product will contain either A) null for name and description and a populated list of translations; or B) a name and description and an empty array for translations.

    There is, IMHO, also a more elegant alternative: unions and interfaces.

    As before, you'd need to construct your returned object appropriately based on whether the language argument is present. But instead of a type, you return a Union or Interface and then utilize the __resolveType field to return a specific type (each with different fields).

    There's two advantages to this approach: One, you avoid returning unnecessary null fields. And two, if you use Apollo as a client, it automatically tacks on a __typename field that you can use on the client-side to easily determine the type that was actually returned by a query.

    Here's an example you can plug right into Launchpad to play around with:

    import { makeExecutableSchema } from 'graphql-tools';
    
    const typeDefs = `
      type Query {
        getProduct (id: ID, language: ID):  ProductInterface
      },
      type Product  implements ProductInterface {
        id: ID
        translations: [Translation!]!
      },
      type TranslatedProduct implements ProductInterface {
        id: ID
        name: String
        description: String
      },
      type Translation {
        language: ID
        name: String
        description: String
      },
      interface ProductInterface {
        id: ID
      }
    `;
    
    const products = [
      {
        id: '1',
        translations: [ 
          {
            language: '100',
            name: 'Foo',
            description: 'Foo!'
          },
          {
            language: '200',
            name: 'Qux',
            description: 'Qux!'
          }
        ]
      }
    ]
    
    const resolvers = {
      Query: {
        getProduct: (root, {id, language}, context) => {
          const product = products.find(p => p.id === id)
          if (language) {
            product.translation = product.translations.find(t => t.language === language)
          }
          return product
        },
      },
      ProductInterface: {
        __resolveType: (root) => {
          if (root.translation) return 'TranslatedProduct'
          return 'Product'
        }
      },
      TranslatedProduct: {
        name: (root) => root.translation.name,
        description: (root) => root.translation.description
      }
    };
    
    export const schema = makeExecutableSchema({
      typeDefs,
      resolvers,
    });
    

    You can then request a query like this:

    {
      getProduct (id: "1", language: "200") {
        __typename
        ... on Product {
          translations {
            language
            name
            description
          }
        }
        ... on TranslatedProduct {
          name
          description
        }
      }
    }