init
This commit is contained in:
		
							
								
								
									
										53
									
								
								HELPS.cabal
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								HELPS.cabal
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| cabal-version: 1.12 | ||||
|  | ||||
| name:           HELPS | ||||
| version:        0.0.1 | ||||
| description:    Haskell, Effectful, Lucid, PostgreSQL, Servant | ||||
| author:         James Eversole | ||||
| maintainer:     james@eversole.co | ||||
| copyright:      James Eversole | ||||
| license:        ISC | ||||
| license-file:   LICENSE | ||||
| build-type:     Simple | ||||
| extra-source-files: | ||||
|     README | ||||
|  | ||||
| executable Main | ||||
|   main-is: Main.hs | ||||
|   hs-source-dirs: | ||||
|       src | ||||
|   default-extensions: | ||||
|       BlockArguments | ||||
|       ConstraintKinds | ||||
|       DataKinds | ||||
|       DeriveAnyClass | ||||
|       DeriveGeneric | ||||
|       DerivingStrategies | ||||
|       FlexibleContexts | ||||
|       FlexibleInstances | ||||
|       GeneralizedNewtypeDeriving | ||||
|       LambdaCase | ||||
|       OverloadedRecordDot | ||||
|       OverloadedStrings | ||||
|       ScopedTypeVariables | ||||
|       StrictData | ||||
|       TypeFamilies | ||||
|   ghc-options: -threaded -rtsopts -with-rtsopts=-N -optl-pthread -fPIC | ||||
|   build-depends: | ||||
|     base | ||||
|     , aeson | ||||
|     , bytestring | ||||
|     , effectful | ||||
|     , exceptions | ||||
|     , lucid | ||||
|     , mtl | ||||
|     , postgresql-simple | ||||
|     , servant-server | ||||
|     , text | ||||
|     , utf8-string | ||||
|     , warp | ||||
|   other-modules: | ||||
|     Core | ||||
|     Database | ||||
|     Handlers | ||||
|   default-language: GHC2021 | ||||
							
								
								
									
										9
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| # servant-effectful-template | ||||
|  | ||||
| A nix starting template for web projects utilizing | ||||
|  | ||||
| - [Haskell](https://wiki.haskell.org/Haskell) | ||||
| - [Effectful](https://github.com/haskell-effectful/effectful) | ||||
| - [Lucid](https://github.com/chrisdone/lucid) | ||||
| - [PostgreSQL](https://www.postgresql.org/) | ||||
| - [Servant](https://github.com/haskell-servant/servant) | ||||
							
								
								
									
										60
									
								
								flake.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								flake.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| { | ||||
|   "nodes": { | ||||
|     "flake-utils": { | ||||
|       "inputs": { | ||||
|         "systems": "systems" | ||||
|       }, | ||||
|       "locked": { | ||||
|         "lastModified": 1726560853, | ||||
|         "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", | ||||
|         "owner": "numtide", | ||||
|         "repo": "flake-utils", | ||||
|         "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "numtide", | ||||
|         "repo": "flake-utils", | ||||
|         "type": "github" | ||||
|       } | ||||
|     }, | ||||
|     "nixpkgs": { | ||||
|       "locked": { | ||||
|         "lastModified": 1728764407, | ||||
|         "narHash": "sha256-J4kaIxwjrGVQkLA6njCFH09xj2oCf/VWFircIy7b65k=", | ||||
|         "owner": "NixOS", | ||||
|         "repo": "nixpkgs", | ||||
|         "rev": "4ada35702a7146e1df24f8d6987a1d7c1a5a4707", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "NixOS", | ||||
|         "repo": "nixpkgs", | ||||
|         "type": "github" | ||||
|       } | ||||
|     }, | ||||
|     "root": { | ||||
|       "inputs": { | ||||
|         "flake-utils": "flake-utils", | ||||
|         "nixpkgs": "nixpkgs" | ||||
|       } | ||||
|     }, | ||||
|     "systems": { | ||||
|       "locked": { | ||||
|         "lastModified": 1681028828, | ||||
|         "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", | ||||
|         "owner": "nix-systems", | ||||
|         "repo": "default", | ||||
|         "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", | ||||
|         "type": "github" | ||||
|       }, | ||||
|       "original": { | ||||
|         "owner": "nix-systems", | ||||
|         "repo": "default", | ||||
|         "type": "github" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "root": "root", | ||||
|   "version": 7 | ||||
| } | ||||
							
								
								
									
										62
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								flake.nix
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,62 @@ | ||||
| { | ||||
|   description = "HELPS"; | ||||
|  | ||||
|   inputs = { | ||||
|     nixpkgs.url = "github:NixOS/nixpkgs"; | ||||
|     flake-utils.url = "github:numtide/flake-utils"; | ||||
|   }; | ||||
|  | ||||
|   outputs = { self, nixpkgs, flake-utils }: | ||||
|     flake-utils.lib.eachDefaultSystem (system: | ||||
|       let | ||||
|         pkgs              = nixpkgs.legacyPackages.${system}; | ||||
|         packageName       = "HELPS"; | ||||
|         containerPackageName = "${packageName}-container"; | ||||
|  | ||||
|         haskellPackages = pkgs.haskellPackages; | ||||
|  | ||||
|         enableSharedExecutables = false; | ||||
|         enableSharedLibraries = false; | ||||
|  | ||||
|         HELPS = pkgs.haskell.lib.justStaticExecutables self.packages.${system}.default; | ||||
|       in { | ||||
|  | ||||
|         packages.${packageName} = | ||||
|           haskellPackages.callCabal2nix packageName self rec {}; | ||||
|  | ||||
|         packages.default = self.packages.${system}.${packageName}; | ||||
|         defaultPackage = self.packages.${system}.default; | ||||
|  | ||||
|         devShells.default = pkgs.mkShell { | ||||
|           buildInputs = with pkgs; [ | ||||
|             ghcid | ||||
|             cabal-install | ||||
|             ghc | ||||
|           ]; | ||||
|           inputsFrom = builtins.attrValues self.packages.${system}; | ||||
|         }; | ||||
|         devShell = self.devShells.${system}.default; | ||||
|  | ||||
|         packages.${containerPackageName} = pkgs.dockerTools.buildImage { | ||||
|           name = "HELPS"; | ||||
|  | ||||
|           copyToRoot = pkgs.buildEnv { | ||||
|             name = "image-root"; | ||||
|             paths = [ HELPS ]; | ||||
|             pathsToLink = [ "/bin" ]; | ||||
|           }; | ||||
|           tag = "latest"; | ||||
|           config = { | ||||
|             Cmd = [ | ||||
|               "/bin/Purr" | ||||
|             ]; | ||||
|             WorkingDir = "/app"; | ||||
|             ExposedPorts = { | ||||
|               "3000/tcp" = {}; | ||||
|             }; | ||||
|             extraCommands = '' | ||||
|             ''; | ||||
|           }; | ||||
|         }; | ||||
|       }); | ||||
| } | ||||
							
								
								
									
										54
									
								
								src/Core.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/Core.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| module Core where | ||||
|  | ||||
| import Control.Exception                  (IOException) | ||||
| import Data.Aeson                         (FromJSON, ToJSON) | ||||
| import qualified Data.Text                as T | ||||
| import Database.PostgreSQL.Simple         (Query) | ||||
| import Database.PostgreSQL.Simple.FromRow (FromRow, field, fromRow) | ||||
| import Database.PostgreSQL.Simple.ToRow   (ToRow, toRow) | ||||
| import Database.PostgreSQL.Simple.ToField (ToField) | ||||
| import Effectful | ||||
| import Effectful.Dispatch.Dynamic | ||||
| import Effectful.Error.Static             (Error) | ||||
| import Effectful.FileSystem | ||||
| import GHC.Generics                       (Generic) | ||||
| import Servant hiding                     ((:>)) | ||||
| import qualified Servant                  as S | ||||
|  | ||||
| -- Core data | ||||
| data User = User { userId :: Int, userName :: String} | ||||
|   deriving (Show, Generic) | ||||
|  | ||||
| instance FromRow User where | ||||
|     fromRow = User <$> field <*> field | ||||
| instance ToRow User where | ||||
|     toRow (User uid name) = toRow (uid, name) | ||||
|  | ||||
| instance ToJSON User | ||||
| instance FromJSON User | ||||
|  | ||||
| -- Effects | ||||
| type AppEff = Eff '[Database, FileSystem, Error ServerError, IOE] | ||||
|  | ||||
| data Database :: Effect where | ||||
|   DatabaseRead  :: (Query, Int)    -> Database (Eff es) (Maybe User) | ||||
|   DatabaseWrite :: (Query, String) -> Database (Eff es) () | ||||
|  | ||||
| type instance DispatchOf Database = 'Dynamic | ||||
|  | ||||
| databaseRead  :: (Database :> es, Error ServerError :> es) => (Query, Int) -> Eff es (Maybe User) | ||||
| databaseRead  = send . DatabaseRead | ||||
|  | ||||
| databaseWrite :: (Database :> es, Error ServerError :> es) => (Query, String) -> Eff es () | ||||
| databaseWrite = send . DatabaseWrite | ||||
|  | ||||
| -- Routes | ||||
| type Root     = Get '[PlainText] T.Text | ||||
| type UserList = "user" S.:> Get '[JSON] [User] | ||||
| type UserGet  = "user" S.:> Capture "userId"     Int    S.:> Get  '[JSON] User | ||||
| type UserPost = "user" S.:> ReqBody '[PlainText] String S.:> PostCreated '[PlainText] NoContent | ||||
|  | ||||
| type API = Root | ||||
|       :<|> UserList | ||||
|       :<|> UserGet | ||||
|       :<|> UserPost | ||||
							
								
								
									
										58
									
								
								src/Database.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/Database.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| module Database where | ||||
|  | ||||
| import Core | ||||
|  | ||||
| import Control.Exception          (IOException) | ||||
| import Control.Monad.Catch        (catch) | ||||
| import Data.Aeson                 (ToJSON) | ||||
| import Data.ByteString.Lazy.UTF8  (fromString) | ||||
| import Data.Maybe                 (listToMaybe) | ||||
| import Database.PostgreSQL.Simple  | ||||
| import Effectful | ||||
| import Effectful.Dispatch.Dynamic | ||||
| import Effectful.Error.Static     (Error, HasCallStack, catchError, runErrorNoCallStack, throwError) | ||||
| import Servant hiding             ((:>), throwError) | ||||
|  | ||||
| runDatabaseDebug :: (IOE :> es, Error ServerError :> es) => Eff (Database : es) a -> Eff es a | ||||
| runDatabaseDebug = interpret $ \_ -> \case | ||||
|   DatabaseRead  (statement, values)  -> adapt $ read  statement values | ||||
|   DatabaseWrite (statement, values)  -> adapt $ write statement values | ||||
|   where | ||||
|     read  _ values =  | ||||
|       putStrLn "We just mocked a READ database operation" >>= \_ -> pure $  | ||||
|         Just (User values "Mock User") | ||||
|     write _ values =  | ||||
|       putStrLn $ "We just mocked a WRITE database operation with a user named "  | ||||
|         ++ values | ||||
|  | ||||
| runDatabaseIO :: (IOE :> es, Error ServerError :> es) => Eff (Database : es) a -> Eff es a | ||||
| runDatabaseIO = interpret $ \_ -> \case | ||||
|   DatabaseRead  (statement, values) -> adapt $ read  statement values | ||||
|   DatabaseWrite (statement, values) -> adapt $ write statement values | ||||
|   where | ||||
|     read  :: Query -> Int -> IO (Maybe User) | ||||
|     read statement values  = do | ||||
|       conn <- openConn | ||||
|       user <- query conn statement (Only values) | ||||
|       pure  $ listToMaybe user | ||||
|     write :: Query -> String -> IO () | ||||
|     write statement values = do | ||||
|       conn <- openConn | ||||
|       execute conn statement (Only values) | ||||
|       putStrLn $ "Wrote user to database using statement:\n" ++ show statement | ||||
|     openConn = connectPostgreSQL "host=localhost port=5432 dbname=demo connect_timeout=10" | ||||
|  | ||||
| adapt :: (IOE :> es, Error ServerError :> es) => IO a -> Eff es a | ||||
| adapt m = liftIO m `catch` \(e::IOException) -> | ||||
|   throwError $ ServerError | ||||
|     { errHTTPCode     = 500 | ||||
|     , errReasonPhrase = "Internal Database Error" | ||||
|     , errBody         = fromString $ show e | ||||
|     , errHeaders      = [] | ||||
|     } | ||||
|  | ||||
| queryUser :: Int -> (Query, Int) | ||||
| queryUser userId = ("SELECT id, name FROM users WHERE id = ?;", userId) | ||||
|  | ||||
| writeUser :: String -> (Query, String) | ||||
| writeUser name   = ("INSERT INTO users (name) VALUES (?);", name) | ||||
							
								
								
									
										29
									
								
								src/Handlers.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/Handlers.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| module Handlers where | ||||
|  | ||||
| import Core | ||||
| import Database | ||||
|  | ||||
| import qualified Data.ByteString.Char8    as C | ||||
| import Data.List | ||||
| import qualified Data.Text                as T | ||||
| import Effectful | ||||
| import Effectful.Error.Static             (Error, throwError) | ||||
| import Effectful.FileSystem | ||||
| import Effectful.FileSystem.IO.ByteString as EBS | ||||
| import Servant hiding                     ((:>), throwError) | ||||
| import qualified Servant                  as S | ||||
|  | ||||
| rootHandler :: (Error ServerError :> es) => Eff es T.Text | ||||
| rootHandler = return "Hello, World!" | ||||
|  | ||||
| userListHandler :: (Database :> es, Error ServerError :> es, FileSystem :> es) => Eff es [User] | ||||
| userListHandler = mapM userGetHandler [1, 2, 3] | ||||
|  | ||||
| userGetHandler :: (Database :> es, Error ServerError :> es, FileSystem :> es) => Int -> Eff es User | ||||
| userGetHandler userId = databaseRead (queryUser userId) >>= \mUser ->  | ||||
|   case mUser of | ||||
|     Just a  -> pure a | ||||
|     Nothing -> pure (User 0 "No user found") | ||||
|  | ||||
| userPostHandler :: (Database :> es, Error ServerError :> es, FileSystem :> es) => String -> Eff es NoContent | ||||
| userPostHandler name = databaseWrite (writeUser name) >>= \_ -> return NoContent | ||||
							
								
								
									
										33
									
								
								src/Main.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/Main.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| module Main (main) where | ||||
|  | ||||
| import Core | ||||
| import Database | ||||
| import Handlers | ||||
|  | ||||
| import Control.Monad.Except     (ExceptT (ExceptT)) | ||||
| import Data.List | ||||
| import Effectful | ||||
| import Effectful.Error.Static   (Error, runError, runErrorNoCallStack, throwError) | ||||
| import Effectful.FileSystem | ||||
| import Network.Wai.Handler.Warp (run) | ||||
| import Servant hiding           ((:>), throwError) | ||||
| import qualified Servant        as S | ||||
|  | ||||
| main :: IO () | ||||
| main = run port $ serve proxy app | ||||
|  | ||||
| app :: Server API | ||||
| app = α $ rootHandler | ||||
|      :<|> userListHandler | ||||
|      :<|> userGetHandler | ||||
|      :<|> userPostHandler | ||||
|  | ||||
| α :: ServerT API AppEff -> ServerT API Handler | ||||
| α = hoistServer proxy $ Handler . ExceptT . | ||||
|   runEff . runErrorNoCallStack . runFileSystem . runDatabaseIO | ||||
|  | ||||
| port :: Int | ||||
| port = 8080 | ||||
|  | ||||
| proxy :: Proxy API | ||||
| proxy = Proxy | ||||
		Reference in New Issue
	
	Block a user
	 James Eversole
						James Eversole