Fix proc resource embedding issue with search_path and refactor return type (#831)
This commit is contained in:
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- #791, malformed nested JSON error - @diogob
|
- #791, malformed nested JSON error - @diogob
|
||||||
- Resource embedding in views referencing tables in public schema - @fab1an
|
- Resource embedding in views referencing tables in public schema - @fab1an
|
||||||
|
- #831, Fix proc resource embedding issue with search_path - @steve-chavez
|
||||||
|
|
||||||
## [0.4.0.0] - 2017-01-19
|
## [0.4.0.0] - 2017-01-19
|
||||||
|
|
||||||
|
|||||||
@@ -232,7 +232,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 = encodeOpenAPI (map snd $ dbProcs dbStructure) ti uri'
|
encodeApi ti = encodeOpenAPI (M.elems $ dbProcs dbStructure) ti uri'
|
||||||
body <- encodeApi . toTableInfo <$> H.query schema accessibleTables
|
body <- encodeApi . toTableInfo <$> H.query schema accessibleTables
|
||||||
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
|
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
|
||||||
|
|
||||||
@@ -264,8 +264,7 @@ app dbStructure conf apiRequest =
|
|||||||
status = rangeStatus lower upper (toInteger <$> tableTotal)
|
status = rangeStatus lower upper (toInteger <$> tableTotal)
|
||||||
in (status, contentRange)
|
in (status, contentRange)
|
||||||
|
|
||||||
mapSnd f (a, b) = (a, f b)
|
readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) (dbProcs dbStructure) apiRequest
|
||||||
readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) (map (mapSnd pdReturnType) $ dbProcs dbStructure) apiRequest
|
|
||||||
fldNames = fieldNames <$> readReq
|
fldNames = fieldNames <$> readReq
|
||||||
readDbRequest = DbRead <$> readReq
|
readDbRequest = DbRead <$> readReq
|
||||||
mutateDbRequest = DbMutate <$> (mutateRequest apiRequest =<< fldNames)
|
mutateDbRequest = DbMutate <$> (mutateRequest apiRequest =<< fldNames)
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import Control.Applicative
|
|||||||
import Control.Lens.Getter (view)
|
import Control.Lens.Getter (view)
|
||||||
import Control.Lens.Tuple (_1)
|
import Control.Lens.Tuple (_1)
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import Data.List (delete, lookup)
|
import Data.List (delete)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Text (isInfixOf, dropWhile, drop)
|
import Data.Text (isInfixOf)
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ import Protolude hiding (from, dropWhile, drop)
|
|||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
import Unsafe (unsafeHead)
|
import Unsafe (unsafeHead)
|
||||||
|
|
||||||
readRequest :: Maybe Integer -> [Relation] -> [(Text, Text)] -> ApiRequest -> Either Response ReadRequest
|
readRequest :: Maybe Integer -> [Relation] -> M.HashMap Text ProcDescription -> ApiRequest -> Either Response ReadRequest
|
||||||
readRequest maxRows allRels allProcs apiRequest =
|
readRequest maxRows allRels allProcs apiRequest =
|
||||||
mapLeft apiRequestError $
|
mapLeft apiRequestError $
|
||||||
treeRestrictRange maxRows =<<
|
treeRestrictRange maxRows =<<
|
||||||
@@ -46,13 +46,13 @@ 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 p) ) -> Just (s, t)
|
(TargetProc (QualifiedIdentifier s proc) ) -> Just (s, tName)
|
||||||
where
|
where
|
||||||
returnType = fromMaybe "" $ lookup p allProcs
|
retType = pdReturnType <$> M.lookup proc allProcs
|
||||||
-- we are looking for results looking like "SETOF schema.tablename" and want to extract tablename
|
tName = case retType of
|
||||||
t = if "SETOF " `isInfixOf` returnType
|
Just (SetOf (Composite qi)) -> qiName qi
|
||||||
then drop 1 $ dropWhile (/= '.') returnType
|
Just (Single (Composite qi)) -> qiName qi
|
||||||
else p
|
_ -> proc
|
||||||
|
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import qualified Hasql.Encoders as HE
|
|||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Query as H
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.List (elemIndex)
|
import Data.List (elemIndex)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Text (split, strip,
|
import Data.Text (split, strip,
|
||||||
@@ -96,12 +97,13 @@ 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
|
||||||
|
|
||||||
accessibleProcs :: H.Query Schema [(Text, ProcDescription)]
|
accessibleProcs :: H.Query Schema (M.HashMap Text ProcDescription)
|
||||||
accessibleProcs =
|
accessibleProcs =
|
||||||
H.statement sql (HE.value HE.text)
|
H.statement sql (HE.value HE.text)
|
||||||
(map addName <$> HD.rowsList (ProcDescription <$> HD.value HD.text
|
(M.fromList . map addName <$> HD.rowsList (ProcDescription <$> HD.value HD.text
|
||||||
<*> (parseArgs <$> HD.value HD.text)
|
<*> (parseArgs <$> HD.value HD.text)
|
||||||
<*> HD.value HD.text)) True
|
<*> (parseRetType <$> HD.value HD.text <*> HD.value HD.text <*>
|
||||||
|
HD.value HD.bool <*> HD.value HD.char))) True
|
||||||
where
|
where
|
||||||
addName :: ProcDescription -> (Text, ProcDescription)
|
addName :: ProcDescription -> (Text, ProcDescription)
|
||||||
addName pd = (pdName pd, pd)
|
addName pd = (pdName pd, pd)
|
||||||
@@ -118,14 +120,30 @@ accessibleProcs =
|
|||||||
else Just $
|
else Just $
|
||||||
PgArg (dropAround (== '"') name) (strip typ) (T.null def)
|
PgArg (dropAround (== '"') name) (strip typ) (T.null def)
|
||||||
|
|
||||||
|
parseRetType :: Text -> Text -> Bool -> Char -> RetType
|
||||||
|
parseRetType schema name isSetOf typ
|
||||||
|
| isSetOf = SetOf pgType
|
||||||
|
| otherwise = Single pgType
|
||||||
|
where
|
||||||
|
qi = QualifiedIdentifier schema name
|
||||||
|
pgType = case typ of
|
||||||
|
'c' -> Composite qi
|
||||||
|
'p' -> Pseudo name
|
||||||
|
_ -> Scalar qi -- 'b'ase, 'd'omain, 'e'num, 'r'ange
|
||||||
|
|
||||||
sql = [q|
|
sql = [q|
|
||||||
SELECT p.proname as "proc_name",
|
SELECT p.proname as "proc_name",
|
||||||
pg_get_function_arguments(p.oid) as "args",
|
pg_get_function_arguments(p.oid) as "args",
|
||||||
pg_get_function_result(p.oid) as "return_type"
|
tn.nspname as "rettype_schema",
|
||||||
FROM pg_namespace n
|
coalesce(comp.relname, t.typname) as "rettype_name",
|
||||||
JOIN pg_proc p
|
p.proretset as "rettype_is_setof",
|
||||||
ON pronamespace = n.oid
|
t.typtype as "rettype_typ"
|
||||||
WHERE n.nspname = $1|]
|
FROM pg_proc p
|
||||||
|
JOIN pg_namespace pn ON pn.oid = p.pronamespace
|
||||||
|
JOIN pg_type t ON t.oid = p.prorettype
|
||||||
|
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
||||||
|
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
|
||||||
|
WHERE pn.nspname = $1|]
|
||||||
|
|
||||||
accessibleTables :: H.Query Schema [Table]
|
accessibleTables :: H.Query Schema [Table]
|
||||||
accessibleTables =
|
accessibleTables =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Protolude
|
|||||||
import qualified GHC.Show
|
import qualified GHC.Show
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import Data.HashMap.Strict as M
|
||||||
import Data.Tree
|
import Data.Tree
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
@@ -27,7 +28,7 @@ data DbStructure = DbStructure {
|
|||||||
, dbColumns :: [Column]
|
, dbColumns :: [Column]
|
||||||
, dbRelations :: [Relation]
|
, dbRelations :: [Relation]
|
||||||
, dbPrimaryKeys :: [PrimaryKey]
|
, dbPrimaryKeys :: [PrimaryKey]
|
||||||
, dbProcs :: [(Text,ProcDescription)]
|
, dbProcs :: M.HashMap Text ProcDescription
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
data PgArg = PgArg {
|
data PgArg = PgArg {
|
||||||
@@ -36,10 +37,14 @@ data PgArg = PgArg {
|
|||||||
, pgaReq :: Bool
|
, pgaReq :: Bool
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier | Pseudo Text deriving (Eq, Show)
|
||||||
|
|
||||||
|
data RetType = Single PgType | SetOf PgType deriving (Eq, Show)
|
||||||
|
|
||||||
data ProcDescription = ProcDescription {
|
data ProcDescription = ProcDescription {
|
||||||
pdName :: Text
|
pdName :: Text
|
||||||
, pdArgs :: [PgArg]
|
, pdArgs :: [PgArg]
|
||||||
, pdReturnType :: Text
|
, pdReturnType :: RetType
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
type Schema = Text
|
type Schema = Text
|
||||||
|
|||||||
@@ -512,25 +512,74 @@ spec = do
|
|||||||
post "/rpc/getproject?select=id,name" [json| { "id": 1} |] `shouldRespondWith`
|
post "/rpc/getproject?select=id,name" [json| { "id": 1} |] `shouldRespondWith`
|
||||||
[str|[{"id":1,"name":"Windows 7"}]|]
|
[str|[{"id":1,"name":"Windows 7"}]|]
|
||||||
|
|
||||||
it "can embed foreign entities to the items returned by a proc" $
|
context "foreign entities embedding" $ do
|
||||||
|
it "can embed if related tables are in the exposed schema" $
|
||||||
post "/rpc/getproject?select=id,name,client{id},tasks{id}" [json| { "id": 1} |] `shouldRespondWith`
|
post "/rpc/getproject?select=id,name,client{id},tasks{id}" [json| { "id": 1} |] `shouldRespondWith`
|
||||||
[str|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
|
[str|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
|
||||||
|
|
||||||
|
it "cannot embed if the related table is not in the exposed schema" $
|
||||||
|
post "/rpc/single_article?select=*,article_stars{*}" [json|{ "id": 1}|]
|
||||||
|
`shouldRespondWith` 400
|
||||||
|
|
||||||
|
it "can embed if the related tables are in a hidden schema but exposed as views" $
|
||||||
|
post "/rpc/single_article?select=id,articleStars{userId}" [json|{ "id": 2}|]
|
||||||
|
`shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "a proc that returns an empty rowset" $
|
context "a proc that returns an empty rowset" $
|
||||||
it "returns empty json array" $
|
it "returns empty json array" $
|
||||||
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
|
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
|
||||||
[json| [] |]
|
[json| [] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "a proc that returns plain text" $ do
|
context "proc return types" $ do
|
||||||
it "returns proper json" $
|
context "returns text" $ do
|
||||||
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
|
it "returns proper json" $
|
||||||
[json|"Hello, world"|]
|
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
|
||||||
|
[json|"Hello, world"|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can handle unicode" $
|
||||||
|
post "/rpc/sayhello" [json| { "name": "¥" } |] `shouldRespondWith`
|
||||||
|
[json|"Hello, ¥"|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "returns enum value" $
|
||||||
|
post "/rpc/ret_enum" [json|{ "val": "foo" }|] `shouldRespondWith`
|
||||||
|
[json|"foo"|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can handle unicode" $
|
it "returns domain value" $
|
||||||
post "/rpc/sayhello" [json| { "name": "¥" } |] `shouldRespondWith`
|
post "/rpc/ret_domain" [json|{ "val": "8" }|] `shouldRespondWith`
|
||||||
[json|"Hello, ¥"|]
|
[json|8|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "returns range" $
|
||||||
|
post "/rpc/ret_range" [json|{ "low": 10, "up": 20 }|] `shouldRespondWith`
|
||||||
|
[json|"[10,20)"|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "returns row of scalars" $
|
||||||
|
post "/rpc/ret_scalars" [json|{}|] `shouldRespondWith`
|
||||||
|
[json|[{"a":"scalars", "b":"foo", "c":1, "d":"[10,20)"}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "returns composite type in exposed schema" $
|
||||||
|
post "/rpc/ret_point_2d" [json|{}|] `shouldRespondWith`
|
||||||
|
[json|[{"x": 10, "y": 5}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "cannot return composite type in hidden schema" $
|
||||||
|
post "/rpc/ret_point_3d" [json|{}|] `shouldRespondWith` 401
|
||||||
|
|
||||||
|
it "returns single row from table" $
|
||||||
|
post "/rpc/single_article?select=id" [json|{"id": 2}|] `shouldRespondWith`
|
||||||
|
[json|[{"id": 2}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "returns null for void" $
|
||||||
|
post "/rpc/ret_void" [json|{}|] `shouldRespondWith`
|
||||||
|
[json|null|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "improper input" $ do
|
context "improper input" $ do
|
||||||
|
|||||||
Vendored
+39
@@ -1108,6 +1108,45 @@ create view images_base64 as (
|
|||||||
select name, replace(encode(img, 'base64'), E'\n', '') as img from images
|
select name, replace(encode(img, 'base64'), E'\n', '') as img from images
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create function test.ret_enum(val text) returns test.enum_menagerie_type as $$
|
||||||
|
select val::test.enum_menagerie_type;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create domain one_nine as integer check (value >= 1 and value <= 9);
|
||||||
|
|
||||||
|
create function test.ret_domain(val integer) returns test.one_nine as $$
|
||||||
|
select val::test.one_nine;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function test.ret_range(low integer, up integer) returns int4range as $$
|
||||||
|
select int4range(low, up);
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function test.ret_scalars() returns table(
|
||||||
|
a text, b test.enum_menagerie_type, c test.one_nine, d int4range
|
||||||
|
) as $$
|
||||||
|
select row('scalars'::text, enum_first(null::test.enum_menagerie_type),
|
||||||
|
1::test.one_nine, int4range(10, 20));
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create type test.point_2d as (x integer, y integer);
|
||||||
|
|
||||||
|
create function test.ret_point_2d() returns test.point_2d as $$
|
||||||
|
select row(10, 5)::test.point_2d;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create type private.point_3d as (x integer, y integer, z integer);
|
||||||
|
|
||||||
|
create function test.ret_point_3d() returns private.point_3d as $$
|
||||||
|
select row(7, -3, 4)::private.point_3d;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function test.ret_void() returns void as '' language sql;
|
||||||
|
|
||||||
|
create function test.single_article(id integer) returns test.articles as $$
|
||||||
|
select a.* from test.articles a where a.id = $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user