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

Value Object - DDD in Ruby on Rails

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

Introduction to DDD in Ruby on Rails

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

Safe data migrations in Rails

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

I love dev, and so do we!

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

Updated guide to recruitment process at Visuality

14
.
11
.
2023
Michał Łęcicki
Visuality
HR

Visuality Academy for wannabe Junior Engineers

14
.
11
.
2023
Michał Piórkowski
HR
Visuality

How to approach funding as an MVP

14
.
11
.
2023
Michał Piórkowski
Business
Startups

Visuality 13th birthday

14
.
11
.
2023
Michał Piórkowski
HR
Visuality

How To Receive Emails With a Rails App in 2021

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

Project Quality in IT - How to Make Sure You Will Get What You Want?

14
.
11
.
2023
Wiktor De Witte
Ruby on Rails
Project Management
Business

5 Trends in HR Tech For 2021

14
.
11
.
2023
Maciej Zdunek
Business
Project Management

Is Go Language the Right Choice for Your Next Project?

14
.
11
.
2023
Maciej Zdunek
Backend
Business

SXSW Tradeshow 2020: Get Your FREE Tickets and Meet Us

14
.
11
.
2023
Michał Krochecki
Ruby on Rails
Conferences
Frontend
Backend
Business

How to build effective website: simplicity & McDonald's

14
.
11
.
2023
Lukasz Jackiewicz
Ruby on Rails
Frontend
Design

Thermal Printer Protocols for Image and Text

14
.
11
.
2023
Burak Aybar
Backend
Tutorial
Software

WebUSB - Print Image and Text in Thermal Printers

14
.
11
.
2023
Burak Aybar
Backend
Tutorial
Software

What happened in Visuality in 2019

14
.
11
.
2023
Maciej Zdunek
Visuality
HR

Three strategies that work in board games and in real life

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