packages feed

tedious-web-0.2.1.2: src/Tedious/Handler.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE Arrows #-}

module Tedious.Handler (
  withDoc,
  withDoc',
  errorHandler,
  authFail,
  audit,
  list,
  list',
  get,
  get',
  add,
  add',
  dup,
  dup',
  upd,
  upd',
  del
) where

import Control.Arrow (returnA)
import Control.Lens ((^.))
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Pool (Pool, withResource)
import Data.Profunctor.Product.Default (Default)
import Data.Text (Text, pack)
import Data.Time (getCurrentTime)
import Data.Tuple.All (Sel1 (..))
import Database.PostgreSQL.Simple (Connection)
import Effectful (Eff, IOE, MonadIO (..), (:>))
import Effectful.Error.Dynamic (throwError)
import Effectful.Reader.Dynamic (Reader, asks)
import Network.HTTP.Types qualified as HTTP
import Opaleye (DefaultFromField, Delete (Delete, dReturning, dTable, dWhere), Field, FromFields, Insert (Insert, iOnConflict, iReturning, iRows, iTable), Order, Select, SqlBool, SqlInt8, Table, Unpackspec, Update (Update, uReturning, uTable, uUpdateWith, uWhere), countRows, limit, offset, orderBy, rCount, rReturning, runDelete, runInsert, runSelect, runUpdate, selectTable, sqlStrictText, sqlUTCTime, toNullable, where_, (.==))
import Opaleye.Internal.Table (tableIdentifier)
import Tedious.Entity (Err (Err), Page (Page), PageI (_pageIFilter, _pageIPage), PageO (PageO), Rep, SysOper' (..), SysOperTargetName, catchRep, fillPage, pageIndex, pageSize, rep, repErr, repOk, sysOperTable)
import WebGear.Core (BasicAuthError (..), Body, Description, Gets, Handler (arrM, setDescription, setSummary), HasTrait (from), HaveTraits, JSON (JSON), Middleware, PathVar, PlainText (..), Request, RequestHandler, RequiredResponseHeader, Response, Sets, StdHandler, Summary, With, pick, requestBody, respondA, (<<<))

withDoc :: (Handler h m) => Summary -> Description -> Middleware h ts ts
withDoc summ desc handler = setDescription desc <<< setSummary summ <<< handler

withDoc' :: (Handler h m) => Summary -> Middleware h ts ts
withDoc' summ handler = setSummary summ <<< handler

errorHandler ::
  ( Show e,
    StdHandler h m,
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep ())]
  ) =>
  h (Request `With` ts, e) Response
errorHandler = proc (_, err) -> respondA HTTP.ok200 JSON -< (repErr 1 (pack . show $ err) :: Rep ())

-- authFail ::
--   ( StdHandler h App,
--     Sets h '[RequiredResponseHeader "WWW-Authenticate" Text]
--   ) =>
--   h (Request `With` ts, BasicAuthError ()) Response
-- authFail = respondUnauthorized "Basic" "MyRealm"

authFail ::
  ( StdHandler h (Eff es)
  ) =>
  h (Request `With` ts, BasicAuthError ()) Response
authFail = proc (_request, err) -> case err of
  BasicAuthAttributeError () ->
    respondA HTTP.forbidden403 PlainText -< "Forbidden" :: Text
  _ ->
    respondA HTTP.unauthorized401 PlainText -< "Unauthorized" :: Text

audit ::
  forall eff env es h ts. -- eff effects (app env) arrow traits
  ( Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff
  ) =>
  (env -> Pool Connection) ->
  Maybe Text ->
  (RequestHandler h ts, SysOper') ->
  RequestHandler h ts
audit envPool user (handler, oper) =
  proc request -> do
    res <- handler -< request
    arrM
      ( const $ do
          now <- liftIO getCurrentTime
          pool <- asks envPool
          liftIO . withResource pool $ \conn -> do
            runInsert
              conn
              Insert
                { iTable = sysOperTable,
                  iRows =
                    [ ( Nothing,
                        toNullable . sqlStrictText <$> user,
                        sqlStrictText . _sysOper'Name $ oper,
                        sqlStrictText . _sysOper'Target $ oper,
                        toNullable . sqlStrictText <$> _sysOper'Content oper,
                        sqlUTCTime now
                      )
                    ],
                  iReturning = rReturning (const ()),
                  iOnConflict = Nothing
                }
      )
      -<
        ()
    returnA -< res

list ::
  forall i d f ids wfs rfs r eff env es h ts. -- (record id) data (data filter) [id] (table write fields) (table read fields) (table record) eff effects (app env) arrow traits
  ( ids ~ [i],
    Integral i,
    DefaultFromField SqlInt8 i,
    Default Unpackspec rfs rfs,
    Default FromFields rfs r,
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    Gets h '[Body JSON (PageI f)],
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep (PageO [d])), Body JSON (Rep ())]
  ) =>
  (env -> Pool Connection) ->
  Table wfs rfs ->
  (Maybe f -> rfs -> Field SqlBool) ->
  Order rfs ->
  (r -> d) ->
  (RequestHandler h ts, SysOper')
list envPool tbl flt ord r2d =
  let handler = requestBody @(PageI f) JSON errorHandler $
        proc request -> do
          let pageI = pick @(Body JSON (PageI f)) $ from request
          res <-
            arrM
              ( \pageI -> catchRep $ do
                  let mPage = _pageIPage pageI
                  pool <- asks envPool
                  (rs, cs) <- liftIO . withResource pool $
                    \conn -> do
                      let sel = do
                            r <- selectTable tbl
                            where_ $ flt (_pageIFilter pageI) r
                            pure r
                      let sel' = case mPage of
                            Nothing -> sel
                            Just page -> limit (fromIntegral $ page ^. pageSize) . offset (fromIntegral $ (page ^. pageIndex - 1) * (page ^. pageSize)) . orderBy ord $ sel
                      rs <- runSelect conn sel'
                      cs <- runSelect conn . countRows $ selectTable tbl
                      return (rs, fromIntegral <$> (cs :: ids))
                  let count = fromMaybe 0 (listToMaybe cs)
                  let pageO = case mPage of
                        Nothing -> PageO (Page 1 count) count
                        Just page -> fillPage page count
                  return . rep . pageO $ r2d <$> rs
              )
              -<
                pageI
          respondA HTTP.ok200 JSON -< res
      sysOper = SysOper' "list" (pack . show . tableIdentifier $ tbl) Nothing
   in (handler, sysOper)

list' ::
  forall i d f ids rfs r eff env es h ts. -- (record id) data (data filter) [id] (table read fields) (table record) eff effects (app env) arrow traits
  ( ids ~ [i],
    Integral i,
    DefaultFromField SqlInt8 i,
    Default FromFields rfs r,
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    Gets h '[Body JSON (PageI f)],
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep (PageO [d])), Body JSON (Rep ())]
  ) =>
  SysOperTargetName ->
  (env -> Pool Connection) ->
  Select rfs ->
  (Maybe f -> rfs -> Field SqlBool) ->
  Order rfs ->
  (r -> d) ->
  (RequestHandler h ts, SysOper')
list' tName envPool sel flt ord r2d =
  let handler = requestBody @(PageI f) JSON errorHandler $
        proc request -> do
          let pageI = pick @(Body JSON (PageI f)) $ from request
          res <-
            arrM
              ( \pageI -> catchRep $ do
                  let mPage = _pageIPage pageI
                  pool <- asks envPool
                  (rs, cs) <- liftIO . withResource pool $
                    \conn -> do
                      let sel_ = do
                            r <- sel
                            where_ $ flt (_pageIFilter pageI) r
                            pure r
                      let sel' = case mPage of
                            Nothing -> sel_
                            Just page -> limit (fromIntegral $ page ^. pageSize) . offset (fromIntegral $ (page ^. pageIndex - 1) * (page ^. pageSize)) . orderBy ord $ sel_
                      rs <- runSelect conn sel'
                      cs <- runSelect conn . countRows $ sel
                      return (rs, fromIntegral <$> (cs :: ids))
                  let count = fromMaybe 0 (listToMaybe cs)
                  let pageO = case mPage of
                        Nothing -> PageO (Page 1 count) count
                        Just page -> fillPage page count
                  return . rep . pageO $ r2d <$> rs
              )
              -<
                pageI
          respondA HTTP.ok200 JSON -< res
      sysOper = SysOper' "list" tName Nothing
   in (handler, sysOper)

get ::
  forall i fi d wfs rfs r eff env es h ts. -- (record id) (field id) data (table write fields) (table read fields) (table record) eff effects (app env) arrow traits
  ( Default Unpackspec rfs rfs,
    Default FromFields rfs r,
    Sel1 rfs (Field fi),
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    HaveTraits '[PathVar "id" i] ts,
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep (Maybe d))]
  ) =>
  (env -> Pool Connection) ->
  Table wfs rfs ->
  (i -> Field fi) ->
  (r -> d) ->
  (RequestHandler h ts, SysOper')
get envPool tbl idf r2d =
  let handler = proc request -> do
        let tid = pick @(PathVar "id" i) $ from request
        (md :: Maybe d) <-
          arrM
            ( \tid -> do
                pool <- asks envPool
                rs <- liftIO . withResource pool $
                  \conn -> runSelect conn $ do
                    r <- selectTable tbl
                    where_ $ sel1 r .== idf tid
                    pure r
                pure . listToMaybe $ r2d <$> rs
            )
            -<
              tid
        respondA HTTP.ok200 JSON -< rep md
      sysOper = SysOper' "get" (pack . show . tableIdentifier $ tbl) Nothing
   in (handler, sysOper)

get' ::
  forall i fi d rfs r eff env es h ts. -- (record id) (field id) data (table read fields) (table record) eff effects (app env) arrow traits
  ( Default FromFields rfs r,
    Sel1 rfs (Field fi),
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    HaveTraits '[PathVar "id" i] ts,
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep (Maybe d))]
  ) =>
  SysOperTargetName ->
  (env -> Pool Connection) ->
  Select rfs ->
  (i -> Field fi) ->
  (r -> d) ->
  (RequestHandler h ts, SysOper')
get' tName envPool sel idf r2d =
  let handler = proc request -> do
        let tid = pick @(PathVar "id" i) $ from request
        (md :: Maybe d) <-
          arrM
            ( \tid -> do
                pool <- asks envPool
                rs <- liftIO . withResource pool $
                  \conn -> runSelect conn $ do
                    r <- sel
                    where_ $ sel1 r .== idf tid
                    pure r
                pure . listToMaybe $ r2d <$> rs
            )
            -<
              tid
        respondA HTTP.ok200 JSON -< rep md
      sysOper = SysOper' "get" tName Nothing
   in (handler, sysOper)

add ::
  forall i fi a wfs rfs eff env es h ts. -- (record id) (field id) (data to add) (table write fields) (table read fields) eff effects (app env) arrow traits
  ( Sel1 rfs (Field fi),
    DefaultFromField fi i,
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    Gets h '[Body JSON a],
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep i), Body JSON (Rep ())]
  ) =>
  (env -> Pool Connection) ->
  Table wfs rfs ->
  (a -> [wfs]) ->
  (RequestHandler h ts, SysOper')
add envPool tbl a2t =
  let handler = requestBody @a JSON errorHandler $
        proc request -> do
          let toAdd = pick @(Body JSON a) $ from request
          rep_ <-
            arrM
              ( \toAdd -> catchRep $ do
                  pool <- asks envPool
                  ids <- liftIO . withResource pool $ \conn -> do
                    runInsert
                      conn
                      Insert
                        { iTable = tbl,
                          iRows = a2t toAdd,
                          iReturning = rReturning sel1,
                          iOnConflict = Nothing
                        }
                  maybe (throwError $ Err 1 "insert failure") (return . rep) (listToMaybe ids)
              )
              -<
                toAdd
          respondA HTTP.ok200 JSON -< (rep_ :: Rep i)
      sysOper = SysOper' "add" (pack . show . tableIdentifier $ tbl) Nothing
   in (handler, sysOper)

add' ::
  forall i a eff env es h ts. -- (record id) (data to add) eff effects (app env) arrow traits
  ( Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    Gets h '[Body JSON a],
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep i), Body JSON (Rep ())]
  ) =>
  SysOperTargetName ->
  (env -> Pool Connection) ->
  (Connection -> a -> IO i) ->
  (RequestHandler h ts, SysOper')
add' tName envPool oper =
  let handler = requestBody @a JSON errorHandler $
        proc request -> do
          let toAdd = pick @(Body JSON a) $ from request
          rep_ <-
            arrM
              ( \toAdd -> catchRep $ do
                  pool <- asks envPool
                  liftIO . withResource pool $ \conn -> rep <$> oper conn toAdd
              )
              -<
                toAdd
          respondA HTTP.ok200 JSON -< rep_
      sysOper = SysOper' "add" tName Nothing
   in (handler, sysOper)

dup ::
  forall i fi wfs rfs r eff env es h ts. -- (record id) (field id) (table write fields) (table read fields) (table record) eff effects (app env) arrow traits
  ( Default Unpackspec rfs rfs,
    Default FromFields rfs r,
    Sel1 rfs (Field fi),
    DefaultFromField fi i,
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    HaveTraits '[PathVar "id" i] ts,
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep i), Body JSON (Rep ())]
  ) =>
  (env -> Pool Connection) ->
  Table wfs rfs ->
  (i -> Field fi) ->
  (r -> wfs) ->
  (RequestHandler h ts, SysOper')
dup envPool tbl idf r2t =
  let handler = proc request -> do
        let tid = pick @(PathVar "id" i) $ from request
        tid_ <-
          arrM
            ( \tid -> catchRep $ do
                pool <- asks envPool
                ids <- liftIO . withResource pool $
                  \conn -> do
                    rs <- runSelect conn $ do
                      r <- selectTable tbl
                      where_ $ sel1 r .== idf tid
                      pure r
                    runInsert
                      conn
                      Insert
                        { iTable = tbl,
                          iRows = r2t <$> rs,
                          iReturning = rReturning sel1,
                          iOnConflict = Nothing
                        }
                maybe (throwError $ Err 1 "duplicate failure") (return . rep) (listToMaybe ids)
            )
            -<
              tid
        respondA HTTP.ok200 JSON -< (tid_ :: Rep i)
      sysOper = SysOper' "dup" (pack . show . tableIdentifier $ tbl) Nothing
   in (handler, sysOper)

dup' ::
  forall i eff env es h ts. -- (record id) eff effects (app env) arrow traits
  ( Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    HaveTraits '[PathVar "id" i] ts,
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep i), Body JSON (Rep ())]
  ) =>
  SysOperTargetName ->
  (env -> Pool Connection) ->
  (Connection -> i -> IO i) ->
  (RequestHandler h ts, SysOper')
dup' tName envPool oper =
  let handler = proc request -> do
        let tid = pick @(PathVar "id" i) $ from request
        rep_ <-
          arrM
            ( \(tid, oper_) -> catchRep $ do
                pool <- asks envPool
                liftIO . withResource pool $
                  \conn -> rep <$> oper_ conn tid
            )
            -<
              (tid, oper)
        respondA HTTP.ok200 JSON -< rep_
      sysOper = SysOper' "dup" tName Nothing
   in (handler, sysOper)

upd ::
  forall i fi u wfs rfs d r eff env es h ts. -- (record id) (field id) update (table write fields) (table read fields) data (table record) eff effects (app env) arrow traits
  ( Sel1 rfs (Field fi),
    Default FromFields rfs r,
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    HaveTraits '[PathVar "id" i] ts,
    Gets h '[Body JSON u],
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep d), Body JSON (Rep ())]
  ) =>
  (env -> Pool Connection) ->
  Table wfs rfs ->
  (i -> Field fi) ->
  (u -> (rfs -> wfs)) ->
  (r -> d) ->
  (RequestHandler h ts, SysOper')
upd envPool tbl idf u2t r2d =
  let handler = requestBody @u JSON errorHandler $
        proc request -> do
          let tid = pick @(PathVar "id" i) $ from request
          let toUpd = pick @(Body JSON u) $ from request
          ru <-
            arrM
              ( \(tid, toUpd) -> catchRep $ do
                  pool <- asks envPool
                  ru <- liftIO . withResource pool $ \conn -> do
                    runUpdate
                      conn
                      Update
                        { uTable = tbl,
                          uUpdateWith = u2t toUpd,
                          uWhere = (.== idf tid) . sel1,
                          uReturning = rReturning id
                        }
                  maybe (throwError $ Err 1 "insert failure") (return . rep . r2d) (listToMaybe ru)
              )
              -<
                (tid, toUpd)
          respondA HTTP.ok200 JSON -< (ru :: Rep d)
      sysOper = SysOper' "upd" (pack . show . tableIdentifier $ tbl) Nothing
   in (handler, sysOper)

upd' ::
  forall i u d eff env es h ts. -- (record id) update data eff effects (app env) arrow traits
  ( Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    HaveTraits '[PathVar "id" i] ts,
    Gets h '[Body JSON u],
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep d), Body JSON (Rep ())]
  ) =>
  SysOperTargetName ->
  (env -> Pool Connection) ->
  (Connection -> i -> u -> IO d) ->
  (RequestHandler h ts, SysOper')
upd' tName envPool oper =
  let handler = requestBody @u JSON errorHandler $
        proc request -> do
          let tid = pick @(PathVar "id" i) $ from request
          let toUpd = pick @(Body JSON u) $ from request
          ru <-
            arrM
              ( \(tid, toUpd) -> catchRep $ do
                  pool <- asks envPool
                  liftIO . withResource pool $ \conn -> rep <$> oper conn tid toUpd
              )
              -<
                (tid, toUpd)
          respondA HTTP.ok200 JSON -< (ru :: Rep d)
      sysOper = SysOper' "upd" tName Nothing
   in (handler, sysOper)

del ::
  forall i fi wfs rfs eff env es h ts. -- (record id) (field id) (table write fields) (table read fields) eff effects (app env) arrow traits
  ( Sel1 rfs (Field fi),
    Reader env :> es,
    IOE :> es,
    eff ~ Eff es,
    StdHandler h eff,
    HaveTraits '[PathVar "id" i] ts,
    Sets h '[RequiredResponseHeader "Content-Type" Text, Body JSON (Rep Text), Body JSON (Rep ())]
  ) =>
  (env -> Pool Connection) ->
  Table wfs rfs ->
  (i -> Field fi) ->
  (RequestHandler h ts, SysOper')
del envPool tbl idf =
  let handler = proc request -> do
        let tid = pick @(PathVar "id" i) $ from request
        r <-
          arrM
            ( \tid -> do
                pool <- asks envPool
                c <- liftIO . withResource pool $
                  \conn ->
                    runDelete conn $
                      Delete
                        { dTable = tbl,
                          dWhere = (.== idf tid) . sel1,
                          dReturning = rCount
                        }
                pure $ if c == 1 then repOk else repErr 1 "delete failure"
            )
            -<
              tid
        respondA HTTP.ok200 JSON -< (r :: Rep Text)
      sysOper = SysOper' "get" (pack . show . tableIdentifier $ tbl) Nothing
   in (handler, sysOper)