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:
committed by
Steve Chavez
parent
caaa34b5de
commit
d4c6abbaec
@@ -13,7 +13,7 @@ module PostgREST.Request.ApiRequest
|
||||
, ContentType(..)
|
||||
, Action(..)
|
||||
, Target(..)
|
||||
, PayloadJSON(..)
|
||||
, Payload(..)
|
||||
, userApiRequest
|
||||
) where
|
||||
|
||||
@@ -74,18 +74,19 @@ import Protolude.Conv (toS)
|
||||
|
||||
type RequestBody = BL.ByteString
|
||||
|
||||
data PayloadJSON
|
||||
data 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
|
||||
-- cache this instead of an Aeson Value because it was detected that for
|
||||
-- large payloads the encoding had high memory usage, see
|
||||
-- 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
|
||||
-- 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
|
||||
-- | 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
|
||||
|
||||
-- | 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 =
|
||||
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)
|
||||
@@ -136,7 +137,7 @@ jsonRpcParams proc prms =
|
||||
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
||||
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 =
|
||||
case target of
|
||||
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
|
||||
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
||||
, 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
|
||||
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
||||
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
||||
@@ -253,7 +254,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
||||
isTargetingDefaultSpec = case path of
|
||||
PathInfo{pIsDefaultSpec=True} -> True
|
||||
_ -> False
|
||||
contentType = ContentType.decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
||||
contentType = maybe CTApplicationJSON ContentType.decodeContentType $ lookupHeader "content-type"
|
||||
columns
|
||||
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
||||
| otherwise = Nothing
|
||||
@@ -264,9 +265,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
||||
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams
|
||||
(CTUrlEncoded, _) -> S.fromList $ map (toS . fst) $ parseSimpleQuery $ toS reqBody
|
||||
_ -> case (relevantPayload, fromRight Nothing parsedColumns) of
|
||||
(Just ProcessedJSON{pjKeys}, _) -> pjKeys
|
||||
(Just RawJSON{}, Just cls) -> cls
|
||||
_ -> S.empty
|
||||
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
||||
(Just RawJSON{}, Just cls) -> cls
|
||||
_ -> S.empty
|
||||
payload = case contentType of
|
||||
CTApplicationJSON ->
|
||||
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
|
||||
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
|
||||
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
|
||||
action =
|
||||
case method of
|
||||
@@ -321,7 +324,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
||||
schema = fromMaybe defaultSchema profile
|
||||
target =
|
||||
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
|
||||
case path of
|
||||
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
|
||||
@@ -426,7 +431,7 @@ csvToJson (_, vals) =
|
||||
else JSON.String $ toS str
|
||||
)
|
||||
|
||||
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
|
||||
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
|
||||
payloadAttributes raw json =
|
||||
-- Test that Array contains only Objects having the same keys
|
||||
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,
|
||||
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 qi argumentsKeys paramsAsSingleObject allProcs =
|
||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ContentType -> Bool -> Either ApiRequestError ProcDescription
|
||||
findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
|
||||
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
|
||||
procs -> Left $ AmbiguousRpc (toList procs)
|
||||
where
|
||||
matchProc = filter matchesParams $ M.lookupDefault mempty qi allProcs -- first find the proc by name
|
||||
matchesParams proc =
|
||||
let params = pdParams proc in
|
||||
-- here we don't match by argument key(there isn't one) but by the single parameter type
|
||||
if paramsAsSingleObject then
|
||||
case params of
|
||||
[prm] -> ppType prm `elem` ["json", "jsonb"]
|
||||
_ -> False
|
||||
-- exceptional case for Prefer: params=single-object
|
||||
if paramsAsSingleObject
|
||||
then length params == 1 && (ppType <$> headMay params) `elem` [Just "json", Just "jsonb"]
|
||||
-- If the function has no parameters, the arguments keys must be empty as well
|
||||
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
|
||||
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
||||
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
|
||||
(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)
|
||||
|
||||
Reference in New Issue
Block a user