/
pagination.ts
177 lines (164 loc) · 5.02 KB
/
pagination.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import {
GraphQLBoolean,
GraphQLInt,
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
GraphQLList,
} from "graphql"
import {
connectionDefinitions,
toGlobalId,
ConnectionConfig,
GraphQLConnectionDefinitions,
} from "graphql-relay"
import { warn } from "lib/loggers"
import { ResolverContext } from "types/graphql"
const PREFIX = "arrayconnection"
// In most cases Gravity caps the pagination results to 100 pages and we may not want to return more than that
// otherwise we'll generate links that do not work. As of writing there are three endpoints that do this:
//
// * https://github.com/artsy/gravity/blob/52635528/app/api/v1/filter_endpoint.rb#L38
// * https://github.com/artsy/gravity/blob/52635528/app/api/v1/filter_endpoint.rb#L79
// * https://github.com/artsy/gravity/blob/52635528/app/api/v1/partners_endpoint.rb#L168
//
const PAGE_NUMBER_CAP = 100
const PageCursor = new GraphQLObjectType<any, ResolverContext>({
name: "PageCursor",
fields: () => ({
cursor: {
type: new GraphQLNonNull(GraphQLString),
},
page: {
type: new GraphQLNonNull(GraphQLInt),
},
isCurrent: {
type: new GraphQLNonNull(GraphQLBoolean),
},
}),
})
export const PageCursorsType = new GraphQLObjectType<any, ResolverContext>({
name: "PageCursors",
fields: () => ({
first: {
type: PageCursor,
description:
"Optional, may be included in `around` (if current page is near the beginning).",
},
last: {
type: PageCursor,
description:
"Optional, may be included in `around` (if current page is near the end).",
},
around: {
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(PageCursor))),
description: "Always includes current page",
},
previous: { type: PageCursor },
}),
})
export function pageToCursor(page, size) {
return toGlobalId(PREFIX, String((page - 1) * size - 1))
}
// Returns an opaque cursor for a page.
function pageToCursorObject(page, currentPage, size) {
return {
cursor: pageToCursor(page, size),
page,
isCurrent: currentPage === page,
}
}
// Returns an array of PageCursor objects
// from start to end (page numbers).
function pageCursorsToArray(start, end, currentPage, size) {
let page
const cursors = []
for (page = start; page <= end; page++) {
// FIXME: Argument of type '{ cursor: string; page: any; isCurrent: boolean; }' is not assignable to parameter of type 'never'.
// @ts-ignore
cursors.push(pageToCursorObject(page, currentPage, size))
}
return cursors
}
// Returns the total number of pagination results capped to PAGE_NUMBER_CAP.
export function computeTotalPages(totalRecords, size) {
return Math.min(Math.ceil(totalRecords / size), PAGE_NUMBER_CAP)
}
export function createPageCursors(
{ page: currentPage, size },
totalRecords,
max = 5
) {
// If max is even, bump it up by 1, and log out a warning.
if (max % 2 === 0) {
warn(`Max of ${max} passed to page cursors, using ${max + 1}`)
max = max + 1
}
const totalPages = computeTotalPages(totalRecords, size)
let pageCursors
// Degenerate case of no records found.
if (totalPages === 0) {
pageCursors = { around: [pageToCursorObject(1, 1, size)] }
} else if (totalPages <= max) {
// Collection is short, and `around` includes page 1 and the last page.
pageCursors = {
around: pageCursorsToArray(1, totalPages, currentPage, size),
}
} else if (currentPage <= Math.floor(max / 2) + 1) {
// We are near the beginning, and `around` will include page 1.
pageCursors = {
last: pageToCursorObject(totalPages, currentPage, size),
around: pageCursorsToArray(1, max - 1, currentPage, size),
}
} else if (currentPage >= totalPages - Math.floor(max / 2)) {
// We are near the end, and `around` will include the last page.
pageCursors = {
first: pageToCursorObject(1, currentPage, size),
around: pageCursorsToArray(
totalPages - max + 2,
totalPages,
currentPage,
size
),
}
} else {
// We are in the middle, and `around` doesn't include the first or last page.
const offset = Math.floor((max - 3) / 2)
pageCursors = {
first: pageToCursorObject(1, currentPage, size),
around: pageCursorsToArray(
currentPage - offset,
currentPage + offset,
currentPage,
size
),
last: pageToCursorObject(totalPages, currentPage, size),
}
}
if (currentPage > 1 && totalPages > 1) {
pageCursors.previous = pageToCursorObject(
currentPage - 1,
currentPage,
size
)
}
return pageCursors
}
export function connectionWithCursorInfo(
config: ConnectionConfig
): GraphQLConnectionDefinitions {
return connectionDefinitions({
...config,
connectionFields: {
pageCursors: {
type: new GraphQLNonNull(PageCursorsType),
resolve: ({ pageCursors }) => pageCursors,
},
totalCount: {
type: GraphQLInt,
resolve: ({ totalCount }) => totalCount,
},
...config.connectionFields,
},
})
}