Fix GDC Metadata API Tests and Typescript Code Generation

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5624
GitOrigin-RevId: a595aeb94971a5ee4968b403af4670e06684b708
This commit is contained in:
Solomon 2022-08-28 17:20:00 -07:00 committed by hasura-bot
parent 54546fcc6b
commit 18f9bf481c
14 changed files with 560 additions and 133 deletions

View File

@ -1249,6 +1249,7 @@ test-suite tests-hspec
Test.BigQuery.Schema.RunSQLSpec
Test.BigQuery.TypeInterpretationSpec
Test.DataConnector.AggregateQuerySpec
Test.DataConnector.MetadataApiSpec
Test.DataConnector.MockAgent.AggregateQuerySpec
Test.DataConnector.MockAgent.BasicQuerySpec
Test.DataConnector.MockAgent.QueryRelationshipsSpec

View File

@ -67,6 +67,7 @@ instance HasCodec TableInfo where
newtype ForeignKeys = ForeignKeys {unConstraints :: HashMap ConstraintName Constraint}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving newtype (FromJSON, ToJSON)
deriving anyclass (NFData, Hashable)
instance HasCodec ForeignKeys where
@ -83,6 +84,7 @@ data Constraint = Constraint
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON) via Autodocodec Constraint
instance HasCodec Constraint where
codec =

View File

@ -17,7 +17,9 @@ 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 Data.Text.NonEmpty qualified as Text.NE
import Hasura.Backends.DataConnector.Adapter.Types qualified as DC.Types
import Hasura.Base.Error qualified as Error
import Hasura.EncJSON (EncJSON)
import Hasura.Prelude
import Hasura.RQL.Types.Common qualified as Common
@ -72,16 +74,22 @@ instance FromJSON DCDeleteAgent where
instance ToJSON DCDeleteAgent where
toJSON DCDeleteAgent {..} = Aeson.object ["name" .= _gdcrName]
runDeleteDataConnectorAgent :: (Metadata.MetadataM m) => DCDeleteAgent -> m EncJSON
runDeleteDataConnectorAgent :: (Metadata.MetadataM m, MonadError Error.QErr 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))
let kindExists = do
agentMap <- BackendMap.lookup @'Backend.DataConnector $ Metadata._metaBackendConfigs oldMetadata
InsOrdHashMap.lookup kind $ Metadata.unBackendConfigWrapper agentMap
case kindExists of
Nothing -> Error.throw400 Error.NotFound $ "DC Agent '" <> Text.NE.unNonEmptyText _gdcrName <> "' not found"
Just _ -> do
let modifiedMetadata =
oldMetadata & Metadata.metaBackendConfigs
%~ BackendMap.alter @'Backend.DataConnector
(fmap (coerce . InsOrdHashMap.delete kind . Metadata.unBackendConfigWrapper))
putMetadata modifiedMetadata
pure Common.successMsg
putMetadata modifiedMetadata
pure Common.successMsg

View File

@ -4,8 +4,10 @@
module Hasura.Backends.DataConnector.API.V0.TableSpec (spec, genTableName, genTableInfo) where
import Data.Aeson.QQ.Simple (aesonQQ)
import Data.HashMap.Strict qualified as HashMap
import Hasura.Backends.DataConnector.API.V0
import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnInfo, genColumnName)
import Hasura.Generator.Common
import Hasura.Prelude
import Hedgehog
import Hedgehog.Gen
@ -44,11 +46,48 @@ spec = do
"description": "my description"
}
|]
describe "foreign-key" $
testToFromJSONToSchema
( TableInfo
(TableName ["my_table_name"])
[ColumnInfo (ColumnName "id") StringTy False Nothing]
(Just [ColumnName "id"])
(Just "my description")
(Just $ ForeignKeys $ HashMap.singleton (ConstraintName "Artist") (Constraint "Artists" (HashMap.singleton "ArtistId" "ArtistId")))
)
[aesonQQ|
{ "name": ["my_table_name"],
"columns": [{"name": "id", "type": "string", "nullable": false}],
"primary_key": ["id"],
"description": "my description",
"foreign_keys": {
"Artist": {
"foreign_table": "Artists",
"column_mapping": {
"ArtistId": "ArtistId"
}
}
}
}
|]
jsonOpenApiProperties genTableInfo
genTableName :: MonadGen m => m TableName
genTableName = TableName <$> Gen.nonEmpty (linear 1 3) (text (linear 0 10) unicode)
genForeignKeys :: MonadGen m => m ForeignKeys
genForeignKeys = ForeignKeys <$> genHashMap genConstraintName genConstraint defaultRange
genConstraintName :: MonadGen m => m ConstraintName
genConstraintName = ConstraintName <$> text (linear 0 10) unicode
genConstraint :: MonadGen m => m Constraint
genConstraint =
let foreignTable = text (linear 0 10) unicode
mapping = genHashMap (text (linear 0 10) unicode) (text (linear 0 10) unicode) defaultRange
in Constraint <$> foreignTable <*> mapping
-- | Note: this generator is intended for serialization tests only and does not ensure valid Foreign Key Constraints.
genTableInfo :: MonadGen m => m TableInfo
genTableInfo =
TableInfo
@ -56,4 +95,4 @@ genTableInfo =
<*> Gen.list (linear 0 5) genColumnInfo
<*> Gen.maybe (Gen.list (linear 0 5) genColumnName)
<*> Gen.maybe (text (linear 0 20) unicode)
<*> pure Nothing
<*> Gen.maybe genForeignKeys

View File

@ -21,6 +21,14 @@
{
"name": ["Album"],
"primary_key": ["AlbumId"],
"foreign_keys": {
"Artist": {
"column_mapping": {
"ArtistId": "ArtistId"
},
"foreign_table": "Artist"
}
},
"description": "Collection of music albums created by artists",
"columns": [
{
@ -46,6 +54,14 @@
{
"name": ["Customer"],
"primary_key": ["CustomerId"],
"foreign_keys": {
"CustomerSupportRep": {
"column_mapping": {
"SupportRepId": "EmployeeId"
},
"foreign_table": "Employee"
}
},
"description": "Collection of customers who can buy tracks",
"columns": [
{
@ -131,6 +147,14 @@
{
"name": ["Employee"],
"primary_key": ["EmployeeId"],
"foreign_keys": {
"EmployeeReportsTo": {
"column_mapping": {
"ReportsTo": "EmployeeId"
},
"foreign_table": "Employee"
}
},
"description": "Collection of employees who work for the business",
"columns": [
{
@ -247,6 +271,14 @@
{
"name": ["Invoice"],
"primary_key": ["InvoiceId"],
"foreign_keys": {
"InvoiceCustomer": {
"column_mapping": {
"CustomerId": "CustomerId"
},
"foreign_table": "Customer"
}
},
"description": "Collection of invoices of music purchases by a customer",
"columns": [
{
@ -308,6 +340,20 @@
{
"name": ["InvoiceLine"],
"primary_key": ["InvoiceLineId"],
"foreign_keys": {
"Invoice": {
"column_mapping": {
"InvoiceId": "InvoiceId"
},
"foreign_table": "Invoice"
},
"Track": {
"column_mapping": {
"TrackId": "TrackId"
},
"foreign_table": "Track"
}
},
"description": "Collection of track purchasing line items of invoices",
"columns": [
{
@ -383,6 +429,20 @@
{
"name": ["PlaylistTrack"],
"primary_key": ["PlaylistId", "TrackId"],
"foreign_keys": {
"Playlist": {
"column_mapping": {
"PlaylistId": "PlaylistId"
},
"foreign_table": "Playlist"
},
"Track": {
"column_mapping": {
"TrackId": "TrackId"
},
"foreign_table": "Track"
}
},
"description": "Associations between playlists and tracks",
"columns": [
{
@ -402,6 +462,26 @@
{
"name": ["Track"],
"primary_key": ["TrackId"],
"foreign_keys": {
"Album": {
"column_mapping": {
"AlbumId": "AlbumId"
},
"foreign_table": "Album"
},
"Genre": {
"column_mapping": {
"GenreId": "GenreId"
},
"foreign_table": "Genre"
},
"MediaType": {
"column_mapping": {
"MediaTypeId": "MediaTypeId"
},
"foreign_table": "MediaType"
}
},
"description": "Collection of music tracks",
"columns": [
{

View File

@ -1,7 +1,10 @@
module Test.SchemaSpec (spec) where
import Data.List (sortOn)
import Hasura.Backends.DataConnector.API (Config, Routes (..), SchemaResponse (..), SourceName, TableInfo (..))
--------------------------------------------------------------------------------
import Data.HashMap.Strict qualified as HashMap
import Data.List (sort, sortOn)
import Hasura.Backends.DataConnector.API (Config, Constraint (..), ForeignKeys (..), Routes (..), SchemaResponse (..), SourceName, TableInfo (..))
import Hasura.Backends.DataConnector.API.V0.Column (ColumnInfo (..))
import Servant.API (NamedRoutes)
import Servant.Client (Client, (//))
@ -10,6 +13,8 @@ import Test.Expectations (jsonShouldBe)
import Test.Hspec (Spec, describe, it)
import Prelude
--------------------------------------------------------------------------------
removeDescriptionFromColumn :: ColumnInfo -> ColumnInfo
removeDescriptionFromColumn c = c {dciDescription = Nothing}
@ -18,8 +23,19 @@ removeDescription t@TableInfo {dtiColumns} = t {dtiDescription = Nothing, dtiCol
where
newColumns = map removeDescriptionFromColumn dtiColumns
removeForeignKeys :: TableInfo -> TableInfo
removeForeignKeys t = t {dtiForeignKeys = Nothing}
extractForeignKeys :: TableInfo -> [Constraint]
extractForeignKeys = foldMap (HashMap.elems . unConstraints) . dtiForeignKeys
spec :: Client IO (NamedRoutes Routes) -> SourceName -> Config -> Spec
spec api sourceName config = describe "schema API" $ do
it "returns Chinook schema" $ do
tables <- (map removeDescription . sortOn dtiName . srTables) <$> (api // _schema) sourceName config
tables `jsonShouldBe` map removeDescription Data.schemaTables
-- NOTE: Constraint names arent guaranteed to be the same across
-- Chinook backends so we compare Constraints without their names
-- independently from the rest of the schema.
(map removeForeignKeys tables) `jsonShouldBe` map (removeForeignKeys . removeDescription) Data.schemaTables
(map (sort . extractForeignKeys) tables) `jsonShouldBe` map (sort . extractForeignKeys) Data.schemaTables

View File

@ -30,7 +30,7 @@ defaultSource = \case
BigQuery -> "bigquery"
Citus -> "citus"
Cockroach -> "cockroach"
DataConnector -> "data-connector"
DataConnector -> "chinook"
-- | The default hasura metadata backend type used for a given backend in this test suite project.
defaultBackendTypeString :: BackendType -> String
@ -41,7 +41,7 @@ defaultBackendTypeString = \case
BigQuery -> "bigquery"
Citus -> "citus"
Cockroach -> "cockroach"
DataConnector -> "data-connector"
DataConnector -> "reference"
schemaKeyword :: BackendType -> Key
schemaKeyword = \case

View File

@ -13,6 +13,7 @@ module Harness.Test.Fixture
defaultBackendTypeString,
noLocalTestEnvironment,
SetupAction (..),
emptySetupAction,
Options (..),
combineOptions,
defaultOptions,
@ -22,6 +23,7 @@ where
import Data.UUID.V4 (nextRandom)
import Harness.Exceptions
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Test.BackendType
import Harness.Test.CustomOptions
import Harness.Test.Hspec.Extended
@ -213,6 +215,15 @@ data SetupAction = forall a.
teardownAction :: Maybe a -> IO ()
}
-- | Setup a test action without any initialization then reset the
-- metadata in the teardown. This is useful for running tests on the Metadata API.
emptySetupAction :: TestEnvironment -> SetupAction
emptySetupAction testEnvironment =
SetupAction
{ setupAction = pure (),
teardownAction = const $ GraphqlEngine.clearMetadata testEnvironment
}
-- | A name describing the given context.
data FixtureName
= Backend BackendType

View File

@ -0,0 +1,283 @@
{-# LANGUAGE QuasiQuotes #-}
-- | Metadata API tests for Data Connector Backend
module Test.DataConnector.MetadataApiSpec
( spec,
)
where
--------------------------------------------------------------------------------
import Data.List.NonEmpty qualified as NE
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Quoter.Yaml (yaml)
import Harness.Test.Fixture (emptySetupAction)
import Harness.Test.Fixture qualified as Fixture
import Harness.TestEnvironment (TestEnvironment)
import Harness.Yaml (shouldReturnYaml)
import Hasura.Prelude
import Test.Hspec (SpecWith, describe, it)
--------------------------------------------------------------------------------
-- Reference Agent Query Tests
spec :: SpecWith TestEnvironment
spec =
Fixture.runWithLocalTestEnvironment
( NE.fromList
[ (Fixture.fixture $ Fixture.Backend Fixture.DataConnector)
{ Fixture.setupTeardown = \(testEnv, _) ->
[emptySetupAction testEnv]
}
]
)
tests
--------------------------------------------------------------------------------
tests :: Fixture.Options -> SpecWith (TestEnvironment, a)
tests opts = describe "Metadata API: A series of actions to setup and teardown a source with tracked tables and relationships" $ do
describe "dc_add_agent" $ do
it "Success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: dc_add_agent
args:
name: reference
url: http://localhost:65005
|]
)
[yaml|
message: success
|]
describe "list_source_kinds" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: list_source_kinds
args: {}
|]
)
[yaml|
sources:
- builtin: true
kind: pg
- builtin: true
kind: citus
- builtin: true
kind: cockroach
- builtin: true
kind: mssql
- builtin: true
kind: bigquery
- builtin: true
kind: mysql
- builtin: false
kind: reference
|]
describe "<kind>_add_source" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: reference_add_source
args:
name: chinook
configuration:
value: {}
|]
)
[yaml|
message: success
|]
describe "<kind>_track_table" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: reference_track_table
args:
source: chinook
table: Album
|]
)
[yaml|
message: success
|]
describe "<kind>_create_object_relationship" $ do
it "success" $ \(testEnvironment, _) -> do
GraphqlEngine.postMetadata_
testEnvironment
[yaml|
type: reference_track_table
args:
source: chinook
table: Artist
|]
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: reference_create_object_relationship
args:
source: chinook
table: Album
name: Artist
using:
foreign_key_constraint_on:
- ArtistId
|]
)
[yaml|
message: success
|]
describe "<kind>_create_array_relationship" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: reference_create_array_relationship
args:
source: chinook
table: Artist
name: Albums
using:
foreign_key_constraint_on:
table: Album
columns:
- ArtistId
|]
)
[yaml|
message: success
|]
describe "export_metadata" $ do
it "produces the expected metadata structure" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: export_metadata
args: {}
|]
)
[yaml|
backend_configs:
dataconnector:
reference:
uri: http://localhost:65005
sources:
- configuration:
template: null
timeout: null
value: {}
kind: reference
name: chinook
tables:
- object_relationships:
- name: Artist
using:
foreign_key_constraint_on: ArtistId
table:
- Album
- array_relationships:
- name: Albums
using:
foreign_key_constraint_on:
column: ArtistId
table:
- Album
table:
- Artist
version: 3
|]
describe "<kind>_drop_relationship" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: reference_drop_relationship
args:
source: chinook
table: Artist
relationship: Albums
|]
)
[yaml|
message: success
|]
describe "<kind>_untrack_table" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: reference_untrack_table
args:
source: chinook
table: Artist
cascade: true
|]
)
[yaml|
message: success
|]
describe "<kind>_drop_source" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: reference_drop_source
args:
name: chinook
cascade: true
|]
)
[yaml|
message: success
|]
describe "dc_delete_agent" $ do
it "success" $ \(testEnvironment, _) -> do
shouldReturnYaml
opts
( GraphqlEngine.postMetadata
testEnvironment
[yaml|
type: dc_delete_agent
args:
name: reference
|]
)
[yaml|
message: success
|]

View File

@ -84,15 +84,19 @@ tables:
remote_table: [Album]
column_mapping:
ArtistId: ArtistId
- table: [Playlist]
- table: [PlaylistTrack]
- table: Playlist
array_relationships:
- name : Tracks
using:
foreign_key_constraint_on:
column: PlaylistId
table:
- PlaylistTrack
- table: PlaylistTrack
object_relationships:
- name: Playlist
using:
manual_configuration:
remote_table: [Playlist]
column_mapping:
PlaylistId: PlaylistId
foreign_key_constraint_on: PlaylistId
- name: Track
using:
manual_configuration:
@ -218,59 +222,109 @@ tests opts = describe "Queries" $ do
|]
describe "Array Relationships" $ do
it "joins on album id" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getArtist {
artists_by_pk(id: 1) {
id
name
albums {
title
describe "Manual" $ do
it "joins on album id" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getArtist {
artists_by_pk(id: 1) {
id
name
albums {
title
}
}
}
}
|]
)
[yaml|
data:
artists_by_pk:
name: AC/DC
id: 1
albums:
- title: For Those About To Rock We Salute You
- title: Let There Be Rock
|]
|]
)
[yaml|
data:
artists_by_pk:
name: AC/DC
id: 1
albums:
- title: For Those About To Rock We Salute You
- title: Let There Be Rock
|]
describe "Foreign Key Constraint On" $ do
it "joins on PlaylistId" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getPlaylist {
Playlist_by_pk(PlaylistId: 1) {
Tracks (limit: 3) {
TrackId
}
}
}
|]
)
[yaml|
data:
Playlist_by_pk:
Tracks:
- TrackId: 3402
- TrackId: 3389
- TrackId: 3390
|]
describe "Object Relationships" $ do
it "joins on artist id" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums_by_pk(id: 1) {
id
title
artist {
name
describe "Manual" $ do
it "joins on artist id" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums_by_pk(id: 1) {
id
title
artist {
name
}
}
}
}
|]
)
[yaml|
data:
albums_by_pk:
id: 1
title: "For Those About To Rock We Salute You"
artist:
name: "AC/DC"
|]
|]
)
[yaml|
data:
albums_by_pk:
id: 1
title: "For Those About To Rock We Salute You"
artist:
name: "AC/DC"
|]
describe "Foreign Key Constraint On" $ do
it "joins on PlaylistId" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getPlaylist {
PlaylistTrack_by_pk(PlaylistId: 1, TrackId: 2) {
Playlist {
Name
}
}
}
|]
)
[yaml|
data:
PlaylistTrack_by_pk:
Playlist:
Name: "Music"
|]
describe "Where Clause Tests" $ do
it "works with '_in' predicate" $ \(testEnvironment, _) ->

View File

@ -1,12 +0,0 @@
- 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

@ -1,24 +0,0 @@
- 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

@ -1,22 +0,0 @@
- 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: cockroach
- builtin: true
kind: mssql
- builtin: true
kind: bigquery
- builtin: true
kind: mysql
query:
type: list_source_kinds
args: {}

View File

@ -398,15 +398,6 @@ 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"