Skip to content

Commit e42a3ee

Browse files
dschaferleebyron
authored andcommitted
[bestpractices] Add connections best practice article (#69)
1 parent 4811f43 commit e42a3ee

File tree

3 files changed

+209
-0
lines changed

3 files changed

+209
-0
lines changed

site/_core/swapiSchema.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ interface Character {
6363
# The friends of the character, or an empty list if they have none
6464
friends: [Character]
6565
66+
# The friends of the character exposed as a connection with edges
67+
friendsConnection(first: Int, after: ID): FriendsConnection!
68+
6669
# The movies this character appears in
6770
appearsIn: [Episode]!
6871
}
@@ -93,6 +96,9 @@ type Human implements Character {
9396
# This human's friends, or an empty list if they have none
9497
friends: [Character]
9598
99+
# The friends of the human exposed as a connection with edges
100+
friendsConnection(first: Int, after: ID): FriendsConnection!
101+
96102
# The movies this human appears in
97103
appearsIn: [Episode]!
98104
@@ -111,13 +117,42 @@ type Droid implements Character {
111117
# This droid's friends, or an empty list if they have none
112118
friends: [Character]
113119
120+
# The friends of the droid exposed as a connection with edges
121+
friendsConnection(first: Int, after: ID): FriendsConnection!
122+
114123
# The movies this droid appears in
115124
appearsIn: [Episode]!
116125
117126
# This droid's primary function
118127
primaryFunction: String
119128
}
120129
130+
# A connection object for a character's friends
131+
type FriendsConnection {
132+
# The total number of friends
133+
totalCount: Int
134+
135+
# The edges for each of the character's friends.
136+
edges: [FriendsEdge]
137+
138+
# Information for paginating this connection
139+
pageInfo: PageInfo!
140+
}
141+
142+
# An edge object for a character's friends
143+
type FriendsEdge {
144+
# A cursor used for pagination
145+
cursor: ID!
146+
147+
# The character represented by this friendship edge
148+
node: Character
149+
}
150+
151+
# Information for paginating this connection
152+
type PageInfo {
153+
hasNextPage: Boolean!
154+
}
155+
121156
# Represents a review for a movie
122157
type Review {
123158
# The number of stars this review gave, 1-5
@@ -306,6 +341,14 @@ function getStarship(id) {
306341
return starshipData[id];
307342
}
308343

344+
function toCursor(str) {
345+
return Buffer("cursor" + str).toString('base64');
346+
}
347+
348+
function fromCursor(str) {
349+
return Buffer.from(str, 'base64').toString().slice(6);
350+
}
351+
309352
const resolvers = {
310353
Query: {
311354
hero: (root, { episode }) => getHero(episode),
@@ -349,13 +392,46 @@ const resolvers = {
349392
return height;
350393
},
351394
friends: ({ friends }) => friends.map(getCharacter),
395+
friendsConnection: ({ friends }, { first, after }) => {
396+
first = first || friends.length;
397+
after = parseInt(fromCursor(after), 10) || 0;
398+
return {
399+
edges: friends.map((friend, i) => ({
400+
cursor: toCursor(i+1),
401+
node: getCharacter(friend)
402+
})).slice(after, first + after),
403+
pageInfo: { hasNextPage: first + after < friends.length },
404+
totalCount: friends.length
405+
};
406+
},
352407
starships: ({ starships }) => starships.map(getStarship),
353408
appearsIn: ({ appearsIn }) => appearsIn,
354409
},
355410
Droid: {
356411
friends: ({ friends }) => friends.map(getCharacter),
412+
friendsConnection: ({ friends }, { first, after }) => {
413+
first = first || friends.length;
414+
after = parseInt(fromCursor(after), 10) || 0;
415+
return {
416+
edges: friends.map((friend, i) => ({
417+
cursor: toCursor(i+1),
418+
node: getCharacter(friend)
419+
})).slice(after, first + after),
420+
pageInfo: { hasNextPage: first + after < friends.length },
421+
totalCount: friends.length
422+
};
423+
},
357424
appearsIn: ({ appearsIn }) => appearsIn,
358425
},
426+
FriendsConnection: {
427+
edges: ({ edges }) => edges,
428+
pageInfo: ({ pageInfo }) => pageInfo,
429+
totalCount: ({ totalCount }) => totalCount,
430+
},
431+
FriendsEdge: {
432+
node: ({ node }) => node,
433+
cursor: ({ cursor }) => cursor,
434+
},
359435
Starship: {
360436
length: ({ length }, { unit }) => {
361437
if (unit === 'FOOT') {

site/learn/BestPractice-Authorization.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ title: Authorization
33
layout: ../_core/DocsLayout
44
category: Best Practices
55
permalink: /learn/authorization/
6+
next: /learn/connections/
67
---
78

89
> Delegate authorization logic to the business logic layer
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
title: Connections
3+
layout: ../_core/DocsLayout
4+
category: Best Practices
5+
permalink: /learn/connections/
6+
---
7+
8+
> Different connection models enable different client capabilities
9+
10+
A common use case in GraphQL is traversing the relationship between sets of objects. There are a number of different ways that these relationships can be exposed in GraphQL, giving a varying set of capabilities to the client developer.
11+
12+
## Plurals
13+
14+
The most simple way to expose a connection between objects is with a field that returns a plural type. For example, if we wanted to get a list of R2-D2's friends, we could just ask for all of them:
15+
16+
```graphql
17+
# { "graphiql": true }
18+
{
19+
hero {
20+
name
21+
friends {
22+
name
23+
}
24+
}
25+
}
26+
```
27+
28+
## Slicing
29+
30+
Quickly, though, we realize that there are additional behaviors a client might want. A client might want to be able to specify how many friends they want to fetch; maybe they only want the first two. So we'd want to expose something like:
31+
32+
33+
```graphql
34+
{
35+
hero {
36+
name
37+
friends(first:2) {
38+
name
39+
}
40+
}
41+
}
42+
```
43+
44+
But if we just fetched the first two, we might want to paginate through the list as well; once the client fetches the first two friends, they might want to send a second request to ask for the next two friends. How can we enable that behavior.
45+
46+
## Pagination and Edges
47+
48+
There are a number of ways we could do pagination:
49+
50+
- We could do something like `friends(first:2 offset:2)` to ask for the next two in the list.
51+
- We could do something like `friends(first:2 after:$friendId)`, to ask for the next two after the last friend we fetched.
52+
- We could do something like `friends(first:2 after:$friendCursor)`, where we get a cursor from the last item and use that to paginate.
53+
54+
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.
55+
56+
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:
57+
58+
```graphql
59+
{
60+
hero {
61+
name
62+
friends(first:2) {
63+
node {
64+
name
65+
}
66+
cursor
67+
}
68+
}
69+
}
70+
```
71+
72+
The concept of an edge also proves useful if there is information that is specific to the edge, rather than to one of the objects. For example, if we wanted to expose "friendship time" in the API, having it live on the edge is a natural place to put it.
73+
74+
## End-of-list, counts, and Connections
75+
76+
Now we have the ability to paginate through the connection using cursors, but how do we know when we reach the end of the connection? We have to keep querying until we get an empty list back, but we'd really like for the connection to tell us when we've reached the end so we don't need that additional request. Similarly, what if we want to know additional information about the connection itself; for example, how many total friends does R2-D2 have?
77+
78+
To solve both of these problems, our `friends` field can return a connection object. The connection object will then have field for the edges, as well as other information (like total count and information about whether a next page exists). So our final query might look more like:
79+
80+
81+
```graphql
82+
{
83+
hero {
84+
name
85+
friends(first:2) {
86+
totalCount
87+
edges {
88+
node {
89+
name
90+
}
91+
cursor
92+
}
93+
pageInfo {
94+
hasNextPage
95+
}
96+
}
97+
}
98+
}
99+
```
100+
101+
## Complete Connection Model
102+
103+
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:
104+
105+
- The ability to paginate through the list.
106+
- The ability to ask for information about the connection itself, like `totalCount` or `pageInfo`.
107+
- The ability to ask for information about the edge itself, like `cursor` or `friendshipTime`.
108+
- The ability to change how our backend does pagination, since the user just uses opaque cursors.
109+
110+
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.
111+
112+
```graphql
113+
# { "graphiql": true }
114+
{
115+
hero {
116+
name
117+
friendsConnection(first:2 after:"Y3Vyc29yMQ==") {
118+
totalCount
119+
edges {
120+
node {
121+
name
122+
}
123+
cursor
124+
}
125+
pageInfo {
126+
hasNextPage
127+
}
128+
}
129+
}
130+
}
131+
```
132+

0 commit comments

Comments
 (0)