Handle overloaded function case

* Add test for params=single-object on GET

* Add tests for procs with DEFAULT args

* Add tests for overloaded functions

* Add test for PATCHing with an empty json array, this previously
  gave a "Something is wrong" error
This commit is contained in:
steve-chavez
2018-01-10 11:43:16 -05:00
committed by Steve Chávez
parent 38f3bcf4a6
commit f7e7834a1c
11 changed files with 147 additions and 97 deletions
+1
View File
@@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Computed columns now only work if they belong to the db-schema - @steve-chavez - Computed columns now only work if they belong to the db-schema - @steve-chavez
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez - To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez
## [0.4.4.0] - 2018-01-08 ## [0.4.4.0] - 2018-01-08
+14 -15
View File
@@ -95,8 +95,6 @@ data ApiRequest = ApiRequest {
, iHeaders :: [(Text, Text)] , iHeaders :: [(Text, Text)]
-- | Request Cookies -- | Request Cookies
, iCookies :: [(Text, Text)] , iCookies :: [(Text, Text)]
-- | Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
, iRpcQParams :: [(Text, Text)]
} }
-- | Examines HTTP request and translates it into user intent. -- | Examines HTTP request and translates it into user intent.
@@ -116,7 +114,6 @@ userApiRequest schema req reqBody
, iPreferSingleObjectParameter = singleObject , iPreferSingleObjectParameter = singleObject
, iPreferCount = hasPrefer "count=exact" , iPreferCount = hasPrefer "count=exact"
, iFilters = filters , iFilters = filters
, iRpcQParams = rpcQParams
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ] , iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams , iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ] , iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
@@ -130,6 +127,7 @@ userApiRequest schema req reqBody
, iCookies = fromMaybe [] $ parseCookiesText <$> lookupHeader "Cookie" , iCookies = fromMaybe [] $ parseCookiesText <$> lookupHeader "Cookie"
} }
where where
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
(filters, rpcQParams) = (filters, rpcQParams) =
case action of case action of
ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
@@ -141,20 +139,22 @@ userApiRequest schema req reqBody
isEmbedPath = T.isInfixOf "." isEmbedPath = T.isInfixOf "."
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
payload = payload =
case decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type" of case (decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type", action) of
CTApplicationJSON -> (_, ActionInvoke{isReadOnly=True}) ->
note "All object keys must match" . consPayloadJSON reqBody Right $ PayloadJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams)
(CTApplicationJSON, _) ->
note "All object keys must match" . payloadAttributes reqBody
=<< if BL.null reqBody && isTargetingProc =<< if BL.null reqBody && isTargetingProc
then Right emptyObject then Right emptyObject
else JSON.eitherDecode reqBody else JSON.eitherDecode reqBody
CTTextCSV -> do (CTTextCSV, _) -> do
json <- csvToJson <$> CSV.decodeByName reqBody json <- csvToJson <$> CSV.decodeByName reqBody
note "All lines must have same number of fields" $ consPayloadJSON (JSON.encode json) json note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
CTOther "application/x-www-form-urlencoded" -> (CTOther "application/x-www-form-urlencoded", _) ->
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
keys = S.fromList $ M.keys json in keys = S.fromList $ M.keys json in
Right $ PayloadJSON (JSON.encode json) PJObject keys Right $ PayloadJSON (JSON.encode json) PJObject keys
ct -> (ct, _) ->
Left $ toS $ "Content-Type not acceptable: " <> toMime ct Left $ toS $ "Content-Type not acceptable: " <> toMime ct
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges
action = action =
@@ -177,9 +177,8 @@ userApiRequest schema req reqBody
["rpc", proc] -> TargetProc ["rpc", proc] -> TargetProc
$ QualifiedIdentifier schema proc $ QualifiedIdentifier schema proc
other -> TargetUnknown other other -> TargetUnknown other
shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}] shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionInvoke{isReadOnly=False}, ActionInvoke{isReadOnly=True}]
relevantPayload | action == ActionInvoke{isReadOnly=True} = Nothing relevantPayload | shouldParsePayload = rightToMaybe payload
| shouldParsePayload = rightToMaybe payload
| otherwise = Nothing | otherwise = Nothing
path = pathInfo req path = pathInfo req
method = requestMethod req method = requestMethod req
@@ -274,8 +273,8 @@ csvToJson (_, vals) =
else JSON.String $ toS str else JSON.String $ toS str
) )
consPayloadJSON :: BL.ByteString -> JSON.Value -> Maybe PayloadJSON payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
consPayloadJSON raw json = payloadAttributes raw json =
-- Test that Array contains only Objects having the same keys -- Test that Array contains only Objects having the same keys
case json of case json of
JSON.Array arr -> JSON.Array arr ->
+41 -38
View File
@@ -38,7 +38,6 @@ import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure import PostgREST.DbStructure
import PostgREST.DbRequestBuilder( readRequest import PostgREST.DbRequestBuilder( readRequest
, mutateRequest , mutateRequest
, readRpcRequest
, fieldNames , fieldNames
) )
import PostgREST.Error ( simpleError, pgError import PostgREST.Error ( simpleError, pgError
@@ -79,35 +78,44 @@ postgrest conf refDbStructure pool worker =
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest)
let authed = containsRole eClaims let authed = containsRole eClaims
handleReq = runWithClaims conf eClaims (app dbStructure conf) apiRequest proc = case (iTarget apiRequest, iPayload apiRequest, iPreferSingleObjectParameter apiRequest) of
txMode = transactionMode dbStructure (TargetProc qi, Just PayloadJSON{pjKeys=pKeys}, s) -> findProc qi pKeys s $ dbProcs dbStructure
(iTarget apiRequest) (iAction apiRequest) _ -> Nothing
handleReq = runWithClaims conf eClaims (app dbStructure proc conf) apiRequest
txMode = transactionMode proc (iAction apiRequest)
response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
return $ either (pgError authed) identity response return $ either (pgError authed) identity response
when (responseStatus response == status503) worker when (responseStatus response == status503) worker
respond response respond response
transactionMode :: DbStructure -> Target -> Action -> H.Mode findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> M.HashMap Text [ProcDescription] -> Maybe ProcDescription
transactionMode structure target action = findProc qi payloadKeys paramsAsSingleObject allProcs =
let procs = M.lookup (qiName qi) allProcs in
-- Handle overloaded functions case
join $ (case length <$> procs of
Just 1 -> headMay -- if it's not an overloaded function then immediatly get the ProcDescription
_ -> find (\x ->
if paramsAsSingleObject
then length (pdArgs x) == 1 -- if the arg is not of json type let the db give the err
else payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> pdArgs x))
) <$> procs
transactionMode :: Maybe ProcDescription -> Action -> H.Mode
transactionMode proc action =
case action of case action of
ActionRead -> HT.Read ActionRead -> HT.Read
ActionInfo -> HT.Read ActionInfo -> HT.Read
ActionInspect -> HT.Read ActionInspect -> HT.Read
ActionInvoke{isReadOnly=False} -> ActionInvoke{isReadOnly=False} ->
let proc = let v = fromMaybe Volatile $ pdVolatility <$> proc in
case target of
(TargetProc qi) -> M.lookup (qiName qi) $
dbProcs structure
_ -> Nothing
v = fromMaybe Volatile $ pdVolatility <$> proc in
if v == Stable || v == Immutable if v == Stable || v == Immutable
then HT.Read then HT.Read
else HT.Write else HT.Write
ActionInvoke{isReadOnly=True} -> HT.Read ActionInvoke{isReadOnly=True} -> HT.Read
_ -> HT.Write _ -> HT.Write
app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response app :: DbStructure -> Maybe ProcDescription -> AppConfig -> ApiRequest -> H.Transaction Response
app dbStructure conf apiRequest = app dbStructure proc conf apiRequest =
case responseContentTypeOrError (iAccepts apiRequest) (iAction apiRequest) of case responseContentTypeOrError (iAccepts apiRequest) (iAction apiRequest) of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right contentType -> Right contentType ->
@@ -140,13 +148,13 @@ app dbStructure conf apiRequest =
case mutateSqlParts of case mutateSqlParts of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right (sq, mq) -> do Right (sq, mq) -> do
let (isSingle, rows) = case pType of let (isSingle, nRows) = case pType of
PJArray len -> (len == 1, len) PJArray len -> (len == 1, len)
PJObject -> (True, 1) PJObject -> (True, 1)
if contentType == CTSingularJSON if contentType == CTSingularJSON
&& not isSingle && not isSingle
&& iPreferRepresentation apiRequest == Full && iPreferRepresentation apiRequest == Full
then return $ singularityError (toInteger rows) then return $ singularityError (toInteger nRows)
else do else do
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself? let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
stm = createWriteStatement sq mq stm = createWriteStatement sq mq
@@ -163,7 +171,7 @@ app dbStructure conf apiRequest =
then Just $ toHeader contentType then Just $ toHeader contentType
else Nothing else Nothing
, Just . contentRangeH 1 0 $ , Just . contentRangeH 1 0 $
toInteger <$> if shouldCount then Just rows else Nothing toInteger <$> if shouldCount then Just nRows else Nothing
] ]
return . responseLBS status201 headers $ return . responseLBS status201 headers $
@@ -228,31 +236,28 @@ app dbStructure conf apiRequest =
let acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in let acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in
return $ responseLBS status200 [allOrigins, acceptH] "" return $ responseLBS status200 [allOrigins, acceptH] ""
(ActionInvoke _isReadOnly, TargetProc qi, payload) -> (ActionInvoke _, TargetProc qi, Just (PayloadJSON payload pType pKeys)) ->
let proc = M.lookup (qiName qi) allProcs let returnsScalar = case proc of
returnsScalar = case proc of
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
_ -> False _ -> False
rpcBinaryField = if returnsScalar rpcBinaryField = if returnsScalar
then Right Nothing then Right Nothing
else binaryField contentType =<< fldNames else binaryField contentType =<< fldNames
parts = (,,) <$> readSqlParts <*> rpcBinaryField <*> rpcQParams in parts = (,) <$> readSqlParts <*> rpcBinaryField in
case parts of case parts of
Left errorResponse -> return errorResponse Left errorResponse -> return errorResponse
Right ((q, cq), bField, params) -> do Right ((q, cq), bField) -> do
let (prms, keys, isObject) = case payload of let isObject = case pType of
Just (PayloadJSON p (PJArray _) ks) -> (p, ks, False) PJObject -> True
Just (PayloadJSON p PJObject ks) -> (p, ks, True) PJArray _ -> False
Nothing -> (JSON.encode $ M.fromList $ second JSON.toJSON <$> params, S.fromList $ fst <$> params, True)
singular = contentType == CTSingularJSON singular = contentType == CTSingularJSON
paramsAsSingleObject = iPreferSingleObjectParameter apiRequest specifiedPgArgs = filter ((`S.member` pKeys) . pgaName) $ fromMaybe [] (pdArgs <$> proc)
specifiedPgArgs = filter (flip S.member keys . pgaName) $ fromMaybe [] (pdArgs <$> proc) row <- H.query (toS payload) $
row <- H.query (toS prms) $
callProc qi specifiedPgArgs returnsScalar q cq shouldCount callProc qi specifiedPgArgs returnsScalar q cq shouldCount
singular paramsAsSingleObject singular (iPreferSingleObjectParameter apiRequest)
(contentType == CTTextCSV) (contentType == CTTextCSV)
(contentType == CTOctetStream) _isReadOnly bField (contentType == CTOctetStream) bField isObject
isObject (pgVersion dbStructure) (pgVersion dbStructure)
let (tableTotal, queryTotal, body, jsonHeaders) = let (tableTotal, queryTotal, body, jsonHeaders) =
fromMaybe (Just 0, 0, "[]", "[]") row fromMaybe (Just 0, 0, "[]", "[]") row
(status, contentRange) = rangeHeader queryTotal tableTotal (status, contentRange) = rangeHeader queryTotal tableTotal
@@ -273,7 +278,7 @@ app dbStructure conf apiRequest =
uri Nothing = ("http", host, port, "/") uri Nothing = ("http", host, port, "/")
uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b) uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b)
uri' = uri proxy uri' = uri proxy
encodeApi ti sd procs = encodeOpenAPI (M.elems procs) (toTableInfo ti) uri' sd (dbPrimaryKeys dbStructure) encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd (dbPrimaryKeys dbStructure)
body <- encodeApi <$> H.query schema accessibleTables <*> H.query schema schemaDescription <*> H.query schema accessibleProcs body <- encodeApi <$> H.query schema accessibleTables <*> H.query schema schemaDescription <*> H.query schema accessibleProcs
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
@@ -292,7 +297,6 @@ app dbStructure conf apiRequest =
filterCol :: Schema -> TableName -> Column -> Bool filterCol :: Schema -> TableName -> Column -> Bool
filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb
allPrKeys = dbPrimaryKeys dbStructure allPrKeys = dbPrimaryKeys dbStructure
allProcs = dbProcs dbStructure
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
shouldCount = iPreferCount apiRequest shouldCount = iPreferCount apiRequest
schema = toS $ configSchema conf schema = toS $ configSchema conf
@@ -304,11 +308,10 @@ app dbStructure conf apiRequest =
status = rangeStatus lower upper (toInteger <$> tableTotal) status = rangeStatus lower upper (toInteger <$> tableTotal)
in (status, contentRange) in (status, contentRange)
readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) allProcs apiRequest readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) proc apiRequest
fldNames = fieldNames <$> readReq fldNames = fieldNames <$> readReq
readDbRequest = DbRead <$> readReq readDbRequest = DbRead <$> readReq
mutateDbRequest = DbMutate <$> (mutateRequest apiRequest =<< fldNames) mutateDbRequest = DbMutate <$> (mutateRequest apiRequest =<< fldNames)
rpcQParams = readRpcRequest apiRequest
selectQuery = requestToQuery schema False <$> readDbRequest selectQuery = requestToQuery schema False <$> readDbRequest
mutateQuery = requestToQuery schema False <$> mutateDbRequest mutateQuery = requestToQuery schema False <$> mutateDbRequest
countQuery = requestToCountQuery schema <$> readDbRequest countQuery = requestToCountQuery schema <$> readDbRequest
+5 -12
View File
@@ -3,7 +3,6 @@
module PostgREST.DbRequestBuilder ( module PostgREST.DbRequestBuilder (
readRequest readRequest
, mutateRequest , mutateRequest
, readRpcRequest
, fieldNames , fieldNames
) where ) where
@@ -37,8 +36,8 @@ import Protolude hiding (from, dropWhile, drop)
import Text.Regex.TDFA ((=~)) import Text.Regex.TDFA ((=~))
import Unsafe (unsafeHead) import Unsafe (unsafeHead)
readRequest :: Maybe Integer -> [Relation] -> M.HashMap Text ProcDescription -> ApiRequest -> Either Response ReadRequest readRequest :: Maybe Integer -> [Relation] -> Maybe ProcDescription -> ApiRequest -> Either Response ReadRequest
readRequest maxRows allRels allProcs apiRequest = readRequest maxRows allRels proc apiRequest =
mapLeft apiRequestError $ mapLeft apiRequestError $
treeRestrictRange maxRows =<< treeRestrictRange maxRows =<<
augumentRequestWithJoin schema relations =<< augumentRequestWithJoin schema relations =<<
@@ -48,13 +47,12 @@ readRequest maxRows allRels allProcs apiRequest =
let target = iTarget apiRequest in let target = iTarget apiRequest in
case target of case target of
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t) (TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
(TargetProc (QualifiedIdentifier s proc) ) -> Just (s, tName) (TargetProc (QualifiedIdentifier s pName) ) -> Just (s, tName)
where where
retType = pdReturnType <$> M.lookup proc allProcs tName = case pdReturnType <$> proc of
tName = case retType of
Just (SetOf (Composite qi)) -> qiName qi Just (SetOf (Composite qi)) -> qiName qi
Just (Single (Composite qi)) -> qiName qi Just (Single (Composite qi)) -> qiName qi
_ -> proc _ -> pName
_ -> Nothing _ -> Nothing
@@ -327,11 +325,6 @@ mutateRequest apiRequest fldNames = mapLeft apiRequestError $
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest) (mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst) onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
readRpcRequest :: ApiRequest -> Either Response [RpcQParam]
readRpcRequest apiRequest = mapLeft apiRequestError rpcQParams
where
rpcQParams = mapM pRequestRpcQParam $ iRpcQParams apiRequest
fieldNames :: ReadRequest -> [FieldName] fieldNames :: ReadRequest -> [FieldName]
fieldNames (Node (sel, _) forest) = fieldNames (Node (sel, _) forest) =
map (fst . view _1) (select sel) ++ map colName fks map (fst . view _1) (select sel) ++ map colName fks
+5 -4
View File
@@ -103,9 +103,10 @@ decodeSynonyms cols =
<*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text
decodeProcs :: HD.Result (M.HashMap Text ProcDescription) decodeProcs :: HD.Result (M.HashMap Text [ProcDescription])
decodeProcs = decodeProcs =
M.fromList . map addName <$> HD.rowsList tblRow -- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowsList tblRow
where where
tblRow = ProcDescription tblRow = ProcDescription
<$> HD.value HD.text <$> HD.value HD.text
@@ -152,10 +153,10 @@ decodeProcs =
| v == 's' = Stable | v == 's' = Stable
| otherwise = Volatile -- only 'v' can happen here | otherwise = Volatile -- only 'v' can happen here
allProcs :: H.Query Schema (M.HashMap Text ProcDescription) allProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
allProcs = H.statement (toS procsSqlQuery) (HE.value HE.text) decodeProcs True allProcs = H.statement (toS procsSqlQuery) (HE.value HE.text) decodeProcs True
accessibleProcs :: H.Query Schema (M.HashMap Text ProcDescription) accessibleProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
accessibleProcs = H.statement (toS sql) (HE.value HE.text) decodeProcs True accessibleProcs = H.statement (toS sql) (HE.value HE.text) decodeProcs True
where where
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')" sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
-6
View File
@@ -47,12 +47,6 @@ pRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree
-- Concat op and v to make pLogicTree argument regular, in the form of "?and=and(.. , ..)" instead of "?and=(.. , ..)" -- Concat op and v to make pLogicTree argument regular, in the form of "?and=and(.. , ..)" instead of "?and=(.. , ..)"
logicTree = join $ parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") . toS <$> ((<>) <$> op <*> pure v) logicTree = join $ parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") . toS <$> ((<>) <$> op <*> pure v)
pRequestRpcQParam :: (Text, Text) -> Either ApiRequestError RpcQParam
pRequestRpcQParam (k, v) = mapError $ (,) <$> name <*> val
where
name = parse pFieldName ("failed to parse rpc arg name (" ++ toS k ++ ")") $ toS k
val = toS <$> parse (many anyChar) ("failed to parse rpc arg value (" ++ toS v ++ ")") v
ws :: Parser Text ws :: Parser Text
ws = toS <$> many (oneOf " \t") ws = toS <$> many (oneOf " \t")
+10 -9
View File
@@ -135,9 +135,9 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString) type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool -> callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool ->
Bool -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> PgVersion -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> PgVersion ->
H.Query ByteString (Maybe ProcResults) H.Query ByteString (Maybe ProcResults)
callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary isReadOnly binaryField isObject pgVer = callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary binaryField isObject pgVer =
unicodeStatement sql (HE.value HE.unknown) decodeProc True unicodeStatement sql (HE.value HE.unknown) decodeProc True
where where
sql = sql =
@@ -164,15 +164,15 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para
{responseHeaders} AS response_headers {responseHeaders} AS response_headers
FROM ({selectQuery}) _postgrest_t;|] FROM ({selectQuery}) _postgrest_t;|]
(argsRecord, args) | paramsAsSingleObject && not isReadOnly = ("_args_record AS (SELECT NULL)", "$1::json") (argsRecord, args) | paramsAsSingleObject = ("_args_record AS (SELECT NULL)", "$1::json")
| null pgArgs = (ignoredBody, "") | null pgArgs = (ignoredBody, "")
| otherwise = ( | otherwise = (
"_args_record AS ( "<> unwords [
"SELECT * FROM " <> (if isObject then "json_to_record" else "json_to_recordset") <> "_args_record AS (",
"($1) AS _(" <> intercalate ", " ((\a -> pgaName a <> " " <> pgaType a) <$> pgArgs) <> ")" <> "SELECT * FROM " <> (if isObject then "json_to_record" else "json_to_recordset") <> "($1)",
")" "AS _(" <> intercalate ", " ((\a -> pgaName a <> " " <> pgaType a) <$> pgArgs) <> ")",
, intercalate ", " ((\a -> pgaName a <> " := (SELECT " <> pgaName a <> " FROM _args_record)") <$> pgArgs) ")"]
) , intercalate ", " ((\a -> pgaName a <> " := (SELECT " <> pgaName a <> " FROM _args_record)") <$> pgArgs))
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
_procName = qiName qi _procName = qiName qi
responseHeaders = responseHeaders =
@@ -322,6 +322,7 @@ requestToQuery schema _ (DbMutate (Delete mainTbl logicForest returnings)) =
-- Due to the use of the `unknown` encoder we need to cast '$1' when the value is not used in the main query -- Due to the use of the `unknown` encoder we need to cast '$1' when the value is not used in the main query
-- otherwise the query will err with a `could not determine data type of parameter $1`. -- otherwise the query will err with a `could not determine data type of parameter $1`.
-- This happens because `unknown` relies on the context to determine the value type. -- This happens because `unknown` relies on the context to determine the value type.
-- The error also happens on raw libpq used with C.
ignoredBody :: SqlFragment ignoredBody :: SqlFragment
ignoredBody = "ignored_body AS (SELECT $1::text) " ignoredBody = "ignored_body AS (SELECT $1::text) "
+14 -6
View File
@@ -30,7 +30,8 @@ data DbStructure = DbStructure {
, dbColumns :: [Column] , dbColumns :: [Column]
, dbRelations :: [Relation] , dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey] , dbPrimaryKeys :: [PrimaryKey]
, dbProcs :: M.HashMap Text ProcDescription -- ProcDescription is a list because a function can be overloaded
, dbProcs :: M.HashMap Text [ProcDescription]
, pgVersion :: PgVersion , pgVersion :: PgVersion
} deriving (Show, Eq) } deriving (Show, Eq)
@@ -38,14 +39,14 @@ data PgArg = PgArg {
pgaName :: Text pgaName :: Text
, pgaType :: Text , pgaType :: Text
, pgaReq :: Bool , pgaReq :: Bool
} deriving (Show, Eq) } deriving (Show, Eq, Ord)
data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier deriving (Eq, Show) data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier deriving (Eq, Show, Ord)
data RetType = Single PgType | SetOf PgType deriving (Eq, Show) data RetType = Single PgType | SetOf PgType deriving (Eq, Show, Ord)
data ProcVolatility = Volatile | Stable | Immutable data ProcVolatility = Volatile | Stable | Immutable
deriving (Eq, Show) deriving (Eq, Show, Ord)
data ProcDescription = ProcDescription { data ProcDescription = ProcDescription {
pdName :: Text pdName :: Text
@@ -55,6 +56,13 @@ data ProcDescription = ProcDescription {
, pdVolatility :: ProcVolatility , pdVolatility :: ProcVolatility
} deriving (Show, Eq) } deriving (Show, Eq)
-- Order by least number of args in the case of overloaded functions
instance Ord ProcDescription where
ProcDescription name1 des1 args1 rt1 vol1 `compare` ProcDescription name2 des2 args2 rt2 vol2
| name1 == name2 && length args1 < length args2 = LT
| name1 == name2 && length args1 > length args2 = GT
| otherwise = (name1, des1, args1, rt1, vol1) `compare` (name2, des2, args2, rt2, vol2)
type Schema = Text type Schema = Text
type TableName = Text type TableName = Text
type SqlQuery = Text type SqlQuery = Text
@@ -111,7 +119,7 @@ data OrderTerm = OrderTerm {
data QualifiedIdentifier = QualifiedIdentifier { data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: Schema qiSchema :: Schema
, qiName :: TableName , qiName :: TableName
} deriving (Show, Eq) } deriving (Show, Eq, Ord)
data RelationType = Child | Parent | Many | Root deriving (Show, Eq) data RelationType = Child | Parent | Many | Root deriving (Show, Eq)
+9 -1
View File
@@ -416,13 +416,21 @@ spec = do
-- put value back for other tests -- put value back for other tests
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |] void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]
it "makes no updates and returns 204, when patching with an empty json object" $ do it "makes no updates and returns 204, when patching with an empty json object/array" $ do
request methodPatch "/items" [] [json| {} |] request methodPatch "/items" [] [json| {} |]
`shouldRespondWith` "" `shouldRespondWith` ""
{ {
matchStatus = 204, matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"] matchHeaders = ["Content-Range" <:> "*/*"]
} }
request methodPatch "/items" [] [json| [] |]
`shouldRespondWith` ""
{
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
get "/items" `shouldRespondWith` get "/items" `shouldRespondWith`
[json|[{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15},{id:16},{"id":2},{"id":1}]|] [json|[{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15},{id:16},{"id":2},{"id":1}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
+28 -3
View File
@@ -275,8 +275,14 @@ spec =
[json|[{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}]|] { matchHeaders = [matchContentTypeJson] } [json|[{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}]|] { matchHeaders = [matchContentTypeJson] }
it "returns a row result when there are many INOUT params" $ it "returns a row result when there are many INOUT params" $
get "/rpc/many_inout_params?num=1&str=two" `shouldRespondWith` get "/rpc/many_inout_params?num=1&str=two&b=false" `shouldRespondWith`
[json| [{"num":1,"str":"two","b":true}]|] { matchHeaders = [matchContentTypeJson] } [json| [{"num":1,"str":"two","b":false}]|] { matchHeaders = [matchContentTypeJson] }
it "can handle procs with args that have a DEFAULT value" $ do
get "/rpc/many_inout_params?num=1&str=two" `shouldRespondWith`
[json| [{"num":1,"str":"two","b":true}]|] { matchHeaders = [matchContentTypeJson] }
get "/rpc/three_defaults?b=4" `shouldRespondWith`
[json|8|] { matchHeaders = [matchContentTypeJson] }
it "can map a RAISE error code and message to a http status" $ it "can map a RAISE error code and message to a http status" $
get "/rpc/raise_pt402" get "/rpc/raise_pt402"
@@ -291,7 +297,7 @@ spec =
context "expects a single json object" $ do context "expects a single json object" $ do
it "does not expand posted json into parameters" $ it "does not expand posted json into parameters" $
request methodPost "/rpc/singlejsonparam" request methodPost "/rpc/singlejsonparam"
[("Prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith` [("prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`
[json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
@@ -304,6 +310,25 @@ spec =
, "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |] , "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
it "works with GET" $
request methodGet "/rpc/singlejsonparam?p1=1&p2=text" [("Prefer","params=single-object")] ""
`shouldRespondWith` [json|{ "p1": "1", "p2": "text"}|]
{ matchHeaders = [matchContentTypeJson] }
it "should work with an overloaded function" $ do
get "/rpc/overloaded" `shouldRespondWith`
[json|[{ "overloaded": 1 },
{ "overloaded": 2 },
{ "overloaded": 3 }]|]
{ matchHeaders = [matchContentTypeJson] }
request methodPost "/rpc/overloaded" [("Prefer","params=single-object")]
[json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]
`shouldRespondWith`
[json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/overloaded?a=1&b=2" `shouldRespondWith` [str|3|]
get "/rpc/overloaded?a=1&b=2&c=3" `shouldRespondWith` [str|"123"|]
context "only for POST rpc" $ context "only for POST rpc" $
it "gives a parse filter error if GET style proc args are specified" $ it "gives a parse filter error if GET style proc args are specified" $
post "/rpc/sayhello?name=John" [json|{}|] `shouldRespondWith` 400 post "/rpc/sayhello?name=John" [json|{}|] `shouldRespondWith` 400
+20 -3
View File
@@ -1325,6 +1325,23 @@ $$ language sql;
create or replace function test.set_cookie_twice() returns void as $$ create or replace function test.set_cookie_twice() returns void as $$
set local "response.headers" = '[{"Set-Cookie": "sessionid=38afes7a8; HttpOnly; Path=/"}, {"Set-Cookie": "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"}]'; set local "response.headers" = '[{"Set-Cookie": "sessionid=38afes7a8; HttpOnly; Path=/"}, {"Set-Cookie": "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"}]';
$$ language sql; $$ language sql;
--
-- PostgreSQL database dump complete create or replace function test.three_defaults(a int default 1, b int default 2, c int default 3) returns int as $$
-- select a + b + c
$$ language sql;
create or replace function test.overloaded() returns setof int as $$
values (1), (2), (3);
$$ language sql;
create or replace function test.overloaded(pg_catalog.json) returns table(x int, y text) as $$
select * from json_to_recordset($1) as r(x int, y text);
$$ language sql;
create or replace function test.overloaded(a int, b int) returns int as $$
select a + b
$$ language sql;
create or replace function test.overloaded(a text, b text, c text) returns text as $$
select a || b || c
$$ language sql;