Spock II: Databases and Sessions!
Last week we learned the basics of the the Spock library. We saw how to set up some simple routes. Like Servant, there's a bit of dependent-type machinery with routing. But we didn't need to learn any complex operators. We just needed to match up the number of arguments to our routes. We also saw how to use an application state to persist some data between requests.
This week, we'll add a couple more complex features to our Spock application. First we'll connect to a database. Second, we'll use sessions to keep track of users.
For some more examples of useful Haskell libraries, check out our Production Checklist!
Adding a Database
Last week, we added some global application state. Even with this improvement, our vistor count doesn't persist. When we reset the server, everything goes away, and our users will see a different number. We can change this by adding a database connection to our server. We'll follow the Spock tutorial example and connect to an SQLite database by using Persistent.
If you haven't used Persistent before, take a look at this tutorial in our Haskell Web series! You can also look at our sample code on Github for any of the boilerplate you might be missing. Here's the super simple schema we'll use. Remember that Persistent will give us an auto-incrementing primary key.
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
NameEntry json
name Text
deriving Show
|]
Spock expects us to use a pool of connections to our database when we use it. So let's create one to an SQLite file using createSqlitePool
. We need to run this from a logging monad. While we're at it, we can migrate our database from the main
startup function. This ensures we're using an up-to-date schema:
import Database.Persist.Sqlite (createSqlitePool)
...
main :: IO ()
main = do
ref <- newIORef M.empty
pool <- runStdoutLoggingT $ createSqlitePool "spock_example.db" 5
runStdoutLoggingT $ runSqlPool (runMigration migrateAll) pool
...
Now that we've created this pool, we can pass that to our configuration. We'll use the PCPool
constructor. We're now using an SQLBackend
for our server, so we'll also have to change the type of our router to reflect this:
main :: IO ()
main = do
…
spockConfig <-
defaultSpockCfg EmptySession (PCPool pool) (AppState ref)
runSpock 8080 (spock spockConfig app)
app :: SpockM SqlBackend MySession AppState ()
app = ...
Now we want to update our route action to access the database instead of this map. But first, we'll write a helper function that will allow us to call any SQL action from within our SpockM
monad. It looks like this:
runSQL :: (HasSpock m, SpockConn m ~ SqlBackend)
=> SqlPersistT (LoggingT IO) a -> m a
runSQL action = runQuery $ \conn ->
runStdoutLoggingT $ runSqlConn action conn
At the core of this is the runQuery
function from the Spock library. It works since our router now uses SpockM SqlBackend
instead of SpockM ()
. Now let's write a couple SQL actions we can use. We'll have one performing a lookup by name, and returning the Key
of the first entry that matches, if one exists. Then we'll also have one that will insert a new name and return its key.
fetchByName
:: T.Text
-> SqlPersistT (LoggingT IO) (Maybe Int64)
fetchByName name = (fmap (fromSqlKey . entityKey)) <$>
(listToMaybe <$> selectList [NameEntryName ==. name] [])
insertAndReturnKey
:: T.Text
-> SqlPersistT (LoggingT IO) Int64
insertAndReturnKey name = fromSqlKey <$> insert (NameEntry name)
Now we can use these functions instead of our map!
app :: SpockM SqlBackend MySession AppState ()
app = do
get root $ text "Hello World!"
get ("hello" <//> var) $ \name -> do
existingKeyMaybe <- runSQL $ fetchByName name
visitorNumber <- case existingKeyMaybe of
Nothing -> runSQL $ insertAndReturnKey name
Just i -> return i
text ("Hello " <> name <> ", you are visitor number " <>
T.pack (show visitorNumber))
And voila! We can shutdown our server between runs, and we'll preserve the visitors we've seen!
Tracking Users
Now, using a route to identify our users isn't what we want to do. Anyone can visit any route after all! So for the last modification to the server, we're going to add a small "login" functionality. We'll use the App's session to track what user is currently visiting. Our new flow will look like this:
- We'll change our entry route to
/hello
. - If the user visits this, we'll show a field allowing them to enter their name and log in.
- Pressing the login button will send a post request to our server. This will update the session to match the session ID with the username.
- It will then send the user to the
/home
page, which will greet them and present a logout button. - If they log out, we'll clear the session.
Note that using the session is different from using the app state map that we had in the first part. We share the app state across everyone who uses our server. But the session will contain user-specific references.
Adding a Session
The first step is to change our session type. Once again, we'll use a IORef
wrapper around a map. This time though, we'll use a simple type synonym to simplify things. Here's our type definition and the updated main function.
type MySession = IORef (M.Map T.Text T.Text)
main :: IO ()
main = do
ref <- newIORef M.empty
-- Initialize a reference for the session
sessionRef <- newIORef M.empty
pool <- runStdoutLoggingT $ createSqlitePool "spock_example.db" 5
runStdoutLoggingT $ runSqlPool (runMigration migrateAll) pool
-- Pass that reference!
spockConfig <-
defaultSpockCfg sessionRef (PCPool pool) (AppState ref)
runSpock 8080 (spock spockConfig app)
Updating the Hello Page
Now let's update our "Hello" page. Check out the appendix below for what our helloHTML
looks like. It's a "login" form with a username field and a submit button.
-- Notice we use MySession!
app :: SpockM SqlBackend MySession AppState ()
app = do
get root $ text "Hello World!"
get "hello" $ html helloHTML
...
Now we need to add a handler for the post request to /hello
. We'll use the post
function instead of get
. Now instead of our action taking an argument, we'll extract the post body using the body
function. If our application were more complicated, we would want to use a proper library for Form URL encoding and decoding. But for this small example, we'll use a simple helper decodeUsername
. You can view this helper in the appendix.
app :: SpockM SqlBackend MySession AppState ()
app = do
…
post "hello" $ do
nameEntry <- decodeUsername <$> body
...
Now we want to save this user using our session and then redirect them to the home page. First we'll need to get the session ID and the session itself. We use the functions getSessionId
and readSession
for this. Then we'll want to update our session by associating the name with the session ID. Finally, we'll redirect to home
.
post "hello" $ do
nameEntry <- decodeUsername <$> body
sessId <- getSessionId
currentSessionRef <- readSession
liftIO $ modifyIORef' currentSessionRef $
M.insert sessId (nameEntryName nameEntry)
redirect "home"
The Home Page
Now on the home page, we'll want to check if we've got a user associated with the session ID. If we do, we'll display some text greeting that user (and also display a logout button). Again, we need to invoke getSessionId
and readSession
. If we have no user associated with the session, we'll bounce them back to the hello
page.
get "home" $ do
sessId <- getSessionId
currentSessionRef <- readSession
currentSession <- liftIO $ readIORef currentSessionRef
case M.lookup sessId currentSession of
Nothing -> redirect "hello"
Just name -> html $ homeHTML name
The last piece of functionality we need is to "logout". We'll follow the familiar pattern of getting the session ID and session. This time, we'll change the session by clearing the session key. Then we'll redirect the user back to the hello
page.
post "logout" $ do
sessId <- getSessionId
currentSessionRef <- readSession
liftIO $ modifyIORef' currentSessionRef $ M.delete sessId
redirect "hello"
And now our site tracks our users' sessions! We can access the same page as a different user on different sessions!
Conclusion
This wraps up our exploration of the Spock library! We've done a shallow but wide look at some of the different features Spock has to offer. We saw several different ways to persist information across requests on our server! Connecting to a database is the most important. But using the session is a pretty advanced feature that is quite easy in Spock!
For some more cool examples of Haskell web libraries, take a look at our Web Skills Series! You can also download our Production Checklist for even more ideas!
Appendix - HTML Fragments and Helpers
helloHTML :: T.Text
helloHTML =
"<html>\
\<body>\
\<p>Hello! Please enter your username!\
\<form action=\"/hello\" method=\"post\">\
\Username: <input type=\"text\" name=\"username\"><br>\
\<input type=\"submit\"><br>\
\</form>\
\</body>\
\</html>"
homeHTML :: T.Text -> T.Text
homeHTML name =
"<html><body><p>Hello " <> name <>
"</p>\
\<form action=\"logout\" method=\"post\">\
\<input type=\"submit\" name=\"logout_button\"<br>\
\</form>\
\</body>\
\</html>"
-- Note: 61 -> '=' in ASCII
-- We expect input like "username=christopher"
parseUsername :: B.ByteString -> T.Text
parseUsername input = decodeUtf8 $ B.drop 1 tail_
where
tail_ = B.dropWhile (/= 61) input