Union and interface types in GraphQL Ruby

leehambley

Lee Hambley

Posted on July 30, 2020

Union and interface types in GraphQL Ruby

This is a quick post of a self-contained MiniTest example of how to implement resolve_type on a GraphQL::Schema if you are using graphql-ruby without using the Ruby DSL to define your schema.

graphql-ruby provides two ways to create a schema, one from the .graphql file directly (from_definition) and by using the "class based definition"

Taking a Schema Definition Language (SDL) such as GraphQL and hiding it behind a Domain Specific Language (DSL) in a higher level turing-complete language strikes me as utterly irresponsible, the value of an SDL is in the interchange format, and for us that was an absolute necessity.

Many features of graphql-ruby behave differently, or simply don't work at all, or are undocumented if you use the SDL approach rather than their preferred DSL, we run into issues with it from time to time.

In this case, without the DSL the Gem cannot infer what type a resolver result has, and how to resolve that using the variant types from the union. It is required to implement a resolve_type function in your resolver tree, but where and how to do that isn't obvious, I hope this post helps:

# frozen_string_literal: true

require 'test_helper'

module Schema
  class UnionsFromDefinitionTest < Minitest::Test
    def definition
      <<~EOSCHEMA
        type Wimpel {
          id: ID!
        }
        type Doodad {
          code: ID!
        }
        union Widget = Doodad | Wimpel
        type Query {
          widget: [Widget]!
        }
      EOSCHEMA
    end

    def resolvers
      Sch3ma.default_resolvers.merge(
        {
          'Query' => {
            'widget' => lambda { |_obj, _args, _ctx|
              [OpenStruct.new(id: 'i am wimpel'), OpenStruct.new(code: 'i am doodad')]
            }
          },
          'resolve_type' => lambda { |_type, obj, ctx|
            type_name = if obj.respond_to? :id
                          'Wimpel'
                        elsif obj.respond_to? :code
                          'Doodad'
                        end
            ctx.schema.types[type_name] || raise('boom')
          }
        }
      )
    end

    def schema
      GraphQL::Schema.from_definition(
        definition,
        default_resolve: resolvers
      )
    end

    def test_querying_union_types
      res = schema.execute(<<~EOQUERY)
        query {
          widget {
            ... on Wimpel {
              id
            }
            ... on Doodad {
              code
            }
          }
        }
      EOQUERY
      assert_equal({ 'widget' => [{ 'id' => 'i am wimpel' }, { 'code' => 'i am doodad' }] }, res['data'])
    end
  end
end

In reality our schema lives in a standlone package (Gem) and we have an implementation of the resolver map in our actual application, the schema is used by other teams too. This test-case in the schema repo helps serve as documentation on how this feature is supposed to be used with the SDL rather than the DSL.

💖 💪 🙅 🚩
leehambley
Lee Hambley

Posted on July 30, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Three Ruby Links #3
ruby Three Ruby Links #3

September 3, 2024

ActionPolicy , GraphQL and Rails
ruby ActionPolicy , GraphQL and Rails

September 12, 2023

Intro to GraphqlRails
undefined Intro to GraphqlRails

August 16, 2023