Skip to content

Commit 6c45cd0

Browse files
committed
Implements relative cursor pagination.
1 parent e5d7e01 commit 6c45cd0

File tree

5 files changed

+240
-0
lines changed

5 files changed

+240
-0
lines changed

lib/active_resource/collection_ext.rb

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require 'shopify_api/collection_pagination'
2+
3+
module ActiveResource
4+
class Collection
5+
prepend ShopifyAPI::CollectionPagination
6+
end
7+
end

lib/shopify_api.rb

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'shopify_api/defined_versions'
1010
require 'shopify_api/api_version'
1111
require 'active_resource/json_errors'
12+
require 'active_resource/collection_ext'
1213
require 'shopify_api/disable_prefix_check'
1314

1415
module ShopifyAPI
@@ -21,6 +22,7 @@ module ShopifyAPI
2122
require 'shopify_api/resources'
2223
require 'shopify_api/session'
2324
require 'shopify_api/connection'
25+
require 'shopify_api/pagination_link_headers'
2426

2527
if ShopifyAPI::Base.respond_to?(:connection_class)
2628
ShopifyAPI::Base.connection_class = ShopifyAPI::Connection
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module ShopifyAPI
2+
module CollectionPagination
3+
4+
def next_page?
5+
next_page_info.present?
6+
end
7+
8+
def previous_page?
9+
previous_page_info.present?
10+
end
11+
12+
def fetch_next_page
13+
fetch_page(next_page_info)
14+
end
15+
16+
def fetch_previous_page
17+
fetch_page(previous_page_info)
18+
end
19+
20+
private
21+
22+
AVAILABLE_IN_VERSION = ShopifyAPI::ApiVersion::Unstable.new
23+
24+
def fetch_page(page_info)
25+
return [] unless page_info
26+
27+
resource_class.where(original_params.merge(page_info: page_info))
28+
end
29+
30+
def previous_page_info
31+
@previous_page_info ||= extract_page_info(pagination_link_headers.previous_link)
32+
end
33+
34+
def next_page_info
35+
@next_page_info ||= extract_page_info(pagination_link_headers.next_link)
36+
end
37+
38+
def extract_page_info(link_header)
39+
raise NotImplementedError unless ShopifyAPI::Base.api_version >= AVAILABLE_IN_VERSION
40+
41+
return nil unless link_header.present?
42+
43+
CGI.parse(link_header.url.query)["page_info"][0]
44+
end
45+
46+
def pagination_link_headers
47+
@pagination_link_headers ||= ShopifyAPI::PaginationLinkHeaders.new(
48+
ShopifyAPI::Base.connection.response["Link"]
49+
)
50+
end
51+
end
52+
end
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
module ShopifyAPI
2+
class InvalidPaginationLinksError < StandardError; end
3+
4+
class PaginationLinkHeaders
5+
LinkHeader = Struct.new(:url, :rel)
6+
attr_reader :previous_link, :next_link
7+
8+
def initialize(link_header)
9+
links = parse_link_header(link_header)
10+
@previous_link = links.find { |link| link.rel == :previous }
11+
@next_link = links.find { |link| link.rel == :next }
12+
13+
self
14+
end
15+
16+
private
17+
18+
def parse_link_header(link_header)
19+
return [] unless link_header.present?
20+
links = link_header.split(',')
21+
links.map do |link|
22+
parts = link.split('; ')
23+
raise ShopifyAPI::InvalidPaginationLinksError.new("Invalid link header: url and rel expected") unless parts.length == 2
24+
25+
url = parts[0][/<(.*)>/, 1]
26+
rel = parts[1][/rel="(.*)"/, 1]&.to_sym
27+
28+
url = URI.parse(url)
29+
LinkHeader.new(url, rel)
30+
end
31+
end
32+
end
33+
end

test/pagination_test.rb

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
require 'test_helper'
2+
3+
class PaginationTest < Test::Unit::TestCase
4+
def setup
5+
super
6+
7+
@version = ShopifyAPI::ApiVersion::Unstable.new
8+
ShopifyAPI::Base.api_version = @version.to_s
9+
@next_page_info = "eyJkaXJlY3Rpb24iOiJuZXh0IiwibGFzdF9pZCI6NDQwMDg5NDIzLCJsYXN0X3ZhbHVlIjoiNDQwMDg5NDIzIn0%3D"
10+
@previous_page_info = "eyJsYXN0X2lkIjoxMDg4MjgzMDksImxhc3RfdmFsdWUiOiIxMDg4MjgzMDkiLCJkaXJlY3Rpb24iOiJuZXh0In0%3D"
11+
12+
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>; rel=\"next\""
13+
@previous_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}>; rel=\"previous\""
14+
end
15+
16+
test "navigates using next and previous link headers" do
17+
link_header =
18+
"<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}>; rel=\"previous\",\
19+
<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>; rel=\"next\""
20+
21+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => link_header
22+
orders = ShopifyAPI::Order.all
23+
24+
fake(
25+
'orders',
26+
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}",
27+
method: :get,
28+
status: 200,
29+
body: load_fixture('orders')
30+
)
31+
32+
next_page = orders.fetch_next_page
33+
assert_equal 450789469, next_page.first.id
34+
35+
fake(
36+
'orders',
37+
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}",
38+
method: :get,
39+
status: 200,
40+
body: load_fixture('orders')
41+
)
42+
43+
previous_page = orders.fetch_previous_page
44+
assert_equal 450789469, next_page.first.id
45+
end
46+
47+
test "retains previous querystring parameters" do
48+
fake(
49+
'orders',
50+
method: :get,
51+
status: 200,
52+
api_version: @version,
53+
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?fields=id%2Cupdated_at",
54+
body: load_fixture('orders'),
55+
link: @next_link_header
56+
)
57+
orders = ShopifyAPI::Order.where(fields: 'id,updated_at')
58+
59+
fake(
60+
'orders',
61+
method: :get,
62+
status: 200,
63+
api_version: @version,
64+
url: "https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?fields=id%2Cupdated_at&page_info=#{@next_page_info}",
65+
body: load_fixture('orders')
66+
)
67+
next_page = orders.fetch_next_page
68+
assert_equal 450789469, next_page.first.id
69+
end
70+
71+
test "returns empty next page if just the previous page is present" do
72+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @previous_link_header
73+
orders = ShopifyAPI::Order.all
74+
75+
next_page = orders.fetch_next_page
76+
assert_empty next_page
77+
end
78+
79+
test "returns an empty previous page if just the next page is present" do
80+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @next_link_header
81+
orders = ShopifyAPI::Order.all
82+
83+
next_page = orders.fetch_previous_page
84+
assert_empty next_page
85+
end
86+
87+
test "#next_page? returns true if next page is present" do
88+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @next_link_header
89+
orders = ShopifyAPI::Order.all
90+
91+
assert orders.next_page?
92+
end
93+
94+
test "#next_page? returns false if next page is not present" do
95+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @previous_link_header
96+
orders = ShopifyAPI::Order.all
97+
98+
refute orders.next_page?
99+
end
100+
101+
test "#previous_page? returns true if previous page is present" do
102+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @previous_link_header
103+
orders = ShopifyAPI::Order.all
104+
105+
assert orders.previous_page?
106+
end
107+
108+
test "#previous_page? returns false if next page is not present" do
109+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => @next_link_header
110+
orders = ShopifyAPI::Order.all
111+
112+
refute orders.previous_page?
113+
end
114+
115+
test "pagination handles no link headers" do
116+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders')
117+
orders = ShopifyAPI::Order.all
118+
119+
refute orders.next_page?
120+
refute orders.previous_page?
121+
assert_empty orders.fetch_next_page
122+
assert_empty orders.fetch_previous_page
123+
end
124+
125+
test "raises on invalid pagination links" do
126+
link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>;"
127+
fake 'orders', :method => :get, :status => 200, api_version: @version, :body => load_fixture('orders'), :link => link_header
128+
orders = ShopifyAPI::Order.all
129+
130+
assert_raises ShopifyAPI::InvalidPaginationLinksError do
131+
orders.fetch_next_page
132+
end
133+
end
134+
135+
test "raises on an invalid API version" do
136+
version = ShopifyAPI::ApiVersion::Release.new('2019-04')
137+
ShopifyAPI::Base.api_version = version.to_s
138+
139+
fake 'orders', :method => :get, :status => 200, api_version: version, :body => load_fixture('orders')
140+
orders = ShopifyAPI::Order.all
141+
142+
assert_raises NotImplementedError do
143+
orders.fetch_next_page
144+
end
145+
end
146+
end

0 commit comments

Comments
 (0)