Introduce GDC Add/Remove Agent and List Source Kinds Metadata API Actions

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5415
GitOrigin-RevId: b46f928eae85a747378bbc18968d93dcb81c895a
This commit is contained in:
Solomon 2022-08-17 15:13:32 -07:00 committed by hasura-bot
parent 11b22f7dc9
commit 7a2352dd6e
15 changed files with 336 additions and 28 deletions

View File

@ -706,6 +706,7 @@ library
, Hasura.RQL.DDL.ApiLimit
, Hasura.RQL.DDL.ComputedField
, Hasura.RQL.DDL.CustomTypes
, Hasura.RQL.DDL.DataConnector
, Hasura.RQL.DDL.Endpoint
, Hasura.RQL.DDL.GraphqlSchemaIntrospection
, Hasura.RQL.DDL.InheritedRoles
@ -730,6 +731,7 @@ library
, Hasura.RQL.DDL.Webhook.Transform.QueryParams
, Hasura.RQL.DDL.Webhook.Transform.Validation
, Hasura.RQL.DDL.Webhook.Transform.Url
, Hasura.RQL.DDL.SourceKinds
, Hasura.RQL.DDL.Schema
, Hasura.RQL.DDL.Schema.Cache
, Hasura.RQL.DDL.Schema.Cache.Common

View File

@ -36,7 +36,7 @@ import Language.GraphQL.Draft.Syntax qualified as G
type Unimplemented = ()
instance Backend 'DataConnector where
type BackendConfig 'DataConnector = Adapter.DataConnectorBackendConfig
type BackendConfig 'DataConnector = InsOrdHashMap Adapter.DataConnectorName Adapter.DataConnectorOptions
type SourceConfig 'DataConnector = Adapter.SourceConfig
type SourceConnConfiguration 'DataConnector = Adapter.ConnSourceConfig

View File

@ -67,7 +67,7 @@ resolveSourceConfig' ::
SourceName ->
DC.ConnSourceConfig ->
BackendSourceKind 'DataConnector ->
DC.DataConnectorBackendConfig ->
InsOrdHashMap DC.DataConnectorName DC.DataConnectorOptions ->
Environment ->
HTTP.Manager ->
m (Either QErr DC.SourceConfig)

View File

@ -3,7 +3,6 @@
module Hasura.Backends.DataConnector.Adapter.Types
( ConnSourceConfig (..),
SourceConfig (..),
DataConnectorBackendConfig,
DataConnectorName (..),
DataConnectorOptions (..),
CountType (..),
@ -111,8 +110,6 @@ newtype DataConnectorName = DataConnectorName {unDataConnectorName :: NonEmptyTe
instance Witch.From DataConnectorName NonEmptyText
type DataConnectorBackendConfig = InsOrdHashMap DataConnectorName DataConnectorOptions
data DataConnectorOptions = DataConnectorOptions
{_dcoUri :: BaseUrl}
deriving stock (Eq, Ord, Show, Generic)

View File

@ -9,6 +9,8 @@ module Hasura.EncJSON
encJFromJValue,
encJFromChar,
encJFromText,
encJFromNonEmptyText,
encJFromBool,
encJFromBS,
encJFromLBS,
encJFromList,
@ -26,6 +28,8 @@ import Data.ByteString.Builder qualified as BB
import Data.ByteString.Lazy qualified as BL
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.Text.Encoding qualified as TE
import Data.Text.NonEmpty (NonEmptyText)
import Data.Text.NonEmpty qualified as NET
import Data.Vector qualified as V
import Hasura.Prelude
@ -79,6 +83,16 @@ encJFromText :: Text -> EncJSON
encJFromText = encJFromBuilder . TE.encodeUtf8Builder
{-# INLINE encJFromText #-}
encJFromNonEmptyText :: NonEmptyText -> EncJSON
encJFromNonEmptyText = encJFromBuilder . TE.encodeUtf8Builder . NET.unNonEmptyText
{-# INLINE encJFromNonEmptyText #-}
encJFromBool :: Bool -> EncJSON
encJFromBool = \case
False -> encJFromText "false"
True -> encJFromText "true"
{-# INLINE encJFromBool #-}
encJFromList :: [EncJSON] -> EncJSON
encJFromList =
encJFromBuilder . \case

View File

@ -0,0 +1,87 @@
-- | This module provides operations to load and modify metadata
-- relating to GraphQL Data Connectors.
module Hasura.RQL.DDL.DataConnector
( -- * DC Add Agent
DCAddAgent (..),
runAddDataConnectorAgent,
-- * DC Delete Agent
DCDeleteAgent (..),
runDeleteDataConnectorAgent,
)
where
--------------------------------------------------------------------------------
import Data.Aeson (FromJSON, ToJSON, (.:), (.=))
import Data.Aeson qualified as Aeson
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.Text.NonEmpty (NonEmptyText)
import Hasura.Backends.DataConnector.Adapter.Types qualified as DC.Types
import Hasura.EncJSON (EncJSON)
import Hasura.Prelude
import Hasura.RQL.Types.Common qualified as Common
import Hasura.RQL.Types.Metadata (MetadataM (putMetadata))
import Hasura.RQL.Types.Metadata qualified as Metadata
import Hasura.SQL.Backend qualified as Backend
import Hasura.SQL.BackendMap qualified as BackendMap
import Servant.Client qualified as Servant
--------------------------------------------------------------------------------
data DCAddAgent = DCAddAgent
{ _gdcaName :: NonEmptyText,
_gdcaUrl :: Servant.BaseUrl
}
instance FromJSON DCAddAgent where
parseJSON = Aeson.withObject "DCAddAgent" \o -> do
_gdcaName <- o .: "name"
mUri <- o .: "url"
case mUri of
Just _gdcaUrl -> pure DCAddAgent {..}
Nothing -> fail "Failed to parse Agent URL"
instance ToJSON DCAddAgent where
toJSON DCAddAgent {..} = Aeson.object ["name" .= _gdcaName, "url" .= show _gdcaUrl]
-- | Insert a new Data Connector Agent into Metadata.
runAddDataConnectorAgent :: (Metadata.MetadataM m) => DCAddAgent -> m EncJSON
runAddDataConnectorAgent DCAddAgent {..} = do
let kind = DC.Types.DataConnectorName _gdcaName
agent = DC.Types.DataConnectorOptions _gdcaUrl
oldMetadata <- Metadata.getMetadata
let modifiedMetadata =
oldMetadata & Metadata.metaBackendConfigs %~ BackendMap.modify @'Backend.DataConnector \oldMap ->
Metadata.BackendConfigWrapper $ InsOrdHashMap.insert kind agent (coerce oldMap)
putMetadata modifiedMetadata
pure Common.successMsg
--------------------------------------------------------------------------------
data DCDeleteAgent = DCDeleteAgent {_gdcrName :: NonEmptyText}
instance FromJSON DCDeleteAgent where
parseJSON = Aeson.withObject "DCDeleteAgent" \o -> do
_gdcrName <- o .: "name"
pure $ DCDeleteAgent {..}
instance ToJSON DCDeleteAgent where
toJSON DCDeleteAgent {..} = Aeson.object ["name" .= _gdcrName]
runDeleteDataConnectorAgent :: (Metadata.MetadataM m) => DCDeleteAgent -> m EncJSON
runDeleteDataConnectorAgent DCDeleteAgent {..} = do
let kind = DC.Types.DataConnectorName _gdcrName
oldMetadata <- Metadata.getMetadata
let modifiedMetadata =
oldMetadata & Metadata.metaBackendConfigs
%~ BackendMap.alter @'Backend.DataConnector
(fmap (coerce . InsOrdHashMap.delete kind . Metadata.unBackendConfigWrapper))
putMetadata modifiedMetadata
pure Common.successMsg

View File

@ -0,0 +1,88 @@
module Hasura.RQL.DDL.SourceKinds
( -- * List Source Kinds
ListSourceKinds (..),
runListSourceKinds,
-- * Source Kind Info
SourceKindInfo (..),
SourceType (..),
)
where
--------------------------------------------------------------------------------
import Data.Aeson (FromJSON, ToJSON, (.:), (.=))
import Data.Aeson qualified as Aeson
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.Text.Extended (ToTxt (..))
import Data.Text.NonEmpty qualified as NE.Text
import Hasura.Backends.DataConnector.Adapter.Types qualified as DC.Types
import Hasura.EncJSON (EncJSON)
import Hasura.EncJSON qualified as EncJSON
import Hasura.Prelude
import Hasura.RQL.Types.Metadata qualified as Metadata
import Hasura.SQL.Backend qualified as Backend
import Hasura.SQL.BackendMap qualified as BackendMap
--------------------------------------------------------------------------------
data ListSourceKinds = ListSourceKinds
instance FromJSON ListSourceKinds where
parseJSON = Aeson.withObject "ListSourceKinds" (const $ pure ListSourceKinds)
instance ToJSON ListSourceKinds where
toJSON ListSourceKinds = Aeson.object []
--------------------------------------------------------------------------------
data SourceKindInfo = SourceKindInfo
{ _skiSourceKind :: Text,
_skiBuiltin :: SourceType
}
instance FromJSON SourceKindInfo where
parseJSON = Aeson.withObject "SourceKindInfo" \o -> do
_skiSourceKind <- o .: "kind"
_skiBuiltin <- o .: "builtin"
pure SourceKindInfo {..}
instance ToJSON SourceKindInfo where
toJSON SourceKindInfo {..} = Aeson.object ["kind" .= _skiSourceKind, "builtin" .= _skiBuiltin]
data SourceType = Builtin | Agent
instance FromJSON SourceType where
parseJSON = Aeson.withBool "source type" \case
True -> pure Builtin
False -> pure Agent
instance ToJSON SourceType where
toJSON Builtin = Aeson.Bool True
toJSON Agent = Aeson.Bool False
--------------------------------------------------------------------------------
agentSourceKinds :: (Metadata.MetadataM m) => m [SourceKindInfo]
agentSourceKinds = do
agentsM <- BackendMap.lookup @'Backend.DataConnector . Metadata._metaBackendConfigs <$> Metadata.getMetadata
case agentsM of
Nothing -> pure []
Just (Metadata.BackendConfigWrapper agents) ->
pure $ fmap mkAgentSource $ InsOrdHashMap.keys agents
mkAgentSource :: DC.Types.DataConnectorName -> SourceKindInfo
mkAgentSource (DC.Types.DataConnectorName name) =
SourceKindInfo {_skiSourceKind = NE.Text.unNonEmptyText name, _skiBuiltin = Agent}
mkNativeSource :: Backend.BackendType -> Maybe SourceKindInfo
mkNativeSource = \case
Backend.DataConnector -> Nothing
b -> Just $ SourceKindInfo {_skiSourceKind = fromMaybe (toTxt b) (Backend.backendShortName b), _skiBuiltin = Builtin}
runListSourceKinds :: Metadata.MetadataM m => ListSourceKinds -> m EncJSON
runListSourceKinds ListSourceKinds = do
let builtins = mapMaybe mkNativeSource $ filter (/= Backend.DataConnector) Backend.supportedBackends
agents <- agentSourceKinds
pure $ EncJSON.encJFromJValue $ Aeson.object ["sources" .= (builtins <> agents)]

View File

@ -1,4 +1,5 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
-- | In order to avoid circular dependencies while splitting
-- 'Hasura.RQL.Types.Metadata' into multiple modules, some definitions must be
@ -499,6 +500,10 @@ deriving newtype instance (Backend b) => ToJSON (BackendConfigWrapper b)
deriving newtype instance (Backend b) => FromJSON (BackendConfigWrapper b)
deriving newtype instance (Semigroup (BackendConfig b)) => Semigroup (BackendConfigWrapper b)
deriving newtype instance (Monoid (BackendConfig b)) => Monoid (BackendConfigWrapper b)
data CatalogStateType
= CSTCli
| CSTConsole

View File

@ -5,22 +5,28 @@ module Hasura.SQL.BackendMap
singleton,
lookup,
elems,
alter,
modify,
)
where
import Data.Aeson (FromJSON, ToJSON, Value, object, withObject)
import Data.Aeson qualified as J
import Data.Aeson.Key qualified as K
import Data.Aeson.KeyMap qualified as KM
--------------------------------------------------------------------------------
import Data.Aeson (FromJSON, Key, ToJSON)
import Data.Aeson qualified as Aeson
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Kind (Type)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text.Extended (toTxt)
import Hasura.Prelude hiding (empty, lookup)
import Hasura.Prelude hiding (empty, lookup, modify)
import Hasura.SQL.AnyBackend (AnyBackend, SatisfiesForAllBackends, dispatchAnyBackend'', mkAnyBackend, parseAnyBackendFromJSON, unpackAnyBackend)
import Hasura.SQL.Backend (BackendType, parseBackendTypeFromText)
import Hasura.SQL.Tag (HasTag, backendTag, reify)
--------------------------------------------------------------------------------
-- | A BackendMap is a data structure that can contain at most one value of an 'i' per 'BackendType'
-- The 'i' type must be one that is parameterized by a BackendType-kinded type parameter
newtype BackendMap (i :: BackendType -> Type) = BackendMap (Map BackendType (AnyBackend i))
@ -31,6 +37,28 @@ deriving newtype instance i `SatisfiesForAllBackends` Show => Show (BackendMap i
deriving newtype instance i `SatisfiesForAllBackends` Eq => Eq (BackendMap i)
instance i `SatisfiesForAllBackends` FromJSON => FromJSON (BackendMap i) where
parseJSON =
Aeson.withObject "BackendMap" $ \obj -> do
BackendMap . Map.fromList
<$> traverse
( \(backendTypeStr, val) -> do
backendType <- parseBackendTypeFromText $ Key.toText backendTypeStr
(backendType,) <$> parseAnyBackendFromJSON backendType val
)
(KeyMap.toList obj)
instance i `SatisfiesForAllBackends` ToJSON => ToJSON (BackendMap i) where
toJSON (BackendMap backendMap) =
Aeson.object $ valueToPair <$> Map.elems backendMap
where
valueToPair :: AnyBackend i -> (Key, Aeson.Value)
valueToPair value = dispatchAnyBackend'' @ToJSON @HasTag value $ \(v :: i b) ->
let backendTypeText = Key.fromText . toTxt . reify $ backendTag @b
in (backendTypeText, Aeson.toJSON v)
--------------------------------------------------------------------------------
singleton :: forall b i. HasTag b => i b -> BackendMap i
singleton value = BackendMap $ Map.singleton (reify $ backendTag @b) (mkAnyBackend value)
@ -47,22 +75,26 @@ lookup (BackendMap backendMap) =
elems :: forall i. BackendMap i -> [AnyBackend i]
elems (BackendMap backendMap) = Map.elems backendMap
instance i `SatisfiesForAllBackends` FromJSON => FromJSON (BackendMap i) where
parseJSON =
withObject "BackendMap" $ \obj -> do
BackendMap . Map.fromList
<$> traverse
( \(backendTypeStr, val) -> do
backendType <- parseBackendTypeFromText $ K.toText backendTypeStr
(backendType,) <$> parseAnyBackendFromJSON backendType val
)
(KM.toList obj)
-- | The expression @modify f bmap@ alters the value @x@ at
-- @b@. @modify@ is a restricted version of 'alter' which cannot
-- delete entries and if there is no @b@ key present in the map, it
-- will apply the modification function to the @i b@ unit value and
-- insert the result at @b@.
modify :: forall b i. (HasTag b, Monoid (i b)) => (i b -> i b) -> BackendMap i -> BackendMap i
modify f = alter \case
Nothing -> Just $ f mempty
Just ab -> Just $ f ab
instance i `SatisfiesForAllBackends` ToJSON => ToJSON (BackendMap i) where
toJSON (BackendMap backendMap) =
object $ valueToPair <$> Map.elems backendMap
where
valueToPair :: AnyBackend i -> (K.Key, Value)
valueToPair value = dispatchAnyBackend'' @ToJSON @HasTag value $ \(v :: i b) ->
let backendTypeText = K.fromText . toTxt . reify $ backendTag @b
in (backendTypeText, J.toJSON v)
-- | The expression @alter f bmap@ alters the value @x@ at @b@, or
-- absence thereof. alter can be used to insert, delete, or update a
-- value in a Map.
--
-- In short : @lookup k (alter f k m) = f (lookup k m)@.
alter :: forall b i. HasTag b => (Maybe (i b) -> Maybe (i b)) -> BackendMap i -> BackendMap i
alter f (BackendMap bmap) = BackendMap $ Map.alter (wrap . f . unwrap) (reify @b backendTag) bmap
where
wrap :: Maybe (i b) -> Maybe (AnyBackend i)
wrap = fmap mkAnyBackend
unwrap :: Maybe (AnyBackend i) -> Maybe (i b)
unwrap x = x >>= unpackAnyBackend @b

View File

@ -26,6 +26,7 @@ import Hasura.RQL.DDL.Action
import Hasura.RQL.DDL.ApiLimit
import Hasura.RQL.DDL.ComputedField
import Hasura.RQL.DDL.CustomTypes
import Hasura.RQL.DDL.DataConnector
import Hasura.RQL.DDL.Endpoint
import Hasura.RQL.DDL.EventTrigger
import Hasura.RQL.DDL.GraphqlSchemaIntrospection
@ -42,6 +43,7 @@ import Hasura.RQL.DDL.RemoteSchema
import Hasura.RQL.DDL.ScheduledTrigger
import Hasura.RQL.DDL.Schema
import Hasura.RQL.DDL.Schema.Source
import Hasura.RQL.DDL.SourceKinds
import Hasura.RQL.DDL.Webhook.Transform.Validation
import Hasura.RQL.Types.Action
import Hasura.RQL.Types.Allowlist
@ -159,6 +161,10 @@ data RQLMetadataV1
| -- Rest endpoints
RMCreateRestEndpoint !CreateEndpoint
| RMDropRestEndpoint !DropEndpoint
| -- GraphQL Data Connectors
RMDCAddAgent !DCAddAgent
| RMDCDeleteAgent !DCDeleteAgent
| RMListSourceKinds !ListSourceKinds
| -- Custom types
RMSetCustomTypes !CustomTypes
| -- Api limits
@ -231,6 +237,9 @@ instance FromJSON RQLMetadataV1 where
"update_scope_of_collection_in_allowlist" -> RMUpdateScopeOfCollectionInAllowlist <$> args
"create_rest_endpoint" -> RMCreateRestEndpoint <$> args
"drop_rest_endpoint" -> RMDropRestEndpoint <$> args
"dc_add_agent" -> RMDCAddAgent <$> args
"dc_delete_agent" -> RMDCDeleteAgent <$> args
"list_source_kinds" -> RMListSourceKinds <$> args
"set_custom_types" -> RMSetCustomTypes <$> args
"set_api_limits" -> RMSetApiLimits <$> args
"remove_api_limits" -> pure RMRemoveApiLimits
@ -515,6 +524,9 @@ runMetadataQueryV1M env currentResourceVersion = \case
RMUpdateScopeOfCollectionInAllowlist q -> runUpdateScopeOfCollectionInAllowlist q
RMCreateRestEndpoint q -> runCreateEndpoint q
RMDropRestEndpoint q -> runDropEndpoint q
RMDCAddAgent q -> runAddDataConnectorAgent q
RMDCDeleteAgent q -> runDeleteDataConnectorAgent q
RMListSourceKinds q -> runListSourceKinds q
RMSetCustomTypes q -> runSetCustomTypes q
RMSetApiLimits q -> runSetApiLimits q
RMRemoveApiLimits -> runRemoveApiLimits

View File

@ -5,6 +5,7 @@ where
import Hasura.RQL.DDL.Action
import Hasura.RQL.DDL.ComputedField
import Hasura.RQL.DDL.DataConnector
import Hasura.RQL.DDL.EventTrigger
import Hasura.RQL.DDL.Metadata
import Hasura.RQL.DDL.Permission
@ -14,6 +15,7 @@ import Hasura.RQL.DDL.Relationship.Rename
import Hasura.RQL.DDL.RemoteRelationship
import Hasura.RQL.DDL.Schema
import Hasura.RQL.DDL.Schema.Source
import Hasura.RQL.DDL.SourceKinds
import Hasura.RQL.DDL.Webhook.Transform.Validation
import Hasura.RQL.Types.Allowlist
import Hasura.RQL.Types.ApiLimit
@ -115,6 +117,10 @@ data RQLMetadataV1
| -- Rest endpoints
RMCreateRestEndpoint !CreateEndpoint
| RMDropRestEndpoint !DropEndpoint
| -- GraphQL Data Connectors
RMDCAddAgent !DCAddAgent
| RMDCDeleteAgent !DCDeleteAgent
| RMListSourceKinds !ListSourceKinds
| -- Custom types
RMSetCustomTypes !CustomTypes
| -- Api limits

View File

@ -0,0 +1,12 @@
- description: Test DC Add Agent
url: /v1/metadata
headers:
X-Hasura-Role: admin
status: 200
response:
message: success
query:
type: dc_add_agent
args:
name: test_agent
url: "http://www.google.com"

View File

@ -0,0 +1,24 @@
- description: Test GDC Add Agent
url: /v1/metadata
headers:
X-Hasura-Role: admin
status: 200
response:
message: success
query:
type: dc_add_agent
args:
name: test_agent
url: "http://www.google.com"
- description: Test DC Delete Agent
url: /v1/metadata
headers:
X-Hasura-Role: admin
status: 200
response:
message: success
query:
type: dc_delete_agent
args:
name: test_agent

View File

@ -0,0 +1,20 @@
- description: Test List Source Kinds
url: /v1/metadata
headers:
X-Hasura-Role: admin
status: 200
response:
sources:
- builtin: true
kind: pg
- builtin: true
kind: citus
- builtin: true
kind: mssql
- builtin: true
kind: bigquery
- builtin: true
kind: mysql
query:
type: list_source_kinds
args: {}

View File

@ -398,6 +398,15 @@ class TestMetadata:
def test_pg_multisource_table_name_conflict(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/pg_multisource_table_name_conflict.yaml')
def test_dc_add_agent(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/test_dc_add_agent.yaml')
def test_dc_delete_agent(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/test_dc_delete_agent.yaml')
def test_list_source_kinds(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/test_list_source_kinds.yaml')
@classmethod
def dir(cls):
return "queries/v1/metadata"