-
Notifications
You must be signed in to change notification settings - Fork 474
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implements relative cursor pagination.
- Loading branch information
Showing
5 changed files
with
240 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
require 'shopify_api/collection_pagination' | ||
|
||
module ActiveResource | ||
class Collection | ||
prepend ShopifyAPI::CollectionPagination | ||
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
module ShopifyAPI | ||
module CollectionPagination | ||
|
||
def next_page? | ||
next_page_info.present? | ||
end | ||
|
||
def previous_page? | ||
previous_page_info.present? | ||
end | ||
|
||
def fetch_next_page | ||
fetch_page(next_page_info) | ||
end | ||
|
||
def fetch_previous_page | ||
fetch_page(previous_page_info) | ||
end | ||
|
||
private | ||
|
||
AVAILABLE_IN_VERSION = ShopifyAPI::ApiVersion::Unstable.new | ||
|
||
def fetch_page(page_info) | ||
return [] unless page_info | ||
|
||
resource_class.where(original_params.merge(page_info: next_page_info)) | ||
end | ||
|
||
def previous_page_info | ||
@previous_page_info ||= extract_page_info(pagination_link_headers.previous_link) | ||
end | ||
|
||
def next_page_info | ||
@next_page_info ||= extract_page_info(pagination_link_headers.next_link) | ||
end | ||
|
||
def extract_page_info(link_header) | ||
raise NotImplementedError unless ShopifyAPI::Base.api_version >= AVAILABLE_IN_VERSION | ||
|
||
return nil unless link_header.present? | ||
|
||
CGI.parse(link_header.url.query)["page_info"][0] | ||
end | ||
|
||
def pagination_link_headers | ||
@pagination_link_headers ||= ShopifyAPI::PaginationLinkHeaders.new( | ||
ShopifyAPI::Base.connection.response["Link"] | ||
) | ||
end | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
module ShopifyAPI | ||
class InvalidPaginationLinksError < StandardError; end | ||
|
||
class PaginationLinkHeaders | ||
LinkHeader = Struct.new(:url, :rel) | ||
attr_reader :previous_link, :next_link | ||
|
||
def initialize(link_header) | ||
links = parse_link_header(link_header) | ||
@previous_link = links.find { |link| link.rel == :previous } | ||
@next_link = links.find { |link| link.rel == :next } | ||
|
||
self | ||
end | ||
|
||
private | ||
|
||
def parse_link_header(link_header) | ||
return [] unless link_header.present? | ||
links = link_header.split(',') | ||
links.map do |link| | ||
parts = link.split('; ') | ||
raise ShopifyAPI::InvalidPaginationLinksError.new("Invalid link header: url and rel expected") unless parts.length == 2 | ||
|
||
url = parts[0][/<(.*)>/, 1] | ||
rel = parts[1][/rel="(.*)"/, 1]&.to_sym | ||
|
||
url = URI.parse(url) | ||
LinkHeader.new(url, rel) | ||
end | ||
end | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
require 'test_helper' | ||
|
||
class PaginationTest < Test::Unit::TestCase | ||
def setup | ||
super | ||
|
||
@version = ShopifyAPI::ApiVersion::Unstable.new | ||
ShopifyAPI::Base.api_version = @version.to_s | ||
@next_page_info = "eyJkaXJlY3Rpb24iOiJuZXh0IiwibGFzdF9pZCI6NDQwMDg5NDIzLCJsYXN0X3ZhbHVlIjoiNDQwMDg5NDIzIn0%3D" | ||
@previous_page_info = "eyJsYXN0X2lkIjoxMDg4MjgzMDksImxhc3RfdmFsdWUiOiIxMDg4MjgzMDkiLCJkaXJlY3Rpb24iOiJuZXh0In0%3D" | ||
|
||
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>; rel=\"next\"" | ||
@previous_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}>; rel=\"previous\"" | ||
end | ||
|
||
test "navigates using next and previous link headers" do | ||
link_header = | ||
"<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}>; rel=\"previous\",\ | ||
<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>; rel=\"next\"" | ||
|
||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
fake( | ||
'orders', | ||
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}", | ||
method: :get, | ||
status: 200, | ||
body: load_fixture('orders') | ||
) | ||
|
||
next_page = orders.fetch_next_page | ||
assert_equal 450789469, next_page.first.id | ||
|
||
fake( | ||
'orders', | ||
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}", | ||
method: :get, | ||
status: 200, | ||
body: load_fixture('orders') | ||
) | ||
|
||
previous_page = orders.fetch_previous_page | ||
assert_equal 450789469, next_page.first.id | ||
end | ||
|
||
test "retains previous querystring parameters" do | ||
fake( | ||
'orders', | ||
method: :get, | ||
status: 200, | ||
api_version: @version, | ||
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?fields=id%2Cupdated_at", | ||
body: load_fixture('orders'), | ||
link: @next_link_header | ||
) | ||
orders = ShopifyAPI::Order.where(fields: 'id,updated_at') | ||
|
||
fake( | ||
'orders', | ||
method: :get, | ||
status: 200, | ||
api_version: @version, | ||
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?fields=id%2Cupdated_at&page_info=eyJkaXJlY3Rpb24iOiJuZXh0IiwibGFzdF9pZCI6NDQwMDg5NDIzLCJsYXN0X3ZhbHVlIjoiNDQwMDg5NDIzIn0%3D", | ||
body: load_fixture('orders') | ||
) | ||
next_page = orders.fetch_next_page | ||
assert_equal 450789469, next_page.first.id | ||
end | ||
|
||
test "returns empty next page if just the previous page is present" do | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @previous_link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
next_page = orders.fetch_next_page | ||
assert_empty next_page | ||
end | ||
|
||
test "returns an empty previous page if just the next page is present" do | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @next_link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
next_page = orders.fetch_previous_page | ||
assert_empty next_page | ||
end | ||
|
||
test "#next_page? returns true if next page is present" do | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @next_link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
assert orders.next_page? | ||
end | ||
|
||
test "#next_page? returns false if next page is not present" do | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @previous_link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
refute orders.next_page? | ||
end | ||
|
||
test "#previous_page? returns true if previous page is present" do | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @previous_link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
assert orders.previous_page? | ||
end | ||
|
||
test "#previous_page? returns false if next page is not present" do | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @next_link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
refute orders.previous_page? | ||
end | ||
|
||
test "pagination handles no link headers" do | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders') | ||
orders = ShopifyAPI::Order.all | ||
|
||
refute orders.next_page? | ||
refute orders.previous_page? | ||
assert_empty orders.fetch_next_page | ||
assert_empty orders.fetch_previous_page | ||
end | ||
|
||
test "raises on invalid pagination links" do | ||
link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>;" | ||
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => link_header | ||
orders = ShopifyAPI::Order.all | ||
|
||
assert_raises ShopifyAPI::InvalidPaginationLinksError do | ||
orders.fetch_next_page | ||
end | ||
end | ||
|
||
test "raises on an invalid API version" do | ||
version = ShopifyAPI::ApiVersion::Release.new('2019-04') | ||
ShopifyAPI::Base.api_version = version.to_s | ||
|
||
fake 'orders', :method => :get, :status => 200, api_version: version, :body => load_fixture('orders') | ||
orders = ShopifyAPI::Order.all | ||
|
||
assert_raises NotImplementedError do | ||
orders.fetch_next_page | ||
end | ||
end | ||
end |