-
-
Save itkrt2y/8adb30c73fb269124427654dbba9f51e to your computer and use it in GitHub Desktop.
Generic page number / per-page pagination with GraphQL-Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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!