Skip to content

[bestpractices] Add friends help and cursors to page info #74

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

Merged
merged 1 commit into from
Sep 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions site/_core/swapiSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ type FriendsConnection {
# The edges for each of the character's friends.
edges: [FriendsEdge]

# A list of the friends, as a convenience when edges are not needed.
friends: [Character]

# Information for paginating this connection
pageInfo: PageInfo!
}
Expand All @@ -150,6 +153,8 @@ type FriendsEdge {

# Information for paginating this connection
type PageInfo {
startCursor: ID
endCursor: ID
hasNextPage: Boolean!
}

Expand Down Expand Up @@ -394,13 +399,20 @@ const resolvers = {
friends: ({ friends }) => friends.map(getCharacter),
friendsConnection: ({ friends }, { first, after }) => {
first = first || friends.length;
after = parseInt(fromCursor(after), 10) || 0;
after = after ? parseInt(fromCursor(after), 10) : 0;
const edges = friends.map((friend, i) => ({
cursor: toCursor(i+1),
node: getCharacter(friend)
})).slice(after, first + after);
const slicedFriends = edges.map(({ node }) => node);
return {
edges: friends.map((friend, i) => ({
cursor: toCursor(i+1),
node: getCharacter(friend)
})).slice(after, first + after),
pageInfo: { hasNextPage: first + after < friends.length },
edges,
friends: slicedFriends,
pageInfo: {
startCursor: edges.length > 0 ? edges[0].cursor : null,
hasNextPage: first + after < friends.length,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
},
totalCount: friends.length
};
},
Expand All @@ -411,20 +423,28 @@ const resolvers = {
friends: ({ friends }) => friends.map(getCharacter),
friendsConnection: ({ friends }, { first, after }) => {
first = first || friends.length;
after = parseInt(fromCursor(after), 10) || 0;
after = after ? parseInt(fromCursor(after), 10) : 0;
const edges = friends.map((friend, i) => ({
cursor: toCursor(i+1),
node: getCharacter(friend)
})).slice(after, first + after);
const slicedFriends = edges.map(({ node }) => node);
return {
edges: friends.map((friend, i) => ({
cursor: toCursor(i+1),
node: getCharacter(friend)
})).slice(after, first + after),
pageInfo: { hasNextPage: first + after < friends.length },
edges,
friends: slicedFriends,
pageInfo: {
startCursor: edges.length > 0 ? edges[0].cursor : null,
hasNextPage: first + after < friends.length,
endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null
},
totalCount: friends.length
};
},
appearsIn: ({ appearsIn }) => appearsIn,
},
FriendsConnection: {
edges: ({ edges }) => edges,
friends: ({ friends }) => friends,
pageInfo: ({ pageInfo }) => pageInfo,
totalCount: ({ totalCount }) => totalCount,
},
Expand Down
8 changes: 6 additions & 2 deletions site/learn/BestPractice-Pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ There are a number of ways we could do pagination:

In general, we've found that **cursor-based pagination** is the most powerful of those designed. Especially if the cursors are opaque, either offset or ID-based pagination can be implemented using cursor-based pagination (by making the cursor the offset or the ID), and using cursors gives additional flexibility if the pagination model changes in the future. As a reminder that the cursors are opaque and that their format should not be relied upon, we suggest base64 encoding them.

That leads us to a problem; though; how do we get the cursor from the object? We wouldn't want cursor to live on the `User` type; it's a property of the connection, not of the object. So we want to introduce a new layer of indirection; our `friends` field should give us a list of edges, and an edge has both a cursor and the underlying node:
That leads us to a problem; though; how do we get the cursor from the object? We wouldn't want cursor to live on the `User` type; it's a property of the connection, not of the object. So we might want to introduce a new layer of indirection; our `friends` field should give us a list of edges, and an edge has both a cursor and the underlying node:

```graphql
{
Expand Down Expand Up @@ -92,13 +92,16 @@ To solve both of these problems, our `friends` field can return a connection obj
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
```

Note that we also might include `endCursor` and `startCursor` in this `PageInfo` object. This way, if we don't need any of the additional information that the edge contains, we don't need to query for the edges at all, since we got the cursors needed for pagination from `pageInfo`. This leads to a potential usability improvement for connections; instead of just exposing the `edges` list, we could also expose a dedicated list of just the nodes, to avoid a layer of indirection.

## Complete Connection Model

Clearly, this is more complex than our original design of just having a plural! But by adopting this design, we've unlocked a number of capabilities for the client:
Expand All @@ -108,7 +111,7 @@ Clearly, this is more complex than our original design of just having a plural!
- The ability to ask for information about the edge itself, like `cursor` or `friendshipTime`.
- The ability to change how our backend does pagination, since the user just uses opaque cursors.

To see this in action, there's an additional field in the example schema, called `friendsConnection`, that exposes all of these concepts. You can check it out in the example query. Try removing the `after` parameter to `friendsConnection` to see how the pagination will be affected.
To see this in action, there's an additional field in the example schema, called `friendsConnection`, that exposes all of these concepts. You can check it out in the example query. Try removing the `after` parameter to `friendsConnection` to see how the pagination will be affected. Also, try replacing the `edges` field with the helper `friends` field on the connection, which lets you get directly to the list of friends without the additional edge layer of indirection, when that's appropriate for clients.

```graphql
# { "graphiql": true }
Expand All @@ -124,6 +127,7 @@ To see this in action, there's an additional field in the example schema, called
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
Expand Down