2023-01-09 10:25:32 +03:00
{- # LANGUAGE DerivingStrategies # -}
{- # LANGUAGE OverloadedLists # -}
-- | This module provides an API for suggesting relationships so that
-- the console (or client) does not need to construct and submit relationship queries itself.
--
-- This suggests reciprocal object relationships A -> object -> B -> object -> A if there is a unique
-- constraint on the column(s) in A mapping A->B, and if not then a reciprocal array relationship
-- A -> object -> B -> array -> A is suggested.
--
-- All JSON fields to the main exported function `runSuggestRels` are optional and behave as follows:
--
-- * _srsSource: The source to suggest relationships for - Defaults to `defaultSource`
-- * _srsTables: The tables to suggest relationships between - Defaults to all tables
-- * _srsOmitTracked: Only suggest untracked relationships - Defaults to False
--
-- Autodocodec Codecs instances are implemented for these datatypes.
module Hasura.RQL.DDL.Relationship.Suggest
( SuggestRels ,
runSuggestRels ,
)
where
import Autodocodec
import Autodocodec.OpenAPI ( )
import Control.Lens ( preview )
2023-01-24 13:26:08 +03:00
import Data.Aeson qualified as Aeson
2023-01-09 10:25:32 +03:00
import Data.HashMap.Strict qualified as Map
import Data.HashMap.Strict.NonEmpty qualified as MapNE
import Data.HashSet qualified as H
import Data.OpenApi ( ToSchema ( .. ) )
2023-02-10 04:51:39 +03:00
import Data.Tuple ( swap )
2023-01-09 10:25:32 +03:00
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.Prelude
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Metadata.Backend
import Hasura.RQL.Types.Relationships.Local ( RelInfo ( riMapping , riRTable ) )
import Hasura.RQL.Types.SchemaCache
import Hasura.RQL.Types.SchemaCache.Build
2023-01-24 13:26:08 +03:00
import Hasura.RQL.Types.Table ( ForeignKey , UniqueConstraint , _cName , _fkColumnMapping , _fkConstraint , _fkForeignTable , _ucColumns )
2023-01-09 10:25:32 +03:00
-- | Datatype used by Metadata API to represent Request for Suggested Relationships
data SuggestRels b = SuggestRels
{ _srsSource :: SourceName ,
_srsTables :: Maybe [ TableName b ] ,
_srsOmitTracked :: Bool
}
deriving ( Generic )
2023-01-24 13:26:08 +03:00
deriving ( Aeson . FromJSON , Aeson . ToJSON , ToSchema ) via Autodocodec ( SuggestRels b )
2023-01-09 10:25:32 +03:00
instance Backend b => HasCodec ( SuggestRels b ) where
codec =
object
" SuggestRels "
( SuggestRels
<$> optionalFieldWithOmittedDefault " source " defaultSource " The source to suggest relationships for - Defaults to 'default'. "
.= _srsSource
<*> optionalFieldOrNull " tables " " The list of tables to suggest relationships for - Defaults to all tracked tables. "
.= _srsTables
<*> optionalFieldWithOmittedDefault " omit_tracked " False " Determines if currently tracked relationships should be ommited from suggestions - Defaults to false. "
.= _srsOmitTracked
)
<??> [ " API call to request suggestions for relationships " ]
newtype SuggestedRelationships b = Relationships
{ sRelationships :: [ Relationship b ]
}
deriving ( Generic )
2023-01-24 13:26:08 +03:00
deriving ( Aeson . FromJSON , Aeson . ToJSON , ToSchema ) via Autodocodec ( SuggestedRelationships b )
2023-01-09 10:25:32 +03:00
instance Backend b => HasCodec ( SuggestedRelationships b ) where
codec =
object
" SuggestedRelationships "
( Relationships
<$> requiredField' " relationships "
.= sRelationships
)
data Relationship b = Relationship
{ rType :: RelType ,
rFrom :: Mapping b ,
rTo :: Mapping b
}
deriving ( Generic )
2023-01-24 13:26:08 +03:00
deriving ( Aeson . FromJSON , Aeson . ToJSON , ToSchema ) via Autodocodec ( Relationship b )
2023-01-09 10:25:32 +03:00
instance Backend b => HasCodec ( Relationship b ) where
codec =
object
" Relationship "
( Relationship
<$> requiredField' " type "
.= rType
<*> requiredField' " from "
.= rFrom
<*> requiredField' " to "
.= rTo
)
data Mapping b = Mapping
{ mTable :: TableName b ,
2023-01-24 13:26:08 +03:00
mColumns :: [ Column b ] ,
mConstraintName :: Maybe Aeson . Value
2023-01-09 10:25:32 +03:00
}
deriving ( Generic )
2023-01-24 13:26:08 +03:00
deriving ( Aeson . FromJSON , Aeson . ToJSON , ToSchema ) via Autodocodec ( Mapping b )
2023-01-09 10:25:32 +03:00
instance Backend b => HasCodec ( Mapping b ) where
codec =
object
" Mapping "
( Mapping
<$> requiredField' " table "
.= mTable
<*> requiredField' " columns "
.= mColumns
2023-01-24 13:26:08 +03:00
<*> optionalFieldOrNull' " constraint_name "
.= mConstraintName
2023-01-09 10:25:32 +03:00
)
-- | Most of the heavy lifting for this module occurs in this function.
-- Suggests reciprocal relationships for foreign keys.
-- Incorporates logic to omit previously-tracked relationships
-- and only considers required tables.
suggestRelsFK ::
forall b .
Backend b =>
-- | Omits currently tracked relationships from recommendations if True.
Bool ->
HashMap ( TableName b ) ( TableCoreInfo b ) ->
TableName b ->
HashSet ( UniqueConstraint b ) ->
H . HashSet ( TableName b , HashMap ( Column b ) ( Column b ) ) ->
2023-01-24 13:26:08 +03:00
( TableName b -> Bool ) ->
2023-01-09 10:25:32 +03:00
ForeignKey b ->
[ Relationship b ]
2023-02-10 04:51:39 +03:00
suggestRelsFK omitTracked tables name uniqueConstraints tracked predicate foreignKey
| not ( predicate name || predicate relatedTableName ) = [] -- Neither table appears in tables list
| isNothing relatedTable = [] -- There is no information for the related table
| omitTracked = catMaybes [ discard toTracked toRelationship , discard fromTracked fromRelationship ] -- Discard tracked relationships if that's requested
| otherwise = [ toRelationship , fromRelationship ] -- Otherwise, return the reciprocal relationships
2023-01-09 10:25:32 +03:00
where
2023-02-10 04:51:39 +03:00
toTracked = H . member ( relatedTableName , columnRelationships ) tracked
fromTracked = H . member ( name , invert columnRelationships ) trackedBack
toRelationship =
Relationship
{ rType = ObjRel ,
rFrom = Mapping { mTable = name , mColumns = localColumns , mConstraintName = Just constraintName } ,
rTo = Mapping { mTable = relatedTableName , mColumns = relatedColumns , mConstraintName = Nothing }
}
fromRelationship =
Relationship
{ rType = if H . fromList localColumns ` H . member ` uniqueConstraintColumns then ObjRel else ArrRel ,
rTo = Mapping { mTable = name , mColumns = localColumns , mConstraintName = Just constraintName } ,
rFrom = Mapping { mTable = relatedTableName , mColumns = relatedColumns , mConstraintName = Nothing }
}
2023-01-09 10:25:32 +03:00
columnRelationships = MapNE . toHashMap ( _fkColumnMapping foreignKey )
localColumns = Map . keys columnRelationships
relatedColumns = Map . elems columnRelationships
uniqueConstraintColumns = H . map _ucColumns uniqueConstraints
2023-02-10 04:51:39 +03:00
relatedTableName = _fkForeignTable foreignKey
relatedTable = Map . lookup relatedTableName tables
2023-01-24 13:26:08 +03:00
constraintName = Aeson . toJSON ( _cName ( _fkConstraint foreignKey ) )
2023-02-10 04:51:39 +03:00
discard b x = bool Nothing ( Just x ) ( not b )
invert = Map . fromList . map swap . Map . toList
trackedBack = H . fromList $ mapMaybe ( relationships ( riRTable &&& riMapping ) ) $ maybe [] ( Map . elems . _tciFieldInfoMap ) relatedTable
2023-01-09 10:25:32 +03:00
suggestRelsTable ::
forall b .
Backend b =>
Bool ->
HashMap ( TableName b ) ( TableCoreInfo b ) ->
2023-01-24 13:26:08 +03:00
( TableName b -> Bool ) ->
2023-01-09 10:25:32 +03:00
( TableName b , TableCoreInfo b ) ->
[ Relationship b ]
2023-01-24 13:26:08 +03:00
suggestRelsTable omitTracked tables predicate ( name , table ) =
suggestRelsFK omitTracked tables name constraints tracked predicate =<< toList foreignKeys
2023-01-09 10:25:32 +03:00
where
foreignKeys = _tciForeignKeys table
constraints = _tciUniqueConstraints table
tracked = H . fromList $ mapMaybe ( relationships ( riRTable &&& riMapping ) ) $ Map . elems $ _tciFieldInfoMap table
2023-02-10 04:51:39 +03:00
relationships :: ( RelInfo b1 -> b2 ) -> FieldInfo b1 -> Maybe b2
relationships f = fmap f . preview _FIRelationship
2023-01-09 10:25:32 +03:00
-- NOTE: This could be grouped by table instead of a list, console stakeholders are happy with this being a list.
suggestRelsResponse ::
forall b .
Backend b =>
Bool ->
HashMap ( TableName b ) ( TableCoreInfo b ) ->
2023-01-24 13:26:08 +03:00
( TableName b -> Bool ) ->
2023-01-09 10:25:32 +03:00
SuggestedRelationships b
2023-01-24 13:26:08 +03:00
suggestRelsResponse omitTracked tables predicate =
2023-01-09 10:25:32 +03:00
Relationships $
2023-01-24 13:26:08 +03:00
suggestRelsTable omitTracked tables predicate =<< Map . toList tables
2023-01-09 10:25:32 +03:00
2023-01-24 13:26:08 +03:00
tablePredicate :: Hashable a => Maybe [ a ] -> a -> Bool
tablePredicate Nothing _ = True
tablePredicate ( Just ns ) n = n ` H . member ` hash
where
hash = H . fromList ns
2023-01-09 10:25:32 +03:00
-- | The method invoked when dispatching on metadata calls in POST /v1/metadata
runSuggestRels ::
forall b m .
( MonadError QErr m , CacheRWM m , BackendMetadata b ) =>
SuggestRels b ->
m EncJSON
runSuggestRels ( SuggestRels source tablesM omitExistingB ) = do
tableCacheM <- fmap ( fmap ( _tiCoreInfo ) ) <$> askTableCache @ b source
case tableCacheM of
Nothing -> throw500 " Couldn't find any schema source information "
2023-01-24 13:26:08 +03:00
Just tableCache -> pure $ encJFromJValue $ suggestRelsResponse @ b omitExistingB tableCache ( tablePredicate tablesM )