From a253ff325d3e4fb9620ebb20a083d23630e82c6e Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Thu, 26 May 2016 10:32:46 +0300 Subject: [PATCH 1/6] don't check if proc exists, just call it and return 400 if it does not --- src/PostgREST/App.hs | 28 ++++++++++++---------------- src/PostgREST/DbStructure.hs | 14 -------------- test/Feature/QuerySpec.hs | 5 +++++ 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 25c6f090f..94a7df5bb 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -186,22 +186,18 @@ app dbStructure conf apiRequest = (ActionInvoke, TargetProc qi, Just (PayloadJSON (UniformObjects payload))) -> do - exists <- H.query qi doesProcExist - if exists - then do - let p = V.head payload - jwtSecret = configJwtSecret conf - respondToRange $ do - row <- H.query () (callProc qi p topLevelRange shouldCount) - returnJWT <- H.query qi doesProcReturnJWT - let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row - (status, contentRange) = rangeHeader queryTotal tableTotal - in - return $ responseLBS status [jsonH, contentRange] - (if returnJWT - then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}" - else cs $ encode body) - else return notFound + let p = V.head payload + jwtSecret = configJwtSecret conf + respondToRange $ do + row <- H.query () (callProc qi p topLevelRange shouldCount) + returnJWT <- H.query qi doesProcReturnJWT + let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row + (status, contentRange) = rangeHeader queryTotal tableTotal + in + return $ responseLBS status [jsonH, contentRange] + (if returnJWT + then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}" + else cs $ encode body) (ActionRead, TargetRoot, Nothing) -> do let encodeApi ti = encodeOpenAPI ti host port diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index c6e6a77f1..c8efa61b2 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -6,7 +6,6 @@ module PostgREST.DbStructure ( getDbStructure , accessibleTables -, doesProcExist , doesProcReturnJWT ) where @@ -104,19 +103,6 @@ decodeSynonyms cols = <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text -doesProcExist :: H.Query QualifiedIdentifier Bool -doesProcExist = - H.statement sql encodeQi (HD.singleRow (HD.value HD.bool)) True - where - sql = [q| SELECT EXISTS ( - SELECT 1 - FROM pg_catalog.pg_namespace n - JOIN pg_catalog.pg_proc p - ON pronamespace = n.oid - WHERE nspname = $1 - AND proname = $2 - ) |] - doesProcReturnJWT :: H.Query QualifiedIdentifier Bool doesProcReturnJWT = H.statement sql encodeQi (HD.singleRow (HD.value HD.bool)) True diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 73e31b506..24ae58671 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -477,6 +477,11 @@ spec = do request methodPost "/rpc/sayhello" (acceptHdrs "application/json") "sdfsdf" `shouldRespondWith` 400 + -- it used to be 404 and it makes sense but in another part we decided that it's good to return + -- PostgreSQL errors (and have the proxy handle them) and this saves us an aditional query on each rpc request + it "responds with 400 on an unexisting proc" $ + post "/rpc/fake" [json| {} |] `shouldRespondWith` 400 + context "unsupported verbs" $ do it "DELETE fails" $ From 744bbf720302148bff5fb4165ec7ee05f2238a61 Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Thu, 26 May 2016 11:05:19 +0300 Subject: [PATCH 2/6] cache the names of functions returning JWT and avoid extra query on each rpc call --- src/PostgREST/App.hs | 3 ++- src/PostgREST/DbStructure.hs | 30 +++++++++++------------------- src/PostgREST/Types.hs | 1 + 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 94a7df5bb..5aeafc711 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -188,9 +188,10 @@ app dbStructure conf apiRequest = Just (PayloadJSON (UniformObjects payload))) -> do let p = V.head payload jwtSecret = configJwtSecret conf + returnJWT = qiName qi `elem` dbProcsReturningJWT dbStructure respondToRange $ do row <- H.query () (callProc qi p topLevelRange shouldCount) - returnJWT <- H.query qi doesProcReturnJWT + --returnJWT <- H.query qi doesProcReturnJWT let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row (status, contentRange) = rangeHeader queryTotal tableTotal in diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index c8efa61b2..acbf8280f 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -6,7 +6,6 @@ module PostgREST.DbStructure ( getDbStructure , accessibleTables -, doesProcReturnJWT ) where import qualified Hasql.Decoders as HD @@ -15,7 +14,6 @@ import qualified Hasql.Query as H import Control.Applicative import Control.Monad (join, replicateM) -import Data.Functor.Contravariant (contramap) import Data.List (elemIndex, find, sort, subsequences, transpose) import Data.Maybe (fromJust, fromMaybe, isJust, @@ -37,6 +35,7 @@ getDbStructure schema = do syns <- H.query () $ allSynonyms cols rels <- H.query () $ allRelations tabs cols keys <- H.query () $ allPrimaryKeys tabs + retJwt <- H.query schema procsReturningJWT let rels' = (addManyToManyRelations . raiseRelations schema syns . addParentRelations . addSynonymousRelations syns) rels cols' = addForeignKeys rels' cols @@ -47,13 +46,9 @@ getDbStructure schema = do , dbColumns = cols' , dbRelations = rels' , dbPrimaryKeys = keys' + , dbProcsReturningJWT = retJwt } -encodeQi :: HE.Params QualifiedIdentifier -encodeQi = - contramap qiSchema (HE.value HE.text) <> - contramap qiName (HE.value HE.text) - decodeTables :: HD.Result [Table] decodeTables = HD.rowsList tblRow @@ -103,19 +98,16 @@ decodeSynonyms cols = <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text -doesProcReturnJWT :: H.Query QualifiedIdentifier Bool -doesProcReturnJWT = - H.statement sql encodeQi (HD.singleRow (HD.value HD.bool)) True +procsReturningJWT :: H.Query Schema [Text] +procsReturningJWT = + H.statement sql (HE.value HE.text) (HD.rowsList (HD.value HD.text)) True where - sql = [q| SELECT EXISTS ( - SELECT 1 - FROM pg_catalog.pg_namespace n - JOIN pg_catalog.pg_proc p - ON pronamespace = n.oid - WHERE nspname = $1 - AND proname = $2 - AND pg_catalog.pg_get_function_result(p.oid) like '%jwt_claims' - ) |] + sql = [q| + SELECT p.proname + FROM pg_namespace n + JOIN pg_proc p + ON pronamespace = n.oid + WHERE n.nspname = $1 AND pg_get_function_result(p.oid) like '%jwt_claims'|] accessibleTables :: H.Query Schema [Table] accessibleTables = diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index b4a225cae..28daebf1e 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -13,6 +13,7 @@ data DbStructure = DbStructure { , dbColumns :: [Column] , dbRelations :: [Relation] , dbPrimaryKeys :: [PrimaryKey] +, dbProcsReturningJWT :: [Text] } deriving (Show, Eq) type Schema = Text From 2f166088c803c8bbf974ffecae1ceb2af2d7ed1a Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Fri, 27 May 2016 11:18:52 +0300 Subject: [PATCH 3/6] response shaping and filtering for rpc proc calls --- src/PostgREST/App.hs | 32 ++++++++++++++++----------- src/PostgREST/QueryBuilder.hs | 41 ++++++++++++++++++++++++----------- test/Feature/QuerySpec.hs | 40 ++++++++++++++++++++++++++++++---- test/fixtures/schema.sql | 12 ++++++++++ 4 files changed, 95 insertions(+), 30 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 5aeafc711..8101fd480 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -187,18 +187,21 @@ app dbStructure conf apiRequest = (ActionInvoke, TargetProc qi, Just (PayloadJSON (UniformObjects payload))) -> do let p = V.head payload + singular = iPreferSingular apiRequest jwtSecret = configJwtSecret conf returnJWT = qiName qi `elem` dbProcsReturningJWT dbStructure - respondToRange $ do - row <- H.query () (callProc qi p topLevelRange shouldCount) - --returnJWT <- H.query qi doesProcReturnJWT - let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row - (status, contentRange) = rangeHeader queryTotal tableTotal - in - return $ responseLBS status [jsonH, contentRange] - (if returnJWT - then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}" - else cs $ encode body) + case readSqlParts of + Left e -> return $ responseLBS status400 [jsonH] $ cs e + Right (q,cq) -> respondToRange $ do + row <- H.query () (callProc qi p q cq topLevelRange shouldCount singular) + --returnJWT <- H.query qi doesProcReturnJWT + let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row + (status, contentRange) = rangeHeader queryTotal tableTotal + in + return $ responseLBS status [jsonH, contentRange] + (if returnJWT + then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}" + else cs $ encode body) (ActionRead, TargetRoot, Nothing) -> do let encodeApi ti = encodeOpenAPI ti host port @@ -323,9 +326,10 @@ addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [ filters = mapM pRequestFilter flts where action = iAction apiRequest - flts = if action == ActionRead - then iFilters apiRequest - else filter (( '.' `elem` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update + flts + | action == ActionRead = iFilters apiRequest + | action == ActionInvoke = iFilters apiRequest + | otherwise = filter (( '.' `elem` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update orders :: Either ParseError [(Path, [OrderTerm])] orders = mapM pRequestOrder $ iOrder apiRequest ranges :: Either ParseError [(Path, NonnegRange)] @@ -347,6 +351,8 @@ buildReadRequest maxRows allRels apiRequest = let target = iTarget apiRequest in case target of (TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t) + (TargetProc (QualifiedIdentifier s p) ) -> Just (s, p) + _ -> Nothing action :: Action diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 621b5476f..37906635e 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -203,29 +203,41 @@ addJoinConditions schema (Node nn@(query, (n, r, a)) forest) = addCond query' con = query'{flt_=con ++ flt_ query'} type ProcResults = (Maybe Int64, Int64, JSON.Value) -callProc :: QualifiedIdentifier -> JSON.Object -> NonnegRange -> Bool -> H.Query () (Maybe ProcResults) -callProc qi params range countTotal = +callProc :: QualifiedIdentifier -> JSON.Object -> SqlQuery -> SqlQuery -> NonnegRange -> Bool -> Bool -> H.Query () (Maybe ProcResults) +callProc qi params selectQuery countQuery _ countTotal isSingle = unicodeStatement sql HE.unit decodeProc True where sql = [qc| - WITH t AS (select * {_callSql}) + WITH {sourceCTEName} AS ({_callSql}) SELECT - {_countExpr} as countTotal, - pg_catalog.count(1) as countResult, - array_to_json( - coalesce(array_agg(row_to_json(r)), '\{}') - )::character varying - FROM (select * from t {limitF range}) r; + {countResultF} AS total_result_set, + pg_catalog.count(t) AS page_total, + case when pg_catalog.count(1) > 1 + then {bodyF} + else ( + select case when ((array_agg(row_to_json(t)))[1]->{_procName}) is not null + then ((array_agg(row_to_json(t)))[1]->{_procName})::character varying + else {bodyF} + end + ) + end as body + FROM ({selectQuery}) t; |] + -- FROM (select * from {sourceCTEName} {limitF range}) t; + countResultF = if countTotal then "("<>countQuery<>")" else "null::bigint" :: Text _args = intercalate "," $ map _assignment (HM.toList params) + _procName = pgFmtLit $ qiName qi _assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v - _callSql = [qc| from {fromQi qi}({_args}) |] :: Text + _callSql = [qc|select * from {fromQi qi}({_args}) |] :: Text _countExpr = if countTotal - then "(select pg_catalog.count(1) from t)" + then [qc|(select pg_catalog.count(1) from {sourceCTEName})|] else "null::bigint" :: Text decodeProc = HD.maybeRow procRow procRow = (,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8 <*> HD.value HD.json + bodyF + | isSingle = asJsonSingleF + | otherwise = asJsonF operators :: [(Text, SqlFragment)] operators = [ @@ -263,10 +275,13 @@ requestToCountQuery _ (DbMutate _) = undefined requestToCountQuery schema (DbRead (Node (Select _ _ conditions _ _, (mainTbl, _, _)) _)) = unwords [ "SELECT pg_catalog.count(1)", - "FROM ", fromQi $ QualifiedIdentifier schema mainTbl, - ("WHERE " <> intercalate " AND " ( map (pgFmtCondition (QualifiedIdentifier schema mainTbl)) localConditions )) `emptyOnNull` localConditions + "FROM ", fromQi qi, + ("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi) localConditions )) `emptyOnNull` localConditions ] where + qi = if mainTbl == sourceCTEName + then QualifiedIdentifier "" mainTbl + else QualifiedIdentifier schema mainTbl fn Filter{value=VText _} = True fn Filter{value=VForeignKey _ _} = False localConditions = filter fn conditions diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 24ae58671..5f63542a6 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -454,6 +454,38 @@ spec = do post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith` [json| [ {"id": 3}, {"id":4} ] |] + context "shaping the response returned by a proc" $ do + it "returns a project" $ + post "/rpc/getproject" [json| { "id": 1} |] `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client_id":1}]|] + + it "can filter proc results" $ + post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" [json| {} |] `shouldRespondWith` + [json|[{"id":2},{"id":3},{"id":4}]|] + + it "can limit proc results" $ + post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |] + `shouldRespondWith` ResponseMatcher { + matchBody = Just [json|[{"id":3},{"id":4}]|] + , matchStatus = 206 + , matchHeaders = ["Content-Range" <:> "1-2/3"] + } + + + + it "prefer singular" $ + request methodPost "/rpc/getproject" + [("Prefer","plurality=singular")] [json| { "id": 1} |] `shouldRespondWith` + [json|{"id":1,"name":"Windows 7","client_id":1}|] + + it "select works on the first level" $ + post "/rpc/getproject?select=id,name" [json| { "id": 1} |] `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7"}]|] + + it "can embed foreign entities to the items returned by a proc" $ + post "/rpc/getproject?select=id,name,client{id},tasks{id}" [json| { "id": 1} |] `shouldRespondWith` + [json|[{"id":1,"name":"Windows 7","client":{"id":2},"tasks":[{"id":1}]}]|] + context "a proc that returns an empty rowset" $ it "returns empty json array" $ post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith` @@ -462,11 +494,11 @@ spec = do context "a proc that returns plain text" $ do it "returns proper json" $ post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith` - [json| [{"sayhello":"Hello, world"}] |] + [json|"Hello, world"|] it "can handle unicode" $ post "/rpc/sayhello" [json| { "name": "¥" } |] `shouldRespondWith` - [json| [{"sayhello":"Hello, ¥"}] |] + [json|"Hello, ¥"|] context "improper input" $ do it "rejects unknown content type even if payload is good" $ @@ -502,9 +534,9 @@ spec = do it "executes the proc exactly once per request" $ do post "/rpc/callcounter" [json| {} |] `shouldRespondWith` - [json| [{"callcounter":1}] |] + [json|1|] post "/rpc/callcounter" [json| {} |] `shouldRespondWith` - [json| [{"callcounter":2}] |] + [json|2|] describe "weird requests" $ do it "can query as normal" $ do diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 17a825de6..bc3973cc9 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1021,6 +1021,18 @@ create table orders ( shipping_address_id int references addresses(id) ); +CREATE FUNCTION getproject(id int) RETURNS SETOF projects + LANGUAGE sql + AS $_$ + SELECT * FROM test.projects WHERE id = $1; +$_$; + +CREATE FUNCTION getallprojects() RETURNS SETOF projects + LANGUAGE sql + AS $_$ + SELECT * FROM test.projects; +$_$; + -- -- PostgreSQL database dump complete -- From 430baf23b98d1830ca1199226a1a5fe9355898a9 Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Mon, 30 May 2016 11:48:46 +0300 Subject: [PATCH 4/6] embed entities to the results returned by procs --- src/PostgREST/App.hs | 25 ++++++++++++++++--------- src/PostgREST/DbStructure.hs | 14 +++++++------- src/PostgREST/Types.hs | 2 +- test/Feature/QuerySpec.hs | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 8101fd480..3464cb8b2 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -14,7 +14,7 @@ import Data.List (find, delete) import Data.Maybe (fromMaybe, fromJust, mapMaybe) import Data.Ranged.Ranges (emptyRange) import Data.String.Conversions (cs) -import Data.Text (Text, replace, strip) +import Data.Text (Text, replace, strip, isInfixOf, dropWhile, drop) import Data.Tree import qualified Hasql.Pool as P @@ -62,7 +62,7 @@ import PostgREST.QueryBuilder ( callProc import PostgREST.Types import PostgREST.OpenAPI -import Prelude +import Prelude hiding (dropWhile, drop) postgrest :: AppConfig -> IORef DbStructure -> P.Pool -> Application @@ -189,17 +189,17 @@ app dbStructure conf apiRequest = let p = V.head payload singular = iPreferSingular apiRequest jwtSecret = configJwtSecret conf - returnJWT = qiName qi `elem` dbProcsReturningJWT dbStructure + returnType = lookup (qiName qi) $ dbProcs dbStructure + returnsJWT = fromMaybe False $ isInfixOf "jwt_claims" <$> returnType case readSqlParts of Left e -> return $ responseLBS status400 [jsonH] $ cs e Right (q,cq) -> respondToRange $ do row <- H.query () (callProc qi p q cq topLevelRange shouldCount singular) - --returnJWT <- H.query qi doesProcReturnJWT let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row (status, contentRange) = rangeHeader queryTotal tableTotal in return $ responseLBS status [jsonH, contentRange] - (if returnJWT + (if returnsJWT then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}" else cs $ encode body) @@ -241,7 +241,7 @@ app dbStructure conf apiRequest = schema = cs $ configSchema conf shouldCount = iPreferCount apiRequest topLevelRange = fromMaybe allRange $ M.lookup "limit" $ iRange apiRequest - readDbRequest = DbRead <$> buildReadRequest (configMaxRows conf) (dbRelations dbStructure) apiRequest + readDbRequest = DbRead <$> buildReadRequest (configMaxRows conf) (dbRelations dbStructure) (dbProcs dbStructure) apiRequest mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest selectQuery = requestToQuery schema False <$> readDbRequest countQuery = requestToCountQuery schema <$> readDbRequest @@ -341,8 +341,8 @@ treeRestrictRange maxRows_ request = pure $ nodeRestrictRange maxRows_ `fmap` re nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i) -buildReadRequest :: Maybe Integer -> [Relation] -> ApiRequest -> Either Text ReadRequest -buildReadRequest maxRows allRels apiRequest = +buildReadRequest :: Maybe Integer -> [Relation] -> [(Text, Text)] -> ApiRequest -> Either Text ReadRequest +buildReadRequest maxRows allRels allProcs apiRequest = treeRestrictRange maxRows =<< augumentRequestWithJoin schema relations =<< first formatParserError readRequest @@ -351,7 +351,13 @@ buildReadRequest maxRows allRels apiRequest = let target = iTarget apiRequest in case target of (TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t) - (TargetProc (QualifiedIdentifier s p) ) -> Just (s, p) + (TargetProc (QualifiedIdentifier s p) ) -> Just (s, t) + where + returnType = fromMaybe "" $ lookup p allProcs + -- we are looking for results looking like "SETOF schema.tablename" and want to extract tablename + t = if "SETOF " `isInfixOf` returnType + then drop 1 $ dropWhile (/= '.') returnType + else p _ -> Nothing @@ -372,6 +378,7 @@ buildReadRequest maxRows allRels apiRequest = ActionCreate -> fakeSourceRelations ++ allRels ActionUpdate -> fakeSourceRelations ++ allRels ActionDelete -> fakeSourceRelations ++ allRels + ActionInvoke -> fakeSourceRelations ++ allRels _ -> allRels where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation diff --git a/src/PostgREST/DbStructure.hs b/src/PostgREST/DbStructure.hs index acbf8280f..84e99cbf5 100644 --- a/src/PostgREST/DbStructure.hs +++ b/src/PostgREST/DbStructure.hs @@ -35,7 +35,7 @@ getDbStructure schema = do syns <- H.query () $ allSynonyms cols rels <- H.query () $ allRelations tabs cols keys <- H.query () $ allPrimaryKeys tabs - retJwt <- H.query schema procsReturningJWT + procs <- H.query schema accessibleProcs let rels' = (addManyToManyRelations . raiseRelations schema syns . addParentRelations . addSynonymousRelations syns) rels cols' = addForeignKeys rels' cols @@ -46,7 +46,7 @@ getDbStructure schema = do , dbColumns = cols' , dbRelations = rels' , dbPrimaryKeys = keys' - , dbProcsReturningJWT = retJwt + , dbProcs = procs } decodeTables :: HD.Result [Table] @@ -98,16 +98,16 @@ decodeSynonyms cols = <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text -procsReturningJWT :: H.Query Schema [Text] -procsReturningJWT = - H.statement sql (HE.value HE.text) (HD.rowsList (HD.value HD.text)) True +accessibleProcs :: H.Query Schema [(Text, Text)] +accessibleProcs = + H.statement sql (HE.value HE.text) (HD.rowsList ((,) <$> HD.value HD.text <*> HD.value HD.text)) True where sql = [q| - SELECT p.proname + SELECT p.proname as "proc_name", pg_get_function_result(p.oid) as "return_type" FROM pg_namespace n JOIN pg_proc p ON pronamespace = n.oid - WHERE n.nspname = $1 AND pg_get_function_result(p.oid) like '%jwt_claims'|] + WHERE n.nspname = $1|] accessibleTables :: H.Query Schema [Table] accessibleTables = diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 28daebf1e..498f06493 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -13,7 +13,7 @@ data DbStructure = DbStructure { , dbColumns :: [Column] , dbRelations :: [Relation] , dbPrimaryKeys :: [PrimaryKey] -, dbProcsReturningJWT :: [Text] +, dbProcs :: [(Text,Text)] } deriving (Show, Eq) type Schema = Text diff --git a/test/Feature/QuerySpec.hs b/test/Feature/QuerySpec.hs index 5f63542a6..75f896ceb 100644 --- a/test/Feature/QuerySpec.hs +++ b/test/Feature/QuerySpec.hs @@ -484,7 +484,7 @@ spec = do it "can embed foreign entities to the items returned by a proc" $ post "/rpc/getproject?select=id,name,client{id},tasks{id}" [json| { "id": 1} |] `shouldRespondWith` - [json|[{"id":1,"name":"Windows 7","client":{"id":2},"tasks":[{"id":1}]}]|] + [json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|] context "a proc that returns an empty rowset" $ it "returns empty json array" $ From 1372de6f4344fa89a4ef823a65110f55a899a829 Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Tue, 28 Jun 2016 12:18:12 +0300 Subject: [PATCH 5/6] query simplification by @diogob --- src/PostgREST/QueryBuilder.hs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 37906635e..1b40ca893 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -212,15 +212,13 @@ callProc qi params selectQuery countQuery _ countTotal isSingle = SELECT {countResultF} AS total_result_set, pg_catalog.count(t) AS page_total, - case when pg_catalog.count(1) > 1 - then {bodyF} - else ( - select case when ((array_agg(row_to_json(t)))[1]->{_procName}) is not null - then ((array_agg(row_to_json(t)))[1]->{_procName})::character varying - else {bodyF} - end - ) - end as body + case + when pg_catalog.count(1) > 1 then + {bodyF} + else + coalesce(((array_agg(row_to_json(t)))[1]->{_procName})::character varying, {bodyF}) + + end as body FROM ({selectQuery}) t; |] -- FROM (select * from {sourceCTEName} {limitF range}) t; From 72aa664b61b1290a0ffe3fd2f3694a0a6411d41a Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Tue, 28 Jun 2016 12:20:53 +0300 Subject: [PATCH 6/6] CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7259e9bd7..5ea6aa688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added - Ability to generate an OpenAPI spec - @mainx07, @hudayou, @ruslantalpa, @begriffs - Ability to set addresses to listen on - @hudayou - +- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa - Output names of used-defined types (instead of 'USER-DEFINED') - @martingms ### Fixed