Cursor based pagination for yesod
using index friendly keyset cursors.
Primer: No Offset
mkPersist sqlSettings [persistLowerCase|
SomeAssignment json
createdAt UTCTime
teacherId Int
courseId Int
|]
getSomeR :: Handler Value
getSomeR = do
teacherId <- requiredParam "teacherId"
mCourseId <- optionalParam "courseId"
page <- withPageAbsolute 100 entityKey $ \Cursor {..} -> do
fmap (sort cursorPosition) . runDB $ selectList
(catMaybes
[ Just $ SomeAssignmentTeacherId ==. teacherId
, (SomeAssignmentCourseId ==.) <$> mCourseId
, whereClause cursorPosition
]
)
[LimitTo $ unLimit cursorLimit, orderBy cursorPosition]
returnJson $ keyValueEntityToJSON <$> page
where
whereClause = \case
First -> Nothing
Previous p -> Just $ persistIdField <. p
Next p -> Just $ persistIdField >. p
Last -> Nothing
orderBy = \case
First -> Asc persistIdField
Previous _ -> Desc persistIdField
Next _ -> Asc persistIdField
Last -> Desc persistIdField
sort = \case
First -> id
Previous _ -> reverse
Next _ -> id
Last -> reverse
cursorLastPosition
is configurable. A page sorted by created_at
may look
like:
getSortedSomeR :: Handler Value
getSortedSomeR = do
page <- withPageAbsolute 100 (someAssignmentCreatedAt . entityVal)
$ \Cursor {..} -> do
fmap (sort cursorPosition) . runDB $ selectList
(whereClause cursorPosition)
[ LimitTo $ unLimit cursorLimit
, orderBy cursorPosition
]
returnJson $ keyValueEntityToJSON <$> page
where
whereClause = \case
First -> []
Previous createdAt ->
[ SomeAssignmentCreatedAt <=. createdAt
]
Next createdAt ->
[ SomeAssignmentCreatedAt >=. createdAt
]
Last -> []
orderBy = \case
First -> Asc SomeAssignmentCreatedAt
Previous _ -> Desc SomeAssignmentCreatedAt
Next _ -> Asc SomeAssignmentCreatedAt
Last -> Desc SomeAssignmentCreatedAt
sort = \case
First -> id
Previous _ -> reverse
Next _ -> id
Last -> reverse
Paginated requests return a single page and a link with a cursor token to retrieve the next page.
% curl 'some-rest.com/endpoint?limit=3'
{
"first": : "https://some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
"previous": null,
"next": "https://some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
"data": [...]
}
The link can be used to retrieve the next page.
% curl 'some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ=='
{
"first": : "https://some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
"previous": "https://some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
"next": "https://some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
"data": [...]
}
If no pages remain then no link is returned
% curl 'some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ=='
{
"first": : "https://some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
"previous": "https://some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
"next": null,
"data": [...]
}