packages feed

relay-pagination-hasql-0.1.0.0: src/Relay/Pagination/Hasql.hs

-- | Keyset-pagination engine for Relay-style cursors over hasql.
--
-- Given a base query 'Snippet' (filters included, no @ORDER BY@, no
-- @LIMIT@), a 'SortSpec' (ordered sort columns ending in a unique
-- tie-breaker, each with a typed codec), and a row decoder, 'paginate'
-- produces a 'Statement' that fetches one validated page and assembles a
-- spec-correct 'Connection'. Cursor values always travel as typed SQL
-- parameters; malformed cursors surface as 'CursorError' before any SQL
-- runs.
module Relay.Pagination.Hasql
  ( -- * Sort specifications
    SortDirection (..),
    KeyColumn (..),
    SortSpec (..),
    sortSpecFingerprint,

    -- * Codecs
    KeyCodec (..),
    int8Key,
    textKey,
    uuidKey,
    timestamptzKey,
    boolKey,

    -- * Engine
    paginate,
    paginateSnippet,
    mkConnection,
    mintCursor,
  )
where

import Hasql.Decoders qualified as Decoders
import Hasql.DynamicStatements.Snippet (Snippet)
import Hasql.DynamicStatements.Snippet qualified as Snippet
import Hasql.Statement (Statement)
import Relay.Pagination (Connection, CursorError, PageRequest)
import Relay.Pagination.Hasql.Connection (mintCursor, mkConnection)
import Relay.Pagination.Hasql.KeyCodec
  ( KeyCodec (..),
    boolKey,
    int8Key,
    textKey,
    timestamptzKey,
    uuidKey,
  )
import Relay.Pagination.Hasql.SortSpec
  ( KeyColumn (..),
    SortDirection (..),
    SortSpec (..),
    sortSpecFingerprint,
  )
import Relay.Pagination.Hasql.Sql (paginateSnippet)

-- | The whole engine in one function: validate the request's cursor against
-- the sort specification, generate the keyset query, and post-process the
-- fetched rows into a 'Connection'.
--
-- 'Left' means the cursor failed to decode against this spec (wrong
-- fingerprint, key count, or key types) — surfaced before any SQL runs;
-- handlers map it to HTTP 400. The statement is unpreparable (dynamic SQL;
-- see hasql-dynamic-statements). Run it with e.g.
-- @Hasql.Session.statement () stmt@.
paginate ::
  SortSpec row ->
  PageRequest ->
  -- | Base query: @SELECT <cols> FROM ... WHERE <filters>@; no @ORDER BY@,
  -- no @LIMIT@.
  Snippet ->
  Decoders.Row row ->
  Either CursorError (Statement () (Connection row))
paginate spec req base rowDecoder = do
  snippet <- paginateSnippet spec req base
  Right (mkConnection spec req <$> Snippet.toStatement snippet (Decoders.rowList rowDecoder))