From 68f7d6e9a4a91f9a0856777b4e75c639ec65bed5 Mon Sep 17 00:00:00 2001 From: Daniel Chambers Date: Thu, 3 Aug 2023 16:09:14 +1000 Subject: [PATCH] Add inherited roles data redaction support to the Data Connector API [GDC-1292]: https://hasurahq.atlassian.net/browse/GDC-1292?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/10023 GitOrigin-RevId: d6947dd89dc59fce3f68b786192c59e731826c60 --- dc-agents/DOCUMENTATION.md | 376 +++++++++++++++++- dc-agents/dc-api-types/package.json | 2 +- dc-agents/dc-api-types/src/agent.openapi.json | 147 ++++++- dc-agents/dc-api-types/src/index.ts | 6 + .../src/models/ColumnCountAggregate.ts | 3 + .../dc-api-types/src/models/ColumnField.ts | 2 + .../src/models/ComparisonColumn.ts | 2 + .../src/models/FunctionRequest.ts | 5 + .../src/models/MutationRequest.ts | 5 + .../dc-api-types/src/models/OrderByColumn.ts | 3 + .../models/OrderBySingleColumnAggregate.ts | 2 + .../src/models/QueryCapabilities.ts | 2 + .../src/models/RedactionCapabilities.ts | 7 + .../src/models/RedactionExpressionName.ts | 5 + .../src/models/SingleColumnAggregate.ts | 2 + .../dc-api-types/src/models/TNFunction.ts | 11 + dc-agents/dc-api-types/src/models/TNTable.ts | 11 + .../dc-api-types/src/models/TableRequest.ts | 5 + .../dc-api-types/src/models/TargetName.ts | 9 + .../src/models/TargetRedactionExpressions.ts | 15 + dc-agents/package-lock.json | 10 +- dc-agents/reference/package-lock.json | 4 +- dc-agents/reference/package.json | 2 +- dc-agents/sqlite/package-lock.json | 4 +- dc-agents/sqlite/package.json | 2 +- server/graphql-engine.cabal | 2 + .../MockAgent/AggregateQuerySpec.hs | 12 +- .../DataConnector/MockAgent/BasicQuerySpec.hs | 16 +- .../MockAgent/CustomScalarsSpec.hs | 36 +- .../MockAgent/DeleteMutationsSpec.hs | 24 +- .../Test/DataConnector/MockAgent/ErrorSpec.hs | 4 +- .../MockAgent/InsertMutationsSpec.hs | 10 +- .../DataConnector/MockAgent/OrderBySpec.hs | 9 +- .../MockAgent/QueryRelationshipsSpec.hs | 24 +- .../MockAgent/RemoteRelationshipsSpec.hs | 12 +- .../DataConnector/MockAgent/TestHelpers.hs | 4 +- .../MockAgent/TransformedConfigurationSpec.hs | 4 +- .../MockAgent/UpdateMutationsSpec.hs | 28 +- server/lib/dc-api/dc-api.cabal | 1 + .../Hasura/Backends/DataConnector/API/V0.hs | 2 + .../DataConnector/API/V0/Aggregate.hs | 5 + .../DataConnector/API/V0/Capabilities.hs | 15 +- .../DataConnector/API/V0/Expression.hs | 45 ++- .../DataConnector/API/V0/Mutations.hs | 4 + .../Backends/DataConnector/API/V0/OrderBy.hs | 11 +- .../Backends/DataConnector/API/V0/Query.hs | 30 +- .../Backends/DataConnector/API/V0/Target.hs | 48 +++ server/lib/dc-api/test/Test/Data.hs | 12 +- .../lib/dc-api/test/Test/Specs/ErrorSpec.hs | 2 +- .../lib/dc-api/test/Test/Specs/ExplainSpec.hs | 2 +- .../Test/Specs/MutationSpec/DeleteSpec.hs | 2 +- .../Test/Specs/MutationSpec/InsertSpec.hs | 6 +- .../Test/Specs/MutationSpec/UpdateSpec.hs | 4 +- .../Test/Specs/QuerySpec/AggregatesSpec.hs | 27 +- .../test/Test/Specs/QuerySpec/BasicSpec.hs | 4 +- .../Specs/QuerySpec/CustomOperatorsSpec.hs | 4 +- .../Test/Specs/QuerySpec/FilteringSpec.hs | 4 +- .../test/Test/Specs/QuerySpec/ForeachSpec.hs | 4 +- .../test/Test/Specs/QuerySpec/OrderBySpec.hs | 10 +- .../Test/Specs/QuerySpec/RelationshipsSpec.hs | 8 +- server/lib/dc-api/test/Test/Specs/UDFSpec.hs | 8 +- .../Backend/DataConnector/Mock/Server.hs | 3 +- .../Backends/DataConnector/Adapter/Execute.hs | 8 +- .../Backends/DataConnector/Plan/Common.hs | 189 ++++++--- .../DataConnector/Plan/MutationPlan.hs | 195 +++++---- .../Backends/DataConnector/Plan/QueryPlan.hs | 351 +++++++++------- .../Plan/RemoteRelationshipPlan.hs | 32 +- .../DataConnector/API/V0/AggregateSpec.hs | 9 +- .../DataConnector/API/V0/CapabilitiesSpec.hs | 5 +- .../DataConnector/API/V0/ExpressionSpec.hs | 50 ++- .../DataConnector/API/V0/FunctionSpec.hs | 42 ++ .../DataConnector/API/V0/MutationsSpec.hs | 16 +- .../DataConnector/API/V0/OrderBySpec.hs | 19 +- .../DataConnector/API/V0/QuerySpec.hs | 35 +- .../DataConnector/API/V0/TargetSpec.hs | 47 +++ 75 files changed, 1547 insertions(+), 539 deletions(-) create mode 100644 dc-agents/dc-api-types/src/models/RedactionCapabilities.ts create mode 100644 dc-agents/dc-api-types/src/models/RedactionExpressionName.ts create mode 100644 dc-agents/dc-api-types/src/models/TNFunction.ts create mode 100644 dc-agents/dc-api-types/src/models/TNTable.ts create mode 100644 dc-agents/dc-api-types/src/models/TargetName.ts create mode 100644 dc-agents/dc-api-types/src/models/TargetRedactionExpressions.ts create mode 100644 server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Target.hs create mode 100644 server/src-test/Hasura/Backends/DataConnector/API/V0/FunctionSpec.hs create mode 100644 server/src-test/Hasura/Backends/DataConnector/API/V0/TargetSpec.hs diff --git a/dc-agents/DOCUMENTATION.md b/dc-agents/DOCUMENTATION.md index 3d292d385c1..beb9c8639b5 100644 --- a/dc-agents/DOCUMENTATION.md +++ b/dc-agents/DOCUMENTATION.md @@ -129,7 +129,8 @@ The `GET /capabilities` endpoint is used by `graphql-engine` to discover the cap { "capabilities": { "queries": { - "foreach": {} + "foreach": {}, + "redaction": {} }, "data_schema": { "supports_primary_keys": true, @@ -183,6 +184,8 @@ The `config_schema` property contains an [OpenAPI 3 Schema](https://swagger.io/s #### Query capabilities The agent can declare whether or not it supports ["foreach queries"](#foreach-queries) by including a `foreach` property with an empty object assigned to it. Foreach query support is optional, but is required if the agent is to be used as the target of remote relationships in HGE. +The agent can also declare whether or not it supports ["data redaction"](#data-redaction) by including a `redaction` property with an empty object assigned to it. Data redaction support is optional, but is required if a user configures HGE with inherited roles with different column selection permissions for the same table in the inherited role's role set. + #### Data schema capabilities The agent can declare whether or not it supports primary keys or foreign keys by setting the `supports_primary_keys` and `supports_foreign_keys` properties under the `data_schema` object on capabilities. If it does not declare support, it is expected that it will not return any such primary/foreign keys in the schema it exposes on the `/schema` endpoint. @@ -1564,8 +1567,7 @@ The `order_by` field can either be null, which means no particular ordering is r "target_path": [], "target": { "type": "column", - "column": "last_name", - "column_type": "string" + "column": "last_name" }, "order_direction": "asc" }, @@ -1573,8 +1575,7 @@ The `order_by` field can either be null, which means no particular ordering is r "target_path": [], "target": { "type": "column", - "column": "first_name", - "column_type": "string" + "column": "first_name" }, "order_direction": "desc" } @@ -1824,6 +1825,369 @@ It is important to point out that a `LATERAL` join is necessary instead of regul The artificial `Index` column is inserted into the foreach rowset to ensure that the ordering of the results matches the original ordering of the foreach array in the query request. +#### Data Redaction +In HGE, it is possible to create inherited roles; these produce the union of the access rights of the roles in the inherited role's role set. Roles in HGE can grant or deny access to columns on tables and can filter the rows accessible on the table. Naturally, different roles composed together into an inherited role may combine different filters and column access rights for the same table. + +Consider the following `Test` table: + +| Id | ColumnA | ColumnB | ColumnC | +|-------|---------|---------|---------| +| **1** | **A1** | **B1** | C1 | +| **2** | **A2** | **B2** | **C2** | +| **3** | A3 | **B3** | **C3** | + +and the following roles defined for this `Test` table: +* `RoleA`: + * Column Select Permissions: `Id`, `ColumnA`, `ColumnB` + * Row Filter: `{ Id: { _in: [1,2] } }` +* `RoleB`: + * Column Select Permissions: `Id`, `ColumnB`, `ColumnC` + * Row Filter: `{ Id: { _in: [2,3] } }` +* `ComboRole`: An inherited role, composed of `RoleA` and `RoleB` + +In this scenario, `ComboRole` grants access to all the data that has been **bolded** in the above table. Note that the `A3` is inaccessible because `RoleA` does not grant access to that row, `RoleB` does, but `RoleB` does not grant access to `ColumnA`. Similarly, `C1` is inaccessible because `RoleA` grants access to the row, but not to `ColumnC`, and `RoleB` does not grant access to the row. + +If a user using the `ComboRole` role was to query this `Test` table, we want the access rights as defined above to be enforced, so we'd expect the following data to be returned: + +| Id | ColumnA | ColumnB | ColumnC | +|----|---------|---------|---------| +| 1 | A1 | B1 | null | +| 2 | A2 | B2 | C2 | +| 3 | null | B3 | C3 | + +Data redaction is the process by which data is "nulled out" (ie. redacted) by the agent when it should be inaccessible to a user, as it has been above. It is only necessary when different roles in one inherited roles have differing column select permissions. When all the roles have the same column select permissions, the user is prevented from querying inaccessible columns in the first place. Redaction is only necessary when they have partial access to the column, such as in the above example scenario. + +To support data redaction, the agent must declare the `redaction` capability under the `queries` capability. + +```json +{ + "queries": { + "redaction": {} + } +} +``` + +Once this is declared, all query requests that require redaction will contain a `redaction_expressions` property that will contain named redaction expressions per table/function. Then, in every place in the query that requires redaction, a `redaction_expression` property will be defined that refers to the particular named expression that needs to be used. + +A redaction expression is an `Expression`, same as what is used for the `where` query property used in [filters](#filters). If specified, the expression needs to be evaluated per row, and if it evaluates to false, the associated column value must be replaced with null. + +##### Redaction in Fields +The first, most obvious, place redaction is applied is against columns requested in a query's `fields`. + +Here's an example query, querying all the columns of the above example `Test` table using the `ComboRole`: + +```jsonc +{ + "table": ["Test"], + "table_relationships": [], + // Redaction expressions are defined per table/function + "redaction_expressions": [ + { + "target": { + "type": "table", + "table": ["Test"] // These expressions are defined for the Test table + }, + "expressions": { + // Redaction expressions are named (names are only unique within a table/function) + "RedactionExp0": { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [1,2], + "value_type": "number" + }, + "RedactionExp1": { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [2,3], + "value_type": "number" + } + } + } + ], + "query": { + "fields": { + "Id": { + "type": "column", + "column": "Id", + "column_type": "number" + }, + "ColumnA": { + "type": "column", + "column": "ColumnA", + "column_type": "string", + // This column must be redacted using the Test table's RedactionExp0 expression + // If this expression evaluates to false for a row, this field must return null for that row + "redaction_expression": "RedactionExp0" + }, + "ColumnB": { + "type": "column", + "column": "ColumnB", + "column_type": "string" + }, + "ColumnC": { + "type": "column", + "column": "ColumnC", + "column_type": "string", + // This column must be redacted using the Test table's RedactionExp1 expression + // If this expression evaluates to false for a row, this field must return null for that row + "redaction_expression": "RedactionExp1" + } + }, + // The row filters from ComboRole are pushed down into the where filter predicate + "where": { + "type": "or", + "expressions": [ + { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [1,2], + "value_type": "number" + }, + { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [2,3], + "value_type": "number" + } + ] + } + } +} +``` + +This query might be translated into SQL that is similar to this: + +```sql +SELECT + "Id", + CASE + WHEN "Id" IN (1,2) THEN "ColumnA" + ELSE NULL + END AS "ColumnA", + "ColumnB", + CASE + WHEN "Id" IN (2,3) THEN "ColumnC" + ELSE NULL + END AS "ColumnC" +FROM "Test" +WHERE "Id" IN (1,2) OR "Id" IN (2,3) +``` + +##### Redaction in Aggregates +Another place redaction can be applied is in aggregates. When aggregations are calculated across a set of rows, the columns being aggregated may need to be redacted _before_ being aggregated over. + +For example, here's an aggregation query: + +```jsonc +{ + "table": ["Test"], + "table_relationships": [], + // Redaction expressions are defined per table/function + "redaction_expressions": [ + { + "target": { + "type": "table", + "table": ["Test"] // These expressions are defined for the Test table + }, + "expressions": { + // Redaction expressions are named (names are only unique within a table/function) + "RedactionExp0": { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [1,2], + "value_type": "number" + }, + "RedactionExp1": { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [2,3], + "value_type": "number" + } + } + } + ], + "query": { + "aggregates": { + // Both "single_column" and "column_count" aggregations can have redaction expressions + "aggregate_max_ColumnA": { + "type": "single_column", + "function": "max", + "column": "ColumnA", + "redaction_expression": "RedactionExp0", + "result_type": "string" + }, + "aggregate_count_ColumnC": { + "type": "column_count", + "column": "ColumnC", + "redaction_expression": "RedactionExp1", + "distinct": false + } + } + } +} +``` + +This query might be translated into SQL that is similar to this: + +```sql +SELECT + MAX( + CASE + WHEN "Id" IN (1,2) THEN "ColumnA" + ELSE NULL + END + ) AS "aggregate_max_ColumnA", + COUNT( + CASE + WHEN "Id" IN (2,3) THEN "ColumnC" + ELSE NULL + END + ) AS "aggregate_count_ColumnC" +FROM "Test" +``` + +##### Redaction in Filtering +When comparing a column to something during filtering, data redaction can require that the column be redacted before the comparison is performed. + +For example, here's a query that uses redaction inside the filter expression in `where`: + +```jsonc +{ + "table": ["Test"], + "table_relationships": [], + // Redaction expressions are defined per table/function + "redaction_expressions": [ + { + "target": { + "type": "table", + "table": ["Test"] // These expressions are defined for the Test table + }, + "expressions": { + // Redaction expressions are named (names are only unique within a table/function) + "RedactionExp0": { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [1,2], + "value_type": "number" + } + } + } + ], + "query": { + "fields": { + "Id": { + "type": "column", + "column": "Id", + "column_type": "number" + } + }, + "where": { + "type": "binary_op", + "operator": "equals", + // ALl column comparisons in "binary_op", "binary_arr_op", and "unary_op" + // can potentially have a redaction expression + "column": { + "name": "ColumnA", + "column_type": "string", + "redaction_expression": "RedactionExp0" + }, + "value": { + "type": "scalar", + "value": "A1", + "value_type": "string" + } + } + } +} +``` + +This query might be translated into SQL that is similar to this: + +```sql +SELECT "Id" +FROM "Test" +WHERE + (CASE + WHEN "Id" IN (1,2) THEN "ColumnA" + ELSE NULL + END) = "A1" +``` + +##### Redaction in Ordering +Data redaction also needs to be applied when ordering upon a column; namely the ordering should be performed across the redacted values. + +For example, here's a query that uses redaction inside the `order_by`: + +```jsonc +{ + "table": ["Test"], + "table_relationships": [], + // Redaction expressions are defined per table/function + "redaction_expressions": [ + { + "target": { + "type": "table", + "table": ["Test"] // These expressions are defined for the Test table + }, + "expressions": { + // Redaction expressions are named (names are only unique within a table/function) + "RedactionExp0": { + "type": "binary_arr_op", + "operator": "in", + "column": { "name": "Id", "column_type": "number" }, + "values": [1,2], + "value_type": "number" + } + } + } + ], + "query": { + "fields": { + "Id": { + "type": "column", + "column": "Id", + "column_type": "number" + } + }, + "order_by": { + "relations": {}, + "elements": [ + { + "target_path": [], + // Both "column" and "single_column_aggregate"-typed ordering targets can have + // redaction expressions applied to them + "target": { + "type": "column", + "column": "ColumnA", + "redaction_expression": "RedactionExp0" + }, + "order_direction": "asc" + } + ] + } + } +} +``` + +This query might be translated into SQL that is similar to this: + +```sql +SELECT "Id" +FROM "Test" +ORDER BY + (CASE + WHEN "Id" IN (1,2) THEN "ColumnA" + ELSE NULL + END) ASC +``` + #### Type Definitions The `QueryRequest` TypeScript type in the [reference implementation](./reference/src/types/index.ts) describes the valid request body payloads which may be passed to the `POST /query` endpoint. The response body structure is captured by the `QueryResponse` type. @@ -2458,7 +2822,7 @@ Schema: ```json { - "tables": [ + "tables": [ { "name": [ "Artist" diff --git a/dc-agents/dc-api-types/package.json b/dc-agents/dc-api-types/package.json index 75c70606f7b..5c8446bc147 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.35.0", + "version": "0.36.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 7d3ae653bca..83b0a7853bb 100644 --- a/dc-agents/dc-api-types/src/agent.openapi.json +++ b/dc-agents/dc-api-types/src/agent.openapi.json @@ -568,10 +568,14 @@ "type": "object" }, "ForeachCapabilities": {}, + "RedactionCapabilities": {}, "QueryCapabilities": { "properties": { "foreach": { "$ref": "#/components/schemas/ForeachCapabilities" + }, + "redaction": { + "$ref": "#/components/schemas/RedactionCapabilities" } }, "type": "object" @@ -1710,28 +1714,56 @@ } ] }, - "Field": { + "TNFunction": { + "properties": { + "function": { + "$ref": "#/components/schemas/FunctionName" + }, + "type": { + "enum": [ + "function" + ], + "type": "string" + } + }, + "required": [ + "function", + "type" + ], + "type": "object" + }, + "TNTable": { + "properties": { + "table": { + "$ref": "#/components/schemas/TableName" + }, + "type": { + "enum": [ + "table" + ], + "type": "string" + } + }, + "required": [ + "table", + "type" + ], + "type": "object" + }, + "TargetName": { "discriminator": { "mapping": { - "array": "NestedArrayField", - "column": "ColumnField", - "object": "NestedObjField", - "relationship": "RelationshipField" + "function": "TNFunction", + "table": "TNTable" }, "propertyName": "type" }, "oneOf": [ { - "$ref": "#/components/schemas/NestedArrayField" + "$ref": "#/components/schemas/TNFunction" }, { - "$ref": "#/components/schemas/NestedObjField" - }, - { - "$ref": "#/components/schemas/ColumnField" - }, - { - "$ref": "#/components/schemas/RelationshipField" + "$ref": "#/components/schemas/TNTable" } ] }, @@ -1753,6 +1785,9 @@ } ] }, + "RedactionExpressionName": { + "type": "string" + }, "ComparisonColumn": { "properties": { "column_type": { @@ -1780,6 +1815,9 @@ "type": "string" }, "type": "array" + }, + "redaction_expression": { + "$ref": "#/components/schemas/RedactionExpressionName" } }, "required": [ @@ -2126,6 +2164,50 @@ ], "type": "object" }, + "TargetRedactionExpressions": { + "properties": { + "expressions": { + "additionalProperties": { + "$ref": "#/components/schemas/Expression" + }, + "description": "The named redaction expressions associated with the target", + "type": "object" + }, + "target": { + "$ref": "#/components/schemas/TargetName" + } + }, + "required": [ + "target", + "expressions" + ], + "type": "object" + }, + "Field": { + "discriminator": { + "mapping": { + "array": "NestedArrayField", + "column": "ColumnField", + "object": "NestedObjField", + "relationship": "RelationshipField" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/NestedArrayField" + }, + { + "$ref": "#/components/schemas/NestedObjField" + }, + { + "$ref": "#/components/schemas/ColumnField" + }, + { + "$ref": "#/components/schemas/RelationshipField" + } + ] + }, "OrderByRelation": { "properties": { "subrelations": { @@ -2157,6 +2239,9 @@ "function": { "$ref": "#/components/schemas/SingleColumnAggregateFunction" }, + "redaction_expression": { + "$ref": "#/components/schemas/RedactionExpressionName" + }, "result_type": { "$ref": "#/components/schemas/ScalarType" }, @@ -2180,6 +2265,9 @@ "column": { "type": "string" }, + "redaction_expression": { + "$ref": "#/components/schemas/RedactionExpressionName" + }, "type": { "enum": [ "column" @@ -2398,6 +2486,9 @@ "column_type": { "$ref": "#/components/schemas/ScalarType" }, + "redaction_expression": { + "$ref": "#/components/schemas/RedactionExpressionName" + }, "type": { "enum": [ "column" @@ -2444,6 +2535,9 @@ "function": { "$ref": "#/components/schemas/SingleColumnAggregateFunction" }, + "redaction_expression": { + "$ref": "#/components/schemas/RedactionExpressionName" + }, "result_type": { "$ref": "#/components/schemas/ScalarType" }, @@ -2472,6 +2566,9 @@ "description": "Whether or not only distinct items should be counted", "type": "boolean" }, + "redaction_expression": { + "$ref": "#/components/schemas/RedactionExpressionName" + }, "type": { "enum": [ "column_count" @@ -2537,6 +2634,14 @@ "query": { "$ref": "#/components/schemas/Query" }, + "redaction_expressions": { + "default": [], + "description": "Expressions that can be referenced by the query to redact fields/columns", + "items": { + "$ref": "#/components/schemas/TargetRedactionExpressions" + }, + "type": "array" + }, "relationships": { "description": "The relationships between entities involved in the entire query request", "items": { @@ -2590,6 +2695,14 @@ "query": { "$ref": "#/components/schemas/Query" }, + "redaction_expressions": { + "default": [], + "description": "Expressions that can be referenced by the query to redact fields/columns", + "items": { + "$ref": "#/components/schemas/TargetRedactionExpressions" + }, + "type": "array" + }, "table": { "$ref": "#/components/schemas/TableName" }, @@ -2702,6 +2815,14 @@ }, "type": "array" }, + "redaction_expressions": { + "default": [], + "description": "Expressions that can be referenced by the query to redact fields/columns", + "items": { + "$ref": "#/components/schemas/TargetRedactionExpressions" + }, + "type": "array" + }, "table_relationships": { "description": "The relationships between tables involved in the entire mutation request", "items": { diff --git a/dc-agents/dc-api-types/src/index.ts b/dc-agents/dc-api-types/src/index.ts index 5c7b702d70f..6536a7706cc 100644 --- a/dc-agents/dc-api-types/src/index.ts +++ b/dc-agents/dc-api-types/src/index.ts @@ -109,6 +109,8 @@ export type { QueryResponse } from './models/QueryResponse'; export type { RawCapabilities } from './models/RawCapabilities'; export type { RawRequest } from './models/RawRequest'; export type { RawResponse } from './models/RawResponse'; +export type { RedactionCapabilities } from './models/RedactionCapabilities'; +export type { RedactionExpressionName } from './models/RedactionExpressionName'; export type { RelatedTable } from './models/RelatedTable'; export type { Relationship } from './models/Relationship'; export type { RelationshipCapabilities } from './models/RelationshipCapabilities'; @@ -137,6 +139,10 @@ export type { TableName } from './models/TableName'; export type { TableRelationships } from './models/TableRelationships'; export type { TableRequest } from './models/TableRequest'; export type { TableType } from './models/TableType'; +export type { TargetName } from './models/TargetName'; +export type { TargetRedactionExpressions } from './models/TargetRedactionExpressions'; +export type { TNFunction } from './models/TNFunction'; +export type { TNTable } from './models/TNTable'; export type { UnaryComparisonOperator } from './models/UnaryComparisonOperator'; export type { UniqueIdentifierGenerationStrategy } from './models/UniqueIdentifierGenerationStrategy'; export type { UnrelatedTable } from './models/UnrelatedTable'; diff --git a/dc-agents/dc-api-types/src/models/ColumnCountAggregate.ts b/dc-agents/dc-api-types/src/models/ColumnCountAggregate.ts index 2adab55dde8..66dbe121301 100644 --- a/dc-agents/dc-api-types/src/models/ColumnCountAggregate.ts +++ b/dc-agents/dc-api-types/src/models/ColumnCountAggregate.ts @@ -2,6 +2,8 @@ /* tslint:disable */ /* eslint-disable */ +import type { RedactionExpressionName } from './RedactionExpressionName'; + export type ColumnCountAggregate = { /** * The column to apply the count aggregate function to @@ -11,6 +13,7 @@ export type ColumnCountAggregate = { * Whether or not only distinct items should be counted */ distinct: boolean; + redaction_expression?: RedactionExpressionName; type: 'column_count'; }; diff --git a/dc-agents/dc-api-types/src/models/ColumnField.ts b/dc-agents/dc-api-types/src/models/ColumnField.ts index b43bcc1d6e5..25c0ed0ff59 100644 --- a/dc-agents/dc-api-types/src/models/ColumnField.ts +++ b/dc-agents/dc-api-types/src/models/ColumnField.ts @@ -2,11 +2,13 @@ /* tslint:disable */ /* eslint-disable */ +import type { RedactionExpressionName } from './RedactionExpressionName'; import type { ScalarType } from './ScalarType'; export type ColumnField = { column: string; column_type: ScalarType; + redaction_expression?: RedactionExpressionName; type: 'column'; }; diff --git a/dc-agents/dc-api-types/src/models/ComparisonColumn.ts b/dc-agents/dc-api-types/src/models/ComparisonColumn.ts index 555031a3971..0477afe5e61 100644 --- a/dc-agents/dc-api-types/src/models/ComparisonColumn.ts +++ b/dc-agents/dc-api-types/src/models/ComparisonColumn.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ +import type { RedactionExpressionName } from './RedactionExpressionName'; import type { ScalarType } from './ScalarType'; export type ComparisonColumn = { @@ -14,5 +15,6 @@ export type ComparisonColumn = { * The path to the table that contains the specified column. Missing or empty array means the current table. ["$"] means the query table. No other values are supported at this time. */ path?: Array; + redaction_expression?: RedactionExpressionName; }; diff --git a/dc-agents/dc-api-types/src/models/FunctionRequest.ts b/dc-agents/dc-api-types/src/models/FunctionRequest.ts index c076f527182..16b40e79480 100644 --- a/dc-agents/dc-api-types/src/models/FunctionRequest.ts +++ b/dc-agents/dc-api-types/src/models/FunctionRequest.ts @@ -6,6 +6,7 @@ import type { FunctionName } from './FunctionName'; import type { FunctionRequestArgument } from './FunctionRequestArgument'; import type { Query } from './Query'; import type { Relationships } from './Relationships'; +import type { TargetRedactionExpressions } from './TargetRedactionExpressions'; export type FunctionRequest = { function: FunctionName; @@ -14,6 +15,10 @@ export type FunctionRequest = { */ function_arguments?: Array; query: Query; + /** + * Expressions that can be referenced by the query to redact fields/columns + */ + redaction_expressions?: Array; /** * The relationships between entities involved in the entire query request */ diff --git a/dc-agents/dc-api-types/src/models/MutationRequest.ts b/dc-agents/dc-api-types/src/models/MutationRequest.ts index 80354b05ac0..ddaefb43e03 100644 --- a/dc-agents/dc-api-types/src/models/MutationRequest.ts +++ b/dc-agents/dc-api-types/src/models/MutationRequest.ts @@ -5,6 +5,7 @@ import type { MutationOperation } from './MutationOperation'; import type { TableInsertSchema } from './TableInsertSchema'; import type { TableRelationships } from './TableRelationships'; +import type { TargetRedactionExpressions } from './TargetRedactionExpressions'; export type MutationRequest = { /** @@ -15,6 +16,10 @@ export type MutationRequest = { * The mutation operations to perform */ operations: Array; + /** + * Expressions that can be referenced by the query to redact fields/columns + */ + redaction_expressions?: Array; /** * The relationships between tables involved in the entire mutation request */ diff --git a/dc-agents/dc-api-types/src/models/OrderByColumn.ts b/dc-agents/dc-api-types/src/models/OrderByColumn.ts index b9755904400..89aa9b933d1 100644 --- a/dc-agents/dc-api-types/src/models/OrderByColumn.ts +++ b/dc-agents/dc-api-types/src/models/OrderByColumn.ts @@ -2,8 +2,11 @@ /* tslint:disable */ /* eslint-disable */ +import type { RedactionExpressionName } from './RedactionExpressionName'; + export type OrderByColumn = { column: string; + redaction_expression?: RedactionExpressionName; type: 'column'; }; diff --git a/dc-agents/dc-api-types/src/models/OrderBySingleColumnAggregate.ts b/dc-agents/dc-api-types/src/models/OrderBySingleColumnAggregate.ts index 3255b8280aa..4a1ba64eeb2 100644 --- a/dc-agents/dc-api-types/src/models/OrderBySingleColumnAggregate.ts +++ b/dc-agents/dc-api-types/src/models/OrderBySingleColumnAggregate.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ +import type { RedactionExpressionName } from './RedactionExpressionName'; import type { ScalarType } from './ScalarType'; import type { SingleColumnAggregateFunction } from './SingleColumnAggregateFunction'; @@ -11,6 +12,7 @@ export type OrderBySingleColumnAggregate = { */ column: string; function: SingleColumnAggregateFunction; + redaction_expression?: RedactionExpressionName; result_type: ScalarType; type: 'single_column_aggregate'; }; diff --git a/dc-agents/dc-api-types/src/models/QueryCapabilities.ts b/dc-agents/dc-api-types/src/models/QueryCapabilities.ts index f08fa88d7b0..28a4a57f7b6 100644 --- a/dc-agents/dc-api-types/src/models/QueryCapabilities.ts +++ b/dc-agents/dc-api-types/src/models/QueryCapabilities.ts @@ -3,8 +3,10 @@ /* eslint-disable */ import type { ForeachCapabilities } from './ForeachCapabilities'; +import type { RedactionCapabilities } from './RedactionCapabilities'; export type QueryCapabilities = { foreach?: ForeachCapabilities; + redaction?: RedactionCapabilities; }; diff --git a/dc-agents/dc-api-types/src/models/RedactionCapabilities.ts b/dc-agents/dc-api-types/src/models/RedactionCapabilities.ts new file mode 100644 index 00000000000..69ee0a89977 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/RedactionCapabilities.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type RedactionCapabilities = { +}; + diff --git a/dc-agents/dc-api-types/src/models/RedactionExpressionName.ts b/dc-agents/dc-api-types/src/models/RedactionExpressionName.ts new file mode 100644 index 00000000000..64ca979e299 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/RedactionExpressionName.ts @@ -0,0 +1,5 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type RedactionExpressionName = string; diff --git a/dc-agents/dc-api-types/src/models/SingleColumnAggregate.ts b/dc-agents/dc-api-types/src/models/SingleColumnAggregate.ts index ec0cb74e5f3..4639d2eec9c 100644 --- a/dc-agents/dc-api-types/src/models/SingleColumnAggregate.ts +++ b/dc-agents/dc-api-types/src/models/SingleColumnAggregate.ts @@ -2,6 +2,7 @@ /* tslint:disable */ /* eslint-disable */ +import type { RedactionExpressionName } from './RedactionExpressionName'; import type { ScalarType } from './ScalarType'; import type { SingleColumnAggregateFunction } from './SingleColumnAggregateFunction'; @@ -11,6 +12,7 @@ export type SingleColumnAggregate = { */ column: string; function: SingleColumnAggregateFunction; + redaction_expression?: RedactionExpressionName; result_type: ScalarType; type: 'single_column'; }; diff --git a/dc-agents/dc-api-types/src/models/TNFunction.ts b/dc-agents/dc-api-types/src/models/TNFunction.ts new file mode 100644 index 00000000000..4e5c1859057 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/TNFunction.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { FunctionName } from './FunctionName'; + +export type TNFunction = { + function: FunctionName; + type: 'function'; +}; + diff --git a/dc-agents/dc-api-types/src/models/TNTable.ts b/dc-agents/dc-api-types/src/models/TNTable.ts new file mode 100644 index 00000000000..2c652b49499 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/TNTable.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { TableName } from './TableName'; + +export type TNTable = { + table: TableName; + type: 'table'; +}; + diff --git a/dc-agents/dc-api-types/src/models/TableRequest.ts b/dc-agents/dc-api-types/src/models/TableRequest.ts index ba53feeb4c3..161de9b3c6b 100644 --- a/dc-agents/dc-api-types/src/models/TableRequest.ts +++ b/dc-agents/dc-api-types/src/models/TableRequest.ts @@ -6,6 +6,7 @@ import type { Query } from './Query'; import type { Relationships } from './Relationships'; import type { ScalarValue } from './ScalarValue'; import type { TableName } from './TableName'; +import type { TargetRedactionExpressions } from './TargetRedactionExpressions'; export type TableRequest = { /** @@ -13,6 +14,10 @@ export type TableRequest = { */ foreach?: Array> | null; query: Query; + /** + * Expressions that can be referenced by the query to redact fields/columns + */ + redaction_expressions?: Array; table: TableName; /** * The relationships between tables involved in the entire query request diff --git a/dc-agents/dc-api-types/src/models/TargetName.ts b/dc-agents/dc-api-types/src/models/TargetName.ts new file mode 100644 index 00000000000..a802b97b429 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/TargetName.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { TNFunction } from './TNFunction'; +import type { TNTable } from './TNTable'; + +export type TargetName = (TNFunction | TNTable); + diff --git a/dc-agents/dc-api-types/src/models/TargetRedactionExpressions.ts b/dc-agents/dc-api-types/src/models/TargetRedactionExpressions.ts new file mode 100644 index 00000000000..f252595d58a --- /dev/null +++ b/dc-agents/dc-api-types/src/models/TargetRedactionExpressions.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Expression } from './Expression'; +import type { TargetName } from './TargetName'; + +export type TargetRedactionExpressions = { + /** + * The named redaction expressions associated with the target + */ + expressions: Record; + target: TargetName; +}; + diff --git a/dc-agents/package-lock.json b/dc-agents/package-lock.json index f08c92b5fdc..32bd95a6d37 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.35.0", + "version": "0.36.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.35.0", + "@hasura/dc-api-types": "0.36.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.35.0", + "@hasura/dc-api-types": "0.36.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.35.0", + "@hasura/dc-api-types": "0.36.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.35.0", + "@hasura/dc-api-types": "0.36.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 008267c81cb..68912754af9 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.35.0", + "@hasura/dc-api-types": "0.36.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.35.0", + "version": "0.36.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 7f85d147834..9beab1dc93f 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.35.0", + "@hasura/dc-api-types": "0.36.0", "fastify": "^4.13.0", "mathjs": "^11.0.0", "pino-pretty": "^8.0.0", diff --git a/dc-agents/sqlite/package-lock.json b/dc-agents/sqlite/package-lock.json index 936318dba13..ad07e4ccb4a 100644 --- a/dc-agents/sqlite/package-lock.json +++ b/dc-agents/sqlite/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.35.0", + "@hasura/dc-api-types": "0.36.0", "fastify": "^4.13.0", "fastify-metrics": "^9.2.1", "nanoid": "^3.3.4", @@ -57,7 +57,7 @@ "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" }, "node_modules/@hasura/dc-api-types": { - "version": "0.35.0", + "version": "0.36.0", "license": "Apache-2.0", "devDependencies": { "@tsconfig/node16": "^1.0.3", diff --git a/dc-agents/sqlite/package.json b/dc-agents/sqlite/package.json index ccdec8bb8dc..9f1dcd4c03e 100644 --- a/dc-agents/sqlite/package.json +++ b/dc-agents/sqlite/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.35.0", + "@hasura/dc-api-types": "0.36.0", "fastify-metrics": "^9.2.1", "fastify": "^4.13.0", "nanoid": "^3.3.4", diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 573c0fe2b5c..1704ee4c718 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -1185,6 +1185,7 @@ test-suite graphql-engine-tests Hasura.Backends.DataConnector.API.V0.ColumnSpec Hasura.Backends.DataConnector.API.V0.ConfigSchemaSpec Hasura.Backends.DataConnector.API.V0.ExpressionSpec + Hasura.Backends.DataConnector.API.V0.FunctionSpec Hasura.Backends.DataConnector.API.V0.MutationsSpec Hasura.Backends.DataConnector.API.V0.OrderBySpec Hasura.Backends.DataConnector.API.V0.QuerySpec @@ -1192,6 +1193,7 @@ test-suite graphql-engine-tests Hasura.Backends.DataConnector.API.V0.ScalarSpec Hasura.Backends.DataConnector.API.V0.SchemaSpec Hasura.Backends.DataConnector.API.V0.TableSpec + Hasura.Backends.DataConnector.API.V0.TargetSpec Hasura.Backends.Postgres.Connection.VersionCheckSpec Hasura.Backends.Postgres.Execute.PrepareSpec Hasura.Backends.Postgres.NativeQueries.NativeQueriesSpec 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 8f84f6ca5fb..2460b51e94f 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/AggregateQuerySpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/AggregateQuerySpec.hs @@ -150,15 +150,15 @@ tests = describe "Aggregate Query Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("ArtistIds_Id", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number")), - ("ArtistNames_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), + [ ("ArtistIds_Id", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number") Nothing), + ("ArtistNames_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing), ( "nodes_Albums", API.RelField ( API.RelationshipField (API.RelationshipName "Albums") ( emptyQuery & API.qFields - ?~ mkFieldsMap [("nodes_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string"))] + ?~ mkFieldsMap [("nodes_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing)] ) ) ) @@ -280,7 +280,7 @@ tests = describe "Aggregate Query Tests" $ do & API.qAggregates ?~ mkFieldsMap [ ("counts_count", API.StarCount), - ("counts_uniqueBillingCountries", API.ColumnCount (API.ColumnCountAggregate (API.ColumnName "BillingCountry") True)), + ("counts_uniqueBillingCountries", API.ColumnCount (API.ColumnCountAggregate (API.ColumnName "BillingCountry") Nothing 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"))) ] @@ -309,7 +309,7 @@ tests = describe "Aggregate Query Tests" $ do ) singleColumnAggregateMax :: API.ColumnName -> API.ScalarType -> API.SingleColumnAggregate -singleColumnAggregateMax = API.SingleColumnAggregate $ API.SingleColumnAggregateFunction [G.name|max|] +singleColumnAggregateMax columnName scalarType = API.SingleColumnAggregate (API.SingleColumnAggregateFunction [G.name|max|]) columnName Nothing scalarType singleColumnAggregateMin :: API.ColumnName -> API.ScalarType -> API.SingleColumnAggregate -singleColumnAggregateMin = API.SingleColumnAggregate $ API.SingleColumnAggregateFunction [G.name|min|] +singleColumnAggregateMin columnName scalarType = API.SingleColumnAggregate (API.SingleColumnAggregateFunction [G.name|min|]) columnName Nothing scalarType 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 34067a26a55..77b0d560f5b 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/BasicQuerySpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/BasicQuerySpec.hs @@ -144,8 +144,8 @@ tests = describe "Basic Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), - ("title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")) + [ ("id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] & API.qLimit ?~ 1 @@ -188,8 +188,8 @@ tests = describe "Basic Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")), - ("title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")) + [ ("id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] & API.qLimit ?~ 1 @@ -243,8 +243,8 @@ tests = describe "Basic Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("id", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), - ("name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + [ ("id", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number") Nothing), + ("name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing) ] & API.qLimit ?~ 3 -- The permissions limit is smaller than the query limit, so it is used @@ -294,14 +294,14 @@ tests = describe "Basic Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("CustomerId", API.ColumnField (API.ColumnName "CustomerId") $ API.ScalarType "number") + [ ("CustomerId", API.ColumnField (API.ColumnName "CustomerId") (API.ScalarType "number") Nothing) ] & API.qWhere ?~ API.Exists (API.UnrelatedTable $ mkTableName "Employee") ( API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "EmployeeId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "EmployeeId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 1) (API.ScalarType "number")) ) ) 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 414e071aff8..f71c4ca85ff 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/CustomScalarsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/CustomScalarsSpec.hs @@ -93,12 +93,12 @@ tests = describe "Custom scalar parsing tests" $ do ( 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") + [ ("MyIntColumn", API.ColumnField (API.ColumnName "MyIntColumn") (API.ScalarType "MyInt") Nothing), + ("MyFloatColumn", API.ColumnField (API.ColumnName "MyFloatColumn") (API.ScalarType "MyFloat") Nothing), + ("MyStringColumn", API.ColumnField (API.ColumnName "MyStringColumn") (API.ScalarType "MyString") Nothing), + ("MyBooleanColumn", API.ColumnField (API.ColumnName "MyBooleanColumn") (API.ScalarType "MyBoolean") Nothing), + ("MyIDColumn", API.ColumnField (API.ColumnName "MyIDColumn") (API.ScalarType "MyID") Nothing), + ("MyAnythingColumn", API.ColumnField (API.ColumnName "MyAnythingColumn") (API.ScalarType "MyAnything") Nothing) ] & API.qLimit ?~ 1 @@ -151,12 +151,12 @@ tests = describe "Custom scalar parsing tests" $ do ( 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") + [ ("MyIntColumn", API.ColumnField (API.ColumnName "MyIntColumn") (API.ScalarType "MyInt") Nothing), + ("MyFloatColumn", API.ColumnField (API.ColumnName "MyFloatColumn") (API.ScalarType "MyFloat") Nothing), + ("MyStringColumn", API.ColumnField (API.ColumnName "MyStringColumn") (API.ScalarType "MyString") Nothing), + ("MyBooleanColumn", API.ColumnField (API.ColumnName "MyBooleanColumn") (API.ScalarType "MyBoolean") Nothing), + ("MyIDColumn", API.ColumnField (API.ColumnName "MyIDColumn") (API.ScalarType "MyID") Nothing), + ("MyAnythingColumn", API.ColumnField (API.ColumnName "MyAnythingColumn") (API.ScalarType "MyAnything") Nothing) ] & API.qLimit ?~ 1 @@ -165,27 +165,27 @@ tests = describe "Custom scalar parsing tests" $ do ( Set.fromList [ ApplyBinaryComparisonOperator Equal - (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyBooleanColumn") (ScalarType "MyBoolean")) + (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyBooleanColumn") (ScalarType "MyBoolean") Nothing) (ScalarValueComparison $ ScalarValue (J.Bool True) (ScalarType "MyBoolean")), ApplyBinaryComparisonOperator Equal - (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyFloatColumn") (ScalarType "MyFloat")) + (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyFloatColumn") (ScalarType "MyFloat") Nothing) (ScalarValueComparison $ ScalarValue (J.Number 3.14) (ScalarType "MyFloat")), ApplyBinaryComparisonOperator Equal - (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyStringColumn") (ScalarType "MyString")) + (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyStringColumn") (ScalarType "MyString") Nothing) (ScalarValueComparison $ ScalarValue (J.String "foo") (ScalarType "MyString")), ApplyBinaryComparisonOperator Equal - (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyIDColumn") (ScalarType "MyID")) + (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyIDColumn") (ScalarType "MyID") Nothing) (ScalarValueComparison $ ScalarValue (J.String "x") (ScalarType "MyID")), ApplyBinaryComparisonOperator Equal - (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyIntColumn") (ScalarType "MyInt")) + (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyIntColumn") (ScalarType "MyInt") Nothing) (ScalarValueComparison $ ScalarValue (J.Number 42.0) (ScalarType "MyInt")), ApplyBinaryComparisonOperator Equal - (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyAnythingColumn") (ScalarType "MyAnything")) + (ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "MyAnythingColumn") (ScalarType "MyAnything") Nothing) (ScalarValueComparison $ ScalarValue (J.Object mempty) (ScalarType "MyAnything")) ] ) 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 7f19339aae5..b19ec312d0f 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/DeleteMutationsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/DeleteMutationsSpec.hs @@ -204,17 +204,17 @@ tests = do $ Set.fromList [ API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "ArtistId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "ArtistId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 90) (API.ScalarType "number")), API.ApplyBinaryComparisonOperator API.GreaterThan - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.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_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("deletedRows_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing), ( "deletedRows_Artist", API.RelField ( API.RelationshipField @@ -222,8 +222,8 @@ tests = do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), - ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number") Nothing), + ("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing) ] ) ) @@ -312,17 +312,17 @@ tests = do $ Set.fromList [ API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "ArtistId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "ArtistId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 90) (API.ScalarType "number")), API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.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")), + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing), ( "Artist", API.RelField ( API.RelationshipField @@ -330,8 +330,8 @@ tests = do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), - ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number") Nothing), + ("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") 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 07079793802..ef613ccc0e5 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/ErrorSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/ErrorSpec.hs @@ -100,8 +100,8 @@ tests = describe "Error Protocol Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("id", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - ("title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + [ ("id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] & 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 2f444a3bf96..2ee982b5951 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/InsertMutationsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/InsertMutationsSpec.hs @@ -221,12 +221,12 @@ tests = do Just $ API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "ArtistId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "ArtistId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.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_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("insertedRows_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing), ( "insertedRows_Artist", API.RelField ( API.RelationshipField @@ -234,8 +234,8 @@ tests = do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"), - ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string") + [ ("ArtistId", API.ColumnField (API.ColumnName "ArtistId") (API.ScalarType "number") Nothing), + ("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") 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 5409446963b..43d55a90d19 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/OrderBySpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/OrderBySpec.hs @@ -119,13 +119,13 @@ tests = describe "Order By Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - ("Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] & API.qLimit ?~ 3 & API.qOrderBy - ?~ API.OrderBy mempty (API.OrderByElement [] (API.OrderByColumn (API.ColumnName "AlbumId")) API.Ascending :| []) + ?~ API.OrderBy mempty (API.OrderByElement [] (API.OrderByColumn (API.ColumnName "AlbumId") Nothing) API.Ascending :| []) ) ) @@ -163,7 +163,7 @@ tests = describe "Order By Tests" $ do (mkTableName "Artist") ( emptyQuery & API.qFields - ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string"))] + ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing)] & API.qLimit ?~ 2 & API.qOrderBy @@ -182,6 +182,7 @@ tests = describe "Order By Tests" $ do $ API.SingleColumnAggregate (API.SingleColumnAggregateFunction [G.name|max|]) (API.ColumnName "AlbumId") + Nothing (API.ScalarType "number") ) API.Ascending 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 abaabdc5483..df726046804 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/QueryRelationshipsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/QueryRelationshipsSpec.hs @@ -198,19 +198,19 @@ tests = describe "Object Relationships Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string"), + [ ("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing), ( "Genre", API.RelField ( API.RelationshipField (API.RelationshipName "Genre") - (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing)]) ) ), ( "MediaType", API.RelField ( API.RelationshipField (API.RelationshipName "MediaType") - (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing)]) ) ) ] @@ -298,7 +298,7 @@ tests = describe "Object Relationships Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string"), + [ ("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing), ( "Album", API.RelField ( API.RelationshipField @@ -310,7 +310,7 @@ tests = describe "Object Relationships Tests" $ do API.RelField ( API.RelationshipField (API.RelationshipName "Artist") - (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string"))]) + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing)]) ) ) ] @@ -338,8 +338,8 @@ tests = describe "Object Relationships Tests" $ do ] ) ( 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.OrderByElement [API.RelationshipName "Album", API.RelationshipName "Artist"] (API.OrderByColumn (API.ColumnName "Name") Nothing) API.Descending, + API.OrderByElement [] (API.OrderByColumn (API.ColumnName "Name") Nothing) API.Ascending ] ) ) @@ -409,7 +409,7 @@ tests = describe "Object Relationships Tests" $ do (mkTableName "Employee") ( emptyQuery & API.qFields - ?~ mkFieldsMap [("EmployeeId", API.ColumnField (API.ColumnName "EmployeeId") $ API.ScalarType "number")] + ?~ mkFieldsMap [("EmployeeId", API.ColumnField (API.ColumnName "EmployeeId") (API.ScalarType "number") Nothing)] & API.qLimit ?~ 1 & API.qWhere @@ -417,8 +417,8 @@ tests = describe "Object Relationships Tests" $ do (API.RelatedTable $ API.RelationshipName "SupportRepForCustomers") ( API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "Country") $ API.ScalarType "string") - (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.mkColumnSelector $ API.ColumnName "Country") $ API.ScalarType "string")) + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "Country") (API.ScalarType "string") Nothing) + (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.mkColumnSelector $ API.ColumnName "Country") (API.ScalarType "string") Nothing)) ) & API.qOrderBy ?~ API.OrderBy @@ -429,8 +429,8 @@ tests = describe "Object Relationships Tests" $ do $ API.Exists (API.RelatedTable $ API.RelationshipName "SupportRep") $ API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "Country") $ API.ScalarType "string") - (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.mkColumnSelector $ API.ColumnName "Country") $ API.ScalarType "string")) + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "Country") (API.ScalarType "string") Nothing) + (API.AnotherColumnComparison (API.ComparisonColumn API.QueryTable (API.mkColumnSelector $ API.ColumnName "Country") (API.ScalarType "string") Nothing)) ) mempty ) 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 53058784f11..7fb36bf4f63 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/RemoteRelationshipsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/RemoteRelationshipsSpec.hs @@ -270,8 +270,8 @@ tests = do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - ("Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] ) & API._QRTable @@ -361,8 +361,8 @@ tests = do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - ("Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + [ ("AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] ) & API._QRTable @@ -464,8 +464,8 @@ tests = do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("nodes_AlbumId", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - ("nodes_Title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + [ ("nodes_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("nodes_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] & API.qAggregates ?~ mkFieldsMap [("aggregate_count", API.StarCount)] diff --git a/server/lib/api-tests/src/Test/DataConnector/MockAgent/TestHelpers.hs b/server/lib/api-tests/src/Test/DataConnector/MockAgent/TestHelpers.hs index 5faaa8108dc..4f501c55b7c 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/TestHelpers.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/TestHelpers.hs @@ -19,13 +19,13 @@ mkTableName :: Text -> API.TableName mkTableName name = API.TableName (name :| []) mkTableRequest :: API.TableName -> API.Query -> API.QueryRequest -mkTableRequest tableName query = API.QRTable $ API.TableRequest tableName mempty query Nothing +mkTableRequest tableName query = API.QRTable $ API.TableRequest tableName mempty mempty query Nothing emptyQuery :: API.Query emptyQuery = API.Query Nothing Nothing Nothing Nothing Nothing Nothing Nothing emptyMutationRequest :: API.MutationRequest -emptyMutationRequest = API.MutationRequest mempty mempty [] +emptyMutationRequest = API.MutationRequest mempty mempty mempty [] mkRowsQueryResponse :: [[(Text, API.FieldValue)]] -> API.QueryResponse mkRowsQueryResponse rows = API.QueryResponse (Just $ mkFieldsMap <$> rows) Nothing 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 b4d84f8837b..880400f530c 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/TransformedConfigurationSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/TransformedConfigurationSpec.hs @@ -139,8 +139,8 @@ tests = describe "Transformed Configuration Tests" $ do ( emptyQuery & API.qFields ?~ mkFieldsMap - [ ("id", API.ColumnField (API.ColumnName "AlbumId") $ API.ScalarType "number"), - ("title", API.ColumnField (API.ColumnName "Title") $ API.ScalarType "string") + [ ("id", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number") Nothing), + ("title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string") Nothing) ] & API.qLimit ?~ 1 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 bca4dfb6973..5d483d3792e 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MockAgent/UpdateMutationsSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MockAgent/UpdateMutationsSpec.hs @@ -239,28 +239,28 @@ tests = do $ Set.fromList [ API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 3) (API.ScalarType "number")), API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "GenreId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "GenreId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 1) (API.ScalarType "number")) ], API._umoPostUpdateCheck = Just $ API.ApplyBinaryComparisonOperator API.GreaterThan - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "UnitPrice") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "UnitPrice") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.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_TrackId", API.ColumnField (API.ColumnName "TrackId") (API.ScalarType "number") Nothing), + ("updatedRows_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing), ( "updatedRows_Genre", API.RelField ( API.RelationshipField (API.RelationshipName "Genre") - (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing)]) ) ) ] @@ -366,17 +366,17 @@ tests = do Just $ API.ApplyBinaryComparisonOperator API.GreaterThan - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "UnitPrice") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "UnitPrice") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 0) (API.ScalarType "number")) let sharedReturning = mkFieldsMap - [ ("updatedRows_TrackId", API.ColumnField (API.ColumnName "TrackId") (API.ScalarType "number")), - ("updatedRows_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string")), + [ ("updatedRows_TrackId", API.ColumnField (API.ColumnName "TrackId") (API.ScalarType "number") Nothing), + ("updatedRows_Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing), ( "updatedRows_Genre", API.RelField ( API.RelationshipField (API.RelationshipName "Genre") - (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")]) + (emptyQuery & API.qFields ?~ mkFieldsMap [("Name", API.ColumnField (API.ColumnName "Name") (API.ScalarType "string") Nothing)]) ) ) ] @@ -429,11 +429,11 @@ tests = do $ Set.fromList [ API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 3) (API.ScalarType "number")), API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "TrackId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "TrackId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 3) (API.ScalarType "number")) ], API._umoPostUpdateCheck = sharedPostUpdateCheck, @@ -469,11 +469,11 @@ tests = do $ Set.fromList [ API.ApplyBinaryComparisonOperator API.Equal - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "AlbumId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 3) (API.ScalarType "number")), API.ApplyBinaryComparisonOperator API.GreaterThan - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "TrackId") $ API.ScalarType "number") + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "TrackId") (API.ScalarType "number") Nothing) (API.ScalarValueComparison $ API.ScalarValue (J.Number 3) (API.ScalarType "number")) ], API._umoPostUpdateCheck = sharedPostUpdateCheck, diff --git a/server/lib/dc-api/dc-api.cabal b/server/lib/dc-api/dc-api.cabal index 0542040ed27..d324bc7bc96 100644 --- a/server/lib/dc-api/dc-api.cabal +++ b/server/lib/dc-api/dc-api.cabal @@ -100,6 +100,7 @@ library Hasura.Backends.DataConnector.API.V0.Scalar Hasura.Backends.DataConnector.API.V0.Schema Hasura.Backends.DataConnector.API.V0.Table + Hasura.Backends.DataConnector.API.V0.Target Hasura.Backends.DataConnector.API.V0.Dataset other-modules: Hasura.Backends.DataConnector.API.V0.Name diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs index 3be26662ba8..60815d9966d 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs @@ -15,6 +15,7 @@ module Hasura.Backends.DataConnector.API.V0 module Scalar, module Schema, module Table, + module Target, module Dataset, ) where @@ -36,3 +37,4 @@ import Hasura.Backends.DataConnector.API.V0.Relationships as Relationships import Hasura.Backends.DataConnector.API.V0.Scalar as Scalar import Hasura.Backends.DataConnector.API.V0.Schema as Schema import Hasura.Backends.DataConnector.API.V0.Table as Table +import Hasura.Backends.DataConnector.API.V0.Target as Target diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Aggregate.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Aggregate.hs index 800a2314c78..8968f0c769c 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Aggregate.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Aggregate.hs @@ -16,6 +16,7 @@ import Data.HashMap.Strict qualified as HashMap import Data.OpenApi (ToSchema) import GHC.Generics (Generic) import Hasura.Backends.DataConnector.API.V0.Column qualified as API.V0 +import Hasura.Backends.DataConnector.API.V0.Expression qualified as API.V0 import Hasura.Backends.DataConnector.API.V0.Name (nameCodec) import Hasura.Backends.DataConnector.API.V0.Scalar qualified as API.V0 import Language.GraphQL.Draft.Syntax qualified as GQL @@ -24,6 +25,7 @@ import Prelude data SingleColumnAggregate = SingleColumnAggregate { _scaFunction :: SingleColumnAggregateFunction, _scaColumn :: API.V0.ColumnName, + _scaRedactionExpression :: Maybe API.V0.RedactionExpressionName, _scaResultType :: API.V0.ScalarType } deriving stock (Eq, Ord, Show, Generic) @@ -33,6 +35,7 @@ singleColumnAggregateObjectCodec = SingleColumnAggregate <$> requiredField "function" "The aggregation function" .= _scaFunction <*> requiredField "column" "The column to apply the aggregation function to" .= _scaColumn + <*> optionalFieldOrNull "redaction_expression" "If present, the name of the redaction expression to evaluate. If the expression is false, the column value must be nulled out before being aggregated." .= _scaRedactionExpression <*> requiredField "result_type" "The scalar type of the result of the aggregate operation" .= _scaResultType newtype SingleColumnAggregateFunction = SingleColumnAggregateFunction {unSingleColumnAggregateFunction :: GQL.Name} @@ -47,6 +50,7 @@ instance HasCodec SingleColumnAggregateFunction where data ColumnCountAggregate = ColumnCountAggregate { _ccaColumn :: API.V0.ColumnName, + _ccaRedactionExpression :: Maybe API.V0.RedactionExpressionName, _ccaDistinct :: Bool } deriving stock (Eq, Ord, Show, Generic, Data) @@ -55,6 +59,7 @@ columnCountAggregateObjectCodec :: JSONObjectCodec ColumnCountAggregate columnCountAggregateObjectCodec = ColumnCountAggregate <$> requiredField "column" "The column to apply the count aggregate function to" .= _ccaColumn + <*> optionalFieldOrNull "redaction_expression" "If present, the name of the redaction expression to evaluate. If the expression is false, the column value must be nulled out before being aggregated." .= _ccaRedactionExpression <*> requiredField "distinct" "Whether or not only distinct items should be counted" .= _ccaDistinct data Aggregate diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs index a971808c857..e1a8c8a6af2 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs @@ -32,7 +32,9 @@ module Hasura.Backends.DataConnector.API.V0.Capabilities ColumnNullability (..), QueryCapabilities (..), qcForeach, + qcRedaction, ForeachCapabilities (..), + RedactionCapabilities (..), MutationCapabilities (..), InsertCapabilities (..), UpdateCapabilities (..), @@ -173,7 +175,8 @@ instance HasCodec ColumnNullability where ] data QueryCapabilities = QueryCapabilities - { _qcForeach :: Maybe ForeachCapabilities + { _qcForeach :: Maybe ForeachCapabilities, + _qcRedaction :: Maybe RedactionCapabilities } deriving stock (Eq, Ord, Show, Generic, Data) deriving anyclass (NFData, Hashable) @@ -184,6 +187,7 @@ instance HasCodec QueryCapabilities where object "QueryCapabilities" $ QueryCapabilities <$> optionalField "foreach" "Whether or not the agent supports foreach queries, which are used to enable remote joins to the agent" .= _qcForeach + <*> optionalField "redaction" "Whether or not the agent supports redaction expressions in the query" .= _qcRedaction data ForeachCapabilities = ForeachCapabilities {} deriving stock (Eq, Ord, Show, Generic, Data) @@ -194,6 +198,15 @@ instance HasCodec ForeachCapabilities where codec = object "ForeachCapabilities" $ pure ForeachCapabilities +data RedactionCapabilities = RedactionCapabilities {} + deriving stock (Eq, Ord, Show, Generic, Data) + deriving anyclass (NFData, Hashable) + deriving (FromJSON, ToJSON, ToSchema) via Autodocodec RedactionCapabilities + +instance HasCodec RedactionCapabilities where + codec = + object "RedactionCapabilities" $ pure RedactionCapabilities + data MutationCapabilities = MutationCapabilities { _mcInsertCapabilities :: Maybe InsertCapabilities, _mcUpdateCapabilities :: Maybe UpdateCapabilities, diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Expression.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Expression.hs index 88d4433820b..8336fd1ce8e 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Expression.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Expression.hs @@ -12,6 +12,9 @@ module Hasura.Backends.DataConnector.API.V0.Expression ColumnSelector (..), mkColumnSelector, ComparisonValue (..), + TargetRedactionExpressions (..), + RedactionExpressionName (..), + RedactionExpression (..), ) where @@ -19,8 +22,9 @@ import Autodocodec.Extended import Autodocodec.OpenAPI () import Control.DeepSeq (NFData) import Control.Lens ((^.), _1, _2, _3, _4) -import Data.Aeson (FromJSON, ToJSON, Value) +import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey, Value) import Data.Data (Data) +import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict qualified as HashMap import Data.Hashable (Hashable) import Data.List.NonEmpty (NonEmpty (..)) @@ -34,6 +38,7 @@ import Hasura.Backends.DataConnector.API.V0.Column qualified as API.V0 import Hasura.Backends.DataConnector.API.V0.Relationships qualified as API.V0 import Hasura.Backends.DataConnector.API.V0.Scalar qualified as API.V0 import Hasura.Backends.DataConnector.API.V0.Table qualified as API.V0 +import Hasura.Backends.DataConnector.API.V0.Target qualified as API.V0 import Prelude -------------------------------------------------------------------------------- @@ -250,7 +255,10 @@ data ComparisonColumn = ComparisonColumn -- | The name of the column _ccName :: ColumnSelector, -- | The scalar type of the column - _ccColumnType :: API.V0.ScalarType + _ccColumnType :: API.V0.ScalarType, + -- | If present, the name of the redaction expression to evaluate. + -- If the expression is false, the column value must be nulled out before being compared to. + _ccRedactionExpression :: Maybe RedactionExpressionName } deriving stock (Eq, Ord, Show, Generic) deriving (FromJSON, ToJSON, ToSchema) via Autodocodec ComparisonColumn @@ -263,6 +271,7 @@ instance HasCodec ComparisonColumn where <$> optionalFieldWithOmittedDefault "path" CurrentTable "The path to the table that contains the specified column. Missing or empty array means the current table. [\"$\"] means the query table. No other values are supported at this time." .= _ccPath <*> requiredField "name" "The name of the column" .= _ccName <*> requiredField "column_type" "The scalar type of the column" .= _ccColumnType + <*> optionalFieldOrNull "redaction_expression" "If present, the name of the redaction expression to evaluate. If the expression is false, the column value must be nulled out before being compared to." .= _ccRedactionExpression -- | Describes what table a column is located on. This may either be the "current" table -- (which would be query table, or the table specified by the closest ancestor 'Exists' @@ -332,3 +341,35 @@ instance HasCodec ComparisonValue where [ ("column", ("AnotherColumnComparison", mapToDecoder AnotherColumnComparison columnCodec)), ("scalar", ("ScalarValueComparison", mapToDecoder ScalarValueComparison objectCodec)) ] + +data TargetRedactionExpressions = TargetRedactionExpressions + { _treTarget :: API.V0.TargetName, + _treExpressions :: HashMap RedactionExpressionName RedactionExpression + } + deriving stock (Eq, Ord, Show, Generic) + deriving anyclass (NFData, Hashable) + deriving (FromJSON, ToJSON, ToSchema) via Autodocodec TargetRedactionExpressions + +instance HasCodec TargetRedactionExpressions where + codec = + object "TargetRedactionExpressions" $ + TargetRedactionExpressions + <$> requiredField "target" "The target entity with whom the redaction expressions are to be used with" .= _treTarget + <*> requiredField "expressions" "The named redaction expressions associated with the target" .= _treExpressions + +newtype RedactionExpressionName = RedactionExpressionName {unRedactionExpressionName :: Text} + deriving stock (Eq, Ord, Show, Generic, Data) + deriving anyclass (NFData, Hashable) + deriving newtype (ToJSONKey, FromJSONKey) + deriving (FromJSON, ToJSON, ToSchema) via Autodocodec RedactionExpressionName + +instance HasCodec RedactionExpressionName where + codec = named "RedactionExpressionName" $ dimapCodec RedactionExpressionName unRedactionExpressionName textCodec + +newtype RedactionExpression = RedactionExpression {unRedactionExpression :: Expression} + deriving stock (Eq, Ord, Show, Generic) + deriving anyclass (NFData, Hashable) + deriving (FromJSON, ToJSON, ToSchema) via Autodocodec RedactionExpression + +instance HasCodec RedactionExpression where + codec = dimapCodec RedactionExpression unRedactionExpression codec diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Mutations.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Mutations.hs index bc3fd9ff55f..16234d1e8d5 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Mutations.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Mutations.hs @@ -5,6 +5,7 @@ module Hasura.Backends.DataConnector.API.V0.Mutations ( MutationRequest (..), mrTableRelationships, + mrRedactionExpressions, mrInsertSchema, mrOperations, TableInsertSchema (..), @@ -82,6 +83,7 @@ import Prelude -- TODO: Does this need to be enhanced ala. QueryRequest to support FunctionRequests? data MutationRequest = MutationRequest { _mrTableRelationships :: Set API.V0.TableRelationships, + _mrRedactionExpressions :: Set API.V0.TargetRedactionExpressions, _mrInsertSchema :: Set TableInsertSchema, _mrOperations :: [MutationOperation] } @@ -95,6 +97,8 @@ instance HasCodec MutationRequest where MutationRequest <$> requiredField "table_relationships" "The relationships between tables involved in the entire mutation request" .= _mrTableRelationships + <*> optionalFieldWithOmittedDefault "redaction_expressions" mempty "Expressions that can be referenced by the query to redact fields/columns" + .= _mrRedactionExpressions <*> requiredField "insert_schema" "The schema by which to interpret row data specified in any insert operations in this request" .= _mrInsertSchema <*> requiredField "operations" "The mutation operations to perform" diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/OrderBy.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/OrderBy.hs index 62c9768c4b9..1681c256724 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/OrderBy.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/OrderBy.hs @@ -76,7 +76,7 @@ instance HasCodec OrderByElement where <*> requiredField "order_direction" "The direction of ordering to apply" .= _obeOrderDirection data OrderByTarget - = OrderByColumn API.V0.ColumnName + = OrderByColumn API.V0.ColumnName (Maybe API.V0.RedactionExpressionName) | OrderByStarCountAggregate | OrderBySingleColumnAggregate API.V0.SingleColumnAggregate deriving stock (Eq, Generic, Ord, Show) @@ -87,16 +87,19 @@ instance HasCodec OrderByTarget where object "OrderByTarget" $ discriminatedUnionCodec "type" enc dec where - columnCodec = requiredField' "column" + columnCodec = + (,) + <$> requiredField' "column" .= fst + <*> optionalFieldOrNull "redaction_expression" "If present, the name of the redaction expression to evaluate. If the expression is false, the column value must be nulled out before being ordered over." .= snd starAggregateCodec = pureCodec () singleColumnAggregateCodec = API.V0.singleColumnAggregateObjectCodec enc = \case - OrderByColumn c -> ("column", mapToEncoder c columnCodec) + OrderByColumn c r -> ("column", mapToEncoder (c, r) columnCodec) OrderByStarCountAggregate -> ("star_count_aggregate", mapToEncoder () starAggregateCodec) OrderBySingleColumnAggregate agg -> ("single_column_aggregate", mapToEncoder agg singleColumnAggregateCodec) dec = HashMap.fromList - [ ("column", ("OrderByColumn", mapToDecoder OrderByColumn columnCodec)), + [ ("column", ("OrderByColumn", mapToDecoder (uncurry OrderByColumn) columnCodec)), ("star_count_aggregate", ("OrderByStarCountAggregate", mapToDecoder (const OrderByStarCountAggregate) starAggregateCodec)), ("single_column_aggregate", ("OrderBySingleColumnAggregate", mapToDecoder OrderBySingleColumnAggregate singleColumnAggregateCodec)) ] 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 c88970700c4..30fd91c8185 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 @@ -17,12 +17,14 @@ module Hasura.Backends.DataConnector.API.V0.Query qrForeach, trTable, trRelationships, + trRedactionExpressions, trQuery, trForeach, frFunction, frFunctionArguments, frQuery, frRelationships, + frRedactionExpressions, FieldName (..), Query (..), qFields, @@ -72,6 +74,7 @@ import Data.OpenApi (ToSchema) import Data.Set (Set) import Data.Text (Text) import Data.Text qualified as T +import Data.Tuple.Extra (fst3, snd3, thd3, uncurry3) import GHC.Generics (Generic) import GHC.Show (appPrec, appPrec1) import Hasura.Backends.DataConnector.API.V0.Aggregate qualified as API.V0 @@ -125,13 +128,14 @@ instance HasCodec QueryRequest where ("function", ("FunctionRequest", mapToDecoder QRFunction objectCodec)) ] -pattern TableQueryRequest :: API.V0.TableName -> Set API.V0.Relationships -> Query -> Maybe (NonEmpty (HashMap API.V0.ColumnName API.V0.ScalarValue)) -> QueryRequest -pattern TableQueryRequest table relationships query foreach = QRTable (TableRequest table relationships query foreach) +pattern TableQueryRequest :: API.V0.TableName -> Set API.V0.Relationships -> Set API.V0.TargetRedactionExpressions -> Query -> Maybe (NonEmpty (HashMap API.V0.ColumnName API.V0.ScalarValue)) -> QueryRequest +pattern TableQueryRequest table relationships redactionExps query foreach = QRTable (TableRequest table relationships redactionExps query foreach) -- | A serializable request to retrieve strutured data from tables. data TableRequest = TableRequest { _trTable :: API.V0.TableName, _trRelationships :: Set API.V0.Relationships, + _trRedactionExpressions :: Set API.V0.TargetRedactionExpressions, _trQuery :: Query, _trForeach :: Maybe (NonEmpty (HashMap API.V0.ColumnName API.V0.ScalarValue)) } @@ -146,19 +150,22 @@ instance HasObjectCodec TableRequest where -- NOTE: This can't be done immediately as it would break compatibility in agents. <*> requiredField "table_relationships" "The relationships between tables involved in the entire query request" .= _trRelationships + <*> optionalFieldWithOmittedDefault "redaction_expressions" mempty "Expressions that can be referenced by the query to redact fields/columns" + .= _trRedactionExpressions <*> requiredField "query" "The details of the query against the table" .= _trQuery <*> optionalFieldOrNull "foreach" "If present, a list of columns and values for the columns that the query must be repeated for, applying the column values as a filter for each query." .= _trForeach -pattern FunctionQueryRequest :: API.V0.FunctionName -> [FunctionArgument] -> Set API.V0.Relationships -> Query -> QueryRequest -pattern FunctionQueryRequest function args relationships query = QRFunction (FunctionRequest function args relationships query) +pattern FunctionQueryRequest :: API.V0.FunctionName -> [FunctionArgument] -> Set API.V0.Relationships -> Set API.V0.TargetRedactionExpressions -> Query -> QueryRequest +pattern FunctionQueryRequest function args relationships redactionExps query = QRFunction (FunctionRequest function args relationships redactionExps query) -- | A serializable request to compute strutured data from a function. data FunctionRequest = FunctionRequest { _frFunction :: API.V0.FunctionName, _frFunctionArguments :: [FunctionArgument], _frRelationships :: Set API.V0.Relationships, + _frRedactionExpressions :: Set API.V0.TargetRedactionExpressions, _frQuery :: Query } deriving stock (Eq, Ord, Show, Generic) @@ -217,6 +224,8 @@ instance HasObjectCodec FunctionRequest where .= _frFunctionArguments <*> requiredField "relationships" "The relationships between entities involved in the entire query request" .= _frRelationships + <*> optionalFieldWithOmittedDefault "redaction_expressions" mempty "Expressions that can be referenced by the query to redact fields/columns" + .= _frRedactionExpressions <*> requiredField "query" "The details of the query against the table" .= _frQuery @@ -313,7 +322,7 @@ arrayFieldObjectCodec = -- 3. an "object", which indicates that the field contains a nested object -- 4. an "array", which indicates that the field contains a nested array data Field - = ColumnField API.V0.ColumnName API.V0.ScalarType + = ColumnField API.V0.ColumnName API.V0.ScalarType (Maybe API.V0.RedactionExpressionName) | RelField RelationshipField | NestedObjField API.V0.ColumnName Query | NestedArrayField ArrayField @@ -327,11 +336,12 @@ instance HasCodec Field where discriminatedUnionCodec "type" enc dec where columnCodec = - (,) + (,,) <$> requiredField' "column" - .= fst + .= fst3 <*> requiredField' "column_type" - .= snd + .= snd3 + <*> optionalFieldOrNull "redaction_expression" "If present, the name of the redaction expression to evaluate. If the expression is false, the column value must be nulled out." .= thd3 nestedObjCodec = (,) <$> requiredField' "column" @@ -339,13 +349,13 @@ instance HasCodec Field where <*> requiredField' "query" .= snd enc = \case - ColumnField columnName scalarType -> ("column", mapToEncoder (columnName, scalarType) columnCodec) + ColumnField columnName scalarType redactionExpName -> ("column", mapToEncoder (columnName, scalarType, redactionExpName) columnCodec) RelField relField -> ("relationship", mapToEncoder relField relationshipFieldObjectCodec) NestedObjField columnName nestedObjQuery -> ("object", mapToEncoder (columnName, nestedObjQuery) nestedObjCodec) NestedArrayField arrayField -> ("array", mapToEncoder arrayField arrayFieldObjectCodec) dec = HashMap.fromList - [ ("column", ("ColumnField", mapToDecoder (uncurry ColumnField) columnCodec)), + [ ("column", ("ColumnField", mapToDecoder (uncurry3 ColumnField) columnCodec)), ("relationship", ("RelationshipField", mapToDecoder RelField relationshipFieldObjectCodec)), ("object", ("NestedObjField", mapToDecoder (uncurry NestedObjField) nestedObjCodec)), ("array", ("NestedArrayField", mapToDecoder NestedArrayField arrayFieldObjectCodec)) diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Target.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Target.hs new file mode 100644 index 00000000000..9b9aeee483e --- /dev/null +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Target.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE TemplateHaskell #-} + +module Hasura.Backends.DataConnector.API.V0.Target + ( TargetName (..), + ) +where + +import Autodocodec.Extended +import Autodocodec.OpenAPI () +import Control.DeepSeq (NFData) +import Control.Lens.TH (makePrisms) +import Data.Aeson (FromJSON, ToJSON) +import Data.Data (Data) +import Data.HashMap.Strict qualified as HashMap +import Data.Hashable (Hashable) +import Data.OpenApi (ToSchema) +import GHC.Generics (Generic) +import Hasura.Backends.DataConnector.API.V0.Function qualified as API.V0 +import Hasura.Backends.DataConnector.API.V0.Table qualified as API.V0 +import Prelude + +data TargetName + = TNTable API.V0.TableName + | TNFunction API.V0.FunctionName + deriving stock (Eq, Ord, Show, Generic, Data) + deriving anyclass (NFData, Hashable) + deriving (FromJSON, ToJSON, ToSchema) via Autodocodec TargetName + +instance HasCodec TargetName where + codec = object "TargetName" $ discriminatedUnionCodec "type" enc dec + where + tableKey = "table" + functionKey = "function" + tnTableObjectCodec = requiredField "table" "The name of the table to query" + tnFunctionObjectCodec = requiredField "function" "The name of the function" + enc = \case + (TNTable t) -> (tableKey, mapToEncoder t tnTableObjectCodec) + (TNFunction f) -> (functionKey, mapToEncoder f tnFunctionObjectCodec) + dec = + HashMap.fromList + [ (tableKey, ("TNTable", mapToDecoder TNTable tnTableObjectCodec)), + (functionKey, ("TNFunction", mapToDecoder TNFunction tnFunctionObjectCodec)) + ] + +$(makePrisms ''TargetName) diff --git a/server/lib/dc-api/test/Test/Data.hs b/server/lib/dc-api/test/Test/Data.hs index d4be3db6618..ed3912d0e82 100644 --- a/server/lib/dc-api/test/Test/Data.hs +++ b/server/lib/dc-api/test/Test/Data.hs @@ -517,8 +517,8 @@ mkTestData schemaResponse testConfig = _tdColumnInsertSchema = columnInsertSchema schemaResponse testConfig, _tdRowColumnOperatorValue = rowColumnOperatorValue schemaResponse testConfig, _tdFindColumnScalarType = \tableName name -> findColumnScalarType schemaResponse tableName (formatColumnName testConfig $ API.ColumnName name), - _tdQueryComparisonColumn = API.ComparisonColumn API.QueryTable . API.mkColumnSelector . formatColumnName testConfig . API.ColumnName, - _tdCurrentComparisonColumn = API.ComparisonColumn API.CurrentTable . API.mkColumnSelector . formatColumnName testConfig . API.ColumnName, + _tdQueryComparisonColumn = \name scalarType -> API.ComparisonColumn API.QueryTable (API.mkColumnSelector . formatColumnName testConfig $ API.ColumnName name) scalarType Nothing, + _tdCurrentComparisonColumn = \name scalarType -> API.ComparisonColumn API.CurrentTable (API.mkColumnSelector . formatColumnName testConfig $ API.ColumnName name) scalarType Nothing, _tdOrderByColumn = \targetPath name -> orderByColumn targetPath (formatColumnName testConfig $ API.ColumnName name) } where @@ -561,7 +561,7 @@ mkEdgeCasesTestData testConfig schemaResponse = _ectdColumnField = columnField schemaResponse testConfig, _ectdMkDefaultTableInsertSchema = mkDefaultTableInsertSchema schemaResponse testConfig edgeCasesSchemaTables, _ectdRowColumnOperatorValue = rowColumnOperatorValue schemaResponse testConfig, - _ectdCurrentComparisonColumn = API.ComparisonColumn API.CurrentTable . API.mkColumnSelector . formatColumnName testConfig . API.ColumnName + _ectdCurrentComparisonColumn = \name scalarType -> API.ComparisonColumn API.CurrentTable (API.mkColumnSelector . formatColumnName testConfig $ API.ColumnName name) scalarType Nothing } where tableExists :: API.TableName -> Bool @@ -636,7 +636,7 @@ formatColumnName TestConfig {..} = API.ColumnName . applyNameCasing _tcColumnNam columnField :: API.SchemaResponse -> TestConfig -> API.TableName -> Text -> API.Field columnField schemaResponse testConfig tableName columnName = - API.ColumnField columnName' scalarType + API.ColumnField columnName' scalarType Nothing where columnName' = formatColumnName testConfig $ API.ColumnName columnName scalarType = findColumnScalarType schemaResponse tableName columnName' @@ -706,7 +706,7 @@ emptyQuery :: API.Query emptyQuery = API.Query Nothing Nothing Nothing Nothing Nothing Nothing Nothing emptyMutationRequest :: API.MutationRequest -emptyMutationRequest = API.MutationRequest mempty mempty mempty +emptyMutationRequest = API.MutationRequest mempty mempty mempty mempty sortBy :: (Ixed m, Ord (IxValue m)) => Index m -> [m] -> [m] sortBy propName = sortOn (^? ix propName) @@ -784,7 +784,7 @@ scalarValueComparison value valueType = API.ScalarValueComparison $ API.ScalarVa orderByColumn :: [API.RelationshipName] -> API.ColumnName -> API.OrderDirection -> API.OrderByElement orderByColumn targetPath columnName orderDirection = - API.OrderByElement targetPath (API.OrderByColumn columnName) orderDirection + API.OrderByElement targetPath (API.OrderByColumn columnName Nothing) orderDirection insertAutoIncPk :: Text -> Integer -> [HashMap API.FieldName API.FieldValue] -> [HashMap API.FieldName API.FieldValue] insertAutoIncPk pkFieldName startingPkId rows = diff --git a/server/lib/dc-api/test/Test/Specs/ErrorSpec.hs b/server/lib/dc-api/test/Test/Specs/ErrorSpec.hs index 62364f86eec..5f280eda786 100644 --- a/server/lib/dc-api/test/Test/Specs/ErrorSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/ErrorSpec.hs @@ -27,6 +27,6 @@ spec TestData {..} = describe "Error Protocol" do (CustomBinaryComparisonOperator "FOOBAR") (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (Data.scalarValueComparison (Number 1) $ artistIdScalarType) - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing artistIdScalarType = _tdFindColumnScalarType _tdArtistsTableName "ArtistId" diff --git a/server/lib/dc-api/test/Test/Specs/ExplainSpec.hs b/server/lib/dc-api/test/Test/Specs/ExplainSpec.hs index c67a8f68971..de4d6dfa02c 100644 --- a/server/lib/dc-api/test/Test/Specs/ExplainSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/ExplainSpec.hs @@ -28,4 +28,4 @@ spec TestData {..} _ = do artistsQueryRequest = let fields = Data.mkFieldsMap [("ArtistId", _tdColumnField _tdArtistsTableName "ArtistId"), ("Name", _tdColumnField _tdArtistsTableName "Name")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing diff --git a/server/lib/dc-api/test/Test/Specs/MutationSpec/DeleteSpec.hs b/server/lib/dc-api/test/Test/Specs/MutationSpec/DeleteSpec.hs index 65b9149acf2..b6857d0b534 100644 --- a/server/lib/dc-api/test/Test/Specs/MutationSpec/DeleteSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/MutationSpec/DeleteSpec.hs @@ -453,7 +453,7 @@ spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Delete Mutati invoiceLinesQueryRequest :: QueryRequest invoiceLinesQueryRequest = let query = Data.emptyQuery & qFields ?~ invoiceLinesFields & qOrderBy ?~ OrderBy mempty (_tdOrderByColumn [] "InvoiceId" Ascending :| []) - in TableQueryRequest _tdInvoiceLinesTableName mempty query Nothing + in TableQueryRequest _tdInvoiceLinesTableName mempty mempty query Nothing invoiceIdScalarType = _tdFindColumnScalarType _tdInvoiceLinesTableName "InvoiceId" invoiceLineIdScalarType = _tdFindColumnScalarType _tdInvoiceLinesTableName "InvoiceLineId" diff --git a/server/lib/dc-api/test/Test/Specs/MutationSpec/InsertSpec.hs b/server/lib/dc-api/test/Test/Specs/MutationSpec/InsertSpec.hs index 5cb6cd18605..77e7da0c732 100644 --- a/server/lib/dc-api/test/Test/Specs/MutationSpec/InsertSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/MutationSpec/InsertSpec.hs @@ -717,7 +717,7 @@ spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Insert Mutati Data.emptyQuery & qFields ?~ mkFieldsFromExpectedData _tdArtistsTableName (expectedInsertedArtists artistsStartingId) & qWhere ?~ ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "ArtistId" artistIdScalarType) (J.Number . fromInteger <$> artistIds) artistIdScalarType - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing albumsQueryRequest :: [Integer] -> QueryRequest albumsQueryRequest albumIds = @@ -725,7 +725,7 @@ spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Insert Mutati Data.emptyQuery & qFields ?~ mkFieldsFromExpectedData _tdAlbumsTableName (expectedInsertedAcdcAlbums albumsStartingId) & qWhere ?~ ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "AlbumId" albumIdScalarType) (J.Number . fromInteger <$> albumIds) albumIdScalarType - in TableQueryRequest _tdAlbumsTableName mempty query Nothing + in TableQueryRequest _tdAlbumsTableName mempty mempty query Nothing employeesQueryRequest :: [Integer] -> QueryRequest employeesQueryRequest employeeIds = @@ -733,7 +733,7 @@ spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Insert Mutati Data.emptyQuery & qFields ?~ mkFieldsFromExpectedData _tdEmployeesTableName (expectedInsertedEmployees employeesStartingId) & qWhere ?~ ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "EmployeeId" albumIdScalarType) (J.Number . fromInteger <$> employeeIds) employeeIdScalarType - in TableQueryRequest _tdEmployeesTableName mempty query Nothing + in TableQueryRequest _tdEmployeesTableName mempty mempty query Nothing artistsInsertSchema :: TableInsertSchema artistsInsertSchema = _tdMkDefaultTableInsertSchema _tdArtistsTableName diff --git a/server/lib/dc-api/test/Test/Specs/MutationSpec/UpdateSpec.hs b/server/lib/dc-api/test/Test/Specs/MutationSpec/UpdateSpec.hs index 6ca15e2006f..b824af6e9e3 100644 --- a/server/lib/dc-api/test/Test/Specs/MutationSpec/UpdateSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/MutationSpec/UpdateSpec.hs @@ -719,7 +719,7 @@ spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Update Mutati artistsQueryRequest :: Expression -> QueryRequest artistsQueryRequest whereExp = let query = Data.emptyQuery & qFields ?~ artistsFields & qWhere ?~ whereExp - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing invoiceLinesFields :: HashMap FieldName Field invoiceLinesFields = @@ -734,7 +734,7 @@ spec TestData {..} edgeCasesTestData Capabilities {..} = describe "Update Mutati invoiceLinesQueryRequest :: Expression -> QueryRequest invoiceLinesQueryRequest whereExp = let query = Data.emptyQuery & qFields ?~ invoiceLinesFields & qWhere ?~ whereExp - in TableQueryRequest _tdInvoiceLinesTableName mempty query Nothing + in TableQueryRequest _tdInvoiceLinesTableName mempty mempty query Nothing incOperator :: UpdateColumnOperatorName incOperator = UpdateColumnOperatorName $ [G.name|inc|] 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 b484a5ad933..ea50b9963b5 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/AggregatesSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/AggregatesSpec.hs @@ -79,7 +79,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do describe "Column Count" $ do it "counts all rows with non-null columns" $ do - let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") False)] + let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") Nothing False)] let queryRequest = invoicesQueryRequest aggregates response <- queryGuarded queryRequest @@ -92,7 +92,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do it "can count all rows with non-null values in a column, after applying pagination and filtering" $ do 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 aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") Nothing False)] let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qAggregatesLimit ?~ aggregatesLimit >>> qWhere ?~ where') response <- queryGuarded queryRequest @@ -109,7 +109,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do Data.responseRows response `rowsShouldBe` [] it "can count all rows with distinct non-null values in a column" $ do - let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") True)] + let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") Nothing True)] let queryRequest = invoicesQueryRequest aggregates response <- queryGuarded queryRequest @@ -124,7 +124,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do 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 aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") Nothing True)] let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qAggregatesLimit ?~ aggregatesLimit >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy) response <- queryGuarded queryRequest @@ -143,7 +143,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do 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 aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") Nothing False)] let queryRequest = invoicesQueryRequest aggregates & qrQuery . qLimit ?~ limit response <- queryGuarded queryRequest @@ -241,7 +241,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do let aggregates = Data.mkFieldsMap [ ("count", StarCount), - ("distinctBillingStates", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") True), + ("distinctBillingStates", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") Nothing True), ("maxTotal", singleColumnAggregateMax (_tdColumnName "Total") invoiceTotalScalarType) ] let queryRequest = invoicesQueryRequest aggregates @@ -597,7 +597,7 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do artistOrderBy = OrderBy mempty $ _tdOrderByColumn [] "ArtistId" Ascending :| [] artistQuery = Data.emptyQuery & qFields ?~ artistFields & qOrderBy ?~ artistOrderBy artistsTableRelationships = Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships - in QRTable $ TableRequest _tdArtistsTableName (Set.fromList [API.RTable artistsTableRelationships]) artistQuery Nothing + in QRTable $ TableRequest _tdArtistsTableName (Set.fromList [API.RTable artistsTableRelationships]) mempty artistQuery Nothing -- This query is basically what would be generated by this complex HGE GraphQL query -- @ @@ -673,35 +673,36 @@ spec TestData {..} relationshipCapabilities = describe "Aggregate Queries" $ do API.RTable $ Data.onlyKeepRelationships [_tdInvoiceLinesRelationshipName, _tdMediaTypeRelationshipName] _tdTracksTableRelationships ] ) + mempty artistQuery Nothing artistsQueryRequest :: HashMap FieldName Aggregate -> QueryRequest artistsQueryRequest aggregates = let query = Data.emptyQuery & qAggregates ?~ aggregates - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing invoicesQueryRequest :: HashMap FieldName Aggregate -> QueryRequest invoicesQueryRequest aggregates = let query = Data.emptyQuery & qAggregates ?~ aggregates - in TableQueryRequest _tdInvoicesTableName mempty query Nothing + in TableQueryRequest _tdInvoicesTableName mempty mempty query Nothing albumsQueryRequest :: QueryRequest albumsQueryRequest = - TableQueryRequest _tdAlbumsTableName mempty Data.emptyQuery Nothing + TableQueryRequest _tdAlbumsTableName mempty mempty Data.emptyQuery Nothing aggregate :: (NonEmpty a -> Value) -> [a] -> Value aggregate aggFn values = maybe Null aggFn $ NonEmpty.nonEmpty values singleColumnAggregateMax :: ColumnName -> ScalarType -> Aggregate - singleColumnAggregateMax columnName resultType = SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|max|]) columnName resultType + singleColumnAggregateMax columnName resultType = SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|max|]) columnName Nothing resultType singleColumnAggregateMin :: ColumnName -> ScalarType -> Aggregate - singleColumnAggregateMin columnName resultType = SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|min|]) columnName resultType + singleColumnAggregateMin columnName resultType = SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|min|]) columnName Nothing resultType singleColumnAggregateSum :: ColumnName -> ScalarType -> Aggregate - singleColumnAggregateSum columnName resultType = SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|sum|]) columnName resultType + singleColumnAggregateSum columnName resultType = SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|sum|]) columnName Nothing resultType billingCityScalarType = _tdFindColumnScalarType _tdInvoicesTableName "BillingCity" billingCountryScalarType = _tdFindColumnScalarType _tdInvoicesTableName "BillingCountry" diff --git a/server/lib/dc-api/test/Test/Specs/QuerySpec/BasicSpec.hs b/server/lib/dc-api/test/Test/Specs/QuerySpec/BasicSpec.hs index 2dba479b167..a5af923899e 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/BasicSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/BasicSpec.hs @@ -74,10 +74,10 @@ spec TestData {..} = describe "Basic Queries" $ do artistsQueryRequest = let fields = Data.mkFieldsMap [("ArtistId", _tdColumnField _tdArtistsTableName "ArtistId"), ("Name", _tdColumnField _tdArtistsTableName "Name")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing albumsQueryRequest :: QueryRequest albumsQueryRequest = let fields = Data.mkFieldsMap [("AlbumId", _tdColumnField _tdAlbumsTableName "AlbumId"), ("ArtistId", _tdColumnField _tdAlbumsTableName "ArtistId"), ("Title", _tdColumnField _tdAlbumsTableName "Title")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdAlbumsTableName mempty query Nothing + in TableQueryRequest _tdAlbumsTableName mempty mempty query Nothing diff --git a/server/lib/dc-api/test/Test/Specs/QuerySpec/CustomOperatorsSpec.hs b/server/lib/dc-api/test/Test/Specs/QuerySpec/CustomOperatorsSpec.hs index 14d9b8f2b34..cf598ba68e0 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/CustomOperatorsSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/CustomOperatorsSpec.hs @@ -42,12 +42,12 @@ spec TestData {..} (ScalarTypesCapabilities scalarTypesCapabilities) = describe let queryRequest = let fields = Data.mkFieldsMap [(unColumnName columnName, _tdColumnField tableName (unColumnName columnName))] query' = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest tableName mempty query' Nothing + in TableQueryRequest tableName mempty mempty query' Nothing where' = ApplyBinaryComparisonOperator (CustomBinaryComparisonOperator (unName operatorName)) (_tdCurrentComparisonColumn (unColumnName columnName) columnType) - (AnotherColumnComparison $ ComparisonColumn CurrentTable (mkColumnSelector argColumnName) argType) + (AnotherColumnComparison $ ComparisonColumn CurrentTable (mkColumnSelector argColumnName) argType Nothing) query = queryRequest & qrQuery . qWhere ?~ where' diff --git a/server/lib/dc-api/test/Test/Specs/QuerySpec/FilteringSpec.hs b/server/lib/dc-api/test/Test/Specs/QuerySpec/FilteringSpec.hs index f740e926098..9c8899fccd1 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/FilteringSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/FilteringSpec.hs @@ -313,13 +313,13 @@ spec TestData {..} comparisonCapabilities = describe "Filtering in Queries" $ do artistsQueryRequest = let fields = Data.mkFieldsMap [("ArtistId", _tdColumnField _tdArtistsTableName "ArtistId"), ("Name", _tdColumnField _tdArtistsTableName "Name")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing albumsQueryRequest :: QueryRequest albumsQueryRequest = let fields = Data.mkFieldsMap [("AlbumId", _tdColumnField _tdAlbumsTableName "AlbumId"), ("ArtistId", _tdColumnField _tdAlbumsTableName "ArtistId"), ("Title", _tdColumnField _tdAlbumsTableName "Title")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdAlbumsTableName mempty query Nothing + in TableQueryRequest _tdAlbumsTableName mempty mempty query Nothing albumIdScalarType = _tdFindColumnScalarType _tdAlbumsTableName "AlbumId" albumTitleScalarType = _tdFindColumnScalarType _tdAlbumsTableName "Title" diff --git a/server/lib/dc-api/test/Test/Specs/QuerySpec/ForeachSpec.hs b/server/lib/dc-api/test/Test/Specs/QuerySpec/ForeachSpec.hs index d864a541a7c..ebffe6671f9 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/ForeachSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/ForeachSpec.hs @@ -261,13 +261,13 @@ spec TestData {..} Capabilities {..} = describe "Foreach Queries" $ do albumsQueryRequest = let fields = Data.mkFieldsMap [("AlbumId", _tdColumnField _tdAlbumsTableName "AlbumId"), ("ArtistId", _tdColumnField _tdAlbumsTableName "ArtistId"), ("Title", _tdColumnField _tdAlbumsTableName "Title")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdAlbumsTableName mempty query Nothing + in TableQueryRequest _tdAlbumsTableName mempty mempty query Nothing playlistTracksQueryRequest :: QueryRequest playlistTracksQueryRequest = let fields = Data.mkFieldsMap [("PlaylistId", _tdColumnField _tdPlaylistTracksTableName "PlaylistId"), ("TrackId", _tdColumnField _tdPlaylistTracksTableName "TrackId")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdPlaylistTracksTableName mempty query Nothing + in TableQueryRequest _tdPlaylistTracksTableName mempty mempty query Nothing mkForeachIds :: TableName -> [(Text, J.Value)] -> HashMap ColumnName ScalarValue mkForeachIds tableName = diff --git a/server/lib/dc-api/test/Test/Specs/QuerySpec/OrderBySpec.hs b/server/lib/dc-api/test/Test/Specs/QuerySpec/OrderBySpec.hs index 40a0b927948..8e4a2ec1524 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/OrderBySpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/OrderBySpec.hs @@ -409,13 +409,13 @@ spec TestData {..} Capabilities {..} = describe "Order By in Queries" $ do albumsQueryRequest :: QueryRequest albumsQueryRequest = - TableQueryRequest _tdAlbumsTableName mempty albumsQuery Nothing + TableQueryRequest _tdAlbumsTableName mempty mempty albumsQuery Nothing artistsQueryRequest :: QueryRequest artistsQueryRequest = let fields = Data.mkFieldsMap [("ArtistId", _tdColumnField _tdArtistsTableName "ArtistId"), ("Name", _tdColumnField _tdArtistsTableName "Name")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdArtistsTableName mempty query Nothing + in TableQueryRequest _tdArtistsTableName mempty mempty query Nothing tracksQuery :: Query tracksQuery = @@ -424,16 +424,16 @@ spec TestData {..} Capabilities {..} = describe "Order By in Queries" $ do tracksQueryRequest :: QueryRequest tracksQueryRequest = - TableQueryRequest _tdTracksTableName mempty tracksQuery Nothing + TableQueryRequest _tdTracksTableName mempty mempty tracksQuery Nothing invoicesQueryRequest :: QueryRequest invoicesQueryRequest = let fields = Data.mkFieldsMap [("InvoiceId", _tdColumnField _tdInvoicesTableName "InvoiceId"), ("BillingState", _tdColumnField _tdInvoicesTableName "BillingState")] query = Data.emptyQuery & qFields ?~ fields - in TableQueryRequest _tdInvoicesTableName mempty query Nothing + in TableQueryRequest _tdInvoicesTableName mempty mempty query Nothing orderBySingleColumnAggregateMax :: ColumnName -> ScalarType -> OrderByTarget - orderBySingleColumnAggregateMax columnName resultType = OrderBySingleColumnAggregate $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|max|]) columnName resultType + orderBySingleColumnAggregateMax columnName resultType = OrderBySingleColumnAggregate $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|max|]) columnName Nothing resultType albumTitleScalarType = _tdFindColumnScalarType _tdAlbumsTableName "Title" artistNameScalarType = _tdFindColumnScalarType _tdArtistsTableName "Name" diff --git a/server/lib/dc-api/test/Test/Specs/QuerySpec/RelationshipsSpec.hs b/server/lib/dc-api/test/Test/Specs/QuerySpec/RelationshipsSpec.hs index 9c5851ca22b..d6c952d17c6 100644 --- a/server/lib/dc-api/test/Test/Specs/QuerySpec/RelationshipsSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/QuerySpec/RelationshipsSpec.hs @@ -247,7 +247,7 @@ spec TestData {..} subqueryComparisonCapabilities = describe "Relationship Queri ("Artist", RelField $ RelationshipField _tdArtistRelationshipName artistsSubquery) ] query = albumsQuery & qFields ?~ fields - in TableQueryRequest _tdAlbumsTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships]) query Nothing + in TableQueryRequest _tdAlbumsTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships]) mempty query Nothing artistsWithAlbumsQuery :: (Query -> Query) -> QueryRequest artistsWithAlbumsQuery modifySubquery = @@ -261,7 +261,7 @@ spec TestData {..} subqueryComparisonCapabilities = describe "Relationship Queri ("Albums", RelField $ RelationshipField _tdAlbumsRelationshipName albumsSubquery) ] query = artistsQuery & qFields ?~ fields - in TableQueryRequest _tdArtistsTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships]) query Nothing + in TableQueryRequest _tdArtistsTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships]) mempty query Nothing employeesWithCustomersQuery :: (Query -> Query) -> QueryRequest employeesWithCustomersQuery modifySubquery = @@ -273,7 +273,7 @@ spec TestData {..} subqueryComparisonCapabilities = describe "Relationship Queri [ ("SupportRepForCustomers", RelField $ RelationshipField _tdSupportRepForCustomersRelationshipName customersSubquery) ] query = employeesQuery & qFields ?~ fields - in TableQueryRequest _tdEmployeesTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdSupportRepForCustomersRelationshipName] _tdEmployeesTableRelationships]) query Nothing + in TableQueryRequest _tdEmployeesTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdSupportRepForCustomersRelationshipName] _tdEmployeesTableRelationships]) mempty query Nothing customersWithSupportRepQuery :: (Query -> Query) -> QueryRequest customersWithSupportRepQuery modifySubquery = @@ -284,7 +284,7 @@ spec TestData {..} subqueryComparisonCapabilities = describe "Relationship Queri [ ("SupportRep", RelField $ RelationshipField _tdSupportRepRelationshipName supportRepSubquery) ] query = customersQuery & qFields ?~ fields - in TableQueryRequest _tdCustomersTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdSupportRepRelationshipName] _tdCustomersTableRelationships]) query Nothing + in TableQueryRequest _tdCustomersTableName (Set.fromList [API.RTable $ Data.onlyKeepRelationships [_tdSupportRepRelationshipName] _tdCustomersTableRelationships]) mempty query Nothing artistsQuery :: Query artistsQuery = diff --git a/server/lib/dc-api/test/Test/Specs/UDFSpec.hs b/server/lib/dc-api/test/Test/Specs/UDFSpec.hs index ffb45c82d52..a915154dc28 100644 --- a/server/lib/dc-api/test/Test/Specs/UDFSpec.hs +++ b/server/lib/dc-api/test/Test/Specs/UDFSpec.hs @@ -60,7 +60,7 @@ spec testConfig API.Capabilities {} = describe "supports functions" $ preloadAge k = "take" :: Text.Text v = API.ScalarValue (Number (fromIntegral fibonacciRows)) (API.ScalarType "number") args = [NamedArgument k (API.ScalarArgumentValue v)] - in QRFunction $ FunctionRequest _ftdFibonacciFunctionName args mempty query' + in FunctionQueryRequest _ftdFibonacciFunctionName args mempty mempty query' testData@FunctionsTestData {..} = mkFunctionsTestData preloadedSchema testConfig query = fibonacciRequest testData @@ -96,7 +96,7 @@ spec testConfig API.Capabilities {} = describe "supports functions" $ preloadAge relationships = Set.singleton (API.RFunction authorRelationship) v = API.ScalarValue (String "x") (API.ScalarType "string") args = [NamedArgument "query" (API.ScalarArgumentValue v)] - in QRFunction $ FunctionRequest _ftdSearchArticlesFunctionName args relationships query' + in FunctionQueryRequest _ftdSearchArticlesFunctionName args relationships mempty query' testData = mkFunctionsTestData preloadedSchema testConfig query = articlesRequest testData @@ -130,7 +130,7 @@ spec testConfig API.Capabilities {} = describe "supports functions" $ preloadAge whereClause = API.ApplyBinaryComparisonOperator API.LessThan - (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "id") (API.ScalarType "number")) + (API.ComparisonColumn API.CurrentTable (API.mkColumnSelector $ API.ColumnName "id") (API.ScalarType "number") Nothing) (API.ScalarValueComparison (API.ScalarValue (Number 10) (API.ScalarType "number"))) query' = Data.emptyQuery & qFields ?~ fields & qWhere ?~ whereClause & qLimit ?~ 2 authorRelationship = @@ -144,7 +144,7 @@ spec testConfig API.Capabilities {} = describe "supports functions" $ preloadAge relationships = Set.singleton (API.RFunction authorRelationship) v = API.ScalarValue (String "y") (API.ScalarType "string") args = [NamedArgument "query" (API.ScalarArgumentValue v)] - in QRFunction $ FunctionRequest _ftdSearchArticlesFunctionName args relationships query' + in FunctionQueryRequest _ftdSearchArticlesFunctionName args relationships mempty query' testData = mkFunctionsTestData preloadedSchema testConfig query = articlesRequest testData diff --git a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs index d588d8e2fe2..8077da70c1e 100644 --- a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs +++ b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs @@ -60,7 +60,8 @@ capabilities = API._cQueries = Just API.QueryCapabilities - { _qcForeach = Just API.ForeachCapabilities + { _qcForeach = Just API.ForeachCapabilities, + _qcRedaction = Just API.RedactionCapabilities }, API._cMutations = Just diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Execute.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Execute.hs index 3f320b5ac1f..daf836e2727 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Execute.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Execute.hs @@ -53,7 +53,7 @@ instance BackendExecute 'DataConnector where type ExecutionMonad 'DataConnector = AgentClientT mkDBQueryPlan UserInfo {..} sourceName sourceConfig ir _headers _gName = do - queryPlan@Plan {..} <- flip runReaderT sourceConfig $ Plan.mkQueryPlan _uiSession ir + queryPlan@Plan {..} <- flip runReaderT (API._cScalarTypes $ _scCapabilities sourceConfig, _uiSession) $ Plan.mkQueryPlan ir transformedSourceConfig <- transformSourceConfig sourceConfig (Just _uiSession) pure DBStepInfo @@ -65,7 +65,7 @@ instance BackendExecute 'DataConnector where } mkDBQueryExplain fieldName UserInfo {..} sourceName sourceConfig ir _headers _gName = do - queryPlan@Plan {..} <- flip runReaderT sourceConfig $ Plan.mkQueryPlan _uiSession ir + queryPlan@Plan {..} <- flip runReaderT (API._cScalarTypes $ _scCapabilities sourceConfig, _uiSession) $ Plan.mkQueryPlan ir transformedSourceConfig <- transformSourceConfig sourceConfig (Just _uiSession) pure $ mkAnyBackend @'DataConnector @@ -78,7 +78,7 @@ instance BackendExecute 'DataConnector where } mkDBMutationPlan _env _manager _logger UserInfo {..} _stringifyNum sourceName sourceConfig mutationDB _headers _gName _maybeSelSetArgs = do - mutationPlan@Plan {..} <- flip runReaderT sourceConfig $ Plan.mkMutationPlan _uiSession mutationDB + mutationPlan@Plan {..} <- flip runReaderT (API._cScalarTypes $ _scCapabilities sourceConfig, _uiSession) $ Plan.mkMutationPlan mutationDB transformedSourceConfig <- transformSourceConfig sourceConfig (Just _uiSession) pure DBStepInfo @@ -96,7 +96,7 @@ instance BackendExecute 'DataConnector where throw400 NotSupported "mkLiveQuerySubscriptionPlan: not implemented for the Data Connector backend." mkDBRemoteRelationshipPlan UserInfo {..} sourceName sourceConfig joinIds joinIdsSchema argumentIdFieldName (resultFieldName, ir) _ _ _ = do - remoteRelationshipPlan@Plan {..} <- flip runReaderT sourceConfig $ Plan.mkRemoteRelationshipPlan _uiSession sourceConfig joinIds joinIdsSchema argumentIdFieldName resultFieldName ir + remoteRelationshipPlan@Plan {..} <- flip runReaderT (API._cScalarTypes $ _scCapabilities sourceConfig, _uiSession) $ Plan.mkRemoteRelationshipPlan sourceConfig joinIds joinIdsSchema argumentIdFieldName resultFieldName ir transformedSourceConfig <- transformSourceConfig sourceConfig (Just _uiSession) pure DBStepInfo diff --git a/server/src-lib/Hasura/Backends/DataConnector/Plan/Common.hs b/server/src-lib/Hasura/Backends/DataConnector/Plan/Common.hs index dc9983a39da..6c288d46ebd 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Plan/Common.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Plan/Common.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} -- | This module contains Data Connector request/response planning code and utility @@ -11,15 +10,19 @@ -- for example 'Hasura.Backends.DataConnector.Plan.QueryPlan.mkQueryPlan`. module Hasura.Backends.DataConnector.Plan.Common ( Plan (..), + writeOutput, + replaceOutput, TableRelationships (..), - TableRelationshipsKey (..), + recordTableRelationship, + recordTableRelationshipFromRelInfo, FieldPrefix, noPrefix, prefixWith, applyPrefix, Cardinality (..), - recordTableRelationship, - recordTableRelationshipFromRelInfo, + RedactionExpressionState (..), + recordRedactionExpression, + translateRedactionExpressions, prepareLiteral, translateBoolExpToExpression, mkRelationshipName, @@ -28,7 +31,6 @@ module Hasura.Backends.DataConnector.Plan.Common ) where -import Control.Monad.Trans.Writer.CPS qualified as CPS import Data.Aeson qualified as J import Data.Aeson.Encoding qualified as JE import Data.Aeson.Types qualified as J @@ -42,6 +44,7 @@ import Data.Set qualified as Set import Data.Text qualified as T import Data.Text.Encoding qualified as TE import Data.Text.Extended (toTxt, (<<>), (<>>)) +import Data.Tuple (swap) import Hasura.Backends.DataConnector.API qualified as API import Hasura.Backends.DataConnector.Adapter.Backend import Hasura.Backends.DataConnector.Adapter.Types @@ -70,18 +73,30 @@ data Plan request response = Plan -------------------------------------------------------------------------------- --- | Key datatype for TableRelationships to avoid having an Either directly as the key, --- and make extending the types of relationships easier in future. -data TableRelationshipsKey - = FunctionNameKey API.FunctionName - | TableNameKey API.TableName - deriving stock (Eq, Show, Generic) - deriving anyclass (Hashable) +-- | Writes some output to state, like one might do if one was using a Writer monad. +-- The output is combined with the existing output using '<>' from 'Semigroup' +writeOutput :: (Semigroup output, MonadState state m, Has output state) => output -> m () +writeOutput x = modify $ modifier (<> x) + +-- | Replaces some output in the state with a new version of the output. Also, a value +-- can be returned from the replacement function. +-- +-- This is useful if you need to inspect the existing state, make a decision, and update it +-- based on that decision. The result of the decision can be returned from the transformation +-- as your 'a' value. +replaceOutput :: (MonadState state m, Has output state) => (output -> (output, a)) -> m a +replaceOutput replace = do + output <- gets getter + let (newOutput, retval) = replace output + modify (modifier (const newOutput)) + pure retval + +-------------------------------------------------------------------------------- -- | A monoidal data structure used to record Table Relationships encountered during request -- translation. Used with 'recordTableRelationship'. newtype TableRelationships = TableRelationships - {unTableRelationships :: HashMap TableRelationshipsKey (HashMap API.RelationshipName API.Relationship)} + {unTableRelationships :: HashMap API.TargetName (HashMap API.RelationshipName API.Relationship)} deriving stock (Eq, Show) instance Semigroup TableRelationships where @@ -93,26 +108,23 @@ instance Monoid TableRelationships where -- | Records a table relationship encountered during request translation into the output of the current -- 'CPS.WriterT' recordTableRelationship :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, - MonadError QErr m + ( MonadState state m, + Has TableRelationships state ) => - TableRelationshipsKey -> + API.TargetName -> API.RelationshipName -> API.Relationship -> - CPS.WriterT writerOutput m () + m () recordTableRelationship sourceName relationshipName relationship = - let newRelationship = TableRelationships $ HashMap.singleton sourceName (HashMap.singleton relationshipName relationship) - in CPS.tell $ modifier (const newRelationship) mempty + writeOutput $ TableRelationships $ HashMap.singleton sourceName (HashMap.singleton relationshipName relationship) recordTableRelationshipFromRelInfo :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, - MonadError QErr m + ( MonadState state m, + Has TableRelationships state ) => - TableRelationshipsKey -> + API.TargetName -> RelInfo 'DataConnector -> - CPS.WriterT writerOutput m (API.RelationshipName, API.Relationship) + m (API.RelationshipName, API.Relationship) recordTableRelationshipFromRelInfo sourceTableName RelInfo {..} = do let relationshipName = mkRelationshipName riName let relationshipType = case riType of @@ -135,6 +147,55 @@ recordTableRelationshipFromRelInfo sourceTableName RelInfo {..} = do -------------------------------------------------------------------------------- +-- | Collects encountered redaction expressions on a per table/function basis. +-- Expressions are deduplicated and assigned a unique name (within that table/function) +-- that is then used to reference the expression inside the query. +newtype RedactionExpressionState = RedactionExpressionState + {unRedactionExpressionState :: HashMap API.TargetName (HashMap API.RedactionExpression API.RedactionExpressionName)} + deriving stock (Eq, Show) + +recordRedactionExpression :: + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => + API.TargetName -> + AnnRedactionExp 'DataConnector (UnpreparedValue 'DataConnector) -> + m (Maybe API.RedactionExpressionName) +recordRedactionExpression target = \case + NoRedaction -> pure Nothing + RedactIfFalse boolExp -> runMaybeT $ do + expression <- MaybeT $ fmap API.RedactionExpression <$> translateBoolExpToExpression target boolExp + replaceOutput $ \existingState@(RedactionExpressionState recordedExps) -> + let targetRecordedExps = fromMaybe mempty $ HashMap.lookup target recordedExps + in case HashMap.lookup expression targetRecordedExps of + Just existingName -> (existingState, existingName) + Nothing -> + -- A unique name is generated by counting up from zero as redaction expressions are added + -- by using the size of the HashMap they are placed into + let newName = API.RedactionExpressionName $ "RedactionExp" <> tshow (HashMap.size targetRecordedExps) + newTargetRecordedExps = HashMap.insert expression newName targetRecordedExps + newState = RedactionExpressionState $ HashMap.insert target newTargetRecordedExps recordedExps + in (newState, newName) + +translateRedactionExpressions :: RedactionExpressionState -> Set API.TargetRedactionExpressions +translateRedactionExpressions (RedactionExpressionState redactionsByTarget) = + redactionsByTarget + & HashMap.toList + <&> ( \(targetKey, redactionExps) -> + API.TargetRedactionExpressions + { _treTarget = targetKey, + _treExpressions = HashMap.fromList $ swap <$> HashMap.toList redactionExps + } + ) + & Set.fromList + +-------------------------------------------------------------------------------- + -- | Represents a potential prefix that can be applied to a field name, useful for -- namespacing field names that may be otherwise duplicated. newtype FieldPrefix = FieldPrefix (Maybe FieldName) @@ -166,15 +227,19 @@ data Cardinality -------------------------------------------------------------------------------- prepareLiteral :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => UnpreparedValue 'DataConnector -> m Literal -prepareLiteral sessionVariables = \case +prepareLiteral = \case UVLiteral literal -> pure $ literal UVParameter _ e -> pure (ValueLiteral (columnTypeToScalarType $ cvType e) (cvValue e)) UVSession -> throw400 NotSupported "prepareLiteral: UVSession" UVSessionVar sessionVarType sessionVar -> do + sessionVariables <- asks getter textValue <- getSessionVariableValue sessionVar sessionVariables `onNothing` throw400 NotSupported ("prepareLiteral: session var not found: " <>> sessionVar) @@ -237,54 +302,54 @@ toColumnSelector (ColumnStack stack) columnName = -------------------------------------------------------------------------------- translateBoolExpToExpression :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> AnnBoolExp 'DataConnector (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m (Maybe API.Expression) -translateBoolExpToExpression sessionVariables sourceName boolExp = do - removeAlwaysTrueExpression <$> translateBoolExp sessionVariables sourceName emptyColumnStack boolExp + m (Maybe API.Expression) +translateBoolExpToExpression sourceName boolExp = do + removeAlwaysTrueExpression <$> translateBoolExp sourceName emptyColumnStack boolExp translateBoolExp :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> ColumnStack -> AnnBoolExp 'DataConnector (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m API.Expression -translateBoolExp sessionVariables sourceName columnStack = \case + m API.Expression +translateBoolExp sourceName columnStack = \case BoolAnd xs -> - mkIfZeroOrMany API.And . mapMaybe removeAlwaysTrueExpression <$> traverse (translateBoolExp' sourceName columnStack) xs + mkIfZeroOrMany API.And . mapMaybe removeAlwaysTrueExpression <$> traverse (translateBoolExp sourceName columnStack) xs BoolOr xs -> - mkIfZeroOrMany API.Or . mapMaybe removeAlwaysFalseExpression <$> traverse (translateBoolExp' sourceName columnStack) xs + mkIfZeroOrMany API.Or . mapMaybe removeAlwaysFalseExpression <$> traverse (translateBoolExp sourceName columnStack) xs BoolNot x -> - API.Not <$> (translateBoolExp' sourceName columnStack) x - BoolField (AVColumn c _redactionExp opExps) -> do - -- TODO(redactionExp): Deal with the redaction expression + API.Not <$> (translateBoolExp sourceName columnStack) x + BoolField (AVColumn c redactionExp opExps) -> do let columnSelector = toColumnSelector columnStack $ ciColumn c - lift $ mkIfZeroOrMany API.And <$> traverse (translateOp sessionVariables columnSelector (Witch.from . columnTypeToScalarType $ ciType c)) opExps + redactionExpName <- recordRedactionExpression sourceName redactionExp + mkIfZeroOrMany API.And <$> traverse (translateOp columnSelector (Witch.from . columnTypeToScalarType $ ciType c) redactionExpName) opExps BoolField (AVNestedObject NestedObjectInfo {..} nestedExp) -> - translateBoolExp' sourceName (pushColumn columnStack _noiColumn) nestedExp + translateBoolExp sourceName (pushColumn columnStack _noiColumn) nestedExp BoolField (AVRelationship relationshipInfo (RelationshipFilters {rfTargetTablePermissions, rfFilter})) -> do (relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceName relationshipInfo -- TODO: How does this function keep track of the root table? - API.Exists (API.RelatedTable relationshipName) <$> translateBoolExp' (TableNameKey _rTargetTable) emptyColumnStack (BoolAnd [rfTargetTablePermissions, rfFilter]) + API.Exists (API.RelatedTable relationshipName) <$> translateBoolExp (API.TNTable _rTargetTable) emptyColumnStack (BoolAnd [rfTargetTablePermissions, rfFilter]) BoolExists GExists {..} -> let tableName = Witch.from _geTable - in API.Exists (API.UnrelatedTable tableName) <$> translateBoolExp' (TableNameKey tableName) emptyColumnStack _geWhere + in API.Exists (API.UnrelatedTable tableName) <$> translateBoolExp (API.TNTable tableName) emptyColumnStack _geWhere where - translateBoolExp' = translateBoolExp sessionVariables - -- Makes an 'API.Expression' like 'API.And' if there is zero or many input expressions otherwise -- just returns the singleton expression. This helps remove redundant 'API.And' etcs from the expression. mkIfZeroOrMany :: (Set API.Expression -> API.Expression) -> [API.Expression] -> API.Expression @@ -305,14 +370,18 @@ removeAlwaysFalseExpression = \case other -> Just other translateOp :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => API.ColumnSelector -> API.ScalarType -> + Maybe API.RedactionExpressionName -> OpExpG 'DataConnector (UnpreparedValue 'DataConnector) -> m API.Expression -translateOp sessionVariables columnName columnType opExp = do - preparedOpExp <- traverse (prepareLiteral sessionVariables) $ opExp +translateOp columnName columnType redactionExpName opExp = do + preparedOpExp <- traverse prepareLiteral $ opExp case preparedOpExp of AEQ _ (ValueLiteral scalarType value) -> pure $ mkApplyBinaryComparisonOperatorToScalar API.Equal value scalarType @@ -370,7 +439,7 @@ translateOp sessionVariables columnName columnType opExp = do pure $ API.ApplyBinaryArrayComparisonOperator (API.CustomBinaryArrayComparisonOperator _cboName) currentComparisonColumn array (Witch.from scalarType) where currentComparisonColumn :: API.ComparisonColumn - currentComparisonColumn = API.ComparisonColumn API.CurrentTable columnName columnType + currentComparisonColumn = API.ComparisonColumn API.CurrentTable columnName columnType redactionExpName mkApplyBinaryComparisonOperatorToAnotherColumn :: API.BinaryComparisonOperator -> RootOrCurrentColumn 'DataConnector -> API.Expression mkApplyBinaryComparisonOperatorToAnotherColumn operator (RootOrCurrentColumn rootOrCurrent otherColumnName) = @@ -379,7 +448,7 @@ translateOp sessionVariables columnName columnType opExp = do IsCurrent -> API.CurrentTable otherColumnSelector = API.mkColumnSelector $ Witch.from otherColumnName in -- TODO(dmoverton): allow otherColumnName to refer to nested object fields. - API.ApplyBinaryComparisonOperator operator currentComparisonColumn (API.AnotherColumnComparison $ API.ComparisonColumn columnPath otherColumnSelector columnType) + API.ApplyBinaryComparisonOperator operator currentComparisonColumn (API.AnotherColumnComparison $ API.ComparisonColumn columnPath otherColumnSelector columnType Nothing) inOperator :: Literal -> API.Expression inOperator literal = diff --git a/server/src-lib/Hasura/Backends/DataConnector/Plan/MutationPlan.hs b/server/src-lib/Hasura/Backends/DataConnector/Plan/MutationPlan.hs index 0a8be75b9b6..29d9602482c 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Plan/MutationPlan.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Plan/MutationPlan.hs @@ -3,10 +3,9 @@ module Hasura.Backends.DataConnector.Plan.MutationPlan ) where -import Control.Monad.Trans.Writer.CPS qualified as CPS import Data.Aeson qualified as J import Data.Aeson.Encoding qualified as JE -import Data.Has (Has, modifier) +import Data.Has (Has) import Data.HashMap.Strict qualified as HashMap import Data.Semigroup.Foldable (toNonEmpty) import Data.Set (Set) @@ -62,36 +61,41 @@ instance Semigroup TableInsertSchema where } recordTableInsertSchema :: - ( Has TableInsertSchemas writerOutput, - Monoid writerOutput, - MonadError QErr m + ( Has TableInsertSchemas state, + MonadState state m ) => API.TableName -> TableInsertSchema -> - CPS.WriterT writerOutput m () + m () recordTableInsertSchema tableName tableInsertSchema = - let newTableSchema = TableInsertSchemas $ HashMap.singleton tableName tableInsertSchema - in CPS.tell $ modifier (const newTableSchema) mempty + writeOutput . TableInsertSchemas $ HashMap.singleton tableName tableInsertSchema -------------------------------------------------------------------------------- mkMutationPlan :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => MutationDB 'DataConnector Void (UnpreparedValue 'DataConnector) -> m (Plan API.MutationRequest API.MutationResponse) -mkMutationPlan sessionVariables mutationDB = do - request <- translateMutationDB sessionVariables mutationDB +mkMutationPlan mutationDB = do + request <- translateMutationDB mutationDB pure $ Plan request (reshapeResponseToMutationGqlShape mutationDB) translateMutationDB :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => MutationDB 'DataConnector Void (UnpreparedValue 'DataConnector) -> m API.MutationRequest -translateMutationDB sessionVariables = \case +translateMutationDB = \case MDBInsert insert -> do - (insertOperation, (tableRelationships, tableInsertSchemas)) <- CPS.runWriterT $ translateInsert sessionVariables insert + (insertOperation, (tableRelationships, redactionExpressionState, tableInsertSchemas)) <- + flip runStateT (mempty, RedactionExpressionState mempty, mempty) $ translateInsert insert let apiTableInsertSchema = unTableInsertSchemas tableInsertSchemas & HashMap.toList @@ -100,11 +104,13 @@ translateMutationDB sessionVariables = \case pure $ API.MutationRequest { _mrTableRelationships = apiTableRelationships, + _mrRedactionExpressions = translateRedactionExpressions redactionExpressionState, _mrInsertSchema = Set.fromList apiTableInsertSchema, _mrOperations = [API.InsertOperation insertOperation] } MDBUpdate update -> do - (updateOperations, tableRelationships) <- CPS.runWriterT $ translateUpdate sessionVariables update + (updateOperations, (tableRelationships, redactionExpressionState)) <- + flip runStateT (mempty, RedactionExpressionState mempty) $ translateUpdate update let apiTableRelationships = Set.fromList $ uncurry API.TableRelationships @@ -112,11 +118,13 @@ translateMutationDB sessionVariables = \case pure $ API.MutationRequest { _mrTableRelationships = apiTableRelationships, + _mrRedactionExpressions = translateRedactionExpressions redactionExpressionState, _mrInsertSchema = mempty, _mrOperations = API.UpdateOperation <$> updateOperations } MDBDelete delete -> do - (deleteOperation, tableRelationships) <- CPS.runWriterT $ translateDelete sessionVariables delete + (deleteOperation, (tableRelationships, redactionExpressionState)) <- + flip runStateT (mempty, RedactionExpressionState mempty) $ translateDelete delete let apiTableRelationships = Set.fromList $ uncurry API.TableRelationships @@ -124,26 +132,34 @@ translateMutationDB sessionVariables = \case pure $ API.MutationRequest { _mrTableRelationships = apiTableRelationships, + _mrRedactionExpressions = translateRedactionExpressions redactionExpressionState, _mrInsertSchema = mempty, _mrOperations = [API.DeleteOperation deleteOperation] } MDBFunction _returnsSet _select -> throw400 NotSupported "translateMutationDB: function mutations not implemented for the Data Connector backend." -eitherKey :: (TableRelationshipsKey, c) -> Either (API.FunctionName, c) (API.TableName, c) -eitherKey (FunctionNameKey f, x) = Left (f, x) -eitherKey (TableNameKey t, x) = Right (t, x) +eitherKey :: (API.TargetName, c) -> Either (API.FunctionName, c) (API.TableName, c) +eitherKey (API.TNFunction f, x) = Left (f, x) +eitherKey (API.TNTable t, x) = Right (t, x) translateInsert :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + Has TableInsertSchemas state, + MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => AnnotatedInsert 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT (TableRelationships, TableInsertSchemas) m API.InsertMutationOperation -translateInsert sessionVariables AnnotatedInsert {_aiData = AnnotatedInsertData {..}, ..} = do + m API.InsertMutationOperation +translateInsert AnnotatedInsert {_aiData = AnnotatedInsertData {..}, ..} = do captureTableInsertSchema tableName _aiTableColumns _aiPrimaryKey _aiExtraTableMetadata - rows <- lift $ traverse (translateInsertRow sessionVariables tableName _aiTableColumns _aiPresetValues) _aiInsertObject - postInsertCheck <- translateBoolExpToExpression sessionVariables (TableNameKey tableName) insertCheckCondition - returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _aiOutput + rows <- traverse (translateInsertRow tableName _aiTableColumns _aiPresetValues) _aiInsertObject + postInsertCheck <- translateBoolExpToExpression (API.TNTable tableName) insertCheckCondition + returningFields <- translateMutationOutputToReturningFields tableName _aiOutput pure $ API.InsertMutationOperation { API._imoTable = tableName, @@ -157,15 +173,14 @@ translateInsert sessionVariables AnnotatedInsert {_aiData = AnnotatedInsertData (insertCheckCondition, _updateCheckCondition) = _aiCheckCondition captureTableInsertSchema :: - ( Has TableInsertSchemas writerOutput, - Monoid writerOutput, - MonadError QErr m + ( MonadState state m, + Has TableInsertSchemas state ) => API.TableName -> [ColumnInfo 'DataConnector] -> Maybe (NESeq ColumnName) -> ExtraTableMetadata -> - CPS.WriterT writerOutput m () + m () captureTableInsertSchema tableName tableColumns primaryKey ExtraTableMetadata {..} = do let fieldSchemas = tableColumns @@ -183,20 +198,23 @@ captureTableInsertSchema tableName tableColumns primaryKey ExtraTableMetadata {. recordTableInsertSchema tableName $ TableInsertSchema primaryKey' fieldSchemas translateInsertRow :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => API.TableName -> [ColumnInfo 'DataConnector] -> HashMap ColumnName (UnpreparedValue 'DataConnector) -> AnnotatedInsertRow 'DataConnector (UnpreparedValue 'DataConnector) -> m API.RowObject -translateInsertRow sessionVariables tableName tableColumns defaultColumnValues insertRow = do +translateInsertRow tableName tableColumns defaultColumnValues insertRow = do columnSchemasAndValues <- forM (HashMap.toList columnUnpreparedValues) $ \(columnName, columnValue) -> do fieldName <- case find (\ColumnInfo {..} -> ciColumn == columnName) tableColumns of Just ColumnInfo {..} -> pure . API.FieldName $ G.unName ciName Nothing -> throw500 $ "Can't find column " <> toTxt columnName <> " in table schema for " <> API.tableNameToText tableName - preparedLiteral <- prepareLiteral sessionVariables columnValue + preparedLiteral <- prepareLiteral columnValue value <- case preparedLiteral of @@ -223,26 +241,38 @@ translateInsertRow sessionVariables tableName tableColumns defaultColumnValues i & HashMap.fromList translateUpdate :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => AnnotatedUpdateG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT TableRelationships m [API.UpdateMutationOperation] -translateUpdate sessionVariables annUpdate@AnnotatedUpdateG {..} = do + m [API.UpdateMutationOperation] +translateUpdate annUpdate@AnnotatedUpdateG {..} = do case _auUpdateVariant of - SingleBatch batch -> (: []) <$> translateUpdateBatch sessionVariables annUpdate batch - MultipleBatches batches -> traverse (translateUpdateBatch sessionVariables annUpdate) batches + SingleBatch batch -> (: []) <$> translateUpdateBatch annUpdate batch + MultipleBatches batches -> traverse (translateUpdateBatch annUpdate) batches translateUpdateBatch :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => AnnotatedUpdateG 'DataConnector Void (UnpreparedValue 'DataConnector) -> UpdateBatch 'DataConnector UpdateOperator (UnpreparedValue 'DataConnector) -> - CPS.WriterT TableRelationships m API.UpdateMutationOperation -translateUpdateBatch sessionVariables AnnotatedUpdateG {..} UpdateBatch {..} = do - updates <- lift $ translateUpdateOperations sessionVariables _ubOperations - whereExp <- translateBoolExpToExpression sessionVariables (TableNameKey tableName) (BoolAnd [_auUpdatePermissions, _ubWhere]) - postUpdateCheck <- translateBoolExpToExpression sessionVariables (TableNameKey tableName) _auCheck - returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _auOutput + m API.UpdateMutationOperation +translateUpdateBatch AnnotatedUpdateG {..} UpdateBatch {..} = do + updates <- translateUpdateOperations _ubOperations + whereExp <- translateBoolExpToExpression (API.TNTable tableName) (BoolAnd [_auUpdatePermissions, _ubWhere]) + postUpdateCheck <- translateBoolExpToExpression (API.TNTable tableName) _auCheck + returningFields <- translateMutationOutputToReturningFields tableName _auOutput pure $ API.UpdateMutationOperation @@ -257,11 +287,14 @@ translateUpdateBatch sessionVariables AnnotatedUpdateG {..} UpdateBatch {..} = d translateUpdateOperations :: forall m r. - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => HashMap ColumnName (UpdateOperator (UnpreparedValue 'DataConnector)) -> m (Set API.RowUpdate) -translateUpdateOperations sessionVariables columnUpdates = +translateUpdateOperations columnUpdates = fmap Set.fromList . forM (HashMap.toList columnUpdates) $ \(columnName, updateOperator) -> do let (mkRowUpdate, value) = case updateOperator of @@ -273,19 +306,25 @@ translateUpdateOperations sessionVariables columnUpdates = where prepareAndExtractLiteralValue :: UnpreparedValue 'DataConnector -> m (ScalarType, J.Value) prepareAndExtractLiteralValue unpreparedValue = do - preparedLiteral <- prepareLiteral sessionVariables unpreparedValue + preparedLiteral <- prepareLiteral unpreparedValue case preparedLiteral of ValueLiteral scalarType value -> pure (scalarType, value) ArrayLiteral _scalarType _values -> throw400 NotSupported "translateUpdateOperations: Array literals are not supported as column update values" translateDelete :: - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => AnnDelG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT TableRelationships m API.DeleteMutationOperation -translateDelete sessionVariables AnnDel {..} = do - whereExp <- translateBoolExpToExpression sessionVariables (TableNameKey tableName) (BoolAnd [permissionFilter, whereClause]) - returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _adOutput + m API.DeleteMutationOperation +translateDelete AnnDel {..} = do + whereExp <- translateBoolExpToExpression (API.TNTable tableName) (BoolAnd [permissionFilter, whereClause]) + returningFields <- translateMutationOutputToReturningFields tableName _adOutput pure $ API.DeleteMutationOperation { API._dmoTable = tableName, @@ -297,35 +336,37 @@ translateDelete sessionVariables AnnDel {..} = do (permissionFilter, whereClause) = _adWhere translateMutationOutputToReturningFields :: - ( MonadError QErr m, - Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> API.TableName -> MutationOutputG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m (HashMap FieldName API.Field) -translateMutationOutputToReturningFields sessionVariables tableName = \case + m (HashMap FieldName API.Field) +translateMutationOutputToReturningFields tableName = \case MOutSinglerowObject annFields -> - translateAnnFields sessionVariables noPrefix (TableNameKey tableName) annFields + translateAnnFields noPrefix (API.TNTable tableName) annFields MOutMultirowFields mutFields -> - HashMap.unions <$> traverse (uncurry $ translateMutField sessionVariables tableName) mutFields + HashMap.unions <$> traverse (uncurry $ translateMutField tableName) mutFields translateMutField :: - ( MonadError QErr m, - Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> API.TableName -> FieldName -> MutFldG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m (HashMap FieldName API.Field) -translateMutField sessionVariables tableName fieldName = \case + m (HashMap FieldName API.Field) +translateMutField tableName fieldName = \case MCount -> -- All mutation operations in a request return their affected rows count. -- The count can just be added to the response JSON during agent response reshaping @@ -336,7 +377,7 @@ translateMutField sessionVariables tableName fieldName = \case -- to us pure mempty MRet annFields -> - translateAnnFields sessionVariables (prefixWith fieldName) (TableNameKey tableName) annFields + translateAnnFields (prefixWith fieldName) (API.TNTable tableName) annFields -------------------------------------------------------------------------------- diff --git a/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs b/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs index 006c3590de7..116b0ac467f 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs @@ -13,11 +13,10 @@ where -------------------------------------------------------------------------------- -import Control.Monad.Trans.Writer.CPS qualified as CPS import Data.Aeson qualified as J import Data.Aeson.Encoding qualified as JE import Data.Bifunctor (Bifunctor (bimap)) -import Data.Has (Has) +import Data.Has import Data.HashMap.Strict qualified as HashMap import Data.List.NonEmpty qualified as NE import Data.Semigroup (Min (..)) @@ -63,11 +62,14 @@ instance Monoid FieldsAndAggregates where -- | Map a 'QueryDB 'DataConnector' term into a 'Plan' mkQueryPlan :: forall m r. - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) -> m (Plan API.QueryRequest API.QueryResponse) -mkQueryPlan sessionVariables ir = do +mkQueryPlan ir = do queryRequest <- translateQueryDB ir pure $ Plan queryRequest (reshapeResponseToQueryShape ir) where @@ -76,78 +78,103 @@ mkQueryPlan sessionVariables ir = do m API.QueryRequest translateQueryDB = \case - QDBMultipleRows simpleSelect -> translateAnnSimpleSelectToQueryRequest sessionVariables simpleSelect - QDBSingleRow simpleSelect -> translateAnnSimpleSelectToQueryRequest sessionVariables simpleSelect - QDBAggregation aggregateSelect -> translateAnnAggregateSelectToQueryRequest sessionVariables aggregateSelect + QDBMultipleRows simpleSelect -> translateAnnSimpleSelectToQueryRequest simpleSelect + QDBSingleRow simpleSelect -> translateAnnSimpleSelectToQueryRequest simpleSelect + QDBAggregation aggregateSelect -> translateAnnAggregateSelectToQueryRequest aggregateSelect translateAnnSimpleSelectToQueryRequest :: forall m r. - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => AnnSimpleSelectG 'DataConnector Void (UnpreparedValue 'DataConnector) -> m API.QueryRequest -translateAnnSimpleSelectToQueryRequest sessionVariables simpleSelect = - translateAnnSelectToQueryRequest sessionVariables (translateAnnFieldsWithNoAggregates sessionVariables noPrefix) simpleSelect +translateAnnSimpleSelectToQueryRequest simpleSelect = + translateAnnSelectToQueryRequest (translateAnnFieldsWithNoAggregates noPrefix) simpleSelect translateAnnAggregateSelectToQueryRequest :: forall m r. - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => AnnAggregateSelectG 'DataConnector Void (UnpreparedValue 'DataConnector) -> m API.QueryRequest -translateAnnAggregateSelectToQueryRequest sessionVariables aggregateSelect = - translateAnnSelectToQueryRequest sessionVariables (translateTableAggregateFields sessionVariables) aggregateSelect +translateAnnAggregateSelectToQueryRequest aggregateSelect = + translateAnnSelectToQueryRequest translateTableAggregateFields aggregateSelect translateAnnSelectToQueryRequest :: forall m r fieldType. - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> - (TableRelationshipsKey -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT TableRelationships m FieldsAndAggregates) -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => + ( forall state m2. + ( MonadState state m2, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m2, + MonadReader r m2, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => + API.TargetName -> + Fields (fieldType (UnpreparedValue 'DataConnector)) -> + m2 FieldsAndAggregates + ) -> AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) -> m API.QueryRequest -translateAnnSelectToQueryRequest sessionVariables translateFieldsAndAggregates selectG = do +translateAnnSelectToQueryRequest translateFieldsAndAggregates selectG = do case _asnFrom selectG of FromIdentifier _ -> throw400 NotSupported "AnnSelectG: FromIdentifier not supported" FromNativeQuery {} -> throw400 NotSupported "AnnSelectG: FromNativeQuery not supported" FromStoredProcedure {} -> throw400 NotSupported "AnnSelectG: FromStoredProcedure not supported" FromTable tableName -> do - (query, TableRelationships tableRelationships) <- - CPS.runWriterT (translateAnnSelect sessionVariables translateFieldsAndAggregates (TableNameKey (Witch.into tableName)) selectG) + (query, (TableRelationships tableRelationships, redactionExpressionState)) <- + flip runStateT (mempty, RedactionExpressionState mempty) $ translateAnnSelect translateFieldsAndAggregates (API.TNTable (Witch.into tableName)) selectG let relationships = mkRelationships <$> HashMap.toList tableRelationships pure $ API.QRTable API.TableRequest { _trTable = Witch.into tableName, _trRelationships = Set.fromList relationships, + _trRedactionExpressions = translateRedactionExpressions redactionExpressionState, _trQuery = query, _trForeach = Nothing } FromFunction fn@(FunctionName functionName) argsExp _dListM -> do - args <- mkArgs sessionVariables argsExp fn - (query, TableRelationships tableRelationships) <- - CPS.runWriterT (translateAnnSelect sessionVariables translateFieldsAndAggregates (FunctionNameKey (Witch.into functionName)) selectG) + args <- mkArgs argsExp fn + (query, (redactionExpressionState, TableRelationships tableRelationships)) <- + flip runStateT (RedactionExpressionState mempty, mempty) $ translateAnnSelect translateFieldsAndAggregates (API.TNFunction (Witch.into functionName)) selectG let relationships = mkRelationships <$> HashMap.toList tableRelationships pure $ API.QRFunction API.FunctionRequest { _frFunction = Witch.into functionName, _frRelationships = Set.fromList relationships, + _frRedactionExpressions = translateRedactionExpressions redactionExpressionState, _frQuery = query, _frFunctionArguments = args } -mkRelationships :: (TableRelationshipsKey, (HashMap API.RelationshipName API.Relationship)) -> API.Relationships -mkRelationships (FunctionNameKey functionName, relationships) = API.RFunction (API.FunctionRelationships functionName relationships) -mkRelationships (TableNameKey tableName, relationships) = API.RTable (API.TableRelationships tableName relationships) +mkRelationships :: (API.TargetName, (HashMap API.RelationshipName API.Relationship)) -> API.Relationships +mkRelationships (API.TNFunction functionName, relationships) = API.RFunction (API.FunctionRelationships functionName relationships) +mkRelationships (API.TNTable tableName, relationships) = API.RTable (API.TableRelationships tableName relationships) mkArgs :: - ( MonadError QErr m + forall r m. + ( MonadError QErr m, + MonadReader r m, + Has SessionVariables r ) => - SessionVariables -> Function.FunctionArgsExpG (ArgumentExp (UnpreparedValue 'DataConnector)) -> FunctionName -> m [API.FunctionArgument] -mkArgs sessionVariables (Function.FunctionArgsExp ps ns) functionName = do +mkArgs (Function.FunctionArgsExp ps ns) functionName = do unless (null ps) $ throw400 NotSupported $ "Positional arguments not supported in function " <> toTxt functionName getNamed where @@ -158,28 +185,31 @@ mkArgs sessionVariables (Function.FunctionArgsExp ps ns) functionName = do UVLiteral _ -> throw400 NotSupported "Literal not supported in Data Connector function args." UVSessionVar _ _ -> throw400 NotSupported "SessionVar not supported in Data Connector function args." UVParameter _ (ColumnValue t v) -> pure (API.ScalarValue v (coerce (toTxt t))) - UVSession -> pure (API.ScalarValue (J.toJSON sessionVariables) (API.ScalarType "json")) + UVSession -> do + (sessionVariables :: SessionVariables) <- asks getter + pure (API.ScalarValue (J.toJSON sessionVariables) (API.ScalarType "json")) translateAnnSelect :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - (TableRelationshipsKey -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT writerOutput m FieldsAndAggregates) -> - TableRelationshipsKey -> + (API.TargetName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> m FieldsAndAggregates) -> + API.TargetName -> AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m API.Query -translateAnnSelect sessionVariables translateFieldsAndAggregates entityName selectG = do + m API.Query +translateAnnSelect translateFieldsAndAggregates entityName selectG = do FieldsAndAggregates {..} <- translateFieldsAndAggregates entityName (_asnFields selectG) let whereClauseWithPermissions = case _saWhere (_asnArgs selectG) of Just expr -> BoolAnd [expr, _tpFilter (_asnPerm selectG)] Nothing -> _tpFilter (_asnPerm selectG) - whereClause <- translateBoolExpToExpression sessionVariables entityName whereClauseWithPermissions - orderBy <- traverse (translateOrderBy sessionVariables entityName) (_saOrderBy $ _asnArgs selectG) + whereClause <- translateBoolExpToExpression entityName whereClauseWithPermissions + orderBy <- traverse (translateOrderBy entityName) (_saOrderBy $ _asnArgs selectG) pure API.Query { _qFields = mapFieldNameHashMap <$> _faaFields, @@ -198,21 +228,22 @@ translateAnnSelect sessionVariables translateFieldsAndAggregates entityName sele } translateOrderBy :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> NE.NonEmpty (AnnotatedOrderByItemG 'DataConnector (UnpreparedValue 'DataConnector)) -> - CPS.WriterT writerOutput m API.OrderBy -translateOrderBy sessionVariables sourceName orderByItems = do + m API.OrderBy +translateOrderBy sourceName orderByItems = do orderByElementsAndRelations <- for orderByItems \OrderByItemG {..} -> do let orderDirection = maybe API.Ascending Witch.from obiType - translateOrderByElement sessionVariables sourceName orderDirection [] obiColumn - relations <- lift . mergeOrderByRelations $ snd <$> orderByElementsAndRelations + translateOrderByElement sourceName orderDirection [] obiColumn + relations <- mergeOrderByRelations $ snd <$> orderByElementsAndRelations pure API.OrderBy { _obRelations = relations, @@ -220,34 +251,35 @@ translateOrderBy sessionVariables sourceName orderByItems = do } translateOrderByElement :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> API.OrderDirection -> [API.RelationshipName] -> AnnotatedOrderByElement 'DataConnector (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m (API.OrderByElement, HashMap API.RelationshipName API.OrderByRelation) -translateOrderByElement sessionVariables sourceName orderDirection targetReversePath = \case - -- TODO(redactionExp): Deal with this redaction expressions - AOCColumn ColumnInfo {..} _redactionExp -> + m (API.OrderByElement, HashMap API.RelationshipName API.OrderByRelation) +translateOrderByElement sourceName orderDirection targetReversePath = \case + AOCColumn ColumnInfo {..} redactionExp -> do + redactionExpName <- recordRedactionExpression sourceName redactionExp pure ( API.OrderByElement { _obeTargetPath = reverse targetReversePath, - _obeTarget = API.OrderByColumn $ Witch.from ciColumn, + _obeTarget = API.OrderByColumn (Witch.from ciColumn) redactionExpName, _obeOrderDirection = orderDirection }, mempty ) AOCObjectRelation relationshipInfo filterExp orderByElement -> do (relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceName relationshipInfo - (translatedOrderByElement, subOrderByRelations) <- translateOrderByElement sessionVariables (TableNameKey _rTargetTable) orderDirection (relationshipName : targetReversePath) orderByElement + (translatedOrderByElement, subOrderByRelations) <- translateOrderByElement (API.TNTable _rTargetTable) orderDirection (relationshipName : targetReversePath) orderByElement - targetTableWhereExp <- translateBoolExpToExpression sessionVariables (TableNameKey _rTargetTable) filterExp + targetTableWhereExp <- translateBoolExpToExpression (API.TNTable _rTargetTable) filterExp let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp subOrderByRelations)] pure (translatedOrderByElement, orderByRelations) @@ -257,10 +289,10 @@ translateOrderByElement sessionVariables sourceName orderDirection targetReverse AAOCount -> pure API.OrderByStarCountAggregate AAOOp AggregateOrderByColumn {_aobcColumn = ColumnInfo {..}, ..} -> do - -- TODO(redactionExp): Deal with the redaction expression: _aobcRedactionExpression - aggFunction <- lift $ translateSingleColumnAggregateFunction _aobcAggregateFunctionName + redactionExpName <- recordRedactionExpression sourceName _aobcRedactionExpression + aggFunction <- translateSingleColumnAggregateFunction _aobcAggregateFunctionName let resultScalarType = Witch.from $ columnTypeToScalarType _aobcAggregateFunctionReturnType - pure . API.OrderBySingleColumnAggregate $ API.SingleColumnAggregate aggFunction (Witch.from ciColumn) resultScalarType + pure . API.OrderBySingleColumnAggregate $ API.SingleColumnAggregate aggFunction (Witch.from ciColumn) redactionExpName resultScalarType let translatedOrderByElement = API.OrderByElement @@ -269,7 +301,7 @@ translateOrderByElement sessionVariables sourceName orderDirection targetReverse _obeOrderDirection = orderDirection } - targetTableWhereExp <- translateBoolExpToExpression sessionVariables (TableNameKey _rTargetTable) filterExp + targetTableWhereExp <- translateBoolExpToExpression (API.TNTable _rTargetTable) filterExp let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp mempty)] pure (translatedOrderByElement, orderByRelations) @@ -293,71 +325,73 @@ mergeOrderByRelations orderByRelationsList = else throw500 "mergeOrderByRelations: Differing filter expressions found for the same table" translateAnnFieldsWithNoAggregates :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> FieldPrefix -> - TableRelationshipsKey -> + API.TargetName -> AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m FieldsAndAggregates -translateAnnFieldsWithNoAggregates sessionVariables fieldNamePrefix sourceName fields = - (\fields' -> FieldsAndAggregates (Just fields') Nothing) <$> translateAnnFields sessionVariables fieldNamePrefix sourceName fields + m FieldsAndAggregates +translateAnnFieldsWithNoAggregates fieldNamePrefix sourceName fields = + (\fields' -> FieldsAndAggregates (Just fields') Nothing) <$> translateAnnFields fieldNamePrefix sourceName fields translateAnnFields :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> FieldPrefix -> - TableRelationshipsKey -> + API.TargetName -> AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m (HashMap FieldName API.Field) -translateAnnFields sessionVariables fieldNamePrefix sourceName fields = do - translatedFields <- traverse (traverse (translateAnnField sessionVariables sourceName)) fields + m (HashMap FieldName API.Field) +translateAnnFields fieldNamePrefix sourceName fields = do + translatedFields <- traverse (traverse (translateAnnField sourceName)) fields pure $ HashMap.fromList (mapMaybe (\(fieldName, field) -> (applyPrefix fieldNamePrefix fieldName,) <$> field) translatedFields) translateAnnField :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> AnnFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m (Maybe API.Field) -translateAnnField sessionVariables sourceTableName = \case + m (Maybe API.Field) +translateAnnField sourceTableName = \case AFNestedObject nestedObj -> Just . API.NestedObjField (Witch.from $ _anosColumn nestedObj) - <$> translateNestedObjectSelect sessionVariables sourceTableName nestedObj + <$> translateNestedObjectSelect sourceTableName nestedObj AFNestedArray _ (ANASSimple field) -> - fmap mkArrayField <$> translateAnnField sessionVariables sourceTableName field + fmap mkArrayField <$> translateAnnField sourceTableName field where mkArrayField nestedField = API.NestedArrayField (API.ArrayField nestedField Nothing Nothing Nothing Nothing) -- TODO(dmoverton): support limit, offset, where and order_by in ArrayField AFNestedArray _ (ANASAggregate _) -> pure Nothing -- TODO(dmoverton): support nested array aggregates - AFColumn colField -> - -- TODO(redactionExp): Deal with the redaction expression: _acfRedactionExpression - -- TODO: make sure certain fields in colField are not in use, since we don't support them - pure . Just $ API.ColumnField (Witch.from $ _acfColumn colField) (Witch.from . columnTypeToScalarType $ _acfType colField) + AFColumn AnnColumnField {..} -> do + redactionExpName <- recordRedactionExpression sourceTableName _acfRedactionExpression + pure . Just $ API.ColumnField (Witch.from _acfColumn) (Witch.from $ columnTypeToScalarType _acfType) redactionExpName AFObjectRelation objRel -> case _aosTarget (_aarAnnSelect objRel) of FromTable tableName -> do let targetTable = Witch.from tableName let relationshipName = mkRelationshipName $ _aarRelationshipName objRel - fields <- translateAnnFields sessionVariables noPrefix (TableNameKey targetTable) (_aosFields (_aarAnnSelect objRel)) - whereClause <- translateBoolExpToExpression sessionVariables (TableNameKey targetTable) (_aosTargetFilter (_aarAnnSelect objRel)) + fields <- translateAnnFields noPrefix (API.TNTable targetTable) (_aosFields (_aarAnnSelect objRel)) + whereClause <- translateBoolExpToExpression (API.TNTable targetTable) (_aosTargetFilter (_aarAnnSelect objRel)) recordTableRelationship sourceTableName @@ -385,9 +419,9 @@ translateAnnField sessionVariables sourceTableName = \case ) other -> error $ "translateAnnField: " <> show other AFArrayRelation (ASSimple arrayRelationSelect) -> do - Just <$> translateArrayRelationSelect sessionVariables sourceTableName (translateAnnFieldsWithNoAggregates sessionVariables noPrefix) arrayRelationSelect + Just <$> translateArrayRelationSelect sourceTableName (translateAnnFieldsWithNoAggregates noPrefix) arrayRelationSelect AFArrayRelation (ASAggregate arrayRelationSelect) -> - Just <$> translateArrayRelationSelect sessionVariables sourceTableName (translateTableAggregateFields sessionVariables) arrayRelationSelect + Just <$> translateArrayRelationSelect sourceTableName translateTableAggregateFields arrayRelationSelect AFExpression _literal -> -- We ignore literal text fields (we don't send them to the data connector agent) -- and add them back to the response JSON when we reshape what the agent returns @@ -395,25 +429,26 @@ translateAnnField sessionVariables sourceTableName = \case pure Nothing translateArrayRelationSelect :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> - (TableRelationshipsKey -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT writerOutput m FieldsAndAggregates) -> + API.TargetName -> + (API.TargetName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> m FieldsAndAggregates) -> AnnRelationSelectG 'DataConnector (AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector)) -> - CPS.WriterT writerOutput m API.Field -translateArrayRelationSelect sessionVariables sourceName translateFieldsAndAggregates arrRel = do + m API.Field +translateArrayRelationSelect sourceName translateFieldsAndAggregates arrRel = do case _asnFrom (_aarAnnSelect arrRel) of - FromIdentifier _ -> lift $ throw400 NotSupported "AnnSelectG: FromIdentifier not supported" - FromNativeQuery {} -> lift $ throw400 NotSupported "AnnSelectG: FromNativeQuery not supported" - FromStoredProcedure {} -> lift $ throw400 NotSupported "AnnSelectG: FromStoredProcedure not supported" - FromFunction {} -> lift $ throw400 NotSupported "translateArrayRelationSelect: FromFunction not currently supported" + FromIdentifier _ -> throw400 NotSupported "AnnSelectG: FromIdentifier not supported" + FromNativeQuery {} -> throw400 NotSupported "AnnSelectG: FromNativeQuery not supported" + FromStoredProcedure {} -> throw400 NotSupported "AnnSelectG: FromStoredProcedure not supported" + FromFunction {} -> throw400 NotSupported "translateArrayRelationSelect: FromFunction not currently supported" FromTable targetTable -> do - query <- translateAnnSelect sessionVariables translateFieldsAndAggregates (TableNameKey (Witch.into targetTable)) (_aarAnnSelect arrRel) + query <- translateAnnSelect translateFieldsAndAggregates (API.TNTable (Witch.into targetTable)) (_aarAnnSelect arrRel) let relationshipName = mkRelationshipName $ _aarRelationshipName arrRel recordTableRelationship @@ -432,41 +467,43 @@ translateArrayRelationSelect sessionVariables sourceName translateFieldsAndAggre query translateTableAggregateFields :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> TableAggregateFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m FieldsAndAggregates -translateTableAggregateFields sessionVariables sourceName fields = do - mconcat <$> traverse (uncurry (translateTableAggregateField sessionVariables sourceName)) fields + m FieldsAndAggregates +translateTableAggregateFields sourceName fields = do + mconcat <$> traverse (uncurry (translateTableAggregateField sourceName)) fields translateTableAggregateField :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> FieldName -> TableAggregateFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m FieldsAndAggregates -translateTableAggregateField sessionVariables sourceName fieldName = \case + m FieldsAndAggregates +translateTableAggregateField sourceName fieldName = \case TAFAgg aggregateFields -> do let fieldNamePrefix = prefixWith fieldName - translatedAggregateFields <- lift $ mconcat <$> traverse (uncurry (translateAggregateField fieldNamePrefix)) aggregateFields + translatedAggregateFields <- mconcat <$> traverse (uncurry (translateAggregateField sourceName fieldNamePrefix)) aggregateFields pure $ FieldsAndAggregates Nothing (Just translatedAggregateFields) TAFNodes _ fields -> - translateAnnFieldsWithNoAggregates sessionVariables (prefixWith fieldName) sourceName fields + translateAnnFieldsWithNoAggregates (prefixWith fieldName) sourceName fields TAFExp _txt -> -- We ignore literal text fields (we don't send them to the data connector agent) -- and add them back to the response JSON when we reshape what the agent returns @@ -474,30 +511,41 @@ translateTableAggregateField sessionVariables sourceName fieldName = \case pure mempty translateAggregateField :: - (MonadError QErr m) => + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, + MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => + API.TargetName -> FieldPrefix -> FieldName -> AggregateField 'DataConnector (UnpreparedValue 'DataConnector) -> m (HashMap FieldName API.Aggregate) -translateAggregateField fieldPrefix fieldName = \case - AFCount countAggregate -> - let aggregate = - case countAggregate of - StarCount -> API.StarCount - -- TODO(redactionExp): Do something with these redaction expressions - ColumnCount (column, _redactionExp) -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = False} - ColumnDistinctCount (column, _redactionExp) -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = True} - in pure $ HashMap.singleton (applyPrefix fieldPrefix fieldName) aggregate +translateAggregateField sourceName fieldPrefix fieldName = \case + AFCount countAggregate -> do + aggregate <- + case countAggregate of + StarCount -> pure API.StarCount + ColumnCount (column, redactionExp) -> do + redactionExpName <- recordRedactionExpression sourceName redactionExp + pure $ API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaRedactionExpression = redactionExpName, _ccaDistinct = False} + ColumnDistinctCount (column, redactionExp) -> do + redactionExpName <- recordRedactionExpression sourceName redactionExp + pure $ API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaRedactionExpression = redactionExpName, _ccaDistinct = True} + pure $ HashMap.singleton (applyPrefix fieldPrefix fieldName) aggregate AFOp AggregateOp {..} -> do let fieldPrefix' = fieldPrefix <> prefixWith fieldName aggFunction <- translateSingleColumnAggregateFunction _aoOp fmap (HashMap.fromList . catMaybes) . forM _aoFields $ \(columnFieldName, columnField) -> case columnField of - -- TODO(redactionExp): Do something with the redactionExp - SFCol column resultType _redactionExp -> + SFCol column resultType redactionExp -> do + redactionExpName <- recordRedactionExpression sourceName redactionExp let resultScalarType = Witch.from $ columnTypeToScalarType resultType - in pure . Just $ (applyPrefix fieldPrefix' columnFieldName, API.SingleColumn $ API.SingleColumnAggregate aggFunction (Witch.from column) resultScalarType) + pure $ Just (applyPrefix fieldPrefix' columnFieldName, API.SingleColumn $ API.SingleColumnAggregate aggFunction (Witch.from column) redactionExpName resultScalarType) SFExp _txt -> -- We ignore literal text fields (we don't send them to the data connector agent) -- and add them back to the response JSON when we reshape what the agent returns @@ -517,18 +565,19 @@ translateSingleColumnAggregateFunction functionName = `onNothing` throw500 ("translateSingleColumnAggregateFunction: Invalid aggregate function encountered: " <> functionName) translateNestedObjectSelect :: - ( Has TableRelationships writerOutput, - Monoid writerOutput, + ( MonadState state m, + Has TableRelationships state, + Has RedactionExpressionState state, MonadError QErr m, MonadReader r m, - Has API.ScalarTypesCapabilities r + Has API.ScalarTypesCapabilities r, + Has SessionVariables r ) => - SessionVariables -> - TableRelationshipsKey -> + API.TargetName -> AnnNestedObjectSelectG 'DataConnector Void (UnpreparedValue 'DataConnector) -> - CPS.WriterT writerOutput m API.Query -translateNestedObjectSelect sessionVariables relationshipKey selectG = do - FieldsAndAggregates {..} <- translateAnnFieldsWithNoAggregates sessionVariables noPrefix relationshipKey $ _anosFields selectG + m API.Query +translateNestedObjectSelect relationshipKey selectG = do + FieldsAndAggregates {..} <- translateAnnFieldsWithNoAggregates noPrefix relationshipKey $ _anosFields selectG pure API.Query { _qFields = mapFieldNameHashMap <$> _faaFields, diff --git a/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs b/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs index 54a5a68982c..8e6ac469bcc 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Plan/RemoteRelationshipPlan.hs @@ -6,7 +6,6 @@ where -------------------------------------------------------------------------------- import Control.Lens ((?~)) -import Control.Monad.Trans.Writer.CPS qualified as CPS import Data.Aeson qualified as J import Data.Aeson.Encoding qualified as JE import Data.Aeson.Key qualified as K @@ -34,8 +33,11 @@ import Witch qualified mkRemoteRelationshipPlan :: forall m r. - (MonadError QErr m, MonadReader r m, Has API.ScalarTypesCapabilities r) => - SessionVariables -> + ( MonadError QErr m, + MonadReader r m, + Has API.ScalarTypesCapabilities r, + Has SessionVariables r + ) => SourceConfig -> -- | List of join json objects, each of which contains IDs to be laterally-joined against -- as well as an argument ID that identifies the particular set of IDs (ie 'row' to join against). @@ -52,7 +54,7 @@ mkRemoteRelationshipPlan :: FieldName -> SourceRelationshipSelection 'DataConnector Void UnpreparedValue -> m (Plan API.QueryRequest API.QueryResponse) -mkRemoteRelationshipPlan sessionVariables _sourceConfig joinIds joinIdsSchema argumentIdFieldName resultFieldName ir = do +mkRemoteRelationshipPlan _sourceConfig joinIds joinIdsSchema argumentIdFieldName resultFieldName ir = do foreachRowFilter <- traverse (translateForeachRowFilter argumentIdFieldName joinIdsSchema) joinIds argumentIds <- extractArgumentIds argumentIdFieldName joinIds queryRequest <- translateSourceRelationshipSelection foreachRowFilter ir @@ -66,10 +68,10 @@ mkRemoteRelationshipPlan sessionVariables _sourceConfig joinIds joinIdsSchema ar SourceRelationshipObject objectSelect -> translateAnnObjectSelectToQueryRequest foreachRowFilter objectSelect SourceRelationshipArray simpleSelect -> - QueryPlan.translateAnnSimpleSelectToQueryRequest sessionVariables simpleSelect + QueryPlan.translateAnnSimpleSelectToQueryRequest simpleSelect <&> (API.qrForeach ?~ foreachRowFilter) SourceRelationshipArrayAggregate aggregateSelect -> - QueryPlan.translateAnnAggregateSelectToQueryRequest sessionVariables aggregateSelect + QueryPlan.translateAnnAggregateSelectToQueryRequest aggregateSelect <&> (API.qrForeach ?~ foreachRowFilter) translateAnnObjectSelectToQueryRequest :: @@ -80,16 +82,18 @@ mkRemoteRelationshipPlan sessionVariables _sourceConfig joinIds joinIdsSchema ar let tableName = case _aosTarget of FromTable table -> Witch.from table other -> error $ "translateAnnObjectSelectToQueryRequest: " <> show other - ((fields, whereClause), (TableRelationships tableRelationships)) <- CPS.runWriterT $ do - fields <- QueryPlan.translateAnnFields sessionVariables noPrefix (TableNameKey tableName) _aosFields - whereClause <- translateBoolExpToExpression sessionVariables (TableNameKey tableName) _aosTargetFilter - pure (fields, whereClause) + ((fields, whereClause), (TableRelationships tableRelationships, redactionExpressionState)) <- + flip runStateT (mempty, RedactionExpressionState mempty) do + fields <- QueryPlan.translateAnnFields noPrefix (API.TNTable tableName) _aosFields + whereClause <- translateBoolExpToExpression (API.TNTable tableName) _aosTargetFilter + pure (fields, whereClause) let apiTableRelationships = Set.fromList $ tableRelationshipsToList tableRelationships pure $ API.QRTable $ API.TableRequest { _trTable = tableName, _trRelationships = apiTableRelationships, + _trRedactionExpressions = translateRedactionExpressions redactionExpressionState, _trQuery = API.Query { _qFields = Just $ mapFieldNameHashMap fields, @@ -103,12 +107,12 @@ mkRemoteRelationshipPlan sessionVariables _sourceConfig joinIds joinIdsSchema ar _trForeach = Just foreachRowFilter } -tableRelationshipsToList :: HashMap TableRelationshipsKey (HashMap API.RelationshipName API.Relationship) -> [API.Relationships] +tableRelationshipsToList :: HashMap API.TargetName (HashMap API.RelationshipName API.Relationship) -> [API.Relationships] tableRelationshipsToList m = map (either (API.RFunction . uncurry API.FunctionRelationships) (API.RTable . uncurry API.TableRelationships) . tableRelationshipsKeyToEither) (HashMap.toList m) -tableRelationshipsKeyToEither :: (TableRelationshipsKey, c) -> Either (API.FunctionName, c) (API.TableName, c) -tableRelationshipsKeyToEither (FunctionNameKey f, x) = Left (f, x) -tableRelationshipsKeyToEither (TableNameKey t, x) = Right (t, x) +tableRelationshipsKeyToEither :: (API.TargetName, c) -> Either (API.FunctionName, c) (API.TableName, c) +tableRelationshipsKeyToEither (API.TNFunction f, x) = Left (f, x) +tableRelationshipsKeyToEither (API.TNTable t, x) = Right (t, x) translateForeachRowFilter :: (MonadError QErr m) => FieldName -> HashMap FieldName (ColumnName, ScalarType) -> J.Object -> m (HashMap API.ColumnName API.ScalarValue) translateForeachRowFilter argumentIdFieldName joinIdsSchema joinIds = diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/AggregateSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/AggregateSpec.hs index dae91f4035b..01ad4180de2 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/AggregateSpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/AggregateSpec.hs @@ -11,6 +11,7 @@ where import Data.Aeson.QQ.Simple (aesonQQ) import Hasura.Backends.DataConnector.API.V0 import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnName) +import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genRedactionExpressionName) import Hasura.Backends.DataConnector.API.V0.ScalarSpec (genScalarType) import Hasura.Prelude import Hedgehog @@ -25,20 +26,22 @@ spec = do describe "Aggregate" $ do describe "SingleColumn" $ do testToFromJSONToSchema - (SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|avg|]) (ColumnName "my_column_name") (ScalarType "number")) + (SingleColumn $ SingleColumnAggregate (SingleColumnAggregateFunction [G.name|avg|]) (ColumnName "my_column_name") (Just $ RedactionExpressionName "RedactionExp2") (ScalarType "number")) [aesonQQ| { "type": "single_column", "function": "avg", "column": "my_column_name", + "redaction_expression": "RedactionExp2", "result_type": "number" } |] describe "ColumnCount" $ do testToFromJSONToSchema - (ColumnCount $ ColumnCountAggregate (ColumnName "my_column_name") True) + (ColumnCount $ ColumnCountAggregate (ColumnName "my_column_name") (Just $ RedactionExpressionName "RedactionExp2") True) [aesonQQ| { "type": "column_count", "column": "my_column_name", + "redaction_expression": "RedactionExp2", "distinct": true } |] @@ -68,12 +71,14 @@ genSingleColumnAggregate = SingleColumnAggregate <$> genSingleColumnAggregateFunction <*> genColumnName + <*> Gen.maybe genRedactionExpressionName <*> genScalarType genColumnCountAggregate :: Gen ColumnCountAggregate genColumnCountAggregate = ColumnCountAggregate <$> genColumnName + <*> Gen.maybe genRedactionExpressionName <*> Gen.bool genSingleColumnAggregateFunction :: Gen SingleColumnAggregateFunction diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs index 804911e5bca..6d77b8c6ba3 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs @@ -69,11 +69,14 @@ genColumnNullability = Gen.element [NullableAndNonNullableColumns, OnlyNullableColumns] genQueryCapabilities :: (MonadGen m) => m QueryCapabilities -genQueryCapabilities = QueryCapabilities <$> Gen.maybe genForeachCapabilities +genQueryCapabilities = QueryCapabilities <$> Gen.maybe genForeachCapabilities <*> Gen.maybe genRedactionCapabilities genForeachCapabilities :: (MonadGen m) => m ForeachCapabilities genForeachCapabilities = pure ForeachCapabilities +genRedactionCapabilities :: (MonadGen m) => m RedactionCapabilities +genRedactionCapabilities = pure RedactionCapabilities + genMutationCapabilities :: (MonadGen m) => m MutationCapabilities genMutationCapabilities = MutationCapabilities diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/ExpressionSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/ExpressionSpec.hs index 60c2ee0368d..e32e64c0e45 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/ExpressionSpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/ExpressionSpec.hs @@ -8,6 +8,8 @@ module Hasura.Backends.DataConnector.API.V0.ExpressionSpec genUnaryComparisonOperator, genComparisonValue, genExpression, + genRedactionExpressionName, + genTargetRedactionExpressions, ) where @@ -18,7 +20,8 @@ import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnName) import Hasura.Backends.DataConnector.API.V0.RelationshipsSpec (genRelationshipName) import Hasura.Backends.DataConnector.API.V0.ScalarSpec (genScalarType, genScalarValue) import Hasura.Backends.DataConnector.API.V0.TableSpec (genTableName) -import Hasura.Generator.Common (defaultRange, genArbitraryAlphaNumTextExcluding) +import Hasura.Backends.DataConnector.API.V0.TargetSpec (genTargetName) +import Hasura.Generator.Common (defaultRange, genArbitraryAlphaNumText, genArbitraryAlphaNumTextExcluding, genHashMap) import Hasura.Prelude import Hedgehog import Hedgehog.Gen qualified as Gen @@ -68,8 +71,8 @@ spec = do describe "ComparisonColumn" $ do testToFromJSONToSchema - (ComparisonColumn QueryTable (mkColumnSelector $ ColumnName "column_name") (ScalarType "string")) - [aesonQQ|{"path": ["$"], "name": "column_name", "column_type": "string"}|] + (ComparisonColumn QueryTable (mkColumnSelector $ ColumnName "column_name") (ScalarType "string") (Just $ RedactionExpressionName "RedactionExp1")) + [aesonQQ|{"path": ["$"], "name": "column_name", "column_type": "string", "redaction_expression": "RedactionExp1"}|] jsonOpenApiProperties genComparisonColumn @@ -90,7 +93,7 @@ spec = do describe "ComparisonValue" $ do describe "AnotherColumnComparison" $ testToFromJSONToSchema - (AnotherColumnComparison $ ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "my_column_name") (ScalarType "string")) + (AnotherColumnComparison $ ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "my_column_name") (ScalarType "string") Nothing) [aesonQQ|{"type": "column", "column": {"name": "my_column_name", "column_type": "string"}}|] describe "ScalarValueComparison" $ testToFromJSONToSchema @@ -119,7 +122,7 @@ spec = do jsonOpenApiProperties genExistsInTable describe "Expression" $ do - let comparisonColumn = ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "my_column_name") (ScalarType "string") + let comparisonColumn = ComparisonColumn CurrentTable (mkColumnSelector $ ColumnName "my_column_name") (ScalarType "string") Nothing let scalarValue = ScalarValueComparison $ ScalarValue (String "scalar value") (ScalarType "string") let scalarValues = [String "scalar value"] let unaryComparisonExpression = ApplyUnaryComparisonOperator IsNull comparisonColumn @@ -226,6 +229,30 @@ spec = do jsonOpenApiProperties genExpression + describe "RedactionExpressionName" $ do + testToFromJSONToSchema (RedactionExpressionName "foo") [aesonQQ|"foo"|] + jsonOpenApiProperties genRedactionExpressionName + + describe "TargetRedactionExpressions" $ do + testToFromJSONToSchema + (TargetRedactionExpressions (TNTable $ TableName ["my_table_name"]) mempty) + [aesonQQ| + { "target": { "type": "table", "table": ["my_table_name"] }, + "expressions": {} + } + |] + jsonOpenApiProperties genTargetRedactionExpressions + + describe "RedactionExpression" $ do + testToFromJSONToSchema + (RedactionExpression $ And []) + [aesonQQ| + { "type": "and", + "expressions": [] + } + |] + jsonOpenApiProperties genRedactionExpression + genBinaryComparisonOperator :: (MonadGen m, GenBase m ~ Identity) => m BinaryComparisonOperator genBinaryComparisonOperator = Gen.choice @@ -256,6 +283,7 @@ genComparisonColumn = <$> genColumnPath <*> genColumnSelector <*> genScalarType + <*> Gen.maybe genRedactionExpressionName genColumnPath :: (MonadGen m) => m ColumnPath genColumnPath = @@ -294,3 +322,15 @@ genExpression = ] where genExpressions = Gen.set defaultRange genExpression + +genRedactionExpressionName :: (MonadGen m) => m RedactionExpressionName +genRedactionExpressionName = RedactionExpressionName <$> genArbitraryAlphaNumText defaultRange + +genTargetRedactionExpressions :: (MonadGen m, GenBase m ~ Identity) => m TargetRedactionExpressions +genTargetRedactionExpressions = + TargetRedactionExpressions + <$> genTargetName + <*> genHashMap genRedactionExpressionName genRedactionExpression defaultRange + +genRedactionExpression :: (MonadGen m, GenBase m ~ Identity) => m RedactionExpression +genRedactionExpression = RedactionExpression <$> genExpression diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/FunctionSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/FunctionSpec.hs new file mode 100644 index 00000000000..081357ec099 --- /dev/null +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/FunctionSpec.hs @@ -0,0 +1,42 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE QuasiQuotes #-} + +module Hasura.Backends.DataConnector.API.V0.FunctionSpec + ( spec, + genFunctionName, + genFunctionArgument, + ) +where + +import Data.Aeson.QQ.Simple (aesonQQ) +import Hasura.Backends.DataConnector.API.V0 +import Hasura.Backends.DataConnector.API.V0.ScalarSpec (genScalarType) +import Hasura.Generator.Common (defaultRange, genArbitraryAlphaNumText) +import Hasura.Prelude +import Hedgehog +import Hedgehog.Gen qualified as Gen +import Hedgehog.Internal.Range (linear) +import Test.Aeson.Utils (genValue, jsonOpenApiProperties, testToFromJSONToSchema) +import Test.Hspec + +spec :: Spec +spec = do + describe "FunctionName" $ do + testToFromJSONToSchema (TableName ["my_function_name"]) [aesonQQ|["my_function_name"]|] + jsonOpenApiProperties genFunctionName + +genFunctionName :: (MonadGen m) => m FunctionName +genFunctionName = FunctionName <$> Gen.nonEmpty (linear 1 3) (genArbitraryAlphaNumText defaultRange) + +genFunctionArgument :: Gen FunctionArgument +genFunctionArgument = + NamedArgument + <$> genArbitraryAlphaNumText defaultRange + <*> genArgumentValue + +genArgumentValue :: Gen ArgumentValue +genArgumentValue = + fmap ScalarArgumentValue + $ ScalarValue + <$> genValue + <*> genScalarType diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/MutationsSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/MutationsSpec.hs index 4deb1229dc9..206676df328 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/MutationsSpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/MutationsSpec.hs @@ -11,7 +11,7 @@ import Data.Aeson.QQ.Simple (aesonQQ) import Hasura.Backends.DataConnector.API.V0 import Hasura.Backends.DataConnector.API.V0.CapabilitiesSpec (genUpdateColumnOperatorName) import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnName, genColumnType, genColumnValueGenerationStrategy) -import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression) +import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression, genTargetRedactionExpressions) import Hasura.Backends.DataConnector.API.V0.QuerySpec (genField, genFieldMap, genFieldValue) import Hasura.Backends.DataConnector.API.V0.RelationshipsSpec (genRelationshipName, genTableRelationships) import Hasura.Backends.DataConnector.API.V0.ScalarSpec (genScalarType) @@ -28,7 +28,7 @@ spec :: Spec spec = do describe "MutationRequest" $ do testToFromJSONToSchema - (MutationRequest [] [] []) + (MutationRequest [] [] [] []) [aesonQQ| { "table_relationships": [], "insert_schema": [], @@ -93,7 +93,7 @@ spec = do jsonOpenApiProperties genInsertFieldSchema describe "MutationOperation" $ do - let returningFields = [(FieldName "field", ColumnField (ColumnName "my_column") (ScalarType "string"))] + let returningFields = [(FieldName "field", ColumnField (ColumnName "my_column") (ScalarType "string") (Just $ RedactionExpressionName "RedactionExp1"))] describe "InsertOperation" $ do testToFromJSONToSchema (InsertOperation (InsertMutationOperation (TableName ["my_table"]) [] (Just $ And []) returningFields)) @@ -106,7 +106,8 @@ spec = do "field": { "type": "column", "column": "my_column", - "column_type": "string" + "column_type": "string", + "redaction_expression": "RedactionExp1" } } } @@ -124,7 +125,8 @@ spec = do "field": { "type": "column", "column": "my_column", - "column_type": "string" + "column_type": "string", + "redaction_expression": "RedactionExp1" } } } @@ -140,7 +142,8 @@ spec = do "field": { "type": "column", "column": "my_column", - "column_type": "string" + "column_type": "string", + "redaction_expression": "RedactionExp1" } } } @@ -250,6 +253,7 @@ genMutationRequest :: Gen MutationRequest genMutationRequest = MutationRequest <$> Gen.set defaultRange genTableRelationships + <*> Gen.set defaultRange genTargetRedactionExpressions <*> Gen.set defaultRange genTableInsertSchema <*> Gen.list defaultRange genMutationOperation diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/OrderBySpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/OrderBySpec.hs index dc4c82647cb..553afb16956 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/OrderBySpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/OrderBySpec.hs @@ -13,7 +13,7 @@ import Data.HashMap.Strict qualified as HashMap import Hasura.Backends.DataConnector.API.V0 import Hasura.Backends.DataConnector.API.V0.AggregateSpec (genSingleColumnAggregate) import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnName) -import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression) +import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression, genRedactionExpressionName) import Hasura.Backends.DataConnector.API.V0.RelationshipsSpec (genRelationshipName) import Hasura.Generator.Common (defaultRange) import Hasura.Prelude @@ -28,10 +28,11 @@ spec = do describe "OrderByTarget" $ do describe "OrderByColumn" $ testToFromJSONToSchema - (OrderByColumn (ColumnName "test_column")) + (OrderByColumn (ColumnName "test_column") (Just $ RedactionExpressionName "RedactionExp2")) [aesonQQ| { "type": "column", - "column": "test_column" + "column": "test_column", + "redaction_expression": "RedactionExp2" } |] describe "OrderByStarCountAggregate" @@ -42,12 +43,13 @@ spec = do |] describe "OrderBySingleColumnAggregate" $ testToFromJSONToSchema - (OrderBySingleColumnAggregate (SingleColumnAggregate (SingleColumnAggregateFunction [G.name|sum|]) (ColumnName "test_column") (ScalarType "number"))) + (OrderBySingleColumnAggregate (SingleColumnAggregate (SingleColumnAggregateFunction [G.name|sum|]) (ColumnName "test_column") (Just $ RedactionExpressionName "RedactionExp2") (ScalarType "number"))) [aesonQQ| { "type": "single_column_aggregate", "function": "sum", "column": "test_column", - "result_type": "number" + "result_type": "number", + "redaction_expression": "RedactionExp2" } |] jsonOpenApiProperties genOrderByTarget @@ -56,14 +58,15 @@ spec = do testToFromJSONToSchema ( OrderByElement [RelationshipName "relation1", RelationshipName "relation2"] - (OrderByColumn (ColumnName "my_column_name")) + (OrderByColumn (ColumnName "my_column_name") (Just $ RedactionExpressionName "RedactionExp2")) Ascending ) [aesonQQ| { "target_path": ["relation1", "relation2"], "target": { "type": "column", - "column": "my_column_name" + "column": "my_column_name", + "redaction_expression": "RedactionExp2" }, "order_direction": "asc" } @@ -145,7 +148,7 @@ genOrderByElement = genOrderByTarget :: Gen OrderByTarget genOrderByTarget = Gen.choice - [ OrderByColumn <$> genColumnName, + [ OrderByColumn <$> genColumnName <*> Gen.maybe genRedactionExpressionName, pure OrderByStarCountAggregate, OrderBySingleColumnAggregate <$> genSingleColumnAggregate ] 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 4c2fc6a6530..6cdbf5defd5 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs @@ -16,7 +16,8 @@ import Data.HashMap.Strict qualified as HashMap import Hasura.Backends.DataConnector.API.V0 import Hasura.Backends.DataConnector.API.V0.AggregateSpec (genAggregate) import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnName) -import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression) +import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression, genRedactionExpressionName, genTargetRedactionExpressions) +import Hasura.Backends.DataConnector.API.V0.FunctionSpec (genFunctionArgument, genFunctionName) import Hasura.Backends.DataConnector.API.V0.OrderBySpec (genOrderBy) import Hasura.Backends.DataConnector.API.V0.RelationshipsSpec (genRelationshipName, genRelationships) import Hasura.Backends.DataConnector.API.V0.ScalarSpec (genScalarType, genScalarValue) @@ -25,7 +26,6 @@ import Hasura.Generator.Common (defaultRange, genArbitraryAlphaNumText, genHashM import Hasura.Prelude import Hedgehog import Hedgehog.Gen qualified as Gen -import Hedgehog.Internal.Range (linear) import Test.Aeson.Utils (genValue, jsonOpenApiProperties, testToFromJSONToSchema) import Test.Hspec @@ -34,11 +34,12 @@ spec = do describe "Field" $ do describe "ColumnField" $ testToFromJSONToSchema - (ColumnField (ColumnName "my_column_name") (ScalarType "string")) + (ColumnField (ColumnName "my_column_name") (ScalarType "string") (Just $ RedactionExpressionName "RedactionExp0")) [aesonQQ| { "type": "column", "column": "my_column_name", - "column_type": "string" + "column_type": "string", + "redaction_expression": "RedactionExp0" } |] describe "RelationshipField" $ do @@ -56,13 +57,13 @@ spec = do describe "Query" $ do let query = Query - { _qFields = Just $ HashMap.fromList [(FieldName "my_field_alias", ColumnField (ColumnName "my_field_name") (ScalarType "string"))], + { _qFields = Just $ HashMap.fromList [(FieldName "my_field_alias", ColumnField (ColumnName "my_field_name") (ScalarType "string") Nothing)], _qAggregates = Just $ HashMap.fromList [(FieldName "my_aggregate", StarCount)], _qAggregatesLimit = Just 5, _qLimit = Just 10, _qOffset = Just 20, _qWhere = Just $ And [], - _qOrderBy = Just $ OrderBy [] (OrderByElement [] (OrderByColumn (ColumnName "my_column_name")) Ascending :| []) + _qOrderBy = Just $ OrderBy [] (OrderByElement [] (OrderByColumn (ColumnName "my_column_name") Nothing) Ascending :| []) } testToFromJSONToSchema query @@ -95,6 +96,7 @@ spec = do $ TableRequest { _trTable = TableName ["my_table"], _trRelationships = [], + _trRedactionExpressions = [], _trQuery = Query (Just mempty) Nothing Nothing Nothing Nothing Nothing Nothing, _trForeach = Just (HashMap.fromList [(ColumnName "my_id", ScalarValue (J.Number 666) (ScalarType "number"))] :| []) } @@ -119,6 +121,7 @@ spec = do { _frFunction = FunctionName ["my_function"], _frFunctionArguments = [], _frRelationships = [], + _frRedactionExpressions = [], _frQuery = Query (Just mempty) Nothing Nothing Nothing Nothing Nothing Nothing } testToFromJSONToSchema @@ -185,7 +188,7 @@ genField :: Gen Field genField = Gen.recursive Gen.choice - [ColumnField <$> genColumnName <*> genScalarType] + [ColumnField <$> genColumnName <*> genScalarType <*> Gen.maybe genRedactionExpressionName] [RelField <$> genRelationshipField] genFieldName :: Gen FieldName @@ -220,29 +223,15 @@ genFunctionRequest = <$> genFunctionName <*> Gen.list defaultRange genFunctionArgument <*> Gen.set defaultRange genRelationships + <*> Gen.set defaultRange genTargetRedactionExpressions <*> genQuery -genFunctionName :: (MonadGen m) => m FunctionName -genFunctionName = FunctionName <$> Gen.nonEmpty (linear 1 3) (genArbitraryAlphaNumText defaultRange) - -genFunctionArgument :: Gen FunctionArgument -genFunctionArgument = - NamedArgument - <$> genArbitraryAlphaNumText defaultRange - <*> genArgumentValue - -genArgumentValue :: Gen ArgumentValue -genArgumentValue = - fmap ScalarArgumentValue - $ ScalarValue - <$> genValue - <*> genScalarType - genTableRequest :: Gen QueryRequest genTableRequest = TableQueryRequest <$> genTableName <*> Gen.set defaultRange genRelationships + <*> Gen.set defaultRange genTargetRedactionExpressions <*> genQuery <*> Gen.maybe (Gen.nonEmpty defaultRange (genHashMap genColumnName genScalarValue defaultRange)) diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/TargetSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/TargetSpec.hs new file mode 100644 index 00000000000..c68d372fec5 --- /dev/null +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/TargetSpec.hs @@ -0,0 +1,47 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE QuasiQuotes #-} + +module Hasura.Backends.DataConnector.API.V0.TargetSpec + ( spec, + genTargetName, + ) +where + +import Data.Aeson.QQ.Simple (aesonQQ) +import Hasura.Backends.DataConnector.API.V0 +import Hasura.Backends.DataConnector.API.V0.FunctionSpec (genFunctionName) +import Hasura.Backends.DataConnector.API.V0.TableSpec (genTableName) +import Hasura.Prelude +import Hedgehog +import Hedgehog.Gen qualified as Gen +import Test.Aeson.Utils (jsonOpenApiProperties, testToFromJSONToSchema) +import Test.Hspec + +spec :: Spec +spec = do + describe "TargetName" $ do + describe "TNTable" + $ testToFromJSONToSchema + (TNTable $ TableName ["my_table"]) + [aesonQQ| + { "type": "table", + "table": ["my_table"] + } + |] + describe "TNFunction" + $ testToFromJSONToSchema + (TNFunction $ FunctionName ["my_function"]) + [aesonQQ| + { "type": "function", + "function": ["my_function"] + } + |] + + jsonOpenApiProperties genTargetName + +genTargetName :: (MonadGen m) => m TargetName +genTargetName = + Gen.choice + [ TNTable <$> genTableName, + TNFunction <$> genFunctionName + ]