Skip to content

Instantly share code, notes, and snippets.

@itkrt2y
Forked from rmosolgo/page_example.rb
Last active April 12, 2023 16:23
Show Gist options
  • Save itkrt2y/8adb30c73fb269124427654dbba9f51e to your computer and use it in GitHub Desktop.
Save itkrt2y/8adb30c73fb269124427654dbba9f51e to your computer and use it in GitHub Desktop.
Generic page number / per-page pagination with GraphQL-Ruby
class Page
DEFAULT_PAGE_SIZE = 20
def initialize(all_nodes, page:, per_page:)
@all_nodes = all_nodes
# Normalize pagination arguments
@page = if page.nil? || page < 1
1
else
page
end
@per_page = if per_page.nil? || per_page < 1
DEFAULT_PAGE_SIZE
else
per_page
end
end
def page_info
{
current_page: @page,
has_previous_page: has_previous_page,
has_next_page: has_next_page,
nodes_count: nodes_count,
pages_count: pages_count,
per_page: @per_page,
}
end
# The items in this page
def nodes
@nodes ||= case @all_nodes
when ActiveRecord::Relation
offset = (@page - 1) * @per_page
@all_nodes.offset(offset).limit(@per_page)
when Array
offset = (@page - 1) * @per_page
@all_nodes[offset, @per_page] || [] # return empty if out-of-bounds
else
# TODO: implement other counts here
raise "`nodes_count` not implemented for #{@all_nodes.class} (#{@all_nodes.inspect})"
end
end
private
# True if there are items on the list after this page of items
def has_next_page
nodes_count > (@page * @per_page)
end
# True if there are items on the list before this page of items
def has_previous_page
@page > 1
end
# The total number of items in the list
def nodes_count
@nodes_count ||= case @all_nodes
when ActiveRecord::Relation
# Remove `ORDER BY` for better performance
@all_nodes.unscope(:order).count
when Array
@all_nodes.count
else
# TODO: implement other counts here
raise "`nodes_count` not implemented for #{@all_nodes.class} (#{@all_nodes.inspect})"
end
end
# The total number of pages for this page size
def pages_count
(nodes_count / @per_page.to_f).ceil
end
end
class Schema < GraphQL::Schema
class BaseField < GraphQL::Schema::Field
def initialize(**kwargs, &block)
# Do all the normal field setup:
super
# Add pagination args if this is a `Page` field
return_type = kwargs[:resolver_class]&.type&.then { |type| type.try(:of_type) || type }
if return_type.is_a?(Class) && return_type < BasePage
self.extension(PageWrapperExtension)
end
end
# Like the built-in ConnectionExtension, this adds arguments
# and automatic argument handling for page fields
# @see https://graphql-ruby.org/type_definitions/field_extensions.html
class PageWrapperExtension < GraphQL::Schema::FieldExtension
# Add the arguments to the field
def apply
field.argument(:page, Integer, required: false)
field.argument(:per_page, Integer, required: false)
end
def resolve(object:, arguments:, **rest)
# Remove pagination arguments
cleaned_arguments = arguments.dup
page = cleaned_arguments.delete(:page)
per_page = cleaned_arguments.delete(:per_page)
# Call the underlying resolver (without pagination args)
resolved_object = yield(object, cleaned_arguments)
# Then, apply the wrapper and return it
::Page.new(resolved_object, page: page, per_page: per_page)
end
end
end
class BaseObject < GraphQL::Schema::Object
field_class BaseField
# Generate a page type for this object,
# or use an already-cached one.
def self.page_type
@page_type ||= BasePage.create(self)
end
end
# A generic page type
class BasePage < BaseObject
def self.create(node_class)
Class.new(self) do
# Override the name so it reflects the node class
graphql_name("#{node_class.graphql_name}Page")
# Add the nodes field which reflects the node class
field :nodes, [node_class], null: false
end
end
field :page_info, Types::PageInfoType, null: false
end
end
@verenion
Copy link

We have recently upgraded to graphql-ruby 2.x and happened to be using the same code from the forked gist. We ran into this problem and this code saved us many more hours of hunting! So thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment