[server] add openapi support

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/1935
Co-authored-by: paritosh-08 <85472423+paritosh-08@users.noreply.github.com>
Co-authored-by: pranshi06 <85474619+pranshi06@users.noreply.github.com>
Co-authored-by: Lyndon Maydwell <92299+sordina@users.noreply.github.com>
GitOrigin-RevId: 3e43b84d4e9e181b405855704112b49467dafdf9
This commit is contained in:
Puru Gupta 2021-10-06 12:45:14 +05:30 committed by hasura-bot
parent 49c07c79e5
commit 8b60122b9e
18 changed files with 970 additions and 26 deletions

View File

@ -2,6 +2,7 @@
## Next release ## Next release
(Add entries below in the order of server, console, cli, docs, others) (Add entries below in the order of server, console, cli, docs, others)
- server: add support for openapi json of REST Endpoints
- server: enable inherited roles by default in the graphql-engine - server: enable inherited roles by default in the graphql-engine
- server: support MSSQL insert mutations - server: support MSSQL insert mutations

View File

@ -1,6 +1,5 @@
active-repositories: hackage.haskell.org:merge active-repositories: hackage.haskell.org:merge
constraints: any.Cabal ==3.2.0.0, constraints: any.Cabal ==3.2.0.0,
any.Glob ==0.10.1,
any.HTTP ==4000.3.15, any.HTTP ==4000.3.15,
HTTP -conduit10 -mtl1 +network-uri -warn-as-error -warp-tests, HTTP -conduit10 -mtl1 +network-uri -warn-as-error -warp-tests,
any.HUnit ==1.6.2.0, any.HUnit ==1.6.2.0,
@ -12,13 +11,12 @@ constraints: any.Cabal ==3.2.0.0,
SHA -exe, SHA -exe,
any.Spock-core ==0.14.0.0, any.Spock-core ==0.14.0.0,
any.StateVar ==1.2.1, any.StateVar ==1.2.1,
any.abstract-deque ==0.3,
abstract-deque -usecas,
any.abstract-par ==0.3.3,
any.adjunctions ==4.4, any.adjunctions ==4.4,
any.aeson ==1.5.5.1, any.aeson ==1.5.5.1,
aeson -bytestring-builder -cffi -developer -fast, aeson -bytestring-builder -cffi -developer -fast,
any.aeson-casing ==0.2.0.0, any.aeson-casing ==0.2.0.0,
any.aeson-pretty ==0.8.8,
aeson-pretty -lib-only,
any.ansi-terminal ==0.11, any.ansi-terminal ==0.11,
ansi-terminal -example, ansi-terminal -example,
any.ansi-wl-pprint ==0.6.9, any.ansi-wl-pprint ==0.6.9,
@ -50,7 +48,6 @@ constraints: any.Cabal ==3.2.0.0,
any.bifunctors ==5.5.10, any.bifunctors ==5.5.10,
bifunctors +semigroups +tagged, bifunctors +semigroups +tagged,
any.binary ==0.8.8.0, any.binary ==0.8.8.0,
any.binary-orphans ==1.0.1,
any.binary-parser ==0.5.6, any.binary-parser ==0.5.6,
any.blaze-builder ==0.4.2.1, any.blaze-builder ==0.4.2.1,
any.blaze-html ==0.9.1.2, any.blaze-html ==0.9.1.2,
@ -67,8 +64,6 @@ constraints: any.Cabal ==3.2.0.0,
any.cabal-doctest ==1.0.8, any.cabal-doctest ==1.0.8,
any.call-stack ==0.3.0, any.call-stack ==0.3.0,
any.case-insensitive ==1.2.1.0, any.case-insensitive ==1.2.1.0,
any.cassava ==0.5.2.0,
cassava -bytestring--lt-0_10_4,
any.cereal ==0.5.8.1, any.cereal ==0.5.8.1,
cereal -bytestring-builder, cereal -bytestring-builder,
any.charset ==0.3.7.1, any.charset ==0.3.7.1,
@ -77,7 +72,6 @@ constraints: any.Cabal ==3.2.0.0,
clock -llvm, clock -llvm,
any.cmdargs ==0.10.20, any.cmdargs ==0.10.20,
cmdargs +quotation -testprog, cmdargs +quotation -testprog,
any.code-page ==0.2.1,
any.colour ==2.3.5, any.colour ==2.3.5,
any.comonad ==5.0.8, any.comonad ==5.0.8,
comonad +containers +distributive +indexed-traversable, comonad +containers +distributive +indexed-traversable,
@ -93,10 +87,6 @@ constraints: any.Cabal ==3.2.0.0,
any.contravariant ==1.5.3, any.contravariant ==1.5.3,
contravariant +semigroups +statevar +tagged, contravariant +semigroups +statevar +tagged,
any.cookie ==0.4.5, any.cookie ==0.4.5,
any.criterion ==1.5.9.0,
criterion -embed-data-files -fast,
any.criterion-measurement ==0.1.2.0,
criterion-measurement -fast,
any.cron ==0.7.0, any.cron ==0.7.0,
cron -lib-werror, cron -lib-werror,
any.crypto-api ==0.13.3, any.crypto-api ==0.13.3,
@ -121,7 +111,6 @@ constraints: any.Cabal ==3.2.0.0,
any.data-textual ==0.3.0.3, any.data-textual ==0.3.0.3,
any.deepseq ==1.4.4.0, any.deepseq ==1.4.4.0,
any.deferred-folds ==0.9.15, any.deferred-folds ==0.9.15,
any.dense-linear-algebra ==0.1.0.0,
any.dependent-map ==0.4.0.0, any.dependent-map ==0.4.0.0,
any.dependent-sum ==0.7.1.0, any.dependent-sum ==0.7.1.0,
any.directory ==1.3.6.0, any.directory ==1.3.6.0,
@ -152,6 +141,7 @@ constraints: any.Cabal ==3.2.0.0,
any.foldl ==1.4.10, any.foldl ==1.4.10,
any.formatting ==7.1.1, any.formatting ==7.1.1,
any.free ==5.1.6, any.free ==5.1.6,
any.generics-sop ==0.5.1.1,
any.ghc ==8.10.2, any.ghc ==8.10.2,
any.ghc-boot ==8.10.2, any.ghc-boot ==8.10.2,
any.ghc-boot-th ==8.10.2, any.ghc-boot-th ==8.10.2,
@ -192,6 +182,7 @@ constraints: any.Cabal ==3.2.0.0,
any.http-conduit ==2.3.7.4, any.http-conduit ==2.3.7.4,
http-conduit +aeson, http-conduit +aeson,
any.http-date ==0.0.10, any.http-date ==0.0.10,
any.http-media ==0.8.0.0,
any.http-types ==0.12.3, any.http-types ==0.12.3,
any.http2 ==2.0.5, any.http2 ==2.0.5,
http2 -devel, http2 -devel,
@ -201,6 +192,7 @@ constraints: any.Cabal ==3.2.0.0,
any.indexed-traversable ==0.1.1, any.indexed-traversable ==0.1.1,
any.insert-ordered-containers ==0.2.3.1, any.insert-ordered-containers ==0.2.3.1,
any.inspection-testing ==0.4.5.0, any.inspection-testing ==0.4.5.0,
inspection-testing -more-tests -old-text-tests,
any.integer-gmp ==1.0.3.0, any.integer-gmp ==1.0.3.0,
any.integer-logarithms ==1.0.3.1, any.integer-logarithms ==1.0.3.1,
integer-logarithms -check-bounds +integer-gmp, integer-logarithms -check-bounds +integer-gmp,
@ -211,7 +203,6 @@ constraints: any.Cabal ==3.2.0.0,
any.iproute ==1.7.10, any.iproute ==1.7.10,
any.jose ==0.8.4, any.jose ==0.8.4,
jose -demos, jose -demos,
any.js-chart ==2.9.4.1,
any.kan-extensions ==5.2.1, any.kan-extensions ==5.2.1,
any.keys ==3.12.3, any.keys ==3.12.3,
any.kriti-lang ==0.2.0.0, any.kriti-lang ==0.2.0.0,
@ -232,15 +223,11 @@ constraints: any.Cabal ==3.2.0.0,
math-functions +system-erf +system-expm1, math-functions +system-erf +system-expm1,
any.memory ==0.15.0, any.memory ==0.15.0,
memory +support_basement +support_bytestring +support_deepseq +support_foundation, memory +support_basement +support_bytestring +support_deepseq +support_foundation,
any.microstache ==1.0.1.2,
any.mime-types ==0.1.0.9, any.mime-types ==0.1.0.9,
any.mmorph ==1.1.4, any.mmorph ==1.1.4,
any.monad-control ==1.0.2.3, any.monad-control ==1.0.2.3,
any.monad-loops ==0.4.3, any.monad-loops ==0.4.3,
monad-loops +base4, monad-loops +base4,
any.monad-par ==0.3.5,
monad-par -chaselev -newgeneric,
any.monad-par-extras ==0.3.3,
any.monad-time ==0.3.1.0, any.monad-time ==0.3.1.0,
any.monad-validate ==1.2.0.0, any.monad-validate ==1.2.0.0,
any.mono-traversable ==1.0.15.1, any.mono-traversable ==1.0.15.1,
@ -248,7 +235,6 @@ constraints: any.Cabal ==3.2.0.0,
any.mtl-compat ==0.2.2, any.mtl-compat ==0.2.2,
mtl-compat -two-point-one -two-point-two, mtl-compat -two-point-one -two-point-two,
any.mustache ==2.3.1, any.mustache ==2.3.1,
any.mwc-probability ==2.3.1,
any.mwc-random ==0.14.0.0, any.mwc-random ==0.14.0.0,
any.raw-strings-qq ==1.1, any.raw-strings-qq ==1.1,
any.mysql ==0.2.0.1, any.mysql ==0.2.0.1,
@ -265,8 +251,10 @@ constraints: any.Cabal ==3.2.0.0,
any.odbc ==0.2.3, any.odbc ==0.2.3,
any.old-locale ==1.0.0.7, any.old-locale ==1.0.0.7,
any.old-time ==1.1.0.3, any.old-time ==1.1.0.3,
any.openapi3 ==3.1.0,
any.optics-core ==0.3.0.1, any.optics-core ==0.3.0.1,
any.optics-extra ==0.3, any.optics-extra ==0.3,
any.optics-th ==0.3.0.2,
any.optparse-applicative ==0.16.1.0, any.optparse-applicative ==0.16.1.0,
optparse-applicative +process, optparse-applicative +process,
any.optparse-generic ==1.4.4, any.optparse-generic ==1.4.4,
@ -286,11 +274,12 @@ constraints: any.Cabal ==3.2.0.0,
any.postgresql-libpq ==0.9.4.3, any.postgresql-libpq ==0.9.4.3,
postgresql-libpq -use-pkg-config, postgresql-libpq -use-pkg-config,
any.pretty ==1.1.3.6, any.pretty ==1.1.3.6,
any.pretty-simple ==4.0.0.0,
any.pretty-show ==1.10, any.pretty-show ==1.10,
any.pretty-simple ==4.0.0.0,
pretty-simple -buildexample -buildexe,
any.prettyprinter ==1.7.0, any.prettyprinter ==1.7.0,
prettyprinter -buildreadme, prettyprinter -buildreadme,
prettyprinter-ansi-terminal ==1.1.2, any.prettyprinter-ansi-terminal ==1.1.2,
any.primitive ==0.7.1.0, any.primitive ==0.7.1.0,
any.primitive-extras ==0.8, any.primitive-extras ==0.8,
any.primitive-unlifted ==0.1.3.0, any.primitive-unlifted ==0.1.3.0,
@ -341,10 +330,10 @@ constraints: any.Cabal ==3.2.0.0,
any.socks ==0.6.1, any.socks ==0.6.1,
any.some ==1.0.1, any.some ==1.0.1,
some +newtype-unsafe, some +newtype-unsafe,
any.sop-core ==0.5.0.1,
any.split ==0.2.3.4, any.split ==0.2.3.4,
any.splitmix ==0.1.0.3, any.splitmix ==0.1.0.3,
splitmix -optimised-mixer, splitmix -optimised-mixer,
any.statistics ==0.15.2.0,
any.stm ==2.5.0.0, any.stm ==2.5.0.0,
any.stm-containers ==1.2, any.stm-containers ==1.2,
any.stm-hamt ==1.2.0.4, any.stm-hamt ==1.2.0.4,
@ -365,8 +354,8 @@ constraints: any.Cabal ==3.2.0.0,
any.tasty-smallcheck ==0.8.2, any.tasty-smallcheck ==0.8.2,
any.template-haskell ==2.16.0.0, any.template-haskell ==2.16.0.0,
any.temporary ==1.3, any.temporary ==1.3,
any.terminfo ==0.4.1.4,
any.terminal-size ==0.3.2.1, any.terminal-size ==0.3.2.1,
any.terminfo ==0.4.1.4,
any.text ==1.2.3.2, any.text ==1.2.3.2,
any.text-builder ==0.6.6.1, any.text-builder ==0.6.6.1,
any.text-conversions ==0.3.1, any.text-conversions ==0.3.1,
@ -418,10 +407,8 @@ constraints: any.Cabal ==3.2.0.0,
vector +boundschecks -internalchecks -unsafechecks -wall, vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-algorithms ==0.8.0.4, any.vector-algorithms ==0.8.0.4,
vector-algorithms +bench +boundschecks -internalchecks -llvm +properties -unsafechecks, vector-algorithms +bench +boundschecks -internalchecks -llvm +properties -unsafechecks,
any.vector-binary-instances ==0.2.5.1,
any.vector-instances ==3.4, any.vector-instances ==3.4,
vector-instances +hashable, vector-instances +hashable,
any.vector-th-unbox ==0.2.1.7,
any.void ==0.7.3, any.void ==0.7.3,
void -safe, void -safe,
any.wai ==3.2.3, any.wai ==3.2.3,

View File

@ -99,3 +99,60 @@ Response
The response is determined by the saved query. The response will be the same as if you had made the query directly in the GraphQL console. The response is determined by the saved query. The response will be the same as if you had made the query directly in the GraphQL console.
See the :ref:`api_reference_graphql` for more details. See the :ref:`api_reference_graphql` for more details.
OpenAPI 3 Specification
---------------------------
The OpenAPI 3 specification of the REST endpoints are exposed at ``/api/swagger/json`` for admin role only:
.. code-block:: http
GET /api/swagger/json HTTP/1.1
X-Hasura-Role: admin
The response JSON will be a OpenAPI 3 specification (OAS 3.0) for all the
RESTified GraphQL Endpoints for admin roles. For more details about OAS 3.0,
`click here <https://swagger.io/specification/>`__.
Sample request
^^^^^^^^^^^^^^
.. code-block:: http
GET /api/swagger/json HTTP/1.1
X-Hasura-Role: admin
Response
^^^^^^^^
.. code-block:: JSON
{
"openapi": "3.0.0",
"info": {
"version": "",
"title": "Rest Endpoints",
"description": "These OpenAPI specifications are automatically generated by Hasura."
},
"paths": {
"/api/rest/users": {
"get": {
"summary": "Fetch user data",
"description": "This API fetches user data (first name and last name) from the users table.\n***\nThe GraphQl query for this endpoint is:\n``` graphql\nquery MyQuery{\n users {\n first_name\n last_name\n }\n}\n```",
"responses": {}
},
"parameters": [
{
"schema": {
"type": "string"
},
"in": "header",
"name": "x-hasura-admin-secret",
"description": "Your x-hasura-admin-secret will be used for authentication of the API request."
}
]
}
},
"components": {}
}

View File

@ -131,6 +131,7 @@ library
, monad-loops , monad-loops
, monad-validate , monad-validate
, mtl , mtl
, openapi3
, optparse-applicative , optparse-applicative
, parsec , parsec
, pg-client , pg-client
@ -217,7 +218,7 @@ library
, template-haskell >= 2.11 , template-haskell >= 2.11
-- websockets interface related -- websockets interface related
, websockets , websockets>=0.12
, stm , stm
, stm-containers , stm-containers
, list-t , list-t
@ -440,6 +441,7 @@ library
, Hasura.Server.Types , Hasura.Server.Types
, Hasura.Server.API.PGDump , Hasura.Server.API.PGDump
, Hasura.Server.Rest , Hasura.Server.Rest
, Hasura.Server.OpenAPI
, Hasura.Prelude , Hasura.Prelude
, Hasura.EncJSON , Hasura.EncJSON

View File

@ -21,6 +21,7 @@ module Hasura.RQL.Types.Endpoint
deName, deName,
splitPath, splitPath,
mkEndpointUrl, mkEndpointUrl,
unEndpointUrl,
) )
where where

View File

@ -10,6 +10,7 @@ module Hasura.RQL.Types.Endpoint.Trie
singletonMultiMap, singletonMultiMap,
singletonTrie, singletonTrie,
insertPath, insertPath,
leaves,
matchPath, matchPath,
ambiguousPaths, ambiguousPaths,
ambiguousPathsGrouped, ambiguousPathsGrouped,
@ -210,3 +211,6 @@ ambiguousPaths (Trie pathMap (MultiMap methodMap)) =
childNodeAmbiguousPaths pc t = first (pc :) <$> ambiguousPaths (mergeWildcardTrie t) childNodeAmbiguousPaths pc t = first (pc :) <$> ambiguousPaths (mergeWildcardTrie t)
wildcardTrie = M.lookup PathParam pathMap wildcardTrie = M.lookup PathParam pathMap
mergeWildcardTrie = maybe id (<>) wildcardTrie mergeWildcardTrie = maybe id (<>) wildcardTrie
leaves :: Trie k v -> [v]
leaves (Trie m v) = v : concatMap leaves (M.elems m)

View File

@ -60,6 +60,7 @@ import Hasura.Server.Limits
import Hasura.Server.Logging import Hasura.Server.Logging
import Hasura.Server.Metrics (ServerMetrics) import Hasura.Server.Metrics (ServerMetrics)
import Hasura.Server.Middleware (corsMiddleware) import Hasura.Server.Middleware (corsMiddleware)
import Hasura.Server.OpenAPI (serveJSON)
import Hasura.Server.Rest import Hasura.Server.Rest
import Hasura.Server.Types import Hasura.Server.Types
import Hasura.Server.Utils import Hasura.Server.Utils
@ -1077,6 +1078,13 @@ httpApp setupHook corsCfg serverCtx enableConsole consoleAssetsDir enableTelemet
onlyAdmin onlyAdmin
respJ <- liftIO $ EL.dumpLiveQueriesState True $ scLQState serverCtx respJ <- liftIO $ EL.dumpLiveQueriesState True $ scLQState serverCtx
return (emptyHttpLogMetadata @m, JSONResp $ HttpResponse (encJFromJValue respJ) []) return (emptyHttpLogMetadata @m, JSONResp $ HttpResponse (encJFromJValue respJ) [])
Spock.get "api/swagger/json" $
spockAction encodeQErr id $
mkGetHandler $ do
onlyAdmin
sc <- getSCFromRef $ scCacheRef serverCtx
let json = serveJSON sc
return (emptyHttpLogMetadata @m, JSONResp $ HttpResponse (encJFromJValue json) [])
forM_ [Spock.GET, Spock.POST] $ \m -> Spock.hookAny m $ \_ -> do forM_ [Spock.GET, Spock.POST] $ \m -> Spock.hookAny m $ \_ -> do
req <- Spock.request req <- Spock.request

View File

@ -0,0 +1,189 @@
{-# LANGUAGE OverloadedStrings #-}
module Hasura.Server.OpenAPI (serveJSON) where
import Control.Lens
import Data.Aeson (Value, toJSON)
import Data.HashMap.Strict qualified as M
import Data.HashMap.Strict.InsOrd qualified as MI
import Data.List.NonEmpty qualified as LNE
import Data.OpenApi
import Data.OpenApi.Declare
import Data.Set.Internal qualified as S
import Data.Text qualified as T
import Data.Text.NonEmpty qualified as TNE
import Hasura.Prelude hiding (get, put)
import Hasura.RQL.Types.Endpoint
import Hasura.RQL.Types.QueryCollection
import Hasura.RQL.Types.SchemaCache
import Language.GraphQL.Draft.Syntax qualified as G
data EndpointData = EndpointData
{ endpointUrl :: String,
method :: [Text],
varList :: [Referenced Param],
endpointDescription :: Text, -- contains API comments and graphql query
endpointName :: Text
}
getVarList :: EndpointMetadata GQLQueryWithText -> [G.VariableDefinition]
getVarList e = vars =<< varLists
where
varLists = G.getExecutableDefinitions . unGQLQuery . getGQLQuery . _edQuery . _ceDefinition $ e
vars x = case x of
G.ExecutableDefinitionOperation (G.OperationDefinitionTyped (G.TypedOperationDefinition _ _ vds _ _)) -> vds
_ -> []
getVariableDefinitions :: EndpointMetadata GQLQueryWithText -> [Referenced Param]
getVariableDefinitions d = fmap varDetails varList
where
pathVars = map T.tail $ lefts $ splitPath Left Right (_ceUrl d) -- NOTE: URL Variable name ':' prefix is removed for `elem` lookup.
varList = getVarList d
varDetails a =
let vName = (G.unName . G._vdName $ a)
in Inline $
mkParam
vName
Nothing
Nothing
(if vName `elem` pathVars then ParamPath else ParamQuery)
Nothing
(getDefaultVar a)
( case G._vdType a of
G.TypeNamed _ na -> case G.unName na of
"Int" -> Just OpenApiInteger
"String" -> Just OpenApiString
"json" -> Just OpenApiObject
_ -> Nothing
G.TypeList _ _ -> Nothing
)
getGQLQueryFromTrie :: EndpointMetadata GQLQueryWithText -> Text
getGQLQueryFromTrie = getGQLQueryText . _edQuery . _ceDefinition
mkParam :: Text -> Maybe Text -> Maybe Bool -> ParamLocation -> Maybe Bool -> Maybe Value -> Maybe OpenApiType -> Param
mkParam nameP desc req loc allowEmpty def varType =
mempty
& name .~ nameP
& description .~ desc
& required .~ req
& in_ .~ loc
& allowEmptyValue .~ allowEmpty
& schema
?~ Inline
( mempty
& default_ .~ def
& type_ .~ varType
)
getDefaultVar :: G.VariableDefinition -> Maybe Value
getDefaultVar var = case G._vdDefaultValue var of
Nothing -> Nothing
Just va -> case va of
G.VNull -> Nothing
G.VInt n -> Just $ toJSON n
G.VFloat sci -> Just $ toJSON sci
G.VString txt -> Just $ toJSON txt
G.VBoolean b -> Just $ toJSON b
G.VEnum ev -> Just $ toJSON ev
_ -> Nothing
getComment :: EndpointMetadata GQLQueryWithText -> Text
getComment d = comment
where
gql = getGQLQueryFromTrie d
comment = case _ceComment d of
(Just c) -> c <> "\n***\nThe GraphQl query for this endpoint is:\n``` graphql\n" <> gql <> "\n```"
Nothing -> "***\nThe GraphQl query for this endpoint is:\n``` graphql\n" <> gql <> "\n```"
getURL :: EndpointMetadata GQLQueryWithText -> Text
getURL d =
"/api/rest/"
-- The url will be of the format <Endpoint>/:<Var1>/:<Var2> ... always, so we can
-- split and take the first element (it should never fail)
<> fst (T.breakOn "/" (TNE.unNonEmptyText . unEndpointUrl . _ceUrl $ d))
<> foldl
( \b a -> b <> "/{" <> a <> "}"
)
""
(map T.tail $ lefts $ splitPath Left Right (_ceUrl d))
extractEndpointInfo :: EndpointMethod -> EndpointMetadata GQLQueryWithText -> EndpointData
extractEndpointInfo method d =
let endpointUrl = T.unpack . getURL $ d
varList = getVariableDefinitions d
endpointDescription = getComment d
endpointName = TNE.unNonEmptyText $ unEndpointName $ _ceName d
in EndpointData
{ endpointUrl = endpointUrl,
method = [unEndpointMethod method], -- NOTE: Methods are grouped with into matching endpoints - Name used for grouping.
varList = varList,
endpointDescription = endpointDescription,
endpointName = endpointName
}
getEndpointsData :: SchemaCache -> [EndpointData]
getEndpointsData sc = map squashEndpointGroup endpointsGrouped
where
endpointTrie = scEndpoints sc
methodMaps = leaves endpointTrie
endpointsWithMethods = concatMap (\(m, s) -> map (m,) (S.toList s)) $ concatMap (M.toList . _unMultiMap) methodMaps
endpointsWithInfo = map (uncurry extractEndpointInfo) endpointsWithMethods
endpointsGrouped = LNE.groupBy (\a b -> endpointName a == endpointName b) endpointsWithInfo
squashEndpointGroup :: NonEmpty EndpointData -> EndpointData
squashEndpointGroup g = (LNE.head g) {method = concatMap method g}
serveJSON :: SchemaCache -> OpenApi
serveJSON sc = spec & components . schemas .~ defs
where
(defs, spec) = runDeclare (declareOpenApiSpec sc) mempty
declareOpenApiSpec :: SchemaCache -> Declare (Definitions Schema) OpenApi
declareOpenApiSpec sc = do
let mkOperation :: EndpointData -> Operation
mkOperation ed =
mempty
& description ?~ endpointDescription ed
& summary ?~ endpointName ed
getOPName :: EndpointData -> Text -> Maybe Operation
getOPName ed methodType =
if methodType `elem` method ed
then Just $ mkOperation ed
else Nothing
xHasuraAS :: Param
xHasuraAS =
mkParam
"x-hasura-admin-secret"
(Just "Your x-hasura-admin-secret will be used for authentication of the API request.")
Nothing
ParamHeader
Nothing
Nothing
(Just OpenApiString)
generatePathItem :: EndpointData -> PathItem
generatePathItem ed =
mempty
& get .~ getOPName ed "GET"
& post .~ getOPName ed "POST"
& put .~ getOPName ed "PUT"
& delete .~ getOPName ed "DELETE"
& patch .~ getOPName ed "PATCH"
& parameters .~ Inline xHasuraAS :
varList ed
endpointLst = getEndpointsData sc
mkOpenAPISchema :: [EndpointData] -> InsOrdHashMap FilePath PathItem
mkOpenAPISchema edLst = foldl (\hm ed -> MI.insert (endpointUrl ed) (generatePathItem ed) hm) mempty edLst
openAPIPaths = mkOpenAPISchema endpointLst
return $
mempty
& paths .~ openAPIPaths
& info . title .~ "Rest Endpoints"
& info . description ?~ "These OpenAPI specifications are automatically generated by Hasura."

View File

@ -0,0 +1,12 @@
description: Call openapi json endpoint for empty response
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: These OpenAPI specifications are automatically generated by Hasura.
components: {}

View File

@ -0,0 +1,137 @@
- description: Try to add a rest endpoint with multiple methods
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: multi_method_endpoint
name: multi_method_endpoint
methods:
- GET
- POST
- PUT
- DELETE
- PATCH
definition:
query:
collection_name: test_collection
query_name: mutation_with_args
- description: Call openapi json endpoint
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: These OpenAPI specifications are automatically generated by Hasura.
paths:
/api/rest/multi_method_endpoint:
get:
summary: multi_method_endpoint
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
mutation ($first_name: String!, $last_name: String!) {
insert_test_table( objects: {first_name: $first_name, last_name:
$last_name }) { returning { id } affected_rows } }
```
responses: {}
put:
summary: multi_method_endpoint
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
mutation ($first_name: String!, $last_name: String!) {
insert_test_table( objects: {first_name: $first_name, last_name:
$last_name }) { returning { id } affected_rows } }
```
responses: {}
post:
summary: multi_method_endpoint
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
mutation ($first_name: String!, $last_name: String!) {
insert_test_table( objects: {first_name: $first_name, last_name:
$last_name }) { returning { id } affected_rows } }
```
responses: {}
delete:
summary: multi_method_endpoint
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
mutation ($first_name: String!, $last_name: String!) {
insert_test_table( objects: {first_name: $first_name, last_name:
$last_name }) { returning { id } affected_rows } }
```
responses: {}
patch:
summary: multi_method_endpoint
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
mutation ($first_name: String!, $last_name: String!) {
insert_test_table( objects: {first_name: $first_name, last_name:
$last_name }) { returning { id } affected_rows } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
- schema:
type: string
in: query
name: first_name
- schema:
type: string
in: query
name: last_name
components: {}
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: multi_method_endpoint

View File

@ -0,0 +1,59 @@
- description: Try to add a GET rest endpoint with no argument
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: simple
name: simple
methods:
- GET
definition:
query:
collection_name: test_collection
query_name: simple_query
- description: Call openapi json endpoint
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: These OpenAPI specifications are automatically generated by Hasura.
paths:
/api/rest/simple:
get:
summary: simple
description: |-
***
The GraphQl query for this endpoint is:
``` graphql
query { test_table { first_name last_name } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
components: {}
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: simple

View File

@ -0,0 +1,186 @@
- description: Try to add a POST rest endpoint with arguments in URL
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: with_args_url/:first_name/:last_name
name: with_args_url
methods:
- POST
definition:
query:
collection_name: test_collection
query_name: query_with_args
- description: Try to add a POST rest endpoint with default argument
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: with_default_arg
name: with_default_arg
methods:
- POST
definition:
query:
collection_name: test_collection
query_name: query_with_default_arg
- description: Try to add a POST rest mutation endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: mutation_with_args
name: mutation_with_args
methods:
- POST
definition:
query:
collection_name: test_collection
query_name: mutation_with_args
- description: Call openapi json endpoint
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: These OpenAPI specifications are automatically generated by Hasura.
paths:
/api/rest/mutation_with_args:
post:
summary: mutation_with_args
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
mutation ($first_name: String!, $last_name: String!) {
insert_test_table( objects: {first_name: $first_name, last_name:
$last_name }) { returning { id } affected_rows } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
- schema:
type: string
in: query
name: first_name
- schema:
type: string
in: query
name: last_name
/api/rest/with_default_arg:
post:
summary: with_default_arg
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
query ($first_name:String="Foo") { test_table(where: {first_name: { _eq:
$first_name } }) { first_name last_name } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
- schema:
default: Foo
type: string
in: query
name: first_name
'/api/rest/with_args_url/{first_name}/{last_name}':
post:
summary: with_args_url
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
query ($first_name: String!, $last_name:String!) { test_table(where:
{first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) {
first_name last_name } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
- schema:
type: string
in: path
name: first_name
- schema:
type: string
in: path
name: last_name
components: {}
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: with_args_url
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: with_default_arg
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: mutation_with_args

View File

@ -0,0 +1,73 @@
- description: Try to add a POST rest endpoint with arguments
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: with_args
name: with_args
methods:
- POST
definition:
query:
collection_name: test_collection
query_name: query_with_args
- description: Call openapi json endpoint
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: These OpenAPI specifications are automatically generated by Hasura.
paths:
/api/rest/with_args:
post:
summary: with_args
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
query ($first_name: String!, $last_name:String!) { test_table(where:
{first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) {
first_name last_name } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
- schema:
type: string
in: query
name: first_name
- schema:
type: string
in: query
name: last_name
components: {}
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: with_args

View File

@ -0,0 +1,73 @@
- description: Try to add a POST rest endpoint with arguments in URL
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: with_args_url/:first_name/:last_name
name: with_args_url
methods:
- POST
definition:
query:
collection_name: test_collection
query_name: query_with_args
- description: Call openapi json endpoint
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: These OpenAPI specifications are automatically generated by Hasura.
paths:
'/api/rest/with_args_url/{first_name}/{last_name}':
post:
summary: with_args_url
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
query ($first_name: String!, $last_name:String!) { test_table(where:
{first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) {
first_name last_name } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
- schema:
type: string
in: path
name: first_name
- schema:
type: string
in: path
name: last_name
components: {}
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: with_args_url

View File

@ -0,0 +1,69 @@
- description: Try to add a POST rest endpoint with default argument
url: /v1/query
status: 200
response:
message: success
query:
type: create_rest_endpoint
args:
url: with_default_arg
name: with_default_arg
methods:
- POST
definition:
query:
collection_name: test_collection
query_name: query_with_default_arg
- description: Call openapi json endpoint
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: These OpenAPI specifications are automatically generated by Hasura.
paths:
/api/rest/with_default_arg:
post:
summary: with_default_arg
description: >-
***
The GraphQl query for this endpoint is:
``` graphql
query ($first_name:String="Foo") { test_table(where: {first_name: { _eq:
$first_name } }) { first_name last_name } }
```
responses: {}
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: >-
Your x-hasura-admin-secret will be used for authentication of the API
request.
- schema:
default: Foo
type: string
in: query
name: first_name
components: {}
- description: Try to remove the endpoint
url: /v1/query
status: 200
response:
message: success
query:
type: drop_rest_endpoint
args:
name: with_default_arg

View File

@ -0,0 +1,42 @@
type: bulk
args:
- type: create_query_collection
args:
name: test_collection
definition:
queries:
- name: simple_query
query: "query { test_table { first_name last_name } }"
- name: query_with_arg
query: "query ($first_name:String!) { test_table(where: {first_name: { _eq: $first_name } }) { first_name last_name } }"
- name: query_with_args
query: "query ($first_name: String!, $last_name:String!) { test_table(where: {first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) { first_name last_name } }"
- name: query_with_uuid_arg
query: "query ($id: uuid!) { test_table(where: {id: { _neq: $id }}) { first_name last_name } }"
- name: query_with_default_arg
query: "query ($first_name:String=\"Foo\") { test_table(where: {first_name: { _eq: $first_name } }) { first_name last_name } }"
- name: mutation_with_args
query: "mutation ($first_name: String!, $last_name: String!) { insert_test_table( objects: {first_name: $first_name, last_name: $last_name }) { returning { id } affected_rows } }"
- type: run_sql
args:
sql: |
create table test_table(
first_name text,
last_name text,
id UUID NOT NULL DEFAULT gen_random_uuid()
);
- type: track_table
args:
schema: public
name: test_table
- type: run_sql
args:
sql: |
insert into test_table (first_name, last_name)
values
('Foo', 'Bar'),
('Baz', 'Qux'),
('X%20Y', 'Test');

View File

@ -0,0 +1,10 @@
type: bulk
args:
- type: drop_query_collection
args:
collection: test_collection
cascade: false
- type: run_sql
args:
sql: |
drop table test_table

View File

@ -0,0 +1,34 @@
import pytest
import os
from validate import check_query_f, check_query, get_conf_f
from context import PytestConf
@pytest.mark.parametrize("transport", ['http'])
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestOpenAPISpec:
@classmethod
def dir(cls):
return 'queries/openapi'
def test_empty_openapi_json(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_empty.yaml', transport)
def test_endpoint_simple(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_get_endpoint_test_simple.yaml', transport)
def test_endpoint_with_args(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_post_endpoint_test_with_args.yaml', transport)
def test_endpoint_with_args_url(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_post_endpoint_test_with_args_url.yaml', transport)
def test_endpoint_with_default_arg(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_post_endpoint_test_with_default_arg.yaml', transport)
def test_endpoint_with_multiple_methods(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_endpoint_with_multiple_methods.yaml', transport)
def test_multiple_endpoints(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_multiple_endpoints_test.yaml', transport)