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
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
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
View File
@@ -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)
+26 -2
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;
$$ 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;