yesod-page-cursor

[ library, mit, unclassified ] [ Propose Tags ]

Cursor based pagination for Yesod


[Skip to Readme]
Versions [faq] 1.0.0.0, 1.0.0.1, 2.0.0.0, 2.0.0.1, 2.0.0.2, 2.0.0.3, 2.0.0.4
Change log ChangeLog.md
Dependencies aeson (>=1.5.5.1 && <1.6), base (>=4.7 && <5), bytestring (>=0.10.12.0 && <0.12), containers (>=0.6.2.1 && <0.7), http-link-header (>=1.0.3.1 && <1.3), network-uri (>=2.6.3.0 && <2.8), text (>=1.2.3.2 && <1.3), unliftio (>=0.2.14 && <0.3), yesod-core (>=1.6.18.8 && <1.7) [details]
License MIT
Copyright 2020 Renaissance Learning Inc
Author Freckle Engineering
Maintainer engineering@freckle.com
Home page https://github.com/freckle/yesod-page-cursor#readme
Bug tracker https://github.com/freckle/yesod-page-cursor/issues
Source repo head: git clone https://github.com/freckle/yesod-page-cursor
Uploaded by PatrickBrisbin at 2021-02-25T15:35:41Z
Distributions NixOS:2.0.0.2
Downloads 354 total (184 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Hackage Matrix CI
Docs not available [build log]
All reported builds failed as of 2021-02-25 [all 3 reports]

Modules

  • Yesod
    • Yesod.Page
      • Yesod.Page.RenderedRoute

Downloads

Maintainer's Corner

For package maintainers and hackage trustees


Readme for yesod-page-cursor-2.0.0.4

[back to package description]

yesod-page-cursor

Cursor based pagination for yesod using index friendly keyset cursors.

Primer: No Offset

getSomeR :: Handler Value
getSomeR = do
  let
    parseParams =
      (,) <$> Param.required "teacherId" <*> Param.optional "courseId"
  page <- withPage 100 entityPage parseParams $ \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:

createdAtPage = PageConfig
  { makePosition = \x ->
      (entityKey x, someAsssignmentCreatedAt $ entityVal x)
  , baseDomain = Nothing
  }

getSortedSomeR :: Handler Value
getSortedSomeR = do
  let parseParams = pure ()
  page <- withPage 100 createdAtPage parseParams $ \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": : "some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
  "previous": null,
  "next": "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": : "some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
  "previous": "some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
  "next": "some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
  "data": [...]
}

If no pages remain then no link is returned

$ curl 'some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ=='
{
  "first": : "some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
  "previous": "some-rest.com/endpoint?next=eyJsYXN0UG9zaXRpb24iOjMsInBhcmFtcyI6WzEsbnVsbF0sImxpbWl0IjozfQ==",
  "next": null,
  "data": [...]
}