Search code examples
jsonhaskellhaskell-persistentservant

Represent Foreign Key Relationship in JSON using Servant and Persistent


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

Solution

  • 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
    

    Gross, more generic version

    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.