yesod-page-cursor
Cursor based pagination for yesod
using index friendly keyset cursors.
Primer: No Offset
getSomeR :: Handler Value
getSomeR = do
teacherId <- Param.required "teacherId"
mCourseId <- Param.optional "courseId"
page <- withPageAbsolute 100 entityKey $ \Cursor {..} -> do
let (teacherId, mCourseId) = cursorParams
fmap (sort cursorPosition) . runDB $ selectList
(catMaybes
[ Just $ SomeAssignmentTeacherId ==. teacherId
, (SomeAssignmentCourseId ==.) <$> mCourseId
, whereClause cursorPosition
]
)
[LimitTo $ fromMaybe 100 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 $ \Cursor {..} -> do
fmap (sort cursorPosition) . runDB $ selectList
(whereClause cursorPosition)
[ LimitTo $ fromMaybe 100 cursorLimit
, orderBy cursorPosition
]
returnJson $ keyValueEntityToJSON <$> page
where
whereClause = \case
First -> []
Previous (pId, createdAt) ->
[ SomeAssingmentCreatedAt <=. createdAt
, persistIdField <. pId
]
Next (pId, createdAt) ->
[ SomeAssingmentCreatedAt >=. createdAt
, persistIdField >. pId
]
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
Usage
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": [...]
}