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
+8
-1
@@ -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
|
||||
- #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
|
||||
|
||||
- #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
|
||||
|
||||
|
||||
+10
-6
@@ -60,7 +60,7 @@ data ApiRequestError
|
||||
| NoRelBetween Text Text
|
||||
| AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [ProcDescription]
|
||||
| NoRpc Text Text [Text] Bool
|
||||
| NoRpc Text Text [Text] Bool ContentType Bool
|
||||
| InvalidFilters
|
||||
| UnacceptableSchema [Text]
|
||||
| ContentTypeError [ByteString]
|
||||
@@ -99,14 +99,18 @@ instance JSON.ToJSON ApiRequestError where
|
||||
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
|
||||
"details" .= (compressedRel <$> rels) ]
|
||||
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])]
|
||||
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),
|
||||
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
||||
(if hasPreferSingleObject
|
||||
then " function with a single json or jsonb parameter"
|
||||
else "(" <> T.intercalate ", " payloadKeys <> ")" <> " function") <>
|
||||
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||
(True, _, _) -> " function with a single json or jsonb parameter"
|
||||
(_, 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")]
|
||||
toJSON UnsupportedVerb = JSON.object [
|
||||
"message" .= ("Unsupported HTTP verb" :: Text)]
|
||||
|
||||
@@ -116,26 +116,27 @@ mutateRequestToQuery (Delete mainQi logicForest returnings) =
|
||||
H.sql (returningF mainQi returnings)
|
||||
|
||||
requestToCallProcQuery :: CallRequest -> H.Snippet
|
||||
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall singleParam returnings) =
|
||||
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall returnings) =
|
||||
prmsCTE <> argsBody
|
||||
where
|
||||
(prmsCTE, argFrag)
|
||||
| null params = (mempty, mempty)
|
||||
| singleParam = ("WITH pgrst_args AS (SELECT NULL)", jsonPlaceHolder args)
|
||||
| otherwise = (
|
||||
(prmsCTE, argFrag) = case params of
|
||||
OnePosParam prm -> ("WITH pgrst_args AS (SELECT NULL)", singleParameter args (encodeUtf8 $ ppType prm))
|
||||
KeyParams [] -> (mempty, mempty)
|
||||
KeyParams prms -> (
|
||||
"WITH " <> normalizedBody args <> ", " <>
|
||||
H.sql (
|
||||
BS.unwords [
|
||||
"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
|
||||
then fmtParams varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
|
||||
else fmtParams varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
|
||||
then fmtParams prms varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
|
||||
else fmtParams prms varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
|
||||
)
|
||||
|
||||
fmtParams :: (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
|
||||
fmtParams prmFragPre prmFragSuf = BS.intercalate ", " ((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> params)
|
||||
fmtParams :: [ProcParam] -> (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
|
||||
fmtParams prms prmFragPre prmFragSuf = BS.intercalate ", "
|
||||
((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> prms)
|
||||
|
||||
varadicPrefix :: ProcParam -> SqlFragment
|
||||
varadicPrefix a = if ppVar a then "VARIADIC " else mempty
|
||||
|
||||
@@ -16,7 +16,6 @@ module PostgREST.Query.SqlFragment
|
||||
, countF
|
||||
, fromQi
|
||||
, ftsOperators
|
||||
, jsonPlaceHolder
|
||||
, limitOffsetF
|
||||
, locationF
|
||||
, normalizedBody
|
||||
@@ -31,6 +30,7 @@ module PostgREST.Query.SqlFragment
|
||||
, responseStatusF
|
||||
, returningF
|
||||
, selectBody
|
||||
, singleParameter
|
||||
, sourceCTEName
|
||||
, unknownEncoder
|
||||
, 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
|
||||
-- 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
|
||||
-- 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 body =
|
||||
"pgrst_payload AS (SELECT " <> jsonPlaceHolder body <> " AS json_data), " <>
|
||||
"pgrst_payload AS (SELECT " <> jsonPlaceHolder <> " AS json_data), " <>
|
||||
H.sql (BS.unwords [
|
||||
"pgrst_body AS (",
|
||||
"SELECT",
|
||||
@@ -116,12 +117,14 @@ normalizedBody body =
|
||||
"ELSE json_build_array(json_data)",
|
||||
"END AS val",
|
||||
"FROM pgrst_payload)"])
|
||||
where
|
||||
jsonPlaceHolder = H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
|
||||
|
||||
-- | Equivalent to "$1::json"
|
||||
-- | TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
||||
jsonPlaceHolder :: Maybe BL.ByteString -> H.Snippet
|
||||
jsonPlaceHolder body =
|
||||
H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
|
||||
singleParameter :: Maybe BL.ByteString -> ByteString -> H.Snippet
|
||||
singleParameter body typ =
|
||||
if typ == "bytea"
|
||||
then H.encoderAndParam (HE.nullable HE.bytea) (toS <$> body) -- needed because bytea fails with HE.unknown(pg tries to utf8 encode)
|
||||
else H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::" <> H.sql typ
|
||||
|
||||
selectBody :: SqlFragment
|
||||
selectBody = "(SELECT val FROM pgrst_body)"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -48,7 +48,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
restrictRange)
|
||||
import PostgREST.Request.ApiRequest (Action (..),
|
||||
ApiRequest (..),
|
||||
PayloadJSON (..))
|
||||
Payload (..))
|
||||
|
||||
import PostgREST.Request.Parsers
|
||||
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
|
||||
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
||||
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 proc apiReq readReq = FunctionCall {
|
||||
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
||||
, funCParams = specifiedParams
|
||||
, funCArgs = pjRaw <$> iPayload apiReq
|
||||
, funCParams = callParams
|
||||
, funCArgs = payRaw <$> iPayload apiReq
|
||||
, funCScalar = procReturnsScalar proc
|
||||
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
||||
, funCSingleParam = paramsAsSingleObject
|
||||
, funCReturning = returningCols readReq []
|
||||
}
|
||||
where
|
||||
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
||||
specifiedParams =
|
||||
if paramsAsSingleObject
|
||||
then pdParams proc
|
||||
else filter (\x -> ppName x `S.member` iColumns apiReq) $ pdParams proc
|
||||
callParams = case pdParams proc of
|
||||
[prm] | paramsAsSingleObject -> OnePosParam prm
|
||||
| ppName prm == mempty -> OnePosParam prm
|
||||
| otherwise -> KeyParams $ specifiedParams [prm]
|
||||
prms -> KeyParams $ specifiedParams prms
|
||||
specifiedParams params = filter (\x -> ppName x `S.member` iColumns apiReq) params
|
||||
|
||||
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
|
||||
returningCols rr@(Node _ forest) pkCols
|
||||
|
||||
@@ -7,6 +7,7 @@ module PostgREST.Request.Types
|
||||
, Field
|
||||
, Filter(..)
|
||||
, CallQuery(..)
|
||||
, CallParams(..)
|
||||
, CallRequest
|
||||
, JoinCondition(..)
|
||||
, JsonOperand(..)
|
||||
@@ -40,7 +41,7 @@ import qualified GHC.Show (show)
|
||||
|
||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||
QualifiedIdentifier)
|
||||
import PostgREST.DbStructure.Proc (ProcParam)
|
||||
import PostgREST.DbStructure.Proc (ProcParam (..))
|
||||
import PostgREST.DbStructure.Relationship (Relationship)
|
||||
import PostgREST.RangeQuery (NonnegRange)
|
||||
import PostgREST.Request.Preferences (PreferResolution)
|
||||
@@ -127,14 +128,17 @@ data MutateQuery
|
||||
|
||||
data CallQuery = FunctionCall
|
||||
{ funCQi :: QualifiedIdentifier
|
||||
, funCParams :: [ProcParam]
|
||||
, funCParams :: CallParams
|
||||
, funCArgs :: Maybe BL.ByteString
|
||||
, funCScalar :: Bool
|
||||
, funCMultipleCall :: Bool
|
||||
, funCSingleParam :: Bool
|
||||
, 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`
|
||||
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
||||
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 324 B |
+77
-3
@@ -1,11 +1,12 @@
|
||||
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.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||
|
||||
import Network.HTTP.Types
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
@@ -145,8 +146,8 @@ spec actualPgVersion =
|
||||
it "should fail with 300 Multiple Choices without explicit type casts" $
|
||||
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
|
||||
[json| {
|
||||
"hint":"Overloaded functions with the same parameter name but different types are not supported",
|
||||
"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)" } |]
|
||||
"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)"}|]
|
||||
{ matchStatus = 300
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
@@ -1059,3 +1060,76 @@ spec actualPgVersion =
|
||||
{ matchStatus = 500
|
||||
, 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
@@ -15,7 +15,7 @@ import Protolude hiding (get, toS)
|
||||
import Protolude.Conv (toS)
|
||||
|
||||
import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
|
||||
import PostgREST.Request.Types (CallQuery (..))
|
||||
import PostgREST.Request.Types
|
||||
|
||||
import PostgREST.DbStructure.Identifiers
|
||||
import PostgREST.DbStructure.Proc
|
||||
@@ -33,30 +33,32 @@ main = do
|
||||
context "call proc query" $ do
|
||||
it "should not exceed cost when calling setof composite proc" $ do
|
||||
cost <- exec pool $
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False]
|
||||
(Just [str| {"id": 3} |]) False False False [])
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||
(KeyParams [ProcParam "id" "int" True False])
|
||||
(Just [str| {"id": 3} |]) False False [])
|
||||
liftIO $
|
||||
cost `shouldSatisfy` (< Just 40)
|
||||
|
||||
it "should not exceed cost when calling setof composite proc with empty params" $ do
|
||||
cost <- exec pool $
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") [] Nothing False False False [])
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])
|
||||
liftIO $
|
||||
cost `shouldSatisfy` (< Just 30)
|
||||
|
||||
it "should not exceed cost when calling scalar proc" $ do
|
||||
cost <- exec pool $
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||
[ProcParam "a" "int" True False, ProcParam "b" "int" True False]
|
||||
(Just [str| {"a": 3, "b": 4} |]) True False False [])
|
||||
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
|
||||
(Just [str| {"a": 3, "b": 4} |]) True False [])
|
||||
liftIO $
|
||||
cost `shouldSatisfy` (< Just 10)
|
||||
|
||||
context "params=multiple-objects" $ do
|
||||
it "should not exceed cost when calling setof composite proc" $ do
|
||||
cost <- exec pool $
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below") [ProcParam "id" "int" True False]
|
||||
(Just [str| [{"id": 1}, {"id": 4}] |]) False True False [])
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||
(KeyParams [ProcParam "id" "int" True False])
|
||||
(Just [str| [{"id": 1}, {"id": 4}] |]) False True [])
|
||||
liftIO $ do
|
||||
-- lower bound needed for now to make sure that cost is not Nothing
|
||||
cost `shouldSatisfy` (> Just 2000)
|
||||
@@ -65,8 +67,8 @@ main = do
|
||||
it "should not exceed cost when calling scalar proc" $ do
|
||||
cost <- exec pool $
|
||||
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||
[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 [])
|
||||
(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 [])
|
||||
liftIO $
|
||||
cost `shouldSatisfy` (< Just 10)
|
||||
|
||||
|
||||
Vendored
+26
-2
@@ -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;
|
||||
$$ 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;
|
||||
$$ language sql;
|
||||
|
||||
@@ -2251,4 +2251,28 @@ A test for partitioned tables$$;
|
||||
foreign key (id_a, name_a) references test.partitioned_a (id, name)
|
||||
);
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user