PATCH path rewriten in new style

This commit is contained in:
Ruslan Talpa
2015-10-27 16:27:44 +02:00
parent f7e6005087
commit 482a43d722
3 changed files with 98 additions and 43 deletions
+78 -26
View File
@@ -22,7 +22,7 @@ import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as CSV import qualified Data.Csv as CSV
import Data.Functor.Identity import Data.Functor.Identity
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as HM
import Data.List (find, sortBy, delete, transpose) import Data.List (find, sortBy, delete, transpose)
import Data.Maybe (fromMaybe, fromJust, isJust, isNothing, mapMaybe) import Data.Maybe (fromMaybe, fromJust, isJust, isNothing, mapMaybe)
import Data.Ord (comparing) import Data.Ord (comparing)
@@ -31,6 +31,7 @@ import qualified Data.Set as S
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Text (Text, replace, strip) import Data.Text (Text, replace, strip)
import Data.Tree import Data.Tree
import qualified Data.Map as M
--import Data.Foldable (forlrM) --import Data.Foldable (forlrM)
import Text.Parsec.Error import Text.Parsec.Error
@@ -169,10 +170,10 @@ app dbstructure conf reqBody req =
"You must speficy all and only primary keys as params" "You must speficy all and only primary keys as params"
else do else do
let tableCols = map (cs . colName) $ filter (filterCol schema table) allCols let tableCols = map (cs . colName) $ filter (filterCol schema table) allCols
cols = map cs $ M.keys obj cols = map cs $ HM.keys obj
if S.fromList tableCols == S.fromList cols if S.fromList tableCols == S.fromList cols
then do then do
let vals = M.elems obj let vals = HM.elems obj
H.unitEx $ iffNotT H.unitEx $ iffNotT
(whereT qt qq $ update qt cols vals) (whereT qt qq $ update qt cols vals)
(insertSelect qt cols vals) (insertSelect qt cols vals)
@@ -183,25 +184,49 @@ app dbstructure conf reqBody req =
else responseLBS status400 [] else responseLBS status400 []
"You must specify all columns in PUT request" "You must specify all columns in PUT request"
([table], "PATCH") -> ([table], "PATCH") -> do
handleJsonObj reqBody $ \obj -> do let echoRequested = hasPrefer "return=representation"
let qt = qualify table case queries of
up = returningStarT Left e -> return $ responseLBS status400 [("Content-Type", "application/json")] $ cs e
. whereT qt qq Right (qu, qs) -> do
$ update qt (map cs $ M.keys obj) (M.elems obj) let q = B.Stmt
patch = withT up "t" $ B.Stmt (
"select count(t), array_to_json(array_agg(row_to_json(t)))::character varying" wrapQuery qu [
V.empty True countF,
if echoRequested
then
case contentType of
"text/csv" -> asCsvF
_ -> asJsonF
else "null"
row <- H.maybeEx patch ] qs Nothing
let (queryTotal, body) = )
fromMaybe (0 :: Int, Just "" :: Maybe Text) row V.empty True
r = contentRangeH 0 (queryTotal-1) (Just queryTotal)
echoRequested = hasPrefer "return=representation" row <- H.maybeEx q
s = case () of _ | queryTotal == 0 -> status404 let (queryTotal, bodyRaw) = fromMaybe (0::Int, Just "" :: Maybe BL.ByteString) row
| echoRequested -> status200 body = fromMaybe "[]" bodyRaw
| otherwise -> status204 r = contentRangeH 0 (queryTotal-1) (Just queryTotal)
return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else "" s = case () of _ | queryTotal == 0 -> status404
| echoRequested -> status200
| otherwise -> status204
--return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else ""
return $ responseLBS s
[
contentTypeH,
r
]
$ if echoRequested then body else ""
where
res = parsePatchRequest table req reqBody
updateApiRequest = fst <$> res
updateQuery = requestToQuery schema <$> updateApiRequest
selectApiRequest = (snd <$> res) >>= augumentRequestWithJoin schema (fakeSourceRelations ++ allRels)
selectQuery = requestToQuery schema <$> selectApiRequest
queries = (,) <$> updateQuery <*> selectQuery
fakeSourceRelations = mapMaybe (toSourceRelation table) allRels
([table], "DELETE") -> do ([table], "DELETE") -> do
let qt = qualify table let qt = qualify table
@@ -221,7 +246,7 @@ app dbstructure conf reqBody req =
if exists if exists
then do then do
let call = B.Stmt "select " V.empty True <> let call = B.Stmt "select " V.empty True <>
asJson (callProc qi $ fromMaybe M.empty (decode reqBody)) asJson (callProc qi $ fromMaybe HM.empty (decode reqBody))
bodyJson :: Maybe (Identity Value) <- H.maybeEx call bodyJson :: Maybe (Identity Value) <- H.maybeEx call
returnJWT <- doesProcReturnJWT schema proc returnJWT <- doesProcReturnJWT schema proc
return $ responseLBS status200 [jsonH] return $ responseLBS status200 [jsonH]
@@ -360,6 +385,32 @@ formatParserError e = cs $ encode $ object [
details = strip $ replace "\n" " " $ cs details = strip $ replace "\n" " " $ cs
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e) $ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
parsePatchRequest :: NodeName -> Request -> BL.ByteString -> Either Text (ApiRequest, ApiRequest)
parsePatchRequest rootTableName httpRequest reqBody =
(,) <$> updateApiRequest <*> returnApiRequest
where
updateApiRequest = Node <$> apiNode <*> pure []
apiNode = (,) <$> (Update rootTableName <$> setWith <*> cond) <*> pure (rootTableName, Nothing)
flds = join $ first formatParserError . mapM (parseField . cs) <$> (fst <$> parsed)
vals = head.snd <$> parsed -- TODO! cheack if head is safe here
parseField f = parse pField ("failed to parse field <<"++f++">>") f
parsed :: Either Text ([Text],[[Value]])
parsed = parseRequestBody isCsv reqBody
returnSingle = (==1) . length . snd <$> parsed
isSingle = either (const False) id returnSingle
setWith = if isSingle
then M.fromList <$> (zip <$> flds <*> vals)
else Left "Expecting a sigle CSV line with header or a JSON object"
hdrs = requestHeaders httpRequest
lookupHeader = flip lookup hdrs
--rootTableName = cs $ head $ pathInfo httpRequest -- TODO unsafe head
isCsv = lookupHeader "Content-Type" == Just csvMT
qParams = queryParams httpRequest
selectFilters = filter (( '.' `elem` ) . fst) $ whereFilters qParams -- there can be no filters on the root table whre we are doing insert
updateFilters = filter (not . ( '.' `elem` ) . fst) $ whereFilters qParams -- update filters can be only on the root table
returnApiRequest = buildSelectApiRequest sourceSubqueryName (selectStr qParams) selectFilters (orderStr qParams)
cond = first formatParserError $ map snd <$> mapM pRequestFilter updateFilters
-- quite ugly return type -- quite ugly return type
parsePostRequest :: NodeName -> Request -> BL.ByteString -> Either Text ((Bool, ApiRequest), ApiRequest) parsePostRequest :: NodeName -> Request -> BL.ByteString -> Either Text ((Bool, ApiRequest), ApiRequest)
parsePostRequest rootTableName httpRequest reqBody = parsePostRequest rootTableName httpRequest reqBody =
@@ -378,7 +429,8 @@ parsePostRequest rootTableName httpRequest reqBody =
--rootTableName = cs $ head $ pathInfo httpRequest -- TODO unsafe head --rootTableName = cs $ head $ pathInfo httpRequest -- TODO unsafe head
isCsv = lookupHeader "Content-Type" == Just csvMT isCsv = lookupHeader "Content-Type" == Just csvMT
qParams = queryParams httpRequest qParams = queryParams httpRequest
returnApiRequest = buildSelectApiRequest sourceSubqueryName (selectStr qParams) (whereFilters qParams) (orderStr qParams) filters = filter (( '.' `elem` ) . fst) $ whereFilters qParams -- there can be no filters on the root table whre we are doing insert
returnApiRequest = buildSelectApiRequest sourceSubqueryName (selectStr qParams) filters (orderStr qParams)
parseRequestBody :: Bool -> BL.ByteString -> Either Text ([Text],[[Value]]) parseRequestBody :: Bool -> BL.ByteString -> Either Text ([Text],[[Value]])
@@ -422,11 +474,11 @@ convertJson v = (,) <$> (header <$> normalized) <*> (vals <$> normalized)
header = map fst header = map fst
groupByKey :: Value -> Either String [(Text,[Value])] groupByKey :: Value -> Either String [(Text,[Value])]
groupByKey (Array a) = M.toList . foldr (M.unionWith (++)) (M.fromList []) <$> maps groupByKey (Array a) = HM.toList . foldr (HM.unionWith (++)) (HM.fromList []) <$> maps
where where
maps :: Either String [M.HashMap Text [Value]] maps :: Either String [HM.HashMap Text [Value]]
maps = mapM getElems $ V.toList a maps = mapM getElems $ V.toList a
getElems (Object o) = Right $ M.map (:[]) o getElems (Object o) = Right $ HM.map (:[]) o
getElems _ = Left invalidMsg getElems _ = Left invalidMsg
groupByKey _ = Left invalidMsg groupByKey _ = Left invalidMsg
+17 -14
View File
@@ -12,8 +12,9 @@ import Control.Applicative
import Data.Tree import Data.Tree
import PostgREST.PgQuery (fromQi, pgFmtCondition, pgFmtSelectItem, import PostgREST.PgQuery (fromQi, pgFmtCondition, pgFmtSelectItem,
pgFmtIdent, pgFmtCondition, pgFmtIdent, pgFmtCondition,
insertableValue, orderF, sourceSubqueryName) insertableValue, orderF, sourceSubqueryName, pgFmtJsonPath)
import PostgREST.Types import PostgREST.Types
import qualified Data.Map as M
--import qualified Data.Vector as V (empty) --import qualified Data.Vector as V (empty)
--import qualified Hasql.Backend as B --import qualified Hasql.Backend as B
@@ -87,6 +88,9 @@ addJoinConditions schema (Node (query, (t, r)) forest) =
-- fn (Filter{value=VForeignKey _ _}) = False -- fn (Filter{value=VForeignKey _ _}) = False
--requestToQuery :: Text -> ApiRequest -> PStmt --requestToQuery :: Text -> ApiRequest -> PStmt
emptyOnNull :: Text -> [a] -> Text
emptyOnNull val x = if null x then "" else val
requestToQuery :: Text -> ApiRequest -> Text requestToQuery :: Text -> ApiRequest -> Text
requestToQuery schema (Node (Select colSelects tbls conditions ord, (mainTbl, _)) forest) = requestToQuery schema (Node (Select colSelects tbls conditions ord, (mainTbl, _)) forest) =
--orderT (fromMaybe [] ord) query --orderT (fromMaybe [] ord) query
@@ -114,7 +118,7 @@ requestToQuery schema (Node (Select colSelects tbls conditions ord, (mainTbl, _)
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions, ("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions,
orderF (fromMaybe [] ord) orderF (fromMaybe [] ord)
] ]
emptyOnNull val x = if null x then "" else val
(withs, selects) = foldr getQueryParts ([],[]) forest (withs, selects) = foldr getQueryParts ([],[]) forest
getQueryParts :: Tree ApiNode -> ([Text], [Text]) -> ([Text], [Text]) getQueryParts :: Tree ApiNode -> ([Text], [Text]) -> ([Text], [Text])
getQueryParts (Node n@(_, (table, Just (Relation {relType=Child}))) forst) (w,s) = (w,sel:s) getQueryParts (Node n@(_, (table, Just (Relation {relType=Child}))) forst) (w,s) = (w,sel:s)
@@ -149,9 +153,7 @@ requestToQuery schema (Node (Select colSelects tbls conditions ord, (mainTbl, _)
requestToQuery schema (Node (Insert _ flds vals, (mainTbl, _)) _) = requestToQuery schema (Node (Insert _ flds vals, (mainTbl, _)) _) =
query query
where where
--query = B.Stmt qStr V.empty True
qi = QualifiedIdentifier schema mainTbl qi = QualifiedIdentifier schema mainTbl
--qStr = Data.Text.unwords [
query = Data.Text.unwords [ query = Data.Text.unwords [
"INSERT INTO ", fromQi qi, "INSERT INTO ", fromQi qi,
" (" <> intercalate ", " (map (pgFmtIdent . fst) flds) <> ") ", " (" <> intercalate ", " (map (pgFmtIdent . fst) flds) <> ") ",
@@ -164,13 +166,14 @@ requestToQuery schema (Node (Insert _ flds vals, (mainTbl, _)) _) =
), ),
"RETURNING " <> fromQi qi <> ".*" "RETURNING " <> fromQi qi <> ".*"
] ]
-- ("insert into " <> fromQi t <> " (" <> requestToQuery schema (Node (Update _ setWith conditions, (mainTbl, _)) _) =
-- T.intercalate ", " (V.toList $ V.map pgFmtIdent cols) <> query
-- ") values " where
-- <> T.intercalate ", " qi = QualifiedIdentifier schema mainTbl
-- (V.toList $ V.map (\v -> "(" query = Data.Text.unwords [
-- <> T.intercalate ", " (V.toList $ V.map insertableValue v) "UPDATE ", fromQi qi,
-- <> ")" " SET " <> intercalate ", " (map formatSet (M.toList setWith)) <> " ",
-- ) vals ("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions,
-- ) "RETURNING " <> fromQi qi <> ".*"
-- <> " returning row_to_json(" <> fromQi t <> ".*)") ]
formatSet ((c, jp), v) = pgFmtIdent c <> pgFmtJsonPath jp <> " = " <> insertableValue v
+3 -3
View File
@@ -3,7 +3,7 @@ import Data.Text
import Data.Tree import Data.Tree
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.Aeson import Data.Aeson
--import Data.Map import Data.Map
data DbStructure = DbStructure { data DbStructure = DbStructure {
tables :: [Table] tables :: [Table]
@@ -80,8 +80,8 @@ type NodeName = Text
type SelectItem = (Field, Maybe Cast) type SelectItem = (Field, Maybe Cast)
type Path = [Text] type Path = [Text]
data Query = Select { select::[SelectItem], from::[Text], where_::[Filter], order::Maybe [OrderTerm] } data Query = Select { select::[SelectItem], from::[Text], where_::[Filter], order::Maybe [OrderTerm] }
| Insert { into::Text, fields::[Field], values::[[Value]] } deriving (Show, Eq) | Insert { into::Text, fields::[Field], values::[[Value]] }
-- | Update { into::Text, set::Map Field Value, where_::[Filter] } deriving (Show, Eq) | Update { into::Text, set::Map Field Value, where_::[Filter] } deriving (Show, Eq)
data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq) data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq)
type ApiNode = (Query, (NodeName, Maybe Relation)) type ApiNode = (Query, (NodeName, Maybe Relation))
type ApiRequest = Tree ApiNode type ApiRequest = Tree ApiNode