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

Michal - Highlights from Ruby Unconf 2024

Highlights from Ruby Unconf 2024

14
.
11
.
2023
Michał Łęcicki
Conferences
Visuality
Cezary Kłos - Optimizing Cloud Infrastructure by $40 000 Annually

Optimizing Cloud Infrastructure by $40 000 Annually

14
.
11
.
2023
Cezary Kłos
Backend
Ruby on Rails

Smooth Concurrent Updates with Hotwire Stimulus

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

Table partitioning in Rails, part 2 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
Ruby on Rails

N+1 in Ruby on Rails

14
.
11
.
2023
Katarzyna Melon-Markowska
Ruby on Rails
Ruby
Backend

Turbo Streams and current user

29
.
11
.
2023
Mateusz Bilski
Hotwire
Ruby on Rails
Backend
Frontend

Showing progress of background jobs with Turbo

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

Table partitioning in Rails, part 1 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend
Ruby on Rails

Table partitioning types - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend

Indexing partitioned table - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
SQL Views in Ruby on Rails

SQL views in Ruby on Rails

14
.
11
.
2023
Jan Grela
Backend
Ruby
Ruby on Rails
Postgresql
Design your bathroom in React

Design your bathroom in React

14
.
11
.
2023
Bartosz Bazański
Frontend
React
Lazy Attributes in Ruby - Krzysztof Wawer

Lazy attributes in Ruby

14
.
11
.
2023
Krzysztof Wawer
Ruby
Software

Exporting CSV files using COPY - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Ruby
Ruby on Rails
Michał Łęcicki - From Celluloid to Concurrent Ruby

From Celluloid to Concurrent Ruby: Practical Examples Of Multithreading Calls

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

Super Slide Me - Game Written in React

14
.
11
.
2023
Antoni Smoliński
Frontend
React
Jarek Kowalewski - ILIKE vs LIKE/LOWER - Postgres Stories

ILIKE vs LIKE/LOWER - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby
Ruby on Rails
Postgresql

A look back at Friendly.rb 2023

14
.
11
.
2023
Cezary Kłos
Conferences
Ruby

Debugging Rails - Ruby Junior Chronicles

14
.
11
.
2023
Piotr Witek
Ruby on Rails
Backend
Tutorial

GraphQL in Ruby on Rails: How to Extend Connections

14
.
11
.
2023
Cezary Kłos
Ruby on Rails
GraphQL
Backend
Tutorial

Tetris on Rails

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

EURUKO 2023 - here's what you've missed

14
.
11
.
2023
Michał Łęcicki
Ruby
Conferences

Easy introduction to Connection Pool in ruby

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

When crazy ideas bring great time or how we organized our first Conference!

04
.
12
.
2023
Alexander Repnikov
Ruby on Rails
Conferences
Visuality