App.hs refactor

This commit is contained in:
Ruslan Talpa
2015-11-16 12:26:55 +02:00
parent 75646247f2
commit 0c3545fa09
3 changed files with 104 additions and 105 deletions
+100 -104
View File
@@ -68,20 +68,30 @@ import PostgREST.Auth (tokenJWT)
import Prelude import Prelude
app :: DbStructure -> AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Tx P.Postgres s Response
app dbStructure conf reqBody req = app dbStructure conf reqBody req =
case (path, verb) of case (path, verb) of
([table], "OPTIONS") -> do
let cols = filter (filterCol schema table) $ dbColumns dbStructure
pkeys = map pkName $ filter (filterPk schema table) allPrKeys
body = encode (TableOptions cols pkeys)
filterCol :: Schema -> TableName -> Column -> Bool
filterCol sc tb (Column{colTable=Table{tableSchema=s, tableName=t}}) = s==sc && t==tb
filterCol _ _ _ = False
([table], "GET") -> return $ responseLBS status200 [jsonH, allOrigins] $ cs body
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error" ([table], _) ->
else case request of
case request of Left e -> return $ responseLBS status400 [jsonH] $ cs e
Left e -> return $ responseLBS status400 [jsonH] $ cs e Right (selectQuery, Nothing) -> -- should we do sanity check to make sure its a GET request?
Right (selectQuery, _, _) -> do if range == Just emptyRange
let q = B.Stmt (createStatement selectQuery Nothing True range [] (not $ hasPrefer "count=none") isCsv) V.empty True then return $ responseLBS status416 [] "HTTP Range error"
else do
let q = createReadStatement selectQuery range (not $ hasPrefer "count=none") isCsv
row <- H.maybeEx q row <- H.maybeEx q
let (tableTotal, queryTotal, _ , body) = extractQueryResult row let (tableTotal, queryTotal, _ , body) = extractQueryResult row
frm = fromMaybe 0 $ rangeOffset <$> range
to = frm+queryTotal-1 to = frm+queryTotal-1
contentRange = contentRangeH frm to tableTotal contentRange = contentRangeH frm to tableTotal
status = rangeStatus frm to tableTotal status = rangeStatus frm to tableTotal
@@ -97,48 +107,37 @@ app dbStructure conf reqBody req =
if Prelude.null canonical then "" else "?" <> cs canonical if Prelude.null canonical then "" else "?" <> cs canonical
) )
] (fromMaybe "[]" body) ] (fromMaybe "[]" body)
where Right (selectQuery, Just (mutateQuery, isSingle)) ->
frm = fromMaybe 0 $ rangeOffset <$> range case verb of
"POST" -> do
([table], "POST") -> let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
case request of q = createWriteStatement selectQuery mutateQuery isSingle echoRequested pKeys isCsv
Left e -> return $ responseLBS status400 [jsonH] $ cs e row <- H.maybeEx q
Right (selectQuery, mutateQuery, isSingle) -> do let (_, _, location, body) = extractQueryResult row
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself? return $ responseLBS status201
q = B.Stmt (createStatement selectQuery (Just (mutateQuery, isSingle)) echoRequested Nothing pKeys False isCsv) V.empty True [
row <- H.maybeEx q contentTypeH,
let (_, _, location, body) = extractQueryResult row (hLocation, "/" <> cs table <> "?" <> cs (fromMaybe "" location))
return $ responseLBS status201 ]
[ $ if echoRequested then fromMaybe "[]" body else ""
contentTypeH, "PATCH" -> do
(hLocation, "/" <> cs table <> "?" <> cs (fromMaybe "" location)) let q = createWriteStatement selectQuery mutateQuery False echoRequested [] isCsv
] row <- H.maybeEx q
$ if echoRequested then fromMaybe "[]" body else "" let (_, queryTotal, _, body) = extractQueryResult row
r = contentRangeH 0 (queryTotal-1) (Just queryTotal)
([_], "PATCH") -> s = case () of _ | queryTotal == 0 -> status404
case request of | echoRequested -> status200
Left e -> return $ responseLBS status400 [jsonH] $ cs e | otherwise -> status204
Right (selectQuery, mutateQuery, _) -> do return $ responseLBS s [contentTypeH, r]
let q = B.Stmt (createStatement selectQuery (Just (mutateQuery, False)) echoRequested Nothing [] False isCsv) V.empty True $ if echoRequested then fromMaybe "[]" body else ""
row <- H.maybeEx q "DELETE" -> do
let (_, queryTotal, _, body) = extractQueryResult row let q = createWriteStatement selectQuery mutateQuery False False [] isCsv
r = contentRangeH 0 (queryTotal-1) (Just queryTotal) row <- H.maybeEx q
s = case () of _ | queryTotal == 0 -> status404 let (_, queryTotal, _, _) = extractQueryResult row
| echoRequested -> status200 return $ if queryTotal == 0
| otherwise -> status204 then responseLBS status404 [] ""
return $ responseLBS s [contentTypeH, r] else responseLBS status204 [("Content-Range", "*/"<> cs (show queryTotal))] ""
$ if echoRequested then fromMaybe "[]" body else "" _ -> return $ responseLBS status404 [] ""
([_], "DELETE") ->
case request of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (selectQuery, mutateQuery, _) -> do
let q = B.Stmt (createStatement selectQuery (Just (mutateQuery, False)) False Nothing [] True isCsv) V.empty True
row <- H.maybeEx q
let (_, queryTotal, _, _) = extractQueryResult row
return $ if queryTotal == 0
then responseLBS status404 [] ""
else responseLBS status204 [("Content-Range", "*/"<> cs (show queryTotal))] ""
(["rpc", proc], "POST") -> do (["rpc", proc], "POST") -> do
let qi = QualifiedIdentifier schema (cs proc) let qi = QualifiedIdentifier schema (cs proc)
@@ -160,26 +159,15 @@ app dbStructure conf reqBody req =
-- check that arg names are all specified -- check that arg names are all specified
-- select * from public.proc(a := "foo"::undefined) where whereT limit limitT -- select * from public.proc(a := "foo"::undefined) where whereT limit limitT
([], _) -> do ([], "GET") -> do -- this should be a GET request only
body <- encode <$> accessibleTables (filter ((== cs schema) . tableSchema) allTabs) body <- encode <$> accessibleTables (filter ((== cs schema) . tableSchema) (dbTables dbStructure))
return $ responseLBS status200 [jsonH] $ cs body return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do
let cols = filter (filterCol schema table) allCols
pkeys = map pkName $ filter (filterPk schema table) allPrKeys
body = encode (TableOptions cols pkeys)
return $ responseLBS status200 [jsonH, allOrigins] $ cs body
(_, _) -> (_, _) ->
return $ responseLBS status404 [] "" return $ responseLBS status404 [] ""
where where
allTabs = dbTables dbStructure
allRels = dbRelations dbStructure
allCols = dbColumns dbStructure
allPrKeys = dbPrimaryKeys dbStructure allPrKeys = dbPrimaryKeys dbStructure
filterCol sc table (Column{colTable=Table{tableSchema=s, tableName=t}}) = s==sc && table==t
filterCol _ _ _ = False
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
path = pathInfo req path = pathInfo req
verb = requestMethod req verb = requestMethod req
@@ -195,7 +183,7 @@ app dbStructure conf reqBody req =
isCsv = contentType == csvMT isCsv = contentType == csvMT
contentTypeH = (hContentType, contentType) contentTypeH = (hContentType, contentType)
echoRequested = hasPrefer "return=representation" echoRequested = hasPrefer "return=representation"
request = parseRequest schema allRels (head path) req reqBody --TODO! is head safe? request = parseRequest schema (dbRelations dbStructure) (head path) req reqBody --TODO! is head safe?
rangeStatus :: Int -> Int -> Maybe Int -> Status rangeStatus :: Int -> Int -> Maybe Int -> Status
rangeStatus _ _ Nothing = status200 rangeStatus _ _ Nothing = status200
@@ -255,7 +243,7 @@ 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)
parseRequestBody :: Bool -> BL.ByteString -> Either Text ([Text],[[Value]]) parseRequestBody :: Bool -> RequestBody -> Either Text ([Text],[[Value]])
parseRequestBody isCsv reqBody = first cs $ parseRequestBody isCsv reqBody = first cs $
checkStructure =<< checkStructure =<<
if isCsv if isCsv
@@ -368,12 +356,13 @@ instance ToJSON TableOptions where
"columns" .= tblOptcolumns t "columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ] , "pkey" .= tblOptpkey t ]
parseRequest :: Schema -> [Relation] -> TableName -> Request -> BL.ByteString -> Either Text (SqlQuery, SqlQuery, Bool) parseRequest :: Schema -> [Relation] -> TableName -> Request -> RequestBody -> Either Text (SqlQuery, Maybe (SqlQuery, Bool))
parseRequest schema allRels rootTableName httpRequest reqBody = parseRequest schema allRels rootTableName httpRequest reqBody =
(,,) <$> selectQuery if method == "GET"
<*> (if method == "GET" then pure "" else mutateQuery) then (,Nothing) <$> selectQuery
<*> (if method == "GET" then pure False else pure isSingleRecord) else (,) <$> selectQuery <*> ( Just <$> mutatePart )
where where
mutatePart = (,) <$> mutateQuery <*> pure isSingleRecord
hdrs = requestHeaders httpRequest hdrs = requestHeaders httpRequest
lookupHeader = flip lookup hdrs lookupHeader = flip lookup hdrs
isCsv = lookupHeader "Content-Type" == Just csvMT isCsv = lookupHeader "Content-Type" == Just csvMT
@@ -381,23 +370,15 @@ parseRequest schema allRels rootTableName httpRequest reqBody =
qParams = queryParams httpRequest qParams = queryParams httpRequest
parsedBody = parseRequestBody isCsv reqBody parsedBody = parseRequestBody isCsv reqBody
isSingleRecord = either (const False) ((==1) . length . snd ) parsedBody isSingleRecord = either (const False) ((==1) . length . snd ) parsedBody
parseField f = parse pField ("failed to parse field <<"++f++">>") f
flds = join $ first formatParserError . mapM (parseField . cs) <$> (fst <$> parsedBody)
vals = snd <$> parsedBody
setWith = if isSingleRecord
then M.fromList <$> (zip <$> flds <*> (head <$> vals))
else Left "Expecting a sigle CSV line with header or a JSON object"
allFilters = whereFilters qParams allFilters = whereFilters qParams
mutateFilters = filter (not . ( '.' `elem` ) . fst) allFilters -- update/delete filters can be only on the root table
cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters
fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
rels = case method of
"POST" -> fakeSourceRelations ++ allRels
"PATCH" -> fakeSourceRelations ++ allRels
_ -> allRels
selectApiRequest = augumentRequestWithJoin schema rels selectApiRequest = augumentRequestWithJoin schema rels
=<< buildSelectApiRequest rootName sel filters (orderStr qParams) =<< buildSelectApiRequest rootName sel filters (orderStr qParams)
where where
rels = case method of
"POST" -> fakeSourceRelations ++ allRels
"PATCH" -> fakeSourceRelations ++ allRels
_ -> allRels
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
sel = if method == "DELETE" sel = if method == "DELETE"
then "*" -- we are not returning the records so no need to consider nested items then "*" -- we are not returning the records so no need to consider nested items
else selectStr qParams else selectStr qParams
@@ -412,29 +393,44 @@ parseRequest schema allRels rootTableName httpRequest reqBody =
"POST" -> Node <$> ((,) <$> (Insert rootTableName <$> flds <*> vals) <*> pure (rootTableName, Nothing)) <*> pure [] "POST" -> Node <$> ((,) <$> (Insert rootTableName <$> flds <*> vals) <*> pure (rootTableName, Nothing)) <*> pure []
"PATCH" -> Node <$> ((,) <$> (Update rootTableName <$> setWith <*> cond) <*> pure (rootTableName, Nothing)) <*> pure [] "PATCH" -> Node <$> ((,) <$> (Update rootTableName <$> setWith <*> cond) <*> pure (rootTableName, Nothing)) <*> pure []
"DELETE" -> Node <$> ((,) <$> (Delete [rootTableName] <$> cond) <*> pure (rootTableName, Nothing)) <*> pure [] "DELETE" -> Node <$> ((,) <$> (Delete [rootTableName] <$> cond) <*> pure (rootTableName, Nothing)) <*> pure []
_ -> undefined _ -> Left "Unsupported HTTP verb"
where
parseField f = parse pField ("failed to parse field <<"++f++">>") f
flds = join $ first formatParserError . mapM (parseField . cs) <$> (fst <$> parsedBody)
vals = snd <$> parsedBody
mutateFilters = filter (not . ( '.' `elem` ) . fst) allFilters -- update/delete filters can be only on the root table
cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters
setWith = if isSingleRecord
then M.fromList <$> (zip <$> flds <*> (head <$> vals))
else Left "Expecting a sigle CSV line with header or a JSON object"
createStatement :: SqlQuery -> Maybe (Text, Bool) -> Bool -> Maybe NonnegRange -> [Text] -> Bool -> Bool -> SqlQuery createReadStatement :: SqlQuery -> Maybe NonnegRange -> Bool -> Bool -> B.Stmt P.Postgres
createStatement selectQuery Nothing _ range _ countTable asCsv = createReadStatement selectQuery range countTable asCsv =
wrapQuery selectQuery [ B.Stmt (
if countTable then countAllF else countNoneF, wrapQuery selectQuery [
countF, if countTable then countAllF else countNoneF,
"null", -- location header can not be calucalted countF,
if asCsv then asCsvF else asJsonF "null", -- location header can not be calucalted
] selectStarF range if asCsv then asCsvF else asJsonF
createStatement selectQuery (Just (changeQuery, isSingle)) echoRequested _ pKeys _ asCsv = ] selectStarF range
wrapQuery changeQuery [ ) V.empty True
countNoneF, -- when updateing it does not make sense
countF,
if isSingle then locationF pKeys else "null",
if echoRequested
then
if asCsv
then asCsvF
else if isSingle then asJsonSingleF else asJsonF
else "null"
] selectQuery Nothing createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> [Text] -> Bool -> B.Stmt P.Postgres
createWriteStatement selectQuery mutateQuery isSingle echoRequested pKeys asCsv =
B.Stmt (
wrapQuery mutateQuery [
countNoneF, -- when updateing it does not make sense
countF,
if isSingle then locationF pKeys else "null",
if echoRequested
then
if asCsv
then asCsvF
else if isSingle then asJsonSingleF else asJsonF
else "null"
] selectQuery Nothing
) V.empty True
extractQueryResult :: Maybe (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString) extractQueryResult :: Maybe (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString)
-> (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString) -> (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString)
+2
View File
@@ -2,6 +2,7 @@ module PostgREST.Types where
import Data.Text import Data.Text
import Data.Tree import Data.Tree
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import Data.Aeson import Data.Aeson
import Data.Map import Data.Map
@@ -16,6 +17,7 @@ type Schema = Text
type TableName = Text type TableName = Text
type SqlQuery = Text type SqlQuery = Text
type SqlFragment = Text type SqlFragment = Text
type RequestBody = BL.ByteString
data Table = Table { data Table = Table {
tableSchema :: Schema tableSchema :: Schema
+2 -1
View File
@@ -218,7 +218,8 @@ spec = afterAll_ resetDb $ around withApp $ do
describe "Putting record" $ do describe "Putting record" $ do
context "to unkonwn uri" $ context "to unkonwn uri" $
it "gives a 404" $ it "gives a 404" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/fake" [] request methodPut "/fake" []
[json| { "real": false } |] [json| { "real": false } |]
`shouldRespondWith` 404 `shouldRespondWith` 404