-
-
Notifications
You must be signed in to change notification settings - Fork 107
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
Foolproof Relay Pagination API #113
base: master
Are you sure you want to change the base?
Conversation
@tmattio, thank you so much for contributing to the conversation! It's great to have insights from you, especially since you're using Relay pagination already! And all the better that you have experience with paginating using other GraphQL clients 😄
If I'm understanding your question correctly, you're asking about the standard use case where you need to take, for example, the 1st page of results, and then combine that with the 2nd page of results after getting that response. Correct me if I misunderstood the question! The way this works is like so:
I hope that answers the question! I think it's a nice design because it's impossible to forget to add it back. The updated
I'm still trying to figure out the most elegant way to do this, but what I have in mind is that I will strip off everything related to low-level pagination, and then keep whatever is left. For example, let's take this definition: type StargazerConnection {
# these are all for pagination
edges: [StargazerEdge]
nodes: [User]
pageInfo: PageInfo!
# this is meta-data about the Connection itself
# it is not needed for pagination, but the user might want to fetch it
totalCount: Int!
}
type StargazerEdge {
node: User!
starredAt: DateTime!
# this is to help with pagination, but it's not strictly required
# the generated code will rely on PageInfo instead
cursor: String!
} Given these types in the schema, the generated code would effectively give you access to data for this schema (the pagination data would not be directly accessible, it would be used by the internals for the high-level pagination API): type StargazerConnection {
totalCount: Int!
}
type StargazerEdge {
# the user creates a SelectionSet for StargazerEdge and
# passes it in to the generated pagination helper function
node: User!
starredAt: DateTime!
} Note that the
Absolutely! See the code snippet in my comment above, it's the (Repository.stargazers
50 -- pageSize - can be hardcoded, or vary over time
paginator
identity -- these are any additional optional arguments passed in
stargazerSelection -- here you define how to get a batch of items
)
Indeed, I've considered this use case as well and it will work out quite nicely with this design!
I have been thinking about that. I'm not sure what that would look like, or if it would be a good idea for the Thanks again for sharing your thoughts, I really appreciate it! |
Great initial work on the concept. I'm a bit confused about the direction though. When a paginator with a number of pages like 1..2....3...4...5 is used would we end up constantly creating and destroying paginators depending on which page users try to go to? |
Hey @Maxim-Filimonov, thanks for the comment. It is an interesting use case, and it's a perfectly reasonable way to do pagination. But it's not supported by the Relay Connections Protocol. It's inherent to the cursor-based pagination that you can't jump to a specific page since cursors are opaque. So there's no way to say "give me page 5". If you wanted to go backwards or forwards and create pages, that's really a client-side concern at the point. You just need to be sure to load enough data to present the page you want to show. I hope that clarifies things! |
Hi @dillonkearns! Thanks for the detailed answer 😄
Yes! This is perfect, the views doesn't have to know the data is paginated, love it. For the other items, it sounds great also, thanks again for taking the time to write back. |
@tmattio fantastic, I'm glad you like the design! Thank you for your input! |
@dillonkearns thank you for this! it looks pretty good. Do you think is going to be merged into master in the near future? |
@AdrianRibao this is still a ways off, there are a few cases I still need to consider and haven't quite figured out the right approach for. I'll keep this thread posted when I pick this back up, but for now this will still require some more brainstorming to push through. |
Adding Relay Pagination to
elm-graphql
In-Progress Design So Far
Here's a summary of how you do pagination using the design I have in this pull request so far. Note that this would be the design available in your generated code if your schema uses the Official Relay Modern Connections Spec to do pagination.
Note: Google gives the Relay Legacy Spec as the first search result, be sure to look at the Relay Modern Spec instead.
Paginator
.This type represents the following things (none of which are changed manually)
Paginator
withbackward
).Repository.stargazers
here follows the Relay Connections Spec, so we pass in apageSize
and aPaginator
type as we see below. Note that you pass in aSelectionSet StarGazer Github.Object.StargazerEdge
. Why StarGazerEdge, not a Node? Two reasons. 1. the edge can contain information not just about a node, but about its relationship through that connection, and 2. The Relay Connections Specification doesn't actually mention anodes { ... }
field, onlyedges { node { ... } }
.Guiding Goals
Make it impossible to do the wrong thing
Make it easy to do the right thing - ideally, we can give just one nice API that disallows using the low-level pagination directly, and instead gives you a high-level interface. This will be less confusing and less error-prone. But having the low-level available could potentially be an escape hatch, so it might be an option to consider.
Make Impossible States Impossible - it would be awesome if it was impossible to make invalid pagination requests. It's common to get runtime exceptions with APIs if you 1) forget to pass in pagination arguments (but perhaps some APIs allow you to paginate or not... Github for example will fail if you're not explicit), 2) pass in both Forward and Backward pagination arguments (though it's not technically illegal in the Relay spec, it's strongly discouraged... let's just assume it's illegal).
Let's also eliminate things like 3) changing directions midway through paginating. And 4) let's make it impossible to update just the new data, but not update the cursor. You update them all atomically. This can be accomplished by passing in a data structure that includes the list of data so far, plus the pagination data. The selection set will add on to that data structure and update the pagination data with its response.
References
Open questions
Paginator
argument be aMaybe
to support this case? Or would some other design allow for it? Or would that cause some unwanted corner cases and more confusing API? TODO look for examples.Notes
hasNextPage
false
, and then try it again later to check if any new data has been added since. To check if that's changed, you can simply make another request afterPaginator.hasMoreData
returnsFalse
and see whether you get more data, and whetherPaginator.hasMoreData
changes toTrue
.first
or only alast
, but not both, we'll use a phantom type to constrain what type of paginator can be passed in (e.g. it will accept onlyPaginator Forward MyDataType
).first
andlast
, but it is highly inadvisable. See this note in the official spec. Let's just disallow it completely.Paginator
type in a way that models the users specific retry logic for their domain. Relay client has retry config built-in. But it's easy to build a wrapper that is custom tailored for your needs, and this will make the core API less bloated and more flexible.Possible strategies
Allow CLI Flag to Skip Relay Generation
Pros
Cons
--fail-on-relay-schema-violation
then it will exit the CLI if there is a violation with non-zero exit status.Allow both low-level access and high-level access.
Pros
Cons