mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-04 22:07:40 +03:00
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:
parent
9a09af4f20
commit
a86345421f
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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))
|
||||
)
|
||||
|
@ -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.
|
||||
|
@ -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 ->
|
||||
|
@ -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)))
|
||||
)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user