From 615fd64c04281a28933e854433f9d106f92168c3 Mon Sep 17 00:00:00 2001 From: Lyndon Maydwell Date: Tue, 24 Jan 2023 20:26:08 +1000 Subject: [PATCH] Add `constraint_name` key to `*_suggest_relationships` Metadata API and increase sensitivity of `tables` argument PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7625 GitOrigin-RevId: d764f1664b63abbe4a4ff166e0bc7224bcb0dc57 --- .../API/Metadata/SuggestRelationshipsSpec.hs | 51 ++++++++++++-- .../Hasura/RQL/DDL/Relationship/Suggest.hs | 68 +++++++++++-------- 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/server/lib/api-tests/src/Test/API/Metadata/SuggestRelationshipsSpec.hs b/server/lib/api-tests/src/Test/API/Metadata/SuggestRelationshipsSpec.hs index 3aa34ce5421..d7d14112939 100644 --- a/server/lib/api-tests/src/Test/API/Metadata/SuggestRelationshipsSpec.hs +++ b/server/lib/api-tests/src/Test/API/Metadata/SuggestRelationshipsSpec.hs @@ -11,7 +11,7 @@ import Harness.Quoter.Yaml (yaml) import Harness.Test.Fixture qualified as Fixture import Harness.Test.Schema qualified as Schema import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment, getBackendTypeConfig) -import Harness.Yaml (mapObject, shouldReturnYaml, shouldReturnYamlF, sortArray) +import Harness.Yaml (mapObject, shouldReturnYamlF, sortArray) import Hasura.Prelude import Test.Hspec (SpecWith, describe, it) @@ -132,7 +132,8 @@ tests opts = do sourceName = Fixture.backendSourceName backendTypeMetadata schemaName = Schema.unSchemaName $ Schema.getSchemaName testEnv - shouldReturnYaml + shouldReturnYamlF + (pure . mapObject sortArray) opts ( GraphqlEngine.postMetadataWithStatus 200 @@ -153,7 +154,8 @@ tests opts = do relationships: - from: columns: - - publication_id + - author_id + constraint_name: article_author_id_fkey table: name: article schema: hasura @@ -161,9 +163,23 @@ tests opts = do columns: - id table: - name: publication + name: author schema: hasura type: object + - from: + columns: + - id + table: + name: author + schema: hasura + to: + columns: + - author_id + constraint_name: article_author_id_fkey + table: + name: article + schema: hasura + type: array - from: columns: - id @@ -173,17 +189,33 @@ tests opts = do to: columns: - publication_id + constraint_name: article_publication_id_fkey table: name: article schema: hasura type: object + - from: + columns: + - publication_id + constraint_name: article_publication_id_fkey + table: + name: article + schema: hasura + to: + columns: + - id + table: + name: publication + schema: hasura + type: object |] it "Omits tracked relationships if that is requested" $ \testEnv -> do let backendTypeMetadata = Maybe.fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnv sourceName = Fixture.backendSourceName backendTypeMetadata - shouldReturnYaml + shouldReturnYamlF + (pure . mapObject sortArray) opts ( GraphqlEngine.postMetadataWithStatus 200 @@ -201,6 +233,7 @@ tests opts = do - from: columns: - author_id + constraint_name: article_author_id_fkey table: name: article schema: hasura @@ -220,6 +253,7 @@ tests opts = do to: columns: - author_id + constraint_name: article_author_id_fkey table: name: article schema: hasura @@ -230,7 +264,8 @@ tests opts = do let backendTypeMetadata = Maybe.fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnv sourceName = Fixture.backendSourceName backendTypeMetadata - shouldReturnYaml + shouldReturnYamlF + (pure . mapObject sortArray) opts ( GraphqlEngine.postMetadataWithStatus 200 @@ -267,6 +302,7 @@ tests opts = do - from: columns: - author_id + constraint_name: article_author_id_fkey table: name: article schema: hasura @@ -286,6 +322,7 @@ tests opts = do to: columns: - author_id + constraint_name: article_author_id_fkey table: name: article schema: hasura @@ -299,6 +336,7 @@ tests opts = do to: columns: - publication_id + constraint_name: article_publication_id_fkey table: name: article schema: hasura @@ -306,6 +344,7 @@ tests opts = do - from: columns: - publication_id + constraint_name: article_publication_id_fkey table: name: article schema: hasura diff --git a/server/src-lib/Hasura/RQL/DDL/Relationship/Suggest.hs b/server/src-lib/Hasura/RQL/DDL/Relationship/Suggest.hs index cc946d11b7c..6d8ed009ddf 100644 --- a/server/src-lib/Hasura/RQL/DDL/Relationship/Suggest.hs +++ b/server/src-lib/Hasura/RQL/DDL/Relationship/Suggest.hs @@ -24,7 +24,7 @@ where import Autodocodec import Autodocodec.OpenAPI () import Control.Lens (preview) -import Data.Aeson (FromJSON (), ToJSON ()) +import Data.Aeson qualified as Aeson import Data.HashMap.Strict qualified as Map import Data.HashMap.Strict.NonEmpty qualified as MapNE import Data.HashSet qualified as H @@ -38,7 +38,7 @@ 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 -import Hasura.RQL.Types.Table (ForeignKey, UniqueConstraint, _fkColumnMapping, _fkForeignTable, _ucColumns) +import Hasura.RQL.Types.Table (ForeignKey, UniqueConstraint, _cName, _fkColumnMapping, _fkConstraint, _fkForeignTable, _ucColumns) -- | Datatype used by Metadata API to represent Request for Suggested Relationships data SuggestRels b = SuggestRels @@ -47,7 +47,7 @@ data SuggestRels b = SuggestRels _srsOmitTracked :: Bool } deriving (Generic) - deriving (FromJSON, ToJSON, ToSchema) via Autodocodec (SuggestRels b) + deriving (Aeson.FromJSON, Aeson.ToJSON, ToSchema) via Autodocodec (SuggestRels b) instance Backend b => HasCodec (SuggestRels b) where codec = @@ -67,7 +67,7 @@ newtype SuggestedRelationships b = Relationships { sRelationships :: [Relationship b] } deriving (Generic) - deriving (FromJSON, ToJSON, ToSchema) via Autodocodec (SuggestedRelationships b) + deriving (Aeson.FromJSON, Aeson.ToJSON, ToSchema) via Autodocodec (SuggestedRelationships b) instance Backend b => HasCodec (SuggestedRelationships b) where codec = @@ -84,7 +84,7 @@ data Relationship b = Relationship rTo :: Mapping b } deriving (Generic) - deriving (FromJSON, ToJSON, ToSchema) via Autodocodec (Relationship b) + deriving (Aeson.FromJSON, Aeson.ToJSON, ToSchema) via Autodocodec (Relationship b) instance Backend b => HasCodec (Relationship b) where codec = @@ -101,10 +101,11 @@ instance Backend b => HasCodec (Relationship b) where data Mapping b = Mapping { mTable :: TableName b, - mColumns :: [Column b] + mColumns :: [Column b], + mConstraintName :: Maybe Aeson.Value } deriving (Generic) - deriving (FromJSON, ToJSON, ToSchema) via Autodocodec (Mapping b) + deriving (Aeson.FromJSON, Aeson.ToJSON, ToSchema) via Autodocodec (Mapping b) instance Backend b => HasCodec (Mapping b) where codec = @@ -115,6 +116,8 @@ instance Backend b => HasCodec (Mapping b) where .= mTable <*> requiredField' "columns" .= mColumns + <*> optionalFieldOrNull' "constraint_name" + .= mConstraintName ) -- | Most of the heavy lifting for this module occurs in this function. @@ -130,40 +133,45 @@ suggestRelsFK :: TableName b -> HashSet (UniqueConstraint b) -> H.HashSet (TableName b, HashMap (Column b) (Column b)) -> + (TableName b -> Bool) -> ForeignKey b -> [Relationship b] -suggestRelsFK omitTracked tables name uniqueConstraints tracked foreignKey = +suggestRelsFK omitTracked tables name uniqueConstraints tracked predicate foreignKey = case (omitTracked, H.member (relatedTable, columnRelationships) tracked, Map.lookup relatedTable tables) of (True, True, _) -> [] (_, _, Nothing) -> [] - (_, _, Just _) -> - [ Relationship - { rType = ObjRel, - rFrom = Mapping {mTable = name, mColumns = localColumns}, - rTo = Mapping {mTable = relatedTable, mColumns = relatedColumns} - }, - Relationship - { rType = if H.fromList localColumns `H.member` uniqueConstraintColumns then ObjRel else ArrRel, - rTo = Mapping {mTable = name, mColumns = localColumns}, - rFrom = Mapping {mTable = relatedTable, mColumns = relatedColumns} - } - ] + (_, _, Just _) + | not (predicate name || predicate relatedTable) -> [] + | otherwise -> + [ Relationship + { rType = ObjRel, + rFrom = Mapping {mTable = name, mColumns = localColumns, mConstraintName = Just constraintName}, + rTo = Mapping {mTable = relatedTable, mColumns = relatedColumns, mConstraintName = Nothing} + }, + 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 = relatedTable, mColumns = relatedColumns, mConstraintName = Nothing} + } + ] where columnRelationships = MapNE.toHashMap (_fkColumnMapping foreignKey) localColumns = Map.keys columnRelationships relatedColumns = Map.elems columnRelationships uniqueConstraintColumns = H.map _ucColumns uniqueConstraints relatedTable = _fkForeignTable foreignKey + constraintName = Aeson.toJSON (_cName (_fkConstraint foreignKey)) suggestRelsTable :: forall b. Backend b => Bool -> HashMap (TableName b) (TableCoreInfo b) -> + (TableName b -> Bool) -> (TableName b, TableCoreInfo b) -> [Relationship b] -suggestRelsTable omitTracked tables (name, table) = - suggestRelsFK omitTracked tables name constraints tracked =<< toList foreignKeys +suggestRelsTable omitTracked tables predicate (name, table) = + suggestRelsFK omitTracked tables name constraints tracked predicate =<< toList foreignKeys where foreignKeys = _tciForeignKeys table constraints = _tciUniqueConstraints table @@ -176,15 +184,17 @@ suggestRelsResponse :: Backend b => Bool -> HashMap (TableName b) (TableCoreInfo b) -> + (TableName b -> Bool) -> SuggestedRelationships b -suggestRelsResponse omitTracked tables = +suggestRelsResponse omitTracked tables predicate = Relationships $ - suggestRelsTable omitTracked tables =<< Map.toList tables + suggestRelsTable omitTracked tables predicate =<< Map.toList tables --- | Helper to filter tables considered for relationships -pluck :: Eq a => Maybe [a] -> Map.HashMap a b -> Map.HashMap a b -pluck Nothing = id -pluck (Just ks) = Map.mapMaybeWithKey (\k v -> if k `elem` ks then Just v else Nothing) +tablePredicate :: Hashable a => Maybe [a] -> a -> Bool +tablePredicate Nothing _ = True +tablePredicate (Just ns) n = n `H.member` hash + where + hash = H.fromList ns -- | The method invoked when dispatching on metadata calls in POST /v1/metadata runSuggestRels :: @@ -196,4 +206,4 @@ 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" - Just tableCache -> pure $ encJFromJValue $ suggestRelsResponse @b omitExistingB (pluck tablesM tableCache) + Just tableCache -> pure $ encJFromJValue $ suggestRelsResponse @b omitExistingB tableCache (tablePredicate tablesM)