mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 21:12:09 +03:00
0b63b0b6b1
[GDC-1063]: https://hasurahq.atlassian.net/browse/GDC-1063?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8249 GitOrigin-RevId: 9d6dec3b4ef38fd7972bd0b9d3cfdfece1dcdcb5
329 lines
20 KiB
Haskell
329 lines
20 KiB
Haskell
module Test.Specs.QuerySpec.RelationshipsSpec (spec) where
|
|
|
|
import Control.Arrow ((>>>))
|
|
import Control.Lens (Traversal', ix, (&), (?~), (^.), (^..), (^?), _Just)
|
|
import Control.Monad (when)
|
|
import Data.Aeson (Value (..))
|
|
import Data.HashMap.Strict (HashMap)
|
|
import Data.List (sortOn)
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
|
import Data.List.NonEmpty qualified as NonEmpty
|
|
import Data.Maybe (fromMaybe, maybeToList)
|
|
import Hasura.Backends.DataConnector.API
|
|
import Test.AgentAPI (queryGuarded)
|
|
import Test.Data (TestData (..))
|
|
import Test.Data qualified as Data
|
|
import Test.Expectations (jsonShouldBe, rowsShouldBe)
|
|
import Test.Sandwich (describe)
|
|
import Test.TestHelpers (AgentDatasetTestSpec, it)
|
|
import Prelude
|
|
|
|
spec :: TestData -> Maybe SubqueryComparisonCapabilities -> AgentDatasetTestSpec
|
|
spec TestData {..} subqueryComparisonCapabilities = describe "Relationship Queries" $ do
|
|
it "perform an object relationship query by joining artist to albums" $ do
|
|
let query = albumsWithArtistQuery id
|
|
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> queryGuarded query
|
|
|
|
let joinInArtist (album :: HashMap FieldName FieldValue) =
|
|
let artist = (album ^? Data.field "ArtistId" . Data._ColumnFieldNumber) >>= \artistId -> _tdArtistsRowsById ^? ix artistId
|
|
artistPropVal = maybeToList artist
|
|
in Data.insertField "Artist" (Data.mkSubqueryRowsFieldValue artistPropVal) album
|
|
let removeArtistId = Data.deleteField "ArtistId"
|
|
|
|
let expectedAlbums = (removeArtistId . joinInArtist) <$> _tdAlbumsRows
|
|
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
|
|
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
|
|
|
|
it "perform an object relationship query by joining artist to albums but with zero artist fields" $ do
|
|
let query = albumsWithArtistQuery (qFields ?~ mempty)
|
|
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> queryGuarded query
|
|
|
|
let joinInArtist (album :: HashMap FieldName FieldValue) =
|
|
let artist = (album ^? Data.field "ArtistId" . Data._ColumnFieldNumber) >>= \artistId -> _tdArtistsRowsById ^? ix artistId
|
|
artistPropVal = Data.filterColumns [] $ maybeToList artist
|
|
in Data.insertField "Artist" (Data.mkSubqueryRowsFieldValue artistPropVal) album
|
|
let removeArtistId = Data.deleteField "ArtistId"
|
|
|
|
let expectedAlbums = (removeArtistId . joinInArtist) <$> _tdAlbumsRows
|
|
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
|
|
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
|
|
|
|
it "perform an array relationship query by joining albums to artists" $ do
|
|
let query = artistsWithAlbumsQuery id
|
|
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded query
|
|
|
|
let joinInAlbums (artist :: HashMap FieldName FieldValue) =
|
|
let artistId = artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
|
albumFilter artistId' album = album ^? Data.field "ArtistId" . Data._ColumnFieldNumber == Just artistId'
|
|
albums = maybe [] (\artistId' -> filter (albumFilter artistId') _tdAlbumsRows) artistId
|
|
albums' = Data.deleteField "ArtistId" <$> albums
|
|
in Data.insertField "Albums" (Data.mkSubqueryRowsFieldValue albums') artist
|
|
|
|
let expectedAlbums = joinInAlbums <$> _tdArtistsRows
|
|
Data.responseRows receivedArtists `rowsShouldBe` expectedAlbums
|
|
_qrAggregates receivedArtists `jsonShouldBe` Nothing
|
|
|
|
it "perform an array relationship query by joining albums to artists but with zero album fields" $ do
|
|
let query = artistsWithAlbumsQuery (qFields ?~ mempty)
|
|
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded query
|
|
|
|
let joinInAlbums (artist :: HashMap FieldName FieldValue) =
|
|
let artistId = artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
|
albumFilter artistId' album = album ^? Data.field "ArtistId" . Data._ColumnFieldNumber == Just artistId'
|
|
albums = maybe [] (\artistId' -> filter (albumFilter artistId') _tdAlbumsRows) artistId
|
|
albums' = Data.filterColumns [] albums
|
|
in Data.insertField "Albums" (Data.mkSubqueryRowsFieldValue albums') artist
|
|
|
|
let expectedAlbums = joinInAlbums <$> _tdArtistsRows
|
|
Data.responseRows receivedArtists `rowsShouldBe` expectedAlbums
|
|
_qrAggregates receivedArtists `jsonShouldBe` Nothing
|
|
|
|
it "perform an array relationship query by joining albums to artists with pagination of albums" $ do
|
|
let albumsOrdering = OrderBy mempty $ NonEmpty.fromList [_tdOrderByColumn [] "AlbumId" Ascending]
|
|
let query = artistsWithAlbumsQuery (qOffset ?~ 1 >>> qLimit ?~ 2 >>> qOrderBy ?~ albumsOrdering)
|
|
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded query
|
|
|
|
let joinInAlbums (artist :: HashMap FieldName FieldValue) = do
|
|
let artistId = artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
|
albumFilter artistId' album = album ^? Data.field "ArtistId" . Data._ColumnFieldNumber == Just artistId'
|
|
albums = maybe [] (\artistId' -> filter (albumFilter artistId') _tdAlbumsRows) artistId
|
|
paginatedAlbums = albums & sortOn (^? Data.field "ArtistId") & drop 1 & take 2
|
|
paginatedAlbums' = Data.deleteField "ArtistId" <$> paginatedAlbums
|
|
in Data.insertField "Albums" (Data.mkSubqueryRowsFieldValue paginatedAlbums') artist
|
|
|
|
let expectedAlbums = joinInAlbums <$> _tdArtistsRows
|
|
Data.responseRows receivedArtists `rowsShouldBe` expectedAlbums
|
|
_qrAggregates receivedArtists `jsonShouldBe` Nothing
|
|
|
|
it "can filter in object relationships" $ do
|
|
let artistWhere = ApplyBinaryComparisonOperator GreaterThanOrEqual (_tdCurrentComparisonColumn "Name" artistNameScalarType) (Data.scalarValueComparison (String "H") artistNameScalarType)
|
|
let query = albumsWithArtistQuery (qWhere ?~ artistWhere)
|
|
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> queryGuarded query
|
|
|
|
let joinInArtist (album :: HashMap FieldName FieldValue) =
|
|
let artist = do
|
|
artistId <- album ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
|
artist' <- _tdArtistsRowsById ^? ix artistId
|
|
if (artist' ^? Data.field "Name" . Data._ColumnFieldString) >= Just "H" then Just artist' else Nothing
|
|
artistPropVal = maybeToList artist
|
|
in Data.insertField "Artist" (Data.mkSubqueryRowsFieldValue artistPropVal) album
|
|
let removeArtistId = Data.deleteField "ArtistId"
|
|
|
|
let expectedAlbums = (removeArtistId . joinInArtist) <$> _tdAlbumsRows
|
|
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
|
|
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
|
|
|
|
it "can filter in array relationships" $ do
|
|
let albumsWhere = ApplyBinaryComparisonOperator GreaterThanOrEqual (_tdCurrentComparisonColumn "Title" albumTitleScalarType) (Data.scalarValueComparison (String "O") albumTitleScalarType)
|
|
let query = artistsWithAlbumsQuery (qWhere ?~ albumsWhere)
|
|
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> queryGuarded query
|
|
|
|
let joinInAlbums (artist :: HashMap FieldName FieldValue) =
|
|
let albums = fromMaybe [] $ do
|
|
artistId <- artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
|
_tdAlbumsRows
|
|
& filter (\album -> album ^? Data.field "ArtistId" . Data._ColumnFieldNumber == Just artistId && album ^? Data.field "Title" . Data._ColumnFieldString >= Just "O")
|
|
& fmap (Data.deleteField "ArtistId")
|
|
& sortOn (^? Data.field "ArtistId")
|
|
& pure
|
|
in Data.insertField "Albums" (Data.mkSubqueryRowsFieldValue albums) artist
|
|
|
|
let expectedAlbums = joinInAlbums <$> _tdArtistsRows
|
|
Data.responseRows receivedArtists `rowsShouldBe` expectedAlbums
|
|
_qrAggregates receivedArtists `jsonShouldBe` Nothing
|
|
|
|
when ((_ctccSupportsRelations <$> subqueryComparisonCapabilities) == Just True) $
|
|
describe "Cross related table comparisons" $ do
|
|
it "perform an object relationship query by joining employee to customers and filter comparing columns across the object relationship" $ do
|
|
-- Join Employee to Customers via SupportRep, and only get those customers that have a rep
|
|
-- that is in the same country as the customer
|
|
-- This sort of thing would come from a permissions filter on Customer that looks like:
|
|
-- { SupportRep: { Country: { _ceq: [ "$", "Country" ] } } }
|
|
let where' =
|
|
Exists (RelatedTable _tdSupportRepRelationshipName) $
|
|
ApplyBinaryComparisonOperator
|
|
Equal
|
|
(_tdCurrentComparisonColumn "Country" employeeCountryScalarType)
|
|
(AnotherColumnComparison (_tdQueryComparisonColumn "Country" employeeCountryScalarType))
|
|
let query = customersWithSupportRepQuery id & qrQuery . qWhere ?~ where'
|
|
receivedCustomers <- Data.sortResponseRowsBy "CustomerId" <$> queryGuarded query
|
|
|
|
let joinInSupportRep (customer :: HashMap FieldName FieldValue) =
|
|
let supportRep = (customer ^? Data.field "SupportRepId" . Data._ColumnFieldNumber) >>= \employeeId -> _tdEmployeesRowsById ^? ix employeeId
|
|
supportRepPropVal = maybeToList $ Data.filterColumnsByQueryFields employeesQuery <$> supportRep
|
|
in Data.insertField "SupportRep" (Data.mkSubqueryRowsFieldValue supportRepPropVal) customer
|
|
|
|
let filterCustomersBySupportRepCountry (customer :: HashMap FieldName FieldValue) =
|
|
let customerCountry = customer ^? Data.field "Country" . Data._ColumnFieldString
|
|
supportRepCountry = customer ^.. Data.field "SupportRep" . subqueryRows . Data.field "Country" . Data._ColumnFieldString
|
|
in any (`elem` supportRepCountry) customerCountry
|
|
|
|
let expectedCustomers = filter filterCustomersBySupportRepCountry $ Data.filterColumnsByQueryFields (query ^. qrQuery) . joinInSupportRep <$> _tdCustomersRows
|
|
Data.responseRows receivedCustomers `rowsShouldBe` expectedCustomers
|
|
_qrAggregates receivedCustomers `jsonShouldBe` Nothing
|
|
|
|
it "perform an array relationship query by joining customers to employees and filter comparing columns across the array relationship" $ do
|
|
-- Join Customers to Employees via SupportRepForCustomers, and only get those employees that are reps for
|
|
-- customers that are in the same country as the employee
|
|
-- This sort of thing would come from a permissions filter on Employees that looks like:
|
|
-- { SupportRepForCustomers: { Country: { _ceq: [ "$", "Country" ] } } }
|
|
let where' =
|
|
Exists (RelatedTable _tdSupportRepForCustomersRelationshipName) $
|
|
ApplyBinaryComparisonOperator
|
|
Equal
|
|
(_tdCurrentComparisonColumn "Country" employeeCountryScalarType)
|
|
(AnotherColumnComparison (_tdQueryComparisonColumn "Country" employeeCountryScalarType))
|
|
let query = employeesWithCustomersQuery id & qrQuery . qWhere ?~ where'
|
|
receivedEmployees <- Data.sortResponseRowsBy "EmployeeId" <$> queryGuarded query
|
|
|
|
let joinInCustomers (employee :: HashMap FieldName FieldValue) =
|
|
let employeeId = employee ^? Data.field "EmployeeId" . Data._ColumnFieldNumber
|
|
customerFilter employeeId' customer = customer ^? Data.field "SupportRepId" . Data._ColumnFieldNumber == Just employeeId'
|
|
customers = maybe [] (\employeeId' -> filter (customerFilter employeeId') _tdCustomersRows) employeeId
|
|
customers' = Data.filterColumnsByQueryFields customersQuery <$> customers
|
|
in Data.insertField "SupportRepForCustomers" (Data.mkSubqueryRowsFieldValue customers') employee
|
|
|
|
let filterEmployeesByCustomerCountry (employee :: HashMap FieldName FieldValue) =
|
|
let employeeCountry = employee ^? Data.field "Country" . Data._ColumnFieldString
|
|
customerCountries = employee ^.. Data.field "SupportRepForCustomers" . subqueryRows . Data.field "Country" . Data._ColumnFieldString
|
|
in any (`elem` customerCountries) employeeCountry
|
|
|
|
let expectedEmployees = filter filterEmployeesByCustomerCountry $ Data.filterColumnsByQueryFields (query ^. qrQuery) . joinInCustomers <$> _tdEmployeesRows
|
|
Data.responseRows receivedEmployees `rowsShouldBe` expectedEmployees
|
|
_qrAggregates receivedEmployees `jsonShouldBe` Nothing
|
|
|
|
it "perform an object relationship query by joining employee to customers but filter employees by comparing columns on the employee" $ do
|
|
-- Join Employee to Customers via SupportRep, and only get those customers that have a rep
|
|
-- However, the Employee table is filtered with a permission rule that compares columns on that table.
|
|
-- This Employee table permissions filter would look like:
|
|
-- { FirstName: { _cgt: ["LastName"] } }
|
|
let customersWhere =
|
|
Exists (RelatedTable _tdSupportRepRelationshipName) $
|
|
And
|
|
[ ( ApplyBinaryComparisonOperator
|
|
GreaterThan
|
|
(_tdCurrentComparisonColumn "FirstName" employeeFirstNameScalarType)
|
|
(AnotherColumnComparison (_tdCurrentComparisonColumn "LastName" employeeLastNameScalarType))
|
|
),
|
|
(Not (ApplyUnaryComparisonOperator IsNull (_tdCurrentComparisonColumn "EmployeeId" employeeIdScalarType)))
|
|
]
|
|
|
|
let employeesWhere =
|
|
ApplyBinaryComparisonOperator
|
|
GreaterThan
|
|
(_tdCurrentComparisonColumn "FirstName" employeeFirstNameScalarType)
|
|
(AnotherColumnComparison (_tdCurrentComparisonColumn "LastName" employeeLastNameScalarType))
|
|
|
|
let query = customersWithSupportRepQuery (\q -> q & qWhere ?~ employeesWhere) & qrQuery . qWhere ?~ customersWhere
|
|
receivedCustomers <- Data.sortResponseRowsBy "CustomerId" <$> queryGuarded query
|
|
|
|
let joinInSupportRep (customer :: HashMap FieldName FieldValue) =
|
|
let supportRep = do
|
|
employeeId <- (customer ^? Data.field "SupportRepId" . Data._ColumnFieldNumber)
|
|
employee <- _tdEmployeesRowsById ^? ix employeeId
|
|
firstName <- employee ^? Data.field "FirstName"
|
|
lastName <- employee ^? Data.field "LastName"
|
|
if firstName > lastName then pure employee else Nothing
|
|
supportRepPropVal = maybeToList $ Data.filterColumnsByQueryFields employeesQuery <$> supportRep
|
|
in Data.insertField "SupportRep" (Data.mkSubqueryRowsFieldValue supportRepPropVal) customer
|
|
|
|
let filterCustomersBySupportRepExistence (customer :: HashMap FieldName FieldValue) =
|
|
let supportRep = customer ^.. Data.field "SupportRep" . subqueryRows
|
|
in not (null supportRep)
|
|
|
|
let expectedCustomers = filter filterCustomersBySupportRepExistence $ Data.filterColumnsByQueryFields (query ^. qrQuery) . joinInSupportRep <$> _tdCustomersRows
|
|
Data.responseRows receivedCustomers `rowsShouldBe` expectedCustomers
|
|
_qrAggregates receivedCustomers `jsonShouldBe` Nothing
|
|
where
|
|
albumsWithArtistQuery :: (Query -> Query) -> QueryRequest
|
|
albumsWithArtistQuery modifySubquery =
|
|
let artistsSubquery = modifySubquery artistsQuery
|
|
fields =
|
|
Data.mkFieldsMap
|
|
[ ("AlbumId", _tdColumnField _tdAlbumsTableName "AlbumId"),
|
|
("Title", _tdColumnField _tdAlbumsTableName "Title"),
|
|
("Artist", RelField $ RelationshipField _tdArtistRelationshipName artistsSubquery)
|
|
]
|
|
query = albumsQuery & qFields ?~ fields
|
|
in QueryRequest _tdAlbumsTableName [Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships] query Nothing
|
|
|
|
artistsWithAlbumsQuery :: (Query -> Query) -> QueryRequest
|
|
artistsWithAlbumsQuery modifySubquery =
|
|
let albumFields = Data.mkFieldsMap [("AlbumId", _tdColumnField _tdAlbumsTableName "AlbumId"), ("Title", _tdColumnField _tdAlbumsTableName "Title")]
|
|
albumsSort = OrderBy mempty $ _tdOrderByColumn [] "AlbumId" Ascending :| []
|
|
albumsSubquery = albumsQuery & qFields ?~ albumFields & qOrderBy ?~ albumsSort & modifySubquery
|
|
fields =
|
|
Data.mkFieldsMap
|
|
[ ("ArtistId", _tdColumnField _tdArtistsTableName "ArtistId"),
|
|
("Name", _tdColumnField _tdArtistsTableName "Name"),
|
|
("Albums", RelField $ RelationshipField _tdAlbumsRelationshipName albumsSubquery)
|
|
]
|
|
query = artistsQuery & qFields ?~ fields
|
|
in QueryRequest _tdArtistsTableName [Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships] query Nothing
|
|
|
|
employeesWithCustomersQuery :: (Query -> Query) -> QueryRequest
|
|
employeesWithCustomersQuery modifySubquery =
|
|
let customersSort = OrderBy mempty $ _tdOrderByColumn [] "CustomerId" Ascending :| []
|
|
customersSubquery = customersQuery & qOrderBy ?~ customersSort & modifySubquery
|
|
fields =
|
|
Data.queryFields employeesQuery
|
|
<> Data.mkFieldsMap
|
|
[ ("SupportRepForCustomers", RelField $ RelationshipField _tdSupportRepForCustomersRelationshipName customersSubquery)
|
|
]
|
|
query = employeesQuery & qFields ?~ fields
|
|
in QueryRequest _tdEmployeesTableName [Data.onlyKeepRelationships [_tdSupportRepForCustomersRelationshipName] _tdEmployeesTableRelationships] query Nothing
|
|
|
|
customersWithSupportRepQuery :: (Query -> Query) -> QueryRequest
|
|
customersWithSupportRepQuery modifySubquery =
|
|
let supportRepSubquery = employeesQuery & modifySubquery
|
|
fields =
|
|
Data.queryFields customersQuery
|
|
<> Data.mkFieldsMap
|
|
[ ("SupportRep", RelField $ RelationshipField _tdSupportRepRelationshipName supportRepSubquery)
|
|
]
|
|
query = customersQuery & qFields ?~ fields
|
|
in QueryRequest _tdCustomersTableName [Data.onlyKeepRelationships [_tdSupportRepRelationshipName] _tdCustomersTableRelationships] query Nothing
|
|
|
|
artistsQuery :: Query
|
|
artistsQuery =
|
|
let fields = Data.mkFieldsMap [("ArtistId", _tdColumnField _tdArtistsTableName "ArtistId"), ("Name", _tdColumnField _tdArtistsTableName "Name")]
|
|
in Data.emptyQuery & qFields ?~ fields
|
|
|
|
albumsQuery :: Query
|
|
albumsQuery =
|
|
let fields = Data.mkFieldsMap [("AlbumId", _tdColumnField _tdAlbumsTableName "AlbumId"), ("ArtistId", _tdColumnField _tdAlbumsTableName "ArtistId"), ("Title", _tdColumnField _tdAlbumsTableName "Title")]
|
|
in Data.emptyQuery & qFields ?~ fields
|
|
|
|
customersQuery :: Query
|
|
customersQuery =
|
|
let fields =
|
|
Data.mkFieldsMap
|
|
[ ("CustomerId", _tdColumnField _tdCustomersTableName "CustomerId"),
|
|
("FirstName", _tdColumnField _tdCustomersTableName "FirstName"),
|
|
("LastName", _tdColumnField _tdCustomersTableName "LastName"),
|
|
("Country", _tdColumnField _tdCustomersTableName "Country"),
|
|
("SupportRepId", _tdColumnField _tdCustomersTableName "SupportRepId")
|
|
]
|
|
in Data.emptyQuery & qFields ?~ fields
|
|
|
|
employeesQuery :: Query
|
|
employeesQuery =
|
|
let fields =
|
|
Data.mkFieldsMap
|
|
[ ("EmployeeId", _tdColumnField _tdEmployeesTableName "EmployeeId"),
|
|
("FirstName", _tdColumnField _tdEmployeesTableName "FirstName"),
|
|
("LastName", _tdColumnField _tdEmployeesTableName "LastName"),
|
|
("Country", _tdColumnField _tdEmployeesTableName "Country")
|
|
]
|
|
in Data.emptyQuery & qFields ?~ fields
|
|
|
|
subqueryRows :: Traversal' FieldValue (HashMap FieldName FieldValue)
|
|
subqueryRows = _RelationshipFieldValue . qrRows . _Just . traverse
|
|
|
|
albumTitleScalarType = _tdFindColumnScalarType _tdAlbumsTableName "Title"
|
|
artistNameScalarType = _tdFindColumnScalarType _tdArtistsTableName "Name"
|
|
employeeIdScalarType = _tdFindColumnScalarType _tdEmployeesTableName "EmployeeId"
|
|
employeeCountryScalarType = _tdFindColumnScalarType _tdEmployeesTableName "Country"
|
|
employeeFirstNameScalarType = _tdFindColumnScalarType _tdEmployeesTableName "FirstName"
|
|
employeeLastNameScalarType = _tdFindColumnScalarType _tdEmployeesTableName "LastName"
|