I'm working on DynamoDB with Haskell now. I have a hard time updating records dynamically.
For example, I have data like:
data Order
= Order
{ _orderId :: Text
, _orderUserId :: Text
, _orderStatus :: OrderStatus
, _orderAddress :: Text
, _orderEmail :: Email
}
deriving (Show)
Then I would like to have a dynamic query where you can pass fields and values to be updated.
So, in Typescript, it would look like:
update: (payload: Partial<Order>) => Promise<Order>
Then I can do like:
orderRepository.update({orderStatus: "Delivered", orderAddress: "updated address"})
In Haskell, I'm using amazonka-dynamodb library. If I want to update order status I can write like:
data UpdatePayload
= UpdatePayload
{ _payloadOrderStatus :: Maybe OrderStatus
, _payloadOrderAddress :: Maybe Text
, _payloadOrderEmail :: Maybe Email
}
deriving (Show, Generic) -- and ToJSON
newtype Email = Email {
_rawEmail::Text
} deriving (Show, Generic) -- and ToJSON
data OrderStatus = Pending | Paid | Processed | Delivered deriving (Show, Read, Generic, ToJSON)
updateStatus :: (MonadReader Config m, MonadUnliftIO m, MonadCatch m) e => Text -> UpdatePayload -> m ()
updateStatus orderId payload = do
res <- handleReq =<< req
pPrint res
where
req = do
tableName <- asks (^. configTableName)
return
$ updateItem tableName
& uiKey
.~ keys
& uiUpdateExpression
?~ expression
& uiExpressionAttributeNames
.~ attrNames
& uiExpressionAttributeValues
.~ values
keys =
mapFromList [("orderId", attributeValue & avS .~ Just orderId)]
expression = "SET #orderStatus = :orderStatus"
attrNames = mapFromList [("#orderStatus", "orderStatus")]
values = mapFromList [(":orderStatus", attributeValue & avS .~ (tshow <$> payload ^. orderStatus))]
But I don't want to make a new query when I need to update the address for instance.
One way I can think of to make it dynamic is to use a hash map and pass keys and values to update, in the same way as the Typescript example.
If it is for uiExpressionAttributeNames, it would look like
getExpression :: Map Text (Maybe a) -> Text
getExpression = foldl (\exp key -> exp ++ " #" ++ key ++ "= :" ++ key) "SET " . keys
However, for uiExpressionAttributeValues, I need to map each value with pattern matching.
getUpdateValues :: Map Text (Maybe a) -> Map Text AttributeValue
getUpdateValues = foldl helper Map.empty . Map.assocs
where
helper acc ("status", val) = insertMap ":orderStatus" (attributeValue & avS .~ val) acc
helper ...
helper ...
Then, I get compile error because of avS .~ val, where val is expected to be Text but actually is a...
Both getExpression and getUpdateValues look quite ugly and the latter wouldn't be compiled. Is there any cleaner way to solve this problem?