yesod-page-cursor

[ library, mit, unclassified ] [ Propose Tags ] [ Report a vulnerability ]

Cursor based pagination for Yesod


[Skip to Readme]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 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, 2.0.0.5, 2.0.0.6, 2.0.0.7, 2.0.0.8, 2.0.0.9, 2.0.0.10, 2.0.1.0
Change log ChangeLog.md
Dependencies aeson (>=1.3.1.1), base (>=4.7 && <5), bytestring (>=0.10.8.2), containers (>=0.5.11.0), http-link-header (>=1.0.3.1), network-uri (>=2.6.1.0), text (>=1.2.3.1), unliftio (>=0.2.9.0), yesod-core (>=1.6.9) [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 2022-03-30T14:36:52Z
Distributions LTSHaskell:2.0.1.0, NixOS:2.0.1.0, Stackage:2.0.1.0
Downloads 2758 total (47 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs available [build log]
Last success reported on 2022-03-30 [all 1 reports]

Readme for yesod-page-cursor-2.0.0.10

[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": [...]
}