14
.
11
.
2023
30
.
10
.
2023
Ruby on Rails
GraphQL
Backend
Tutorial

GraphQL in Ruby on Rails: How to Extend Connections

Cezary Kłos
Ruby Developer

Connections in GraphQL

To enable pagination, GraphQL utilizes a cursor-based system known as Connections. As outlined in the specification, the default connection should at least implement the following structure:

  • Edges: These represent the actual data nodes in the connection. Each edge contains a cursor that points to the corresponding data item.
  • Nodes: The actual data objects within each edge. These nodes contain the relevant information associated with the particular data item.
  • Page Info: This provides information about the pagination, such as the existence of previous and next pages.

The specification states that each of the above can be extended with additional fields as the schema designer deems necessary. If you're interested in learning how to do this, continue reading.

An example GraphQL application

Let’s use this simple has_many :through association as an example

Diagram of 3 models: 
Student model with fields: id and name. Course model with fields: id and title. Enrollment model with fields: id, student_id, course_id, enrollment_date. 
Models are connected with 2 arrows. The first arrow points from Enrollment#student_id to Student#id, second arrow points from Enrollment#course_id to Course#id.

The GraphQL code could look like this:


# app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :student, StudentType, null: true do
      argument :id, ID, required: true
    end

    def student(id:)
      ::Student.find(id)
    end
end

# app/graphql/types/student_type.rb
module Types
  class StudentType < BaseObject
    field :id, ID, null: false
    field :name, String
    field :courses, CourseType.connection_type, null: false
  end
end

# app/graphql/types/course_type.rb
module Types
  class CourseType < BaseObject
    field :id, ID, null: false
    field :title, String
  end
end

A query with all fields required by GraphQL specification would look like this:


query {
  student(id: 1) {
    id
    name
    courses(first: 10) {
      edges {
        cursor
        node {
          id
          title
        }
      }
      pageInfo {
        hasNextPage
        hasPreviousPage
        startCursor
        endCursor
      }
    }
  }
}

Exposing information from the join table

The enrollment join table contains the field enrollment_date that should be exposed in a query. According to the GraphQL-Ruby docs:

Edges can reveal (…) information about the relationship.

However, the documentation does not provide an example of how to do it. But don't worry, you can accomplish this in these 4 steps:

  1. Create a Custom Connection Type class and:

    • register a custom Edge class,
    • define the nodes method (resolver method for default field).
    
    # app/graphql/types/enrollment_connection.rb
    module Types
      class EnrollmentConnection < BaseConnection
        # register a custom Edge class
        edge_type(EnrollmentEdge)
    
        # This method is required for 'nodes' shorthand to work
        def nodes
          # object is GraphQL::Pagination wrapper for ActiveRecord::AssociationRelation
          object.edge_nodes.map(&:course)
        end
      end
    end
    
    
  2. Create a Custom Edge Type class and:

    • register a Node class,
    • define of fields to expose,
    • define resolver methods for defined fields,
    • define a node method (resolver method for default field).
    
    # app/graphql/types/enrollment_edge.rb
    module Types
      class EnrollmentEdge < BaseEdge
        # register a Node class
        node_type(Types::CourseType)
    
        # define field and resolver
        field :enrollment_date, GraphQL::Types::ISO8601Date, null: true
    
        def enrollment_date
          # object.node is an instance of Enrollment class
          object.node.enrollment_date
        end
    
        # point node method to the associated course
        def node
          object.node.course
        end
      end
    end
    
    
  3. Register a custom Connection field on a Student Type

    
    # app/graphql/types/student_type.rb
    module Types
      class StudentType < BaseObject
        field :id, ID, null: false
        field :name, String
        # connection is inferred from the type's name ending in *Connection
        field :courses, EnrollmentConnection 
    
        def courses
          object.enrollments.includes(:course)
        end
      end
    end
    
    
  4. Include enrollmentDate in the query

    
    query {
      student(id: 1) {
        id
        name
        courses(first: 10) {
          edges {
            cursor
            enrollmentDate  # <--------- here
            node {
              id
              title
            }
          }
        }
      }
    }
    
    

That was easy!

Remark: Please be cautious of the N+1 issue that can be easily introduced here. Instead of using includes(:course), consider using GraphQL::Dataloader to batch-load tags from EnrollmentEdge#node, or you can also explore the ar_lazy_preload gem for automagic preloading.

Showing the number of records

With the above structure, we can easily paginate by passing the cursor to the after argument until we reach the endCursor or until hasNextPage returns false.

However, in this implementation, there is no way to determine the total number of pages or records.

To address this, modify the EnrollmentConnection class. However, since this is a common requirement for accessing data, you can also add it to your BaseConnection. Let's add a recordCount field and a resolver.


# app/graphql/types/base_connection.rb
module Types
  class BaseConnection < Types::BaseObject
    include GraphQL::Types::Relay::ConnectionBehaviors

    field :record_count, Integer

    def record_count
      object.items.size
    end
  end
end

Now add it to the query:


query {
  student(id: 1) {
    id
    name
    courses(first: 10) {
      recordCount  # <--------- here
      edges {
        cursor
        enrollmentDate
        node {
          id
          title
        }
      }
    }
  }
}

If you need, you can extend other parts of a Connection like pageInfo in a similar fashion. Here is the repository with the app used in the examples. Feel free to experiment with it. Be sure to check out branches other than 'main' as well.

Cezary Kłos
Ruby Developer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

Stacey Matrix & Takeaways - why does your IT project suck?

14
.
11
.
2023
Wiktor De Witte
Project Management
Business

A simple guide to pessimistic locking in Rails

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Backend
Ruby
Tutorial

Poltrax design - story of POLTRAX (part 3)

04
.
12
.
2023
Mateusz Wodyk
Startups
Business
Design

Writing Chrome Extensions Is (probably) Easier Than You Think

14
.
11
.
2023
Antoni Smoliński
Tutorial
Frontend
Backend

Bounded Context - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

The origin of Poltrax development - story of POLTRAX (part 2)

29
.
11
.
2023
Stanisław Zawadzki
Ruby on Rails
Startups
Business
Backend

Ruby Meetups in 2022 - Summary

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Visuality
Conferences

Repository - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Example Application - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

How to launch a successful startup - story of POLTRAX (part 1)

14
.
11
.
2023
Michał Piórkowski
Ruby on Rails
Startups
Business

How to use different git emails for different projects

14
.
11
.
2023
Michał Łęcicki
Backend
Tutorial

Aggregate - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Visuality at wroc_love.rb 2022: It's back and it's good!

14
.
11
.
2023
Patryk Ptasiński
Ruby on Rails
Conferences
Ruby

Our journey to Event Storming

14
.
11
.
2023
Michał Łęcicki
Visuality
Event Storming

Should I use Active Record Callbacks?

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Backend
Tutorial

How to rescue a transaction to roll back changes?

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Backend
Ruby
Tutorial

Safe navigation operator '&.' vs '.try' in Rails

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Backend
Ruby
Tutorial

What does the ||= operator actually mean in Ruby?

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Backend
Ruby
Tutorial

How to design an entity - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Entity - DDD in Ruby on Rails

17
.
03
.
2024
Paweł Strzałkowski
Ruby on Rails
Domain-Driven Design
Backend
Tutorial

Should I use instance variables in Rails views?

14
.
11
.
2023
Mateusz Woźniczka
Ruby on Rails
Frontend
Backend
Tutorial

Data Quality in Ruby on Rails

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Backend
Software