This morning I was following along with this interesting tutorial on using Servant to build a simple API server.
At the end of the tutorial, the author suggests adding a Blog type, so I figured I would give it a shot, but I got stuck trying to implement and serialize a foreign key relationship that extends upon the logic in the tutorial (perhaps an important disclosure here: I'm new to both Servant and Persistent).
Here are my Persistent definitions (I added the Post
):
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
name String
email String
deriving Show
Post
title String
user UserId
summary String
content String
deriving Show
|]
The tutorial builds a separate Person
data type for the Servant API, so I added one called Article
as well:
-- API Data Types
data Person = Person
{ name :: String
, email :: String
} deriving (Eq, Show, Generic)
data Article = Article
{ title :: String
, author :: Person
, summary :: String
, content :: String
} deriving (Eq, Show, Generic)
instance ToJSON Person
instance FromJSON Person
instance ToJSON Article
instance FromJSON Article
userToPerson :: User -> Person
userToPerson User{..} = Person { name = userName, email = userEmail }
Now, however, when I attempt to create a function that turns a Post
into an Article
, I get stuck trying to deal with the User
foreign key:
postToArticle :: Post -> Article
postToArticle Post{..} = Article {
title = postTitle
, author = userToPerson postUser -- this fails
, summary = postSummary
, content = postContent
}
I tried a number of things, but the above seemed to be close to the direction I'd like to go in. It doesn't compile, however, due to the following the error:
Couldn't match expected type ‘User’
with actual type ‘persistent-2.2.2:Database.Persist.Class.PersistEntity.Key
User’
In the first argument of ‘userToPerson’, namely ‘postUser’
In the ‘author’ field of a record
Ultimately, I'm not really sure what a PersistEntity.Key User
really is and my errant googling has not gotten me much closer.
How do I deal with this foreign-key relationship?
Working Version
Edited with an answer thanks to haoformayor
postToArticle :: MonadIO m => Post -> SqlPersistT m (Maybe Article)
postToArticle Post{..} = do
authorMaybe <- selectFirst [UserId ==. postUser] []
return $ case authorMaybe of
Just (Entity _ author) ->
Just Article {
title = postTitle
, author = userToPerson author
, summary = postSummary
, content = postContent
}
Nothing ->
Nothing
For some record type r
, Entity r
is the datatype containing Key r
and r
. You can think of it as a dollied-up tuple (Key r, r)
.
(You might wonder what Key r
is. Different backends have different kinds of Key r
. For Postgres it'll be a 64-bit integer. For MongoDB there are object IDs. The documentation goes into more detail. It's an abstraction that allows Persistent to support multiple datastores.)
Your problem here is that you have a Key User
. Our strategy will be to get you an Entity User
, from which we'll be able to pull out a User
. Fortunately, going from Key User
to Entity User
is easy with a selectFirst
– a trip to the database. And going from Entity User
to User
is one pattern match.
postToArticle :: MonadIO m => Post -> SqlPersistT m (Maybe Article)
postToArticle Post{..} = do
authorMaybe <- selectFirst [UserId ==. postUser] []
return $ case authorMaybe of
Just (Entity _ author) ->
Article {
title = postTitle
, author = author
, summary = postSummary
, content = postContent
}
Nothing ->
Nothing
We assumed a SQL backend above, but that function also has the more generic type
postToArticle ::
(MonadIO m, PersistEntity val, backend ~ PersistEntityBackend val) =>
Post -> ReaderT backend m (Maybe Article)
Which you might need if you're not using a SQL backend.