Data Connectors Delete Mutations Support [GDC-714]

[GDC-714]: https://hasurahq.atlassian.net/browse/GDC-714?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7623
GitOrigin-RevId: 676682343bc9aa54e1ed6553ebdf39756cfd7b5d
This commit is contained in:
Daniel Chambers 2023-01-24 15:17:31 +11:00 committed by hasura-bot
parent 9e04135f98
commit b3a4855fbb
3 changed files with 384 additions and 3 deletions

View File

@ -100,6 +100,7 @@ library
Test.DataConnector.MockAgent.AggregateQuerySpec
Test.DataConnector.MockAgent.BasicQuerySpec
Test.DataConnector.MockAgent.CustomScalarsSpec
Test.DataConnector.MockAgent.DeleteMutationsSpec
Test.DataConnector.MockAgent.ErrorSpec
Test.DataConnector.MockAgent.InsertMutationsSpec
Test.DataConnector.MockAgent.MetadataApiSpec

View File

@ -0,0 +1,354 @@
module Test.DataConnector.MockAgent.DeleteMutationsSpec
( spec,
)
where
import Data.Aeson qualified as Aeson
import Data.ByteString (ByteString)
import Data.HashMap.Strict qualified as HashMap
import Data.List.NonEmpty qualified as NE
import Harness.Backend.DataConnector.Mock (AgentRequest (..), MockRequestResults (..), mockAgentGraphqlTest, mockMutationResponse)
import Harness.Backend.DataConnector.Mock qualified as Mock
import Harness.Quoter.Graphql (graphql)
import Harness.Quoter.Yaml (yaml)
import Harness.Test.BackendType qualified as BackendType
import Harness.Test.Fixture qualified as Fixture
import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment)
import Harness.Yaml
import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Prelude
import Test.Hspec
--------------------------------------------------------------------------------
spec :: SpecWith GlobalTestEnvironment
spec =
Fixture.runWithLocalTestEnvironment
( NE.fromList
[ (Fixture.fixture $ Fixture.Backend Mock.backendTypeMetadata)
{ Fixture.mkLocalTestEnvironment = Mock.mkLocalTestEnvironment,
Fixture.setupTeardown = \(testEnv, mockEnv) ->
[Mock.setupAction sourceMetadata Mock.agentConfig (testEnv, mockEnv)]
}
]
)
tests
--------------------------------------------------------------------------------
testRoleName :: ByteString
testRoleName = "test-role"
sourceMetadata :: Aeson.Value
sourceMetadata =
let source = BackendType.backendSourceName Mock.backendTypeMetadata
backendType = BackendType.backendTypeString Mock.backendTypeMetadata
in [yaml|
name : *source
kind: *backendType
tables:
- table: [Album]
object_relationships:
- name: Artist
using:
manual_configuration:
remote_table: [Artist]
column_mapping:
ArtistId: ArtistId
select_permissions:
- role: *testRoleName
permission:
columns:
- AlbumId
- ArtistId
- Title
filter: {}
delete_permissions:
- role: *testRoleName
permission:
filter:
ArtistId: "X-Hasura-ArtistId"
- table: [Artist]
select_permissions:
- role: *testRoleName
permission:
columns:
- ArtistId
- Name
filter: {}
configuration: {}
|]
--------------------------------------------------------------------------------
tests :: Fixture.Options -> SpecWith (TestEnvironment, Mock.MockAgentEnvironment)
tests _opts = do
mockAgentGraphqlTest "delete rows with delete permissions" $ \performGraphqlRequest -> do
let headers = [("X-Hasura-ArtistId", "90"), ("X-Hasura-Role", testRoleName)]
let graphqlRequest =
[graphql|
mutation DeleteMutation {
delete_Album(where: {AlbumId: {_gt: 111}}) {
deleteCount: affected_rows
deletedRows: returning {
AlbumId
Title
Artist {
ArtistId
Name
}
}
}
}
|]
let mockAgentResponse =
API.MutationResponse
[ API.MutationOperationResults
{ API._morAffectedRows = 3,
API._morReturning =
Just
[ HashMap.fromList
[ (API.FieldName "deletedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 112),
(API.FieldName "deletedRows_Title", API.mkColumnFieldValue $ Aeson.String "The Number of The Beast"),
( API.FieldName "deletedRows_Artist",
API.mkRelationshipFieldValue $
rowsResponse
[ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90),
(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden")
]
]
)
],
HashMap.fromList
[ (API.FieldName "deletedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 113),
(API.FieldName "deletedRows_Title", API.mkColumnFieldValue $ Aeson.String "The X Factor"),
( API.FieldName "deletedRows_Artist",
API.mkRelationshipFieldValue $
rowsResponse
[ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90),
(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden")
]
]
)
],
HashMap.fromList
[ (API.FieldName "deletedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 114),
(API.FieldName "deletedRows_Title", API.mkColumnFieldValue $ Aeson.String "Virtual XI"),
( API.FieldName "deletedRows_Artist",
API.mkRelationshipFieldValue $
rowsResponse
[ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90),
(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden")
]
]
)
]
]
}
]
let mockConfig = Mock.chinookMock & mockMutationResponse mockAgentResponse
MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest
_mrrResponse
`shouldBeYaml` [yaml|
data:
delete_Album:
deleteCount: 3
deletedRows:
- AlbumId: 112
Title: The Number of The Beast
Artist:
ArtistId: 90
Name: Iron Maiden
- AlbumId: 113
Title: The X Factor
Artist:
ArtistId: 90
Name: Iron Maiden
- AlbumId: 114
Title: Virtual XI
Artist:
ArtistId: 90
Name: Iron Maiden
|]
let expectedRequest =
API.MutationRequest
{ API._mrTableRelationships =
[ API.TableRelationships
{ API._trSourceTable = API.TableName ("Album" :| []),
API._trRelationships =
HashMap.fromList
[ ( API.RelationshipName "Artist",
API.Relationship
{ API._rTargetTable = API.TableName ("Artist" :| []),
API._rRelationshipType = API.ObjectRelationship,
API._rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")]
}
)
]
}
],
API._mrInsertSchema = [],
API._mrOperations =
[ API.DeleteOperation $
API.DeleteMutationOperation
{ API._dmoTable = API.TableName ("Album" :| []),
API._dmoWhere =
Just $
API.And
[ API.ApplyBinaryComparisonOperator
API.Equal
(API.ComparisonColumn API.CurrentTable (API.ColumnName "ArtistId") $ API.ScalarType "number")
(API.ScalarValue (Aeson.Number 90) $ API.ScalarType "number"),
API.ApplyBinaryComparisonOperator
API.GreaterThan
(API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number")
(API.ScalarValue (Aeson.Number 111) $ API.ScalarType "number")
],
API._dmoReturningFields =
HashMap.fromList
[ (API.FieldName "deletedRows_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")),
(API.FieldName "deletedRows_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")),
( API.FieldName "deletedRows_Artist",
API.RelField
( API.RelationshipField
(API.RelationshipName "Artist")
API.Query
{ _qFields =
Just $
HashMap.fromList
[ (API.FieldName "ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"),
(API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")
],
_qAggregates = Nothing,
_qLimit = Nothing,
_qOffset = Nothing,
_qWhere = Nothing,
_qOrderBy = Nothing
}
)
)
]
}
]
}
_mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest)
mockAgentGraphqlTest "delete row by pk with delete permissions" $ \performGraphqlRequest -> do
let headers = [("X-Hasura-ArtistId", "90"), ("X-Hasura-Role", testRoleName)]
let graphqlRequest =
[graphql|
mutation DeleteMutation {
delete_Album_by_pk(AlbumId: 112) {
AlbumId
Title
Artist {
ArtistId
Name
}
}
}
|]
let mockAgentResponse =
API.MutationResponse
[ API.MutationOperationResults
{ API._morAffectedRows = 1,
API._morReturning =
Just
[ HashMap.fromList
[ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 112),
(API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "The Number of The Beast"),
( API.FieldName "Artist",
API.mkRelationshipFieldValue $
rowsResponse
[ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90),
(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden")
]
]
)
]
]
}
]
let mockConfig = Mock.chinookMock & mockMutationResponse mockAgentResponse
MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest
_mrrResponse
`shouldBeYaml` [yaml|
data:
delete_Album_by_pk:
AlbumId: 112
Title: The Number of The Beast
Artist:
ArtistId: 90
Name: Iron Maiden
|]
let expectedRequest =
API.MutationRequest
{ API._mrTableRelationships =
[ API.TableRelationships
{ API._trSourceTable = API.TableName ("Album" :| []),
API._trRelationships =
HashMap.fromList
[ ( API.RelationshipName "Artist",
API.Relationship
{ API._rTargetTable = API.TableName ("Artist" :| []),
API._rRelationshipType = API.ObjectRelationship,
API._rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")]
}
)
]
}
],
API._mrInsertSchema = [],
API._mrOperations =
[ API.DeleteOperation $
API.DeleteMutationOperation
{ API._dmoTable = API.TableName ("Album" :| []),
API._dmoWhere =
Just $
API.And
[ API.ApplyBinaryComparisonOperator
API.Equal
(API.ComparisonColumn API.CurrentTable (API.ColumnName "ArtistId") $ API.ScalarType "number")
(API.ScalarValue (Aeson.Number 90) $ API.ScalarType "number"),
API.ApplyBinaryComparisonOperator
API.Equal
(API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number")
(API.ScalarValue (Aeson.Number 112) $ API.ScalarType "number")
],
API._dmoReturningFields =
HashMap.fromList
[ (API.FieldName "AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")),
(API.FieldName "Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")),
( API.FieldName "Artist",
API.RelField
( API.RelationshipField
(API.RelationshipName "Artist")
API.Query
{ _qFields =
Just $
HashMap.fromList
[ (API.FieldName "ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"),
(API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")
],
_qAggregates = Nothing,
_qLimit = Nothing,
_qOffset = Nothing,
_qWhere = Nothing,
_qOrderBy = Nothing
}
)
)
]
}
]
}
_mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest)
rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse
rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing

View File

@ -92,8 +92,15 @@ translateMutationDB sessionVariables = \case
_mrInsertSchema = [],
_mrOperations = API.UpdateOperation <$> updateOperations
}
MDBDelete _delete ->
throw400 NotSupported "translateMutationDB: delete mutations not implemented for the Data Connector backend."
MDBDelete delete -> do
(deleteOperation, tableRelationships) <- CPS.runWriterT $ translateDelete sessionVariables delete
let apiTableRelationships = uncurry API.TableRelationships <$> HashMap.toList (unTableRelationships tableRelationships)
pure $
API.MutationRequest
{ _mrTableRelationships = apiTableRelationships,
_mrInsertSchema = [],
_mrOperations = [API.DeleteOperation deleteOperation]
}
MDBFunction _returnsSet _select ->
throw400 NotSupported "translateMutationDB: function mutations not implemented for the Data Connector backend."
@ -115,7 +122,8 @@ translateInsert sessionVariables AnnotatedInsert {..} = do
}
where
tableName = Witch.from _aiTableName
insertCheckCondition = fst _aiCheckCondition
-- Update check condition must be used once upserts are supported
(insertCheckCondition, _updateCheckCondition) = _aiCheckCondition
AnnotatedInsertData {..} = _aiData
translateInsertRow ::
@ -224,6 +232,24 @@ translateUpdateOperations sessionVariables columnUpdates =
ValueLiteral scalarType value -> pure (scalarType, value)
ArrayLiteral _scalarType _values -> throw400 NotSupported "translateUpdateOperations: Array literals are not supported as column update values"
translateDelete ::
MonadError QErr m =>
SessionVariables ->
AnnDelG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
CPS.WriterT TableRelationships m API.DeleteMutationOperation
translateDelete sessionVariables AnnDel {..} = do
whereExp <- translateBoolExpToExpression sessionVariables tableName (BoolAnd [permissionFilter, whereClause])
returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _adOutput
pure $
API.DeleteMutationOperation
{ API._dmoTable = tableName,
API._dmoWhere = whereExp,
API._dmoReturningFields = HashMap.mapKeys (API.FieldName . getFieldNameTxt) returningFields
}
where
tableName = Witch.from _adTable
(permissionFilter, whereClause) = _adWhere
translateMutationOutputToReturningFields ::
( MonadError QErr m,
Has TableRelationships writerOutput,