Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
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 PCPoolconstructor. 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 arunSQL 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) Int64insertAndReturnKey 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 bodyfunction. 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.TexthelloHTML = "<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.TexthomeHTML 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.TextparseUsername input = decodeUtf8 $ B.drop 1 tail_ where tail_ = B.dropWhile (/= 61) input
Spock II: Databases and Sessions! was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.