Since then I did some iterations and actualy end up with similar solution, but as you rightly pointed out, there is something wrong with AppM
to by more specific, I’m trying to implement ReaderT
pattern… Here is whole code:
module Main where
import Prelude
import Control.Monad.Indexed.Qualified as Ix
import Control.Monad.Reader (ReaderT, asks, runReaderT)
import Control.Monad.Reader.Class (class MonadAsk)
import Data.Number as Number
import Data.Maybe (Maybe(Just, Nothing))
import Data.Either (Either(Right, Left))
import Data.HTTP.Method (Method(GET, POST))
import Data.DateTime (DateTime)
import Effect (Effect)
import Effect.Aff (Aff)
import Hyper.Middleware (Middleware)
import Hyper.Node.Server (defaultOptionsWithLogging, runServer', HttpRequest, HttpResponse)
import Hyper.Response (closeHeaders, respond, writeStatus, StatusLineOpen, ResponseEnded)
import Hyper.Request (getRequestData)
import Hyper.Status (Status, statusOK, statusBadRequest)
import Hyper.Form (Form, parseForm, required)
import Hyper.Conn (Conn)
import Text.Smolder.HTML (Html, html, p, form, label, input, button, div)
import Text.Smolder.HTML.Attributes (method, for, id, name, type')
import Text.Smolder.Markup (text, (!))
import Text.Smolder.Renderer.String (render)
import Database.PostgreSQL (Pool, PoolConfiguration, newPool, withConnection, execute, defaultPoolConfiguration, PGError, Connection, Row0 (Row0), Row3(Row3), Query(Query))
type ComponentsType = Record ()
type ConnHttp resp = Conn HttpRequest (HttpResponse resp) ComponentsType
type Env =
{ pool :: Pool
}
type AppM m = ReaderT Env m
getPool :: forall m. MonadAsk Env m => m Pool
getPool = asks _.pool
runAppM ∷ forall a. Env -> (AppM Aff a) -> Aff a
runAppM = flip runReaderT
config :: PoolConfiguration
config =
let default = defaultPoolConfiguration "purescript"
in default
{ host = Just "localhost"
, password = Just "postgres"
, user = Just "postgres"
}
send :: Status -> Html Unit -> Middleware (AppM Aff) (ConnHttp StatusLineOpen) (ConnHttp ResponseEnded) Unit
send status body = Ix.do
writeStatus status
closeHeaders
respond (render body)
greetTemplate :: String -> Html Unit
greetTemplate name = html (p (text $ "Hello " <> name))
formTemplate :: Html Unit
formTemplate =
html do
form ! method "post" $ do
label ! for "name" $ text "Name"
input ! id "name" ! name "name"
button $ text "Greet"
fruitForm :: Html Unit
fruitForm =
html do
form ! method "post" $ do
div do
label ! for "name" $ text "Name"
input ! id "name" ! name "name"
div do
label ! for "delicious" $ text "Delicious"
input ! id "delicious" ! name "delicious" ! type' "checkbox"
div do
label ! for "price" $ text "Price"
input ! id "price" ! name "price" ! type' "number"
button $ text "Save"
errorTemplate :: String -> Html Unit
errorTemplate error = html do
p (text error)
formHandler :: Middleware (AppM Aff) (ConnHttp StatusLineOpen) (ConnHttp ResponseEnded) Unit
formHandler = send statusOK formTemplate
fruitNewHandler :: Middleware (AppM Aff) (ConnHttp StatusLineOpen) (ConnHttp ResponseEnded) Unit
fruitNewHandler = send statusOK fruitForm
fruitCreateHandler :: Middleware (AppM Aff) (ConnHttp StatusLineOpen) (ConnHttp ResponseEnded) Unit
fruitCreateHandler = Ix.do
eitherBodyParams <- parseForm
result <- pure $ getPool >>= \pool -> withConnection pool \eitherConn -> do
input <- pure $ do
conn <- case eitherConn of
Left e -> Left (show e)
Right a -> Right a
params <- eitherBodyParams
name <- required "name" params
delicious <- required "delicious" params
price <- required "price" params
pure $ {conn, params, name, delicious, price}
case input of
Right {conn, name, delicious, price} ->
execute conn (Query "INSERT INTO fruits (name, delicious, price) VALUE ($1, $2, $3)") (Row3 name (castBoolean delicious) (Number.fromString price))
Left e ->
pure $ mempty
badRequestHandler "Head"
castBoolean :: String -> Boolean
castBoolean value = value == "on"
badRequestHandler :: String -> Middleware (AppM Aff) (ConnHttp StatusLineOpen) (ConnHttp ResponseEnded) Unit
badRequestHandler message = send statusBadRequest (errorTemplate message)
postHandler :: Middleware (AppM Aff) (ConnHttp StatusLineOpen) (ConnHttp ResponseEnded) Unit
postHandler = Ix.do
bodyData <- parseForm
case bodyData of
Right params -> case required "name" params of
Right name -> send statusOK (greetTemplate name)
_ -> badRequestHandler "Missing name"
_ -> badRequestHandler "Invalid Body"
handler :: Middleware (AppM Aff) (ConnHttp StatusLineOpen) (ConnHttp ResponseEnded) Unit
handler = Ix.do
requestData <- getRequestData
case requestData.url of
"/fruits" ->
case requestData.method of
Left GET -> fruitNewHandler
_ -> badRequestHandler "Unsupported method"
_ ->
case requestData.method of
Left GET -> formHandler
Left POST -> postHandler
_ -> badRequestHandler "Unsupported method"
main :: Effect Unit
main = do
pool <- newPool config
runServer' defaultOptionsWithLogging {} (runAppM { pool: pool }) handler
Here is packages and spago dhall (in case anybody would like to try run it localy)
packages.dhall
let upstream =
https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20201007/packages.dhall sha256:35633f6f591b94d216392c9e0500207bb1fec42dd355f4fecdfd186956567b6b
in upstream
with purescript-postgres-client =
{ name = "postgresql-client"
, version = "v3.0.0"
, dependencies =
[ "aff"
, "argonaut"
, "arrays"
, "assert"
, "bifunctors"
, "bytestrings"
, "console"
, "datetime"
, "decimals"
, "effect"
, "either"
, "exceptions"
, "foldable-traversable"
, "foreign"
, "foreign-generic"
, "foreign-object"
, "js-date"
, "lists"
, "maybe"
, "newtype"
, "nullable"
, "prelude"
, "psci-support"
, "string-parsers"
, "test-unit"
, "transformers"
, "tuples"
]
, repo = "https://github.com/rightfold/purescript-postgresql-client.git"
}
spago.dhall
{ name = "my-project"
, dependencies =
[ "console"
, "effect"
, "hyper"
, "hypertrout"
, "node-buffer"
, "node-http"
, "nodetrout"
, "numbers"
, "psci-support"
, "purescript-postgres-client"
, "smolder"
]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
}
Thank you for your effort so far I’m quite new to purescript