Get the refactored code to compile (5 test failing)
This commit is contained in:
+230
-139
@@ -13,7 +13,7 @@ import Control.Monad (join)
|
|||||||
import Data.Bifunctor (first)
|
import Data.Bifunctor (first)
|
||||||
import qualified Data.ByteString.Char8 as BS
|
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 HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import Data.List (find, sortBy, delete, transpose)
|
import Data.List (find, sortBy, delete, transpose)
|
||||||
@@ -23,7 +23,8 @@ import Data.Ranged.Ranges (emptyRange, singletonRange)
|
|||||||
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 qualified Data.Map as M
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
import Text.Parsec.Error
|
import Text.Parsec.Error
|
||||||
import Text.ParserCombinators.Parsec (parse)
|
import Text.ParserCombinators.Parsec (parse)
|
||||||
@@ -75,20 +76,22 @@ import Prelude
|
|||||||
|
|
||||||
app :: DbStructure -> AppConfig -> RequestBody -> 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 =
|
||||||
let schema = configSchema conf
|
let
|
||||||
intent = userIntent schema req reqBody
|
|
||||||
-- TODO: blow up for Left values
|
-- TODO: blow up for Left values
|
||||||
contentType = either (const ApplicationJSON) id (iAccepts intent)
|
contentType = either (const ApplicationJSON) id (iAccepts intent)
|
||||||
contentTypeH = (hContentType, contentType) in
|
contentTypeS ct = case ct of
|
||||||
|
ApplicationJSON -> "application/json"
|
||||||
|
TextCSV -> "text/csv"
|
||||||
|
contentTypeH = (hContentType, contentTypeS contentType) in
|
||||||
|
|
||||||
case (iAction intent, iTarget intent, iPayload intent) of
|
case (iAction intent, iTarget intent, iPayload intent) of
|
||||||
(ActionUnknown _, _, _) -> return notFound
|
(ActionUnknown _, _, _) -> return notFound
|
||||||
(_, TargetUnknown _, _) -> return notFound
|
(_, TargetUnknown _, _) -> return notFound
|
||||||
(_, _, PayloadParseError e) ->
|
(_, _, Just (PayloadParseError e)) ->
|
||||||
return $ responseLBS status400 [jsonH]
|
return $ responseLBS status400 [jsonH] $
|
||||||
(formatGeneralError "Cannot parse request payload" e)
|
cs (formatGeneralError "Cannot parse request payload" (cs e))
|
||||||
|
|
||||||
(ActionInfo, TargetIdent tSchema tTable, _) -> do
|
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), _) -> do
|
||||||
let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
|
let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
|
||||||
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
|
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
|
||||||
body = encode (TableOptions cols pkeys)
|
body = encode (TableOptions cols pkeys)
|
||||||
@@ -101,12 +104,16 @@ app dbStructure conf reqBody req =
|
|||||||
body <- encode <$> accessibleTables (filter ((== cs schema) . tableSchema) (dbTables dbStructure))
|
body <- encode <$> accessibleTables (filter ((== cs schema) . tableSchema) (dbTables dbStructure))
|
||||||
return $ responseLBS status200 [jsonH] $ cs body
|
return $ responseLBS status200 [jsonH] $ cs body
|
||||||
|
|
||||||
(ActionInvoke, TargetIdent qi, PayloadJSON payload) -> do
|
(ActionInvoke, TargetIdent qi, Just (PayloadJSON payload)) -> do
|
||||||
exists <- doesProcExist (qiSchema qi) (qiName qi)
|
exists <- doesProcExist qi
|
||||||
if exists
|
if exists
|
||||||
then do
|
then do
|
||||||
let call = B.Stmt "select " V.empty True <>
|
let p = case pp of
|
||||||
asJson (callProc qi payload)
|
JSON.Object o -> o
|
||||||
|
_ -> undefined
|
||||||
|
where pp = V.head payload
|
||||||
|
call = B.Stmt "select " V.empty True <>
|
||||||
|
asJson (callProc qi p)
|
||||||
jwtSecret = configJwtSecret conf
|
jwtSecret = configJwtSecret conf
|
||||||
|
|
||||||
bodyJson :: Maybe (Identity Value) <- H.maybeEx call
|
bodyJson :: Maybe (Identity Value) <- H.maybeEx call
|
||||||
@@ -118,42 +125,77 @@ app dbStructure conf reqBody req =
|
|||||||
else cs $ encode body)
|
else cs $ encode body)
|
||||||
else return notFound
|
else return notFound
|
||||||
|
|
||||||
(ActionRead, TargetIdent qi, _) -> do
|
(ActionRead, TargetIdent qi, _) ->
|
||||||
let range = iRange intent
|
case selectQuery of
|
||||||
singular = iPreferSingular intent
|
Left e -> return $ responseLBS status400 [jsonH] $ cs e
|
||||||
selectQuery = requestToQuery schema <$> selectApiRequest
|
Right q -> do
|
||||||
q = createReadStatement selectQuery range singular
|
let range = iRange intent
|
||||||
(not $ iPreferCount intent) contentType
|
singular = iPreferSingular intent
|
||||||
if range == Just emptyRange
|
stm = createReadStatement q range singular
|
||||||
then return $ errResponse status416 "HTTP Range error"
|
(iPreferCount intent) (contentType == TextCSV)
|
||||||
else do
|
if range == Just emptyRange
|
||||||
row <- H.maybeEx q
|
then return $ errResponse status416 "HTTP Range error"
|
||||||
let (tableTotal, queryTotal, _ , body) = extractQueryResult row
|
else do
|
||||||
if singular
|
row <- H.maybeEx stm
|
||||||
then return $ if queryTotal <= 0
|
let (tableTotal, queryTotal, _ , body) = extractQueryResult row
|
||||||
then responseLBS status404 [] ""
|
if singular
|
||||||
else responseLBS status200 [contentTypeH] (fromMaybe "{}" body)
|
then return $ if queryTotal <= 0
|
||||||
else do
|
then responseLBS status404 [] ""
|
||||||
let frm = fromMaybe 0 $ rangeOffset <$> range
|
else responseLBS status200 [contentTypeH] (fromMaybe "{}" body)
|
||||||
to = frm+queryTotal-1
|
else do
|
||||||
contentRange = contentRangeH frm to tableTotal
|
let frm = fromMaybe 0 $ rangeOffset <$> range
|
||||||
status = rangeStatus frm to tableTotal
|
to = frm+queryTotal-1
|
||||||
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
|
contentRange = contentRangeH frm to tableTotal
|
||||||
. sortBy (comparing fst)
|
status = rangeStatus frm to tableTotal
|
||||||
. map (join (***) cs)
|
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
|
||||||
. parseSimpleQuery
|
. sortBy (comparing fst)
|
||||||
$ rawQueryString req
|
. map (join (***) cs)
|
||||||
return $ responseLBS status
|
. parseSimpleQuery
|
||||||
[contentTypeH, contentRange,
|
$ rawQueryString req
|
||||||
("Content-Location",
|
return $ responseLBS status
|
||||||
"/" <> cs (qiName qi) <>
|
[contentTypeH, contentRange,
|
||||||
if Prelude.null canonical then "" else "?" <> cs canonical
|
("Content-Location",
|
||||||
)
|
"/" <> cs (qiName qi) <>
|
||||||
] (fromMaybe "[]" body)
|
if Prelude.null canonical then "" else "?" <> cs canonical
|
||||||
(ActionCreate, TargetIdent qi, PayloadJSON payload) -> undefined
|
)
|
||||||
(ActionUpdate, TargetIdent qi, PayloadJSON payload) -> undefined
|
] (fromMaybe "[]" body)
|
||||||
(ActionDelete, TargetIdent qi, _) -> undefined
|
(ActionCreate, TargetIdent (QualifiedIdentifier _ table), _) ->
|
||||||
(ActionRead, TargetIdent qi, _) -> undefined
|
case queries of
|
||||||
|
Left e -> return $ responseLBS status400 [jsonH] $ cs e
|
||||||
|
Right (sq,mq,isSingle) -> do
|
||||||
|
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
|
||||||
|
let stm = createWriteStatement sq mq isSingle (iPreferRepresentation intent) pKeys (contentType == TextCSV)
|
||||||
|
row <- H.maybeEx stm
|
||||||
|
let (_, _, location, body) = extractQueryResult row
|
||||||
|
return $ responseLBS status201
|
||||||
|
[
|
||||||
|
contentTypeH,
|
||||||
|
(hLocation, "/" <> cs table <> "?" <> cs (fromMaybe "" location))
|
||||||
|
]
|
||||||
|
$ if iPreferRepresentation intent then fromMaybe "[]" body else ""
|
||||||
|
(ActionUpdate, TargetIdent _, _) ->
|
||||||
|
case queries of
|
||||||
|
Left e -> return $ responseLBS status400 [jsonH] $ cs e
|
||||||
|
Right (sq,mq,_) -> do
|
||||||
|
let stm = createWriteStatement sq mq False (iPreferRepresentation intent) [] (contentType == TextCSV)
|
||||||
|
row <- H.maybeEx stm
|
||||||
|
let (_, queryTotal, _, body) = extractQueryResult row
|
||||||
|
r = contentRangeH 0 (queryTotal-1) (Just queryTotal)
|
||||||
|
s = case () of _ | queryTotal == 0 -> status404
|
||||||
|
| iPreferRepresentation intent -> status200
|
||||||
|
| otherwise -> status204
|
||||||
|
return $ responseLBS s [contentTypeH, r]
|
||||||
|
$ if iPreferRepresentation intent then fromMaybe "[]" body else ""
|
||||||
|
(ActionDelete, TargetIdent _, _) ->
|
||||||
|
case queries of
|
||||||
|
Left e -> return $ responseLBS status400 [jsonH] $ cs e
|
||||||
|
Right (sq,mq,_) -> do
|
||||||
|
let stm = createWriteStatement sq mq False False [] (contentType == TextCSV)
|
||||||
|
row <- H.maybeEx stm
|
||||||
|
let (_, queryTotal, _, _) = extractQueryResult row
|
||||||
|
return $ if queryTotal == 0
|
||||||
|
then notFound
|
||||||
|
else responseLBS status204 [("Content-Range", "*/"<> cs (show queryTotal))] ""
|
||||||
|
|
||||||
(_, _, _) -> return notFound
|
(_, _, _) -> return notFound
|
||||||
|
|
||||||
@@ -162,12 +204,20 @@ app dbStructure conf reqBody req =
|
|||||||
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
|
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
|
||||||
allPrKeys = dbPrimaryKeys dbStructure
|
allPrKeys = dbPrimaryKeys dbStructure
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||||
|
schema = cs $ configSchema conf
|
||||||
|
intent = userIntent schema req reqBody
|
||||||
|
selectApiRequest = buildSelectApiRequest intent (dbRelations dbStructure)
|
||||||
|
selectQuery = requestToQuery schema <$> selectApiRequest
|
||||||
|
mutateTuple = buildMutateApiRequest intent
|
||||||
|
mutateApiRequest = fst <$> mutateTuple
|
||||||
|
isSingleRecord = snd <$> mutateTuple
|
||||||
|
mutateQuery = requestToQuery schema <$> mutateApiRequest
|
||||||
|
queries = (,,) <$> selectQuery <*> mutateQuery <*> isSingleRecord
|
||||||
-- path = pathInfo req
|
-- path = pathInfo req
|
||||||
-- verb = requestMethod req
|
-- verb = requestMethod req
|
||||||
-- hdrs = requestHeaders req
|
-- hdrs = requestHeaders req
|
||||||
-- lookupHeader = flip lookup hdrs
|
-- lookupHeader = flip lookup hdrs
|
||||||
-- hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
|
-- hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
|
||||||
-- schema = cs $ configSchema conf
|
|
||||||
-- range = rangeRequested hdrs
|
-- range = rangeRequested hdrs
|
||||||
-- request = parseRequest schema (dbRelations dbStructure) (head path) req reqBody --TODO! is head safe?
|
-- request = parseRequest schema (dbRelations dbStructure) (head path) req reqBody --TODO! is head safe?
|
||||||
|
|
||||||
@@ -253,17 +303,17 @@ contentTypeForAccept accept
|
|||||||
findInAccept = flip find $ parseHttpAccept acceptH
|
findInAccept = flip find $ parseHttpAccept acceptH
|
||||||
has = isJust . findInAccept . BS.isPrefixOf
|
has = isJust . findInAccept . BS.isPrefixOf
|
||||||
|
|
||||||
parseCsvCell :: BL.ByteString -> Value
|
-- parseCsvCell :: BL.ByteString -> Value
|
||||||
parseCsvCell s = if s == "NULL" then Null else String $ cs s
|
-- parseCsvCell s = if s == "NULL" then Null else String $ cs s
|
||||||
|
|
||||||
formatRelationError :: Text -> Text
|
formatRelationError :: Text -> Text
|
||||||
formatRelationError e = formatGeneralError
|
formatRelationError = formatGeneralError
|
||||||
"could not find foreign keys between these entities" e
|
"could not find foreign keys between these entities"
|
||||||
|
|
||||||
formatParserError :: ParseError -> Text
|
formatParserError :: ParseError -> Text
|
||||||
formatParserError e = formatGeneralError message details
|
formatParserError e = formatGeneralError message details
|
||||||
where
|
where
|
||||||
message = show (errorPos e)
|
message = cs $ show (errorPos e)
|
||||||
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)
|
||||||
|
|
||||||
@@ -272,31 +322,31 @@ formatGeneralError message details = cs $ encode $ object [
|
|||||||
"message" .= message,
|
"message" .= message,
|
||||||
"details" .= details]
|
"details" .= details]
|
||||||
|
|
||||||
parseRequestBody :: Bool -> RequestBody -> 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
|
||||||
then do
|
-- then do
|
||||||
rows <- (map V.toList . V.toList) <$> CSV.decode CSV.NoHeader reqBody
|
-- rows <- (map V.toList . V.toList) <$> CSV.decode CSV.NoHeader reqBody
|
||||||
if null rows then Left "CSV requires header" -- TODO! should check if length rows > 1 (header and 1 row)
|
-- if null rows then Left "CSV requires header" -- TODO! should check if length rows > 1 (header and 1 row)
|
||||||
else Right (head rows, (map $ map $ parseCsvCell . cs) (tail rows))
|
-- else Right (head rows, (map $ map $ parseCsvCell . cs) (tail rows))
|
||||||
else eitherDecode reqBody >>= convertJson
|
-- else eitherDecode reqBody >>= convertJson
|
||||||
where
|
-- where
|
||||||
checkStructure :: ([Text], [[Value]]) -> Either String ([Text], [[Value]])
|
-- checkStructure :: ([Text], [[Value]]) -> Either String ([Text], [[Value]])
|
||||||
checkStructure v
|
-- checkStructure v
|
||||||
| headerMatchesContent v = Right v
|
-- | headerMatchesContent v = Right v
|
||||||
| isCsv = Left "CSV header does not match rows length"
|
-- | isCsv = Left "CSV header does not match rows length"
|
||||||
| otherwise = Left "The number of keys in objects do not match"
|
-- | otherwise = Left "The number of keys in objects do not match"
|
||||||
|
--
|
||||||
|
-- headerMatchesContent :: ([Text], [[Value]]) -> Bool
|
||||||
|
-- headerMatchesContent (header, vals) = all ( (headerLength ==) . length) vals
|
||||||
|
-- where headerLength = length header
|
||||||
|
|
||||||
headerMatchesContent :: ([Text], [[Value]]) -> Bool
|
convertJson :: Value -> Either Text ([Text],[[Value]])
|
||||||
headerMatchesContent (header, vals) = all ( (headerLength ==) . length) vals
|
|
||||||
where headerLength = length header
|
|
||||||
|
|
||||||
convertJson :: Value -> Either String ([Text],[[Value]])
|
|
||||||
convertJson v = (,) <$> (header <$> normalized) <*> (vals <$> normalized)
|
convertJson v = (,) <$> (header <$> normalized) <*> (vals <$> normalized)
|
||||||
where
|
where
|
||||||
invalidMsg = "Expecting single JSON object or JSON array of objects"
|
invalidMsg = "Expecting single JSON object or JSON array of objects"::Text
|
||||||
normalized :: Either String [(Text, [Value])]
|
normalized :: Either Text [(Text, [Value])]
|
||||||
normalized = groupByKey =<< normalizeValue v
|
normalized = groupByKey =<< normalizeValue v
|
||||||
|
|
||||||
vals :: [(Text, [Value])] -> [[Value]]
|
vals :: [(Text, [Value])] -> [[Value]]
|
||||||
@@ -305,16 +355,16 @@ convertJson v = (,) <$> (header <$> normalized) <*> (vals <$> normalized)
|
|||||||
header :: [(Text, [Value])] -> [Text]
|
header :: [(Text, [Value])] -> [Text]
|
||||||
header = map fst
|
header = map fst
|
||||||
|
|
||||||
groupByKey :: Value -> Either String [(Text,[Value])]
|
groupByKey :: Value -> Either Text [(Text,[Value])]
|
||||||
groupByKey (Array a) = HM.toList . foldr (HM.unionWith (++)) (HM.fromList []) <$> maps
|
groupByKey (Array a) = HM.toList . foldr (HM.unionWith (++)) (HM.fromList []) <$> maps
|
||||||
where
|
where
|
||||||
maps :: Either String [HM.HashMap Text [Value]]
|
maps :: Either Text [HM.HashMap Text [Value]]
|
||||||
maps = mapM getElems $ V.toList a
|
maps = mapM getElems $ V.toList a
|
||||||
getElems (Object o) = Right $ HM.map (:[]) o
|
getElems (Object o) = Right $ HM.map (:[]) o
|
||||||
getElems _ = Left invalidMsg
|
getElems _ = Left invalidMsg
|
||||||
groupByKey _ = Left invalidMsg
|
groupByKey _ = Left invalidMsg
|
||||||
|
|
||||||
normalizeValue :: Value -> Either String Value
|
normalizeValue :: Value -> Either Text Value
|
||||||
normalizeValue val =
|
normalizeValue val =
|
||||||
case val of
|
case val of
|
||||||
Object obj -> Right $ Array (V.fromList[Object obj])
|
Object obj -> Right $ Array (V.fromList[Object obj])
|
||||||
@@ -327,63 +377,104 @@ augumentRequestWithJoin schema allRels request =
|
|||||||
>>= addJoinConditions schema
|
>>= addJoinConditions schema
|
||||||
|
|
||||||
-- we use strings here because most of this data will be sent to parsers (which need strings for now)
|
-- we use strings here because most of this data will be sent to parsers (which need strings for now)
|
||||||
queryParams :: Request -> [(String, Maybe String)]
|
-- queryParams :: Request -> [(String, Maybe String)]
|
||||||
queryParams httpRequest = [(cs k, cs <$> v)|(k,v) <- queryString httpRequest]
|
-- queryParams httpRequest = [(cs k, cs <$> v)|(k,v) <- queryString httpRequest]
|
||||||
|
--
|
||||||
|
-- selectStr :: [(String, Maybe String)] -> String
|
||||||
|
-- selectStr qParams = fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
||||||
|
--
|
||||||
|
-- whereFilters :: [(String, Maybe String)] -> [(String, String)]
|
||||||
|
-- whereFilters qParams = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
|
||||||
|
--
|
||||||
|
-- orderStr :: [(String, Maybe String)] -> Maybe String
|
||||||
|
-- orderStr qParams = join $ lookup "order" qParams
|
||||||
|
|
||||||
selectStr :: [(String, Maybe String)] -> String
|
buildSelectApiRequest :: Intent -> [Relation] -> Either Text ApiRequest
|
||||||
selectStr qParams = fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
buildSelectApiRequest intent allRels =
|
||||||
|
|
||||||
whereFilters :: [(String, Maybe String)] -> [(String, String)]
|
|
||||||
whereFilters qParams = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
|
|
||||||
|
|
||||||
orderStr :: [(String, Maybe String)] -> Maybe String
|
|
||||||
orderStr qParams = join $ lookup "order" qParams
|
|
||||||
|
|
||||||
buildSelectApiRequest :: Text -> Schema -> TableName -> [(String, String)] -> [Relation] -> [(String, Maybe String)] -> Either Text ApiRequest
|
|
||||||
buildSelectApiRequest method schema rootTableName allFilters allRels qParams =
|
|
||||||
augumentRequestWithJoin schema rels =<< first formatParserError (foldr addFilter <$> (addOrder <$> apiRequest <*> ord) <*> flts)
|
augumentRequestWithJoin schema rels =<< first formatParserError (foldr addFilter <$> (addOrder <$> apiRequest <*> ord) <*> flts)
|
||||||
where
|
where
|
||||||
selStr = selectStr qParams
|
selStr = iSelect intent
|
||||||
orderS = orderStr qParams
|
orderS = iOrder intent
|
||||||
rels = case method of
|
action = iAction intent
|
||||||
"POST" -> fakeSourceRelations ++ allRels
|
target = iTarget intent
|
||||||
"PATCH" -> fakeSourceRelations ++ allRels
|
(schema, rootTableName) = fromJust $ -- Make it safe
|
||||||
_ -> allRels
|
case target of
|
||||||
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
|
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
|
||||||
sel = if method == "DELETE"
|
_ -> Nothing
|
||||||
then "*" -- we are not returning the records so no need to consider nested items
|
|
||||||
else selStr
|
rootName = if action == ActionRead
|
||||||
rootName = if method == "GET"
|
|
||||||
then rootTableName
|
then rootTableName
|
||||||
else sourceSubqueryName
|
else sourceSubqueryName
|
||||||
filters = if method == "GET"
|
filters = if action == ActionRead
|
||||||
then allFilters
|
then iFilters intent
|
||||||
else filter (( '.' `elem` ) . fst) allFilters -- there can be no filters on the root table whre we are doing insert/update
|
else filter (( '.' `elem` ) . fst) $ iFilters intent -- there can be no filters on the root table whre we are doing insert/update
|
||||||
apiRequest = parse (pRequestSelect rootName) ("failed to parse select parameter <<"++sel++">>") sel
|
rels = case action of
|
||||||
|
ActionCreate -> fakeSourceRelations ++ allRels
|
||||||
|
ActionUpdate -> fakeSourceRelations ++ allRels
|
||||||
|
_ -> allRels
|
||||||
|
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
|
||||||
|
apiRequest = parse (pRequestSelect rootName) ("failed to parse select parameter <<"++selStr++">>") selStr
|
||||||
addOrder (Node (q,i) f) o = Node (q{order=o}, i) f
|
addOrder (Node (q,i) f) o = Node (q{order=o}, i) f
|
||||||
flts = mapM pRequestFilter filters
|
flts = mapM pRequestFilter filters
|
||||||
ord = traverse (parse pOrder ("failed to parse order parameter <<"++fromMaybe "" orderS++">>")) orderS
|
ord = traverse (parse pOrder ("failed to parse order parameter <<"++fromMaybe "" orderS++">>")) orderS
|
||||||
|
|
||||||
buildMutateApiRequest :: Text -> Bool -> TableName -> RequestBody -> [(String, String)] -> Either Text (ApiRequest, Bool)
|
--buildMutateApiRequest :: Text -> Bool -> TableName -> RequestBody -> [(String, String)] -> Either Text (ApiRequest, Bool)
|
||||||
buildMutateApiRequest method isCsv rootTableName reqBody allFilters =
|
--buildMutateApiRequest method isCsv rootTableName reqBody allFilters =
|
||||||
|
buildMutateApiRequest :: Intent -> Either Text (ApiRequest, Bool)
|
||||||
|
buildMutateApiRequest intent =
|
||||||
(,) <$> mutateApiRequest <*> pure isSingleRecord
|
(,) <$> mutateApiRequest <*> pure isSingleRecord
|
||||||
where
|
where
|
||||||
mutateApiRequest = case method of
|
action = iAction intent
|
||||||
"POST" -> Node <$> ((,) <$> (Insert rootTableName <$> flds <*> vals) <*> pure (rootTableName, Nothing)) <*> pure []
|
target = iTarget intent
|
||||||
"PATCH" -> Node <$> ((,) <$> (Update rootTableName <$> setWith <*> cond) <*> pure (rootTableName, Nothing)) <*> pure []
|
rootTableName = fromJust $ -- Make it safe
|
||||||
"DELETE" -> Node <$> ((,) <$> (Delete [rootTableName] <$> cond) <*> pure (rootTableName, Nothing)) <*> pure []
|
case target of
|
||||||
|
(TargetIdent (QualifiedIdentifier _ t) ) -> Just t
|
||||||
|
_ -> Nothing
|
||||||
|
mutateApiRequest = case action of
|
||||||
|
ActionCreate -> Node <$> ((,) <$> (Insert rootTableName <$> flds <*> vals) <*> pure (rootTableName, Nothing)) <*> pure []
|
||||||
|
ActionUpdate -> Node <$> ((,) <$> (Update rootTableName <$> setWith <*> cond) <*> pure (rootTableName, Nothing)) <*> pure []
|
||||||
|
ActionDelete -> Node <$> ((,) <$> (Delete [rootTableName] <$> cond) <*> pure (rootTableName, Nothing)) <*> pure []
|
||||||
_ -> Left "Unsupported HTTP verb"
|
_ -> Left "Unsupported HTTP verb"
|
||||||
parseField f = parse pField ("failed to parse field <<"++f++">>") f
|
parseField f = parse pField ("failed to parse field <<"++f++">>") f
|
||||||
parsedBody = parseRequestBody isCsv reqBody
|
payload = case iPayload intent of
|
||||||
|
Just (PayloadJSON v) -> JSON.Array v
|
||||||
|
_ -> undefined --TODO! fix
|
||||||
|
parsedBody = convertJson payload -- TODO! either check structure or refactor to send json directly to postgres
|
||||||
isSingleRecord = either (const False) ((==1) . length . snd ) parsedBody
|
isSingleRecord = either (const False) ((==1) . length . snd ) parsedBody
|
||||||
flds = join $ first formatParserError . mapM (parseField . cs) <$> (fst <$> parsedBody)
|
flds = join $ first formatParserError . mapM (parseField . cs) <$> (fst <$> parsedBody)
|
||||||
vals = snd <$> parsedBody
|
vals = snd <$> parsedBody
|
||||||
mutateFilters = filter (not . ( '.' `elem` ) . fst) allFilters -- update/delete filters can be only on the root table
|
mutateFilters = filter (not . ( '.' `elem` ) . fst) $ iFilters intent -- update/delete filters can be only on the root table
|
||||||
cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters
|
cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters
|
||||||
setWith = if isSingleRecord
|
setWith = if isSingleRecord
|
||||||
then M.fromList <$> (zip <$> flds <*> (head <$> vals))
|
then M.fromList <$> (zip <$> flds <*> (head <$> vals))
|
||||||
else Left "Expecting a sigle CSV line with header or a JSON object"
|
else Left "Expecting a sigle CSV line with header or a JSON object"
|
||||||
|
|
||||||
|
-- buildSelectApiRequest :: Text -> Schema -> TableName -> [(String, String)] -> [Relation] -> [(String, Maybe String)] -> Either Text ApiRequest
|
||||||
|
-- buildSelectApiRequest method schema rootTableName allFilters allRels qParams =
|
||||||
|
-- augumentRequestWithJoin schema rels =<< first formatParserError (foldr addFilter <$> (addOrder <$> apiRequest <*> ord) <*> flts)
|
||||||
|
-- where
|
||||||
|
-- selStr = selectStr qParams
|
||||||
|
-- orderS = orderStr qParams
|
||||||
|
-- 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"
|
||||||
|
-- then "*" -- we are not returning the records so no need to consider nested items
|
||||||
|
-- else selStr
|
||||||
|
-- rootName = if method == "GET"
|
||||||
|
-- then rootTableName
|
||||||
|
-- else sourceSubqueryName
|
||||||
|
-- filters = if method == "GET"
|
||||||
|
-- then allFilters
|
||||||
|
-- else filter (( '.' `elem` ) . fst) allFilters -- there can be no filters on the root table whre we are doing insert/update
|
||||||
|
-- apiRequest = parse (pRequestSelect rootName) ("failed to parse select parameter <<"++sel++">>") sel
|
||||||
|
-- addOrder (Node (q,i) f) o = Node (q{order=o}, i) f
|
||||||
|
-- flts = mapM pRequestFilter filters
|
||||||
|
-- ord = traverse (parse pOrder ("failed to parse order parameter <<"++fromMaybe "" orderS++">>")) orderS
|
||||||
|
|
||||||
|
|
||||||
addFilter :: (Path, Filter) -> ApiRequest -> ApiRequest
|
addFilter :: (Path, Filter) -> ApiRequest -> ApiRequest
|
||||||
addFilter ([], flt) (Node (q@(Select {where_=flts}), i) forest) = Node (q {where_=flt:flts}, i) forest
|
addFilter ([], flt) (Node (q@(Select {where_=flts}), i) forest) = Node (q {where_=flt:flts}, i) forest
|
||||||
addFilter (path, flt) (Node rn forest) =
|
addFilter (path, flt) (Node rn forest) =
|
||||||
@@ -421,30 +512,30 @@ instance ToJSON TableOptions where
|
|||||||
"columns" .= tblOptcolumns t
|
"columns" .= tblOptcolumns t
|
||||||
, "pkey" .= tblOptpkey t ]
|
, "pkey" .= tblOptpkey t ]
|
||||||
|
|
||||||
createSelectQuery :: [Relation] -> QualifiedIdentifier -> SqlQuery
|
-- createSelectQuery :: [Relation] -> QualifiedIdentifier -> SqlQuery
|
||||||
createSelectQuery rels qi =
|
-- createSelectQuery rels qi =
|
||||||
requestToQuery schema <$> selectApiRequest
|
-- requestToQuery schema <$> selectApiRequest
|
||||||
undefined
|
-- undefined
|
||||||
|
|
||||||
parseRequest :: Schema -> [Relation] -> TableName -> Request -> RequestBody -> Either Text (SqlQuery, Maybe (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 =
|
||||||
if method == "GET"
|
-- if method == "GET"
|
||||||
then (,Nothing) <$> selectQuery
|
-- then (,Nothing) <$> selectQuery
|
||||||
else (,) <$> selectQuery <*> ( Just <$> mutatePart )
|
-- else (,) <$> selectQuery <*> ( Just <$> mutatePart )
|
||||||
where
|
-- where
|
||||||
mutatePart = (,) <$> mutateQuery <*> isSingleRecord
|
-- mutatePart = (,) <$> mutateQuery <*> 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
|
||||||
method = requestMethod httpRequest
|
-- method = requestMethod httpRequest
|
||||||
qParams = queryParams httpRequest
|
-- qParams = queryParams httpRequest
|
||||||
allFilters = whereFilters qParams
|
-- allFilters = whereFilters qParams
|
||||||
selectApiRequest = buildSelectApiRequest (cs method) schema rootTableName allFilters allRels qParams
|
-- selectApiRequest = buildSelectApiRequest (cs method) schema rootTableName allFilters allRels qParams
|
||||||
mutateTuple = buildMutateApiRequest (cs method) isCsv rootTableName reqBody allFilters
|
-- mutateTuple = buildMutateApiRequest (cs method) isCsv rootTableName reqBody allFilters
|
||||||
mutateApiRequest = fst <$> mutateTuple
|
-- mutateApiRequest = fst <$> mutateTuple
|
||||||
isSingleRecord = snd <$> mutateTuple
|
-- isSingleRecord = snd <$> mutateTuple
|
||||||
selectQuery = requestToQuery schema <$> selectApiRequest
|
-- selectQuery = requestToQuery schema <$> selectApiRequest
|
||||||
mutateQuery = requestToQuery schema <$> mutateApiRequest
|
-- mutateQuery = requestToQuery schema <$> mutateApiRequest
|
||||||
|
|
||||||
createReadStatement :: SqlQuery -> Maybe NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres
|
createReadStatement :: SqlQuery -> Maybe NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres
|
||||||
createReadStatement selectQuery range isSingle countTable asCsv =
|
createReadStatement selectQuery range isSingle countTable asCsv =
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ doesProc stmt qi = do
|
|||||||
return $ isJust row
|
return $ isJust row
|
||||||
|
|
||||||
doesProcExist :: QualifiedIdentifier -> H.Tx P.Postgres s Bool
|
doesProcExist :: QualifiedIdentifier -> H.Tx P.Postgres s Bool
|
||||||
doesProcExist = doesProc $ [H.stmt|
|
doesProcExist = doesProc [H.stmt|
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM pg_catalog.pg_namespace n
|
FROM pg_catalog.pg_namespace n
|
||||||
JOIN pg_catalog.pg_proc p
|
JOIN pg_catalog.pg_proc p
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import qualified Data.Csv as CSV
|
|||||||
import Data.List (find)
|
import Data.List (find)
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.Maybe (fromMaybe, isJust, isNothing,
|
import Data.Maybe (fromMaybe, isJust, isNothing,
|
||||||
listToMaybe)
|
listToMaybe, fromJust)
|
||||||
|
import Control.Monad (join)
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
@@ -23,14 +24,14 @@ type RequestBody = BL.ByteString
|
|||||||
data Action = ActionCreate | ActionRead
|
data Action = ActionCreate | ActionRead
|
||||||
| ActionUpdate | ActionDelete
|
| ActionUpdate | ActionDelete
|
||||||
| ActionInfo | ActionInvoke
|
| ActionInfo | ActionInvoke
|
||||||
| ActionUnknown BS.ByteString
|
| ActionUnknown BS.ByteString deriving Eq
|
||||||
-- | The target db object of a user action
|
-- | The target db object of a user action
|
||||||
data Target = TargetIdent QualifiedIdentifier
|
data Target = TargetIdent QualifiedIdentifier
|
||||||
| TargetRoot
|
| TargetRoot
|
||||||
| TargetUnknown [T.Text]
|
| TargetUnknown [T.Text]
|
||||||
-- | Enumeration of currently supported content types for
|
-- | Enumeration of currently supported content types for
|
||||||
-- route responses and upload payloads
|
-- route responses and upload payloads
|
||||||
data ContentType = ApplicationJSON | TextCSV
|
data ContentType = ApplicationJSON | TextCSV deriving Eq
|
||||||
-- | When Hasql supports the COPY command then we can
|
-- | When Hasql supports the COPY command then we can
|
||||||
-- have a special payload just for CSV, but until
|
-- have a special payload just for CSV, but until
|
||||||
-- then CSV is converted to a JSON array.
|
-- then CSV is converted to a JSON array.
|
||||||
@@ -54,19 +55,25 @@ data Intent = Intent {
|
|||||||
-- | The content type the client most desires (or JSON if undecided)
|
-- | The content type the client most desires (or JSON if undecided)
|
||||||
, iAccepts :: Either BS.ByteString ContentType
|
, iAccepts :: Either BS.ByteString ContentType
|
||||||
-- | Data sent by client and used for mutation actions
|
-- | Data sent by client and used for mutation actions
|
||||||
, iPayload :: Payload
|
, iPayload :: Maybe Payload
|
||||||
-- | If client wants created items echoed back
|
-- | If client wants created items echoed back
|
||||||
, iPreferRepresentation :: Bool
|
, iPreferRepresentation :: Bool
|
||||||
-- | If client wants first row as raw object
|
-- | If client wants first row as raw object
|
||||||
, iPreferSingular :: Bool
|
, iPreferSingular :: Bool
|
||||||
-- | Whether the client wants a result count (slower)
|
-- | Whether the client wants a result count (slower)
|
||||||
, iPreferCount :: Bool
|
, iPreferCount :: Bool
|
||||||
|
-- | Filters on the result ("id", "eq.10")
|
||||||
|
, iFilters :: [(String, String)]
|
||||||
|
-- | &select parameter used to shape the response
|
||||||
|
, iSelect :: String
|
||||||
|
-- | &order parameter
|
||||||
|
, iOrder :: Maybe String
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
userIntent :: Schema -> Request -> RequestBody -> Intent
|
userIntent :: Schema -> Request -> RequestBody -> Intent
|
||||||
userIntent schema req reqBody =
|
userIntent schema req reqBody =
|
||||||
let action = case requestMethod req of
|
let action = case method of
|
||||||
"GET" -> ActionRead
|
"GET" -> ActionRead
|
||||||
"POST" -> if isTargetingProc
|
"POST" -> if isTargetingProc
|
||||||
then ActionInvoke
|
then ActionInvoke
|
||||||
@@ -82,7 +89,11 @@ userIntent schema req reqBody =
|
|||||||
["rpc", proc] -> TargetIdent
|
["rpc", proc] -> TargetIdent
|
||||||
$ QualifiedIdentifier schema proc
|
$ QualifiedIdentifier schema proc
|
||||||
other -> TargetUnknown other
|
other -> TargetUnknown other
|
||||||
reqPayload = case pickContentType (lookupHeader "content-type") of
|
reqPayload = case action of
|
||||||
|
ActionCreate -> Just payload
|
||||||
|
ActionUpdate -> Just payload
|
||||||
|
_ -> Nothing
|
||||||
|
where payload = case pickContentType (lookupHeader "content-type") of
|
||||||
Right ApplicationJSON ->
|
Right ApplicationJSON ->
|
||||||
either (PayloadParseError . cs)
|
either (PayloadParseError . cs)
|
||||||
(PayloadJSON . pluralize)
|
(PayloadJSON . pluralize)
|
||||||
@@ -95,22 +106,32 @@ userIntent schema req reqBody =
|
|||||||
PayloadParseError $
|
PayloadParseError $
|
||||||
"Content-type not acceptable: " <> accept in
|
"Content-type not acceptable: " <> accept in
|
||||||
|
|
||||||
Intent action
|
Intent {
|
||||||
(if singular then Nothing else rangeRequested hdrs)
|
iAction = action
|
||||||
target
|
, iRange = if singular then Nothing else rangeRequested hdrs
|
||||||
(pickContentType $ lookupHeader "accept")
|
, iTarget = target
|
||||||
reqPayload
|
, iAccepts = pickContentType $ lookupHeader "accept"
|
||||||
(hasPrefer "return=representation")
|
, iPayload = reqPayload
|
||||||
singular
|
, iPreferRepresentation = hasPrefer "return=representation"
|
||||||
(not $ hasPrefer "count=none")
|
, iPreferSingular = singular
|
||||||
|
, iPreferCount = not $ hasPrefer "count=none"
|
||||||
|
, iFilters = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
|
||||||
|
, iSelect = if method == "DELETE"
|
||||||
|
then "*"
|
||||||
|
else fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
|
||||||
|
, iOrder = join $ lookup "order" qParams
|
||||||
|
}
|
||||||
|
|
||||||
where
|
where
|
||||||
path = pathInfo req
|
path = pathInfo req
|
||||||
|
method = requestMethod req
|
||||||
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
|
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
|
||||||
hdrs = requestHeaders req
|
hdrs = requestHeaders req
|
||||||
|
qParams = [(cs k, cs <$> v)|(k,v) <- queryString req]
|
||||||
lookupHeader = flip lookup hdrs
|
lookupHeader = flip lookup hdrs
|
||||||
hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
|
hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
|
||||||
singular = (hasPrefer "plurality=singular")
|
singular = hasPrefer "plurality=singular"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- PRIVATE ---------------------------------------------------------------
|
-- PRIVATE ---------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user