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

5 marketing hacks which will make your life easier

14
.
11
.
2023
Maciej Zdunek
Marketing
Design

JSON:API consumption in Rails

14
.
11
.
2023
Jan Matusz
Ruby on Rails
Backend
Tutorial

Marketing hacks #01: How to Track off-line conversions

14
.
11
.
2023
Marek Łukaszuk
Ruby on Rails
Business
Marketing

Common communication issues in project management

02
.
10
.
2024
Michał Krochecki
Project Management

Selected SXSW lectures takeaways

14
.
11
.
2023
Michał Piórkowski
Conferences
Frontend
Backend
Business

SXSW Summary

14
.
11
.
2023
Michał Piórkowski
Ruby on Rails
Conferences
Frontend
Backend
Business

How to get the most out of SXSW Interactive

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Conferences
Frontend
Backend
Business

Guide to recruitment at Visuality

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

TOP Ruby on Rails Developers

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business

How to conquer Westworld?

14
.
11
.
2023
Maciej Zdunek
Business
Marketing

2018 Rewind by Visuality

02
.
10
.
2024
Michał Krochecki
HR
Visuality

Quality Assurance Testing

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

Why do we like to be together?

02
.
10
.
2024
Michał Krochecki
Visuality
HR

Wallboards - a great value for our teams and clients

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Design
Project Management
Backend

2018 Clutch Global Leader

14
.
11
.
2023
Maciej Zdunek
Ruby on Rails
Visuality
Business
Marketing

Hot topic: Progressive Web Apps instead of native mobile apps

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Business
Backend
Frontend

Docker hosted on Jelastic

14
.
11
.
2023
Marcin Prokop
Ruby on Rails
Backend
Tutorial

All the pieces matter - Visuality DNA

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

Tech conferences 2018/2019 you definitely should attend

02
.
10
.
2024
Michał Krochecki
Conferences

Visuality Poznań is here!

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

Why we chose Ruby on Rails and React.js for our main technologies? (FAQ).

02
.
10
.
2024
Michał Krochecki
Ruby on Rails
Backend
Frontend
Visuality