App.hs refactor
This commit is contained in:
+100
-104
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user