Fix proc resource embedding issue with search_path and refactor return type (#831)

This commit is contained in:
Steve Chávez
2017-03-20 23:50:42 -07:00
committed by Joe Nelson
parent 206ab163b6
commit 47b023e858
7 changed files with 143 additions and 32 deletions
+1
View File
@@ -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
+2 -3
View File
@@ -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
View File
@@ -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
+28 -10
View File
@@ -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 =
+7 -2
View File
@@ -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
+57 -8
View File
@@ -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
+39
View File
@@ -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
-- --