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

NASA 1st global hackaton in Poland? Visuality Created it!

14
.
11
.
2023
Rafał Maliszewski
Ruby on Rails

Berlin StartupCamp 2016 summary

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

Investment Days for productivity

14
.
11
.
2023
Rafał Maliszewski
Visuality
HR

Happy new year

14
.
11
.
2023
Michał Piórkowski
Visuality

Does Norway need Polish software development?

14
.
11
.
2023
Rafał Maliszewski
Ruby on Rails

Visuality is 8 years old

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

Use less javascript plugins

14
.
11
.
2023
Michał Młoźniak
Frontend

Front-Trends 2015

14
.
11
.
2023
Adam Król
Frontend

Automatic door opener controlled through slack

14
.
11
.
2023
Sakir Temel
Backend
Software
Tutorial

Wolves Summit

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

Berlin Startup Camp

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

Why you shouldn't work at Visuality

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

SaaS Meetup #People

14
.
11
.
2023
Michał Piórkowski
Conferences

Startup Safary Berlin 2015

14
.
11
.
2023
Michał Piórkowski
Conferences

Emmet makes HTML and CSS easier

14
.
11
.
2023
Michał Piórkowski
Frontend
Tutorial

Optional dependencies in gems

14
.
11
.
2023
Karol Słuszniak
Ruby on Rails
Backend

Text messaging with textris gem

14
.
11
.
2023
Karol Słuszniak
Ruby on Rails
Backend