diff --git a/dc-agents/README.md b/dc-agents/README.md index 426b409318b..c16c9432e82 100644 --- a/dc-agents/README.md +++ b/dc-agents/README.md @@ -473,7 +473,125 @@ The rows returned by the query must be put into the `rows` property array in the #### Pagination -If the GraphQL query contains pagination information, then the `limit` and `offset` fields may be set to integer values, indicating the number of rows to return, and the index of the first row to return, respectively. +There are three properties that are used to control pagination of queried data: + +* `aggregates_limit`: The maximum number of rows to consider in aggregations calculated and returned in the `aggregrates` property. `aggregates_limit` does not influence the rows returned in the `rows` property. It will only be used if there are aggregates in the query. +* `limit`: The maximum number of rows to return from a query in the `rows` property. `limit` does not influence the rows considered by aggregations. +* `offset`: The index of the first row to return. This affects the rows returned, and also the rows considered by aggregations. + +`limit` and `aggregates_limit` are set when the user uses a `limit` parameter in their GraphQL query. This restricts the dataset considered when returning rows as well as calculating aggregates. +HGE also has a row limit setting in a table's select permissions. This row limit will be used in the `limit` property if it is specified or if it is smaller than the limit specified in the GraphQL query itself. + +To illustrate the difference between `limit` and `aggregates_limit`, consider this GraphQL query, where a row limit of `2` has been placed on the Artist table in its select permissions. + +```graphql +query ArtistsQuery { + Artist_aggregate { + aggregate { + count + } + nodes { + Name + } + } +} +``` + +This would produce the following agent query request JSON: + +```json +{ + "table": ["Artist"], + "table_relationships": [], + "query": { + "aggregates_limit": null, + "limit": 2, + "offset": null, + "aggregates": { + "aggregate_count": { + "type": "star_count" + } + }, + "fields": { + "nodes_Name": { + "type": "column", + "column": "Name", + "column_type": "string" + } + } + } +} +``` + +The expected response to that query request would be the following JSON. Note that the row count has counted all rows (since `aggregates_limit` was null), but query has only returned the maximum number of rows as specified by the `limit` property: `2`. +```json +{ + "aggregates": { + "aggregate_count": 275 + }, + "rows":[ + { "nodes_Name": "AC/DC" }, + { "nodes_Name": "Accept" } + ] +} +``` + +By comparison, if we added a limit to our GraphQL query: + +```graphql +query ArtistsQuery { + Artist_aggregate(limit: 5) { + aggregate { + count + } + nodes { + Name + } + } +} +``` + +This would produce the following agent query request JSON: + +```json +{ + "table": ["Artist"], + "table_relationships": [], + "query": { + "aggregates_limit": 5, + "limit": 2, + "offset": null, + "aggregates": { + "aggregate_count": { + "type": "star_count" + } + }, + "fields": { + "nodes_Name": { + "type": "column", + "column": "Name", + "column_type": "string" + } + } + } +} +``` + +We would expect the following result: + +```json +{ + "aggregates": { + "aggregate_count": 5 + }, + "rows":[ + { "nodes_Name": "AC/DC" }, + { "nodes_Name": "Accept" } + ] +} +``` + +Note that the row count aggregation has been limited to `5` because `aggregates_limit` was `5`, and the rows returned were limited by the value of `limit`: `2`. #### Filters diff --git a/dc-agents/dc-api-types/package.json b/dc-agents/dc-api-types/package.json index 71b14de9c70..0d29e3e83f8 100644 --- a/dc-agents/dc-api-types/package.json +++ b/dc-agents/dc-api-types/package.json @@ -1,6 +1,6 @@ { "name": "@hasura/dc-api-types", - "version": "0.26.0", + "version": "0.27.0", "description": "Hasura GraphQL Engine Data Connector Agent API types", "author": "Hasura (https://github.com/hasura/graphql-engine)", "license": "Apache-2.0", diff --git a/dc-agents/dc-api-types/src/agent.openapi.json b/dc-agents/dc-api-types/src/agent.openapi.json index 4b3fa921d45..7028e78e12f 100644 --- a/dc-agents/dc-api-types/src/agent.openapi.json +++ b/dc-agents/dc-api-types/src/agent.openapi.json @@ -1305,6 +1305,13 @@ "nullable": true, "type": "object" }, + "aggregates_limit": { + "description": "Optionally limit the maximum number of rows considered while applying aggregations. This limit does not apply to returned rows.", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "nullable": true, + "type": "number" + }, "fields": { "additionalProperties": { "$ref": "#/components/schemas/Field" @@ -1314,14 +1321,14 @@ "type": "object" }, "limit": { - "description": "Optionally limit to N results", + "description": "Optionally limit the maximum number of returned rows. This limit does not apply to records considered while apply aggregations.", "maximum": 9223372036854776000, "minimum": -9223372036854776000, "nullable": true, "type": "number" }, "offset": { - "description": "Optionally offset from the Nth result", + "description": "Optionally offset from the Nth result. This applies to both row and aggregation results.", "maximum": 9223372036854776000, "minimum": -9223372036854776000, "nullable": true, diff --git a/dc-agents/dc-api-types/src/models/Query.ts b/dc-agents/dc-api-types/src/models/Query.ts index c598559ad5c..efebc8fd6d4 100644 --- a/dc-agents/dc-api-types/src/models/Query.ts +++ b/dc-agents/dc-api-types/src/models/Query.ts @@ -12,16 +12,20 @@ export type Query = { * Aggregate fields of the query */ aggregates?: Record | null; + /** + * Optionally limit the maximum number of rows considered while applying aggregations. This limit does not apply to returned rows. + */ + aggregates_limit?: number | null; /** * Fields of the query */ fields?: Record | null; /** - * Optionally limit to N results + * Optionally limit the maximum number of returned rows. This limit does not apply to records considered while apply aggregations. */ limit?: number | null; /** - * Optionally offset from the Nth result + * Optionally offset from the Nth result. This applies to both row and aggregation results. */ offset?: number | null; order_by?: OrderBy; diff --git a/dc-agents/package-lock.json b/dc-agents/package-lock.json index c89ef0a60ad..3c107f1ff3c 100644 --- a/dc-agents/package-lock.json +++ b/dc-agents/package-lock.json @@ -24,7 +24,7 @@ }, "dc-api-types": { "name": "@hasura/dc-api-types", - "version": "0.26.0", + "version": "0.27.0", "license": "Apache-2.0", "devDependencies": { "@tsconfig/node16": "^1.0.3", @@ -2227,7 +2227,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.26.0", + "@hasura/dc-api-types": "0.27.0", "fastify": "^4.13.0", "mathjs": "^11.0.0", "pino-pretty": "^8.0.0", @@ -2547,7 +2547,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.26.0", + "@hasura/dc-api-types": "0.27.0", "fastify": "^4.13.0", "fastify-metrics": "^9.2.1", "nanoid": "^3.3.4", @@ -2868,7 +2868,7 @@ "version": "file:reference", "requires": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.26.0", + "@hasura/dc-api-types": "0.27.0", "@tsconfig/node16": "^1.0.3", "@types/node": "^16.11.49", "@types/xml2js": "^0.4.11", @@ -3080,7 +3080,7 @@ "version": "file:sqlite", "requires": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.26.0", + "@hasura/dc-api-types": "0.27.0", "@tsconfig/node16": "^1.0.3", "@types/node": "^16.11.49", "@types/sqlite3": "^3.1.8", diff --git a/dc-agents/reference/package-lock.json b/dc-agents/reference/package-lock.json index e1989c9acd6..0bd5d2dc0bc 100644 --- a/dc-agents/reference/package-lock.json +++ b/dc-agents/reference/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.26.0", + "@hasura/dc-api-types": "0.27.0", "fastify": "^4.13.0", "mathjs": "^11.0.0", "pino-pretty": "^8.0.0", @@ -52,7 +52,7 @@ "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" }, "node_modules/@hasura/dc-api-types": { - "version": "0.26.0", + "version": "0.27.0", "license": "Apache-2.0", "devDependencies": { "@tsconfig/node16": "^1.0.3", diff --git a/dc-agents/reference/package.json b/dc-agents/reference/package.json index 884e56147b0..9ad81da84b6 100644 --- a/dc-agents/reference/package.json +++ b/dc-agents/reference/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.26.0", + "@hasura/dc-api-types": "0.27.0", "fastify": "^4.13.0", "mathjs": "^11.0.0", "pino-pretty": "^8.0.0", diff --git a/dc-agents/reference/src/query.ts b/dc-agents/reference/src/query.ts index 72503930f78..9f25502d3a1 100644 --- a/dc-agents/reference/src/query.ts +++ b/dc-agents/reference/src/query.ts @@ -578,12 +578,22 @@ export const queryData = (getTable: (tableName: TableName) => Record[], request: Query null, null, null, + null, ); return tag('foreach_query', `WITH ${escapeTableName(foreachTableName)} AS (${foreachIdsTableValue}) SELECT ${tableSubquery} AS data`); } diff --git a/server/lib/api-tests/api-tests.cabal b/server/lib/api-tests/api-tests.cabal index a6defb8953e..8c19e0081dc 100644 --- a/server/lib/api-tests/api-tests.cabal +++ b/server/lib/api-tests/api-tests.cabal @@ -119,6 +119,7 @@ library Test.DataConnector.MockAgent.OrderBySpec Test.DataConnector.MockAgent.QueryRelationshipsSpec Test.DataConnector.MockAgent.RemoteRelationshipsSpec + Test.DataConnector.MockAgent.TestHelpers Test.DataConnector.MockAgent.TransformedConfigurationSpec Test.DataConnector.MockAgent.UpdateMutationsSpec Test.DataConnector.QuerySpec diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/AggregateQuerySpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/AggregateQuerySpec.hs index dfb77c8daac..8757f847ba4 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/AggregateQuerySpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/AggregateQuerySpec.hs @@ -4,6 +4,7 @@ module Test.DataConnector.MockAgent.AggregateQuerySpec (spec) where -------------------------------------------------------------------------------- +import Control.Lens ((.~), (?~)) import Data.Aeson qualified as Aeson import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE @@ -18,6 +19,7 @@ import Harness.Yaml (shouldBeYaml) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude import Language.GraphQL.Draft.Syntax.QQ qualified as G +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (SpecWith, describe, shouldBe) -------------------------------------------------------------------------------- @@ -108,14 +110,14 @@ tests _opts = describe "Aggregate Query Tests" $ do } |] let queryResponse = - rowsResponse - [ [ (API.FieldName "ArtistIds_Id", API.mkColumnFieldValue $ Aeson.Number 1), - (API.FieldName "ArtistNames_Name", API.mkColumnFieldValue $ Aeson.String "AC/DC"), - ( API.FieldName "nodes_Albums", + mkRowsQueryResponse + [ [ ("ArtistIds_Id", API.mkColumnFieldValue $ Aeson.Number 1), + ("ArtistNames_Name", API.mkColumnFieldValue $ Aeson.String "AC/DC"), + ( "nodes_Albums", API.mkRelationshipFieldValue $ - rowsResponse - [ [(API.FieldName "nodes_Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You")], - [(API.FieldName "nodes_Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock")] + mkRowsQueryResponse + [ [("nodes_Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You")], + [("nodes_Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock")] ] ) ] @@ -142,57 +144,40 @@ tests _opts = describe "Aggregate Query Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Artist" :| []), - _qrTableRelationships = - [ API.TableRelationships - { _trSourceTable = API.TableName ("Artist" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "Albums", - API.Relationship - { _rTargetTable = API.TableName ("Album" :| []), - _rRelationshipType = API.ArrayRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] - } - ) - ] - } - ], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "ArtistIds_Id", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number")), - (API.FieldName "ArtistNames_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), - ( API.FieldName "nodes_Albums", - API.RelField - ( API.RelationshipField - (API.RelationshipName "Albums") - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "nodes_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")) - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } - ) - ) - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "Artist") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("ArtistIds_Id", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number")), + ("ArtistNames_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), + ( "nodes_Albums", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Albums") + ( emptyQuery + & API.qFields ?~ mkFieldsMap [("nodes_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string"))] + ) + ) + ) + ] + & API.qLimit ?~ 1 + ) + & API.qrTableRelationships + .~ [ API.TableRelationships + { _trSourceTable = mkTableName "Artist", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Albums", + API.Relationship + { _rTargetTable = mkTableName "Album", + _rRelationshipType = API.ArrayRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] + } + ) + ] + } + ] ) mockAgentGraphqlTest "works with multiple aggregate fields and through array relations" $ \_testEnv performGraphqlRequest -> do @@ -224,28 +209,28 @@ tests _opts = describe "Aggregate Query Tests" $ do } |] let aggregates = - [ (API.FieldName "counts_count", Aeson.Number 2), - (API.FieldName "counts_uniqueBillingCountries", Aeson.Number 2), - (API.FieldName "ids_minimum_Id", Aeson.Number 1), - (API.FieldName "ids_max_InvoiceId", Aeson.Number 2) + [ ("counts_count", Aeson.Number 2), + ("counts_uniqueBillingCountries", Aeson.Number 2), + ("ids_minimum_Id", Aeson.Number 1), + ("ids_max_InvoiceId", Aeson.Number 2) ] rows = - [ [ ( API.FieldName "nodes_Lines", + [ [ ( "nodes_Lines", API.mkRelationshipFieldValue $ - aggregatesResponse - [ (API.FieldName "aggregate_count", Aeson.Number 2) + mkAggregatesQueryResponse + [ ("aggregate_count", Aeson.Number 2) ] ) ], - [ ( API.FieldName "nodes_Lines", + [ ( "nodes_Lines", API.mkRelationshipFieldValue $ - aggregatesResponse - [ (API.FieldName "aggregate_count", Aeson.Number 4) + mkAggregatesQueryResponse + [ ("aggregate_count", Aeson.Number 4) ] ) ] ] - let mockConfig = Mock.chinookMock & mockQueryResponse (aggregatesAndRowsResponse aggregates rows) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkQueryResponse rows aggregates) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -273,72 +258,47 @@ tests _opts = describe "Aggregate Query Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Invoice" :| []), - _qrTableRelationships = - [ API.TableRelationships - { _trSourceTable = API.TableName ("Invoice" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "InvoiceLines", - API.Relationship - { _rTargetTable = API.TableName ("InvoiceLine" :| []), - _rRelationshipType = API.ArrayRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "InvoiceId", API.ColumnName "InvoiceId")] - } - ) - ] - } - ], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ ( API.FieldName "nodes_Lines", - API.RelField - ( API.RelationshipField - (API.RelationshipName "InvoiceLines") - API.Query - { _qFields = Nothing, - _qAggregates = - Just $ - HashMap.fromList - [(API.FieldName "aggregate_count", API.StarCount)], - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } - ) - ) - ], - _qAggregates = - Just $ - HashMap.fromList - [ (API.FieldName "counts_count", API.StarCount), - (API.FieldName "counts_uniqueBillingCountries", API.ColumnCount (API.ColumnCountAggregate (API.ColumnName "BillingCountry") True)), - (API.FieldName "ids_minimum_Id", API.SingleColumn (singleColumnAggregateMin (API.ColumnName "InvoiceId") (API.ScalarType "number"))), - (API.FieldName "ids_max_InvoiceId", API.SingleColumn (singleColumnAggregateMax (API.ColumnName "InvoiceId") (API.ScalarType "number"))) - ], - _qLimit = Just 2, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "Invoice") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ( "nodes_Lines", + API.RelField + ( API.RelationshipField + (API.RelationshipName "InvoiceLines") + ( emptyQuery & API.qAggregates ?~ mkFieldsMap [("aggregate_count", API.StarCount)] + ) + ) + ) + ] + & API.qAggregates + ?~ mkFieldsMap + [ ("counts_count", API.StarCount), + ("counts_uniqueBillingCountries", API.ColumnCount (API.ColumnCountAggregate (API.ColumnName "BillingCountry") True)), + ("ids_minimum_Id", API.SingleColumn (singleColumnAggregateMin (API.ColumnName "InvoiceId") (API.ScalarType "number"))), + ("ids_max_InvoiceId", API.SingleColumn (singleColumnAggregateMax (API.ColumnName "InvoiceId") (API.ScalarType "number"))) + ] + & API.qLimit ?~ 2 + & API.qAggregatesLimit ?~ 2 + ) + & API.qrTableRelationships + .~ [ API.TableRelationships + { _trSourceTable = mkTableName "Invoice", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "InvoiceLines", + API.Relationship + { _rTargetTable = mkTableName "InvoiceLine", + _rRelationshipType = API.ArrayRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "InvoiceId", API.ColumnName "InvoiceId")] + } + ) + ] + } + ] ) -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing - -aggregatesResponse :: [(API.FieldName, Aeson.Value)] -> API.QueryResponse -aggregatesResponse aggregates = API.QueryResponse Nothing (Just $ HashMap.fromList aggregates) - -aggregatesAndRowsResponse :: [(API.FieldName, Aeson.Value)] -> [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -aggregatesAndRowsResponse aggregates rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) (Just $ HashMap.fromList aggregates) - singleColumnAggregateMax :: API.ColumnName -> API.ScalarType -> API.SingleColumnAggregate singleColumnAggregateMax = API.SingleColumnAggregate $ API.SingleColumnAggregateFunction [G.name|max|] diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/BasicQuerySpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/BasicQuerySpec.hs index c614881521e..1df9b4e5243 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/BasicQuerySpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/BasicQuerySpec.hs @@ -6,9 +6,9 @@ module Test.DataConnector.MockAgent.BasicQuerySpec (spec) where -------------------------------------------------------------------------------- +import Control.Lens ((?~)) 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, mockQueryResponse) import Harness.Backend.DataConnector.Mock qualified as Mock @@ -20,6 +20,7 @@ import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment) import Harness.Yaml (shouldBeYaml) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (SpecWith, describe, shouldBe) -------------------------------------------------------------------------------- @@ -73,19 +74,19 @@ sourceMetadata = configuration: custom_root_fields: select: artists - select_by_pk: artists_by_pk column_config: ArtistId: custom_name: id Name: custom_name: name - array_relationships: - - name: albums - using: - manual_configuration: - remote_table: [Album] - column_mapping: - ArtistId: ArtistId + select_permissions: + - role: *testRoleName + permission: + columns: + - ArtistId + - Name + limit: 3 + filter: {} - table: [Employee] - table: [Customer] select_permissions: @@ -118,9 +119,9 @@ tests _opts = describe "Basic Tests" $ do } |] let queryResponse = - rowsResponse - [ [ (API.FieldName "id", API.mkColumnFieldValue $ Aeson.Number 1), - (API.FieldName "title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") + mkRowsQueryResponse + [ [ ("id", API.mkColumnFieldValue $ Aeson.Number 1), + ("title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") ] ] let mockConfig = Mock.chinookMock & mockQueryResponse queryResponse @@ -138,25 +139,70 @@ tests _opts = describe "Basic Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Album" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), - (API.FieldName "title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")) - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = Nothing + mkQueryRequest + (mkTableName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), + ("title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")) + ] + & API.qLimit ?~ 1 + ) + ) + + mockAgentGraphqlTest "permissions-based row limits are applied" $ \_testEnv performGraphqlRequest -> do + let headers = [("X-Hasura-Role", testRoleName)] + let graphqlRequest = + [graphql| + query getArtists { + artists(limit: 5) { + id + name } + } + |] + let queryResponse = + mkRowsQueryResponse + [ [ ("id", API.mkColumnFieldValue $ Aeson.Number 1), + ("name", API.mkColumnFieldValue $ Aeson.String "AC/DC") + ], + [ ("id", API.mkColumnFieldValue $ Aeson.Number 2), + ("name", API.mkColumnFieldValue $ Aeson.String "Accept") + ], + [ ("id", API.mkColumnFieldValue $ Aeson.Number 3), + ("name", API.mkColumnFieldValue $ Aeson.String "Aerosmith") + ] + ] + let mockConfig = Mock.chinookMock & mockQueryResponse queryResponse + + MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest + + _mrrResponse + `shouldBeYaml` [yaml| + data: + artists: + - id: 1 + name: AC/DC + - id: 2 + name: Accept + - id: 3 + name: Aerosmith + |] + + _mrrRecordedRequest + `shouldBe` Just + ( Query $ + mkQueryRequest + (mkTableName "Artist") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("id", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), + ("name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + ] + & API.qLimit ?~ 3 -- The permissions limit is smaller than the query limit, so it is used + ) ) mockAgentGraphqlTest "works with an exists-based permissions filter" $ \_testEnv performGraphqlRequest -> do @@ -173,12 +219,12 @@ tests _opts = describe "Basic Tests" $ do } |] let queryResponse = - rowsResponse - [ [ (API.FieldName "CustomerId", API.mkColumnFieldValue $ Aeson.Number 1) + mkRowsQueryResponse + [ [ ("CustomerId", API.mkColumnFieldValue $ Aeson.Number 1) ], - [ (API.FieldName "CustomerId", API.mkColumnFieldValue $ Aeson.Number 2) + [ ("CustomerId", API.mkColumnFieldValue $ Aeson.Number 2) ], - [ (API.FieldName "CustomerId", API.mkColumnFieldValue $ Aeson.Number 3) + [ ("CustomerId", API.mkColumnFieldValue $ Aeson.Number 3) ] ] let mockConfig = Mock.chinookMock & mockQueryResponse queryResponse @@ -197,31 +243,20 @@ tests _opts = describe "Basic Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Customer" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "CustomerId", API.ColumnField (API.ColumnName "CustomerId") $ API.ScalarType "number") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = - Just $ - API.Exists (API.UnrelatedTable $ API.TableName ("Employee" :| [])) $ - API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "EmployeeId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 1) (API.ScalarType "number")), - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "Customer") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("CustomerId", API.ColumnField (API.ColumnName "CustomerId") $ API.ScalarType "number") + ] + & API.qWhere + ?~ API.Exists + (API.UnrelatedTable $ mkTableName "Employee") + ( API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "EmployeeId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 1) (API.ScalarType "number")) + ) + ) ) - -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/CustomScalarsSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/CustomScalarsSpec.hs index a238c56a0d9..663604dd161 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/CustomScalarsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/CustomScalarsSpec.hs @@ -6,8 +6,8 @@ module Test.DataConnector.MockAgent.CustomScalarsSpec (spec) where -------------------------------------------------------------------------------- +import Control.Lens ((?~)) import Data.Aeson qualified as Aeson -import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE import Harness.Backend.DataConnector.Mock (AgentRequest (..), MockRequestResults (..), mockAgentGraphqlTest, mockQueryResponse) import Harness.Backend.DataConnector.Mock qualified as Mock @@ -21,6 +21,7 @@ import Hasura.Backends.DataConnector.API (ColumnName (..), ScalarType (..), Scal import Hasura.Backends.DataConnector.API qualified as API import Hasura.Backends.DataConnector.API.V0.Expression import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (SpecWith, describe, shouldBe) -------------------------------------------------------------------------------- @@ -67,7 +68,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse customScalarsTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse customScalarsTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -86,29 +87,20 @@ tests _opts = describe "Custom scalar parsing tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("MyCustomScalarsTable" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "MyIntColumn", API.ColumnField (API.ColumnName "MyIntColumn") $ API.ScalarType "MyInt"), - (API.FieldName "MyFloatColumn", API.ColumnField (API.ColumnName "MyFloatColumn") $ API.ScalarType "MyFloat"), - (API.FieldName "MyStringColumn", API.ColumnField (API.ColumnName "MyStringColumn") $ API.ScalarType "MyString"), - (API.FieldName "MyBooleanColumn", API.ColumnField (API.ColumnName "MyBooleanColumn") $ API.ScalarType "MyBoolean"), - (API.FieldName "MyIDColumn", API.ColumnField (API.ColumnName "MyIDColumn") $ API.ScalarType "MyID"), - (API.FieldName "MyAnythingColumn", API.ColumnField (API.ColumnName "MyAnythingColumn") $ API.ScalarType "MyAnything") - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "MyCustomScalarsTable") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("MyIntColumn", API.ColumnField (API.ColumnName "MyIntColumn") $ API.ScalarType "MyInt"), + ("MyFloatColumn", API.ColumnField (API.ColumnName "MyFloatColumn") $ API.ScalarType "MyFloat"), + ("MyStringColumn", API.ColumnField (API.ColumnName "MyStringColumn") $ API.ScalarType "MyString"), + ("MyBooleanColumn", API.ColumnField (API.ColumnName "MyBooleanColumn") $ API.ScalarType "MyBoolean"), + ("MyIDColumn", API.ColumnField (API.ColumnName "MyIDColumn") $ API.ScalarType "MyID"), + ("MyAnythingColumn", API.ColumnField (API.ColumnName "MyAnythingColumn") $ API.ScalarType "MyAnything") + ] + & API.qLimit ?~ 1 + ) ) mockAgentGraphqlTest "parses scalar literals in where queries" $ \_testEnv performGraphqlRequest -> do @@ -133,7 +125,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse customScalarsTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse customScalarsTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -152,56 +144,47 @@ tests _opts = describe "Custom scalar parsing tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("MyCustomScalarsTable" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "MyIntColumn", API.ColumnField (API.ColumnName "MyIntColumn") $ API.ScalarType "MyInt"), - (API.FieldName "MyFloatColumn", API.ColumnField (API.ColumnName "MyFloatColumn") $ API.ScalarType "MyFloat"), - (API.FieldName "MyStringColumn", API.ColumnField (API.ColumnName "MyStringColumn") $ API.ScalarType "MyString"), - (API.FieldName "MyBooleanColumn", API.ColumnField (API.ColumnName "MyBooleanColumn") $ API.ScalarType "MyBoolean"), - (API.FieldName "MyIDColumn", API.ColumnField (API.ColumnName "MyIDColumn") $ API.ScalarType "MyID"), - (API.FieldName "MyAnythingColumn", API.ColumnField (API.ColumnName "MyAnythingColumn") $ API.ScalarType "MyAnything") - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = - Just $ - And - [ ApplyBinaryComparisonOperator - Equal - (ComparisonColumn {_ccPath = CurrentTable, _ccName = ColumnName {unColumnName = "MyBooleanColumn"}, _ccColumnType = ScalarType "MyBoolean"}) - (ScalarValueComparison $ ScalarValue (Aeson.Bool True) (ScalarType "MyBoolean")), - ApplyBinaryComparisonOperator - Equal - (ComparisonColumn {_ccPath = CurrentTable, _ccName = ColumnName {unColumnName = "MyFloatColumn"}, _ccColumnType = ScalarType "MyFloat"}) - (ScalarValueComparison $ ScalarValue (Aeson.Number 3.14) (ScalarType "MyFloat")), - ApplyBinaryComparisonOperator - Equal - (ComparisonColumn {_ccPath = CurrentTable, _ccName = ColumnName {unColumnName = "MyStringColumn"}, _ccColumnType = ScalarType "MyString"}) - (ScalarValueComparison $ ScalarValue (Aeson.String "foo") (ScalarType "MyString")), - ApplyBinaryComparisonOperator - Equal - (ComparisonColumn {_ccPath = CurrentTable, _ccName = ColumnName {unColumnName = "MyIDColumn"}, _ccColumnType = ScalarType "MyID"}) - (ScalarValueComparison $ ScalarValue (Aeson.String "x") (ScalarType "MyID")), - ApplyBinaryComparisonOperator - Equal - (ComparisonColumn {_ccPath = CurrentTable, _ccName = ColumnName {unColumnName = "MyIntColumn"}, _ccColumnType = ScalarType "MyInt"}) - (ScalarValueComparison $ ScalarValue (Aeson.Number 42.0) (ScalarType "MyInt")), - ApplyBinaryComparisonOperator - Equal - (ComparisonColumn {_ccPath = CurrentTable, _ccName = ColumnName {unColumnName = "MyAnythingColumn"}, _ccColumnType = ScalarType "MyAnything"}) - (ScalarValueComparison $ ScalarValue (Aeson.Object mempty) (ScalarType "MyAnything")) - ], - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "MyCustomScalarsTable") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("MyIntColumn", API.ColumnField (API.ColumnName "MyIntColumn") $ API.ScalarType "MyInt"), + ("MyFloatColumn", API.ColumnField (API.ColumnName "MyFloatColumn") $ API.ScalarType "MyFloat"), + ("MyStringColumn", API.ColumnField (API.ColumnName "MyStringColumn") $ API.ScalarType "MyString"), + ("MyBooleanColumn", API.ColumnField (API.ColumnName "MyBooleanColumn") $ API.ScalarType "MyBoolean"), + ("MyIDColumn", API.ColumnField (API.ColumnName "MyIDColumn") $ API.ScalarType "MyID"), + ("MyAnythingColumn", API.ColumnField (API.ColumnName "MyAnythingColumn") $ API.ScalarType "MyAnything") + ] + & API.qLimit ?~ 1 + & API.qWhere + ?~ And + [ ApplyBinaryComparisonOperator + Equal + (ComparisonColumn CurrentTable (ColumnName "MyBooleanColumn") (ScalarType "MyBoolean")) + (ScalarValueComparison $ ScalarValue (Aeson.Bool True) (ScalarType "MyBoolean")), + ApplyBinaryComparisonOperator + Equal + (ComparisonColumn CurrentTable (ColumnName "MyFloatColumn") (ScalarType "MyFloat")) + (ScalarValueComparison $ ScalarValue (Aeson.Number 3.14) (ScalarType "MyFloat")), + ApplyBinaryComparisonOperator + Equal + (ComparisonColumn CurrentTable (ColumnName "MyStringColumn") (ScalarType "MyString")) + (ScalarValueComparison $ ScalarValue (Aeson.String "foo") (ScalarType "MyString")), + ApplyBinaryComparisonOperator + Equal + (ComparisonColumn CurrentTable (ColumnName "MyIDColumn") (ScalarType "MyID")) + (ScalarValueComparison $ ScalarValue (Aeson.String "x") (ScalarType "MyID")), + ApplyBinaryComparisonOperator + Equal + (ComparisonColumn CurrentTable (ColumnName "MyIntColumn") (ScalarType "MyInt")) + (ScalarValueComparison $ ScalarValue (Aeson.Number 42.0) (ScalarType "MyInt")), + ApplyBinaryComparisonOperator + Equal + (ComparisonColumn CurrentTable (ColumnName "MyAnythingColumn") (ScalarType "MyAnything")) + (ScalarValueComparison $ ScalarValue (Aeson.Object mempty) (ScalarType "MyAnything")) + ] + ) ) mockAgentGraphqlTest "fails parsing float when expecting int" $ \_testEnv performGraphqlRequest -> do @@ -216,7 +199,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -243,7 +226,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -270,7 +253,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -297,7 +280,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -320,7 +303,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -347,7 +330,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -374,7 +357,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -401,7 +384,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -428,7 +411,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -455,7 +438,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -482,7 +465,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -509,7 +492,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -536,7 +519,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -559,7 +542,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -586,7 +569,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -613,7 +596,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -636,7 +619,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -659,7 +642,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -682,7 +665,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -705,7 +688,7 @@ tests _opts = describe "Custom scalar parsing tests" $ do } } |] - let mockConfig = Mock.chinookMock & mockQueryResponse (rowsResponse myIntTable) + let mockConfig = Mock.chinookMock & mockQueryResponse (mkRowsQueryResponse myIntTable) MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest @@ -717,18 +700,15 @@ tests _opts = describe "Custom scalar parsing tests" $ do |] where customScalarsTable = - [ [ (API.FieldName "MyIntColumn", API.mkColumnFieldValue $ Aeson.Number 42), - (API.FieldName "MyFloatColumn", API.mkColumnFieldValue $ Aeson.Number 3.14), - (API.FieldName "MyStringColumn", API.mkColumnFieldValue $ Aeson.String "foo"), - (API.FieldName "MyBooleanColumn", API.mkColumnFieldValue $ Aeson.Bool True), - (API.FieldName "MyIDColumn", API.mkColumnFieldValue $ Aeson.String "x"), - (API.FieldName "MyAnythingColumn", API.mkColumnFieldValue $ Aeson.Object mempty) + [ [ ("MyIntColumn", API.mkColumnFieldValue $ Aeson.Number 42), + ("MyFloatColumn", API.mkColumnFieldValue $ Aeson.Number 3.14), + ("MyStringColumn", API.mkColumnFieldValue $ Aeson.String "foo"), + ("MyBooleanColumn", API.mkColumnFieldValue $ Aeson.Bool True), + ("MyIDColumn", API.mkColumnFieldValue $ Aeson.String "x"), + ("MyAnythingColumn", API.mkColumnFieldValue $ Aeson.Object mempty) ] ] myIntTable = - [ [ (API.FieldName "MyIntColumn", API.mkColumnFieldValue $ Aeson.Number 42) + [ [ ("MyIntColumn", API.mkColumnFieldValue $ Aeson.Number 42) ] ] - -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/DeleteMutationsSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/DeleteMutationsSpec.hs index 9ff4cf3941e..2f4037622ae 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/DeleteMutationsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/DeleteMutationsSpec.hs @@ -3,6 +3,7 @@ module Test.DataConnector.MockAgent.DeleteMutationsSpec ) where +import Control.Lens ((.~), (?~)) import Data.Aeson qualified as Aeson import Data.ByteString (ByteString) import Data.HashMap.Strict qualified as HashMap @@ -17,6 +18,7 @@ import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment) import Harness.Yaml import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec -------------------------------------------------------------------------------- @@ -107,38 +109,38 @@ tests _opts = do { 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", + [ mkFieldsMap + [ ("deletedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 112), + ("deletedRows_Title", API.mkColumnFieldValue $ Aeson.String "The Number of The Beast"), + ( "deletedRows_Artist", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), - (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden") + mkRowsQueryResponse + [ [ ("ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), + ("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", + mkFieldsMap + [ ("deletedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 113), + ("deletedRows_Title", API.mkColumnFieldValue $ Aeson.String "The X Factor"), + ( "deletedRows_Artist", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), - (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden") + mkRowsQueryResponse + [ [ ("ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), + ("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", + mkFieldsMap + [ ("deletedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 114), + ("deletedRows_Title", API.mkColumnFieldValue $ Aeson.String "Virtual XI"), + ( "deletedRows_Artist", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), - (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden") + mkRowsQueryResponse + [ [ ("ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), + ("Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden") ] ] ) @@ -174,66 +176,58 @@ tests _opts = do |] 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.ScalarValueComparison $ API.ScalarValue (Aeson.Number 90) (API.ScalarType "number")), - API.ApplyBinaryComparisonOperator - API.GreaterThan - (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") - (API.ScalarValueComparison $ 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 - } - ) - ) - ] - } - ] - } + emptyMutationRequest + & API.mrTableRelationships + .~ [ API.TableRelationships + { API._trSourceTable = mkTableName "Album", + API._trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Artist", + API.Relationship + { API._rTargetTable = mkTableName "Artist", + API._rRelationshipType = API.ObjectRelationship, + API._rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] + } + ) + ] + } + ] + & API.mrOperations + .~ [ API.DeleteOperation $ + API.DeleteMutationOperation + { API._dmoTable = mkTableName "Album", + API._dmoWhere = + Just $ + API.And + [ API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "ArtistId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 90) (API.ScalarType "number")), + API.ApplyBinaryComparisonOperator + API.GreaterThan + (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 111) (API.ScalarType "number")) + ], + API._dmoReturningFields = + mkFieldsMap + [ ("deletedRows_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), + ("deletedRows_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")), + ( "deletedRows_Artist", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Artist") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), + ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + ] + ) + ) + ) + ] + } + ] _mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest) mockAgentGraphqlTest "delete row by pk with delete permissions" $ \_testEnv performGraphqlRequest -> do @@ -257,14 +251,14 @@ tests _opts = do { 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", + [ mkFieldsMap + [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 112), + ("Title", API.mkColumnFieldValue $ Aeson.String "The Number of The Beast"), + ( "Artist", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), - (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden") + mkRowsQueryResponse + [ [ ("ArtistId", API.mkColumnFieldValue $ Aeson.Number 90), + ("Name", API.mkColumnFieldValue $ Aeson.String "Iron Maiden") ] ] ) @@ -288,67 +282,56 @@ tests _opts = do |] 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.ScalarValueComparison $ API.ScalarValue (Aeson.Number 90) (API.ScalarType "number")), - API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") - (API.ScalarValueComparison $ 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 - } - ) - ) - ] - } - ] - } + emptyMutationRequest + & API.mrTableRelationships + .~ [ API.TableRelationships + { API._trSourceTable = mkTableName "Album", + API._trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Artist", + API.Relationship + { API._rTargetTable = mkTableName "Artist", + API._rRelationshipType = API.ObjectRelationship, + API._rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] + } + ) + ] + } + ] + & API.mrOperations + .~ [ API.DeleteOperation $ + API.DeleteMutationOperation + { API._dmoTable = mkTableName "Album", + API._dmoWhere = + Just $ + API.And + [ API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "ArtistId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 90) (API.ScalarType "number")), + API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 112) (API.ScalarType "number")) + ], + API._dmoReturningFields = + mkFieldsMap + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), + ("Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")), + ( "Artist", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Artist") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), + ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + ] + ) + ) + ) + ] + } + ] _mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest) - -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/ErrorSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/ErrorSpec.hs index 09c64c46b65..1dde9cdcef1 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/ErrorSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/ErrorSpec.hs @@ -6,8 +6,8 @@ module Test.DataConnector.MockAgent.ErrorSpec (spec) where -------------------------------------------------------------------------------- +import Control.Lens ((?~)) import Data.Aeson qualified as Aeson -import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE import Harness.Backend.DataConnector.Mock (AgentRequest (..), MockRequestResults (..), mockAgentGraphqlTest) import Harness.Backend.DataConnector.Mock qualified as Mock @@ -19,6 +19,7 @@ import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment) import Harness.Yaml (shouldBeYaml) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (SpecWith, describe, shouldBe) -------------------------------------------------------------------------------- @@ -92,23 +93,14 @@ tests _opts = describe "Error Protocol Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Album" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "id", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - (API.FieldName "title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("id", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), + ("title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + ] + & API.qLimit ?~ 1 + ) ) diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/InsertMutationsSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/InsertMutationsSpec.hs index 0b83f3e8c60..dc6548bbe9c 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/InsertMutationsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/InsertMutationsSpec.hs @@ -3,6 +3,7 @@ module Test.DataConnector.MockAgent.InsertMutationsSpec ) where +import Control.Lens ((.~), (?~)) import Data.Aeson qualified as Aeson import Data.ByteString (ByteString) import Data.HashMap.Strict qualified as HashMap @@ -17,6 +18,7 @@ import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment) import Harness.Yaml import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec -------------------------------------------------------------------------------- @@ -116,26 +118,26 @@ tests _opts = do { API._morAffectedRows = 2, API._morReturning = Just - [ HashMap.fromList - [ (API.FieldName "insertedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 9001), - (API.FieldName "insertedRows_Title", API.mkColumnFieldValue $ Aeson.String "Super Mega Rock"), - ( API.FieldName "insertedRows_Artist", + [ mkFieldsMap + [ ("insertedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 9001), + ("insertedRows_Title", API.mkColumnFieldValue $ Aeson.String "Super Mega Rock"), + ( "insertedRows_Artist", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 2), - (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Accept") + mkRowsQueryResponse + [ [ ("ArtistId", API.mkColumnFieldValue $ Aeson.Number 2), + ("Name", API.mkColumnFieldValue $ Aeson.String "Accept") ] ] ) ], - HashMap.fromList - [ (API.FieldName "insertedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 9002), - (API.FieldName "insertedRows_Title", API.mkColumnFieldValue $ Aeson.String "Accept This"), - ( API.FieldName "insertedRows_Artist", + mkFieldsMap + [ ("insertedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 9002), + ("insertedRows_Title", API.mkColumnFieldValue $ Aeson.String "Accept This"), + ( "insertedRows_Artist", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 2), - (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Accept") + mkRowsQueryResponse + [ [ ("ArtistId", API.mkColumnFieldValue $ Aeson.Number 2), + ("Name", API.mkColumnFieldValue $ Aeson.String "Accept") ] ] ) @@ -166,85 +168,75 @@ tests _opts = do |] 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.TableInsertSchema - { API._tisTable = API.TableName ("Album" :| []), - API._tisFields = - HashMap.fromList - [ (API.FieldName "AlbumId", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "AlbumId") (API.ScalarType "number")), - (API.FieldName "ArtistId", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "ArtistId") (API.ScalarType "number")), - (API.FieldName "Title", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "Title") (API.ScalarType "string")) - ] - } - ], - API._mrOperations = - [ API.InsertOperation $ - API.InsertMutationOperation - { API._imoTable = API.TableName ("Album" :| []), - API._imoRows = - [ API.RowObject $ - HashMap.fromList - [ (API.FieldName "AlbumId", API.mkColumnInsertFieldValue $ Aeson.Number 9001), - (API.FieldName "ArtistId", API.mkColumnInsertFieldValue $ Aeson.Number 2), - (API.FieldName "Title", API.mkColumnInsertFieldValue $ Aeson.String "Super Mega Rock") - ], - API.RowObject $ - HashMap.fromList - [ (API.FieldName "AlbumId", API.mkColumnInsertFieldValue $ Aeson.Number 9002), - (API.FieldName "ArtistId", API.mkColumnInsertFieldValue $ Aeson.Number 2), - (API.FieldName "Title", API.mkColumnInsertFieldValue $ Aeson.String "Accept This") - ] - ], - API._imoPostInsertCheck = - Just $ - API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "ArtistId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 2) (API.ScalarType "number")), - API._imoReturningFields = - HashMap.fromList - [ (API.FieldName "insertedRows_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), - (API.FieldName "insertedRows_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")), - ( API.FieldName "insertedRows_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 - } - ) - ) - ] - } - ] - } + emptyMutationRequest + & API.mrTableRelationships + .~ [ API.TableRelationships + { API._trSourceTable = mkTableName "Album", + API._trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Artist", + API.Relationship + { API._rTargetTable = mkTableName "Artist", + API._rRelationshipType = API.ObjectRelationship, + API._rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] + } + ) + ] + } + ] + & API.mrInsertSchema + .~ [ API.TableInsertSchema + { API._tisTable = mkTableName "Album", + API._tisFields = + mkFieldsMap + [ ("AlbumId", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "AlbumId") (API.ScalarType "number")), + ("ArtistId", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "ArtistId") (API.ScalarType "number")), + ("Title", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "Title") (API.ScalarType "string")) + ] + } + ] + & API.mrOperations + .~ [ API.InsertOperation $ + API.InsertMutationOperation + { API._imoTable = mkTableName "Album", + API._imoRows = + [ API.RowObject $ + mkFieldsMap + [ ("AlbumId", API.mkColumnInsertFieldValue $ Aeson.Number 9001), + ("ArtistId", API.mkColumnInsertFieldValue $ Aeson.Number 2), + ("Title", API.mkColumnInsertFieldValue $ Aeson.String "Super Mega Rock") + ], + API.RowObject $ + mkFieldsMap + [ ("AlbumId", API.mkColumnInsertFieldValue $ Aeson.Number 9002), + ("ArtistId", API.mkColumnInsertFieldValue $ Aeson.Number 2), + ("Title", API.mkColumnInsertFieldValue $ Aeson.String "Accept This") + ] + ], + API._imoPostInsertCheck = + Just $ + API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "ArtistId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 2) (API.ScalarType "number")), + API._imoReturningFields = + mkFieldsMap + [ ("insertedRows_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), + ("insertedRows_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")), + ( "insertedRows_Artist", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Artist") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), + ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + ] + ) + ) + ) + ] + } + ] _mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest) - -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/OrderBySpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/OrderBySpec.hs index 35e0973372f..643a78abbc2 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/OrderBySpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/OrderBySpec.hs @@ -5,6 +5,7 @@ module Test.DataConnector.MockAgent.OrderBySpec (spec) where -------------------------------------------------------------------------------- +import Control.Lens ((.~), (?~)) import Data.Aeson qualified as Aeson import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE @@ -19,6 +20,7 @@ import Harness.Yaml (shouldBeYaml) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude import Language.GraphQL.Draft.Syntax.QQ qualified as G +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (SpecWith, describe, shouldBe) -------------------------------------------------------------------------------- @@ -111,25 +113,17 @@ tests _opts = describe "Order By Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Album" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - (API.FieldName "Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Just 3, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Just (API.OrderBy mempty (API.OrderByElement [] (API.OrderByColumn (API.ColumnName "AlbumId")) API.Ascending :| [])) - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), + ("Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + ] + & API.qLimit ?~ 3 + & API.qOrderBy ?~ API.OrderBy mempty (API.OrderByElement [] (API.OrderByColumn (API.ColumnName "AlbumId")) API.Ascending :| []) + ) ) mockAgentGraphqlTest "can order by aggregates" $ \_testEnv performGraphqlRequest -> do @@ -162,59 +156,48 @@ tests _opts = describe "Order By Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Artist" :| []), - _qrTableRelationships = - [ API.TableRelationships - { _trSourceTable = API.TableName ("Artist" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "Albums", - API.Relationship - { _rTargetTable = API.TableName ("Album" :| []), - _rRelationshipType = API.ArrayRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] - } + mkQueryRequest + (mkTableName "Artist") + ( emptyQuery + & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string"))] + & API.qLimit ?~ 2 + & API.qOrderBy + ?~ API.OrderBy + ( HashMap.fromList + [ ( API.RelationshipName "Albums", + API.OrderByRelation Nothing mempty + ) + ] + ) + ( NE.fromList + [ API.OrderByElement [API.RelationshipName "Albums"] API.OrderByStarCountAggregate API.Ascending, + API.OrderByElement + [API.RelationshipName "Albums"] + ( API.OrderBySingleColumnAggregate $ + API.SingleColumnAggregate + (API.SingleColumnAggregateFunction [G.name|max|]) + (API.ColumnName "AlbumId") + (API.ScalarType "number") ) - ] - } - ], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")) - ], - _qAggregates = Nothing, - _qLimit = Just 2, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = - Just $ - API.OrderBy - ( HashMap.fromList - [ ( API.RelationshipName "Albums", - API.OrderByRelation Nothing mempty - ) - ] - ) - ( NE.fromList - [ API.OrderByElement [API.RelationshipName "Albums"] API.OrderByStarCountAggregate API.Ascending, - API.OrderByElement - [API.RelationshipName "Albums"] - ( API.OrderBySingleColumnAggregate $ - API.SingleColumnAggregate - (API.SingleColumnAggregateFunction [G.name|max|]) - (API.ColumnName "AlbumId") - (API.ScalarType "number") - ) - API.Ascending - ] - ) - }, - _qrForeach = Nothing - } + API.Ascending + ] + ) + ) + & API.qrTableRelationships + .~ [ API.TableRelationships + { _trSourceTable = mkTableName "Artist", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Albums", + API.Relationship + { _rTargetTable = mkTableName "Album", + _rRelationshipType = API.ArrayRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] + } + ) + ] + } + ] ) rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/QueryRelationshipsSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/QueryRelationshipsSpec.hs index 9d8b7b6b356..383680148b0 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/QueryRelationshipsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/QueryRelationshipsSpec.hs @@ -5,6 +5,7 @@ module Test.DataConnector.MockAgent.QueryRelationshipsSpec (spec) where -------------------------------------------------------------------------------- +import Control.Lens ((.~), (?~)) import Data.Aeson qualified as Aeson import Data.ByteString (ByteString) import Data.HashMap.Strict qualified as HashMap @@ -19,6 +20,7 @@ import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment) import Harness.Yaml (shouldBeYaml) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (SpecWith, describe, shouldBe) -------------------------------------------------------------------------------- @@ -145,18 +147,18 @@ tests _opts = describe "Object Relationships Tests" $ do } |] let queryResponse = - rowsResponse - [ [ (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock (We Salute You)"), - ( API.FieldName "Genre", + mkRowsQueryResponse + [ [ ("Name", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock (We Salute You)"), + ( "Genre", API.mkRelationshipFieldValue $ - rowsResponse - [ [(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Rock")] + mkRowsQueryResponse + [ [("Name", API.mkColumnFieldValue $ Aeson.String "Rock")] ] ), - ( API.FieldName "MediaType", + ( "MediaType", API.mkRelationshipFieldValue $ - rowsResponse - [ [(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "MPEG audio file")] + mkRowsQueryResponse + [ [("Name", API.mkColumnFieldValue $ Aeson.String "MPEG audio file")] ] ) ] @@ -179,83 +181,53 @@ tests _opts = describe "Object Relationships Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Track" :| []), - _qrTableRelationships = - [ API.TableRelationships - { _trSourceTable = API.TableName ("Track" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "Genre", - API.Relationship - { _rTargetTable = API.TableName ("Genre" :| []), - _rRelationshipType = API.ObjectRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "GenreId", API.ColumnName "GenreId")] - } - ), - ( API.RelationshipName "MediaType", - API.Relationship - { _rTargetTable = API.TableName ("MediaType" :| []), - _rRelationshipType = API.ObjectRelationship, - _rColumnMapping = - HashMap.fromList - [(API.ColumnName "MediaTypeId", API.ColumnName "MediaTypeId")] - } - ) - ] - } - ], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string"), - ( API.FieldName "Genre", - API.RelField - ( API.RelationshipField - (API.RelationshipName "Genre") - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } - ) - ), - ( API.FieldName "MediaType", - API.RelField - ( API.RelationshipField - (API.RelationshipName "MediaType") - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } - ) - ) - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "Track") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string"), + ( "Genre", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Genre") + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) + ) + ), + ( "MediaType", + API.RelField + ( API.RelationshipField + (API.RelationshipName "MediaType") + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) + ) + ) + ] + & API.qLimit ?~ 1 + ) + & API.qrTableRelationships + .~ [ API.TableRelationships + { _trSourceTable = mkTableName "Track", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Genre", + API.Relationship + { _rTargetTable = mkTableName "Genre", + _rRelationshipType = API.ObjectRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "GenreId", API.ColumnName "GenreId")] + } + ), + ( API.RelationshipName "MediaType", + API.Relationship + { _rTargetTable = mkTableName "MediaType", + _rRelationshipType = API.ObjectRelationship, + _rColumnMapping = + HashMap.fromList + [(API.ColumnName "MediaTypeId", API.ColumnName "MediaTypeId")] + } + ) + ] + } + ] ) mockAgentGraphqlTest "works with an order by that navigates relationships" $ \_testEnv performGraphqlRequest -> do @@ -274,19 +246,19 @@ tests _opts = describe "Object Relationships Tests" $ do } |] let queryResponse = - rowsResponse - [ [ ( API.FieldName "Album", + mkRowsQueryResponse + [ [ ( "Album", API.mkRelationshipFieldValue $ - rowsResponse - [ [ ( API.FieldName "Artist", + mkRowsQueryResponse + [ [ ( "Artist", API.mkRelationshipFieldValue $ - rowsResponse - [[(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Zeca Pagodinho")]] + mkRowsQueryResponse + [[("Name", API.mkColumnFieldValue $ Aeson.String "Zeca Pagodinho")]] ) ] ] ), - (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Camarão que Dorme e Onda Leva") + ("Name", API.mkColumnFieldValue $ Aeson.String "Camarão que Dorme e Onda Leva") ] ] let mockConfig = Mock.chinookMock & mockQueryResponse queryResponse @@ -306,108 +278,83 @@ tests _opts = describe "Object Relationships Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Track" :| []), - _qrTableRelationships = - [ API.TableRelationships - { _trSourceTable = API.TableName ("Track" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "Album", - API.Relationship - { _rTargetTable = API.TableName ("Album" :| []), - _rRelationshipType = API.ObjectRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "AlbumId", API.ColumnName "AlbumId")] - } - ) - ] - }, - API.TableRelationships - { _trSourceTable = API.TableName ("Album" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "Artist", - API.Relationship - { _rTargetTable = API.TableName ("Artist" :| []), - _rRelationshipType = API.ObjectRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] - } - ) - ] - } - ], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string"), - ( API.FieldName "Album", - API.RelField - ( API.RelationshipField - (API.RelationshipName "Album") - API.Query - { _qFields = - Just $ - HashMap.fromList - [ ( API.FieldName "Artist", - API.RelField - ( API.RelationshipField - (API.RelationshipName "Artist") - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } - ) - ) - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } - ) - ) - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = - Just $ - API.OrderBy - ( HashMap.fromList - [ ( API.RelationshipName "Album", - API.OrderByRelation - Nothing - ( HashMap.fromList - [ ( API.RelationshipName "Artist", - API.OrderByRelation - Nothing - mempty - ) - ] + mkQueryRequest + (mkTableName "Track") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string"), + ( "Album", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ( "Artist", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Artist") + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string"))]) + ) + ) + ] + ) + ) + ) + ] + & API.qLimit ?~ 1 + & API.qOrderBy + ?~ API.OrderBy + ( HashMap.fromList + [ ( API.RelationshipName "Album", + API.OrderByRelation + Nothing + ( HashMap.fromList + [ ( API.RelationshipName "Artist", + API.OrderByRelation + Nothing + mempty ) - ) - ] + ] + ) ) - ( NE.fromList - [ API.OrderByElement [API.RelationshipName "Album", API.RelationshipName "Artist"] (API.OrderByColumn (API.ColumnName "Name")) API.Descending, - API.OrderByElement [] (API.OrderByColumn (API.ColumnName "Name")) API.Ascending - ] - ) - }, - _qrForeach = Nothing - } + ] + ) + ( NE.fromList + [ API.OrderByElement [API.RelationshipName "Album", API.RelationshipName "Artist"] (API.OrderByColumn (API.ColumnName "Name")) API.Descending, + API.OrderByElement [] (API.OrderByColumn (API.ColumnName "Name")) API.Ascending + ] + ) + ) + & API.qrTableRelationships + .~ [ API.TableRelationships + { _trSourceTable = mkTableName "Track", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Album", + API.Relationship + { _rTargetTable = mkTableName "Album", + _rRelationshipType = API.ObjectRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "AlbumId", API.ColumnName "AlbumId")] + } + ) + ] + }, + API.TableRelationships + { _trSourceTable = mkTableName "Album", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Artist", + API.Relationship + { _rTargetTable = mkTableName "Artist", + _rRelationshipType = API.ObjectRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")] + } + ) + ] + } + ] ) mockAgentGraphqlTest "works with an order by that navigates a relationship with table permissions" $ \_testEnv performGraphqlRequest -> do @@ -421,8 +368,8 @@ tests _opts = describe "Object Relationships Tests" $ do } |] let queryResponse = - rowsResponse - [ [ (API.FieldName "EmployeeId", API.mkColumnFieldValue $ Aeson.Number 3) + mkRowsQueryResponse + [ [ ("EmployeeId", API.mkColumnFieldValue $ Aeson.Number 3) ] ] let mockConfig = Mock.chinookMock & mockQueryResponse queryResponse @@ -439,75 +386,63 @@ tests _opts = describe "Object Relationships Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Employee" :| []), - _qrTableRelationships = - [ API.TableRelationships - { _trSourceTable = API.TableName ("Customer" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "SupportRep", - API.Relationship - { _rTargetTable = API.TableName ("Employee" :| []), - _rRelationshipType = API.ObjectRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "SupportRepId", API.ColumnName "EmployeeId")] - } - ) - ] - }, - API.TableRelationships - { _trSourceTable = API.TableName ("Employee" :| []), - _trRelationships = - HashMap.fromList - [ ( API.RelationshipName "SupportRepForCustomers", - API.Relationship - { _rTargetTable = API.TableName ("Customer" :| []), - _rRelationshipType = API.ArrayRelationship, - _rColumnMapping = HashMap.fromList [(API.ColumnName "EmployeeId", API.ColumnName "SupportRepId")] - } - ) - ] - } - ], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "EmployeeId", API.ColumnField (API.ColumnName "EmployeeId") $ API.ScalarType "number") - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = - Just $ - API.Exists (API.RelatedTable $ API.RelationshipName "SupportRepForCustomers") $ - API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "Country") $ API.ScalarType "string") - (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.ColumnName "Country") $ API.ScalarType "string")), - _qOrderBy = - Just $ - API.OrderBy - ( HashMap.fromList - [ ( API.RelationshipName "SupportRepForCustomers", - API.OrderByRelation - ( Just $ - API.Exists (API.RelatedTable $ API.RelationshipName "SupportRep") $ - API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "Country") $ API.ScalarType "string") - (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.ColumnName "Country") $ API.ScalarType "string")) - ) - mempty - ) - ] + mkQueryRequest + (mkTableName "Employee") + ( emptyQuery + & API.qFields ?~ mkFieldsMap [("EmployeeId", API.ColumnField (API.ColumnName "EmployeeId") $ API.ScalarType "number")] + & API.qLimit ?~ 1 + & API.qWhere + ?~ API.Exists + (API.RelatedTable $ API.RelationshipName "SupportRepForCustomers") + ( API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "Country") $ API.ScalarType "string") + (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.ColumnName "Country") $ API.ScalarType "string")) + ) + & API.qOrderBy + ?~ API.OrderBy + ( HashMap.fromList + [ ( API.RelationshipName "SupportRepForCustomers", + API.OrderByRelation + ( Just $ + API.Exists (API.RelatedTable $ API.RelationshipName "SupportRep") $ + API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "Country") $ API.ScalarType "string") + (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.ColumnName "Country") $ API.ScalarType "string")) + ) + mempty ) - (API.OrderByElement [API.RelationshipName "SupportRepForCustomers"] API.OrderByStarCountAggregate API.Descending :| []) - }, - _qrForeach = Nothing - } + ] + ) + (API.OrderByElement [API.RelationshipName "SupportRepForCustomers"] API.OrderByStarCountAggregate API.Descending :| []) + ) + & API.qrTableRelationships + .~ [ API.TableRelationships + { _trSourceTable = mkTableName "Customer", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "SupportRep", + API.Relationship + { _rTargetTable = mkTableName "Employee", + _rRelationshipType = API.ObjectRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "SupportRepId", API.ColumnName "EmployeeId")] + } + ) + ] + }, + API.TableRelationships + { _trSourceTable = mkTableName "Employee", + _trRelationships = + HashMap.fromList + [ ( API.RelationshipName "SupportRepForCustomers", + API.Relationship + { _rTargetTable = mkTableName "Customer", + _rRelationshipType = API.ArrayRelationship, + _rColumnMapping = HashMap.fromList [(API.ColumnName "EmployeeId", API.ColumnName "SupportRepId")] + } + ) + ] + } + ] ) - -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/RemoteRelationshipsSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/RemoteRelationshipsSpec.hs index 9b14a2ec122..047075d82e7 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/RemoteRelationshipsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/RemoteRelationshipsSpec.hs @@ -5,7 +5,7 @@ module Test.DataConnector.MockAgent.RemoteRelationshipsSpec (spec) where -------------------------------------------------------------------------------- -import Control.Lens ((.~), _Just) +import Control.Lens ((.~), (?~), _Just) import Data.Aeson qualified as Aeson import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE @@ -24,6 +24,7 @@ import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment) import Harness.Yaml (shouldBeYaml, shouldReturnYaml) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (HasCallStack, SpecWith, describe, it, shouldBe) -------------------------------------------------------------------------------- @@ -211,27 +212,27 @@ tests _opts = do } |] let queryResponse = - mkRowsResponse - [ [ ( API.FieldName "query", + mkRowsQueryResponse + [ [ ( "query", API.mkRelationshipFieldValue $ - mkRowsResponse - [ [ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 1), - (API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") + mkRowsQueryResponse + [ [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 1), + ("Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") ], - [ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 4), - (API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock") + [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 4), + ("Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock") ] ] ) ], - [ ( API.FieldName "query", + [ ( "query", API.mkRelationshipFieldValue $ - mkRowsResponse - [ [ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 2), - (API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "Balls to the Wall") + mkRowsQueryResponse + [ [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 2), + ("Title", API.mkColumnFieldValue $ Aeson.String "Balls to the Wall") ], - [ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 3), - (API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "Restless and Wild") + [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 3), + ("Title", API.mkColumnFieldValue $ Aeson.String "Restless and Wild") ] ] ) @@ -264,30 +265,20 @@ tests _opts = do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Album" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - (API.FieldName "Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = - Just $ - NonEmpty.fromList - [ HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 1) (API.ScalarType "number"))], - HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 2) (API.ScalarType "number"))] + mkQueryRequest + (mkTableName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), + ("Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") ] - } + ) + & API.qrForeach + ?~ NonEmpty.fromList + [ HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 1) (API.ScalarType "number"))], + HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 2) (API.ScalarType "number"))] + ] ) mockAgentGraphqlTest "can act as the target of a remote object relationship" $ \testEnv performGraphqlRequest -> do @@ -307,30 +298,30 @@ tests _opts = do } |] let queryResponse = - mkRowsResponse - [ [ ( API.FieldName "query", + mkRowsQueryResponse + [ [ ( "query", API.mkRelationshipFieldValue $ - mkRowsResponse - [ [ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 3), - (API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "Restless and Wild") + mkRowsQueryResponse + [ [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 3), + ("Title", API.mkColumnFieldValue $ Aeson.String "Restless and Wild") ] ] ) ], - [ ( API.FieldName "query", + [ ( "query", API.mkRelationshipFieldValue $ - mkRowsResponse - [ [ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 1), - (API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") + mkRowsQueryResponse + [ [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 1), + ("Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") ] ] ) ], - [ ( API.FieldName "query", + [ ( "query", API.mkRelationshipFieldValue $ - mkRowsResponse - [ [ (API.FieldName "AlbumId", API.mkColumnFieldValue $ Aeson.Number 4), - (API.FieldName "Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock") + mkRowsQueryResponse + [ [ ("AlbumId", API.mkColumnFieldValue $ Aeson.Number 4), + ("Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock") ] ] ) @@ -364,31 +355,21 @@ tests _opts = do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Album" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - (API.FieldName "Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = - Just $ - NonEmpty.fromList - [ HashMap.fromList [(API.ColumnName "AlbumId", API.ScalarValue (Aeson.Number 3) (API.ScalarType "number"))], - HashMap.fromList [(API.ColumnName "AlbumId", API.ScalarValue (Aeson.Number 1) (API.ScalarType "number"))], - HashMap.fromList [(API.ColumnName "AlbumId", API.ScalarValue (Aeson.Number 4) (API.ScalarType "number"))] + mkQueryRequest + (mkTableName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), + ("Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") ] - } + ) + & API.qrForeach + ?~ NonEmpty.fromList + [ HashMap.fromList [(API.ColumnName "AlbumId", API.ScalarValue (Aeson.Number 3) (API.ScalarType "number"))], + HashMap.fromList [(API.ColumnName "AlbumId", API.ScalarValue (Aeson.Number 1) (API.ScalarType "number"))], + HashMap.fromList [(API.ColumnName "AlbumId", API.ScalarValue (Aeson.Number 4) (API.ScalarType "number"))] + ] ) mockAgentGraphqlTest "can act as the target of an aggregation over a remote array relationship" $ \testEnv performGraphqlRequest -> do @@ -413,32 +394,32 @@ tests _opts = do } |] let queryResponse = - mkRowsResponse - [ [ ( API.FieldName "query", + mkRowsQueryResponse + [ [ ( "query", API.mkRelationshipFieldValue $ mkQueryResponse - [ [ (API.FieldName "nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 1), - (API.FieldName "nodes_Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") + [ [ ("nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 1), + ("nodes_Title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") ], - [ (API.FieldName "nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 4), - (API.FieldName "nodes_Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock") + [ ("nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 4), + ("nodes_Title", API.mkColumnFieldValue $ Aeson.String "Let There Be Rock") ] ] - [ (API.FieldName "aggregate_count", Aeson.Number 2) + [ ("aggregate_count", Aeson.Number 2) ] ) ], - [ ( API.FieldName "query", + [ ( "query", API.mkRelationshipFieldValue $ mkQueryResponse - [ [ (API.FieldName "nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 2), - (API.FieldName "nodes_Title", API.mkColumnFieldValue $ Aeson.String "Balls to the Wall") + [ [ ("nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 2), + ("nodes_Title", API.mkColumnFieldValue $ Aeson.String "Balls to the Wall") ], - [ (API.FieldName "nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 3), - (API.FieldName "nodes_Title", API.mkColumnFieldValue $ Aeson.String "Restless and Wild") + [ ("nodes_AlbumId", API.mkColumnFieldValue $ Aeson.Number 3), + ("nodes_Title", API.mkColumnFieldValue $ Aeson.String "Restless and Wild") ] ] - [ (API.FieldName "aggregate_count", Aeson.Number 2) + [ ("aggregate_count", Aeson.Number 2) ] ) ] @@ -476,41 +457,23 @@ tests _opts = do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Album" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "nodes_AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - (API.FieldName "nodes_Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") - ], - _qAggregates = - Just $ - HashMap.fromList - [(API.FieldName "aggregate_count", API.StarCount)], - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = - Just $ - NonEmpty.fromList - [ HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 1) (API.ScalarType "number"))], - HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 2) (API.ScalarType "number"))] + mkQueryRequest + (mkTableName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("nodes_AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), + ("nodes_Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") ] - } + & API.qAggregates ?~ mkFieldsMap [("aggregate_count", API.StarCount)] + ) + & API.qrForeach + ?~ NonEmpty.fromList + [ HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 1) (API.ScalarType "number"))], + HashMap.fromList [(API.ColumnName "ArtistId", API.ScalarValue (Aeson.Number 2) (API.ScalarType "number"))] + ] ) -mkRowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -mkRowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing - -mkQueryResponse :: [[(API.FieldName, API.FieldValue)]] -> [(API.FieldName, Aeson.Value)] -> API.QueryResponse -mkQueryResponse rows aggregates = API.QueryResponse (Just $ HashMap.fromList <$> rows) (Just $ HashMap.fromList aggregates) - errorTests :: Fixture.Options -> SpecWith (TestEnvironment, Mock.MockAgentEnvironment) errorTests opts = do it "creating a remote relationship returns an error when it is unsupported by the target" $ \(testEnv, _) -> do diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/TestHelpers.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/TestHelpers.hs new file mode 100644 index 00000000000..7f517159185 --- /dev/null +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/TestHelpers.hs @@ -0,0 +1,40 @@ +module Test.DataConnector.MockAgent.TestHelpers + ( mkTableName, + mkQueryRequest, + emptyQuery, + emptyMutationRequest, + mkRowsQueryResponse, + mkAggregatesQueryResponse, + mkQueryResponse, + mkFieldsMap, + ) +where + +import Data.Aeson qualified as Aeson +import Data.HashMap.Strict qualified as HashMap +import Hasura.Backends.DataConnector.API qualified as API +import Hasura.Prelude + +mkTableName :: Text -> API.TableName +mkTableName name = API.TableName (name :| []) + +mkQueryRequest :: API.TableName -> API.Query -> API.QueryRequest +mkQueryRequest tableName query = API.QueryRequest tableName [] query Nothing + +emptyQuery :: API.Query +emptyQuery = API.Query Nothing Nothing Nothing Nothing Nothing Nothing Nothing + +emptyMutationRequest :: API.MutationRequest +emptyMutationRequest = API.MutationRequest [] [] [] + +mkRowsQueryResponse :: [[(Text, API.FieldValue)]] -> API.QueryResponse +mkRowsQueryResponse rows = API.QueryResponse (Just $ mkFieldsMap <$> rows) Nothing + +mkAggregatesQueryResponse :: [(Text, Aeson.Value)] -> API.QueryResponse +mkAggregatesQueryResponse aggregates = API.QueryResponse Nothing (Just $ mkFieldsMap aggregates) + +mkQueryResponse :: [[(Text, API.FieldValue)]] -> [(Text, Aeson.Value)] -> API.QueryResponse +mkQueryResponse rows aggregates = API.QueryResponse (Just $ mkFieldsMap <$> rows) (Just $ mkFieldsMap aggregates) + +mkFieldsMap :: [(Text, v)] -> HashMap API.FieldName v +mkFieldsMap = HashMap.fromList . fmap (first API.FieldName) diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/TransformedConfigurationSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/TransformedConfigurationSpec.hs index 3f7e368978e..5bc56886c86 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/TransformedConfigurationSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/TransformedConfigurationSpec.hs @@ -5,8 +5,8 @@ module Test.DataConnector.MockAgent.TransformedConfigurationSpec (spec) where -------------------------------------------------------------------------------- +import Control.Lens ((?~)) import Data.Aeson qualified as Aeson -import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE import Harness.Backend.DataConnector.Mock (AgentRequest (..), MockRequestResults (..), mockAgentGraphqlTest, mockQueryResponse) import Harness.Backend.DataConnector.Mock qualified as Mock @@ -18,6 +18,7 @@ import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment) import Harness.Yaml (shouldBeYaml) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec (SpecWith, describe, shouldBe) -------------------------------------------------------------------------------- @@ -113,9 +114,9 @@ tests _opts = describe "Transformed Configuration Tests" $ do } |] let queryResponse = - rowsResponse - [ [ (API.FieldName "id", API.mkColumnFieldValue $ Aeson.Number 1), - (API.FieldName "title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") + mkRowsQueryResponse + [ [ ("id", API.mkColumnFieldValue $ Aeson.Number 1), + ("title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You") ] ] let mockConfig = Mock.chinookMock & mockQueryResponse queryResponse @@ -133,25 +134,16 @@ tests _opts = describe "Transformed Configuration Tests" $ do _mrrRecordedRequest `shouldBe` Just ( Query $ - API.QueryRequest - { _qrTable = API.TableName ("Album" :| []), - _qrTableRelationships = [], - _qrQuery = - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "id", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - (API.FieldName "title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Just 1, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - }, - _qrForeach = Nothing - } + mkQueryRequest + (mkTableName "Album") + ( emptyQuery + & API.qFields + ?~ mkFieldsMap + [ ("id", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), + ("title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + ] + & API.qLimit ?~ 1 + ) ) Aeson.toJSON _mrrRecordedRequestConfig @@ -161,6 +153,3 @@ tests _opts = describe "Transformed Configuration Tests" $ do env: "bar env default" session: "foo session default" |] - -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/UpdateMutationsSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/UpdateMutationsSpec.hs index fd4ec0d5197..8c605494a91 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/UpdateMutationsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/UpdateMutationsSpec.hs @@ -3,6 +3,7 @@ module Test.DataConnector.MockAgent.UpdateMutationsSpec ) where +import Control.Lens ((.~), (?~)) import Data.Aeson qualified as Aeson import Data.ByteString (ByteString) import Data.HashMap.Strict qualified as HashMap @@ -18,6 +19,7 @@ import Harness.Yaml import Hasura.Backends.DataConnector.API qualified as API import Hasura.Prelude import Language.GraphQL.Draft.Syntax.QQ qualified as G +import Test.DataConnector.MockAgent.TestHelpers import Test.Hspec -------------------------------------------------------------------------------- @@ -127,35 +129,35 @@ tests _opts = do { API._morAffectedRows = 3, API._morReturning = Just - [ HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 3), - (API.FieldName "updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), - ( API.FieldName "updatedRows_Genre", + [ mkFieldsMap + [ ("updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 3), + ("updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), + ( "updatedRows_Genre", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Rock") + mkRowsQueryResponse + [ [ ("Name", API.mkColumnFieldValue $ Aeson.String "Rock") ] ] ) ], - HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 4), - (API.FieldName "updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), - ( API.FieldName "updatedRows_Genre", + mkFieldsMap + [ ("updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 4), + ("updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), + ( "updatedRows_Genre", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Rock") + mkRowsQueryResponse + [ [ ("Name", API.mkColumnFieldValue $ Aeson.String "Rock") ] ] ) ], - HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 5), - (API.FieldName "updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), - ( API.FieldName "updatedRows_Genre", + mkFieldsMap + [ ("updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 5), + ("updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), + ( "updatedRows_Genre", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Rock") + mkRowsQueryResponse + [ [ ("Name", API.mkColumnFieldValue $ Aeson.String "Rock") ] ] ) @@ -188,91 +190,78 @@ tests _opts = do |] let expectedRequest = - API.MutationRequest - { API._mrTableRelationships = - [ API.TableRelationships - { API._trSourceTable = API.TableName ("Track" :| []), - API._trRelationships = - HashMap.fromList - [ ( API.RelationshipName "Genre", - API.Relationship - { API._rTargetTable = API.TableName ("Genre" :| []), - API._rRelationshipType = API.ObjectRelationship, - API._rColumnMapping = HashMap.fromList [(API.ColumnName "GenreId", API.ColumnName "GenreId")] - } - ) - ] - } - ], - API._mrInsertSchema = [], - API._mrOperations = - [ API.UpdateOperation $ - API.UpdateMutationOperation - { API._umoTable = API.TableName ("Track" :| []), - API._umoUpdates = - [ API.SetColumn $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "Name", - API._rcovValue = Aeson.String "Another Name", - API._rcovValueType = API.ScalarType "string" - }, - API.CustomUpdateColumnOperator (API.UpdateColumnOperatorName [G.name|inc|]) $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "Milliseconds", - API._rcovValue = Aeson.Number 1000, - API._rcovValueType = API.ScalarType "number" - }, - API.SetColumn $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "AlbumId", - API._rcovValue = Aeson.Number 3, - API._rcovValueType = API.ScalarType "number" - } - ], - API._umoWhere = - Just $ - API.And - [ API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")), - API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "GenreId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 1) (API.ScalarType "number")) - ], - API._umoPostUpdateCheck = - Just $ - API.ApplyBinaryComparisonOperator - API.GreaterThan - (API.ComparisonColumn API.CurrentTable (API.ColumnName "UnitPrice") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 0) (API.ScalarType "number")), - API._umoReturningFields = - HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.ColumnField (API.ColumnName "TrackId") (API.ScalarType "number")), - (API.FieldName "updatedRows_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), - ( API.FieldName "updatedRows_Genre", - API.RelField - ( API.RelationshipField - (API.RelationshipName "Genre") - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } - ) - ) - ] - } - ] - } + emptyMutationRequest + & API.mrTableRelationships + .~ [ API.TableRelationships + { API._trSourceTable = mkTableName "Track", + API._trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Genre", + API.Relationship + { API._rTargetTable = mkTableName "Genre", + API._rRelationshipType = API.ObjectRelationship, + API._rColumnMapping = HashMap.fromList [(API.ColumnName "GenreId", API.ColumnName "GenreId")] + } + ) + ] + } + ] + & API.mrOperations + .~ [ API.UpdateOperation $ + API.UpdateMutationOperation + { API._umoTable = mkTableName "Track", + API._umoUpdates = + [ API.SetColumn $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "Name", + API._rcovValue = Aeson.String "Another Name", + API._rcovValueType = API.ScalarType "string" + }, + API.CustomUpdateColumnOperator (API.UpdateColumnOperatorName [G.name|inc|]) $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "Milliseconds", + API._rcovValue = Aeson.Number 1000, + API._rcovValueType = API.ScalarType "number" + }, + API.SetColumn $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "AlbumId", + API._rcovValue = Aeson.Number 3, + API._rcovValueType = API.ScalarType "number" + } + ], + API._umoWhere = + Just $ + API.And + [ API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")), + API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "GenreId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 1) (API.ScalarType "number")) + ], + API._umoPostUpdateCheck = + Just $ + API.ApplyBinaryComparisonOperator + API.GreaterThan + (API.ComparisonColumn API.CurrentTable (API.ColumnName "UnitPrice") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 0) (API.ScalarType "number")), + API._umoReturningFields = + mkFieldsMap + [ ("updatedRows_TrackId", API.ColumnField (API.ColumnName "TrackId") (API.ScalarType "number")), + ("updatedRows_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), + ( "updatedRows_Genre", + API.RelField + ( API.RelationshipField + (API.RelationshipName "Genre") + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) + ) + ) + ] + } + ] _mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest) mockAgentGraphqlTest "update_many rows with update permissions" $ \_testEnv performGraphqlRequest -> do @@ -301,13 +290,13 @@ tests _opts = do { API._morAffectedRows = 1, API._morReturning = Just - [ HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 3), - (API.FieldName "updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), - ( API.FieldName "updatedRows_Genre", + [ mkFieldsMap + [ ("updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 3), + ("updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Another Name"), + ( "updatedRows_Genre", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Rock") + mkRowsQueryResponse + [ [ ("Name", API.mkColumnFieldValue $ Aeson.String "Rock") ] ] ) @@ -318,24 +307,24 @@ tests _opts = do { API._morAffectedRows = 2, API._morReturning = Just - [ HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 4), - (API.FieldName "updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Better Name"), - ( API.FieldName "updatedRows_Genre", + [ mkFieldsMap + [ ("updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 4), + ("updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Better Name"), + ( "updatedRows_Genre", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Rock") + mkRowsQueryResponse + [ [ ("Name", API.mkColumnFieldValue $ Aeson.String "Rock") ] ] ) ], - HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 5), - (API.FieldName "updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Better Name"), - ( API.FieldName "updatedRows_Genre", + mkFieldsMap + [ ("updatedRows_TrackId", API.mkColumnFieldValue $ Aeson.Number 5), + ("updatedRows_Name", API.mkColumnFieldValue $ Aeson.String "Better Name"), + ( "updatedRows_Genre", API.mkRelationshipFieldValue $ - rowsResponse - [ [ (API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Rock") + mkRowsQueryResponse + [ [ ("Name", API.mkColumnFieldValue $ Aeson.String "Rock") ] ] ) @@ -376,126 +365,110 @@ tests _opts = do (API.ComparisonColumn API.CurrentTable (API.ColumnName "UnitPrice") $ API.ScalarType "number") (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 0) (API.ScalarType "number")) let sharedReturning = - HashMap.fromList - [ (API.FieldName "updatedRows_TrackId", API.ColumnField (API.ColumnName "TrackId") (API.ScalarType "number")), - (API.FieldName "updatedRows_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), - ( API.FieldName "updatedRows_Genre", + mkFieldsMap + [ ("updatedRows_TrackId", API.ColumnField (API.ColumnName "TrackId") (API.ScalarType "number")), + ("updatedRows_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), + ( "updatedRows_Genre", API.RelField ( API.RelationshipField (API.RelationshipName "Genre") - API.Query - { _qFields = - Just $ - HashMap.fromList - [ (API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") - ], - _qAggregates = Nothing, - _qLimit = Nothing, - _qOffset = Nothing, - _qWhere = Nothing, - _qOrderBy = Nothing - } + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) ) ) ] let expectedRequest = - API.MutationRequest - { API._mrTableRelationships = - [ API.TableRelationships - { API._trSourceTable = API.TableName ("Track" :| []), - API._trRelationships = - HashMap.fromList - [ ( API.RelationshipName "Genre", - API.Relationship - { API._rTargetTable = API.TableName ("Genre" :| []), - API._rRelationshipType = API.ObjectRelationship, - API._rColumnMapping = HashMap.fromList [(API.ColumnName "GenreId", API.ColumnName "GenreId")] - } - ) - ] - } - ], - API._mrInsertSchema = [], - API._mrOperations = - [ API.UpdateOperation $ - API.UpdateMutationOperation - { API._umoTable = API.TableName ("Track" :| []), - API._umoUpdates = - [ API.SetColumn $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "Name", - API._rcovValue = Aeson.String "Another Name", - API._rcovValueType = API.ScalarType "string" - }, - API.CustomUpdateColumnOperator (API.UpdateColumnOperatorName [G.name|inc|]) $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "Milliseconds", - API._rcovValue = Aeson.Number 1000, - API._rcovValueType = API.ScalarType "number" - }, - API.SetColumn $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "AlbumId", - API._rcovValue = Aeson.Number 3, - API._rcovValueType = API.ScalarType "number" - } - ], - API._umoWhere = - Just $ - API.And - [ API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")), - API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "TrackId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")) - ], - API._umoPostUpdateCheck = sharedPostUpdateCheck, - API._umoReturningFields = sharedReturning - }, - API.UpdateOperation $ - API.UpdateMutationOperation - { API._umoTable = API.TableName ("Track" :| []), - API._umoUpdates = - [ API.SetColumn $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "Name", - API._rcovValue = Aeson.String "Better Name", - API._rcovValueType = API.ScalarType "string" - }, - API.CustomUpdateColumnOperator (API.UpdateColumnOperatorName [G.name|inc|]) $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "UnitPrice", - API._rcovValue = Aeson.Number 1, - API._rcovValueType = API.ScalarType "number" - }, - API.SetColumn $ - API.RowColumnOperatorValue - { API._rcovColumn = API.ColumnName "AlbumId", - API._rcovValue = Aeson.Number 3, - API._rcovValueType = API.ScalarType "number" - } - ], - API._umoWhere = - Just $ - API.And - [ API.ApplyBinaryComparisonOperator - API.Equal - (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")), - API.ApplyBinaryComparisonOperator - API.GreaterThan - (API.ComparisonColumn API.CurrentTable (API.ColumnName "TrackId") $ API.ScalarType "number") - (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")) - ], - API._umoPostUpdateCheck = sharedPostUpdateCheck, - API._umoReturningFields = sharedReturning - } - ] - } + emptyMutationRequest + & API.mrTableRelationships + .~ [ API.TableRelationships + { API._trSourceTable = mkTableName "Track", + API._trRelationships = + HashMap.fromList + [ ( API.RelationshipName "Genre", + API.Relationship + { API._rTargetTable = mkTableName "Genre", + API._rRelationshipType = API.ObjectRelationship, + API._rColumnMapping = HashMap.fromList [(API.ColumnName "GenreId", API.ColumnName "GenreId")] + } + ) + ] + } + ] + & API.mrOperations + .~ [ API.UpdateOperation $ + API.UpdateMutationOperation + { API._umoTable = mkTableName "Track", + API._umoUpdates = + [ API.SetColumn $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "Name", + API._rcovValue = Aeson.String "Another Name", + API._rcovValueType = API.ScalarType "string" + }, + API.CustomUpdateColumnOperator (API.UpdateColumnOperatorName [G.name|inc|]) $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "Milliseconds", + API._rcovValue = Aeson.Number 1000, + API._rcovValueType = API.ScalarType "number" + }, + API.SetColumn $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "AlbumId", + API._rcovValue = Aeson.Number 3, + API._rcovValueType = API.ScalarType "number" + } + ], + API._umoWhere = + Just $ + API.And + [ API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")), + API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "TrackId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")) + ], + API._umoPostUpdateCheck = sharedPostUpdateCheck, + API._umoReturningFields = sharedReturning + }, + API.UpdateOperation $ + API.UpdateMutationOperation + { API._umoTable = mkTableName "Track", + API._umoUpdates = + [ API.SetColumn $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "Name", + API._rcovValue = Aeson.String "Better Name", + API._rcovValueType = API.ScalarType "string" + }, + API.CustomUpdateColumnOperator (API.UpdateColumnOperatorName [G.name|inc|]) $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "UnitPrice", + API._rcovValue = Aeson.Number 1, + API._rcovValueType = API.ScalarType "number" + }, + API.SetColumn $ + API.RowColumnOperatorValue + { API._rcovColumn = API.ColumnName "AlbumId", + API._rcovValue = Aeson.Number 3, + API._rcovValueType = API.ScalarType "number" + } + ], + API._umoWhere = + Just $ + API.And + [ API.ApplyBinaryComparisonOperator + API.Equal + (API.ComparisonColumn API.CurrentTable (API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")), + API.ApplyBinaryComparisonOperator + API.GreaterThan + (API.ComparisonColumn API.CurrentTable (API.ColumnName "TrackId") $ API.ScalarType "number") + (API.ScalarValueComparison $ API.ScalarValue (Aeson.Number 3) (API.ScalarType "number")) + ], + API._umoPostUpdateCheck = sharedPostUpdateCheck, + API._umoReturningFields = sharedReturning + } + ] _mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest) - -rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse -rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs index f7c6363b90f..122f39ec8bf 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Query.hs @@ -11,6 +11,7 @@ module Hasura.Backends.DataConnector.API.V0.Query Query (..), qFields, qAggregates, + qAggregatesLimit, qLimit, qOffset, qWhere, @@ -92,9 +93,14 @@ data Query = Query _qFields :: Maybe (HashMap FieldName Field), -- | Map of aggregate field name to Aggregate definition _qAggregates :: Maybe (HashMap FieldName API.V0.Aggregate), - -- | Optionally limit to N results. + -- | Optionally limit the maximum number of rows considered while applying + -- aggregations. This limit does not apply to returned rows. + _qAggregatesLimit :: Maybe Int, + -- | Optionally limit the maximum number of returned rows. This limit does not + -- apply to records considered while apply aggregations. _qLimit :: Maybe Int, - -- | Optionally offset from the Nth result. + -- | Optionally offset from the Nth result. This applies to both row + -- and aggregation results. _qOffset :: Maybe Int, -- | Optionally constrain the results to satisfy some predicate. _qWhere :: Maybe API.V0.Expression, @@ -112,9 +118,11 @@ instance HasCodec Query where .= _qFields <*> optionalFieldOrNull "aggregates" "Aggregate fields of the query" .= _qAggregates - <*> optionalFieldOrNull "limit" "Optionally limit to N results" + <*> optionalFieldOrNull "aggregates_limit" "Optionally limit the maximum number of rows considered while applying aggregations. This limit does not apply to returned rows." + .= _qAggregatesLimit + <*> optionalFieldOrNull "limit" "Optionally limit the maximum number of returned rows. This limit does not apply to records considered while apply aggregations." .= _qLimit - <*> optionalFieldOrNull "offset" "Optionally offset from the Nth result" + <*> optionalFieldOrNull "offset" "Optionally offset from the Nth result. This applies to both row and aggregation results." .= _qOffset <*> optionalFieldOrNull "where" "Optionally constrain the results to satisfy some predicate" .= _qWhere diff --git a/server/lib/dc-api/test/Test/Data.hs b/server/lib/dc-api/test/Test/Data.hs index 05be154956d..2a379a90e9b 100644 --- a/server/lib/dc-api/test/Test/Data.hs +++ b/server/lib/dc-api/test/Test/Data.hs @@ -598,7 +598,7 @@ findColumnScalarType API.SchemaResponse {..} tableName columnName = columnInfo = find (\API.ColumnInfo {..} -> _ciName == columnName) =<< API._tiColumns <$> tableInfo emptyQuery :: API.Query -emptyQuery = API.Query Nothing Nothing Nothing Nothing Nothing Nothing +emptyQuery = API.Query Nothing Nothing Nothing Nothing Nothing Nothing Nothing emptyMutationRequest :: API.MutationRequest emptyMutationRequest = API.MutationRequest mempty mempty mempty diff --git a/server/lib/dc-api/test/Test/Specs/QuerySpec/AggregatesSpec.hs b/server/lib/dc-api/test/Test/Specs/QuerySpec/AggregatesSpec.hs index 85e3f6b3b17..4d965e6bccf 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/AggregatesSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/AggregatesSpec.hs @@ -52,12 +52,24 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do it "counts all rows, after applying pagination" $ do let offset = 400 - let limit = 20 + let aggregatesLimit = 20 let aggregates = Data.mkFieldsMap [("count_all", StarCount)] - let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ limit >>> qOffset ?~ offset) + let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qAggregatesLimit ?~ aggregatesLimit >>> qOffset ?~ offset) response <- queryGuarded queryRequest - let invoiceCount = length . take limit $ drop offset _tdInvoicesRows + let invoiceCount = length . take aggregatesLimit $ drop offset _tdInvoicesRows + let expectedAggregates = Data.mkFieldsMap [("count_all", Number $ fromIntegral invoiceCount)] + + Data.responseAggregates response `jsonShouldBe` expectedAggregates + Data.responseRows response `rowsShouldBe` [] + + it "limit does not limit the count aggregation" $ do + let limit = 20 + let aggregates = Data.mkFieldsMap [("count_all", StarCount)] + let queryRequest = invoicesQueryRequest aggregates & qrQuery . qLimit ?~ limit + response <- queryGuarded queryRequest + + let invoiceCount = length _tdInvoicesRows let expectedAggregates = Data.mkFieldsMap [("count_all", Number $ fromIntegral invoiceCount)] Data.responseAggregates response `jsonShouldBe` expectedAggregates @@ -76,16 +88,16 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do Data.responseRows response `rowsShouldBe` [] it "can count all rows with non-null values in a column, after applying pagination and filtering" $ do - let limit = 50 + let aggregatesLimit = 50 let where' = ApplyBinaryComparisonOperator GreaterThanOrEqual (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (Data.scalarValueComparison (Number 380) invoiceIdScalarType) let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") False)] - let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ limit >>> qWhere ?~ where') + let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qAggregatesLimit ?~ aggregatesLimit >>> qWhere ?~ where') response <- queryGuarded queryRequest let invoiceCount = _tdInvoicesRows & filter ((^? Data.field "InvoiceId" . Data._ColumnFieldNumber) >>> (>= Just 380)) - & take limit + & take aggregatesLimit & mapMaybe ((^? Data.field "BillingState" . Data._ColumnFieldString)) & length @@ -106,18 +118,18 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do Data.responseRows response `rowsShouldBe` [] it "can count all rows with distinct non-null values in a column, after applying pagination and filtering" $ do - let limit = 20 + let aggregatesLimit = 20 let where' = ApplyBinaryComparisonOperator GreaterThanOrEqual (_tdCurrentComparisonColumn "InvoiceId" invoiceIdScalarType) (Data.scalarValueComparison (Number 380) invoiceIdScalarType) -- It is important to add an explicit order by for this query as different database engines will order implicitly resulting in incorrect results let orderBy = OrderBy mempty $ _tdOrderByColumn [] "InvoiceId" Ascending :| [] let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") True)] - let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ limit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) + let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qAggregatesLimit ?~ aggregatesLimit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) response <- queryGuarded queryRequest let billingStateCount = _tdInvoicesRows & filter ((^? Data.field "InvoiceId" . Data._ColumnFieldNumber) >>> (>= Just 380)) - & take limit + & take aggregatesLimit & mapMaybe ((^? Data.field "BillingState" . Data._ColumnFieldString)) & HashSet.fromList & length @@ -127,6 +139,22 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do Data.responseAggregates response `jsonShouldBe` expectedAggregates Data.responseRows response `rowsShouldBe` [] + it "limit does not limit the column count aggregation" $ do + let limit = 50 + let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") False)] + let queryRequest = invoicesQueryRequest aggregates & qrQuery . qLimit ?~ limit + response <- queryGuarded queryRequest + + let invoiceCount = + _tdInvoicesRows + & mapMaybe ((^? Data.field "BillingState" . Data._ColumnFieldString)) + & length + + let expectedAggregates = Data.mkFieldsMap [("count_cols", Number $ fromIntegral invoiceCount)] + + Data.responseAggregates response `jsonShouldBe` expectedAggregates + Data.responseRows response `rowsShouldBe` [] + describe "Single Column Function" $ do it "can get the max total from all rows" $ do let aggregates = Data.mkFieldsMap [("max", singleColumnAggregateMax (_tdColumnName "Total") invoiceTotalScalarType)] @@ -140,18 +168,18 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do Data.responseRows response `rowsShouldBe` [] it "can get the max total from all rows, after applying pagination, filtering and ordering" $ do - let limit = 20 + let aggregatesLimit = 20 let where' = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "BillingCountry" billingCountryScalarType) (Data.scalarValueComparison (String "USA") billingCountryScalarType) let orderBy = OrderBy mempty $ _tdOrderByColumn [] "BillingPostalCode" Descending :| [_tdOrderByColumn [] "InvoiceId" Ascending] let aggregates = Data.mkFieldsMap [("max", singleColumnAggregateMax (_tdColumnName "Total") invoiceTotalScalarType)] - let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ limit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) + let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qAggregatesLimit ?~ aggregatesLimit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) response <- queryGuarded queryRequest let maxTotal = _tdInvoicesRows & filter ((^? Data.field "BillingCountry" . Data._ColumnFieldString) >>> (== Just "USA")) & sortOn (Down . (^? Data.field "BillingPostalCode")) - & take limit + & take aggregatesLimit & mapMaybe ((^? Data.field "Total" . Data._ColumnFieldNumber)) & maximum @@ -190,6 +218,22 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do Data.responseAggregates response `jsonShouldBe` expectedAggregates Data.responseRows response `rowsShouldBe` [] + it "limit does not limit the single column function aggregation" $ do + let limit = 20 + let aggregates = Data.mkFieldsMap [("max", singleColumnAggregateMax (_tdColumnName "Total") invoiceTotalScalarType)] + let queryRequest = invoicesQueryRequest aggregates & qrQuery . qLimit ?~ limit + response <- queryGuarded queryRequest + + let maxTotal = + _tdInvoicesRows + & mapMaybe ((^? Data.field "Total" . Data._ColumnFieldNumber)) + & maximum + + let expectedAggregates = Data.mkFieldsMap [("max", Number maxTotal)] + + Data.responseAggregates response `jsonShouldBe` expectedAggregates + Data.responseRows response `rowsShouldBe` [] + describe "Multiple Aggregates and Returning Rows" $ do it "can get the max total from all rows, the count and the distinct count, simultaneously" $ do let aggregates = @@ -246,7 +290,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do let where' = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "BillingCountry" billingCountryScalarType) (Data.scalarValueComparison (String "Canada") billingCountryScalarType) let orderBy = OrderBy mempty $ _tdOrderByColumn [] "BillingAddress" Ascending :| [_tdOrderByColumn [] "InvoiceId" Ascending] let aggregates = Data.mkFieldsMap [("min", singleColumnAggregateMin (_tdColumnName "Total") invoiceTotalScalarType)] - let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qFields ?~ fields >>> qLimit ?~ limit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) + let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qFields ?~ fields >>> qLimit ?~ limit >>> qAggregatesLimit ?~ limit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) response <- queryGuarded queryRequest let invoiceRows = @@ -266,6 +310,56 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do Data.responseRows response `rowsShouldBe` expectedRows Data.responseAggregates response `jsonShouldBe` expectedAggregates + it "limit limits the number of returned rows but not the rows considered by the aggregate function" $ do + let limit = 20 + let fields = Data.mkFieldsMap [("InvoiceId", _tdColumnField _tdInvoicesTableName "InvoiceId")] + let orderBy = OrderBy mempty $ _tdOrderByColumn [] "InvoiceId" Ascending :| [] + let aggregates = Data.mkFieldsMap [("count_all", StarCount)] + let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qFields ?~ fields >>> qLimit ?~ limit >>> qOrderBy ?~ orderBy) + response <- queryGuarded queryRequest + + let invoiceCount = length _tdInvoicesRows + let expectedAggregates = Data.mkFieldsMap [("count_all", Number $ fromIntegral invoiceCount)] + let expectedRows = take limit $ Data.filterColumnsByQueryFields (_qrQuery queryRequest) <$> _tdInvoicesRows + + Data.responseAggregates response `jsonShouldBe` expectedAggregates + Data.responseRows response `rowsShouldBe` expectedRows + + it "aggregates limit is applied separately to row limit" $ do + let aggregatesLimit = 30 + let limit = 20 + let fields = + Data.mkFieldsMap + [ ("InvoiceId", _tdColumnField _tdInvoicesTableName "InvoiceId"), + ("BillingCountry", _tdColumnField _tdInvoicesTableName "BillingCountry") + ] + let where' = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "BillingCountry" billingCountryScalarType) (Data.scalarValueComparison (String "Canada") billingCountryScalarType) + let orderBy = OrderBy mempty $ _tdOrderByColumn [] "BillingAddress" Ascending :| [_tdOrderByColumn [] "InvoiceId" Ascending] + let aggregates = Data.mkFieldsMap [("min", singleColumnAggregateMin (_tdColumnName "Total") invoiceTotalScalarType)] + let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qFields ?~ fields >>> qAggregatesLimit ?~ aggregatesLimit >>> qLimit ?~ limit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) + response <- queryGuarded queryRequest + + let aggregateLimitedInvoiceRows = + _tdInvoicesRows + & filter ((^? Data.field "BillingCountry" . Data._ColumnFieldString) >>> (== Just "Canada")) + & sortOn (^? Data.field "BillingAddress") + & take aggregatesLimit + + -- Limit is smaller than aggregatesLimit, so we can just take from the aggregateLimitedInvoiceRows + let invoiceRows = take limit aggregateLimitedInvoiceRows + + let maxTotal = + aggregateLimitedInvoiceRows + & take limit + & mapMaybe ((^? Data.field "Total" . Data._ColumnFieldNumber)) + & aggregate (Number . minimum) + + let expectedAggregates = Data.mkFieldsMap [("min", maxTotal)] + let expectedRows = Data.filterColumnsByQueryFields (_qrQuery queryRequest) <$> invoiceRows + + Data.responseRows response `rowsShouldBe` expectedRows + Data.responseAggregates response `jsonShouldBe` expectedAggregates + when (isJust relationshipCapabilities) $ describe "Aggregates via Relationships" $ do it "can query aggregates via an array relationship" $ do @@ -387,8 +481,8 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do describe "Aggregates over ordered and paginated tables" $ do it "orders by a column" $ do let offset = 2 - let limit = 5 - let orderBy = OrderBy mempty $ _tdOrderByColumn [] "Title" Descending :| [] + let aggregatesLimit = 5 + let orderBy = OrderBy mempty $ _tdOrderByColumn [] "Title" Ascending :| [] let aggregates = Data.mkFieldsMap [("max", singleColumnAggregateMax (_tdColumnName "Title") albumTitleScalarType)] let queryRequest = albumsQueryRequest @@ -396,15 +490,15 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do %~ ( qAggregates ?~ aggregates >>> qOrderBy ?~ orderBy >>> qOffset ?~ offset - >>> qLimit ?~ limit + >>> qAggregatesLimit ?~ aggregatesLimit ) response <- queryGuarded queryRequest let names = _tdAlbumsRows - & sortOn (Down . (^? Data.field "Title")) + & sortOn ((^? Data.field "Title")) & drop offset - & take limit + & take aggregatesLimit & mapMaybe (^? Data.field "Title" . Data._ColumnFieldString) let expectedAggregates = Data.mkFieldsMap [("max", aggregate (String . maximum) names)] @@ -415,7 +509,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do when (isJust relationshipCapabilities) . describe "involving related tables in the ordering" $ do it "orders by a column" $ do let offset = 10 - let limit = 50 + let aggregatesLimit = 50 let orderByRelations = HashMap.fromList [(_tdArtistRelationshipName, OrderByRelation Nothing mempty)] let orderBy = OrderBy orderByRelations $ @@ -431,7 +525,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do %~ ( qAggregates ?~ aggregates >>> qOrderBy ?~ orderBy >>> qOffset ?~ offset - >>> qLimit ?~ limit + >>> qAggregatesLimit ?~ aggregatesLimit ) response <- queryGuarded queryRequest @@ -442,7 +536,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do _tdAlbumsRows & sortOn (\album -> getRelatedArtist album ^? _Just . Data.field "Name") & drop offset - & take limit + & take aggregatesLimit & mapMaybe (^? Data.field "Title" . Data._ColumnFieldString) let expectedAggregates = Data.mkFieldsMap [("max", aggregate (String . maximum) names)] @@ -452,7 +546,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do it "orders by an aggregate" $ do let offset = 15 - let limit = 10 + let aggregatesLimit = 10 let orderByRelations = HashMap.fromList [(_tdTracksRelationshipName, OrderByRelation Nothing mempty)] let orderBy = OrderBy orderByRelations $ @@ -468,7 +562,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do %~ ( qAggregates ?~ aggregates >>> qOrderBy ?~ orderBy >>> qOffset ?~ offset - >>> qLimit ?~ limit + >>> qAggregatesLimit ?~ aggregatesLimit ) response <- queryGuarded queryRequest @@ -480,7 +574,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do _tdAlbumsRows & sortOn (\album -> (Down $ getRelatedTracksCount album, Down $ album ^? Data.field "Title" . Data._ColumnFieldString)) & drop offset - & take limit + & take aggregatesLimit & mapMaybe (^? Data.field "Title" . Data._ColumnFieldString) let expectedAggregates = Data.mkFieldsMap [("max", aggregate (String . maximum) names)] @@ -549,7 +643,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do tracksAggregates = Data.mkFieldsMap [("aggregate_count", StarCount)] tracksWhere = ApplyBinaryComparisonOperator LessThan (_tdCurrentComparisonColumn "Milliseconds" millisecondsScalarType) (Data.scalarValueComparison (Number 300000) millisecondsScalarType) tracksOrderBy = OrderBy mempty $ _tdOrderByColumn [] "Name" Descending :| [] - tracksSubquery = Query (Just tracksFields) (Just tracksAggregates) Nothing Nothing (Just tracksWhere) (Just tracksOrderBy) + tracksSubquery = Query (Just tracksFields) (Just tracksAggregates) Nothing Nothing Nothing (Just tracksWhere) (Just tracksOrderBy) albumsFields = Data.mkFieldsMap [ ("nodes_Title", _tdColumnField _tdAlbumsTableName "Title"), @@ -568,7 +662,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do ApplyBinaryComparisonOperator LessThan (_tdCurrentComparisonColumn "Name" artistNameScalarType) (Data.scalarValueComparison (String "B") artistNameScalarType) ] artistOrderBy = OrderBy mempty $ _tdOrderByColumn [] "Name" Descending :| [] - artistQuery = Query (Just artistFields) Nothing (Just 3) (Just 1) (Just artistWhere) (Just artistOrderBy) + artistQuery = Query (Just artistFields) Nothing Nothing (Just 3) (Just 1) (Just artistWhere) (Just artistOrderBy) in QueryRequest _tdArtistsTableName [ Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships, diff --git a/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs b/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs index 73e2df71b79..75256ab176e 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs @@ -146,6 +146,7 @@ translateAnnSelect sessionVariables translateFieldsAndAggregates tableName selec API.Query { _qFields = mapFieldNameHashMap <$> _faaFields, _qAggregates = mapFieldNameHashMap <$> _faaAggregates, + _qAggregatesLimit = _saLimit (_asnArgs selectG) <* _faaAggregates, -- Only include the aggregates limit if we actually have aggregrates _qLimit = fmap getMin $ foldMap @@ -310,6 +311,7 @@ translateAnnField sessionVariables sourceTableName = \case { _qFields = Just $ mapFieldNameHashMap fields, _qAggregates = mempty, _qWhere = whereClause, + _qAggregatesLimit = Nothing, _qLimit = Nothing, _qOffset = Nothing, _qOrderBy = Nothing diff --git a/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs b/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs index 727a8bc44a3..d2a978cc703 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs @@ -89,6 +89,7 @@ mkRemoteRelationshipPlan sessionVariables _sourceConfig joinIds joinIdsSchema ar API.Query { _qFields = Just $ mapFieldNameHashMap fields, _qAggregates = Nothing, + _qAggregatesLimit = Nothing, _qLimit = Nothing, _qOffset = Nothing, _qWhere = whereClause, diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs index 14c47f8eeac..d8a9a218fd1 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs @@ -41,7 +41,7 @@ spec = do } |] describe "RelationshipField" $ do - let query = Query (Just mempty) Nothing Nothing Nothing Nothing Nothing + let query = Query (Just mempty) Nothing Nothing Nothing Nothing Nothing Nothing testToFromJSONToSchema (RelField $ RelationshipField (RelationshipName "a_relationship") query) [aesonQQ| @@ -57,6 +57,7 @@ spec = do Query { _qFields = Just $ HashMap.fromList [(FieldName "my_field_alias", ColumnField (ColumnName "my_field_name") (ScalarType "string"))], _qAggregates = Just $ HashMap.fromList [(FieldName "my_aggregate", StarCount)], + _qAggregatesLimit = Just 5, _qLimit = Just 10, _qOffset = Just 20, _qWhere = Just $ And [], @@ -67,6 +68,7 @@ spec = do [aesonQQ| { "fields": {"my_field_alias": {"type": "column", "column": "my_field_name", "column_type": "string"}}, "aggregates": { "my_aggregate": { "type": "star_count" } }, + "aggregates_limit": 5, "limit": 10, "offset": 20, "where": {"type": "and", "expressions": []}, @@ -91,7 +93,7 @@ spec = do QueryRequest { _qrTable = TableName ["my_table"], _qrTableRelationships = [], - _qrQuery = Query (Just mempty) Nothing Nothing Nothing Nothing Nothing, + _qrQuery = Query (Just mempty) Nothing Nothing Nothing Nothing Nothing Nothing, _qrForeach = Just (HashMap.fromList [(ColumnName "my_id", ScalarValue (J.Number 666) (ScalarType "number"))] :| []) } testToFromJSONToSchema @@ -181,6 +183,7 @@ genQuery = <*> Gen.maybe (genFieldMap genAggregate) <*> Gen.maybe (Gen.int defaultRange) <*> Gen.maybe (Gen.int defaultRange) + <*> Gen.maybe (Gen.int defaultRange) <*> Gen.maybe genExpression <*> Gen.maybe genOrderBy