Skip to content

Instantly share code, notes, and snippets.

@goromlagche
Last active December 8, 2023 13:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save goromlagche/d2075bc8ad8fedd3a9930cd9f3c38ae4 to your computer and use it in GitHub Desktop.
Save goromlagche/d2075bc8ad8fedd3a9930cd9f3c38ae4 to your computer and use it in GitHub Desktop.
Cursor pagination
# frozen_string_literal: true
# drop this file in app/lib/pagination.rb
module Pagination
DEFAULT_PAGE_LIMIT = 10
def pages(records:, url:)
paginate = Paginate.new(records: records,
url: url,
limit: limit,
params: pagination_params)
paginate.pages
end
def limit
return self.class::PAGE_LIMIT if defined? self.class::PAGE_LIMIT
DEFAULT_PAGE_LIMIT
end
def pagination_params
params.permit(:cursor_created_at, :direction)
end
class Paginate
def initialize(records:, url:, limit:, params:)
@records = records
@url = url
@limit = limit
@params = params
end
def pages
{
records: paginated_records,
next_url: pagination_url(direction: "next"),
prev_url: pagination_url(direction: "prev"),
}
end
private
def pagination_window
return if @params[:cursor_created_at].blank?
if @params[:direction] == "prev"
@records.arel_table[:created_at].gt(@params[:cursor_created_at])
else
@records.arel_table[:created_at].lt(@params[:cursor_created_at])
end
end
def pagination_url(direction:)
return "" if @seek == false && direction == @params[:direction]
return "" if @params[:cursor_created_at].blank? && direction == "prev" # first page
cursor = direction == "prev" ? paginated_records.first : paginated_records.last
uri = URI(@url)
params = Hash[URI.decode_www_form(uri.query || "")]
.merge("cursor_created_at" => cursor.created_at.iso8601(6), # PG default is 6
"direction" => direction)
uri.query = URI.encode_www_form(params)
uri
end
def paginated_records
return @paginated_records if defined? @paginated_records
@paginated_records = @records
.unscope(:order)
.where(pagination_window)
.order(created_at: order_direction)
.limit(@limit + 1)
.to_a
@seek = (@paginated_records.size > @limit)
@paginated_records.pop if @seek == true
@paginated_records.reverse! if @params[:direction] == "prev"
@paginated_records
end
def order_direction
return :asc if @params[:direction] == "prev"
:desc
end
end
end
class Records extends React.Component {
static propTypes = {
recordsUrl: PropTypes.string
};
componentDidMount() {
this.fetchRecords(this.props.recordsUrl);
}
state = {
records: [],
prevUrl: "",
nextUrl: ""
};
fetchRecords = url => {
fetch(url)
.then(data => {
if (data.ok) {
return data.json();
}
throw new Error("Network error!");
})
.then(data => {
this.setState({
records: data.records,
nextUrl: data.next_url,
prevUrl: data.prev_url,
});
})
.catch(err => console.log("Error: " + err));
};
...
...
...
module Api
module V1
class RecordsController < BaseApiController
include Pagination
def index
render json: pages(records: records, url: api_v1_records_path),
except: [:updated_at],
status: :ok
end
private
def records
Records.with_attached_image
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment