Merge pull request #706 from begriffs/func-descriptions
Include RPC endpoints in OpenAPI output
This commit is contained in:
@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa
|
- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa
|
||||||
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
|
||||||
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
|
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
|
||||||
|
- Include RPC endpoints in OpenAPI output - @begriffs, @LogvinovLeon
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Do not apply limit to parent items - @ruslantalpa
|
- Do not apply limit to parent items - @ruslantalpa
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ Test-Suite spec
|
|||||||
, SpecHelper
|
, SpecHelper
|
||||||
, TestTypes
|
, TestTypes
|
||||||
Build-Depends: aeson
|
Build-Depends: aeson
|
||||||
|
, aeson-qq
|
||||||
, async
|
, async
|
||||||
, base
|
, base
|
||||||
, protolude
|
, protolude
|
||||||
|
|||||||
@@ -213,7 +213,8 @@ app dbStructure conf apiRequest =
|
|||||||
singular = iPreferSingular apiRequest
|
singular = iPreferSingular apiRequest
|
||||||
jwtSecret = configJwtSecret conf
|
jwtSecret = configJwtSecret conf
|
||||||
returnType = lookup (qiName qi) $ dbProcs dbStructure
|
returnType = lookup (qiName qi) $ dbProcs dbStructure
|
||||||
returnsJWT = fromMaybe False $ isInfixOf "jwt_claims" <$> returnType
|
returnsJWT = fromMaybe False $
|
||||||
|
isInfixOf "jwt_claims" . pdReturnType <$> returnType
|
||||||
serves [CTApplicationJSON] (iAccepts apiRequest) $ \_ -> case readSqlParts of
|
serves [CTApplicationJSON] (iAccepts apiRequest) $ \_ -> case readSqlParts of
|
||||||
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
Left e -> return $ responseLBS status400 [jsonH] $ toS e
|
||||||
Right (q,cq) -> respondToRange $ do
|
Right (q,cq) -> respondToRange $ do
|
||||||
@@ -233,7 +234,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 ti uri'
|
encodeApi ti = encodeOpenAPI (map snd $ dbProcs dbStructure) ti uri'
|
||||||
serves [CTOpenAPI] (iAccepts apiRequest) $ \_ -> do
|
serves [CTOpenAPI] (iAccepts apiRequest) $ \_ -> do
|
||||||
body <- encodeApi . toTableInfo <$> H.query schema accessibleTables
|
body <- encodeApi . toTableInfo <$> H.query schema accessibleTables
|
||||||
return $ responseLBS status200 [openapiH] $ toS body
|
return $ responseLBS status200 [openapiH] $ toS body
|
||||||
@@ -269,7 +270,8 @@ app dbStructure conf apiRequest =
|
|||||||
schema = toS $ configSchema conf
|
schema = toS $ configSchema conf
|
||||||
shouldCount = iPreferCount apiRequest
|
shouldCount = iPreferCount apiRequest
|
||||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" $ iRange apiRequest
|
topLevelRange = fromMaybe allRange $ M.lookup "limit" $ iRange apiRequest
|
||||||
readDbRequest = DbRead <$> buildReadRequest (configMaxRows conf) (dbRelations dbStructure) (dbProcs dbStructure) apiRequest
|
mapSnd f (a, b) = (a, f b)
|
||||||
|
readDbRequest = DbRead <$> buildReadRequest (configMaxRows conf) (dbRelations dbStructure) (map (mapSnd pdReturnType) $ dbProcs dbStructure) apiRequest
|
||||||
mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
|
mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
|
||||||
selectQuery = requestToQuery schema False <$> readDbRequest
|
selectQuery = requestToQuery schema False <$> readDbRequest
|
||||||
countQuery = requestToCountQuery schema <$> readDbRequest
|
countQuery = requestToCountQuery schema <$> readDbRequest
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import Control.Applicative
|
|||||||
import Data.List (elemIndex)
|
import Data.List (elemIndex)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import Data.Text (split)
|
import Data.Text (split, strip,
|
||||||
|
breakOn, dropAround)
|
||||||
|
import qualified Data.Text as T
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
@@ -95,12 +97,32 @@ 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, Text)]
|
accessibleProcs :: H.Query Schema [(Text, ProcDescription)]
|
||||||
accessibleProcs =
|
accessibleProcs =
|
||||||
H.statement sql (HE.value HE.text) (HD.rowsList ((,) <$> HD.value HD.text <*> HD.value HD.text)) True
|
H.statement sql (HE.value HE.text)
|
||||||
|
(map addName <$> HD.rowsList (ProcDescription <$> HD.value HD.text
|
||||||
|
<*> (parseArgs <$> HD.value HD.text)
|
||||||
|
<*> HD.value HD.text)) True
|
||||||
where
|
where
|
||||||
|
addName :: ProcDescription -> (Text, ProcDescription)
|
||||||
|
addName pd = (pdName pd, pd)
|
||||||
|
|
||||||
|
parseArgs :: Text -> [PgArg]
|
||||||
|
parseArgs = mapMaybe (parseArg . strip) . split (==',')
|
||||||
|
|
||||||
|
parseArg :: Text -> Maybe PgArg
|
||||||
|
parseArg a =
|
||||||
|
let (body, def) = breakOn " DEFAULT " a
|
||||||
|
(name, typ) = breakOn " " body in
|
||||||
|
if T.null typ
|
||||||
|
then Nothing
|
||||||
|
else Just $
|
||||||
|
PgArg (dropAround (== '"') name) (strip typ) (T.null def)
|
||||||
|
|
||||||
sql = [q|
|
sql = [q|
|
||||||
SELECT p.proname as "proc_name", pg_get_function_result(p.oid) as "return_type"
|
SELECT p.proname as "proc_name",
|
||||||
|
pg_get_function_arguments(p.oid) as "args",
|
||||||
|
pg_get_function_result(p.oid) as "return_type"
|
||||||
FROM pg_namespace n
|
FROM pg_namespace n
|
||||||
JOIN pg_proc p
|
JOIN pg_proc p
|
||||||
ON pronamespace = n.oid
|
ON pronamespace = n.oid
|
||||||
|
|||||||
+48
-21
@@ -23,8 +23,8 @@ import Data.Swagger
|
|||||||
import PostgREST.ApiRequest (ContentType(..), toHeader)
|
import PostgREST.ApiRequest (ContentType(..), toHeader)
|
||||||
import PostgREST.Config (prettyVersion)
|
import PostgREST.Config (prettyVersion)
|
||||||
import PostgREST.QueryBuilder (operators)
|
import PostgREST.QueryBuilder (operators)
|
||||||
import PostgREST.Types (Table(..), Column(..),
|
import PostgREST.Types (Table(..), Column(..), PgArg(..),
|
||||||
Proxy(..))
|
Proxy(..), ProcDescription(..))
|
||||||
|
|
||||||
makeMimeList :: [ContentType] -> MimeList
|
makeMimeList :: [ContentType] -> MimeList
|
||||||
makeMimeList cs = MimeList $ map (fromString . toS . toHeader) cs
|
makeMimeList cs = MimeList $ map (fromString . toS . toHeader) cs
|
||||||
@@ -36,6 +36,13 @@ toSwaggerType "boolean" = SwaggerBoolean
|
|||||||
toSwaggerType "numeric" = SwaggerNumber
|
toSwaggerType "numeric" = SwaggerNumber
|
||||||
toSwaggerType _ = SwaggerString
|
toSwaggerType _ = SwaggerString
|
||||||
|
|
||||||
|
makeTableDef :: (Table, [Column], [Text]) -> (Text, Schema)
|
||||||
|
makeTableDef (t, cs, _) =
|
||||||
|
let tn = tableName t in
|
||||||
|
(tn, (mempty :: Schema)
|
||||||
|
& type_ .~ SwaggerObject
|
||||||
|
& properties .~ fromList (map makeProperty cs))
|
||||||
|
|
||||||
makeProperty :: Column -> (Text, Referenced Schema)
|
makeProperty :: Column -> (Text, Referenced Schema)
|
||||||
makeProperty c = (colName c, Inline u)
|
makeProperty c = (colName c, Inline u)
|
||||||
where
|
where
|
||||||
@@ -46,18 +53,20 @@ makeProperty c = (colName c, Inline u)
|
|||||||
t = s & type_ .~ toSwaggerType (colType c)
|
t = s & type_ .~ toSwaggerType (colType c)
|
||||||
u = t & format ?~ colType c
|
u = t & format ?~ colType c
|
||||||
|
|
||||||
makeProperties :: [Column] -> InsOrdHashMap Text (Referenced Schema)
|
makeProcDef :: ProcDescription -> (Text, Schema)
|
||||||
makeProperties cs = fromList $ map makeProperty cs
|
makeProcDef pd = ("(rpc) " <> pdName pd, s)
|
||||||
|
where
|
||||||
|
s = (mempty :: Schema)
|
||||||
|
& type_ .~ SwaggerObject
|
||||||
|
& properties .~ fromList (map makeProcProperty (pdArgs pd))
|
||||||
|
& required .~ map pgaName (filter pgaReq (pdArgs pd))
|
||||||
|
|
||||||
makeDefinition :: (Table, [Column], [Text]) -> (Text, Schema)
|
makeProcProperty :: PgArg -> (Text, Referenced Schema)
|
||||||
makeDefinition (t, cs, _) =
|
makeProcProperty (PgArg n t _) = (n, Inline s)
|
||||||
let tn = tableName t in
|
where
|
||||||
(tn, (mempty :: Schema)
|
s = (mempty :: Schema)
|
||||||
& type_ .~ SwaggerObject
|
& type_ .~ toSwaggerType t
|
||||||
& properties .~ makeProperties cs)
|
& format ?~ t
|
||||||
|
|
||||||
makeDefinitions :: [(Table, [Column], [Text])] -> InsOrdHashMap Text Schema
|
|
||||||
makeDefinitions ti = fromList $ map makeDefinition ti
|
|
||||||
|
|
||||||
makeOperatorPattern :: Text
|
makeOperatorPattern :: Text
|
||||||
makeOperatorPattern =
|
makeOperatorPattern =
|
||||||
@@ -173,6 +182,13 @@ makePostParams tn =
|
|||||||
& schema .~ ParamBody (Ref (Reference tn))
|
& schema .~ ParamBody (Ref (Reference tn))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
makeProcParam :: Text -> Param
|
||||||
|
makeProcParam refName =
|
||||||
|
(mempty :: Param)
|
||||||
|
& name .~ "args"
|
||||||
|
& required ?~ True
|
||||||
|
& schema .~ ParamBody (Ref (Reference refName))
|
||||||
|
|
||||||
makeDeleteParams :: [Param]
|
makeDeleteParams :: [Param]
|
||||||
makeDeleteParams =
|
makeDeleteParams =
|
||||||
[ makePreferParam ["return=representation", "return=minimal", "return=none"] ]
|
[ makePreferParam ["return=representation", "return=minimal", "return=none"] ]
|
||||||
@@ -204,6 +220,16 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
|||||||
rs = makeRowFilters cs
|
rs = makeRowFilters cs
|
||||||
tn = tableName t
|
tn = tableName t
|
||||||
|
|
||||||
|
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
|
||||||
|
makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
||||||
|
where
|
||||||
|
postOp = (mempty :: Operation)
|
||||||
|
& parameters .~ [Inline (makeProcParam $ "(rpc) " <> pdName pd)]
|
||||||
|
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
||||||
|
& produces ?~ makeMimeList [CTApplicationJSON]
|
||||||
|
& at 200 ?~ "OK"
|
||||||
|
pe = (mempty :: PathItem) & post ?~ postOp
|
||||||
|
|
||||||
makeRootPathItem :: (FilePath, PathItem)
|
makeRootPathItem :: (FilePath, PathItem)
|
||||||
makeRootPathItem = ("/", p)
|
makeRootPathItem = ("/", p)
|
||||||
where
|
where
|
||||||
@@ -214,8 +240,9 @@ makeRootPathItem = ("/", p)
|
|||||||
pr = (mempty :: PathItem) & get ?~ getOp
|
pr = (mempty :: PathItem) & get ?~ getOp
|
||||||
p = pr
|
p = pr
|
||||||
|
|
||||||
makePathItems :: [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
|
makePathItems :: [ProcDescription] -> [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
|
||||||
makePathItems ti = fromList $ makeRootPathItem : map makePathItem ti
|
makePathItems pds ti = fromList $ makeRootPathItem :
|
||||||
|
map makePathItem ti ++ map makeProcPathItem pds
|
||||||
|
|
||||||
escapeHostName :: Text -> Text
|
escapeHostName :: Text -> Text
|
||||||
escapeHostName "*" = "0.0.0.0"
|
escapeHostName "*" = "0.0.0.0"
|
||||||
@@ -225,8 +252,8 @@ escapeHostName "*6" = "0.0.0.0"
|
|||||||
escapeHostName "!6" = "0.0.0.0"
|
escapeHostName "!6" = "0.0.0.0"
|
||||||
escapeHostName h = h
|
escapeHostName h = h
|
||||||
|
|
||||||
postgrestSpec:: [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Swagger
|
postgrestSpec :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Swagger
|
||||||
postgrestSpec ti (s, h, p, b) = (mempty :: Swagger)
|
postgrestSpec pds ti (s, h, p, b) = (mempty :: Swagger)
|
||||||
& basePath ?~ unpack b
|
& basePath ?~ unpack b
|
||||||
& schemes ?~ [s']
|
& schemes ?~ [s']
|
||||||
& info .~ ((mempty :: Info)
|
& info .~ ((mempty :: Info)
|
||||||
@@ -234,14 +261,14 @@ postgrestSpec ti (s, h, p, b) = (mempty :: Swagger)
|
|||||||
& title .~ "PostgREST API"
|
& title .~ "PostgREST API"
|
||||||
& description ?~ "This is a dynamic API generated by PostgREST")
|
& description ?~ "This is a dynamic API generated by PostgREST")
|
||||||
& host .~ h'
|
& host .~ h'
|
||||||
& definitions .~ makeDefinitions ti
|
& definitions .~ fromList (map makeTableDef ti <> map makeProcDef pds)
|
||||||
& paths .~ makePathItems ti
|
& paths .~ makePathItems pds ti
|
||||||
where
|
where
|
||||||
s' = if s == "http" then Http else Https
|
s' = if s == "http" then Http else Https
|
||||||
h' = Just $ Host (unpack $ escapeHostName h) (Just (fromInteger p))
|
h' = Just $ Host (unpack $ escapeHostName h) (Just (fromInteger p))
|
||||||
|
|
||||||
encodeOpenAPI :: [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> LByteString
|
encodeOpenAPI :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> LByteString
|
||||||
encodeOpenAPI ti uri = encode $ postgrestSpec ti uri
|
encodeOpenAPI pds ti uri = encode $ postgrestSpec pds ti uri
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Test whether a proxy uri is malformed or not.
|
Test whether a proxy uri is malformed or not.
|
||||||
|
|||||||
+13
-1
@@ -13,7 +13,19 @@ data DbStructure = DbStructure {
|
|||||||
, dbColumns :: [Column]
|
, dbColumns :: [Column]
|
||||||
, dbRelations :: [Relation]
|
, dbRelations :: [Relation]
|
||||||
, dbPrimaryKeys :: [PrimaryKey]
|
, dbPrimaryKeys :: [PrimaryKey]
|
||||||
, dbProcs :: [(Text,Text)]
|
, dbProcs :: [(Text,ProcDescription)]
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
data PgArg = PgArg {
|
||||||
|
pgaName :: Text
|
||||||
|
, pgaType :: Text
|
||||||
|
, pgaReq :: Bool
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
data ProcDescription = ProcDescription {
|
||||||
|
pdName :: Text
|
||||||
|
, pdArgs :: [PgArg]
|
||||||
|
, pdReturnType :: Text
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
type Schema = Text
|
type Schema = Text
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import Test.Hspec hiding (pendingWith)
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
|
||||||
|
import Control.Lens ((^?))
|
||||||
|
import Data.Aeson.Lens
|
||||||
|
import Data.Aeson.QQ
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import Network.Wai (Application)
|
import Network.Wai (Application)
|
||||||
import Network.Wai.Test (SResponse(simpleHeaders))
|
import Network.Wai.Test (SResponse(..))
|
||||||
|
|
||||||
spec :: SpecWith Application
|
spec :: SpecWith Application
|
||||||
spec = do
|
spec = do
|
||||||
@@ -21,6 +25,63 @@ spec = do
|
|||||||
(acceptHdrs "application/openapi+json") ""
|
(acceptHdrs "application/openapi+json") ""
|
||||||
`shouldRespondWith` 415
|
`shouldRespondWith` 415
|
||||||
|
|
||||||
|
describe "RPC" $
|
||||||
|
|
||||||
|
it "includes a representative function with parameters" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
let ref = r ^? key "paths" . key "/rpc/varied_arguments"
|
||||||
|
. key "post" . key "parameters"
|
||||||
|
. nth 0 . key "schema"
|
||||||
|
. key "$ref" . _String
|
||||||
|
args = r ^? key "definitions" . key "(rpc) varied_arguments"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
ref `shouldBe` Just "#/definitions/(rpc) varied_arguments"
|
||||||
|
args `shouldBe` Just
|
||||||
|
[aesonQQ|
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"double",
|
||||||
|
"varchar",
|
||||||
|
"boolean",
|
||||||
|
"date",
|
||||||
|
"money",
|
||||||
|
"enum"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"double": {
|
||||||
|
"format": "double precision",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"varchar": {
|
||||||
|
"format": "character varying",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"boolean": {
|
||||||
|
"format": "boolean",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"format": "date",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"money": {
|
||||||
|
"format": "money",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"enum": {
|
||||||
|
"format": "test.enum_menagerie_type",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"integer": {
|
||||||
|
"format": "integer",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
|
||||||
describe "Allow header" $ do
|
describe "Allow header" $ do
|
||||||
|
|
||||||
it "includes read/write verbs for writeable table" $ do
|
it "includes read/write verbs for writeable table" $ do
|
||||||
|
|||||||
Vendored
+15
@@ -215,6 +215,21 @@ SELECT rolname::text, id::text FROM postgrest.auth WHERE id = id AND pass = pass
|
|||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE FUNCTION varied_arguments(
|
||||||
|
double double precision,
|
||||||
|
"varchar" character varying,
|
||||||
|
"boolean" boolean,
|
||||||
|
date date,
|
||||||
|
money money,
|
||||||
|
enum enum_menagerie_type,
|
||||||
|
"integer" integer default 42
|
||||||
|
) RETURNS text
|
||||||
|
LANGUAGE sql
|
||||||
|
AS $_$
|
||||||
|
SELECT 'Hi'::text;
|
||||||
|
$_$;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
|
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
|
||||||
--
|
--
|
||||||
|
|||||||
Reference in New Issue
Block a user