How to combine Servant and React Admin, Part 5 : Deleting comments
4 minutes reading timeOverview
We will add a delete method to the REST api and explain how to connect it to React Admin.
The DELETE method
We could start defining the API type like this :
type WebApi =
"comments" :> Get '[JSON] (ListHeaders [Comment])
:<|> "comments" :> Capture "id" Int :> Get '[JSON] Comment
:<|> "comments" :> ReqBody '[JSON] NewComment :> Post '[JSON] Comment
:<|> "comments" :> Capture "id" Int :> Delete '[JSON] NoContent
but by now I am likely fooling nobody anymore, you know that React Admin will want the deleted record to be returned to it else it will tell us JSON not defined
.
So let’s immediately define it correctly :
type WebApi =
"comments" :> Get '[JSON] (ListHeaders [Comment])
:<|> "comments" :> Capture "id" Int :> Get '[JSON] Comment
:<|> "comments" :> ReqBody '[JSON] NewComment :> Post '[JSON] Comment
:<|> "comments" :> Capture "id" Int :> Delete '[JSON] Comment
It’s nothing unexpected, it resembles the GET resource, except for the REST method being Delete
instead of Get
.
The next id property
The real fun comes in the implementation, where, as you recall from our last blog, we took a shortcut to define the unique id that is going to bite us now.
So, let’s first try to rewrite the insert method. The easiest way to keep track of the next id is by actually keeping track of it in the State, which is what we will do. To keep it clean we will extract a data type that contains everything related to our in memory database.
type InMemDb = ([Comment], Int)
After which our state becomes a TVar
wrapped around the InMemDb
type:
newtype State
= State
{ commentsInMemDb :: TVar InMemDb
}
Now, last time during the POST blog, we were leaning heavily on the readTVar and writeTVar methods, mainly to show what is going on. The code became complicated because of that, when you know what’s going on behind the scenes it is clearer to use the methods that work on state directly, like for instance stateTVar, which looks like it was made for our use case!. The insert code then becomes a lot clearer than before :
insertInternal :: NewComment -> InMemDb -> (Comment, InMemDb)
insertInternal newComment curState@(comments, nextId) =
let comment = fromNewComment nextId newComment in
let updatedComments = List.insert comment comments in
(comment, (updatedComments, nextId + 1))
insertComment :: NewComment -> AppM Comment
insertComment newComment = do
commentsDbTVar <- asks commentsInMemDb
liftIO $ atomically $ stateTVar commentsDbTVar $ insertInternal newComment
Makes for a lot more readable code. The insertComment
function handles the state stuff and the insertInternal
becomes a very straightforward, testable function.
Deleting, with this way of coding, becomes easy to write, although a little bit more complicated due to the fact that we have to take failure into account in our code:
deleteInternal :: Int -> InMemDb -> (Maybe Comment, InMemDb)
deleteInternal idToDelete curState@(comments, nextId) =
let maybeCommentToDelete = List.find (\comment -> Comment.id comment == idToDelete) comments
in maybe
(Nothing, curState)
( \commentToDelete ->
let prunedComments = List.delete commentToDelete comments
in (Just commentToDelete, (prunedComments, nextId))
)
maybeCommentToDelete
deleteComment :: Int -> AppM Comment
deleteComment idToDelete = do
commentsDbTVar <- asks commentsInMemDb
maybeDeletedComment <-
liftIO $ atomically $
stateTVar commentsDbTVar $ deleteInternal idToDelete
maybe (throwError err404) return maybeDeletedComment
In case no comment has been found we should of course not update any state. In case it has been found we should remove the comment and update the state. We will return a 404 in case the id supplied hasn’t been found.
That concludes our backend, now for the front end part.
React Admin and delete
The great thing about React Admin is that the UI is already totally ready to perform DELETEs. You can click on one of the checkboxes next to a comment
in the list overview and a Delete
icon will appear. Clicking that will send the delete request to our REST server and all will be fine.
Of course it won’t, in our console we see our now-familiar NetworkError, related to CORS again. Who would have thought.
The problem is easy to spot :
Method requested in Access-Control-Request-Method of CORS request is not supported; requested: DELETE; supported are GET, POST, HEAD, OPTIONS.
We could have known. Alright, let’s also allow the DELETE
method to be used, we expand the CORS config with HttpTypes.methodDelete
:
corsConfig :: Middleware
corsConfig = cors (const $ Just policy)
where
policy =
simpleCorsResourcePolicy
{ corsExposedHeaders = Just ["X-Total-Count"],
corsMethods = [HttpTypes.methodGet, HttpTypes.methodPost, HttpTypes.methodHead, HttpTypes.methodOptions, HttpTypes.methodDelete],
corsRequestHeaders = [HttpTypes.hContentType]
}
and hooray, we can delete. So now we can list comments, we can read a single comment, we can add one and we can delete one. The one thing missing now is updating comments. Before we continue with that, we will first take a deeper dive into Servant and try to explain away some of the black magic that we took for granted up until now. The code can be found here.