Skip to content

Instantly share code, notes, and snippets.

@itkrt2y
Forked from rmosolgo/page_example.rb
Last active August 1, 2022 02:39
Embed
What would you like to do?
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment