Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements relative cursor pagination. #594

Merged
merged 1 commit into from Aug 8, 2019

Conversation

garethson
Copy link
Contributor

@garethson garethson commented Jul 4, 2019

Implements a convenient API for using our new relative cursor pagination, which has been partially released as of API version 2019-07.

Rather than providing an automatic way to iterate through all records in the query, we're adding the following methods to any model collection:

  • next_page?
  • previous_page?
  • fetch_next_page
  • fetch_previous_page

This allows you to perform the navigation however you see fit. Here is an example of using the API to iterate and process a list of all pending orders:

  orders = ShopifyAPI::Order.find(:all, params: { financial_status: 'pending' })
  process_orders(orders)
  while orders.next_page?
    orders = orders.fetch_next_page
    process_orders(orders)
  end

Currently making all endpoints available on the unstable API, and we can merge this when all endpoints have relative cursor pagination available in Shopify.

Tested:

products = ShopifyAPI::Product.where(limit: 3)
=> [1933186826296, 1933188005944, 1933188563000]

products.previous_page?
=> false

products.next_page?
=> true

products = products.fetch_next_page
=> [1933189120056, 1933200949304, 1970112430136]

products.previous_page?
=> true

products.next_page?
=> true

products = products.fetch_previous_page
=> [1933186826296, 1933188005944, 1933188563000]

@garethson garethson requested a review from a team as a code owner July 4, 2019 17:50
@garethson garethson force-pushed the relative-cursor-pagination branch 3 times, most recently from 950c67f to 9d6464b Compare July 4, 2019 18:16
def fetch_page(page_info)
return [] unless page_info

resource_class.where(original_params.merge(page_info: next_page_info))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do where(page_info: next_page_info)but we're supporting old versions of ActiveResource, and where was added only in later versions.


def fetch_next_page
fetch_page(next_page_info)
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about it, but decided to not memoize the next/previous page. If you're calling fetch, just have it fetch.

@garethson garethson force-pushed the relative-cursor-pagination branch 2 times, most recently from 20af685 to 6c45cd0 Compare July 5, 2019 14:39
@garethson garethson changed the title WIP - Implements relative cursor pagination. Implements relative cursor pagination. Jul 5, 2019
Copy link
Member

@kmcphillips kmcphillips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach is good! A few small questions. Thanks for the great tests.


return nil unless link_header.present?

CGI.parse(link_header.url.query)["page_info"][0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good place to dig here, as this will raises if page_info key is missing or not an array. Do we support ruby below 2.3 on this gem?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s.required_ruby_version = ">= 2.4"


def pagination_link_headers
@pagination_link_headers ||= ShopifyAPI::PaginationLinkHeaders.new(
ShopifyAPI::Base.connection.response["Link"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I understand why we get a response off the base connection here.

Does that mean this gem is not threadsafe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActiveResource appears to be threadsafe, and sets the connection class to be threadsafe.

We have a few places where we reference the base connection for response attributes. Here and here.

This was added in AR 4.1, and we only support >= 4.1

Of note, is that headers is explicitly handled as threadsafe in this gem, but that was done before the threadsafe stuff was added. I believe this can be removed, which I'll look into after this.

lib/shopify_api/pagination_link_headers.rb Show resolved Hide resolved
lib/shopify_api/pagination_link_headers.rb Show resolved Hide resolved
Copy link
Member

@kmcphillips kmcphillips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All makes sense to me. Thanks for doing this.

@DrewMartin
Copy link
Contributor

Should we update the readme?

def fetch_page(page_info)
return [] unless page_info

resource_class.where(original_params.merge(page_info: page_info))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If your first request had the param order: 'title asc' then this would merge page_info to that? Because that would make a bad URL. If we tried to request a page against Shopify with the original params + page_info it will fail if there are filters or sort orders in there.

We're probably better off just parsing out all the params out of the next/previous url and using them directly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline, I'm going to take that approach, parse the next/previous page and use those for params.

@@ -9,14 +9,12 @@ def setup
@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\""
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>; rel=\"next\">"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is right. The chevrons enclose the URL, not the whole tuple

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>; rel=\"next\">"
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@next_page_info}>; rel=\"next\""

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DrewMartin Fixed up for both.

@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\">"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@previous_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}>; rel=\"previous\">"
@previous_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?page_info=#{@previous_page_info}>; rel=\"previous\""


test "uses all passed in querystring parameters" do
params = "page_info=#{@next_page_info}&limit=50&fields=#{CGI.escape('id,created_at')}"
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?#{params}>; rel=\"next\">"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?#{params}>; rel=\"next\">"
@next_link_header = "<https://this-is-my-test-shop.myshopify.com/admin/api/unstable/orders.json?#{params}>; rel=\"next\""

@garethson garethson force-pushed the relative-cursor-pagination branch 2 times, most recently from 65ab254 to 42770b8 Compare July 17, 2019 16:54
@garethson
Copy link
Contributor Author

I've updated this to, instead of using original_params, simply passing in all the params returned via the next/previous link headers.

Tested this here, with products with order, as well as products with fields,limit,order.

Tested with order (which is not yet converted).

Finally: Tested with events with filter,limit,order.

README.md Outdated Show resolved Hide resolved
Copy link
Member

@ignacio-chiazzo ignacio-chiazzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM, just two questions

url = parts[0][/<(.*)>/, 1]
rel = parts[1][/rel="(.*)"/, 1]&.to_sym

url = URI.parse(url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add validations for url + rel?

class Collection
prepend ShopifyAPI::CollectionPagination
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed IRL do we have to solve the problem of querying the next/prev link after doing another query?, e.g.

page1 = Produdcts.find(:all)
page2 = Produdcts.find(:all)
page2.fetch_next_page
page1.next_page
page1.previous_page

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to parse the headers on collection initialization, vs lazily evaluating them. Also added a test for that case. The collection objects don't have access to the actual response, so we have to rely on ShopifyAPI::Base.connection.response["Link"], which is always for the last made request.

@garethson garethson force-pushed the relative-cursor-pagination branch 3 times, most recently from 7e411e9 to dc1d360 Compare July 17, 2019 18:53
Copy link
Contributor

@tanema tanema left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, do you have plans for disabling page based pagination in the future like raising an error if they pass in a page param? I just worry that there are old tutorials out there that will be used and the debelopers will be confused why they are not getting more items (because the page param is ignored)

@garethson
Copy link
Contributor Author

@tanema Thanks. Regarding disabling the page param, the API currently raises a fairly descriptive error with a link to the docs if a page param is passed in for a version of the API that isn't supported.

Having a client side error raise would be helpful and would probably catch errors earlier for developers, rather than seeing it in production. I can look into adding this after this is merged.

@dominiquesr
Copy link
Contributor

❤️ It's happening! This is going to have a huge impact on adoption!

@isaacbowen
Copy link

🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants