1-- a simple example of a blog schema
2{-# LANGUAGE DeriveAnyClass, DeriveGeneric, OverloadedStrings #-}
4import ProjectM36.Client
6import ProjectM36.Relation
7import ProjectM36.Tupleable
8import ProjectM36.Atom (relationForAtom)
9import ProjectM36.Tuple (atomForAttributeName)
14import qualified Data.Text as T
15import qualified Data.Text.Lazy as TL
17import Data.Time.Calendar
22import Control.Monad (when, forM_)
25import Text.Blaze.Html5 (h1, h2, h3, p, form, input, (!), toHtml, Html, a, toValue, hr, textarea)
26import Text.Blaze.Html5.Attributes (name, href, type_, method, action, value)
27import Text.Blaze.Html.Renderer.Text
28import Control.Monad.IO.Class (liftIO)
29import Network.HTTP.Types.Status
30import Data.Time.Format
32--define your data types
37 category :: Category --note that this type is an algebraic data type
39 deriving (Generic, Show) --derive Generic so that Tupleable can use default instances
41--instantiate default Tupleable instances
42instance Tupleable Blog
44data Comment = Comment {
46 commentTime :: UTCTime,
48 } deriving (Generic, Show)
50instance Tupleable Comment
52data Category = Food | Cats | Photos | Other T.Text -- note that this data type could not be represented by an "enumeration" as found in SQL databases
53 deriving (Atomable, Eq, Show, NFData, Binary, Generic) -- derive Atomable so that values of this type can be stored as a database value
55-- add some short-hand error handling- your application should have proper handling
56handleIOError :: Show e => IO (Either e a) -> IO a
61handleError :: Show e => Either e a -> IO a
62handleError eErr = case eErr of
63 Left err -> print err >> error "Died due to errors."
66handleIOErrors :: Show e => IO [Either e a] -> IO [a]
70 [] -> pure (rights eErrs)
71 err:_ -> handleError (Left err)
75 --connect to the database
76 let connInfo = InProcessConnectionInfo NoPersistence emptyNotificationCallback []
77 conn <- handleIOError $ connectProjectM36 connInfo
79 sessionId <- handleIOError $ createSessionAtHead conn "master"
81 createSchema sessionId conn
82 insertSampleData sessionId conn
83 --create the web routes
85 S.get "/" (listBlogs sessionId conn)
86 S.get "/blog/:blogid" (showBlogEntry sessionId conn)
87 S.post "/comment" (addComment sessionId conn)
89--define the schema with the new Category atom (data) type, blog relvar, a comment relvar, and a foreign key relationship between them
90createSchema :: SessionId -> Connection -> IO ()
91createSchema sessionId conn = do
92 _ <- handleIOErrors $ mapM (executeDatabaseContextExpr sessionId conn) [
93 toAddTypeExpr (Proxy :: Proxy Category),
94 toDefineExpr (Proxy :: Proxy Blog) "blog",
95 toDefineExpr (Proxy :: Proxy Comment) "comment",
96 databaseContextExprForForeignKey "blog_comment" ("comment", ["blogTitle"]) ("blog", ["title"]),
97 databaseContextExprForUniqueKey "blog" ["title"]
101--create some sample values and insert them into the database's relation variables
102insertSampleData :: SessionId -> Connection -> IO ()
103insertSampleData sessionId conn = do
104 let blogs = [Blog { title = "Haskell Lenses",
105 entry = "I wear Haskell rose-colored lenses.",
106 stamp = UTCTime (fromGregorian 2017 5 8) (secondsToDiffTime 1000),
108 Blog { title = "Haskell Monad Analogy",
109 entry = "Monads are like burritos going through intestines.",
110 stamp = UTCTime (fromGregorian 2017 6 10) (secondsToDiffTime 2000),
113 comments = [Comment { blogTitle = "Haskell Lenses",
114 commentTime = UTCTime (fromGregorian 2017 7 8) (secondsToDiffTime 3000),
115 contents = "You suck!" },
116 Comment {blogTitle = "Haskell Lenses",
117 commentTime = UTCTime (fromGregorian 2017 7 9) (secondsToDiffTime 2000),
118 contents = "I find your ideas intriguing and would like to subscribe to your newsletter."}
120 insertBlogsExpr <- handleError $ toInsertExpr blogs "blog"
121 handleIOError $ executeDatabaseContextExpr sessionId conn insertBlogsExpr
123 insertCommentsExpr <- handleError $ toInsertExpr comments "comment"
124 handleIOError $ executeDatabaseContextExpr sessionId conn insertCommentsExpr
126-- handle relational errors with scotty
127handleWebError :: Either RelationalError a -> ActionM a
128handleWebError (Left err) = render500 (toHtml (show err)) >> pure (error "bad")
129handleWebError (Right v) = pure v
131-- show a page with all the blog entries
132listBlogs :: SessionId -> Connection -> ActionM ()
133listBlogs sessionId conn = do
134 eRel <- liftIO $ executeRelationalExpr sessionId conn (RelationVariable "blog" ())
136 Left err -> render500 (toHtml (show err))
138 blogs <- liftIO (toList blogRel) >>= mapM (handleWebError . fromTuple) :: ActionM [Blog]
139 let sortedBlogs = sortBy (\b1 b2 -> stamp b1 `compare` stamp b2) blogs
140 html . renderHtml $ do
142 forM_ sortedBlogs $ \blog -> a ! href (toValue $ "/blog/" <> title blog) $ h2 (toHtml (title blog))
144render500 :: Html -> ActionM ()
146 html . renderHtml $ do
147 h1 "Internal Server Error"
149 status internalServerError500
151--display one blog post along with its comments
152showBlogEntry :: SessionId -> Connection -> ActionM ()
153showBlogEntry sessionId conn = do
154 blogid <- param "blogid"
155 --query the database to return the blog entry with a relation-valued attribute of the associated comments
156 let blogRestrictionExpr = AttributeEqualityPredicate "title" (NakedAtomExpr (TextAtom blogid))
157 extendExpr = AttributeExtendTupleExpr "comments" (RelationAtomExpr commentsRestriction)
158 commentsRestriction = Restrict
159 (AttributeEqualityPredicate "blogTitle" (AttributeAtomExpr "title"))
160 (RelationVariable "comment" ())
161 eRel <- liftIO $ executeRelationalExpr sessionId conn (Extend extendExpr
164 (RelationVariable "blog" ())))
165 let render = html . renderHtml
166 formatStamp = formatTime defaultTimeLocale (iso8601DateFormat (Just "%H:%M:%S"))
168 Left err -> render500 (toHtml (show err))
169 --handle successful query execution
170 Right rel -> case singletonTuple rel of
171 Nothing -> do --no results for this blog id
172 render (h1 "No such blog post")
174 Just blogTuple -> case fromTuple blogTuple of --just one blog post found- it's a match!
175 Left err -> render500 (toHtml (show err))
177 --extract comments for the blog
178 commentsAtom <- handleWebError (atomForAttributeName "comments" blogTuple)
179 commentsRel <- handleWebError (relationForAtom commentsAtom)
180 comments <- liftIO (toList commentsRel) >>= mapM (handleWebError . fromTuple) :: ActionM [Comment]
181 let commentsSorted = sortBy (\c1 c2 -> commentTime c1 `compare` commentTime c2) comments
184 h1 (toHtml (title blog))
185 p (toHtml ("Posted at " <> formatStamp (stamp blog) <> " under " <> show (category blog)))
186 p (toHtml (entry blog))
190 forM_ commentsSorted $ \comment -> do
191 p (toHtml ("Commented at " <> formatStamp (commentTime comment)))
192 p (toHtml (contents comment))
193 when (null comments) (p "No comments.")
196 form ! method "POST" ! action "/comment" $ do
197 input ! type_ "hidden" ! name "blogid" ! value (toValue blogid)
198 textarea ! name "contents" $ ""
199 input ! type_ "submit"
201--add a comment to a blog post
202addComment :: SessionId -> Connection -> ActionM ()
203addComment sessionId conn = do
204 blogid <- param "blogid"
205 commentText <- param "contents"
206 now <- liftIO getCurrentTime
208 case toInsertExpr [Comment {blogTitle = blogid,
210 contents = commentText }] "comment" of
211 Left err -> handleWebError (Left err)
212 Right insertExpr -> do
213 eRet <- liftIO (withTransaction sessionId conn (executeDatabaseContextExpr sessionId conn insertExpr) (commit sessionId conn))
215 Left err -> handleWebError (Left err)
217 redirect (TL.fromStrict ("/blog/" <> blogid))
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list