From cf4e157de7835d252b276e96a43fa1549494f99f Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Tue, 14 Jun 2016 08:39:07 +0800 Subject: [PATCH 01/24] Provide a swagger2 spec for the dynamic API Related issue: #144 --- main/Main.hs | 5 +- postgrest.cabal | 25 +++-- src/PostgREST/ApiSpec.hs | 204 +++++++++++++++++++++++++++++++++++++++ src/PostgREST/App.hs | 22 ++++- src/PostgREST/Auth.hs | 4 +- src/PostgREST/Config.hs | 2 + stack.yaml | 6 +- 7 files changed, 254 insertions(+), 14 deletions(-) create mode 100644 src/PostgREST/ApiSpec.hs diff --git a/main/Main.hs b/main/Main.hs index f0fe92de1..24398affe 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -19,6 +19,7 @@ import qualified Hasql.Decoders as HD import qualified Hasql.Encoders as HE import qualified Hasql.Pool as P import Network.Wai.Handler.Warp +import Network.BSD (getHostName) import System.IO (BufferMode (..), hSetBuffering, stderr, stdin, stdout) @@ -46,7 +47,9 @@ main = do hSetBuffering stdin LineBuffering hSetBuffering stderr NoBuffering - conf <- readOptions + conf' <- readOptions + host <- getHostName + conf <- return conf' { configHost = host } let port = configPort conf pgSettings = cs (configDatabase conf) appSettings = setPort port diff --git a/postgrest.cabal b/postgrest.cabal index d70ae754e..ee5a412d8 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -44,8 +44,8 @@ executable postgrest , http-types , interpolatedstring-perl6 , jwt - , microlens >= 0.4.2 && < 0.5 - , microlens-aeson >= 2.1.1 && < 2.2 + , lens >=3.8 && < 5.0 + , lens-aeson >= 1.0.0.0 && < 1.1.0.0 , mtl , optparse-applicative >= 0.11 && < 0.13 , parsec @@ -65,6 +65,10 @@ executable postgrest , wai-extra , wai-middleware-static >= 0.6.0 , warp >= 3.1.0 + , insert-ordered-containers >= 0.1.0.1 + , http-media >= 0.6.3 + , swagger2 >= 2.1 + , network >= 2.6.2.1 , HTTP , Ranged-sets if !os(windows) @@ -89,8 +93,8 @@ library , http-types , interpolatedstring-perl6 , jwt - , microlens - , microlens-aeson + , lens + , lens-aeson , mtl , optparse-applicative , parsec @@ -109,6 +113,10 @@ library , wai-extra , wai-middleware-static >= 0.6.0 , warp >= 3.1.0 + , insert-ordered-containers >= 0.1.0.1 + , http-media >= 0.6.3 + , swagger2 >= 2.1 + , network >= 2.6.2.1 Other-Modules: Paths_postgrest Exposed-Modules: PostgREST.App @@ -122,6 +130,7 @@ library , PostgREST.RangeQuery , PostgREST.ApiRequest , PostgREST.Types + , PostgREST.ApiSpec hs-source-dirs: src Test-Suite spec @@ -163,8 +172,8 @@ Test-Suite spec , http-types , interpolatedstring-perl6 , jwt - , microlens - , microlens-aeson + , lens + , lens-aeson , monad-control , mtl , optparse-applicative @@ -186,5 +195,9 @@ Test-Suite spec , wai-extra , wai-middleware-static , warp + , insert-ordered-containers + , http-media + , swagger2 + , network , HTTP , Ranged-sets diff --git a/src/PostgREST/ApiSpec.hs b/src/PostgREST/ApiSpec.hs new file mode 100644 index 000000000..d7a1c704c --- /dev/null +++ b/src/PostgREST/ApiSpec.hs @@ -0,0 +1,204 @@ +{-# LANGUAGE OverloadedStrings #-} + +module PostgREST.ApiSpec ( + apiSpec + ) where + +import Control.Lens +import Data.Aeson (decode, encode) +import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList) +import Data.String (IsString (..)) +import Data.Text (Text, unpack, pack, concat, intercalate) +import Network.HTTP.Media (MediaType) +import qualified Data.Set as Set + +import Prelude hiding (concat) + +import Data.Swagger + +import PostgREST.ApiRequest (ContentType(..)) +import PostgREST.Config (prettyVersion) +import PostgREST.QueryBuilder (operators) +import PostgREST.Types (Table(..), Column(..)) + +makeMimeList :: [MediaType] +makeMimeList = map (fromString . show) [ApplicationJSON, TextCSV] + +toSwaggerType :: Text -> SwaggerType t +toSwaggerType "text" = SwaggerString +toSwaggerType "integer" = SwaggerInteger +toSwaggerType "boolean" = SwaggerBoolean +toSwaggerType "numeric" = SwaggerNumber +toSwaggerType _ = SwaggerString + +makeProperty :: Column -> (Text, Referenced Schema) +makeProperty c = (colName c, Inline $ u) + where + r = (mempty :: Schema) + s = if null $ colEnum c + then r + else r & enum_ .~ decode (encode (colEnum c)) + t = s & type_ .~ (toSwaggerType $ colType c) + u = t & format ?~ colType c + +makeProperties :: [Column] -> InsOrdHashMap Text (Referenced Schema) +makeProperties cs = fromList $ map makeProperty cs + +makeDefinition :: (Table, [Column], [Text]) -> (Text, Schema) +makeDefinition (t, cs, _) = + let tn = tableName t in + (tn, (mempty :: Schema) + & type_ .~ SwaggerObject + & properties .~ (makeProperties cs)) + +makeDefinitions :: [(Table, [Column], [Text])] -> InsOrdHashMap Text Schema +makeDefinitions ti = fromList $ map makeDefinition ti + +makeOperatorPattern :: Text +makeOperatorPattern = + intercalate "|" + [ concat ["^", x, y, "[.]"] | + x <- ["not[.]", ""], + y <- map fst operators ] + +makeRowFilter :: Column -> Param +makeRowFilter c = + (mempty :: Param) + & name .~ colName c + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamQuery + & type_ .~ SwaggerString + & format ?~ colType c + & pattern ?~ makeOperatorPattern) + +makeRowFilters :: [Column] -> [Param] +makeRowFilters cs = map makeRowFilter cs + +makeOrderItems :: [Column] -> [Text] +makeOrderItems cs = + [ concat [x, y, z] | + x <- map colName cs, + y <- [".asc", ".desc", ""], + z <- [".nullsfirst", ".nulllast", ""] + ] + +makeRangeParams :: [Param] +makeRangeParams = + [ (mempty :: Param) + & name .~ "Range" + & description ?~ "Limiting and Pagination" + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamHeader + & type_ .~ SwaggerString) + , (mempty :: Param) + & name .~ "Range-Unit" + & description ?~ "Limiting and Pagination" + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamHeader + & type_ .~ SwaggerString + & default_ .~ decode "\"items\"") + , (mempty :: Param) + & name .~ "offset" + & description ?~ "Limiting and Pagination" + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamQuery + & type_ .~ SwaggerString) + , (mempty :: Param) + & name .~ "limit" + & description ?~ "Limiting and Pagination" + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamQuery + & type_ .~ SwaggerString) + ] + +makePreferParam :: [Text] -> Param +makePreferParam ts = + (mempty :: Param) + & name .~ "Prefer" + & description ?~ "Preference" + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamHeader + & type_ .~ SwaggerString + & enum_ .~ decode (encode ts)) + +makeGetParams :: [Column] -> [Param] +makeGetParams cs = + makeRangeParams ++ + [ (mempty :: Param) + & name .~ "select" + & description ?~ "Filtering Columns" + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamQuery + & type_ .~ SwaggerString) + , (mempty :: Param) + & name .~ "order" + & description ?~ "Ordering" + & required ?~ False + & schema .~ ParamOther ((mempty :: ParamOtherSchema) + & in_ .~ ParamQuery + & type_ .~ SwaggerString + & enum_ .~ decode (encode $ makeOrderItems cs)) + , makePreferParam ["plurality=singular", "count=none"] + ] + +makePostParams :: Text -> [Param] +makePostParams tn = + [ makePreferParam ["return=representation", "return=minimal"] + , (mempty :: Param) + & name .~ "body" + & description ?~ tn + & required ?~ False + & schema .~ ParamBody (Ref (Reference tn)) + ] + +makeDeleteParams :: [Param] +makeDeleteParams = + [ makePreferParam ["return=representation", "return=minimal"] ] + +makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem) +makePathItem (t, cs, _) = ("/" ++ (unpack tn), p $ tableInsertable t) + where + tOp = (mempty :: Operation) + & tags .~ Set.fromList [tn] + & produces ?~ MimeList makeMimeList + & at 200 ?~ "OK" + getOp = tOp + & parameters .~ map Inline (makeGetParams cs ++ rs) + postOp = tOp + & consumes ?~ MimeList makeMimeList + & parameters .~ map Inline (makePostParams tn) + patchOp = tOp + & consumes ?~ MimeList makeMimeList + & parameters .~ map Inline (makePostParams tn ++ rs) + deletOp = tOp + & parameters .~ map Inline (makeDeleteParams ++ rs) + pr = (mempty :: PathItem) & get ?~ getOp + pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp + p False = pr + p True = pw + rs = makeRowFilters cs + tn = tableName t + +makePathItems :: [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem +makePathItems ti = fromList (map makePathItem ti) + +apiSpec :: [(Table, [Column], [Text])] -> String -> Integer -> Swagger +apiSpec ti h p = (mempty :: Swagger) + & basePath ?~ "/" + & schemes ?~ [Http] + & info .~ ((mempty :: Info) + & version .~ (pack prettyVersion) + & title .~ "PostgREST API" + & description ?~ "This is a dynamic API generated by PostgREST") + & host .~ h' + & definitions .~ (makeDefinitions ti) + & paths .~ (makePathItems ti) + where + h' = Just $ Host h (Just (fromInteger p)) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 0a3c4722a..ac686d368 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -9,6 +9,7 @@ module PostgREST.App ( import Control.Applicative import Data.Bifunctor (first) import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy as BL import Data.IORef (IORef, readIORef) import Data.List (find, delete) import Data.Maybe (fromMaybe, fromJust, mapMaybe) @@ -60,6 +61,7 @@ import PostgREST.QueryBuilder ( callProc , ResultsWithCount ) import PostgREST.Types +import PostgREST.ApiSpec import Prelude @@ -180,9 +182,6 @@ app dbStructure conf apiRequest = let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys body = encode (TableOptions cols pkeys) - filterCol :: Schema -> TableName -> Column -> Bool - filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb - filterCol _ _ _ = False acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in return $ responseLBS status200 [jsonH, allOrigins, acceptH] $ cs body @@ -206,7 +205,7 @@ app dbStructure conf apiRequest = else return notFound (ActionRead, TargetRoot, Nothing) -> do - body <- encode <$> H.query schema accessibleTables + body <- (encodeApi . toTableInfo) <$> H.query schema accessibleTables return $ responseLBS status200 [jsonH] $ cs body (ActionInappropriate, _, _) -> return $ responseLBS status405 [] "" @@ -220,8 +219,23 @@ app dbStructure conf apiRequest = (_, _, _) -> return notFound where + toTableInfo :: [Table] -> [(Table, [Column], [Text])] + toTableInfo ts = map (\t -> + let tSchema = tableSchema t + tTable = tableName t + cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure + pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys + in + (t, cols, pkeys)) ts + encodeApi :: [(Table, [Column], [Text])] -> BL.ByteString + encodeApi ti = encode $ apiSpec ti host port + host = configHost conf + port = toInteger $ configPort conf notFound = responseLBS status404 [] "" filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk + filterCol :: Schema -> TableName -> Column -> Bool + filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb + filterCol _ _ _ = False allPrKeys = dbPrimaryKeys dbStructure allOrigins = ("Access-Control-Allow-Origin", "*") :: Header schema = cs $ configSchema conf diff --git a/src/PostgREST/Auth.hs b/src/PostgREST/Auth.hs index 701ba5f23..ed6591f4f 100644 --- a/src/PostgREST/Auth.hs +++ b/src/PostgREST/Auth.hs @@ -18,9 +18,9 @@ module PostgREST.Auth ( , tokenJWT ) where -import Lens.Micro -import Lens.Micro.Aeson +import Control.Lens import Data.Aeson (Value (..), parseJSON, toJSON) +import Data.Aeson.Lens import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray) import qualified Data.ByteString as BS import qualified Data.Vector as V diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 1f317c1bb..6f91e3b8f 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -39,6 +39,7 @@ data AppConfig = AppConfig { configDatabase :: String , configAnonRole :: String , configSchema :: String + , configHost :: String , configPort :: Int , configJwtSecret :: Secret , configPool :: Int @@ -51,6 +52,7 @@ argParser = AppConfig <$> argument str (help "(REQUIRED) database connection string, e.g. postgres://user:pass@host:port/db" <> metavar "DB_URL") <*> strOption (long "anonymous" <> short 'a' <> help "(REQUIRED) postgres role to use for non-authenticated requests" <> metavar "ROLE") <*> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault) + <*> pure "localhost" <*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault) <*> (secret . cs <$> strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault)) diff --git a/stack.yaml b/stack.yaml index cfe320b1b..5a4acbecf 100644 --- a/stack.yaml +++ b/stack.yaml @@ -16,7 +16,11 @@ extra-deps: - wai-cors-0.2.5 - cryptohash-sha256-0.11.100.0 - hackage-security-0.5.2.1 - + - unordered-containers-0.2.7.1 + - insert-ordered-containers-0.1.0.1 + - swagger2-2.1 + - http-media-0.6.3 + - network-2.6.2.1 ghc-options: postgrest: -O2 -Werror -Wall -fwarn-identities From 4b0c5cb36f9262251ca832f2d8e8199f24feeabd Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Tue, 14 Jun 2016 10:59:11 +0800 Subject: [PATCH 02/24] Disable schema test --- test/Feature/StructureSpec.hs | 48 ----------------------------------- test/SpecHelper.hs | 6 ++--- 2 files changed, 3 insertions(+), 51 deletions(-) diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs index 3d58947ac..8abe90e9c 100644 --- a/test/Feature/StructureSpec.hs +++ b/test/Feature/StructureSpec.hs @@ -13,54 +13,6 @@ import Network.Wai.Test (SResponse(simpleHeaders)) spec :: SpecWith Application spec = do - describe "GET /" $ do - it "lists views in schema" $ - request methodGet "/" [] "" - `shouldRespondWith` [json| [ - {"schema":"test","name":"Escap3e;","insertable":true} - , {"schema":"test","name":"addresses","insertable":true} - , {"schema":"test","name":"articleStars","insertable":true} - , {"schema":"test","name":"articles","insertable":true} - , {"schema":"test","name":"auto_incrementing_pk","insertable":true} - , {"schema":"test","name":"clients","insertable":true} - , {"schema":"test","name":"comments","insertable":true} - , {"schema":"test","name":"complex_items","insertable":true} - , {"schema":"test","name":"compound_pk","insertable":true} - , {"schema":"test","name":"empty_table","insertable":true} - , {"schema":"test","name":"filtered_tasks","insertable":true} - , {"schema":"test","name":"ghostBusters","insertable":true} - , {"schema":"test","name":"has_count_column","insertable":false} - , {"schema":"test","name":"has_fk","insertable":true} - , {"schema":"test","name":"insertable_view_with_join","insertable":true} - , {"schema":"test","name":"insertonly","insertable":true} - , {"schema":"test","name":"items","insertable":true} - , {"schema":"test","name":"json","insertable":true} - , {"schema":"test","name":"materialized_view","insertable":false} - , {"schema":"test","name":"menagerie","insertable":true} - , {"schema":"test","name":"no_pk","insertable":true} - , {"schema":"test","name":"nullable_integer","insertable":true} - , {"schema":"test","name":"orders","insertable":true} - , {"schema":"test","name":"projects","insertable":true} - , {"schema":"test","name":"projects_view","insertable":true} - , {"schema":"test","name":"simple_pk","insertable":true} - , {"schema":"test","name":"tasks","insertable":true} - , {"schema":"test","name":"tsearch","insertable":true} - , {"schema":"test","name":"users","insertable":true} - , {"schema":"test","name":"users_projects","insertable":true} - , {"schema":"test","name":"users_tasks","insertable":true} - , {"schema":"test","name":"withUnique","insertable":true} - ] |] - {matchStatus = 200} - - it "lists only views user has permission to see" $ do - let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" - - request methodGet "/" [auth] "" - `shouldRespondWith` [json| [ - {"schema":"test","name":"authors_only","insertable":true} - ] |] - {matchStatus = 200} - describe "Table info" $ do it "The structure of complex views is correctly detected" $ request methodOptions "/filtered_tasks" [] "" `shouldRespondWith` diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index e837887fb..9e80f9e26 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -19,15 +19,15 @@ testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_t testCfg :: AppConfig testCfg = - AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 Nothing True + AppConfig testDbConn "postgrest_test_anonymous" "test" "localhost" 3000 (secret "safe") 10 Nothing True testUnicodeCfg :: AppConfig testUnicodeCfg = - AppConfig testDbConn "postgrest_test_anonymous" "تست" 3000 (secret "safe") 10 Nothing True + AppConfig testDbConn "postgrest_test_anonymous" "تست" "localhost" 3000 (secret "safe") 10 Nothing True testLtdRowsCfg :: AppConfig testLtdRowsCfg = - AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 (Just 2) True + AppConfig testDbConn "postgrest_test_anonymous" "test" "localhost" 3000 (secret "safe") 10 (Just 2) True setupDb :: IO () setupDb = do From 74ea4aba379c94b0d7faaf03d8e7841ed3a06457 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Tue, 14 Jun 2016 11:30:31 +0800 Subject: [PATCH 03/24] Fix all hlint errors except for pattern error --- main/Main.hs | 2 +- src/PostgREST/ApiSpec.hs | 18 +++++++++--------- src/PostgREST/App.hs | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/main/Main.hs b/main/Main.hs index 24398affe..b9b8723bd 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -49,7 +49,7 @@ main = do conf' <- readOptions host <- getHostName - conf <- return conf' { configHost = host } + let conf = conf'{configHost = host} let port = configPort conf pgSettings = cs (configDatabase conf) appSettings = setPort port diff --git a/src/PostgREST/ApiSpec.hs b/src/PostgREST/ApiSpec.hs index d7a1c704c..408cc0a03 100644 --- a/src/PostgREST/ApiSpec.hs +++ b/src/PostgREST/ApiSpec.hs @@ -32,13 +32,13 @@ toSwaggerType "numeric" = SwaggerNumber toSwaggerType _ = SwaggerString makeProperty :: Column -> (Text, Referenced Schema) -makeProperty c = (colName c, Inline $ u) +makeProperty c = (colName c, Inline u) where - r = (mempty :: Schema) + r = mempty :: Schema s = if null $ colEnum c then r else r & enum_ .~ decode (encode (colEnum c)) - t = s & type_ .~ (toSwaggerType $ colType c) + t = s & type_ .~ toSwaggerType (colType c) u = t & format ?~ colType c makeProperties :: [Column] -> InsOrdHashMap Text (Referenced Schema) @@ -49,7 +49,7 @@ makeDefinition (t, cs, _) = let tn = tableName t in (tn, (mempty :: Schema) & type_ .~ SwaggerObject - & properties .~ (makeProperties cs)) + & properties .~ makeProperties cs) makeDefinitions :: [(Table, [Column], [Text])] -> InsOrdHashMap Text Schema makeDefinitions ti = fromList $ map makeDefinition ti @@ -73,7 +73,7 @@ makeRowFilter c = & pattern ?~ makeOperatorPattern) makeRowFilters :: [Column] -> [Param] -makeRowFilters cs = map makeRowFilter cs +makeRowFilters = map makeRowFilter makeOrderItems :: [Column] -> [Text] makeOrderItems cs = @@ -163,7 +163,7 @@ makeDeleteParams = [ makePreferParam ["return=representation", "return=minimal"] ] makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem) -makePathItem (t, cs, _) = ("/" ++ (unpack tn), p $ tableInsertable t) +makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t) where tOp = (mempty :: Operation) & tags .~ Set.fromList [tn] @@ -194,11 +194,11 @@ apiSpec ti h p = (mempty :: Swagger) & basePath ?~ "/" & schemes ?~ [Http] & info .~ ((mempty :: Info) - & version .~ (pack prettyVersion) + & version .~ pack prettyVersion & title .~ "PostgREST API" & description ?~ "This is a dynamic API generated by PostgREST") & host .~ h' - & definitions .~ (makeDefinitions ti) - & paths .~ (makePathItems ti) + & definitions .~ makeDefinitions ti + & paths .~ makePathItems ti where h' = Just $ Host h (Just (fromInteger p)) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index ac686d368..a03afd9ed 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -220,13 +220,13 @@ app dbStructure conf apiRequest = where toTableInfo :: [Table] -> [(Table, [Column], [Text])] - toTableInfo ts = map (\t -> + toTableInfo = map (\t -> let tSchema = tableSchema t tTable = tableName t cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys in - (t, cols, pkeys)) ts + (t, cols, pkeys)) encodeApi :: [(Table, [Column], [Text])] -> BL.ByteString encodeApi ti = encode $ apiSpec ti host port host = configHost conf From d34056b3b93bf61e4cfd576a922a4624aa69a909 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Tue, 14 Jun 2016 11:31:29 +0800 Subject: [PATCH 04/24] Work around hlint pattern error See also: https://github.com/ndmitchell/hlint/issues/216 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 532d44050..1e6da4d1e 100644 --- a/circle.yml +++ b/circle.yml @@ -18,7 +18,7 @@ dependencies: test: override: - stack test - - git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes "$@" + - git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes -X NoPatternSynonyms "$@" - stack exec -- cabal update - stack exec --no-ghc-package-path -- cabal install --only-d --dry-run - stack exec -- packdeps *.cabal || true From 0e456543bf219fbb9c4d7e211ac6e9601e17e529 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Tue, 14 Jun 2016 20:33:04 +0800 Subject: [PATCH 05/24] Add a root path item --- src/PostgREST/ApiSpec.hs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PostgREST/ApiSpec.hs b/src/PostgREST/ApiSpec.hs index 408cc0a03..9b3159fae 100644 --- a/src/PostgREST/ApiSpec.hs +++ b/src/PostgREST/ApiSpec.hs @@ -186,8 +186,18 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t) rs = makeRowFilters cs tn = tableName t +makeRootPathItem :: (FilePath, PathItem) +makeRootPathItem = ("/", p) + where + getOp = (mempty :: Operation) + & tags .~ Set.fromList ["/"] + & produces ?~ MimeList [(fromString . show) ApplicationJSON] + & at 200 ?~ "OK" + pr = (mempty :: PathItem) & get ?~ getOp + p = pr + makePathItems :: [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem -makePathItems ti = fromList (map makePathItem ti) +makePathItems ti = fromList $ makeRootPathItem : map makePathItem ti apiSpec :: [(Table, [Column], [Text])] -> String -> Integer -> Swagger apiSpec ti h p = (mempty :: Swagger) From 563c5fa778f886cab2a046d9cc2f00b515f5243f Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Wed, 15 Jun 2016 10:52:14 +0800 Subject: [PATCH 06/24] Make hostname configurable --- main/Main.hs | 5 +---- postgrest.cabal | 3 --- src/PostgREST/Config.hs | 2 +- stack.yaml | 1 - 4 files changed, 2 insertions(+), 9 deletions(-) diff --git a/main/Main.hs b/main/Main.hs index b9b8723bd..f0fe92de1 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -19,7 +19,6 @@ import qualified Hasql.Decoders as HD import qualified Hasql.Encoders as HE import qualified Hasql.Pool as P import Network.Wai.Handler.Warp -import Network.BSD (getHostName) import System.IO (BufferMode (..), hSetBuffering, stderr, stdin, stdout) @@ -47,9 +46,7 @@ main = do hSetBuffering stdin LineBuffering hSetBuffering stderr NoBuffering - conf' <- readOptions - host <- getHostName - let conf = conf'{configHost = host} + conf <- readOptions let port = configPort conf pgSettings = cs (configDatabase conf) appSettings = setPort port diff --git a/postgrest.cabal b/postgrest.cabal index ee5a412d8..e6f754ace 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -68,7 +68,6 @@ executable postgrest , insert-ordered-containers >= 0.1.0.1 , http-media >= 0.6.3 , swagger2 >= 2.1 - , network >= 2.6.2.1 , HTTP , Ranged-sets if !os(windows) @@ -116,7 +115,6 @@ library , insert-ordered-containers >= 0.1.0.1 , http-media >= 0.6.3 , swagger2 >= 2.1 - , network >= 2.6.2.1 Other-Modules: Paths_postgrest Exposed-Modules: PostgREST.App @@ -198,6 +196,5 @@ Test-Suite spec , insert-ordered-containers , http-media , swagger2 - , network , HTTP , Ranged-sets diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 6f91e3b8f..d8d840c41 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -52,7 +52,7 @@ argParser = AppConfig <$> argument str (help "(REQUIRED) database connection string, e.g. postgres://user:pass@host:port/db" <> metavar "DB_URL") <*> strOption (long "anonymous" <> short 'a' <> help "(REQUIRED) postgres role to use for non-authenticated requests" <> metavar "ROLE") <*> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault) - <*> pure "localhost" + <*> strOption (long "hostname" <> short 'n' <> help "hostname on which the HTTP server is running" <> metavar "HOSTNAME" <> value "localhost" <> showDefault) <*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault) <*> (secret . cs <$> strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault)) diff --git a/stack.yaml b/stack.yaml index 5a4acbecf..8ba4393b0 100644 --- a/stack.yaml +++ b/stack.yaml @@ -20,7 +20,6 @@ extra-deps: - insert-ordered-containers-0.1.0.1 - swagger2-2.1 - http-media-0.6.3 - - network-2.6.2.1 ghc-options: postgrest: -O2 -Werror -Wall -fwarn-identities From 103fa0550c0ab5d7f60b865622eff2988a6eb8b8 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Wed, 15 Jun 2016 11:17:39 +0800 Subject: [PATCH 07/24] Use a specific name for OpenAPI module --- postgrest.cabal | 2 +- src/PostgREST/App.hs | 6 ++---- src/PostgREST/{ApiSpec.hs => OpenAPI.hs} | 12 ++++++++---- 3 files changed, 11 insertions(+), 9 deletions(-) rename src/PostgREST/{ApiSpec.hs => OpenAPI.hs} (94%) diff --git a/postgrest.cabal b/postgrest.cabal index e6f754ace..f7f77a745 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -128,7 +128,7 @@ library , PostgREST.RangeQuery , PostgREST.ApiRequest , PostgREST.Types - , PostgREST.ApiSpec + , PostgREST.OpenAPI hs-source-dirs: src Test-Suite spec diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index a03afd9ed..7ed46136f 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -9,7 +9,6 @@ module PostgREST.App ( import Control.Applicative import Data.Bifunctor (first) import qualified Data.ByteString.Char8 as BS -import qualified Data.ByteString.Lazy as BL import Data.IORef (IORef, readIORef) import Data.List (find, delete) import Data.Maybe (fromMaybe, fromJust, mapMaybe) @@ -61,7 +60,7 @@ import PostgREST.QueryBuilder ( callProc , ResultsWithCount ) import PostgREST.Types -import PostgREST.ApiSpec +import PostgREST.OpenAPI import Prelude @@ -227,8 +226,7 @@ app dbStructure conf apiRequest = pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys in (t, cols, pkeys)) - encodeApi :: [(Table, [Column], [Text])] -> BL.ByteString - encodeApi ti = encode $ apiSpec ti host port + encodeApi ti = encodeOpenAPI ti host port host = configHost conf port = toInteger $ configPort conf notFound = responseLBS status404 [] "" diff --git a/src/PostgREST/ApiSpec.hs b/src/PostgREST/OpenAPI.hs similarity index 94% rename from src/PostgREST/ApiSpec.hs rename to src/PostgREST/OpenAPI.hs index 9b3159fae..6d8c3a0e1 100644 --- a/src/PostgREST/ApiSpec.hs +++ b/src/PostgREST/OpenAPI.hs @@ -1,11 +1,12 @@ {-# LANGUAGE OverloadedStrings #-} -module PostgREST.ApiSpec ( - apiSpec +module PostgREST.OpenAPI ( + encodeOpenAPI ) where import Control.Lens import Data.Aeson (decode, encode) +import Data.ByteString.Lazy (ByteString) import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList) import Data.String (IsString (..)) import Data.Text (Text, unpack, pack, concat, intercalate) @@ -199,8 +200,8 @@ makeRootPathItem = ("/", p) makePathItems :: [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem makePathItems ti = fromList $ makeRootPathItem : map makePathItem ti -apiSpec :: [(Table, [Column], [Text])] -> String -> Integer -> Swagger -apiSpec ti h p = (mempty :: Swagger) +postgrestSpec:: [(Table, [Column], [Text])] -> String -> Integer -> Swagger +postgrestSpec ti h p = (mempty :: Swagger) & basePath ?~ "/" & schemes ?~ [Http] & info .~ ((mempty :: Info) @@ -212,3 +213,6 @@ apiSpec ti h p = (mempty :: Swagger) & paths .~ makePathItems ti where h' = Just $ Host h (Just (fromInteger p)) + +encodeOpenAPI :: [(Table, [Column], [Text])] -> String -> Integer -> ByteString +encodeOpenAPI ti h p = encode $ postgrestSpec ti h p From d1ed884a8ddf9a9a1176b78327a8b1efc16eab17 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Wed, 15 Jun 2016 12:06:51 +0800 Subject: [PATCH 08/24] Introduce the OpenAPI header Also bring back the original behavior of GET "/" --- postgrest.cabal | 3 --- src/PostgREST/ApiRequest.hs | 7 ++++- src/PostgREST/App.hs | 4 ++- src/PostgREST/OpenAPI.hs | 14 +++++----- stack.yaml | 1 - test/Feature/StructureSpec.hs | 48 +++++++++++++++++++++++++++++++++++ 6 files changed, 64 insertions(+), 13 deletions(-) diff --git a/postgrest.cabal b/postgrest.cabal index f7f77a745..0735657a0 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -66,7 +66,6 @@ executable postgrest , wai-middleware-static >= 0.6.0 , warp >= 3.1.0 , insert-ordered-containers >= 0.1.0.1 - , http-media >= 0.6.3 , swagger2 >= 2.1 , HTTP , Ranged-sets @@ -113,7 +112,6 @@ library , wai-middleware-static >= 0.6.0 , warp >= 3.1.0 , insert-ordered-containers >= 0.1.0.1 - , http-media >= 0.6.3 , swagger2 >= 2.1 Other-Modules: Paths_postgrest @@ -194,7 +192,6 @@ Test-Suite spec , wai-middleware-static , warp , insert-ordered-containers - , http-media , swagger2 , HTTP , Ranged-sets diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index 02fa5da37..c1da04df7 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -45,10 +45,11 @@ data Target = TargetIdent QualifiedIdentifier data PreferRepresentation = Full | HeadersOnly | None deriving Eq -- | Enumeration of currently supported content types for -- route responses and upload payloads -data ContentType = ApplicationJSON | TextCSV deriving Eq +data ContentType = ApplicationJSON | TextCSV | OpenAPI deriving Eq instance Show ContentType where show ApplicationJSON = "application/json; charset=utf-8" show TextCSV = "text/csv; charset=utf-8" + show OpenAPI = "application/openapi+json; charset=utf-8" {-| Describes what the user wants to do. This data type is a @@ -123,6 +124,8 @@ userApiRequest schema req reqBody = Nothing -> PayloadParseError "All lines must have same number of fields" Just json -> PayloadJSON json) (CSV.decodeByName reqBody) + Right OpenAPI -> + PayloadParseError "Content-type not acceptable" -- This is a Left value because form-urlencoded is not a content -- type which we ever use for responses, only something we handle -- just this once for requests @@ -208,11 +211,13 @@ pickContentType :: Maybe BS.ByteString -> Either BS.ByteString ContentType pickContentType accept | isNothing accept || has ctAll || has ctJson = Right ApplicationJSON | has ctCsv = Right TextCSV + | has ctOpenAPI = Right OpenAPI | otherwise = Left accept' where ctAll = "*/*" ctCsv = "text/csv" ctJson = "application/json" + ctOpenAPI = "application/openapi+json" Just accept' = accept findInAccept = flip find $ parseHttpAccept accept' has = isJust . findInAccept . BS.isPrefixOf diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 7ed46136f..ca3ea19c7 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -204,7 +204,9 @@ app dbStructure conf apiRequest = else return notFound (ActionRead, TargetRoot, Nothing) -> do - body <- (encodeApi . toTableInfo) <$> H.query schema accessibleTables + body <- if contentType == OpenAPI + then (encodeApi . toTableInfo) <$> H.query schema accessibleTables + else encode <$> H.query schema accessibleTables return $ responseLBS status200 [jsonH] $ cs body (ActionInappropriate, _, _) -> return $ responseLBS status405 [] "" diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index 6d8c3a0e1..58d85f236 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -10,7 +10,6 @@ import Data.ByteString.Lazy (ByteString) import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList) import Data.String (IsString (..)) import Data.Text (Text, unpack, pack, concat, intercalate) -import Network.HTTP.Media (MediaType) import qualified Data.Set as Set import Prelude hiding (concat) @@ -22,8 +21,8 @@ import PostgREST.Config (prettyVersion) import PostgREST.QueryBuilder (operators) import PostgREST.Types (Table(..), Column(..)) -makeMimeList :: [MediaType] -makeMimeList = map (fromString . show) [ApplicationJSON, TextCSV] +makeMimeList :: [ContentType] -> MimeList +makeMimeList cs = MimeList $ map (fromString . show) cs toSwaggerType :: Text -> SwaggerType t toSwaggerType "text" = SwaggerString @@ -168,15 +167,15 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t) where tOp = (mempty :: Operation) & tags .~ Set.fromList [tn] - & produces ?~ MimeList makeMimeList + & produces ?~ makeMimeList [ApplicationJSON, TextCSV] & at 200 ?~ "OK" getOp = tOp & parameters .~ map Inline (makeGetParams cs ++ rs) postOp = tOp - & consumes ?~ MimeList makeMimeList + & consumes ?~ makeMimeList [ApplicationJSON, TextCSV] & parameters .~ map Inline (makePostParams tn) patchOp = tOp - & consumes ?~ MimeList makeMimeList + & consumes ?~ makeMimeList [ApplicationJSON, TextCSV] & parameters .~ map Inline (makePostParams tn ++ rs) deletOp = tOp & parameters .~ map Inline (makeDeleteParams ++ rs) @@ -192,7 +191,8 @@ makeRootPathItem = ("/", p) where getOp = (mempty :: Operation) & tags .~ Set.fromList ["/"] - & produces ?~ MimeList [(fromString . show) ApplicationJSON] + & produces ?~ makeMimeList [ApplicationJSON, OpenAPI] + & consumes ?~ makeMimeList [ApplicationJSON, OpenAPI] & at 200 ?~ "OK" pr = (mempty :: PathItem) & get ?~ getOp p = pr diff --git a/stack.yaml b/stack.yaml index 8ba4393b0..318c8ce61 100644 --- a/stack.yaml +++ b/stack.yaml @@ -19,7 +19,6 @@ extra-deps: - unordered-containers-0.2.7.1 - insert-ordered-containers-0.1.0.1 - swagger2-2.1 - - http-media-0.6.3 ghc-options: postgrest: -O2 -Werror -Wall -fwarn-identities diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs index 8abe90e9c..3d58947ac 100644 --- a/test/Feature/StructureSpec.hs +++ b/test/Feature/StructureSpec.hs @@ -13,6 +13,54 @@ import Network.Wai.Test (SResponse(simpleHeaders)) spec :: SpecWith Application spec = do + describe "GET /" $ do + it "lists views in schema" $ + request methodGet "/" [] "" + `shouldRespondWith` [json| [ + {"schema":"test","name":"Escap3e;","insertable":true} + , {"schema":"test","name":"addresses","insertable":true} + , {"schema":"test","name":"articleStars","insertable":true} + , {"schema":"test","name":"articles","insertable":true} + , {"schema":"test","name":"auto_incrementing_pk","insertable":true} + , {"schema":"test","name":"clients","insertable":true} + , {"schema":"test","name":"comments","insertable":true} + , {"schema":"test","name":"complex_items","insertable":true} + , {"schema":"test","name":"compound_pk","insertable":true} + , {"schema":"test","name":"empty_table","insertable":true} + , {"schema":"test","name":"filtered_tasks","insertable":true} + , {"schema":"test","name":"ghostBusters","insertable":true} + , {"schema":"test","name":"has_count_column","insertable":false} + , {"schema":"test","name":"has_fk","insertable":true} + , {"schema":"test","name":"insertable_view_with_join","insertable":true} + , {"schema":"test","name":"insertonly","insertable":true} + , {"schema":"test","name":"items","insertable":true} + , {"schema":"test","name":"json","insertable":true} + , {"schema":"test","name":"materialized_view","insertable":false} + , {"schema":"test","name":"menagerie","insertable":true} + , {"schema":"test","name":"no_pk","insertable":true} + , {"schema":"test","name":"nullable_integer","insertable":true} + , {"schema":"test","name":"orders","insertable":true} + , {"schema":"test","name":"projects","insertable":true} + , {"schema":"test","name":"projects_view","insertable":true} + , {"schema":"test","name":"simple_pk","insertable":true} + , {"schema":"test","name":"tasks","insertable":true} + , {"schema":"test","name":"tsearch","insertable":true} + , {"schema":"test","name":"users","insertable":true} + , {"schema":"test","name":"users_projects","insertable":true} + , {"schema":"test","name":"users_tasks","insertable":true} + , {"schema":"test","name":"withUnique","insertable":true} + ] |] + {matchStatus = 200} + + it "lists only views user has permission to see" $ do + let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" + + request methodGet "/" [auth] "" + `shouldRespondWith` [json| [ + {"schema":"test","name":"authors_only","insertable":true} + ] |] + {matchStatus = 200} + describe "Table info" $ do it "The structure of complex views is correctly detected" $ request methodOptions "/filtered_tasks" [] "" `shouldRespondWith` From 077bb5a4341c6596fa59160830a2934461ada421 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Wed, 15 Jun 2016 12:22:14 +0800 Subject: [PATCH 09/24] Verbose parse error for OpenAPI --- src/PostgREST/ApiRequest.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index c1da04df7..9cf82097e 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -125,7 +125,7 @@ userApiRequest schema req reqBody = Just json -> PayloadJSON json) (CSV.decodeByName reqBody) Right OpenAPI -> - PayloadParseError "Content-type not acceptable" + PayloadParseError "Content-type not acceptable: application/openapi+json; charset=utf-8" -- This is a Left value because form-urlencoded is not a content -- type which we ever use for responses, only something we handle -- just this once for requests From 733b2cc05bb80ed6572e77990bdf0e7b09cf0100 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 08:10:02 +0800 Subject: [PATCH 10/24] Move around encodeFn and realted helpers --- src/PostgREST/App.hs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index ca3ea19c7..e3088b368 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -204,9 +204,11 @@ app dbStructure conf apiRequest = else return notFound (ActionRead, TargetRoot, Nothing) -> do - body <- if contentType == OpenAPI - then (encodeApi . toTableInfo) <$> H.query schema accessibleTables - else encode <$> H.query schema accessibleTables + let encodeApi ti = encodeOpenAPI ti host port + host = configHost conf + port = toInteger $ configPort conf + encodeFn = if contentType == OpenAPI then encodeApi . toTableInfo else encode + body <- encodeFn <$> H.query schema accessibleTables return $ responseLBS status200 [jsonH] $ cs body (ActionInappropriate, _, _) -> return $ responseLBS status405 [] "" @@ -228,9 +230,6 @@ app dbStructure conf apiRequest = pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys in (t, cols, pkeys)) - encodeApi ti = encodeOpenAPI ti host port - host = configHost conf - port = toInteger $ configPort conf notFound = responseLBS status404 [] "" filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk filterCol :: Schema -> TableName -> Column -> Bool From fd0b354cd079ce25dc5485b99a522d15def8ae2f Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 08:11:05 +0800 Subject: [PATCH 11/24] Remove redundant consumes property for get operation --- src/PostgREST/OpenAPI.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index 58d85f236..787376128 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -192,7 +192,6 @@ makeRootPathItem = ("/", p) getOp = (mempty :: Operation) & tags .~ Set.fromList ["/"] & produces ?~ makeMimeList [ApplicationJSON, OpenAPI] - & consumes ?~ makeMimeList [ApplicationJSON, OpenAPI] & at 200 ?~ "OK" pr = (mempty :: PathItem) & get ?~ getOp p = pr From 0be5f5299feede5f2cdae9e804a345ef860ea8d7 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 08:25:34 +0800 Subject: [PATCH 12/24] Reponse with the correct header for openapi --- src/PostgREST/App.hs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index e3088b368..25c6f090f 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -208,8 +208,9 @@ app dbStructure conf apiRequest = host = configHost conf port = toInteger $ configPort conf encodeFn = if contentType == OpenAPI then encodeApi . toTableInfo else encode + header = if contentType == OpenAPI then openapiH else jsonH body <- encodeFn <$> H.query schema accessibleTables - return $ responseLBS status200 [jsonH] $ cs body + return $ responseLBS status200 [header] $ cs body (ActionInappropriate, _, _) -> return $ responseLBS status405 [] "" @@ -286,6 +287,9 @@ contentRangeH frm to total = jsonH :: Header jsonH = (hContentType, "application/json; charset=utf-8") +openapiH :: Header +openapiH = (hContentType, "application/openapi+json; charset=utf-8") + formatRelationError :: Text -> Text formatRelationError = formatGeneralError "could not find foreign keys between these entities" From c10bde8d655609196dfbd76fc8bb9c85797cabf8 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 12:34:14 +0800 Subject: [PATCH 13/24] Add a simple test for openapi --- test/Feature/StructureSpec.hs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs index 3d58947ac..4b5cdd85a 100644 --- a/test/Feature/StructureSpec.hs +++ b/test/Feature/StructureSpec.hs @@ -8,7 +8,7 @@ import SpecHelper import Network.HTTP.Types import Network.Wai (Application) -import Network.Wai.Test (SResponse(simpleHeaders)) +import Network.Wai.Test (SResponse(simpleStatus, simpleHeaders, simpleBody)) spec :: SpecWith Application spec = do @@ -61,6 +61,21 @@ spec = do ] |] {matchStatus = 200} + it "returns a valid swagger spec" $ do + r <- request methodGet "/" [("Accept", "application/openapi+json")] "" + liftIO $ + let respStatus = simpleStatus r in + respStatus `shouldSatisfy` + \s -> s == Status { statusCode = 200, statusMessage="OK" } + liftIO $ + let respHeaders = simpleHeaders r in + respHeaders `shouldSatisfy` + \hs -> ("Content-Type", "application/openapi+json; charset=utf-8") `elem` hs + liftIO $ + let respBody = simpleBody r in + respBody `shouldSatisfy` + \b -> b == b + describe "Table info" $ do it "The structure of complex views is correctly detected" $ request methodOptions "/filtered_tasks" [] "" `shouldRespondWith` From e88a0577e5297d049d76ea841a36c8a732e0b7bb Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 12:41:27 +0800 Subject: [PATCH 14/24] Avoid repeating mime types --- src/PostgREST/ApiRequest.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index 9cf82097e..5002454fe 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -124,8 +124,8 @@ userApiRequest schema req reqBody = Nothing -> PayloadParseError "All lines must have same number of fields" Just json -> PayloadJSON json) (CSV.decodeByName reqBody) - Right OpenAPI -> - PayloadParseError "Content-type not acceptable: application/openapi+json; charset=utf-8" + Right oa@OpenAPI -> + PayloadParseError $ "Content-type not acceptable: " <> cs (show oa) -- This is a Left value because form-urlencoded is not a content -- type which we ever use for responses, only something we handle -- just this once for requests From a7316aff014d67b8f640ce1ef878bf2025aa96da Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 15:43:52 +0800 Subject: [PATCH 15/24] Add a changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b566bd72..b1b3333ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Added +- Ability to generate an OpenAPI spec - @hudayou, @ruslantalpa, @begriffs ### Fixed - Do not apply limit to parent items - @ruslantalpa From 816d577f53dcb08b5c43ad00f6e5cae5bd2f10b3 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 15:52:13 +0800 Subject: [PATCH 16/24] Add undocumented "return=none" preference --- src/PostgREST/OpenAPI.hs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index 787376128..563405973 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -148,9 +148,13 @@ makeGetParams cs = , makePreferParam ["plurality=singular", "count=none"] ] +makeReturnPreferenceParam :: Param +makeReturnPreferenceParam = + makePreferParam ["return=representation", "return=minimal", "return=none"] + makePostParams :: Text -> [Param] makePostParams tn = - [ makePreferParam ["return=representation", "return=minimal"] + [ makeReturnPreferenceParam , (mempty :: Param) & name .~ "body" & description ?~ tn @@ -160,7 +164,7 @@ makePostParams tn = makeDeleteParams :: [Param] makeDeleteParams = - [ makePreferParam ["return=representation", "return=minimal"] ] + [ makeReturnPreferenceParam ] makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem) makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t) From d3d1fbe7e5c391c7c0d5f9b94c7064ff8ed3b583 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 16:40:16 +0800 Subject: [PATCH 17/24] Return HTTP 415 on non root path for openapi req --- src/PostgREST/Middleware.hs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/PostgREST/Middleware.hs b/src/PostgREST/Middleware.hs index 969b20c09..6682fdb35 100644 --- a/src/PostgREST/Middleware.hs +++ b/src/PostgREST/Middleware.hs @@ -6,6 +6,7 @@ module PostgREST.Middleware where import Data.Aeson (Value (..)) import qualified Data.HashMap.Strict as M import Data.String.Conversions (cs) +import Data.Maybe (fromMaybe, listToMaybe) import Data.Text import qualified Hasql.Transaction as H @@ -17,7 +18,7 @@ import Network.Wai.Middleware.Cors (cors) import Network.Wai.Middleware.Gzip (def, gzip) import Network.Wai.Middleware.Static (only, staticPolicy) -import PostgREST.ApiRequest (ApiRequest(..), pickContentType) +import PostgREST.ApiRequest (ApiRequest(..), ContentType(..), pickContentType) import PostgREST.Auth (claimsToSQL) import PostgREST.Config (AppConfig (..), corsPolicy) import PostgREST.Error (errResponse) @@ -40,10 +41,14 @@ runWithClaims conf eClaims app req = unsupportedAccept :: Application -> Application unsupportedAccept app req respond = - case accept of - Left _ -> respond $ errResponse status415 "Unsupported Accept header, try: application/json" - Right _ -> app req respond + case (isTargetRoot, accept) of + (_, Left _) -> unsupportedAcceptRespond + (False, Right OpenAPI) -> unsupportedAcceptRespond + (_, Right _) -> app req respond where accept = pickContentType $ lookup hAccept $ requestHeaders req + path = pathInfo req + isTargetRoot = fromMaybe True $ (== "") <$> listToMaybe path + unsupportedAcceptRespond = respond $ errResponse status415 "Unsupported Accept header, try: application/json" defaultMiddle :: Application -> Application defaultMiddle = From 11aeb4fbda8e71a6cbc1eb6ac3f758fb71d63868 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Thu, 16 Jun 2016 17:54:24 +0800 Subject: [PATCH 18/24] Add all supported successful responses --- src/PostgREST/OpenAPI.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index 563405973..c39a28b81 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -175,12 +175,15 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t) & at 200 ?~ "OK" getOp = tOp & parameters .~ map Inline (makeGetParams cs ++ rs) + & at 206 ?~ "Partial Content" postOp = tOp & consumes ?~ makeMimeList [ApplicationJSON, TextCSV] & parameters .~ map Inline (makePostParams tn) + & at 201 ?~ "Created" patchOp = tOp & consumes ?~ makeMimeList [ApplicationJSON, TextCSV] & parameters .~ map Inline (makePostParams tn ++ rs) + & at 204 ?~ "No Content" deletOp = tOp & parameters .~ map Inline (makeDeleteParams ++ rs) pr = (mempty :: PathItem) & get ?~ getOp From 0220040341df34af2dbfca2351440a52af41840e Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Fri, 17 Jun 2016 10:17:10 +0800 Subject: [PATCH 19/24] Add test to validate openapi spec --- postgrest.cabal | 2 + stack.yaml | 6 + test/Feature/StructureSpec.hs | 19 +- test/fixtures/draft04.json | 151 ++++ test/fixtures/openapi.json | 1607 +++++++++++++++++++++++++++++++++ 5 files changed, 1781 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/draft04.json create mode 100644 test/fixtures/openapi.json diff --git a/postgrest.cabal b/postgrest.cabal index 0735657a0..011ad2f1b 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -192,6 +192,8 @@ Test-Suite spec , wai-middleware-static , warp , insert-ordered-containers + , hjsonpointer + , hjsonschema , swagger2 , HTTP , Ranged-sets diff --git a/stack.yaml b/stack.yaml index 318c8ce61..e699f7080 100644 --- a/stack.yaml +++ b/stack.yaml @@ -19,8 +19,14 @@ extra-deps: - unordered-containers-0.2.7.1 - insert-ordered-containers-0.1.0.1 - swagger2-2.1 + # - hjsonschema-0.10.0.2 + - hjsonpointer-0.3.0.1 ghc-options: postgrest: -O2 -Werror -Wall -fwarn-identities packages: - . +- location: + git: https://github.com/hudayou/hjsonschema.git + commit: 2dccf1b738920e4b1bea50804d2f1d546ab1b210 + extra-dep: true diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs index 4b5cdd85a..1a81d6a2a 100644 --- a/test/Feature/StructureSpec.hs +++ b/test/Feature/StructureSpec.hs @@ -10,6 +10,10 @@ import Network.HTTP.Types import Network.Wai (Application) import Network.Wai.Test (SResponse(simpleStatus, simpleHeaders, simpleBody)) +import Data.Maybe (fromJust) +import Data.Aeson (decode) +import qualified Data.JsonSchema.Draft4 as D4 + spec :: SpecWith Application spec = do @@ -61,7 +65,7 @@ spec = do ] |] {matchStatus = 200} - it "returns a valid swagger spec" $ do + it "returns a valid openapi spec" $ do r <- request methodGet "/" [("Accept", "application/openapi+json")] "" liftIO $ let respStatus = simpleStatus r in @@ -72,9 +76,16 @@ spec = do respHeaders `shouldSatisfy` \hs -> ("Content-Type", "application/openapi+json; charset=utf-8") `elem` hs liftIO $ - let respBody = simpleBody r in - respBody `shouldSatisfy` - \b -> b == b + let respBody = simpleBody r + schema :: D4.Schema + schema = D4.emptySchema { D4._schemaRef = Just "openapi.json" } + schemaContext :: D4.SchemaWithURI D4.Schema + schemaContext = D4.SchemaWithURI + { D4._swSchema = schema + , D4._swURI = Just "test/fixtures/openapi.json" + } + in + D4.fetchFilesystemAndValidate schemaContext ((fromJust . decode) respBody) `shouldReturn` Right () describe "Table info" $ do it "The structure of complex views is correctly detected" $ diff --git a/test/fixtures/draft04.json b/test/fixtures/draft04.json new file mode 100644 index 000000000..fabd35473 --- /dev/null +++ b/test/fixtures/draft04.json @@ -0,0 +1,151 @@ +{ + "id": "draft04.json", + "$schema": "draft04.json", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/test/fixtures/openapi.json b/test/fixtures/openapi.json new file mode 100644 index 000000000..376db12e5 --- /dev/null +++ b/test/fixtures/openapi.json @@ -0,0 +1,1607 @@ +{ + "title": "A JSON Schema for Swagger 2.0 API.", + "id": "openapi.json", + "$schema": "draft04.json", + "type": "object", + "required": [ + "swagger", + "info", + "paths" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "swagger": { + "type": "string", + "enum": [ + "2.0" + ], + "description": "The Swagger version of this document." + }, + "info": { + "$ref": "#/definitions/info" + }, + "host": { + "type": "string", + "pattern": "^[^{}/ :\\\\]+(?::\\d+)?$", + "description": "The host (name or ip) of the API. Example: 'swagger.io'" + }, + "basePath": { + "type": "string", + "pattern": "^/", + "description": "The base path to the API. Example: '/api'." + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "consumes": { + "description": "A list of MIME types accepted by the API.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "paths": { + "$ref": "#/definitions/paths" + }, + "definitions": { + "$ref": "#/definitions/definitions" + }, + "parameters": { + "$ref": "#/definitions/parameterDefinitions" + }, + "responses": { + "$ref": "#/definitions/responseDefinitions" + }, + "security": { + "$ref": "#/definitions/security" + }, + "securityDefinitions": { + "$ref": "#/definitions/securityDefinitions" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "definitions": { + "info": { + "type": "object", + "description": "General information about the API.", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." + }, + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed." + }, + "termsOfService": { + "type": "string", + "description": "The terms of service for the API." + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "license": { + "$ref": "#/definitions/license" + } + } + }, + "contact": { + "type": "object", + "description": "Contact information for the owners of the API.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The identifying name of the contact person/organization." + }, + "url": { + "type": "string", + "description": "The URL pointing to the contact information.", + "format": "uri" + }, + "email": { + "type": "string", + "description": "The email address of the contact person/organization.", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "license": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the license type. It's encouraged to use an OSI compatible license." + }, + "url": { + "type": "string", + "description": "The URL pointing to the license.", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "paths": { + "type": "object", + "description": "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + }, + "^/": { + "$ref": "#/definitions/pathItem" + } + }, + "additionalProperties": false + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "description": "One or more JSON objects describing the schemas being consumed and produced by the API." + }, + "parameterDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + }, + "description": "One or more JSON representations for parameters" + }, + "responseDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/response" + }, + "description": "One or more JSON representations for parameters" + }, + "externalDocs": { + "type": "object", + "additionalProperties": false, + "description": "information about external documentation", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "examples": { + "type": "object", + "additionalProperties": true + }, + "mimeType": { + "type": "string", + "description": "The MIME type of the HTTP message." + }, + "operation": { + "type": "object", + "required": [ + "responses" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the operation." + }, + "description": { + "type": "string", + "description": "A longer description of the operation, GitHub Flavored Markdown is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string", + "description": "A unique identifier of the operation." + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "consumes": { + "description": "A list of MIME types the API can consume.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "parameters": { + "$ref": "#/definitions/parametersList" + }, + "responses": { + "$ref": "#/definitions/responses" + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "$ref": "#/definitions/security" + } + } + }, + "pathItem": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "get": { + "$ref": "#/definitions/operation" + }, + "put": { + "$ref": "#/definitions/operation" + }, + "post": { + "$ref": "#/definitions/operation" + }, + "delete": { + "$ref": "#/definitions/operation" + }, + "options": { + "$ref": "#/definitions/operation" + }, + "head": { + "$ref": "#/definitions/operation" + }, + "patch": { + "$ref": "#/definitions/operation" + }, + "parameters": { + "$ref": "#/definitions/parametersList" + } + } + }, + "responses": { + "type": "object", + "description": "Response objects names can either be any valid HTTP status code or 'default'.", + "minProperties": 1, + "additionalProperties": false, + "patternProperties": { + "^([0-9]{3})$|^(default)$": { + "$ref": "#/definitions/responseValue" + }, + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "not": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + } + }, + "responseValue": { + "oneOf": [ + { + "$ref": "#/definitions/response" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "$ref": "#/definitions/fileSchema" + } + ] + }, + "headers": { + "$ref": "#/definitions/headers" + }, + "examples": { + "$ref": "#/definitions/examples" + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/header" + } + }, + "header": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "vendorExtension": { + "description": "Any property starting with x- is valid.", + "additionalProperties": true, + "additionalItems": true + }, + "bodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "schema" + ], + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "body" + ] + }, + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "schema": { + "$ref": "#/definitions/schema" + } + }, + "additionalProperties": false + }, + "headerParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "header" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "queryParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "query" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "formDataParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "formData" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array", + "file" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "pathParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "required" + ], + "properties": { + "required": { + "type": "boolean", + "enum": [ + true + ], + "description": "Determines whether or not this parameter is required or optional." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "path" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "nonBodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "type" + ], + "oneOf": [ + { + "$ref": "#/definitions/headerParameterSubSchema" + }, + { + "$ref": "#/definitions/formDataParameterSubSchema" + }, + { + "$ref": "#/definitions/queryParameterSubSchema" + }, + { + "$ref": "#/definitions/pathParameterSubSchema" + } + ] + }, + "parameter": { + "oneOf": [ + { + "$ref": "#/definitions/bodyParameter" + }, + { + "$ref": "#/definitions/nonBodyParameter" + } + ] + }, + "schema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "format": { + "type": "string" + }, + "title": { + "$ref": "draft04.json#/properties/title" + }, + "description": { + "$ref": "draft04.json#/properties/description" + }, + "default": { + "$ref": "draft04.json#/properties/default" + }, + "multipleOf": { + "$ref": "draft04.json#/properties/multipleOf" + }, + "maximum": { + "$ref": "draft04.json#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "draft04.json#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "draft04.json#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "draft04.json#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "draft04.json#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "draft04.json#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "draft04.json#/properties/pattern" + }, + "maxItems": { + "$ref": "draft04.json#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "draft04.json#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "draft04.json#/properties/uniqueItems" + }, + "maxProperties": { + "$ref": "draft04.json#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "draft04.json#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "draft04.json#/definitions/stringArray" + }, + "enum": { + "$ref": "draft04.json#/properties/enum" + }, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "boolean" + } + ], + "default": {} + }, + "type": { + "$ref": "draft04.json#/properties/type" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + } + ], + "default": {} + }, + "allOf": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "discriminator": { + "type": "string" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/xml" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "fileSchema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "type" + ], + "properties": { + "format": { + "type": "string" + }, + "title": { + "$ref": "draft04.json#/properties/title" + }, + "description": { + "$ref": "draft04.json#/properties/description" + }, + "default": { + "$ref": "draft04.json#/properties/default" + }, + "required": { + "$ref": "draft04.json#/definitions/stringArray" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "primitivesItems": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/securityRequirement" + }, + "uniqueItems": true + }, + "securityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "xml": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "securityDefinitions": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/basicAuthenticationSecurity" + }, + { + "$ref": "#/definitions/apiKeySecurity" + }, + { + "$ref": "#/definitions/oauth2ImplicitSecurity" + }, + { + "$ref": "#/definitions/oauth2PasswordSecurity" + }, + { + "$ref": "#/definitions/oauth2ApplicationSecurity" + }, + { + "$ref": "#/definitions/oauth2AccessCodeSecurity" + } + ] + } + }, + "basicAuthenticationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "basic" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "apiKeySecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ImplicitSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "implicit" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2PasswordSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "password" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ApplicationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "application" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2AccessCodeSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "accessCode" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2Scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mediaTypeList": { + "type": "array", + "items": { + "$ref": "#/definitions/mimeType" + }, + "uniqueItems": true + }, + "parametersList": { + "type": "array", + "description": "The parameters needed to send a valid API call.", + "additionalItems": false, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/parameter" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "uniqueItems": true + }, + "schemesList": { + "type": "array", + "description": "The transfer protocol of the API.", + "items": { + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss" + ] + }, + "uniqueItems": true + }, + "collectionFormat": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes" + ], + "default": "csv" + }, + "collectionFormatWithMulti": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes", + "multi" + ], + "default": "csv" + }, + "title": { + "$ref": "draft04.json#/properties/title" + }, + "description": { + "$ref": "draft04.json#/properties/description" + }, + "default": { + "$ref": "draft04.json#/properties/default" + }, + "multipleOf": { + "$ref": "draft04.json#/properties/multipleOf" + }, + "maximum": { + "$ref": "draft04.json#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "draft04.json#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "draft04.json#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "draft04.json#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "draft04.json#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "draft04.json#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "draft04.json#/properties/pattern" + }, + "maxItems": { + "$ref": "draft04.json#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "draft04.json#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "draft04.json#/properties/uniqueItems" + }, + "enum": { + "$ref": "draft04.json#/properties/enum" + }, + "jsonReference": { + "type": "object", + "required": [ + "$ref" + ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string" + } + } + } + } +} From a9ffde4d5faca51a75361fb4620a9493cb954f4f Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Fri, 17 Jun 2016 10:18:28 +0800 Subject: [PATCH 20/24] Fix the invalid order parameter bug found by test --- src/PostgREST/OpenAPI.hs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index c39a28b81..4a03b4c22 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -127,16 +127,25 @@ makePreferParam ts = & type_ .~ SwaggerString & enum_ .~ decode (encode ts)) -makeGetParams :: [Column] -> [Param] -makeGetParams cs = - makeRangeParams ++ - [ (mempty :: Param) +makeSelectParam :: Param +makeSelectParam = + (mempty :: Param) & name .~ "select" & description ?~ "Filtering Columns" & required ?~ False & schema .~ ParamOther ((mempty :: ParamOtherSchema) & in_ .~ ParamQuery & type_ .~ SwaggerString) + +makeGetParams :: [Column] -> [Param] +makeGetParams [] = + makeRangeParams ++ + [ makeSelectParam + , makePreferParam ["plurality=singular", "count=none"] + ] +makeGetParams cs = + makeRangeParams ++ + [ makeSelectParam , (mempty :: Param) & name .~ "order" & description ?~ "Ordering" From 1a25bb501f8afefd48f155f0c1ccb3078f129889 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Fri, 17 Jun 2016 10:27:46 +0800 Subject: [PATCH 21/24] Add 415 test for openapi req on none root path --- test/Feature/StructureSpec.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/Feature/StructureSpec.hs b/test/Feature/StructureSpec.hs index 1a81d6a2a..3d0b18608 100644 --- a/test/Feature/StructureSpec.hs +++ b/test/Feature/StructureSpec.hs @@ -87,6 +87,11 @@ spec = do in D4.fetchFilesystemAndValidate schemaContext ((fromJust . decode) respBody) `shouldReturn` Right () + it "should respond to openapi request on none root path with 415" $ + request methodGet "/none_root_path" + (acceptHdrs "application/openapi+json") "" + `shouldRespondWith` 415 + describe "Table info" $ do it "The structure of complex views is correctly detected" $ request methodOptions "/filtered_tasks" [] "" `shouldRespondWith` From 86e81c135e42e11daa15730901aa9dfb6edd14b4 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Fri, 17 Jun 2016 10:31:39 +0800 Subject: [PATCH 22/24] Use hjsonschema from the develop branch --- stack.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stack.yaml b/stack.yaml index e699f7080..6419ec45f 100644 --- a/stack.yaml +++ b/stack.yaml @@ -27,6 +27,6 @@ ghc-options: packages: - . - location: - git: https://github.com/hudayou/hjsonschema.git - commit: 2dccf1b738920e4b1bea50804d2f1d546ab1b210 + git: https://github.com/seagreen/hjsonschema + commit: 075da33626d9d5cf20645a998b86518222d6b2d6 extra-dep: true From c1764c497607e655f5a53f0e678f648b9f14ae41 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Fri, 17 Jun 2016 10:32:56 +0800 Subject: [PATCH 23/24] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b3333ad..c4faca171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Added -- Ability to generate an OpenAPI spec - @hudayou, @ruslantalpa, @begriffs +- Ability to generate an OpenAPI spec - @mainx07, @hudayou, @ruslantalpa, @begriffs ### Fixed - Do not apply limit to parent items - @ruslantalpa From e571cb9fcb53d361e03c48d916a4f3e4d473b545 Mon Sep 17 00:00:00 2001 From: Jacky Hu Date: Sat, 18 Jun 2016 10:00:44 +0800 Subject: [PATCH 24/24] Ability to set addresses to listen on Fixes #461 --- CHANGELOG.md | 1 + main/Main.hs | 7 +++++-- src/PostgREST/Config.hs | 2 +- src/PostgREST/OpenAPI.hs | 10 +++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4faca171..0bdb00360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,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 ### Fixed - Do not apply limit to parent items - @ruslantalpa diff --git a/main/Main.hs b/main/Main.hs index f0fe92de1..9bac6b36e 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -13,6 +13,7 @@ import PostgREST.DbStructure import Control.Monad import Data.Monoid ((<>)) import Data.String.Conversions (cs) +import Data.String (IsString (..)) import qualified Hasql.Query as H import qualified Hasql.Session as H import qualified Hasql.Decoders as HD @@ -47,9 +48,11 @@ main = do hSetBuffering stderr NoBuffering conf <- readOptions - let port = configPort conf + let host = configHost conf + port = configPort conf pgSettings = cs (configDatabase conf) - appSettings = setPort port + appSettings = setHost (fromString host) + . setPort port . setServerName (cs $ "postgrest/" <> prettyVersion) $ defaultSettings diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index d8d840c41..3835b342a 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -52,7 +52,7 @@ argParser = AppConfig <$> argument str (help "(REQUIRED) database connection string, e.g. postgres://user:pass@host:port/db" <> metavar "DB_URL") <*> strOption (long "anonymous" <> short 'a' <> help "(REQUIRED) postgres role to use for non-authenticated requests" <> metavar "ROLE") <*> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault) - <*> strOption (long "hostname" <> short 'n' <> help "hostname on which the HTTP server is running" <> metavar "HOSTNAME" <> value "localhost" <> showDefault) + <*> strOption (long "host" <> short 'l' <> help "hostname or ip on which to run HTTP server" <> metavar "HOST" <> value "*4" <> showDefault) <*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault) <*> (secret . cs <$> strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault)) diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index 4a03b4c22..dcea58909 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -215,6 +215,14 @@ makeRootPathItem = ("/", p) makePathItems :: [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem makePathItems ti = fromList $ makeRootPathItem : map makePathItem ti +escapeHostName :: String -> String +escapeHostName "*" = "0.0.0.0" +escapeHostName "*4" = "0.0.0.0" +escapeHostName "!4" = "0.0.0.0" +escapeHostName "*6" = "0.0.0.0" +escapeHostName "!6" = "0.0.0.0" +escapeHostName h = h + postgrestSpec:: [(Table, [Column], [Text])] -> String -> Integer -> Swagger postgrestSpec ti h p = (mempty :: Swagger) & basePath ?~ "/" @@ -227,7 +235,7 @@ postgrestSpec ti h p = (mempty :: Swagger) & definitions .~ makeDefinitions ti & paths .~ makePathItems ti where - h' = Just $ Host h (Just (fromInteger p)) + h' = Just $ Host (escapeHostName h) (Just (fromInteger p)) encodeOpenAPI :: [(Table, [Column], [Text])] -> String -> Integer -> ByteString encodeOpenAPI ti h p = encode $ postgrestSpec ti h p