feat: RPC POST for function w/single unnamed param

For POST on RPC, allows:

* passing a json object without using `Prefer: params=single-object`
  The function must be defined with a single unnamed json param and
  `Content-Type: application/json` must be specified.

* uploading binary to a function
  The function must be defined with a single unnamed bytea param and
  `Content-Type: application/octet-stream` must be specified.

* uploading raw text to a function
  The function must be defined with a single unnamed text param and
  `Content-Type: text/plain` must be specified.

BREAKING CHANGE If there's a function "my_func" having a single
unnamed json param and other overloaded pairs(with any number of
params), PostgREST won't be able to resolve a POST request to
"my_func". For solving this, you can name the unnamed json param.

my_func(json) -> my_func(prm json)
This commit is contained in:
steve-chavez
2021-08-30 18:17:59 -05:00
committed by Steve Chavez
parent caaa34b5de
commit d4c6abbaec
11 changed files with 209 additions and 76 deletions
+7
View File
@@ -9,10 +9,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1783, Include partitioned tables into the schema cache. Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables - @laurenceisla - #1783, Include partitioned tables into the schema cache. Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables - @laurenceisla
- #1878, Add Retry-After hint header when in recovery mode - @gautam1168 - #1878, Add Retry-After hint header when in recovery mode - @gautam1168
- #1735, Allow calling function with single unnamed param through RPC POST. - @steve-chavez
+ Enables calling a function with a single json parameter without using `Prefer: params=single-object`
+ Enables uploading bytea to a function with `Content-Type: application/octet-stream`
+ Enables uploading raw text to a function with `Content-Type: text/plain`
### Fixed ### Fixed
- #1871, Fix OpenAPI missing default values for String types and identify Array types as "array" instead of "string" - @laurenceisla - #1871, Fix OpenAPI missing default values for String types and identify Array types as "array" instead of "string" - @laurenceisla
### Changed
- #1927, Overloaded Functions: If there's a function "my_func" having a single unnamed json param and other overloaded pairs(with any number of params), PostgREST won't be able to resolve a POST request to "my_func". For solving this, you can name the unnamed json param `my_func(json) -> my_func(prm json)`.
## [8.0.0] - 2021-07-25 ## [8.0.0] - 2021-07-25
+10 -6
View File
@@ -60,7 +60,7 @@ data ApiRequestError
| NoRelBetween Text Text | NoRelBetween Text Text
| AmbiguousRelBetween Text Text [Relationship] | AmbiguousRelBetween Text Text [Relationship]
| AmbiguousRpc [ProcDescription] | AmbiguousRpc [ProcDescription]
| NoRpc Text Text [Text] Bool | NoRpc Text Text [Text] Bool ContentType Bool
| InvalidFilters | InvalidFilters
| UnacceptableSchema [Text] | UnacceptableSchema [Text]
| ContentTypeError [ByteString] | ContentTypeError [ByteString]
@@ -99,14 +99,18 @@ instance JSON.ToJSON ApiRequestError where
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text), "message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
"details" .= (compressedRel <$> rels) ] "details" .= (compressedRel <$> rels) ]
toJSON (AmbiguousRpc procs) = JSON.object [ toJSON (AmbiguousRpc procs) = JSON.object [
"hint" .= ("Overloaded functions with the same parameter name but different types are not supported" :: Text), "hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text),
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])] "message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])]
toJSON (NoRpc schema procName payloadKeys hasPreferSingleObject) = JSON.object [ toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost) =
let prms = "(" <> T.intercalate ", " argumentKeys <> ")" in JSON.object [
"hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text), "hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text),
"message" .= ("Could not find the " <> schema <> "." <> procName <> "message" .= ("Could not find the " <> schema <> "." <> procName <>
(if hasPreferSingleObject (case (hasPreferSingleObject, isInvPost, contentType) of
then " function with a single json or jsonb parameter" (True, _, _) -> " function with a single json or jsonb parameter"
else "(" <> T.intercalate ", " payloadKeys <> ")" <> " function") <> (_, True, CTTextPlain) -> " function with a single unnamed text parameter"
(_, True, CTOctetStream) -> " function with a single unnamed bytea parameter"
(_, True, CTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
_ -> prms <> " function") <>
" in the schema cache")] " in the schema cache")]
toJSON UnsupportedVerb = JSON.object [ toJSON UnsupportedVerb = JSON.object [
"message" .= ("Unsupported HTTP verb" :: Text)] "message" .= ("Unsupported HTTP verb" :: Text)]
+11 -10
View File
@@ -116,26 +116,27 @@ mutateRequestToQuery (Delete mainQi logicForest returnings) =
H.sql (returningF mainQi returnings) H.sql (returningF mainQi returnings)
requestToCallProcQuery :: CallRequest -> H.Snippet requestToCallProcQuery :: CallRequest -> H.Snippet
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall singleParam returnings) = requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall returnings) =
prmsCTE <> argsBody prmsCTE <> argsBody
where where
(prmsCTE, argFrag) (prmsCTE, argFrag) = case params of
| null params = (mempty, mempty) OnePosParam prm -> ("WITH pgrst_args AS (SELECT NULL)", singleParameter args (encodeUtf8 $ ppType prm))
| singleParam = ("WITH pgrst_args AS (SELECT NULL)", jsonPlaceHolder args) KeyParams [] -> (mempty, mempty)
| otherwise = ( KeyParams prms -> (
"WITH " <> normalizedBody args <> ", " <> "WITH " <> normalizedBody args <> ", " <>
H.sql ( H.sql (
BS.unwords [ BS.unwords [
"pgrst_args AS (", "pgrst_args AS (",
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtParams (const mempty) (\a -> " " <> encodeUtf8 (ppType a)) <> ")", "SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtParams prms (const mempty) (\a -> " " <> encodeUtf8 (ppType a)) <> ")",
")"]) ")"])
, H.sql $ if multipleCall , H.sql $ if multipleCall
then fmtParams varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a)) then fmtParams prms varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
else fmtParams varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)") else fmtParams prms varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
) )
fmtParams :: (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment fmtParams :: [ProcParam] -> (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
fmtParams prmFragPre prmFragSuf = BS.intercalate ", " ((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> params) fmtParams prms prmFragPre prmFragSuf = BS.intercalate ", "
((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> prms)
varadicPrefix :: ProcParam -> SqlFragment varadicPrefix :: ProcParam -> SqlFragment
varadicPrefix a = if ppVar a then "VARIADIC " else mempty varadicPrefix a = if ppVar a then "VARIADIC " else mempty
+10 -7
View File
@@ -16,7 +16,6 @@ module PostgREST.Query.SqlFragment
, countF , countF
, fromQi , fromQi
, ftsOperators , ftsOperators
, jsonPlaceHolder
, limitOffsetF , limitOffsetF
, locationF , locationF
, normalizedBody , normalizedBody
@@ -31,6 +30,7 @@ module PostgREST.Query.SqlFragment
, responseStatusF , responseStatusF
, returningF , returningF
, selectBody , selectBody
, singleParameter
, sourceCTEName , sourceCTEName
, unknownEncoder , unknownEncoder
, intercalateSnippet , intercalateSnippet
@@ -105,9 +105,10 @@ ftsOperators = HM.fromList [
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads -- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
-- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays -- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays
-- We do this in SQL to avoid processing the JSON in application code -- We do this in SQL to avoid processing the JSON in application code
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
normalizedBody :: Maybe BL.ByteString -> H.Snippet normalizedBody :: Maybe BL.ByteString -> H.Snippet
normalizedBody body = normalizedBody body =
"pgrst_payload AS (SELECT " <> jsonPlaceHolder body <> " AS json_data), " <> "pgrst_payload AS (SELECT " <> jsonPlaceHolder <> " AS json_data), " <>
H.sql (BS.unwords [ H.sql (BS.unwords [
"pgrst_body AS (", "pgrst_body AS (",
"SELECT", "SELECT",
@@ -116,12 +117,14 @@ normalizedBody body =
"ELSE json_build_array(json_data)", "ELSE json_build_array(json_data)",
"END AS val", "END AS val",
"FROM pgrst_payload)"]) "FROM pgrst_payload)"])
where
jsonPlaceHolder = H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
-- | Equivalent to "$1::json" singleParameter :: Maybe BL.ByteString -> ByteString -> H.Snippet
-- | TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body singleParameter body typ =
jsonPlaceHolder :: Maybe BL.ByteString -> H.Snippet if typ == "bytea"
jsonPlaceHolder body = then H.encoderAndParam (HE.nullable HE.bytea) (toS <$> body) -- needed because bytea fails with HE.unknown(pg tries to utf8 encode)
H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json" else H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::" <> H.sql typ
selectBody :: SqlFragment selectBody :: SqlFragment
selectBody = "(SELECT val FROM pgrst_body)" selectBody = "(SELECT val FROM pgrst_body)"
+38 -25
View File
@@ -13,7 +13,7 @@ module PostgREST.Request.ApiRequest
, ContentType(..) , ContentType(..)
, Action(..) , Action(..)
, Target(..) , Target(..)
, PayloadJSON(..) , Payload(..)
, userApiRequest , userApiRequest
) where ) where
@@ -74,18 +74,19 @@ import Protolude.Conv (toS)
type RequestBody = BL.ByteString type RequestBody = BL.ByteString
data PayloadJSON data Payload
= ProcessedJSON -- ^ Cached attributes of a JSON payload = ProcessedJSON -- ^ Cached attributes of a JSON payload
{ pjRaw :: BL.ByteString { payRaw :: BL.ByteString
-- ^ This is the raw ByteString that comes from the request body. We -- ^ This is the raw ByteString that comes from the request body. We
-- cache this instead of an Aeson Value because it was detected that for -- cache this instead of an Aeson Value because it was detected that for
-- large payloads the encoding had high memory usage, see -- large payloads the encoding had high memory usage, see
-- https://github.com/PostgREST/postgrest/pull/1005 for more details -- https://github.com/PostgREST/postgrest/pull/1005 for more details
, pjKeys :: S.Set Text , payKeys :: S.Set Text
-- ^ Keys of the object or if it's an array these keys are guaranteed to -- ^ Keys of the object or if it's an array these keys are guaranteed to
-- be the same across all its objects -- be the same across all its objects
} }
| RawJSON { pjRaw :: BL.ByteString } | RawJSON { payRaw :: BL.ByteString }
| RawPay { payRaw :: BL.ByteString }
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
-- | Types of things a user wants to do to tables/views/procs -- | Types of things a user wants to do to tables/views/procs
@@ -124,7 +125,7 @@ toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
prmIsVariadic prm = isJust $ find (\ProcParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc prmIsVariadic prm = isJust $ find (\ProcParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"} -- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> PayloadJSON jsonRpcParams :: ProcDescription -> [(Text, Text)] -> Payload
jsonRpcParams proc prms = jsonRpcParams proc prms =
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms) ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms)
@@ -136,7 +137,7 @@ jsonRpcParams proc prms =
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
mergeParams v _ = v -- repeated params for non-variadic parameters are not merged mergeParams v _ = v -- repeated params for non-variadic parameters are not merged
targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe PayloadJSON targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe Payload
targetToJsonRpcParams target params = targetToJsonRpcParams target params =
case target of case target of
Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params
@@ -154,7 +155,7 @@ data ApiRequest = ApiRequest {
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response , iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level , iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table , iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
, iPayload :: Maybe PayloadJSON -- ^ Data sent by client and used for mutation actions , iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back , iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure , iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count , iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
@@ -253,7 +254,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
isTargetingDefaultSpec = case path of isTargetingDefaultSpec = case path of
PathInfo{pIsDefaultSpec=True} -> True PathInfo{pIsDefaultSpec=True} -> True
_ -> False _ -> False
contentType = ContentType.decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type" contentType = maybe CTApplicationJSON ContentType.decodeContentType $ lookupHeader "content-type"
columns columns
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams) | action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
| otherwise = Nothing | otherwise = Nothing
@@ -264,9 +265,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams (_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams
(CTUrlEncoded, _) -> S.fromList $ map (toS . fst) $ parseSimpleQuery $ toS reqBody (CTUrlEncoded, _) -> S.fromList $ map (toS . fst) $ parseSimpleQuery $ toS reqBody
_ -> case (relevantPayload, fromRight Nothing parsedColumns) of _ -> case (relevantPayload, fromRight Nothing parsedColumns) of
(Just ProcessedJSON{pjKeys}, _) -> pjKeys (Just ProcessedJSON{payKeys}, _) -> payKeys
(Just RawJSON{}, Just cls) -> cls (Just RawJSON{}, Just cls) -> cls
_ -> S.empty _ -> S.empty
payload = case contentType of payload = case contentType of
CTApplicationJSON -> CTApplicationJSON ->
if isJust columns if isJust columns
@@ -282,7 +283,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> parseSimpleQuery (toS reqBody) in let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> parseSimpleQuery (toS reqBody) in
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap) Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
ct -> ct ->
Left $ toS $ "Content-Type not acceptable: " <> ContentType.toMime ct if isTargetingProc && ct `elem` [CTTextPlain, CTOctetStream]
then Right $ RawPay reqBody
else Left $ toS $ "Content-Type not acceptable: " <> ContentType.toMime ct
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
action = action =
case method of case method of
@@ -321,7 +324,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
schema = fromMaybe defaultSchema profile schema = fromMaybe defaultSchema profile
target = target =
let let
callFindProc procSch procNam = findProc (QualifiedIdentifier procSch procNam) payloadColumns (hasPrefer (show SingleObject)) $ dbProcs dbStructure callFindProc procSch procNam = findProc
(QualifiedIdentifier procSch procNam) payloadColumns (hasPrefer (show SingleObject)) (dbProcs dbStructure)
contentType (action == ActionInvoke InvPost)
in in
case path of case path of
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec} PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
@@ -426,7 +431,7 @@ csvToJson (_, vals) =
else JSON.String $ toS str else JSON.String $ toS str
) )
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
payloadAttributes 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
@@ -482,26 +487,34 @@ rawContentTypes AppConfig{..} =
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded, Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type. the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
-} -}
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> Either ApiRequestError ProcDescription findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ContentType -> Bool -> Either ApiRequestError ProcDescription
findProc qi argumentsKeys paramsAsSingleObject allProcs = findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
case matchProc of case matchProc of
[] -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject [] -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentType isInvPost
[proc] -> Right proc [proc] -> Right proc
procs -> Left $ AmbiguousRpc (toList procs) procs -> Left $ AmbiguousRpc (toList procs)
where where
matchProc = filter matchesParams $ M.lookupDefault mempty qi allProcs -- first find the proc by name matchProc = filter matchesParams $ M.lookupDefault mempty qi allProcs -- first find the proc by name
matchesParams proc = matchesParams proc =
let params = pdParams proc in let params = pdParams proc in
-- here we don't match by argument key(there isn't one) but by the single parameter type -- exceptional case for Prefer: params=single-object
if paramsAsSingleObject then if paramsAsSingleObject
case params of then length params == 1 && (ppType <$> headMay params) `elem` [Just "json", Just "jsonb"]
[prm] -> ppType prm `elem` ["json", "jsonb"] -- If the function has no parameters, the arguments keys must be empty as well
_ -> False else if null params
then null argumentsKeys
-- If the function is called with post and has a single unnamed parameter
-- it can be called depending on content type and the parameter type
else if isInvPost && length params == 1 && (ppName <$> headMay params) == Just mempty
then case headMay params of
Just prm | contentType == CTApplicationJSON -> ppType prm `elem` ["json", "jsonb"]
| contentType == CTTextPlain -> ppType prm == "text"
| contentType == CTOctetStream -> ppType prm == "bytea"
| otherwise -> False
Nothing -> False
-- A function has optional and required parameters. Optional parameters have a default value and -- A function has optional and required parameters. Optional parameters have a default value and
-- don't require arguments for the function to be executed, required parameters must have an argument present. -- don't require arguments for the function to be executed, required parameters must have an argument present.
else case L.partition ppReq params of else case L.partition ppReq params of
-- If the function has no parameters, the arguments keys must be empty as well
([], []) -> null argumentsKeys
-- If the function only has required parameters, the arguments keys must match those parameters -- If the function only has required parameters, the arguments keys must match those parameters
(reqParams, []) -> argumentsKeys == S.fromList (ppName <$> reqParams) (reqParams, []) -> argumentsKeys == S.fromList (ppName <$> reqParams)
-- If the function only has optional parameters, the arguments keys can match none or any of them(a subset) -- If the function only has optional parameters, the arguments keys can match none or any of them(a subset)
+10 -9
View File
@@ -48,7 +48,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
restrictRange) restrictRange)
import PostgREST.Request.ApiRequest (Action (..), import PostgREST.Request.ApiRequest (Action (..),
ApiRequest (..), ApiRequest (..),
PayloadJSON (..)) Payload (..))
import PostgREST.Request.Parsers import PostgREST.Request.Parsers
import PostgREST.Request.Preferences import PostgREST.Request.Preferences
@@ -344,24 +344,25 @@ mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
-- update/delete filters can be only on the root table -- update/delete filters can be only on the root table
(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)
body = pjRaw <$> iPayload apiRequest body = payRaw <$> iPayload apiRequest -- the body is assumed to be json at this stage(ApiRequest validates)
callRequest :: ProcDescription -> ApiRequest -> ReadRequest -> CallRequest callRequest :: ProcDescription -> ApiRequest -> ReadRequest -> CallRequest
callRequest proc apiReq readReq = FunctionCall { callRequest proc apiReq readReq = FunctionCall {
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc) funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
, funCParams = specifiedParams , funCParams = callParams
, funCArgs = pjRaw <$> iPayload apiReq , funCArgs = payRaw <$> iPayload apiReq
, funCScalar = procReturnsScalar proc , funCScalar = procReturnsScalar proc
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects , funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
, funCSingleParam = paramsAsSingleObject
, funCReturning = returningCols readReq [] , funCReturning = returningCols readReq []
} }
where where
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
specifiedParams = callParams = case pdParams proc of
if paramsAsSingleObject [prm] | paramsAsSingleObject -> OnePosParam prm
then pdParams proc | ppName prm == mempty -> OnePosParam prm
else filter (\x -> ppName x `S.member` iColumns apiReq) $ pdParams proc | otherwise -> KeyParams $ specifiedParams [prm]
prms -> KeyParams $ specifiedParams prms
specifiedParams params = filter (\x -> ppName x `S.member` iColumns apiReq) params
returningCols :: ReadRequest -> [FieldName] -> [FieldName] returningCols :: ReadRequest -> [FieldName] -> [FieldName]
returningCols rr@(Node _ forest) pkCols returningCols rr@(Node _ forest) pkCols
+7 -3
View File
@@ -7,6 +7,7 @@ module PostgREST.Request.Types
, Field , Field
, Filter(..) , Filter(..)
, CallQuery(..) , CallQuery(..)
, CallParams(..)
, CallRequest , CallRequest
, JoinCondition(..) , JoinCondition(..)
, JsonOperand(..) , JsonOperand(..)
@@ -40,7 +41,7 @@ import qualified GHC.Show (show)
import PostgREST.DbStructure.Identifiers (FieldName, import PostgREST.DbStructure.Identifiers (FieldName,
QualifiedIdentifier) QualifiedIdentifier)
import PostgREST.DbStructure.Proc (ProcParam) import PostgREST.DbStructure.Proc (ProcParam (..))
import PostgREST.DbStructure.Relationship (Relationship) import PostgREST.DbStructure.Relationship (Relationship)
import PostgREST.RangeQuery (NonnegRange) import PostgREST.RangeQuery (NonnegRange)
import PostgREST.Request.Preferences (PreferResolution) import PostgREST.Request.Preferences (PreferResolution)
@@ -127,14 +128,17 @@ data MutateQuery
data CallQuery = FunctionCall data CallQuery = FunctionCall
{ funCQi :: QualifiedIdentifier { funCQi :: QualifiedIdentifier
, funCParams :: [ProcParam] , funCParams :: CallParams
, funCArgs :: Maybe BL.ByteString , funCArgs :: Maybe BL.ByteString
, funCScalar :: Bool , funCScalar :: Bool
, funCMultipleCall :: Bool , funCMultipleCall :: Bool
, funCSingleParam :: Bool
, funCReturning :: [FieldName] , funCReturning :: [FieldName]
} }
data CallParams
= KeyParams [ProcParam] -- ^ Call with key params: func(a := val1, b:= val2)
| OnePosParam ProcParam -- ^ Call with positional params(only one supported): func(val)
-- | The select value in `/tbl?select=alias:field::cast` -- | The select value in `/tbl?select=alias:field::cast`
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint) type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

+77 -3
View File
@@ -1,11 +1,12 @@
module Feature.RpcSpec where module Feature.RpcSpec where
import qualified Data.ByteString.Lazy as BL (empty) import qualified Data.ByteString.Lazy as BL (empty, readFile)
import Network.Wai (Application) import Network.Wai (Application)
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus)) import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
import Network.HTTP.Types import Network.HTTP.Types
import System.IO.Unsafe (unsafePerformIO)
import Test.Hspec hiding (pendingWith) import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
@@ -145,8 +146,8 @@ spec actualPgVersion =
it "should fail with 300 Multiple Choices without explicit type casts" $ it "should fail with 300 Multiple Choices without explicit type casts" $
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith` get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
[json| { [json| {
"hint":"Overloaded functions with the same parameter name but different types are not supported", "hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
"message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)" } |] "message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)"}|]
{ matchStatus = 300 { matchStatus = 300
, matchHeaders = [matchContentTypeJson] , matchHeaders = [matchContentTypeJson]
} }
@@ -1059,3 +1060,76 @@ spec actualPgVersion =
{ matchStatus = 500 { matchStatus = 500
, matchHeaders = [ matchContentTypeJson ] , matchHeaders = [ matchContentTypeJson ]
} }
context "single unnamed param" $ do
it "can insert json directly" $
post "/rpc/unnamed_json_param"
[json|{"A": 1, "B": 2, "C": 3}|]
`shouldRespondWith`
[json|{"A": 1, "B": 2, "C": 3}|]
it "can insert text directly" $
request methodPost "/rpc/unnamed_text_param"
[("Content-Type", "text/plain"), ("Accept", "text/plain")]
[str|unnamed text arg|]
`shouldRespondWith`
[str|unnamed text arg|]
it "can insert bytea directly" $ do
let file = unsafePerformIO $ BL.readFile "test/C.png"
r <- request methodPost "/rpc/unnamed_bytea_param"
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
file
liftIO $ do
let respBody = simpleBody r
respBody `shouldBe` file
it "will err when no function with single unnamed json parameter exists and application/json is specified" $
request methodPost "/rpc/unnamed_int_param" [("Content-Type", "application/json")]
[json|{"x": 1, "y": 2}|]
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache"
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
}
it "will err when no function with single unnamed text parameter exists and text/plain is specified" $
request methodPost "/rpc/unnamed_int_param"
[("Content-Type", "text/plain")]
[str|a simple text|]
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache"
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
}
it "will err when no function with single unnamed bytea parameter exists and application/octet-stream is specified" $
let file = unsafePerformIO $ BL.readFile "test/C.png" in
request methodPost "/rpc/unnamed_int_param"
[("Content-Type", "application/octet-stream")]
file
`shouldRespondWith`
[json|{
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
"message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache"
}|]
{ matchStatus = 404
, matchHeaders = [ matchContentTypeJson ]
}
it "will not be able to resolve when a single unnamed json parameter exists and other overloaded functions exist" $
request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]
[json|{"x": 1, "y": 2}|]
`shouldRespondWith`
[json| {
"hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
"message":"Could not choose the best candidate function between: test.overloaded_unnamed_param( => json), test.overloaded_unnamed_param(x => integer, y => integer)"}|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
}
+12 -10
View File
@@ -15,7 +15,7 @@ import Protolude hiding (get, toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
import PostgREST.Query.QueryBuilder (requestToCallProcQuery) import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
import PostgREST.Request.Types (CallQuery (..)) import PostgREST.Request.Types
import PostgREST.DbStructure.Identifiers import PostgREST.DbStructure.Identifiers
import PostgREST.DbStructure.Proc import PostgREST.DbStructure.Proc
@@ -33,30 +33,32 @@ main = do
context "call proc query" $ do context "call proc query" $ do
it "should not exceed cost when calling setof composite proc" $ do it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool $ cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False] requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
(Just [str| {"id": 3} |]) False False False []) (KeyParams [ProcParam "id" "int" True False])
(Just [str| {"id": 3} |]) False False [])
liftIO $ liftIO $
cost `shouldSatisfy` (< Just 40) cost `shouldSatisfy` (< Just 40)
it "should not exceed cost when calling setof composite proc with empty params" $ do it "should not exceed cost when calling setof composite proc with empty params" $ do
cost <- exec pool $ cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") [] Nothing False False False []) requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])
liftIO $ liftIO $
cost `shouldSatisfy` (< Just 30) cost `shouldSatisfy` (< Just 30)
it "should not exceed cost when calling scalar proc" $ do it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool $ cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them") requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
[ProcParam "a" "int" True False, ProcParam "b" "int" True False] (KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
(Just [str| {"a": 3, "b": 4} |]) True False False []) (Just [str| {"a": 3, "b": 4} |]) True False [])
liftIO $ liftIO $
cost `shouldSatisfy` (< Just 10) cost `shouldSatisfy` (< Just 10)
context "params=multiple-objects" $ do context "params=multiple-objects" $ do
it "should not exceed cost when calling setof composite proc" $ do it "should not exceed cost when calling setof composite proc" $ do
cost <- exec pool $ cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False] requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
(Just [str| [{"id": 1}, {"id": 4}] |]) False True False []) (KeyParams [ProcParam "id" "int" True False])
(Just [str| [{"id": 1}, {"id": 4}] |]) False True [])
liftIO $ do liftIO $ do
-- lower bound needed for now to make sure that cost is not Nothing -- lower bound needed for now to make sure that cost is not Nothing
cost `shouldSatisfy` (> Just 2000) cost `shouldSatisfy` (> Just 2000)
@@ -65,8 +67,8 @@ main = do
it "should not exceed cost when calling scalar proc" $ do it "should not exceed cost when calling scalar proc" $ do
cost <- exec pool $ cost <- exec pool $
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them") requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
[ProcParam "a" "int" True False, ProcParam "b" "int" True False] (KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
(Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False False []) (Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False [])
liftIO $ liftIO $
cost `shouldSatisfy` (< Just 10) cost `shouldSatisfy` (< Just 10)
+25 -1
View File
@@ -1050,7 +1050,7 @@ create function test.ret_point_overloaded(x int, y int) returns test.point_2d as
select row(x, y)::test.point_2d; select row(x, y)::test.point_2d;
$$ language sql; $$ language sql;
create function test.ret_point_overloaded(json) returns json as $$ create function test.ret_point_overloaded(x json) returns json as $$
select $1; select $1;
$$ language sql; $$ language sql;
@@ -2252,3 +2252,27 @@ A test for partitioned tables$$;
); );
end if; end if;
end$do$; end$do$;
create or replace function test.unnamed_json_param(json) returns json as $$
select $1;
$$ language sql;
create or replace function test.unnamed_text_param(text) returns text as $$
select $1;
$$ language sql ;
create or replace function test.unnamed_bytea_param(bytea) returns bytea as $$
select $1::bytea;
$$ language sql ;
create or replace function test.unnamed_int_param(int) returns int as $$
select $1;
$$ language sql;
create or replace function test.overloaded_unnamed_param(json) returns int as $$
select $1;
$$ language sql;
create or replace function test.overloaded_unnamed_param(x int, y int) returns int as $$
select x + y;
$$ language sql;