From 8b60122b9e1159ae92bbf015773568bb32468557 Mon Sep 17 00:00:00 2001 From: Puru Gupta Date: Wed, 6 Oct 2021 12:45:14 +0530 Subject: [PATCH] [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 --- CHANGELOG.md | 1 + cabal.project.freeze | 37 ++-- docs/graphql/core/api-reference/restified.rst | 57 ++++++ server/graphql-engine.cabal | 4 +- server/src-lib/Hasura/RQL/Types/Endpoint.hs | 1 + .../src-lib/Hasura/RQL/Types/Endpoint/Trie.hs | 4 + server/src-lib/Hasura/Server/App.hs | 8 + server/src-lib/Hasura/Server/OpenAPI.hs | 189 ++++++++++++++++++ .../queries/openapi/openapi_empty.yaml | 12 ++ ...penapi_endpoint_with_multiple_methods.yaml | 137 +++++++++++++ .../openapi_get_endpoint_test_simple.yaml | 59 ++++++ .../openapi_multiple_endpoints_test.yaml | 186 +++++++++++++++++ .../openapi_post_endpoint_test_with_args.yaml | 73 +++++++ ...napi_post_endpoint_test_with_args_url.yaml | 73 +++++++ ...i_post_endpoint_test_with_default_arg.yaml | 69 +++++++ server/tests-py/queries/openapi/setup.yaml | 42 ++++ server/tests-py/queries/openapi/teardown.yaml | 10 + server/tests-py/test_openapi.py | 34 ++++ 18 files changed, 970 insertions(+), 26 deletions(-) create mode 100644 server/src-lib/Hasura/Server/OpenAPI.hs create mode 100644 server/tests-py/queries/openapi/openapi_empty.yaml create mode 100644 server/tests-py/queries/openapi/openapi_endpoint_with_multiple_methods.yaml create mode 100644 server/tests-py/queries/openapi/openapi_get_endpoint_test_simple.yaml create mode 100644 server/tests-py/queries/openapi/openapi_multiple_endpoints_test.yaml create mode 100644 server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args.yaml create mode 100644 server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args_url.yaml create mode 100644 server/tests-py/queries/openapi/openapi_post_endpoint_test_with_default_arg.yaml create mode 100644 server/tests-py/queries/openapi/setup.yaml create mode 100644 server/tests-py/queries/openapi/teardown.yaml create mode 100644 server/tests-py/test_openapi.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 99826f1f3bf..e4671dd7825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next release (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: support MSSQL insert mutations diff --git a/cabal.project.freeze b/cabal.project.freeze index a5754c51a5b..66663536c42 100644 --- a/cabal.project.freeze +++ b/cabal.project.freeze @@ -1,6 +1,5 @@ active-repositories: hackage.haskell.org:merge constraints: any.Cabal ==3.2.0.0, - any.Glob ==0.10.1, any.HTTP ==4000.3.15, HTTP -conduit10 -mtl1 +network-uri -warn-as-error -warp-tests, any.HUnit ==1.6.2.0, @@ -12,13 +11,12 @@ constraints: any.Cabal ==3.2.0.0, SHA -exe, any.Spock-core ==0.14.0.0, any.StateVar ==1.2.1, - any.abstract-deque ==0.3, - abstract-deque -usecas, - any.abstract-par ==0.3.3, any.adjunctions ==4.4, any.aeson ==1.5.5.1, aeson -bytestring-builder -cffi -developer -fast, any.aeson-casing ==0.2.0.0, + any.aeson-pretty ==0.8.8, + aeson-pretty -lib-only, any.ansi-terminal ==0.11, ansi-terminal -example, any.ansi-wl-pprint ==0.6.9, @@ -50,7 +48,6 @@ constraints: any.Cabal ==3.2.0.0, any.bifunctors ==5.5.10, bifunctors +semigroups +tagged, any.binary ==0.8.8.0, - any.binary-orphans ==1.0.1, any.binary-parser ==0.5.6, any.blaze-builder ==0.4.2.1, 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.call-stack ==0.3.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, cereal -bytestring-builder, any.charset ==0.3.7.1, @@ -77,7 +72,6 @@ constraints: any.Cabal ==3.2.0.0, clock -llvm, any.cmdargs ==0.10.20, cmdargs +quotation -testprog, - any.code-page ==0.2.1, any.colour ==2.3.5, any.comonad ==5.0.8, comonad +containers +distributive +indexed-traversable, @@ -93,10 +87,6 @@ constraints: any.Cabal ==3.2.0.0, any.contravariant ==1.5.3, contravariant +semigroups +statevar +tagged, 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, cron -lib-werror, 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.deepseq ==1.4.4.0, any.deferred-folds ==0.9.15, - any.dense-linear-algebra ==0.1.0.0, any.dependent-map ==0.4.0.0, any.dependent-sum ==0.7.1.0, any.directory ==1.3.6.0, @@ -152,6 +141,7 @@ constraints: any.Cabal ==3.2.0.0, any.foldl ==1.4.10, any.formatting ==7.1.1, any.free ==5.1.6, + any.generics-sop ==0.5.1.1, any.ghc ==8.10.2, any.ghc-boot ==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, http-conduit +aeson, any.http-date ==0.0.10, + any.http-media ==0.8.0.0, any.http-types ==0.12.3, any.http2 ==2.0.5, http2 -devel, @@ -201,6 +192,7 @@ constraints: any.Cabal ==3.2.0.0, any.indexed-traversable ==0.1.1, any.insert-ordered-containers ==0.2.3.1, any.inspection-testing ==0.4.5.0, + inspection-testing -more-tests -old-text-tests, any.integer-gmp ==1.0.3.0, any.integer-logarithms ==1.0.3.1, integer-logarithms -check-bounds +integer-gmp, @@ -211,7 +203,6 @@ constraints: any.Cabal ==3.2.0.0, any.iproute ==1.7.10, any.jose ==0.8.4, jose -demos, - any.js-chart ==2.9.4.1, any.kan-extensions ==5.2.1, any.keys ==3.12.3, any.kriti-lang ==0.2.0.0, @@ -232,15 +223,11 @@ constraints: any.Cabal ==3.2.0.0, math-functions +system-erf +system-expm1, any.memory ==0.15.0, memory +support_basement +support_bytestring +support_deepseq +support_foundation, - any.microstache ==1.0.1.2, any.mime-types ==0.1.0.9, any.mmorph ==1.1.4, any.monad-control ==1.0.2.3, any.monad-loops ==0.4.3, 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-validate ==1.2.0.0, any.mono-traversable ==1.0.15.1, @@ -248,7 +235,6 @@ constraints: any.Cabal ==3.2.0.0, any.mtl-compat ==0.2.2, mtl-compat -two-point-one -two-point-two, any.mustache ==2.3.1, - any.mwc-probability ==2.3.1, any.mwc-random ==0.14.0.0, any.raw-strings-qq ==1.1, any.mysql ==0.2.0.1, @@ -265,8 +251,10 @@ constraints: any.Cabal ==3.2.0.0, any.odbc ==0.2.3, any.old-locale ==1.0.0.7, any.old-time ==1.1.0.3, + any.openapi3 ==3.1.0, any.optics-core ==0.3.0.1, any.optics-extra ==0.3, + any.optics-th ==0.3.0.2, any.optparse-applicative ==0.16.1.0, optparse-applicative +process, any.optparse-generic ==1.4.4, @@ -286,11 +274,12 @@ constraints: any.Cabal ==3.2.0.0, any.postgresql-libpq ==0.9.4.3, postgresql-libpq -use-pkg-config, any.pretty ==1.1.3.6, - any.pretty-simple ==4.0.0.0, any.pretty-show ==1.10, + any.pretty-simple ==4.0.0.0, + pretty-simple -buildexample -buildexe, any.prettyprinter ==1.7.0, prettyprinter -buildreadme, - prettyprinter-ansi-terminal ==1.1.2, + any.prettyprinter-ansi-terminal ==1.1.2, any.primitive ==0.7.1.0, any.primitive-extras ==0.8, any.primitive-unlifted ==0.1.3.0, @@ -341,10 +330,10 @@ constraints: any.Cabal ==3.2.0.0, any.socks ==0.6.1, any.some ==1.0.1, some +newtype-unsafe, + any.sop-core ==0.5.0.1, any.split ==0.2.3.4, any.splitmix ==0.1.0.3, splitmix -optimised-mixer, - any.statistics ==0.15.2.0, any.stm ==2.5.0.0, any.stm-containers ==1.2, 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.template-haskell ==2.16.0.0, any.temporary ==1.3, - any.terminfo ==0.4.1.4, any.terminal-size ==0.3.2.1, + any.terminfo ==0.4.1.4, any.text ==1.2.3.2, any.text-builder ==0.6.6.1, any.text-conversions ==0.3.1, @@ -418,10 +407,8 @@ constraints: any.Cabal ==3.2.0.0, vector +boundschecks -internalchecks -unsafechecks -wall, any.vector-algorithms ==0.8.0.4, vector-algorithms +bench +boundschecks -internalchecks -llvm +properties -unsafechecks, - any.vector-binary-instances ==0.2.5.1, any.vector-instances ==3.4, vector-instances +hashable, - any.vector-th-unbox ==0.2.1.7, any.void ==0.7.3, void -safe, any.wai ==3.2.3, diff --git a/docs/graphql/core/api-reference/restified.rst b/docs/graphql/core/api-reference/restified.rst index 414709d3a2a..d5c2dfe61d2 100644 --- a/docs/graphql/core/api-reference/restified.rst +++ b/docs/graphql/core/api-reference/restified.rst @@ -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. 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 `__. + +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": {} + } diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 7aa70a70c96..b51f96922a2 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -131,6 +131,7 @@ library , monad-loops , monad-validate , mtl + , openapi3 , optparse-applicative , parsec , pg-client @@ -217,7 +218,7 @@ library , template-haskell >= 2.11 -- websockets interface related - , websockets + , websockets>=0.12 , stm , stm-containers , list-t @@ -440,6 +441,7 @@ library , Hasura.Server.Types , Hasura.Server.API.PGDump , Hasura.Server.Rest + , Hasura.Server.OpenAPI , Hasura.Prelude , Hasura.EncJSON diff --git a/server/src-lib/Hasura/RQL/Types/Endpoint.hs b/server/src-lib/Hasura/RQL/Types/Endpoint.hs index 0e6eb8d627e..e39f5a10c65 100644 --- a/server/src-lib/Hasura/RQL/Types/Endpoint.hs +++ b/server/src-lib/Hasura/RQL/Types/Endpoint.hs @@ -21,6 +21,7 @@ module Hasura.RQL.Types.Endpoint deName, splitPath, mkEndpointUrl, + unEndpointUrl, ) where diff --git a/server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs b/server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs index a4e52eee920..54a1e02476f 100644 --- a/server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs +++ b/server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs @@ -10,6 +10,7 @@ module Hasura.RQL.Types.Endpoint.Trie singletonMultiMap, singletonTrie, insertPath, + leaves, matchPath, ambiguousPaths, ambiguousPathsGrouped, @@ -210,3 +211,6 @@ ambiguousPaths (Trie pathMap (MultiMap methodMap)) = childNodeAmbiguousPaths pc t = first (pc :) <$> ambiguousPaths (mergeWildcardTrie t) wildcardTrie = M.lookup PathParam pathMap mergeWildcardTrie = maybe id (<>) wildcardTrie + +leaves :: Trie k v -> [v] +leaves (Trie m v) = v : concatMap leaves (M.elems m) diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index b4785f5886b..cc709aec6b4 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -60,6 +60,7 @@ import Hasura.Server.Limits import Hasura.Server.Logging import Hasura.Server.Metrics (ServerMetrics) import Hasura.Server.Middleware (corsMiddleware) +import Hasura.Server.OpenAPI (serveJSON) import Hasura.Server.Rest import Hasura.Server.Types import Hasura.Server.Utils @@ -1077,6 +1078,13 @@ httpApp setupHook corsCfg serverCtx enableConsole consoleAssetsDir enableTelemet onlyAdmin respJ <- liftIO $ EL.dumpLiveQueriesState True $ scLQState serverCtx 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 req <- Spock.request diff --git a/server/src-lib/Hasura/Server/OpenAPI.hs b/server/src-lib/Hasura/Server/OpenAPI.hs new file mode 100644 index 00000000000..a0b317fe944 --- /dev/null +++ b/server/src-lib/Hasura/Server/OpenAPI.hs @@ -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 /:/: ... 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." diff --git a/server/tests-py/queries/openapi/openapi_empty.yaml b/server/tests-py/queries/openapi/openapi_empty.yaml new file mode 100644 index 00000000000..ef8fcec026e --- /dev/null +++ b/server/tests-py/queries/openapi/openapi_empty.yaml @@ -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: {} \ No newline at end of file diff --git a/server/tests-py/queries/openapi/openapi_endpoint_with_multiple_methods.yaml b/server/tests-py/queries/openapi/openapi_endpoint_with_multiple_methods.yaml new file mode 100644 index 00000000000..ee232089b4e --- /dev/null +++ b/server/tests-py/queries/openapi/openapi_endpoint_with_multiple_methods.yaml @@ -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 \ No newline at end of file diff --git a/server/tests-py/queries/openapi/openapi_get_endpoint_test_simple.yaml b/server/tests-py/queries/openapi/openapi_get_endpoint_test_simple.yaml new file mode 100644 index 00000000000..3b1f43214fb --- /dev/null +++ b/server/tests-py/queries/openapi/openapi_get_endpoint_test_simple.yaml @@ -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 \ No newline at end of file diff --git a/server/tests-py/queries/openapi/openapi_multiple_endpoints_test.yaml b/server/tests-py/queries/openapi/openapi_multiple_endpoints_test.yaml new file mode 100644 index 00000000000..6609276f7c1 --- /dev/null +++ b/server/tests-py/queries/openapi/openapi_multiple_endpoints_test.yaml @@ -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 \ No newline at end of file diff --git a/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args.yaml b/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args.yaml new file mode 100644 index 00000000000..061c79282b4 --- /dev/null +++ b/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args.yaml @@ -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 \ No newline at end of file diff --git a/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args_url.yaml b/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args_url.yaml new file mode 100644 index 00000000000..278f3cacc88 --- /dev/null +++ b/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_args_url.yaml @@ -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 \ No newline at end of file diff --git a/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_default_arg.yaml b/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_default_arg.yaml new file mode 100644 index 00000000000..08ca65d6175 --- /dev/null +++ b/server/tests-py/queries/openapi/openapi_post_endpoint_test_with_default_arg.yaml @@ -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 \ No newline at end of file diff --git a/server/tests-py/queries/openapi/setup.yaml b/server/tests-py/queries/openapi/setup.yaml new file mode 100644 index 00000000000..c62571849be --- /dev/null +++ b/server/tests-py/queries/openapi/setup.yaml @@ -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'); diff --git a/server/tests-py/queries/openapi/teardown.yaml b/server/tests-py/queries/openapi/teardown.yaml new file mode 100644 index 00000000000..85bb52b8761 --- /dev/null +++ b/server/tests-py/queries/openapi/teardown.yaml @@ -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 diff --git a/server/tests-py/test_openapi.py b/server/tests-py/test_openapi.py new file mode 100644 index 00000000000..a2331899c01 --- /dev/null +++ b/server/tests-py/test_openapi.py @@ -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) +