graphql-engine/server/src-lib/Hasura/RQL/DDL/RemoteRelationship.hs
Antoine Leblanc aaadd2c81f Build schema cache for relationships from remote schemas
## Description

This PR is a subset of #3069, that does roughly that #3031 was aiming to do: add the schema cache building phase for relationships from remote servers. This PR does not change any of the code that *uses* remote relationships, meaning we ignore the added schema cache information. It also contains dependency-tracking code, which was originally missing from #3031; in turn, this pulls some of the metadata API as well, since we identify remote relationships by how they were created.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3540
GitOrigin-RevId: ed962b6d07fd4adbf0a71e0d79736a4e8b422fea
2022-02-03 18:59:32 +00:00

375 lines
16 KiB
Haskell

{-# LANGUAGE UndecidableInstances #-}
module Hasura.RQL.DDL.RemoteRelationship
( CreateFromSourceRelationship (..),
runCreateRemoteRelationship,
runDeleteRemoteRelationship,
runUpdateRemoteRelationship,
DeleteFromSourceRelationship (..),
dropRemoteRelationshipInMetadata,
PartiallyResolvedSource (..),
buildRemoteFieldInfo,
CreateRemoteSchemaRemoteRelationship (..),
runCreateRemoteSchemaRemoteRelationship,
runUpdateRemoteSchemaRemoteRelationship,
DeleteRemoteSchemaRemoteRelationship (..),
runDeleteRemoteSchemaRemoteRelationship,
getRemoteSchemaEntityJoinColumns,
)
where
import Control.Lens (at, non)
import Control.Lens.Extended (to, (^?))
import Data.Aeson (FromJSON (..), ToJSON (..), (.!=), (.:), (.:?), (.=))
import Data.Aeson qualified as J
import Data.Aeson.TH qualified as J
import Data.HashMap.Strict qualified as Map
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.Text.Extended ((<<>), (<>>))
import Hasura.Base.Error
( Code (NotExists, NotFound, NotSupported, RemoteSchemaError),
QErr,
QErrM,
runAesonParser,
throw400,
)
import Hasura.EncJSON (EncJSON)
import Hasura.Prelude
import Hasura.RQL.DDL.RemoteRelationship.Validate
( errorToText,
validateToSchemaRelationship,
)
import Hasura.RQL.Types
import Hasura.SQL.AnyBackend qualified as AB
import Language.GraphQL.Draft.Syntax qualified as G
--------------------------------------------------------------------------------
-- Create or update relationship from source
-- | Argument to the @_create_remote_relationship@ and
-- @_update_remote_relationship@ families of metadata commands.
--
-- For historical reason, this type is also used to represent a db-to-rs schema
-- in the metadata.
data CreateFromSourceRelationship (b :: BackendType) = CreateFromSourceRelationship
{ _crrSource :: SourceName,
_crrTable :: TableName b,
_crrName :: RelName,
_crrDefinition :: RemoteRelationshipDefinition
}
deriving stock instance (Eq (TableName b)) => Eq (CreateFromSourceRelationship b)
deriving stock instance (Show (TableName b)) => Show (CreateFromSourceRelationship b)
instance Backend b => FromJSON (CreateFromSourceRelationship b) where
parseJSON = J.withObject "CreateFromSourceRelationship" $ \o -> do
_crrSource <- o .:? "source" .!= defaultSource
_crrTable <- o .: "table"
_crrName <- o .: "name"
-- In the old format, the definition is inlined; in the new format, the
-- definition is in the "definition" object, and we don't allow legacy
-- fields to appear under it.
remoteSchema :: Maybe J.Value <- o .:? "remote_schema"
definition <- o .:? "definition"
_crrDefinition <- case (remoteSchema, definition) of
-- old format
(Just {}, Nothing) -> parseRemoteRelationshipDefinition RRPLegacy $ J.Object o
-- new format
(Nothing, Just def) -> parseRemoteRelationshipDefinition RRPStrict def
-- both or neither
_ -> fail "create_remote_relationship expects exactly one of: remote_schema, definition"
pure $ CreateFromSourceRelationship {..}
instance (Backend b) => ToJSON (CreateFromSourceRelationship b) where
toJSON CreateFromSourceRelationship {..} =
-- We need to introspect the definition, to know whether we need to inline
-- it, or if it needs to be in a distinct "definition" object.
J.object $ case _crrDefinition of
-- old format
RelationshipToSchema RRFOldDBToRemoteSchema _ ->
case J.toJSON _crrDefinition of
-- The result of this serialization will be an empty list if this
-- conversion fails (which it should _never_ do), in which case those
-- fields will be omitted from the serialized JSON. This could only
-- happen if the ToJSON instance of RemoteRelationshipDefinition were
-- changed to return something that isn't an object.
J.Object obj -> commonFields <> Map.toList obj
_ -> []
-- new format
_ -> ("definition" .= _crrDefinition) : commonFields
where
commonFields =
[ "source" .= _crrSource,
"table" .= _crrTable,
"name" .= _crrName
]
runCreateRemoteRelationship ::
forall b m.
(MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b) =>
CreateFromSourceRelationship b ->
m EncJSON
runCreateRemoteRelationship CreateFromSourceRelationship {..} = do
void $ askTabInfo @b _crrSource _crrTable
let metadataObj =
MOSourceObjId _crrSource $
AB.mkAnyBackend $
SMOTableObj @b _crrTable $
MTORemoteRelationship _crrName
buildSchemaCacheFor metadataObj $
MetadataModifier $
tableMetadataSetter @b _crrSource _crrTable . tmRemoteRelationships
%~ OMap.insert _crrName (RemoteRelationship _crrName _crrDefinition)
pure successMsg
runUpdateRemoteRelationship ::
forall b m.
(MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b) =>
CreateFromSourceRelationship b ->
m EncJSON
runUpdateRemoteRelationship CreateFromSourceRelationship {..} = do
fieldInfoMap <- askFieldInfoMap @b _crrSource _crrTable
let metadataObj =
MOSourceObjId _crrSource $
AB.mkAnyBackend $
SMOTableObj @b _crrTable $
MTORemoteRelationship _crrName
void $ askRemoteRel fieldInfoMap _crrName
buildSchemaCacheFor metadataObj $
MetadataModifier $
tableMetadataSetter @b _crrSource _crrTable . tmRemoteRelationships
%~ OMap.insert _crrName (RemoteRelationship _crrName _crrDefinition)
pure successMsg
--------------------------------------------------------------------------------
-- Drop relationship from source
-- | Argument to the @_drop_remote_relationship@ family of metadata commands.
data DeleteFromSourceRelationship (b :: BackendType) = DeleteFromSourceRelationship
{ _drrSource :: SourceName,
_drrTable :: TableName b,
_drrName :: RelName
}
instance Backend b => FromJSON (DeleteFromSourceRelationship b) where
parseJSON = J.withObject "DeleteFromSourceRelationship" $ \o ->
DeleteFromSourceRelationship
<$> o .:? "source" .!= defaultSource
<*> o .: "table"
<*> o .: "name"
runDeleteRemoteRelationship ::
forall b m.
(BackendMetadata b, MonadError QErr m, CacheRWM m, MetadataM m) =>
DeleteFromSourceRelationship b ->
m EncJSON
runDeleteRemoteRelationship (DeleteFromSourceRelationship source table relName) = do
fieldInfoMap <- askFieldInfoMap @b source table
void $ askRemoteRel fieldInfoMap relName
let metadataObj =
MOSourceObjId source $
AB.mkAnyBackend $
SMOTableObj @b table $
MTORemoteRelationship relName
buildSchemaCacheFor metadataObj $
MetadataModifier $
tableMetadataSetter @b source table %~ dropRemoteRelationshipInMetadata relName
pure successMsg
--------------------------------------------------------------------------------
-- Create relationship from remote schema
data CreateRemoteSchemaRemoteRelationship = CreateRemoteSchemaRemoteRelationship
{ _crsrrRemoteSchema :: RemoteSchemaName,
_crsrrType :: G.Name,
_crsrrName :: RelName,
_crsrrDefinition :: RemoteRelationshipDefinition
}
deriving (Generic)
instance FromJSON CreateRemoteSchemaRemoteRelationship where
parseJSON = J.withObject "CreateRemoteSchemaRemoteRelationship" $ \o ->
CreateRemoteSchemaRemoteRelationship
<$> o .: "remote_schema"
<*> o .: "type_name"
<*> o .: "name"
<*> (o .: "definition" >>= parseRemoteRelationshipDefinition RRPStrict)
$(J.deriveToJSON hasuraJSON ''CreateRemoteSchemaRemoteRelationship)
runCreateRemoteSchemaRemoteRelationship ::
forall m.
(MonadError QErr m, CacheRWM m, MetadataM m) =>
CreateRemoteSchemaRemoteRelationship ->
m EncJSON
runCreateRemoteSchemaRemoteRelationship CreateRemoteSchemaRemoteRelationship {..} = do
let metadataObj =
MORemoteSchemaRemoteRelationship _crsrrRemoteSchema _crsrrType _crsrrName
buildSchemaCacheFor metadataObj $
MetadataModifier $
metaRemoteSchemas . ix _crsrrRemoteSchema . rsmRemoteRelationships
. at _crsrrType
. non (RemoteSchemaTypeRelationships _crsrrType mempty)
. rstrsRelationships
%~ OMap.insert _crsrrName (RemoteRelationship _crsrrName _crsrrDefinition)
pure successMsg
runUpdateRemoteSchemaRemoteRelationship ::
forall m.
(MonadError QErr m, CacheRWM m, MetadataM m) =>
CreateRemoteSchemaRemoteRelationship ->
m EncJSON
runUpdateRemoteSchemaRemoteRelationship crss@CreateRemoteSchemaRemoteRelationship {..} = do
schemaCache <- askSchemaCache
let remoteRelationship =
schemaCache
^? to scRemoteSchemas
. ix _crsrrRemoteSchema
. rscRemoteRelationships
. ix _crsrrType
. ix _crsrrName
void $
onNothing remoteRelationship $
throw400 NotExists $
"no relationship defined on remote schema " <>> _crsrrRemoteSchema <<> " with name " <>> _crsrrName
runCreateRemoteSchemaRemoteRelationship crss
--------------------------------------------------------------------------------
-- Drop relationship from remote schema
-- | Argument to the @_drop_remote_relationship@ family of metadata commands.
data DeleteRemoteSchemaRemoteRelationship = DeleteRemoteSchemaRemoteRelationship
{ _drsrrRemoteSchema :: RemoteSchemaName,
_drsrrTypeName :: G.Name,
_drsrrName :: RelName
}
instance FromJSON DeleteRemoteSchemaRemoteRelationship where
parseJSON = J.withObject "DeleteRemoteSchemaRemoteRelationship" $ \o ->
DeleteRemoteSchemaRemoteRelationship
<$> o .: "remote_schema"
<*> o .: "type_name"
<*> o .: "name"
runDeleteRemoteSchemaRemoteRelationship ::
forall m.
(MonadError QErr m, CacheRWM m, MetadataM m) =>
DeleteRemoteSchemaRemoteRelationship ->
m EncJSON
runDeleteRemoteSchemaRemoteRelationship DeleteRemoteSchemaRemoteRelationship {..} = do
let relName = _drsrrName
metadataObj =
MORemoteSchemaRemoteRelationship _drsrrRemoteSchema _drsrrTypeName relName
buildSchemaCacheFor metadataObj $
MetadataModifier $
metaRemoteSchemas . ix _drsrrRemoteSchema . rsmRemoteRelationships . ix _drsrrTypeName . rstrsRelationships
%~ OMap.delete relName
pure successMsg
--------------------------------------------------------------------------------
-- Schema cache building (TODO: move this elsewere!)
-- | Internal intermediary step.
--
-- We build the output of sources in two steps:
-- 1. we first resolve sources, and collect the core info of their tables
-- 2. we then build the entire output from the collection of partially resolved sources
--
-- We need this split to be able to resolve cross-source relationships: to process one source's
-- remote relationship, we need to know about the target source's tables core info.
--
-- This data structure is used as an argument to @AnyBackend@ in the backend-agnostic intermediary
-- collection, and used here to build remote field info.
data PartiallyResolvedSource b = PartiallyResolvedSource
{ _prsSourceMetadata :: !(SourceMetadata b),
_resolvedSource :: !(ResolvedSource b),
_tableCoreInfoMap :: !(HashMap (TableName b) (TableCoreInfoG b (ColumnInfo b) (ColumnInfo b)))
}
-- | Builds the schema cache representation of a remote relationship
-- TODO: this is not actually called by the remote relationship DDL API and is only used as part of
-- the schema cache process. Should this be moved elsewhere?
buildRemoteFieldInfo ::
QErrM m =>
-- | The entity on which the remote relationship is defined
LHSIdentifier ->
-- | join fields provided by the LHS entity
Map.HashMap FieldName lhsJoinField ->
-- | definition of remote relationship
RemoteRelationship ->
-- | Required context to process cross boundary relationships
HashMap SourceName (AB.AnyBackend PartiallyResolvedSource) ->
-- | Required context to process cross boundary relationships
RemoteSchemaMap ->
-- | returns
-- 1. schema cache representation of the remote relationships
-- 2. the dependencies on the RHS of the join. The dependencies
-- on the LHS entities has to be handled by the calling function
m (RemoteFieldInfo lhsJoinField, [SchemaDependency])
buildRemoteFieldInfo lhsIdentifier lhsJoinFields RemoteRelationship {..} allSources remoteSchemaMap =
case _rrDefinition of
RelationshipToSource ToSourceRelationshipDef {..} -> do
targetTables <-
Map.lookup _tsrdSource allSources
`onNothing` throw400 NotFound ("source not found: " <>> _tsrdSource)
AB.dispatchAnyBackend @Backend targetTables \(partiallyResolvedSource :: PartiallyResolvedSource b') -> do
let PartiallyResolvedSource _ targetSourceInfo targetTablesInfo = partiallyResolvedSource
(targetTable :: TableName b') <- runAesonParser J.parseJSON _tsrdTable
targetColumns <-
fmap _tciFieldInfoMap $
onNothing (Map.lookup targetTable targetTablesInfo) $ throwTableDoesNotExist @b' targetTable
-- TODO: rhs fields should also ideally be DBJoinFields
columnPairs <- for (Map.toList _tsrdFieldMapping) \(srcFieldName, tgtFieldName) -> do
lhsJoinField <- askFieldInfo lhsJoinFields srcFieldName
tgtField <- askFieldInfo targetColumns tgtFieldName
pure (srcFieldName, lhsJoinField, tgtField)
columnMapping <- for columnPairs \(srcFieldName, srcColumn, tgtColumn) -> do
tgtScalar <- case ciType tgtColumn of
ColumnScalar scalarType -> pure scalarType
ColumnEnumReference _ -> throw400 NotSupported "relationships to enum fields are not supported yet"
pure (srcFieldName, (srcColumn, tgtScalar, ciColumn tgtColumn))
let sourceConfig = _rsConfig targetSourceInfo
sourceCustomization = _rsCustomization targetSourceInfo
rsri =
RemoteSourceFieldInfo _rrName _tsrdRelationshipType _tsrdSource sourceConfig sourceCustomization targetTable $
fmap (\(_, tgtType, tgtColumn) -> (tgtType, tgtColumn)) $ Map.fromList columnMapping
rhsDependencies =
SchemaDependency (SOSourceObj _tsrdSource $ AB.mkAnyBackend $ SOITable @b' targetTable) DRTable :
flip map columnPairs \(_, _srcColumn, tgtColumn) ->
SchemaDependency
( SOSourceObj _tsrdSource $
AB.mkAnyBackend $ SOITableObj @b' targetTable $ TOCol @b' $ ciColumn tgtColumn
)
DRRemoteRelationship
requiredLHSJoinFields = fmap (\(srcField, _, _) -> srcField) $ Map.fromList columnMapping
pure (RemoteFieldInfo requiredLHSJoinFields $ RFISource $ AB.mkAnyBackend @b' rsri, rhsDependencies)
RelationshipToSchema _ remoteRelationship@ToSchemaRelationshipDef {..} -> do
RemoteSchemaCtx {..} <-
onNothing (Map.lookup _trrdRemoteSchema remoteSchemaMap) $
throw400 RemoteSchemaError $ "remote schema with name " <> _trrdRemoteSchema <<> " not found"
(requiredLHSJoinFields, remoteField) <-
validateToSchemaRelationship remoteRelationship lhsIdentifier _rrName (_rscInfo, _rscIntroOriginal) lhsJoinFields
`onLeft` (throw400 RemoteSchemaError . errorToText)
let rhsDependencies = [SchemaDependency (SORemoteSchema _trrdRemoteSchema) DRRemoteSchema]
pure (RemoteFieldInfo requiredLHSJoinFields $ RFISchema remoteField, rhsDependencies)
getRemoteSchemaEntityJoinColumns ::
(MonadError QErr m) =>
RemoteSchemaName ->
RemoteSchemaIntrospection ->
G.Name ->
m (HashMap FieldName G.Name)
getRemoteSchemaEntityJoinColumns remoteSchemaName introspection typeName = do
typeDefinition <-
onNothing (lookupType introspection typeName) $
throw400 NotFound ("no such type defined in remote schema: " <>> remoteSchemaName)
case typeDefinition of
G.TypeDefinitionObject objectDefinition ->
pure $
Map.fromList $ do
fieldDefinition <- G._otdFieldsDefinition objectDefinition
guard $ null $ G._fldArgumentsDefinition fieldDefinition
pure (FieldName $ G.unName $ G._fldName fieldDefinition, G._fldName fieldDefinition)
_ -> throw400 NotSupported "remote relationships on a remote schema can only be defined on object types"