Fix inherited roles issue with ordering

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

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9868
GitOrigin-RevId: 0c2b50ec1a2d179a4f79b6c795bf8fecb7ad7c9f
This commit is contained in:
Daniel Chambers 2023-07-18 23:07:35 +10:00 committed by hasura-bot
parent 9a09af4f20
commit a86345421f
28 changed files with 1086 additions and 551 deletions

View File

@ -115,7 +115,7 @@ library
Test.Auth.Authorization.DisableRootFields.SelectPermission.EnableAllRootFieldsSpec
Test.Auth.Authorization.DisableRootFields.SelectPermission.EnablePKSpec
Test.Auth.Authorization.DisableRootFields.SelectPermission.RelationshipSpec
Test.Auth.Authorization.InheritedRoles.ColumnCensorshipSpec
Test.Auth.Authorization.InheritedRoles.ColumnRedactionSpec
Test.DataConnector.AggregateQuerySpec
Test.DataConnector.MetadataApiSpec
Test.DataConnector.MockAgent.AggregateQuerySpec

View File

@ -1,328 +0,0 @@
module Test.Auth.Authorization.InheritedRoles.ColumnCensorshipSpec
( spec,
)
where
import Data.Aeson (Value (String), object, (.=))
import Data.List.NonEmpty qualified as NE
import Data.String.Interpolate (i)
import Harness.Backend.Postgres qualified as Postgres
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Permissions (InheritedRoleDetails (..), Permission (..), SelectPermissionDetails (..), selectPermission)
import Harness.Quoter.Graphql
import Harness.Quoter.Yaml (interpolateYaml, yaml)
import Harness.Schema (Table (..), table)
import Harness.Schema qualified as Schema
import Harness.Test.Fixture qualified as Fixture
import Harness.Test.SetupAction (setupPermissionsAction)
import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment, getBackendTypeConfig)
import Harness.Yaml (shouldReturnYaml)
import Hasura.Prelude
import Test.Hspec (SpecWith, describe, it)
--------------------------------------------------------------------------------
-- Preamble
spec :: SpecWith GlobalTestEnvironment
spec =
Fixture.run
( NE.fromList
[ (Fixture.fixture $ Fixture.Backend Postgres.backendTypeMetadata)
{ Fixture.setupTeardown = \(testEnv, _) ->
Postgres.setupTablesAction schema testEnv
: computedFieldSetupActions testEnv
<> [setupPermissionsAction permissions testEnv]
}
]
)
tests
--------------------------------------------------------------------------------
-- Schema
schema :: [Schema.Table]
schema = [employee]
employee :: Schema.Table
employee =
(table "employee")
{ tableColumns =
[ Schema.column "id" Schema.TInt,
Schema.column "first_name" Schema.TStr,
Schema.column "last_name" Schema.TStr,
Schema.column "monthly_salary" Schema.TInt
],
tablePrimaryKey = ["id"],
tableData =
[ [Schema.VInt 1, Schema.VStr "David", Schema.VStr "Holden", Schema.VInt 5000],
[Schema.VInt 2, Schema.VStr "Grant", Schema.VStr "Smith", Schema.VInt 6000],
[Schema.VInt 3, Schema.VStr "Xin", Schema.VStr "Cheng", Schema.VInt 5500],
[Schema.VInt 4, Schema.VStr "Sarah", Schema.VStr "Smith", Schema.VInt 6100]
]
}
computedFieldSetupActions :: TestEnvironment -> [Fixture.SetupAction]
computedFieldSetupActions testEnv =
let schemaName = Schema.getSchemaName testEnv
backendTypeMetadata = fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnv
source = Fixture.backendSourceName backendTypeMetadata
in [ Fixture.SetupAction
{ Fixture.setupAction =
Postgres.run_ testEnv
$ [i|
CREATE FUNCTION #{ employee_yearly_salary schemaName }(employee_row employee)
RETURNS integer AS $$
SELECT employee_row.monthly_salary * 12
$$ LANGUAGE sql STABLE;
|],
Fixture.teardownAction = \_ -> pure ()
},
Fixture.SetupAction
{ Fixture.setupAction =
Schema.trackComputedField
source
employee
"employee_yearly_salary"
"yearly_salary"
[yaml| null |]
[yaml| null |]
testEnv,
Fixture.teardownAction = \_ -> pure ()
}
]
employee_yearly_salary :: Schema.SchemaName -> Text
employee_yearly_salary (Schema.SchemaName name) = name <> ".employee_yearly_salary"
--------------------------------------------------------------------------------
-- Permissions
permissions :: [Permission]
permissions =
[ SelectPermission
selectPermission
{ selectPermissionTable = "employee",
selectPermissionRole = "employee_public_info",
selectPermissionColumns = ["id", "first_name", "last_name"],
selectPermissionAllowAggregations = True,
selectPermissionRows = object []
},
SelectPermission
selectPermission
{ selectPermissionTable = "employee",
selectPermissionRole = "employee_private_info",
selectPermissionColumns = ["id", "first_name", "last_name", "monthly_salary"],
selectPermissionComputedFields = ["yearly_salary"],
selectPermissionAllowAggregations = True,
selectPermissionRows =
object
[ "id" .= String "X-Hasura-Employee-Id"
]
},
InheritedRole
InheritedRoleDetails
{ inheritedRoleName = "employee",
inheritedRoleRoleSet = ["employee_public_info", "employee_private_info"]
}
]
--------------------------------------------------------------------------------
-- Tests
tests :: SpecWith TestEnvironment
tests = do
describe "Censorship in column selection sets" $ do
it "Check censorship in regular queries" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee(order_by: { id: asc }) {
id
first_name
last_name
monthly_salary
yearly_salary
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee:
- id: 1
first_name: David
last_name: Holden
monthly_salary: null
yearly_salary: null
- id: 2
first_name: Grant
last_name: Smith
monthly_salary: null
yearly_salary: null
- id: 3
first_name: Xin
last_name: Cheng
monthly_salary: 5500
yearly_salary: 66000
- id: 4
first_name: Sarah
last_name: Smith
monthly_salary: null
yearly_salary: null
|]
shouldReturnYaml testEnvironment actual expected
it "Check column censorship in nodes in aggregate queries" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee_aggregate(order_by: { id: asc }) {
nodes {
id
first_name
last_name
monthly_salary
yearly_salary
}
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee_aggregate:
nodes:
- id: 1
first_name: David
last_name: Holden
monthly_salary: null
yearly_salary: null
- id: 2
first_name: Grant
last_name: Smith
monthly_salary: null
yearly_salary: null
- id: 3
first_name: Xin
last_name: Cheng
monthly_salary: 5500
yearly_salary: 66000
- id: 4
first_name: Sarah
last_name: Smith
monthly_salary: null
yearly_salary: null
|]
shouldReturnYaml testEnvironment actual expected
describe "Censorship in aggregation calculations" $ do
it "Check censorship of input values to aggregation functions" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee_aggregate {
aggregate {
count
sum {
monthly_salary
yearly_salary
}
}
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee_aggregate:
aggregate:
count: 4
sum:
monthly_salary: 5500
yearly_salary: 66000
|]
shouldReturnYaml testEnvironment actual expected
it "Check censorship of input values to count aggregations that use columns" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee_aggregate {
aggregate {
count
count_distinct_salary: count(distinct: true, columns:[monthly_salary])
count_not_distinct_salary: count(distinct: false, columns:[monthly_salary])
}
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee_aggregate:
aggregate:
count: 4
count_distinct_salary: 1
count_not_distinct_salary: 1
|]
shouldReturnYaml testEnvironment actual expected

View File

@ -0,0 +1,609 @@
module Test.Auth.Authorization.InheritedRoles.ColumnRedactionSpec
( spec,
)
where
import Data.Aeson (Value (String), object, (.=))
import Data.List.NonEmpty qualified as NE
import Data.String.Interpolate (i)
import Harness.Backend.Postgres qualified as Postgres
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Permissions (InheritedRoleDetails (..), Permission (..), SelectPermissionDetails (..), selectPermission)
import Harness.Quoter.Graphql
import Harness.Quoter.Yaml (interpolateYaml, yaml)
import Harness.Schema (Table (..), table)
import Harness.Schema qualified as Schema
import Harness.Test.Fixture qualified as Fixture
import Harness.Test.SetupAction (setupPermissionsAction)
import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment, getBackendTypeConfig)
import Harness.Yaml (shouldReturnYaml)
import Hasura.Prelude
import Test.Hspec (SpecWith, describe, it)
--------------------------------------------------------------------------------
-- Preamble
spec :: SpecWith GlobalTestEnvironment
spec =
Fixture.run
( NE.fromList
[ (Fixture.fixture $ Fixture.Backend Postgres.backendTypeMetadata)
{ Fixture.setupTeardown = \(testEnv, _) ->
Postgres.setupTablesAction schema testEnv
: computedFieldSetupActions testEnv
<> [setupPermissionsAction permissions testEnv]
}
]
)
tests
--------------------------------------------------------------------------------
-- Schema
schema :: [Schema.Table]
schema = [manager, employee]
manager :: Schema.Table
manager =
(table "manager")
{ tableColumns =
[ Schema.column "id" Schema.TInt,
Schema.column "first_name" Schema.TStr,
Schema.column "last_name" Schema.TStr
],
tablePrimaryKey = ["id"],
tableData =
[ [Schema.VInt 1, Schema.VStr "Ryan", Schema.VStr "Ray"],
[Schema.VInt 2, Schema.VStr "Martin", Schema.VStr "Graham"],
[Schema.VInt 3, Schema.VStr "Althea", Schema.VStr "Weiss"],
[Schema.VInt 4, Schema.VStr "Bec", Schema.VStr "Vo"]
]
}
employee :: Schema.Table
employee =
(table "employee")
{ tableColumns =
[ Schema.column "id" Schema.TInt,
Schema.column "first_name" Schema.TStr,
Schema.column "last_name" Schema.TStr,
Schema.column "nationality" Schema.TStr,
Schema.column "monthly_salary" Schema.TInt,
Schema.column "engineering_manager_id" Schema.TInt,
Schema.column "hr_manager_id" Schema.TInt
],
tablePrimaryKey = ["id"],
tableReferences =
[ Schema.reference "engineering_manager_id" "manager" "id",
Schema.reference "hr_manager_id" "manager" "id"
],
tableData =
[ [Schema.VInt 1, Schema.VStr "David", Schema.VStr "Holden", Schema.VStr "Australian", Schema.VInt 5000, Schema.VInt 1, Schema.VInt 3],
[Schema.VInt 2, Schema.VStr "Grant", Schema.VStr "Smith", Schema.VStr "Australian", Schema.VInt 6000, Schema.VInt 1, Schema.VInt 4],
[Schema.VInt 3, Schema.VStr "Xin", Schema.VStr "Cheng", Schema.VStr "Chinese", Schema.VInt 5500, Schema.VInt 2, Schema.VInt 3],
[Schema.VInt 4, Schema.VStr "Sarah", Schema.VStr "Smith", Schema.VStr "British", Schema.VInt 4000, Schema.VInt 2, Schema.VInt 4]
]
}
computedFieldSetupActions :: TestEnvironment -> [Fixture.SetupAction]
computedFieldSetupActions testEnv =
let schemaName = Schema.getSchemaName testEnv
backendTypeMetadata = fromMaybe (error "Unknown backend") $ getBackendTypeConfig testEnv
source = Fixture.backendSourceName backendTypeMetadata
in [ Fixture.SetupAction
{ Fixture.setupAction =
Postgres.run_ testEnv
$ [i|
CREATE FUNCTION #{ employee_yearly_salary schemaName }(employee_row employee)
RETURNS integer AS $$
SELECT employee_row.monthly_salary * 12
$$ LANGUAGE sql STABLE;
|],
Fixture.teardownAction = \_ -> pure ()
},
Fixture.SetupAction
{ Fixture.setupAction =
Schema.trackComputedField
source
employee
"employee_yearly_salary"
"yearly_salary"
[yaml| null |]
[yaml| null |]
testEnv,
Fixture.teardownAction = \_ -> pure ()
}
]
employee_yearly_salary :: Schema.SchemaName -> Text
employee_yearly_salary (Schema.SchemaName name) = name <> ".employee_yearly_salary"
--------------------------------------------------------------------------------
-- Permissions
permissions :: [Permission]
permissions =
[ SelectPermission
selectPermission
{ selectPermissionTable = "employee",
selectPermissionRole = "employee_public_info",
selectPermissionColumns = ["id", "first_name", "last_name"],
selectPermissionAllowAggregations = True,
selectPermissionRows = object []
},
SelectPermission
selectPermission
{ selectPermissionTable = "employee",
selectPermissionRole = "employee_private_info",
selectPermissionColumns = ["id", "first_name", "last_name", "monthly_salary"],
selectPermissionComputedFields = ["yearly_salary"],
selectPermissionAllowAggregations = True,
selectPermissionRows =
object
[ "id" .= String "X-Hasura-Employee-Id"
]
},
InheritedRole
InheritedRoleDetails
{ inheritedRoleName = "employee",
inheritedRoleRoleSet = ["employee_public_info", "employee_private_info"]
},
SelectPermission
selectPermission
{ selectPermissionTable = "manager",
selectPermissionRole = "all_managers",
selectPermissionColumns = ["id", "first_name", "last_name"],
selectPermissionAllowAggregations = True,
selectPermissionRows = object []
},
SelectPermission
selectPermission
{ selectPermissionTable = "employee",
selectPermissionRole = "all_managers",
selectPermissionColumns = ["id", "first_name", "last_name", "engineering_manager_id", "hr_manager_id"],
selectPermissionAllowAggregations = True,
selectPermissionRows = object []
},
SelectPermission
selectPermission
{ selectPermissionTable = "employee",
selectPermissionRole = "manager_employee_private_info",
selectPermissionColumns = ["id", "first_name", "last_name", "nationality", "monthly_salary", "engineering_manager_id", "hr_manager_id"],
selectPermissionComputedFields = ["yearly_salary"],
selectPermissionAllowAggregations = True,
selectPermissionRows =
object
[ "hr_manager_id" .= String "X-Hasura-Manager-Id"
]
},
InheritedRole
InheritedRoleDetails
{ inheritedRoleName = "hr_manager",
inheritedRoleRoleSet = ["all_managers", "manager_employee_private_info"]
}
]
--------------------------------------------------------------------------------
-- Tests
tests :: SpecWith TestEnvironment
tests = do
describe "Redaction in column selection sets" $ do
it "Check redaction in regular queries" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee(order_by: { id: asc }) {
id
first_name
last_name
monthly_salary
yearly_salary
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee:
- id: 1
first_name: David
last_name: Holden
monthly_salary: null
yearly_salary: null
- id: 2
first_name: Grant
last_name: Smith
monthly_salary: null
yearly_salary: null
- id: 3
first_name: Xin
last_name: Cheng
monthly_salary: 5500
yearly_salary: 66000
- id: 4
first_name: Sarah
last_name: Smith
monthly_salary: null
yearly_salary: null
|]
shouldReturnYaml testEnvironment actual expected
it "Check column redaction in nodes in aggregate queries" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee_aggregate(order_by: { id: asc }) {
nodes {
id
first_name
last_name
monthly_salary
yearly_salary
}
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee_aggregate:
nodes:
- id: 1
first_name: David
last_name: Holden
monthly_salary: null
yearly_salary: null
- id: 2
first_name: Grant
last_name: Smith
monthly_salary: null
yearly_salary: null
- id: 3
first_name: Xin
last_name: Cheng
monthly_salary: 5500
yearly_salary: 66000
- id: 4
first_name: Sarah
last_name: Smith
monthly_salary: null
yearly_salary: null
|]
shouldReturnYaml testEnvironment actual expected
describe "Redaction in aggregation calculations" $ do
it "Check redaction of input values to aggregation functions" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee_aggregate {
aggregate {
count
sum {
monthly_salary
yearly_salary
}
}
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee_aggregate:
aggregate:
count: 4
sum:
monthly_salary: 5500
yearly_salary: 66000
|]
shouldReturnYaml testEnvironment actual expected
it "Check redaction of input values to count aggregations that use columns" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee_aggregate {
aggregate {
count
count_distinct_salary: count(distinct: true, columns:[monthly_salary])
count_not_distinct_salary: count(distinct: false, columns:[monthly_salary])
}
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column & yearly_salary computed field,
-- but the 'employee_private_info' role does, but only for the current
-- employee's record (ie. hers)
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee_aggregate:
aggregate:
count: 4
count_distinct_salary: 1
count_not_distinct_salary: 1
|]
shouldReturnYaml testEnvironment actual expected
describe "Redaction in ordering and distinct on" $ do
it "ordering by column is applied over redacted column value" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee(order_by: [{ monthly_salary: desc }, {id: desc}]) {
id
first_name
last_name
monthly_salary
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column, but the 'employee_private_info' role
-- does, but only for the current employee's record (ie. hers).
-- This means when she orders by monthly salary, the ordering
-- should not know the value of any salary other than hers and therefore
-- should fall back to order by the id since all other salaries should
-- appear as null.
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee:
- id: 4
first_name: Sarah
last_name: Smith
monthly_salary: null
- id: 2
first_name: Grant
last_name: Smith
monthly_salary: null
- id: 1
first_name: David
last_name: Holden
monthly_salary: null
- id: 3
first_name: Xin
last_name: Cheng
monthly_salary: 5500
|]
shouldReturnYaml testEnvironment actual expected
it "ordering by a computed field is applied over redacted computed field value" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "employee"),
("X-Hasura-Employee-Id", "3")
]
[graphql|
query {
#{schemaName}_employee(order_by: [{ yearly_salary: desc }, {id: desc}]) {
id
first_name
last_name
yearly_salary
}
}
|]
-- Xin Cheng can see her own salary, but not her peers' because the
-- 'employee_public_info' role does not provide access to
-- the monthly_salary column, but the 'employee_private_info' role
-- does, but only for the current employee's record (ie. hers).
-- This means when she orders by monthly salary, the ordering
-- should not know the value of any salary other than hers and therefore
-- should fall back to order by the id since all other salaries should
-- appear as null.
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee:
- id: 4
first_name: Sarah
last_name: Smith
yearly_salary: null
- id: 2
first_name: Grant
last_name: Smith
yearly_salary: null
- id: 1
first_name: David
last_name: Holden
yearly_salary: null
- id: 3
first_name: Xin
last_name: Cheng
yearly_salary: 66000
|]
shouldReturnYaml testEnvironment actual expected
it "ordering by aggregate is applied over the aggregate over the redacted column value" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "hr_manager"),
("X-Hasura-Manager-Id", "3")
]
[graphql|
query {
#{schemaName}_manager(order_by: [{employees_by_id_to_engineering_manager_id_aggregate: { sum: { monthly_salary: desc } }}, {id: asc}]) {
id
first_name
last_name
employees_by_id_to_engineering_manager_id_aggregate {
aggregate {
sum {
monthly_salary
}
}
}
}
}
|]
-- Althea Weiss can only see the salaries of the employees she is HR manager for.
-- This is because the 'manager_employee_private_info' role provides access to the salary
-- for the current manager's HR-managed employees, but the rest of the employees
-- are accessed via 'all_managers', which does not expose 'monthly_salary'.
-- So when Althea orders all managers by the sum of the salary of the employees they
-- are the _engineering manager_ for, she should only be ordering them by
-- aggregate of the salaries she can see.
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_manager:
- id: 3
first_name: Althea
last_name: Weiss
employees_by_id_to_engineering_manager_id_aggregate:
aggregate:
sum:
monthly_salary: null
- id: 4
first_name: Bec
last_name: Vo
employees_by_id_to_engineering_manager_id_aggregate:
aggregate:
sum:
monthly_salary: null
- id: 2
first_name: Martin
last_name: Graham
employees_by_id_to_engineering_manager_id_aggregate:
aggregate:
sum:
monthly_salary: 5500
- id: 1
first_name: Ryan
last_name: Ray
employees_by_id_to_engineering_manager_id_aggregate:
aggregate:
sum:
monthly_salary: 5000
|]
shouldReturnYaml testEnvironment actual expected
it "distinct_on is applied over redacted column values" \testEnvironment -> do
let schemaName = Schema.getSchemaName testEnvironment
actual :: IO Value
actual =
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[ ("X-Hasura-Role", "hr_manager"),
("X-Hasura-Manager-Id", "3")
]
[graphql|
query {
#{schemaName}_employee(distinct_on: [nationality], order_by: [{nationality: asc}, {id: asc}]) {
id
first_name
last_name
nationality
}
}
|]
-- Althea Weiss can only see the nationality of the employees she is HR manager for.
-- This is because the 'manager_employee_private_info' role provides access to the nationality
-- for the current manager's HR-managed employees, but the rest of the employees
-- are accessed via 'all_managers', which does not expose 'nationality'.
-- So when Althea performs a distinct_on nationality, the distinct should be done over the
-- values of nationality after redaction, so only the first redacted nationality row gets kept
expected :: Value
expected =
[interpolateYaml|
data:
#{schemaName}_employee:
- id: 1
first_name: David
last_name: Holden
nationality: Australian
- id: 3
first_name: Xin
last_name: Cheng
nationality: Chinese
- id: 2
first_name: Grant
last_name: Smith
nationality: null
|]
shouldReturnYaml testEnvironment actual expected

View File

@ -592,7 +592,8 @@ fromSelectArgsG selectArgsG = do
Args
{ argsJoins = toList (fmap unfurledJoin joins),
argsOrderBy = NE.nonEmpty argsOrderBy,
argsDistinct = mdistinct,
-- TODO(caseBoolExp): Deal with the redaction expressions in distinct
argsDistinct = fmap Ir._adcColumn <$> mdistinct,
..
}
where
@ -625,7 +626,7 @@ unfurlAnnotatedOrderByElement ::
Ir.AnnotatedOrderByElement 'BigQuery Expression -> WriterT (Seq UnfurledJoin) (ReaderT EntityAlias FromIr) FieldName
unfurlAnnotatedOrderByElement =
\case
Ir.AOCColumn columnInfo -> lift (fromColumnInfo columnInfo)
Ir.AOCColumn columnInfo _redactionExp -> lift (fromColumnInfo columnInfo) -- TODO(caseBoolExp): Use this redaction expression
Ir.AOCObjectRelation Rql.RelInfo {riTarget = Rql.RelTargetNativeQuery _} _annBoolExp _annOrderByElementG ->
error "unfurlAnnotatedOrderByElement RelTargetNativeQuery"
Ir.AOCObjectRelation Rql.RelInfo {riMapping = mapping, riTarget = Rql.RelTargetTable tableName} annBoolExp annOrderByElementG -> do
@ -686,7 +687,8 @@ unfurlAnnotatedOrderByElement =
(const (fromAlias selectFrom))
( case annAggregateOrderBy of
Ir.AAOCount -> pure (CountAggregate StarCountable)
Ir.AAOOp text _resultType columnInfo -> do
-- TODO(caseBoolExp): Deal with the redaction expression
Ir.AAOOp (Ir.AggregateOrderByColumn text _resultType columnInfo _redactionExp) -> do
fieldName <- fromColumnInfo columnInfo
pure (OpAggregate text (ColumnExpression fieldName))
)

View File

@ -375,9 +375,9 @@ bqCountTypeInput = \case
mkCountType :: IR.CountDistinct -> Maybe [(Column 'BigQuery, Maybe (AnnColumnCaseBoolExpUnpreparedValue 'BigQuery))] -> CountType 'BigQuery (IR.UnpreparedValue 'BigQuery)
mkCountType _ Nothing = Const $ BigQuery.StarCountable
mkCountType IR.SelectCountDistinct (Just cols) =
maybe (Const BigQuery.StarCountable) (Const . BigQuery.DistinctCountable) $ nonEmpty (fst <$> cols) -- TODO(caseBoolExp): Deal with censorship expressions
maybe (Const BigQuery.StarCountable) (Const . BigQuery.DistinctCountable) $ nonEmpty (fst <$> cols) -- TODO(caseBoolExp): Deal with redaction expressions
mkCountType IR.SelectCountNonDistinct (Just cols) =
maybe (Const BigQuery.StarCountable) (Const . BigQuery.NonNullFieldCountable) $ nonEmpty (fst <$> cols) -- TODO(caseBoolExp): Deal with censorship expressions
maybe (Const BigQuery.StarCountable) (Const . BigQuery.NonNullFieldCountable) $ nonEmpty (fst <$> cols) -- TODO(caseBoolExp): Deal with redaction expressions
geographyWithinDistanceInput ::
forall m n r.

View File

@ -233,7 +233,8 @@ translateOrderByElement ::
AnnotatedOrderByElement 'DataConnector (UnpreparedValue 'DataConnector) ->
CPS.WriterT writerOutput m (API.OrderByElement, HashMap API.RelationshipName API.OrderByRelation)
translateOrderByElement sessionVariables sourceName orderDirection targetReversePath = \case
AOCColumn (ColumnInfo {..}) ->
-- TODO(caseBoolExp): Deal with this redaction expressions
AOCColumn ColumnInfo {..} _redactionExp ->
pure
( API.OrderByElement
{ _obeTargetPath = reverse targetReversePath,
@ -255,9 +256,10 @@ translateOrderByElement sessionVariables sourceName orderDirection targetReverse
orderByTarget <- case aggregateOrderByElement of
AAOCount ->
pure API.OrderByStarCountAggregate
AAOOp aggFunctionTxt resultType ColumnInfo {..} -> do
aggFunction <- lift $ translateSingleColumnAggregateFunction aggFunctionTxt
let resultScalarType = Witch.from $ columnTypeToScalarType resultType
AAOOp AggregateOrderByColumn {_aobcColumn = ColumnInfo {..}, ..} -> do
-- TODO(caseBoolExp): Deal with the redaction expression: _aobcCaseBoolExpression
aggFunction <- lift $ translateSingleColumnAggregateFunction _aobcAggregateFunctionName
let resultScalarType = Witch.from $ columnTypeToScalarType _aobcAggregateFunctionReturnType
pure . API.OrderBySingleColumnAggregate $ API.SingleColumnAggregate aggFunction (Witch.from ciColumn) resultScalarType
let translatedOrderByElement =
@ -346,6 +348,7 @@ translateAnnField sessionVariables sourceTableName = \case
AFNestedArray _ (ANASAggregate _) ->
pure Nothing -- TODO(dmoverton): support nested array aggregates
AFColumn colField ->
-- TODO(caseBoolExp): Deal with the redaction expression: _acfCaseBoolExpression
-- 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)
AFObjectRelation objRel ->
@ -481,9 +484,9 @@ translateAggregateField fieldPrefix fieldName = \case
let aggregate =
case countAggregate of
StarCount -> API.StarCount
-- TODO(caseBoolExp): Do something with these censor expressions
ColumnCount (column, _censorExp) -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = False}
ColumnDistinctCount (column, _censorExp) -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = True}
-- TODO(caseBoolExp): 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
AFOp AggregateOp {..} -> do
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
@ -491,8 +494,8 @@ translateAggregateField fieldPrefix fieldName = \case
fmap (HashMap.fromList . catMaybes) . forM _aoFields $ \(columnFieldName, columnField) ->
case columnField of
-- TODO(caseBoolExp): Do something with the censorExp
SFCol column resultType _censorExp ->
-- TODO(caseBoolExp): Do something with the redactionExp
SFCol column resultType _redactionExp ->
let resultScalarType = Witch.from $ columnTypeToScalarType resultType
in pure . Just $ (applyPrefix fieldPrefix' columnFieldName, API.SingleColumn $ API.SingleColumnAggregate aggFunction (Witch.from column) resultScalarType)
SFExp _txt ->

View File

@ -584,7 +584,7 @@ fromAggregateField alias aggregateField =
let projections :: [Projection] =
fields <&> \(fieldName, columnField) ->
case columnField of
-- TODO(caseBoolExp): Deal with censor expression?
-- TODO(caseBoolExp): Deal with redaction expression?
IR.SFCol column _columnType _caseBoolExp ->
let fname = columnFieldAggEntity column
in AggregateProjection $ Aliased (OpAggregate op [ColumnExpression fname]) (IR.getFieldNameTxt fieldName)
@ -972,7 +972,8 @@ unfurlAnnotatedOrderByElement ::
WriterT (Seq UnfurledJoin) (ReaderT EntityAlias FromIr) (FieldName, Maybe TSQL.ScalarType)
unfurlAnnotatedOrderByElement =
\case
IR.AOCColumn columnInfo -> do
-- TODO(caseBoolExp): Use the redaction expression
IR.AOCColumn columnInfo _redactionExp -> do
fieldName <- lift (fromColumnInfo columnInfo)
pure
( fieldName,
@ -1009,7 +1010,8 @@ unfurlAnnotatedOrderByElement =
(const (fromAlias selectFrom))
( case annAggregateOrderBy of
IR.AAOCount -> pure (CountAggregate StarCountable)
IR.AAOOp text _resultType columnInfo -> do
-- TODO(caseBoolExp): Use the redaction expression
IR.AAOOp (IR.AggregateOrderByColumn text _resultType columnInfo _redactionExp) -> do
fieldName <- fromColumnInfo columnInfo
pure (OpAggregate text (pure (ColumnExpression fieldName)))
)

View File

@ -404,8 +404,8 @@ msCountTypeInput = \case
where
mkCountType :: IR.CountDistinct -> Maybe (Column 'MSSQL, Maybe (AnnColumnCaseBoolExpUnpreparedValue 'MSSQL)) -> CountType 'MSSQL (UnpreparedValue 'MSSQL)
mkCountType _ Nothing = Const MSSQL.StarCountable
mkCountType IR.SelectCountDistinct (Just (col, _censorExp)) = Const $ MSSQL.DistinctCountable col -- TODO(caseBoolExp): Deal with censorship expressions
mkCountType IR.SelectCountNonDistinct (Just (col, _censorExp)) = Const $ MSSQL.NonNullFieldCountable col -- TODO(caseBoolExp): Deal with censorship expressions
mkCountType IR.SelectCountDistinct (Just (col, _redactionExp)) = Const $ MSSQL.DistinctCountable col -- TODO(caseBoolExp): Deal with redaction expressions
mkCountType IR.SelectCountNonDistinct (Just (col, _redactionExp)) = Const $ MSSQL.NonNullFieldCountable col -- TODO(caseBoolExp): Deal with redaction expressions
msParseUpdateOperators ::
forall m n r.

View File

@ -140,8 +140,8 @@ tableInsertMatchColumnsEnum tableInfo = do
[ ( define $ ciName column,
ciColumn column
)
| -- TODO(caseBoolExp): Does the censor expression need to be considered here?
(SCIScalarColumn column, _censorExp) <- columns,
| -- TODO(caseBoolExp): Does the redaction expression need to be considered here?
(SCIScalarColumn column, _redactionExp) <- columns,
isMatchColumnValid column
]
where

View File

@ -174,7 +174,7 @@ selectFunctionConnection mkRootFieldName fi@FunctionInfo {..} description pkeyCo
lift do
let fieldName = runMkRootFieldName mkRootFieldName $ _fiGQLName <> Name.__connection
stringifyNumbers <- retrieve Options.soStringifyNumbers
tableConnectionArgsParser <- tableConnectionArgs pkeyColumns returnTableInfo
tableConnectionArgsParser <- tableConnectionArgs pkeyColumns returnTableInfo selectPermissions
functionArgsParser <- customSQLFunctionArgs fi _fiGQLName _fiGQLArgsName
let argsParser = liftA2 (,) functionArgsParser tableConnectionArgsParser
pure

View File

@ -34,7 +34,7 @@ import Hasura.RQL.Types.Relationships.Local
mkAnnOrderByAlias ::
TableIdentifier -> FieldName -> SimilarArrayFields -> AnnotatedOrderByElement ('Postgres pgKind) v -> S.ColumnAlias
mkAnnOrderByAlias tablePrefix parAls similarFields = \case
AOCColumn pgColumnInfo ->
AOCColumn pgColumnInfo _redactionExp ->
let pgColumn = ciColumn pgColumnInfo
obColAls = contextualizeBaseTableColumn tablePrefix pgColumn
in obColAls
@ -55,7 +55,7 @@ mkAnnOrderByAlias tablePrefix parAls similarFields = \case
AOCComputedField cfOrderBy ->
let fieldName = fromComputedField $ _cfobName cfOrderBy
in case _cfobOrderByElement cfOrderBy of
CFOBEScalar _ -> S.tableIdentifierToColumnAlias $ mkComputedFieldTableIdentifier tablePrefix fieldName
CFOBEScalar _ _redactionExp -> S.tableIdentifierToColumnAlias $ mkComputedFieldTableIdentifier tablePrefix fieldName
CFOBETableAggregation _ _ aggOrderBy ->
let cfPfx = mkComputedFieldTableIdentifier tablePrefix fieldName
obAls = S.tableIdentifierToColumnAlias cfPfx <> "." <> mkAggregateOrderByAlias aggOrderBy
@ -87,11 +87,11 @@ contextualizeAggregateInput :: TableIdentifier -> FieldName -> FieldName -> S.Co
contextualizeAggregateInput pfx aggregateField field =
S.tableIdentifierToColumnAlias pfx <> ".ai." <> S.toColumnAlias aggregateField <> "." <> S.toColumnAlias field
mkAggregateOrderByAlias :: AnnotatedAggregateOrderBy ('Postgres pgKind) -> S.ColumnAlias
mkAggregateOrderByAlias :: AnnotatedAggregateOrderBy ('Postgres pgKind) v -> S.ColumnAlias
mkAggregateOrderByAlias =
(S.toColumnAlias . Identifier) . \case
AAOCount -> "count"
AAOOp opText _resultType col -> opText <> "." <> getPGColTxt (ciColumn col)
AAOOp AggregateOrderByColumn {..} -> _aobcAggregateFunctionName <> "." <> getPGColTxt (ciColumn _aobcColumn)
mkOrderByFieldName :: (ToTxt a) => a -> FieldName
mkOrderByFieldName name =

View File

@ -50,7 +50,7 @@ aggregateFieldsToExtractorExps sourcePrefix aggregateFields =
AFExp _ -> []
where
colsToExps :: [(PGCol, Maybe (AnnColumnCaseBoolExp ('Postgres pgKind) S.SQLExp))] -> [(S.ColumnAlias, S.SQLExp)]
colsToExps = fmap (\(col, censorExp) -> mkColumnExp censorExp col)
colsToExps = fmap (\(col, redactionExp) -> mkColumnExp redactionExp col)
-- Extract the columns and computed fields we need
colToMaybeExp ::
@ -69,7 +69,8 @@ aggregateFieldsToExtractorExps sourcePrefix aggregateFields =
(S.ColumnAlias, S.SQLExp)
mkColumnExp maybeCaseBoolExp column =
let baseTableIdentifier = mkBaseTableIdentifier sourcePrefix
qualifiedColumn = withColumnCaseBoolExp baseTableIdentifier maybeCaseBoolExp $ S.mkQIdenExp baseTableIdentifier (toIdentifier column)
baseTableQual = S.QualifiedIdentifier baseTableIdentifier Nothing
qualifiedColumn = withColumnCaseBoolExp baseTableQual maybeCaseBoolExp $ S.mkQIdenExp baseTableIdentifier (toIdentifier column)
columnAlias = contextualizeBaseTableColumn sourcePrefix column
in (S.toColumnAlias columnAlias, qualifiedColumn)
@ -81,7 +82,7 @@ mkAggregateOrderByExtractorAndFields ::
forall pgKind.
(Backend ('Postgres pgKind)) =>
TableIdentifier ->
AnnotatedAggregateOrderBy ('Postgres pgKind) ->
AnnotatedAggregateOrderBy ('Postgres pgKind) S.SQLExp ->
(S.Extractor, AggregateFields ('Postgres pgKind) S.SQLExp)
mkAggregateOrderByExtractorAndFields sourcePrefix annAggOrderBy =
case annAggOrderBy of
@ -89,7 +90,7 @@ mkAggregateOrderByExtractorAndFields sourcePrefix annAggOrderBy =
( S.Extractor S.countStar alias,
[(FieldName "count", AFCount $ CountAggregate S.CTStar)]
)
AAOOp opText _resultType pgColumnInfo ->
AAOOp (AggregateOrderByColumn opText _resultType pgColumnInfo redactionExp) ->
let pgColumn = ciColumn pgColumnInfo
pgType = ciType pgColumnInfo
in ( S.Extractor (S.SEFnApp opText [S.SEQIdentifier $ columnToQIdentifier pgColumn] Nothing) alias,
@ -98,7 +99,7 @@ mkAggregateOrderByExtractorAndFields sourcePrefix annAggOrderBy =
$ AggregateOp
opText
[ ( fromCol @('Postgres pgKind) pgColumn,
SFCol pgColumn pgType Nothing -- TODO(caseBoolExp): This might need censoring too?
SFCol pgColumn pgType redactionExp
)
]
)
@ -125,7 +126,7 @@ mkRawComputedFieldExpression sourcePrefix (ComputedFieldScalarSelect fn args _ c
-- evaluates to. `caseBoolExpMaybe` will be set only in the
-- case of an inherited role.
-- See [SQL generation for inherited role]
withColumnCaseBoolExp (mkBaseTableIdentifier sourcePrefix) maybeCaseBoolExp
withColumnCaseBoolExp (S.QualifiedIdentifier (mkBaseTableIdentifier sourcePrefix) Nothing) maybeCaseBoolExp
$ withColumnOp colOpM
$ S.SEFunction
$ S.FunctionExp fn (fromTableRowArgs sourcePrefix args) Nothing
@ -221,18 +222,18 @@ withColumnOp colOpM sqlExp = case colOpM of
withColumnCaseBoolExp ::
(Backend ('Postgres pgKind)) =>
TableIdentifier ->
S.Qual ->
Maybe (AnnColumnCaseBoolExp ('Postgres pgKind) S.SQLExp) ->
S.SQLExp ->
S.SQLExp
withColumnCaseBoolExp tableIdentifier maybeCaseBoolExp sqlExpression =
withColumnCaseBoolExp tableQual maybeCaseBoolExp sqlExpression =
-- Check out [SQL generation for inherited role]
case maybeCaseBoolExp of
Nothing -> sqlExpression
Just caseBoolExp ->
let boolExp =
S.simplifyBoolExp
$ toSQLBoolExp (S.QualifiedIdentifier tableIdentifier Nothing)
$ toSQLBoolExp tableQual
$ _accColCaseBoolExpField
<$> caseBoolExp
in S.SECond boolExp sqlExpression S.SENull

View File

@ -31,6 +31,7 @@ import Hasura.Backends.Postgres.Translate.Select.Internal.Aliases
import Hasura.Backends.Postgres.Translate.Select.Internal.Extractor
( aggregateFieldsToExtractorExps,
mkAggregateOrderByExtractorAndFields,
withColumnCaseBoolExp,
)
import Hasura.Backends.Postgres.Translate.Select.Internal.Helpers (fromTableRowArgs)
import Hasura.Backends.Postgres.Translate.Select.Internal.JoinTree
@ -80,17 +81,18 @@ processOrderByItems ::
Backend ('Postgres pgKind)
) =>
TableIdentifier ->
S.Qual ->
FieldName ->
SimilarArrayFields ->
Maybe (NE.NonEmpty PGCol) ->
Maybe (NE.NonEmpty (AnnDistinctColumn ('Postgres pgKind) S.SQLExp)) ->
Maybe (NE.NonEmpty (AnnotatedOrderByItem ('Postgres pgKind))) ->
m
( [(S.ColumnAlias, S.SQLExp)], -- Order by Extractors
SelectSorting,
Maybe S.SQLExp -- The cursor expression
)
processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \case
Nothing -> pure ([], NoSorting $ applyDistinctOnAtBase <$> distOnCols, Nothing)
processOrderByItems sourcePrefix' selectSourceQual fieldAlias' similarArrayFields distOnCols = \case
Nothing -> pure ([], NoSorting $ applyDistinctOnAtBase selectSourceQual <$> distOnCols, Nothing)
Just orderByItems -> do
orderByItemExps <- forM orderByItems processAnnOrderByItem
let (sorting, distinctOnExtractors) = generateSorting orderByItemExps
@ -100,21 +102,28 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \c
where
processAnnOrderByItem ::
AnnotatedOrderByItem ('Postgres pgKind) ->
m (OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) (SQLExpression ('Postgres pgKind)), (S.ColumnAlias, SQLExpression ('Postgres pgKind))))
m
( OrderByItemG
('Postgres pgKind)
( AnnotatedOrderByElement ('Postgres pgKind) S.SQLExp,
(S.ColumnAlias, S.SQLExp)
)
)
processAnnOrderByItem orderByItem =
forM orderByItem $ \ordByCol ->
(ordByCol,)
<$> processAnnotatedOrderByElement sourcePrefix' fieldAlias' ordByCol
processAnnotatedOrderByElement ::
TableIdentifier -> FieldName -> AnnotatedOrderByElement ('Postgres pgKind) S.SQLExp -> m (S.ColumnAlias, (SQLExpression ('Postgres pgKind)))
TableIdentifier -> FieldName -> AnnotatedOrderByElement ('Postgres pgKind) S.SQLExp -> m (S.ColumnAlias, S.SQLExp)
processAnnotatedOrderByElement sourcePrefix fieldAlias annObCol = do
let ordByAlias = mkAnnOrderByAlias sourcePrefix fieldAlias similarArrayFields annObCol
let baseTableIdentifier = mkBaseTableIdentifier sourcePrefix
(ordByAlias,) <$> case annObCol of
AOCColumn pgColInfo ->
AOCColumn pgColInfo redactionExp ->
pure
$ S.mkQIdenExp (mkBaseTableIdentifier sourcePrefix)
$ toIdentifier
$ withColumnCaseBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) redactionExp
$ S.mkQIdenExp baseTableIdentifier
$ ciColumn pgColInfo
AOCObjectRelation relInfo relFilter rest -> withWriteObjectRelation $ do
let RelInfo {riName = relName, riMapping = colMapping, riTarget = relTarget} = relInfo
@ -165,10 +174,12 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \c
)
AOCComputedField ComputedFieldOrderBy {..} ->
case _cfobOrderByElement of
CFOBEScalar _ -> do
CFOBEScalar _ redactionExp -> do
let functionArgs = fromTableRowArgs sourcePrefix _cfobFunctionArgsExp
functionExp = S.FunctionExp _cfobFunction functionArgs Nothing
pure $ S.SEFunction functionExp
pure
$ withColumnCaseBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) redactionExp
$ S.SEFunction functionExp
CFOBETableAggregation _ tableFilter aggOrderBy -> withWriteComputedFieldTableSet $ do
let fieldName = mkOrderByFieldName _cfobName
computedFieldSourcePrefix = mkComputedFieldTableIdentifier sourcePrefix fieldName
@ -198,19 +209,19 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \c
$ S.mkFunctionAlias qf Nothing
generateSorting ::
NE.NonEmpty (OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) (SQLExpression ('Postgres pgKind)), (S.ColumnAlias, SQLExpression ('Postgres pgKind)))) ->
NE.NonEmpty (OrderByItemG ('Postgres pgKind) (AnnotatedOrderByElement ('Postgres pgKind) S.SQLExp, (S.ColumnAlias, S.SQLExp))) ->
( SelectSorting,
[(S.ColumnAlias, SQLExpression ('Postgres pgKind))] -- 'distinct on' column extractors
[(S.ColumnAlias, S.SQLExp)] -- 'distinct on' column extractors
)
generateSorting orderByExps@(firstOrderBy NE.:| restOrderBys) =
case fst $ obiColumn firstOrderBy of
AOCColumn columnInfo ->
AOCColumn columnInfo redactionExp ->
-- If rest order by expressions are all columns then apply order by clause at base selection.
if all (isJust . getColumnOrderBy . obiColumn) restOrderBys
then -- Collect column order by expressions from the rest.
let restColumnOrderBys = mapMaybe (traverse getColumnOrderBy) restOrderBys
firstColumnOrderBy = firstOrderBy {obiColumn = columnInfo}
firstColumnOrderBy = firstOrderBy {obiColumn = (columnInfo, redactionExp)}
in sortAtNodeAndBase $ firstColumnOrderBy NE.:| restColumnOrderBys
else -- Else rest order by expressions contain atleast one non-column order by.
-- So, apply order by clause at node selection.
@ -233,13 +244,13 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \c
(Sorting $ ASorting (nodeOrderBy, nodeDistinctOn) Nothing, nodeDistinctOnExtractors)
sortAtNodeAndBase baseColumnOrderBys =
let mkBaseOrderByItem (OrderByItemG orderByType columnInfo nullsOrder) =
S.OrderByItem
(S.SEIdentifier $ toIdentifier $ ciColumn columnInfo)
orderByType
nullsOrder
let mkBaseOrderByItem (OrderByItemG orderByType (columnInfo, redactionExp) nullsOrder) =
let columnExp =
withColumnCaseBoolExp selectSourceQual redactionExp
$ S.mkSIdenExp (ciColumn columnInfo)
in S.OrderByItem columnExp orderByType nullsOrder
baseOrderByExp = S.OrderByExp $ mkBaseOrderByItem <$> baseColumnOrderBys
baseDistOnExp = applyDistinctOnAtBase <$> distOnCols
baseDistOnExp = applyDistinctOnAtBase selectSourceQual <$> distOnCols
sorting = Sorting $ ASorting (nodeOrderBy, nodeDistinctOn) $ Just (baseOrderByExp, baseDistOnExp)
in (sorting, nodeDistinctOnExtractors)
@ -255,13 +266,13 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \c
where
mkAggOrderByValExp valExp = \case
AAOCount -> [S.SELit "count", valExp]
AAOOp opText _resultType colInfo ->
AAOOp (AggregateOrderByColumn opText _resultType colInfo _redactionExp) ->
[ S.SELit opText,
S.applyJsonBuildObj [S.SELit $ getPGColTxt $ ciColumn colInfo, valExp]
]
annObColToJSONField valExp = \case
AOCColumn pgCol -> [S.SELit $ getPGColTxt $ ciColumn pgCol, valExp]
AOCColumn pgCol _redactionExp -> [S.SELit $ getPGColTxt $ ciColumn pgCol, valExp]
AOCObjectRelation relInfo _ obCol ->
[ S.SELit $ relNameToTxt $ riName relInfo,
S.applyJsonBuildObj $ annObColToJSONField valExp obCol
@ -273,27 +284,42 @@ processOrderByItems sourcePrefix' fieldAlias' similarArrayFields distOnCols = \c
AOCComputedField cfOrderBy ->
let fieldNameText = computedFieldNameToText $ _cfobName cfOrderBy
in case _cfobOrderByElement cfOrderBy of
CFOBEScalar _ -> [S.SELit fieldNameText, valExp]
CFOBEScalar _ _redactionExp -> [S.SELit fieldNameText, valExp]
CFOBETableAggregation _ _ aggOrderBy ->
[ S.SELit $ fieldNameText <> "_aggregate",
S.applyJsonBuildObj $ mkAggOrderByValExp valExp aggOrderBy
]
applyDistinctOnAtBase ::
NE.NonEmpty PGCol -> S.DistinctExpr
applyDistinctOnAtBase =
S.DistinctOn . map (S.SEIdentifier . toIdentifier) . toList
forall pgKind.
(Backend ('Postgres pgKind)) =>
S.Qual ->
NE.NonEmpty (AnnDistinctColumn ('Postgres pgKind) S.SQLExp) ->
S.DistinctExpr
applyDistinctOnAtBase selectSourceQual distinctColumns =
let distinctExps =
distinctColumns
& toList
<&> (\AnnDistinctColumn {..} -> withColumnCaseBoolExp selectSourceQual _adcCaseBoolExpression $ S.mkSIdenExp _adcColumn)
in S.DistinctOn distinctExps
applyDistinctOnAtNode ::
forall pgKind.
(Backend ('Postgres pgKind)) =>
TableIdentifier ->
NE.NonEmpty PGCol ->
NE.NonEmpty (AnnDistinctColumn ('Postgres pgKind) S.SQLExp) ->
( S.DistinctExpr,
[(S.ColumnAlias, S.SQLExp)] -- additional column extractors
)
applyDistinctOnAtNode pfx neCols = (distOnExp, colExtrs)
applyDistinctOnAtNode pfx distinctColumns = (distinctOnExp, extractors)
where
cols = toList neCols
distOnExp = S.DistinctOn $ map (S.SEIdentifier . toIdentifier . mkQColAls) cols
mkQCol c = S.mkQIdenExp (mkBaseTableIdentifier pfx) $ toIdentifier c
mkQColAls = contextualizeBaseTableColumn pfx
colExtrs = flip map cols $ mkQColAls &&& mkQCol
columns = _adcColumn <$> toList distinctColumns
distinctOnExp = S.DistinctOn $ map (S.SEIdentifier . toIdentifier . mkQColAlias) columns
baseTableIdentifier = mkBaseTableIdentifier pfx
mkExtractor AnnDistinctColumn {..} =
let extractorExp =
withColumnCaseBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) _adcCaseBoolExpression
$ S.mkQIdenExp baseTableIdentifier _adcColumn
in (mkQColAlias _adcColumn, extractorExp)
mkQColAlias = contextualizeBaseTableColumn pfx
extractors = mkExtractor <$> toList distinctColumns

View File

@ -116,12 +116,12 @@ processSelectParams
permLimitSubQ
tablePermissions
tableArgs = do
(additionalExtrs, selectSorting, cursorExp) <-
processOrderByItems (identifierToTableIdentifier thisSourcePrefix) fieldAlias similarArrFields distM orderByM
let prefix = identifierToTableIdentifier $ _pfBase sourcePrefixes
(whereSource, fromItem) <- selectFromToQual prefix selectFrom
(selectSourceQual, fromItem) <- selectFromToQual prefix selectFrom
(additionalExtrs, selectSorting, cursorExp) <-
processOrderByItems (identifierToTableIdentifier thisSourcePrefix) selectSourceQual fieldAlias similarArrFields distM orderByM
let finalWhere =
toSQLBoolExp whereSource
toSQLBoolExp selectSourceQual
$ maybe permFilter (andAnnBoolExps permFilter) whereM
sortingAndSlicing = SortingAndSlicing selectSorting selectSlicing
selectSource =
@ -393,7 +393,7 @@ processAnnFields sourcePrefix fieldAlias annFields tCase = do
toSQLCol (AnnColumnField col typ asText colOpM caseBoolExpMaybe) = do
strfyNum <- ask
let sqlExpression =
withColumnCaseBoolExp baseTableIdentifier caseBoolExpMaybe
withColumnCaseBoolExp (S.QualifiedIdentifier baseTableIdentifier Nothing) caseBoolExpMaybe
$ withColumnOp colOpM
$ S.mkQIdenExp baseTableIdentifier col
pure $ toJSONableExp strfyNum typ asText tCase sqlExpression

View File

@ -83,7 +83,7 @@ mkStreamSQLSelect ::
mkStreamSQLSelect (AnnSelectStreamG () fields from perm args strfyNum) = do
let cursorArg = _ssaCursorArg args
cursorColInfo = _sciColInfo cursorArg
annOrderbyCol = AOCColumn cursorColInfo
annOrderbyCol = AOCColumn cursorColInfo Nothing -- TODO(caseBoolExp): Does a redaction expression need to go here?
basicOrderType =
bool S.OTAsc S.OTDesc $ _sciOrdering cursorArg == CODescending
orderByItems =

View File

@ -14,8 +14,8 @@ import Hasura.RQL.Types.BackendType
-- | This newtype allows us to reuse 'S.CountType' for the 'Backend.CountType' type family
-- We reuse the overall structure, but our column type is a PGCol column name, plus
-- the column censorship expression used by inherited roles.
-- See [SQL generation for inherited roles] for more information about column censorship
-- the column redaction expression used by inherited roles.
-- See [SQL generation for inherited roles] for more information about column redaction
newtype CountAggregate pgKind v = CountAggregate {getCountType :: S.CountType (PGCol, Maybe (AnnColumnCaseBoolExp ('Postgres pgKind) v))}
deriving stock (Generic)

View File

@ -102,7 +102,7 @@ defaultAggregationPredicatesParser aggFns ti = runMaybeT do
ArgumentsStar ->
maybe AggregationPredicateArgumentsStar AggregationPredicateArguments
. nonEmpty
-- TODO(caseBoolExp): Probably need to deal with the censorship expressions from tableSelectColumnsEnum here
-- TODO(caseBoolExp): Probably need to deal with the redaction expressions from tableSelectColumnsEnum here
<$> fuse (fieldOptionalDefault Name._arguments Nothing [] . P.list . fmap fst <$> fails (tableSelectColumnsEnum relTable))
SingleArgument typ ->
AggregationPredicateArguments

View File

@ -7,7 +7,7 @@ module Hasura.GraphQL.Schema.OrderBy
)
where
import Control.Lens ((^?), _1)
import Control.Lens ((^?))
import Data.Has
import Data.HashMap.Strict.Extended qualified as HashMap
import Data.Text.Casing qualified as C
@ -25,11 +25,12 @@ import Hasura.GraphQL.Schema.Parser
import Hasura.GraphQL.Schema.Parser qualified as P
import Hasura.GraphQL.Schema.Table
import Hasura.GraphQL.Schema.Typename
import Hasura.LogicalModel.Cache (LogicalModelInfo (_lmiFields, _lmiName))
import Hasura.LogicalModel.Common (columnsFromFields, toFieldInfo)
import Hasura.LogicalModel.Cache (LogicalModelInfo (..))
import Hasura.LogicalModel.Common (columnsFromFields, getSelPermInfoForLogicalModel, toFieldInfo)
import Hasura.LogicalModel.Types (LogicalModelName (..))
import Hasura.Name qualified as Name
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp (AnnColumnCaseBoolExpUnpreparedValue)
import Hasura.RQL.IR.OrderBy qualified as IR
import Hasura.RQL.IR.Select qualified as IR
import Hasura.RQL.IR.Value qualified as IR
@ -76,18 +77,20 @@ logicalModelOrderByExp ::
) =>
LogicalModelInfo b ->
SchemaT r m (Parser 'Input n [IR.AnnotatedOrderByItemG b (IR.UnpreparedValue b)])
logicalModelOrderByExp logicalModel =
logicalModelOrderByExp logicalModel = do
roleName <- retrieve scRole
let name = getLogicalModelName (_lmiName logicalModel)
in case toFieldInfo (columnsFromFields $ _lmiFields logicalModel) of
Nothing -> throw500 $ "Error creating fields for logical model " <> tshow name
Just tableFields -> do
let description =
G.Description
$ "Ordering options when selecting data from "
<> name
<<> "."
memoizeKey = name
orderByExpInternal (C.fromCustomName name) description tableFields memoizeKey
selectPermissions = getSelPermInfoForLogicalModel roleName logicalModel
case toFieldInfo (columnsFromFields $ _lmiFields logicalModel) of
Nothing -> throw500 $ "Error creating fields for logical model " <> tshow name
Just tableFields -> do
let description =
G.Description
$ "Ordering options when selecting data from "
<> name
<<> "."
memoizeKey = name
orderByExpInternal (C.fromCustomName name) description selectPermissions tableFields memoizeKey
-- | Corresponds to an object type for an order by.
--
@ -104,10 +107,11 @@ orderByExpInternal ::
(Ord name, Typeable name, MonadBuildSchema b r m n) =>
C.GQLNameIdentifier ->
G.Description ->
Maybe (SelPermInfo b) ->
[FieldInfo b] ->
name ->
SchemaT r m (Parser 'Input n [IR.AnnotatedOrderByItemG b (IR.UnpreparedValue b)])
orderByExpInternal gqlName description tableFields memoizeKey = do
orderByExpInternal gqlName description selectPermissions tableFields memoizeKey = do
sourceInfo <- asks getter
P.memoizeOn 'orderByExpInternal (_siName sourceInfo, memoizeKey) do
let customization = _siCustomization sourceInfo
@ -123,17 +127,17 @@ orderByExpInternal gqlName description tableFields memoizeKey = do
FieldInfo b ->
SchemaT r m (Maybe (InputFieldsParser n (Maybe [IR.AnnotatedOrderByItemG b (IR.UnpreparedValue b)])))
mkField sourceInfo tCase fieldInfo = runMaybeT $ do
selectPermissions' <- hoistMaybe selectPermissions
roleName <- retrieve scRole
case fieldInfo of
FIColumn (SCIScalarColumn columnInfo) -> do
let !fieldName = ciName columnInfo
pure
$ P.fieldOptional
fieldName
Nothing
(orderByOperator @b tCase sourceInfo)
<&> fmap (pure . mkOrderByItemG @b (IR.AOCColumn columnInfo))
. join
let redactionExp = join . HashMap.lookup (ciColumn columnInfo) $ spiCols selectPermissions'
let redactionExpUnpreparedValue = fmap (fmap partialSQLExpToUnpreparedValue) <$!> redactionExp
orderByOperator @b tCase sourceInfo
& P.fieldOptional fieldName Nothing
<&> (fmap (pure . mkOrderByItemG @b (IR.AOCColumn columnInfo redactionExpUnpreparedValue)) . join)
& pure
FIColumn (SCIObjectColumn _) -> empty -- TODO(dmoverton)
FIColumn (SCIArrayColumn _) -> empty -- TODO(dmoverton)
FIRelationship relationshipInfo -> do
@ -167,7 +171,9 @@ orderByExpInternal gqlName description tableFields memoizeKey = do
guard $ _cffInputArgs == mempty -- No input arguments other than table row and session argument
case computedFieldReturnType @b _cfiReturnType of
ReturnsScalar scalarType -> do
let computedFieldOrderBy = mkComputedFieldOrderBy $ IR.CFOBEScalar scalarType
let redactionExp = join . HashMap.lookup _cfiName $ spiComputedFields selectPermissions'
let redactionExpUnpreparedValue = fmap (fmap partialSQLExpToUnpreparedValue) <$!> redactionExp
let computedFieldOrderBy = mkComputedFieldOrderBy $ IR.CFOBEScalar scalarType redactionExpUnpreparedValue
pure
$ P.fieldOptional
fieldName
@ -211,15 +217,17 @@ tableOrderByExp ::
TableInfo b ->
SchemaT r m (Parser 'Input n [IR.AnnotatedOrderByItemG b (IR.UnpreparedValue b)])
tableOrderByExp tableInfo = do
roleName <- retrieve scRole
tableGQLName <- getTableIdentifierName tableInfo
tableFields <- tableSelectFields tableInfo
let selectPermissions = tableSelectPermissions roleName tableInfo
let description =
G.Description
$ "Ordering options when selecting data from "
<> tableInfoName tableInfo
<<> "."
memoizeKey = tableInfoName tableInfo
orderByExpInternal tableGQLName description tableFields memoizeKey
orderByExpInternal tableGQLName description selectPermissions tableFields memoizeKey
-- FIXME!
-- those parsers are directly using Postgres' SQL representation of
@ -230,7 +238,7 @@ orderByAggregation ::
(MonadBuildSchema b r m n) =>
SourceInfo b ->
TableInfo b ->
SchemaT r m (Parser 'Input n [IR.OrderByItemG b (IR.AnnotatedAggregateOrderBy b)])
SchemaT r m (Parser 'Input n [IR.OrderByItemG b (IR.AnnotatedAggregateOrderBy b (IR.UnpreparedValue b))])
orderByAggregation sourceInfo tableInfo = P.memoizeOn 'orderByAggregation (_siName sourceInfo, tableName) do
-- WIP NOTE
-- there is heavy duplication between this and Select.tableAggregationFields
@ -241,15 +249,14 @@ orderByAggregation sourceInfo tableInfo = P.memoizeOn 'orderByAggregation (_siNa
tCase = _rscNamingConvention customization
mkTypename = _rscTypeNames customization
tableIdentifierName <- getTableIdentifierName @b tableInfo
-- TODO(caseBoolExp): Probably need to deal with the censorship expressions here
allColumns <- mapMaybe (^? _1 . _SCIScalarColumn) <$> tableSelectColumns tableInfo
let numColumns = stdAggOpColumns tCase $ onlyNumCols allColumns
compColumns = stdAggOpColumns tCase $ onlyComparableCols allColumns
allScalarColumns <- mapMaybe (\(column, redactionExp) -> column ^? _SCIScalarColumn <&> (,redactionExp)) <$> tableSelectColumns tableInfo
let numColumns = stdAggOpColumns tCase $ filter (isNumCol . fst) allScalarColumns
compColumns = stdAggOpColumns tCase $ filter (isComparableCol . fst) allScalarColumns
numOperatorsAndColumns = HashMap.fromList $ (,numColumns) <$> numericAggOperators
compOperatorsAndColumns = HashMap.fromList $ (,compColumns) <$> comparisonAggOperators
customOperatorsAndColumns =
HashMap.mapKeys (C.fromCustomName)
$ getCustomAggOpsColumns tCase allColumns
$ getCustomAggOpsColumns tCase allScalarColumns
<$> getCustomAggregateOperators @b (_siConfiguration sourceInfo)
allOperatorsAndColumns =
HashMap.catMaybes
@ -282,50 +289,53 @@ orderByAggregation sourceInfo tableInfo = P.memoizeOn 'orderByAggregation (_siNa
stdAggOpColumns ::
NamingCase ->
[ColumnInfo b] ->
Maybe (InputFieldsParser n [(ColumnInfo b, ColumnType b, (BasicOrderType b, NullsOrderType b))])
[(ColumnInfo b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b))] ->
Maybe (InputFieldsParser n [(ColumnInfo b, ColumnType b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b), (BasicOrderType b, NullsOrderType b))])
stdAggOpColumns tCase columns =
columns
-- ALl std aggregate functions return the same type as the column used with it
& fmap (\colInfo -> (colInfo, ciType colInfo))
& fmap (\(colInfo, redactionExp) -> (colInfo, ciType colInfo, redactionExp))
& mkAgOpsFields tCase
-- Build an InputFieldsParser only if the column list is non-empty
mkAgOpsFields ::
NamingCase ->
-- Assoc list of column types with the type returned by the aggregate function when it is applied to that column
[(ColumnInfo b, ColumnType b)] ->
Maybe (InputFieldsParser n [(ColumnInfo b, ColumnType b, (BasicOrderType b, NullsOrderType b))])
[(ColumnInfo b, ColumnType b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b))] ->
Maybe (InputFieldsParser n [(ColumnInfo b, ColumnType b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b), (BasicOrderType b, NullsOrderType b))])
mkAgOpsFields tCase =
fmap (fmap (catMaybes . toList) . traverse (mkField tCase)) . nonEmpty
getCustomAggOpsColumns ::
NamingCase ->
-- All columns
[ColumnInfo b] ->
[(ColumnInfo b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b))] ->
-- Map of type the aggregate function accepts to the type it returns
HashMap (ScalarType b) (ScalarType b) ->
Maybe (InputFieldsParser n [(ColumnInfo b, ColumnType b, (BasicOrderType b, NullsOrderType b))])
Maybe (InputFieldsParser n [(ColumnInfo b, ColumnType b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b), (BasicOrderType b, NullsOrderType b))])
getCustomAggOpsColumns tCase allColumns typeMap =
allColumns
-- Filter by columns with a scalar type supported by this aggregate function
-- and retrieve the result type of the aggregate function
& mapMaybe
( \columnInfo ->
( \(columnInfo, redactionExp) ->
case ciType columnInfo of
ColumnEnumReference _ -> Nothing
ColumnScalar scalarType ->
(\resultType -> (columnInfo, ColumnScalar resultType)) <$> HashMap.lookup scalarType typeMap
(\resultType -> (columnInfo, ColumnScalar resultType, redactionExp)) <$> HashMap.lookup scalarType typeMap
)
& mkAgOpsFields tCase
mkField :: NamingCase -> (ColumnInfo b, ColumnType b) -> InputFieldsParser n (Maybe (ColumnInfo b, ColumnType b, (BasicOrderType b, NullsOrderType b)))
mkField tCase (columnInfo, resultType) =
mkField ::
NamingCase ->
(ColumnInfo b, ColumnType b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b)) ->
InputFieldsParser n (Maybe (ColumnInfo b, ColumnType b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b), (BasicOrderType b, NullsOrderType b)))
mkField tCase (columnInfo, resultType, redactionExp) =
P.fieldOptional
(ciName columnInfo)
(ciDescription columnInfo)
(orderByOperator @b tCase sourceInfo)
<&> fmap (columnInfo,resultType,)
<&> fmap (columnInfo,resultType,redactionExp,)
. join
parseOperator ::
@ -333,8 +343,8 @@ orderByAggregation sourceInfo tableInfo = P.memoizeOn 'orderByAggregation (_siNa
C.GQLNameIdentifier ->
C.GQLNameIdentifier ->
NamingCase ->
InputFieldsParser n [(ColumnInfo b, ColumnType b, (BasicOrderType b, NullsOrderType b))] ->
InputFieldsParser n (Maybe [IR.OrderByItemG b (IR.AnnotatedAggregateOrderBy b)])
InputFieldsParser n [(ColumnInfo b, ColumnType b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b), (BasicOrderType b, NullsOrderType b))] ->
InputFieldsParser n (Maybe [IR.OrderByItemG b (IR.AnnotatedAggregateOrderBy b (IR.UnpreparedValue b))])
parseOperator makeTypename operator tableGQLName tCase columns =
let opText = G.unName $ applyFieldNameCaseIdentifier tCase operator
opTypeName = applyTypeNameCaseIdentifier tCase $ mkTableAggregateOrderByOpTypeName tableGQLName operator
@ -342,7 +352,7 @@ orderByAggregation sourceInfo tableInfo = P.memoizeOn 'orderByAggregation (_siNa
objectName = runMkTypename makeTypename opTypeName
objectDesc = Just $ G.Description $ "order by " <> opText <> "() on columns of table " <>> tableName
in P.fieldOptional opFieldName Nothing (P.object objectName objectDesc columns)
`mapField` map (\(col, resultType, info) -> mkOrderByItemG (IR.AAOOp opText resultType col) info)
`mapField` map (\(col, resultType, redactionExp, info) -> mkOrderByItemG (IR.AAOOp $ IR.AggregateOrderByColumn opText resultType col redactionExp) info)
orderByOperatorsHasuraCase ::
forall b n.

View File

@ -184,7 +184,7 @@ selectTableConnection tableInfo fieldName description pkeyColumns = runMaybeT do
selectionSetParser <- fmap P.nonNullableParser <$> MaybeT $ tableConnectionSelectionSet tableInfo
lift $ P.memoizeOn 'selectTableConnection (_siName sourceInfo, tableName, fieldName) do
stringifyNumbers <- retrieve Options.soStringifyNumbers
selectArgsParser <- tableConnectionArgs pkeyColumns tableInfo
selectArgsParser <- tableConnectionArgs pkeyColumns tableInfo selectPermissions
pure
$ P.subselection fieldName description selectArgsParser selectionSetParser
<&> \((args, split, slice), fields) ->
@ -402,7 +402,7 @@ groupByKeyField tableInfo = do
tableColumnsEnumParser <- MaybeT $ tableSelectColumnsEnum tableInfo
let groupByKeyFields =
(\(column, _censorExp) -> IR.GKFColumn column)
(\(column, _redactionExp) -> IR.GKFColumn column)
<$> P.field columnFieldName (Just "A column grouping key") tableColumnsEnumParser
pure $ P.object groupByKeyTypeName (Just groupByKeyDescription) groupByKeyFields
where
@ -470,7 +470,7 @@ groupByKeySelectionSet tableInfo = do
tableGQLName <- getTableIdentifierName tableInfo
let groupByKeyFieldsTypeName = mkTypename $ applyTypeNameCaseIdentifier namingCase $ mkGroupByKeyFieldsTypeName tableGQLName
-- TODO(caseBoolExp): Probably need to deal with the censor expression here
-- TODO(caseBoolExp): Probably need to deal with the redaction expression here
scalarColumns <- mapMaybe (^? _1 . _SCIScalarColumn) <$> tableSelectColumns tableInfo
columnFieldParsers <-
for scalarColumns $ \columnInfo -> do
@ -835,11 +835,10 @@ tableDistinctArg ::
forall b r m n.
(MonadBuildSchema b r m n) =>
TableInfo b ->
SchemaT r m (InputFieldsParser n (Maybe (NonEmpty (Column b))))
SchemaT r m (InputFieldsParser n (Maybe (NonEmpty (IR.AnnDistinctColumn b (IR.UnpreparedValue b)))))
tableDistinctArg tableInfo = do
tCase <- retrieve $ _rscNamingConvention . _siCustomization @b
-- TODO(caseBoolExp): Probably need to deal with the censor expression here
columnsEnum <- fmap (fmap fst) <$> tableSelectColumnsEnum tableInfo
columnsEnum <- fmap (fmap (uncurry IR.AnnDistinctColumn)) <$> tableSelectColumnsEnum tableInfo
let distinctOnName = applyFieldNameCaseCust tCase Name._distinct_on
distinctOnDesc = Just $ G.Description "distinct select on columns"
pure do
@ -893,6 +892,7 @@ tableConnectionArgs ::
(MonadBuildSchema b r m n, AggregationPredicatesSchema b) =>
PrimaryKeyColumns b ->
TableInfo b ->
SelPermInfo b ->
SchemaT
r
m
@ -903,7 +903,7 @@ tableConnectionArgs ::
Maybe IR.ConnectionSlice
)
)
tableConnectionArgs pkeyColumns tableInfo = do
tableConnectionArgs pkeyColumns tableInfo selectPermissions = do
whereParser <- tableWhereArg tableInfo
orderByParser <- fmap (fmap appendPrimaryKeyOrderBy) <$> tableOrderByArg tableInfo
distinctParser <- tableDistinctArg tableInfo
@ -943,14 +943,17 @@ tableConnectionArgs pkeyColumns tableInfo = do
where
base64Text = base64Decode <$> P.string
appendPrimaryKeyOrderBy :: NonEmpty (IR.AnnotatedOrderByItemG b v) -> NonEmpty (IR.AnnotatedOrderByItemG b v)
appendPrimaryKeyOrderBy :: NonEmpty (IR.AnnotatedOrderByItemG b (IR.UnpreparedValue b)) -> NonEmpty (IR.AnnotatedOrderByItemG b (IR.UnpreparedValue b))
appendPrimaryKeyOrderBy orderBys@(h NE.:| t) =
let orderByColumnNames =
orderBys ^.. traverse . to IR.obiColumn . IR._AOCColumn . to ciColumn
orderBys ^.. traverse . to IR.obiColumn . IR._AOCColumn . _1 . to ciColumn
pkeyOrderBys = flip mapMaybe (toList pkeyColumns) $ \columnInfo ->
if ciColumn columnInfo `elem` orderByColumnNames
then Nothing
else Just $ IR.OrderByItemG Nothing (IR.AOCColumn columnInfo) Nothing
else
let redactionExp = join . HashMap.lookup (ciColumn columnInfo) $ spiCols selectPermissions
redactionExpUnpreparedValue = fmap (fmap partialSQLExpToUnpreparedValue) <$!> redactionExp
in Just $ IR.OrderByItemG Nothing (IR.AOCColumn columnInfo redactionExpUnpreparedValue) Nothing
in h NE.:| (t <> pkeyOrderBys)
parseConnectionSplit ::
@ -966,6 +969,8 @@ tableConnectionArgs pkeyColumns tableInfo = do
$ \columnInfo -> do
let columnJsonPath = [J.Key $ K.fromText $ toTxt $ ciColumn columnInfo]
columnType = ciType columnInfo
redactionExp = join . HashMap.lookup (ciColumn columnInfo) $ spiCols selectPermissions
redactionExpUnpreparedValue = fmap (fmap partialSQLExpToUnpreparedValue) <$!> redactionExp
columnValue <-
iResultToMaybe (executeJSONPath columnJsonPath cursorValue)
`onNothing` throwInvalidCursor
@ -973,7 +978,7 @@ tableConnectionArgs pkeyColumns tableInfo = do
let unresolvedValue = IR.UVParameter IR.FreshVar $ ColumnValue columnType pgValue
pure
$ IR.ConnectionSplit splitKind unresolvedValue
$ IR.OrderByItemG Nothing (IR.AOCColumn columnInfo) Nothing
$ IR.OrderByItemG Nothing (IR.AOCColumn columnInfo redactionExpUnpreparedValue) Nothing
Just orderBys ->
forM orderBys $ \orderBy -> do
let IR.OrderByItemG orderType annObCol nullsOrder = orderBy
@ -992,10 +997,10 @@ tableConnectionArgs pkeyColumns tableInfo = do
mkAggregateOrderByPath = \case
IR.AAOCount -> ["count"]
IR.AAOOp t _resultType col -> [t, toTxt $ ciColumn col]
IR.AAOOp IR.AggregateOrderByColumn {..} -> [_aobcAggregateFunctionName, toTxt $ ciColumn _aobcColumn]
getPathFromOrderBy = \case
IR.AOCColumn columnInfo ->
IR.AOCColumn columnInfo _redactionExp ->
let pathElement = toTxt $ ciColumn columnInfo
in [pathElement]
IR.AOCObjectRelation relInfo _ obCol ->
@ -1007,22 +1012,22 @@ tableConnectionArgs pkeyColumns tableInfo = do
IR.AOCComputedField cfob ->
let fieldNameText = computedFieldNameToText $ IR._cfobName cfob
in case IR._cfobOrderByElement cfob of
IR.CFOBEScalar _ -> [fieldNameText]
IR.CFOBEScalar _ _redactionExp -> [fieldNameText]
IR.CFOBETableAggregation _ _ aggOb ->
(fieldNameText <> "_aggregate") : mkAggregateOrderByPath aggOb
getOrderByColumnType = \case
IR.AOCColumn columnInfo -> ciType columnInfo
IR.AOCColumn columnInfo _redactionExp -> ciType columnInfo
IR.AOCObjectRelation _ _ obCol -> getOrderByColumnType obCol
IR.AOCArrayAggregation _ _ aggOb -> aggregateOrderByColumnType aggOb
IR.AOCComputedField cfob ->
case IR._cfobOrderByElement cfob of
IR.CFOBEScalar scalarType -> ColumnScalar scalarType
IR.CFOBEScalar scalarType _redactionExp -> ColumnScalar scalarType
IR.CFOBETableAggregation _ _ aggOb -> aggregateOrderByColumnType aggOb
where
aggregateOrderByColumnType = \case
IR.AAOCount -> ColumnScalar (aggregateOrderByCountType @b)
IR.AAOOp _ resultType _colInfo -> resultType
IR.AAOOp IR.AggregateOrderByColumn {..} -> _aobcAggregateFunctionReturnType
-- | Aggregation fields
--
@ -1051,7 +1056,7 @@ tableAggregationFields tableInfo = do
mkTypename = _rscTypeNames customization
P.memoizeOn 'tableAggregationFields (sourceName, tableName) do
tableGQLName <- getTableIdentifierName tableInfo
allScalarColumns <- mapMaybe (\(column, censorExp) -> column ^? _SCIScalarColumn <&> (,censorExp)) <$> tableSelectColumns tableInfo
allScalarColumns <- mapMaybe (\(column, redactionExp) -> column ^? _SCIScalarColumn <&> (,redactionExp)) <$> tableSelectColumns tableInfo
allComputedFields <-
if supportsAggregateComputedFields @b -- See 'supportsAggregateComputedFields' for an explanation
then tableSelectComputedFields tableInfo
@ -1134,11 +1139,11 @@ tableAggregationFields tableInfo = do
getCustomAggOpsColumns columnInfos typeMap =
columnInfos
& mapMaybe
( \(ci@ColumnInfo {..}, censorExp) ->
( \(ci@ColumnInfo {..}, redactionExp) ->
case ciType of
ColumnEnumReference _ -> Nothing
ColumnScalar scalarType ->
((ci, censorExp),) <$> HashMap.lookup scalarType typeMap
((ci, redactionExp),) <$> HashMap.lookup scalarType typeMap
)
& nonEmpty
@ -1164,13 +1169,13 @@ tableAggregationFields tableInfo = do
| (C.toSnakeG name) == Name._sum = traverse mkColumnAggField
-- Memoize here for more sharing. Note: we can't do `P.memoizeOn 'mkNumericAggFields...`
-- due to stage restrictions, so just add a string key:
| otherwise = traverse \(columnInfo, censorExp) ->
| otherwise = traverse \(columnInfo, redactionExp) ->
P.memoizeOn 'tableAggregationFields ("mkNumericAggFields" :: Text, columnInfo)
$
-- CAREFUL!: below must only reference columnInfo else memoization key needs to be adapted
pure
$! do
let !cfcol = IR.SFCol (ciColumn columnInfo) (ciType columnInfo) censorExp
let !cfcol = IR.SFCol (ciColumn columnInfo) (ciType columnInfo) redactionExp
P.selection_
(ciName columnInfo)
(ciDescription columnInfo)
@ -1178,18 +1183,18 @@ tableAggregationFields tableInfo = do
$> cfcol
mkColumnAggField :: (ColumnInfo b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b)) -> SchemaT r m (FieldParser n (IR.SelectionField b (IR.UnpreparedValue b)))
mkColumnAggField columnAndCensorExp@(columnInfo, _censorExp) =
mkColumnAggField' columnAndCensorExp (ciType columnInfo)
mkColumnAggField columnAndRedactionExp@(columnInfo, _redactionExp) =
mkColumnAggField' columnAndRedactionExp (ciType columnInfo)
mkColumnAggField' :: (ColumnInfo b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b)) -> ColumnType b -> SchemaT r m (FieldParser n (IR.SelectionField b (IR.UnpreparedValue b)))
mkColumnAggField' (columnInfo, censorExp) resultType = do
mkColumnAggField' (columnInfo, redactionExp) resultType = do
field <- columnParser resultType (G.Nullability True)
pure
$ P.selection_
(ciName columnInfo)
(ciDescription columnInfo)
field
$> IR.SFCol (ciColumn columnInfo) (ciType columnInfo) censorExp
$> IR.SFCol (ciColumn columnInfo) (ciType columnInfo) redactionExp
mkNullableScalarTypeAggField :: (ColumnInfo b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b)) -> ScalarType b -> SchemaT r m (FieldParser n (IR.SelectionField b (IR.UnpreparedValue b)))
mkNullableScalarTypeAggField columnInfo resultType =
@ -1236,7 +1241,7 @@ defaultArgsParser ::
) =>
InputFieldsParser n (Maybe (AnnBoolExp b (IR.UnpreparedValue b))) ->
InputFieldsParser n (Maybe (NonEmpty (IR.AnnotatedOrderByItemG b (IR.UnpreparedValue b)))) ->
InputFieldsParser n (Maybe (NonEmpty (Column b))) ->
InputFieldsParser n (Maybe (NonEmpty (IR.AnnDistinctColumn b (IR.UnpreparedValue b)))) ->
SchemaT r m (InputFieldsParser n (SelectArgs b))
defaultArgsParser whereParser orderByParser distinctParser = do
let result = do
@ -1267,11 +1272,11 @@ defaultArgsParser whereParser orderByParser distinctParser = do
initOrderBys = take colsLen $ NE.toList orderByCols
initOrdByCols = flip mapMaybe initOrderBys $ \ob ->
case IR.obiColumn ob of
IR.AOCColumn columnInfo -> Just $ ciColumn columnInfo
IR.AOCColumn columnInfo _redactionExp -> Just $ ciColumn columnInfo
_ -> Nothing
isValid =
(colsLen == length initOrdByCols)
&& all (`elem` initOrdByCols) (toList distinctCols)
&& all (`elem` initOrdByCols) (IR._adcColumn <$> toList distinctCols)
unless isValid
$ parseError
"\"distinct_on\" columns must match initial \"order_by\" columns"

View File

@ -191,7 +191,7 @@ tableStreamCursorExp tableInfo = runMaybeT do
mkTypename = runMkTypename $ _rscTypeNames customization
tableGQLName <- getTableGQLName tableInfo
tableGQLIdentifier <- getTableIdentifierName tableInfo
-- TODO(caseBoolExp): Do we need to deal with censorship expressions here too?
-- TODO(caseBoolExp): Do we need to deal with redaction expressions here too?
columnInfos <- mapMaybe (^? _1 . _SCIScalarColumn) <$> tableSelectColumns tableInfo
columnInfosNE <- hoistMaybe $ NE.nonEmpty columnInfos
lift $ memoizeOn 'tableStreamCursorExp (sourceName, tableName) do

View File

@ -98,8 +98,8 @@ tableSelectColumnsEnum tableInfo = do
let tCase = _rscNamingConvention customization
mkTypename = runMkTypename $ _rscTypeNames customization
tableGQLName <- getTableIdentifierName @b tableInfo
columnsWithCensorExps <- tableSelectColumns tableInfo
let columns = fst <$> columnsWithCensorExps
columnsWithRedactionExps <- tableSelectColumns tableInfo
let columns = fst <$> columnsWithRedactionExps
let enumName = mkTypename $ applyTypeNameCaseIdentifier tCase $ mkTableSelectColumnTypeName tableGQLName
description =
Just
@ -109,7 +109,7 @@ tableSelectColumnsEnum tableInfo = do
-- We noticed many 'Definition's allocated, from 'define' below, so memoize
-- to gain more sharing and lower memory residency.
let columnDefinitions =
columnsWithCensorExps
columnsWithRedactionExps
<&> ( \(structuredColumnInfo, caseBoolExp) ->
let definition = define $ structuredColumnInfoName structuredColumnInfo
column = structuredColumnInfoColumn structuredColumnInfo
@ -288,16 +288,16 @@ tableSelectColumns tableInfo = do
case spiCols <$> tableSelectPermissions roleName tableInfo of
Nothing -> pure []
Just columnPermissions ->
mapMaybe (getColumnsAndCensorExps columnPermissions) <$> tableSelectFields tableInfo
mapMaybe (getColumnsAndRedactionExps columnPermissions) <$> tableSelectFields tableInfo
where
getColumnsAndCensorExps ::
getColumnsAndRedactionExps ::
HashMap (Column b) (Maybe (AnnColumnCaseBoolExpPartialSQL b)) ->
FieldInfo b ->
Maybe ((StructuredColumnInfo b, Maybe (AnnColumnCaseBoolExpUnpreparedValue b)))
getColumnsAndCensorExps columnPermissions = \case
getColumnsAndRedactionExps columnPermissions = \case
FIColumn structuredColumnInfo -> do
censorExp <- HashMap.lookup (structuredColumnInfoColumn structuredColumnInfo) columnPermissions
pure (structuredColumnInfo, (fmap . fmap) partialSQLExpToUnpreparedValue <$!> censorExp)
redactionExp <- HashMap.lookup (structuredColumnInfoColumn structuredColumnInfo) columnPermissions
pure (structuredColumnInfo, (fmap . fmap) partialSQLExpToUnpreparedValue <$!> redactionExp)
_ ->
Nothing

View File

@ -2,6 +2,7 @@ module Hasura.LogicalModel.Common
( toFieldInfo,
columnsFromFields,
logicalModelFieldsToFieldInfo,
getSelPermInfoForLogicalModel,
)
where
@ -9,12 +10,16 @@ import Data.Bifunctor (bimap)
import Data.HashMap.Strict qualified as HashMap
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.Text.Extended (ToTxt (toTxt))
import Hasura.LogicalModel.Cache
import Hasura.LogicalModel.NullableScalarType (NullableScalarType (..))
import Hasura.LogicalModel.Types (LogicalModelField (..), LogicalModelType (..), LogicalModelTypeScalar (..))
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp (gBoolExpTrue)
import Hasura.RQL.Types.Backend (Backend (..))
import Hasura.RQL.Types.Column (ColumnInfo (..), ColumnMutability (..), ColumnType (..), StructuredColumnInfo (..), fromCol)
import Hasura.Table.Cache (FieldInfo (..), FieldInfoMap)
import Hasura.RQL.Types.Permission (AllowedRootFields (..))
import Hasura.RQL.Types.Roles (RoleName, adminRoleName)
import Hasura.Table.Cache (FieldInfo (..), FieldInfoMap, RolePermInfo (..), SelPermInfo (..))
import Language.GraphQL.Draft.Syntax qualified as G
columnsFromFields ::
@ -76,3 +81,26 @@ logicalModelFieldsToFieldInfo =
(\i (column, lmf) -> (,) column <$> logicalModelToColumnInfo i (column, lmf))
. InsOrdHashMap.toList
. columnsFromFields
getSelPermInfoForLogicalModel ::
(Backend b) =>
RoleName ->
LogicalModelInfo b ->
Maybe (SelPermInfo b)
getSelPermInfoForLogicalModel role logicalModel =
if role == adminRoleName
then Just $ mkAdminSelPermInfo logicalModel
else HashMap.lookup role (_lmiPermissions logicalModel) >>= _permSel
mkAdminSelPermInfo :: (Backend b) => LogicalModelInfo b -> SelPermInfo b
mkAdminSelPermInfo LogicalModelInfo {..} =
SelPermInfo
{ spiCols = HashMap.fromList $ (,Nothing) <$> InsOrdHashMap.keys _lmiFields,
spiComputedFields = mempty,
spiFilter = gBoolExpTrue,
spiLimit = Nothing,
spiAllowAgg = True,
spiRequiredHeaders = mempty,
spiAllowedQueryRootFields = ARFAllowAllRootFields,
spiAllowedSubscriptionRootFields = ARFAllowAllRootFields
}

View File

@ -3,7 +3,8 @@
-- | Schema parsers for logical models
module Hasura.LogicalModel.Schema
( buildLogicalModelIR,
( getSelPermInfoForLogicalModel,
buildLogicalModelIR,
buildLogicalModelPermissions,
logicalModelSelectionList,
defaultLogicalModelArgs,
@ -46,6 +47,7 @@ import Hasura.GraphQL.Schema.Parser
import Hasura.GraphQL.Schema.Parser qualified as P
import Hasura.GraphQL.Schema.Select (defaultArgsParser)
import Hasura.LogicalModel.Cache (LogicalModelInfo (..))
import Hasura.LogicalModel.Common (getSelPermInfoForLogicalModel)
import Hasura.LogicalModel.IR (LogicalModel (..))
import Hasura.LogicalModel.Types
( LogicalModelField (..),
@ -58,7 +60,6 @@ import Hasura.LogicalModel.Types
import Hasura.Name qualified as Name
import Hasura.Prelude
import Hasura.RQL.IR qualified as IR
import Hasura.RQL.IR.BoolExp (gBoolExpTrue)
import Hasura.RQL.Types.Backend (Backend, Column)
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common (RelName (..))
@ -71,30 +72,18 @@ import Hasura.RQL.Types.SourceCustomization
import Hasura.Table.Cache (SelPermInfo (..), _permSel)
import Language.GraphQL.Draft.Syntax qualified as G
-- | find list of columns we're allowed to access for this role
getSelPermInfoForLogicalModel ::
RoleName ->
LogicalModelInfo b ->
Maybe (SelPermInfo b)
getSelPermInfoForLogicalModel role logicalModel =
HashMap.lookup role (_lmiPermissions logicalModel) >>= _permSel
-- | build select permissions for logical model
-- `admin` can always select everything
logicalModelPermissions ::
(Backend b) =>
LogicalModelInfo b ->
RoleName ->
Maybe (IR.TablePermG b (IR.UnpreparedValue b))
logicalModelPermissions logicalModel roleName = do
if roleName == adminRoleName
then Just $ IR.TablePerm gBoolExpTrue Nothing
else
getSelPermInfoForLogicalModel roleName logicalModel <&> \selectPermissions ->
IR.TablePerm
{ IR._tpFilter = fmap partialSQLExpToUnpreparedValue <$> spiFilter selectPermissions,
IR._tpLimit = spiLimit selectPermissions
}
getSelPermInfoForLogicalModel roleName logicalModel <&> \selectPermissions ->
IR.TablePerm
{ IR._tpFilter = fmap partialSQLExpToUnpreparedValue <$> spiFilter selectPermissions,
IR._tpLimit = spiLimit selectPermissions
}
-- | turn post-schema cache LogicalModelInfo into IR
buildLogicalModelIR :: LogicalModelInfo b -> LogicalModel b
@ -193,6 +182,7 @@ parseLogicalModelField knownRelNames column logimoField = runMaybeT do
columnName <- G.mkName (toTxt column) `onNothing` throw500 (column <<> " is not a valid GraphQL name")
-- We have not yet worked out what providing permissions here enables
-- TODO(caseBoolExp): This should be drawn from SelPermInfo.spiCols
let caseBoolExpUnpreparedValue = Nothing
columnType = ColumnScalar lmtsScalar
pathArg = scalarSelectionArgumentsParser columnType
@ -366,14 +356,17 @@ logicalModelDistinctArg ::
( MonadBuildSchema b r m n
) =>
LogicalModelInfo b ->
SchemaT r m (InputFieldsParser n (Maybe (NonEmpty (Column b))))
SchemaT r m (InputFieldsParser n (Maybe (NonEmpty (IR.AnnDistinctColumn b (IR.UnpreparedValue b)))))
logicalModelDistinctArg logicalModel = do
roleName <- retrieve scRole
let selectPermissions = getSelPermInfoForLogicalModel roleName logicalModel
let name = getLogicalModelName (_lmiName logicalModel)
tCase <- retrieve $ _rscNamingConvention . _siCustomization @b
let maybeColumnDefinitions =
traverse definitionFromTypeRow (InsOrdHashMap.keys (_lmiFields logicalModel))
traverse (definitionFromTypeRow selectPermissions) (InsOrdHashMap.keys (_lmiFields logicalModel))
>>= NE.nonEmpty
case (,) <$> G.mkName "_enum_name" <*> maybeColumnDefinitions of
@ -394,8 +387,9 @@ logicalModelDistinctArg logicalModel = do
(P.fieldOptional distinctOnName distinctOnDesc . P.nullable . P.list)
pure $ maybeDistinctOnColumns >>= NE.nonEmpty
where
definitionFromTypeRow :: Column b -> Maybe (P.Definition P.EnumValueInfo, Column b)
definitionFromTypeRow name' = do
definitionFromTypeRow :: Maybe (SelPermInfo b) -> Column b -> Maybe (P.Definition P.EnumValueInfo, IR.AnnDistinctColumn b (IR.UnpreparedValue b))
definitionFromTypeRow maybeSelectPermissions name' = do
selectPermissions <- maybeSelectPermissions
columnName <- G.mkName (toTxt name')
let definition =
@ -406,7 +400,11 @@ logicalModelDistinctArg logicalModel = do
dDirectives = mempty,
dInfo = P.EnumValueInfo
}
pure (definition, name')
let redactionExp = join . HashMap.lookup name' $ spiCols selectPermissions
let redactionExpUnpreparedValue = fmap (fmap partialSQLExpToUnpreparedValue) <$!> redactionExp
pure (definition, IR.AnnDistinctColumn name' redactionExpUnpreparedValue)
defaultLogicalModelArgs ::
forall b r m n.

View File

@ -139,13 +139,15 @@ convOrderByElem sessVarBldr (flds, spi) = \case
case fldInfo of
FIColumn (SCIScalarColumn colInfo) -> do
checkSelOnCol spi (ciColumn colInfo)
let redactionExp = join $ HashMap.lookup (ciColumn colInfo) (spiCols spi)
resolvedRedactionExp <- traverse (convAnnColumnCaseBoolExpPartialSQL sessVarBldr) redactionExp
let ty = ciType colInfo
if isScalarColumnWhere isGeoType ty
then
throw400 UnexpectedPayload
$ fldName
<<> " has type 'geometry' and cannot be used in order_by"
else pure $ AOCColumn colInfo
else pure $ AOCColumn colInfo resolvedRedactionExp
FIRelationship _ ->
throw400 UnexpectedPayload
$ fldName

View File

@ -5,6 +5,7 @@
module Hasura.RQL.IR.Select.Args
( SelectArgs,
SelectArgsG (..),
AnnDistinctColumn (..),
SelectStreamArgsG (..),
SelectStreamArgs,
StreamCursorItem (..),
@ -56,28 +57,31 @@ data SelectArgsG (b :: BackendType) v = SelectArgs
_saOrderBy :: Maybe (NE.NonEmpty (AnnotatedOrderByItemG b v)),
_saLimit :: Maybe Int,
_saOffset :: Maybe Int64,
_saDistinct :: (Maybe (NE.NonEmpty (Column b)))
_saDistinct :: (Maybe (NE.NonEmpty (AnnDistinctColumn b v)))
}
deriving stock (Generic, Functor, Foldable, Traversable)
deriving stock instance
( Backend b,
Eq (AnnBoolExp b v),
Eq (AnnotatedOrderByItemG b v)
Eq (AnnotatedOrderByItemG b v),
Eq (AnnDistinctColumn b v)
) =>
Eq (SelectArgsG b v)
instance
( Backend b,
Hashable (AnnBoolExp b v),
Hashable (AnnotatedOrderByItemG b v)
Hashable (AnnotatedOrderByItemG b v),
Hashable (AnnDistinctColumn b v)
) =>
Hashable (SelectArgsG b v)
deriving stock instance
( Backend b,
Show (AnnBoolExp b v),
Show (AnnotatedOrderByItemG b v)
Show (AnnotatedOrderByItemG b v),
Show (AnnDistinctColumn b v)
) =>
Show (SelectArgsG b v)
@ -86,6 +90,35 @@ type SelectArgs b = SelectArgsG b (SQLExpression b)
noSelectArgs :: SelectArgsG backend v
noSelectArgs = SelectArgs Nothing Nothing Nothing Nothing Nothing
data AnnDistinctColumn b v = AnnDistinctColumn
{ _adcColumn :: Column b,
-- | This type is used to determine whether the column
-- should be nullified. When the value is `Nothing`, the column value
-- will be outputted as computed and when the value is `Just c`, the
-- column will be outputted when `c` evaluates to `true` and `null`
-- when `c` evaluates to `false`.
_adcCaseBoolExpression :: (Maybe (AnnColumnCaseBoolExp b v))
}
deriving stock (Generic, Functor, Foldable, Traversable)
deriving stock instance
( Backend b,
Eq (AnnColumnCaseBoolExp b v)
) =>
Eq (AnnDistinctColumn b v)
instance
( Backend b,
Hashable (AnnColumnCaseBoolExp b v)
) =>
Hashable (AnnDistinctColumn b v)
deriving stock instance
( Backend b,
Show (AnnColumnCaseBoolExp b v)
) =>
Show (AnnDistinctColumn b v)
-- | Cursor for streaming subscription
data StreamCursorItem (b :: BackendType) = StreamCursorItem
{ -- | Specifies how the cursor item should be ordered

View File

@ -3,6 +3,7 @@
module Hasura.RQL.IR.Select.OrderBy
( AnnotatedAggregateOrderBy (..),
AggregateOrderByColumn (..),
AnnotatedOrderByElement (..),
AnnotatedOrderByItem,
AnnotatedOrderByItemG,
@ -22,7 +23,14 @@ import Hasura.RQL.Types.ComputedField
import Hasura.RQL.Types.Relationships.Local
data AnnotatedOrderByElement (b :: BackendType) v
= AOCColumn (ColumnInfo b)
= AOCColumn
(ColumnInfo b)
-- | This type is used to determine whether the column
-- should be nullified. When the value is `Nothing`, the column value
-- will be outputted as computed and when the value is `Just c`, the
-- column will be outputted when `c` evaluates to `true` and `null`
-- when `c` evaluates to `false`.
(Maybe (AnnColumnCaseBoolExp b v))
| AOCObjectRelation
(RelInfo b)
-- | Permission filter of the remote table to which the relationship is defined
@ -32,46 +40,67 @@ data AnnotatedOrderByElement (b :: BackendType) v
(RelInfo b)
-- | Permission filter of the remote table to which the relationship is defined
(AnnBoolExp b v)
(AnnotatedAggregateOrderBy b)
(AnnotatedAggregateOrderBy b v)
| AOCComputedField (ComputedFieldOrderBy b v)
deriving stock (Generic, Functor, Foldable, Traversable)
deriving stock instance
( Backend b,
Eq (AnnBoolExp b v),
Eq (AnnotatedAggregateOrderBy b),
Eq (ComputedFieldOrderBy b v)
Eq (AnnotatedAggregateOrderBy b v),
Eq (ComputedFieldOrderBy b v),
Eq (AnnColumnCaseBoolExp b v)
) =>
Eq (AnnotatedOrderByElement b v)
deriving stock instance
( Backend b,
Show (AnnBoolExp b v),
Show (AnnotatedAggregateOrderBy b),
Show (ComputedFieldOrderBy b v)
Show (AnnotatedAggregateOrderBy b v),
Show (ComputedFieldOrderBy b v),
Show (AnnColumnCaseBoolExp b v)
) =>
Show (AnnotatedOrderByElement b v)
instance
( Backend b,
Hashable (AnnBoolExp b v),
Hashable (AnnotatedAggregateOrderBy b),
Hashable (ComputedFieldOrderBy b v)
Hashable (AnnotatedAggregateOrderBy b v),
Hashable (ComputedFieldOrderBy b v),
Hashable (AnnColumnCaseBoolExp b v)
) =>
Hashable (AnnotatedOrderByElement b v)
data AnnotatedAggregateOrderBy (b :: BackendType)
data AnnotatedAggregateOrderBy (b :: BackendType) v
= AAOCount
| -- | Order by an aggregate function applied to a column
-- Fields are: Aggregate function name, aggregate function return type, column being aggregated
AAOOp Text (ColumnType b) (ColumnInfo b)
deriving stock (Generic)
AAOOp (AggregateOrderByColumn b v)
deriving stock (Generic, Functor, Foldable, Traversable)
deriving stock instance (Backend b) => Eq (AnnotatedAggregateOrderBy b)
deriving stock instance (Backend b, Eq (AggregateOrderByColumn b v)) => Eq (AnnotatedAggregateOrderBy b v)
deriving stock instance (Backend b) => Show (AnnotatedAggregateOrderBy b)
deriving stock instance (Backend b, Show (AggregateOrderByColumn b v)) => Show (AnnotatedAggregateOrderBy b v)
instance (Backend b) => Hashable (AnnotatedAggregateOrderBy b)
instance (Backend b, Hashable (AggregateOrderByColumn b v)) => Hashable (AnnotatedAggregateOrderBy b v)
data AggregateOrderByColumn b v = AggregateOrderByColumn
{ _aobcAggregateFunctionName :: Text,
_aobcAggregateFunctionReturnType :: ColumnType b,
_aobcColumn :: ColumnInfo b,
-- | This type is used to determine whether the column
-- should be nullified. When the value is `Nothing`, the column value
-- will be outputted as computed and when the value is `Just c`, the
-- column will be outputted when `c` evaluates to `true` and `null`
-- when `c` evaluates to `false`.
_aobcCaseBoolExpression :: (Maybe (AnnColumnCaseBoolExp b v))
}
deriving stock (Generic, Functor, Foldable, Traversable)
deriving stock instance (Backend b, Eq (AnnColumnCaseBoolExp b v)) => Eq (AggregateOrderByColumn b v)
deriving stock instance (Backend b, Show (AnnColumnCaseBoolExp b v)) => Show (AggregateOrderByColumn b v)
instance (Backend b, Hashable (AnnColumnCaseBoolExp b v)) => Hashable (AggregateOrderByColumn b v)
type AnnotatedOrderByItemG b v = OrderByItemG b (AnnotatedOrderByElement b v)
@ -80,19 +109,27 @@ type AnnotatedOrderByItem b = AnnotatedOrderByItemG b (SQLExpression b)
-- | The order by element for a computed field based on its return type
data ComputedFieldOrderByElement (b :: BackendType) v
= -- | Sort by the scalar computed field
CFOBEScalar (ScalarType b)
CFOBEScalar
(ScalarType b)
-- | This type is used to determine whether the column
-- should be nullified. When the value is `Nothing`, the column value
-- will be outputted as computed and when the value is `Just c`, the
-- column will be outputted when `c` evaluates to `true` and `null`
-- when `c` evaluates to `false`.
(Maybe (AnnColumnCaseBoolExp b v))
| CFOBETableAggregation
(TableName b)
-- | Permission filter of the retuning table
(AnnBoolExp b v)
-- | Sort by aggregation fields of table rows returned by computed field
(AnnotatedAggregateOrderBy b)
(AnnotatedAggregateOrderBy b v)
deriving stock (Generic, Functor, Foldable, Traversable)
deriving stock instance
( Backend b,
Eq (AnnBoolExp b v),
Eq (AnnotatedAggregateOrderBy b)
Eq (AnnotatedAggregateOrderBy b v),
Eq (AnnColumnCaseBoolExp b v)
) =>
Eq (ComputedFieldOrderByElement b v)
@ -100,14 +137,16 @@ deriving stock instance
( Backend b,
Show v,
Show (AnnBoolExp b v),
Show (AnnotatedAggregateOrderBy b)
Show (AnnotatedAggregateOrderBy b v),
Show (AnnColumnCaseBoolExp b v)
) =>
Show (ComputedFieldOrderByElement b v)
instance
( Backend b,
Hashable (AnnBoolExp b v),
Hashable (AnnotatedAggregateOrderBy b)
Hashable (AnnotatedAggregateOrderBy b v),
Hashable (AnnColumnCaseBoolExp b v)
) =>
Hashable (ComputedFieldOrderByElement b v)

View File

@ -20,10 +20,11 @@ import Hasura.RQL.IR.BoolExp
import Hasura.RQL.IR.Generator
( genAnnBoolExp,
genAnnBoolExpFld,
genAnnColumnCaseBoolExp,
genAnnotatedOrderByElement,
genAnnotatedOrderByItemG,
)
import Hasura.RQL.IR.Select (AnnotatedOrderByItemG, SelectArgsG (..))
import Hasura.RQL.IR.Select (AnnDistinctColumn (..), AnnotatedOrderByItemG, SelectArgsG (..))
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.BackendType
import Hedgehog (MonadGen)
@ -81,8 +82,21 @@ genSelectArgsG genA = do
offset :: m (Maybe Int64)
offset = Gen.maybe $ Gen.integral defaultRange
distinct :: m (Maybe (NonEmpty (Column ('Postgres 'Vanilla))))
distinct = Gen.maybe . Gen.nonEmpty defaultRange $ genColumn
distinct :: m (Maybe (NonEmpty (AnnDistinctColumn ('Postgres 'Vanilla) a)))
distinct =
Gen.maybe
. Gen.nonEmpty defaultRange
$ AnnDistinctColumn
<$> genColumn
<*> genAnnColumnCaseBoolExp
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
(genBooleanOperators genA)
(genFunctionArgumentExp genA)
genA
--------------------------------------------------------------------------------
-- Unexported Helpers

View File

@ -11,6 +11,7 @@
module Hasura.RQL.IR.Generator
( genAnnBoolExp,
genAnnBoolExpFld,
genAnnColumnCaseBoolExp,
genAnnotatedOrderByItemG,
genAnnotatedOrderByElement,
genAnnotatedAggregateOrderBy,
@ -57,6 +58,44 @@ genFunctionArgsExpG genA =
<$> list defaultRange genA
<*> genHashMap (genArbitraryUnicodeText defaultRange) genA defaultRange
genAnnColumnCaseBoolExp ::
(MonadGen m) =>
(Hashable (ScalarType b)) =>
(Hashable (Column b)) =>
m (Column b) ->
m (TableName b) ->
m (ScalarType b) ->
m (FunctionName b) ->
m (XComputedField b) ->
m (BooleanOperators b a) ->
m (FunctionArgumentExp b a) ->
m a ->
m (Maybe (AnnColumnCaseBoolExp b a))
genAnnColumnCaseBoolExp
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA =
maybe
$ genAnnBoolExp
( AnnColumnCaseBoolExpField
<$> ( genAnnBoolExpFld
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA
)
)
genTableName
genGExists ::
(MonadGen m) =>
m a ->
@ -464,6 +503,15 @@ genAnnotatedOrderByElement
genColumn
genTableName
genScalarType
<*> genAnnColumnCaseBoolExp
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA
objectRelation =
AOCObjectRelation
<$> genRelInfo genTableName genColumn
@ -507,6 +555,11 @@ genAnnotatedOrderByElement
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA
computedField =
AOCComputedField
<$> genComputedFieldOrderBy
@ -521,23 +574,46 @@ genAnnotatedOrderByElement
genAnnotatedAggregateOrderBy ::
(MonadGen m) =>
(Hashable (ScalarType b)) =>
(Hashable (Column b)) =>
m (Column b) ->
m (TableName b) ->
m (ScalarType b) ->
m (AnnotatedAggregateOrderBy b)
m (FunctionName b) ->
m (XComputedField b) ->
m (BooleanOperators b a) ->
m (FunctionArgumentExp b a) ->
m a ->
m (AnnotatedAggregateOrderBy b a)
genAnnotatedAggregateOrderBy
genColumn
genTableName
genScalarType =
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA =
choice
[ pure AAOCount,
AAOOp
<$> genArbitraryUnicodeText defaultRange
<*> genColumnType genTableName genScalarType
<*> genColumnInfo
genColumn
genTableName
genScalarType
<$> ( AggregateOrderByColumn
<$> genArbitraryUnicodeText defaultRange
<*> genColumnType genTableName genScalarType
<*> genColumnInfo
genColumn
genTableName
genScalarType
<*> genAnnColumnCaseBoolExp
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA
)
]
genComputedFieldOrderBy ::
@ -600,7 +676,17 @@ genComputedFieldOrderByElement
genFunctionArgumentExp
genA =
choice
[ CFOBEScalar <$> genScalarType,
[ CFOBEScalar
<$> genScalarType
<*> genAnnColumnCaseBoolExp
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA,
CFOBETableAggregation
<$> genTableName
<*> genAnnBoolExp
@ -619,6 +705,11 @@ genComputedFieldOrderByElement
genColumn
genTableName
genScalarType
genFunctionName
genXComputedField
genBooleanOperators
genFunctionArgumentExp
genA
]
genIdentifier :: (MonadGen m) => m FIIdentifier