mirror of
https://github.com/hasura/graphql-engine.git
synced 2025-01-05 22:34:22 +03:00
Add schema implementation for Aggregation Predicates
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5259 GitOrigin-RevId: f53e310951fa4eb7570006d8c616398a98078632
This commit is contained in:
parent
967bdcc5d5
commit
4431fb5ea9
@ -815,6 +815,7 @@ library
|
|||||||
, Hasura.GraphQL.Schema.Action
|
, Hasura.GraphQL.Schema.Action
|
||||||
, Hasura.GraphQL.Schema.Backend
|
, Hasura.GraphQL.Schema.Backend
|
||||||
, Hasura.GraphQL.Schema.BoolExp
|
, Hasura.GraphQL.Schema.BoolExp
|
||||||
|
, Hasura.GraphQL.Schema.BoolExp.AggregationPredicates
|
||||||
, Hasura.GraphQL.Schema.Build
|
, Hasura.GraphQL.Schema.Build
|
||||||
, Hasura.GraphQL.Schema.Common
|
, Hasura.GraphQL.Schema.Common
|
||||||
, Hasura.GraphQL.Schema.Instances
|
, Hasura.GraphQL.Schema.Instances
|
||||||
|
@ -55,7 +55,9 @@ import Language.GraphQL.Draft.Syntax qualified as G
|
|||||||
-- @tablename@ defined /and/ these grant non-empty column permissions.
|
-- @tablename@ defined /and/ these grant non-empty column permissions.
|
||||||
ifMatchedFieldParser ::
|
ifMatchedFieldParser ::
|
||||||
forall r m n.
|
forall r m n.
|
||||||
MonadBuildSchema 'MSSQL r m n =>
|
( MonadBuildSchema 'MSSQL r m n,
|
||||||
|
AggregationPredicatesSchema 'MSSQL
|
||||||
|
) =>
|
||||||
SourceInfo 'MSSQL ->
|
SourceInfo 'MSSQL ->
|
||||||
TableInfo 'MSSQL ->
|
TableInfo 'MSSQL ->
|
||||||
m (InputFieldsParser n (Maybe (IfMatched (UnpreparedValue 'MSSQL))))
|
m (InputFieldsParser n (Maybe (IfMatched (UnpreparedValue 'MSSQL))))
|
||||||
@ -66,7 +68,9 @@ ifMatchedFieldParser sourceInfo tableInfo = do
|
|||||||
-- | Parse a @tablename_if_matched@ object.
|
-- | Parse a @tablename_if_matched@ object.
|
||||||
ifMatchedObjectParser ::
|
ifMatchedObjectParser ::
|
||||||
forall r m n.
|
forall r m n.
|
||||||
MonadBuildSchema 'MSSQL r m n =>
|
( MonadBuildSchema 'MSSQL r m n,
|
||||||
|
AggregationPredicatesSchema 'MSSQL
|
||||||
|
) =>
|
||||||
SourceInfo 'MSSQL ->
|
SourceInfo 'MSSQL ->
|
||||||
TableInfo 'MSSQL ->
|
TableInfo 'MSSQL ->
|
||||||
m (Maybe (Parser 'Input n (IfMatched (UnpreparedValue 'MSSQL))))
|
m (Maybe (Parser 'Input n (IfMatched (UnpreparedValue 'MSSQL))))
|
||||||
|
@ -56,7 +56,9 @@ import Language.GraphQL.Draft.Syntax qualified as G
|
|||||||
-- enum. See <https://github.com/hasura/graphql-engine/issues/6804>.
|
-- enum. See <https://github.com/hasura/graphql-engine/issues/6804>.
|
||||||
onConflictFieldParser ::
|
onConflictFieldParser ::
|
||||||
forall pgKind r m n.
|
forall pgKind r m n.
|
||||||
MonadBuildSchema ('Postgres pgKind) r m n =>
|
( MonadBuildSchema ('Postgres pgKind) r m n,
|
||||||
|
AggregationPredicatesSchema ('Postgres pgKind)
|
||||||
|
) =>
|
||||||
SourceInfo ('Postgres pgKind) ->
|
SourceInfo ('Postgres pgKind) ->
|
||||||
TableInfo ('Postgres pgKind) ->
|
TableInfo ('Postgres pgKind) ->
|
||||||
m (InputFieldsParser n (Maybe (IR.OnConflictClause ('Postgres pgKind) (IR.UnpreparedValue ('Postgres pgKind)))))
|
m (InputFieldsParser n (Maybe (IR.OnConflictClause ('Postgres pgKind) (IR.UnpreparedValue ('Postgres pgKind)))))
|
||||||
@ -73,7 +75,9 @@ onConflictFieldParser sourceInfo tableInfo = do
|
|||||||
-- | Create a parser for the @_on_conflict@ object of the given table.
|
-- | Create a parser for the @_on_conflict@ object of the given table.
|
||||||
conflictObjectParser ::
|
conflictObjectParser ::
|
||||||
forall pgKind r m n.
|
forall pgKind r m n.
|
||||||
MonadBuildSchema ('Postgres pgKind) r m n =>
|
( MonadBuildSchema ('Postgres pgKind) r m n,
|
||||||
|
AggregationPredicatesSchema ('Postgres pgKind)
|
||||||
|
) =>
|
||||||
SourceInfo ('Postgres pgKind) ->
|
SourceInfo ('Postgres pgKind) ->
|
||||||
TableInfo ('Postgres pgKind) ->
|
TableInfo ('Postgres pgKind) ->
|
||||||
Maybe (UpdPermInfo ('Postgres pgKind)) ->
|
Maybe (UpdPermInfo ('Postgres pgKind)) ->
|
||||||
|
@ -23,6 +23,7 @@ import Hasura.Backends.Postgres.Types.ComputedField qualified as PG
|
|||||||
import Hasura.Backends.Postgres.Types.Function qualified as PG
|
import Hasura.Backends.Postgres.Types.Function qualified as PG
|
||||||
import Hasura.Base.Error
|
import Hasura.Base.Error
|
||||||
import Hasura.GraphQL.Schema.Backend
|
import Hasura.GraphQL.Schema.Backend
|
||||||
|
import Hasura.GraphQL.Schema.BoolExp
|
||||||
import Hasura.GraphQL.Schema.Common
|
import Hasura.GraphQL.Schema.Common
|
||||||
import Hasura.GraphQL.Schema.Options qualified as Options
|
import Hasura.GraphQL.Schema.Options qualified as Options
|
||||||
import Hasura.GraphQL.Schema.Parser
|
import Hasura.GraphQL.Schema.Parser
|
||||||
@ -146,6 +147,7 @@ selectFunctionAggregate mkRootFieldName sourceInfo fi@FunctionInfo {..} descript
|
|||||||
selectFunctionConnection ::
|
selectFunctionConnection ::
|
||||||
forall pgKind r m n.
|
forall pgKind r m n.
|
||||||
( MonadBuildSchema ('Postgres pgKind) r m n,
|
( MonadBuildSchema ('Postgres pgKind) r m n,
|
||||||
|
AggregationPredicatesSchema ('Postgres pgKind),
|
||||||
BackendTableSelectSchema ('Postgres pgKind)
|
BackendTableSelectSchema ('Postgres pgKind)
|
||||||
) =>
|
) =>
|
||||||
MkRootFieldName ->
|
MkRootFieldName ->
|
||||||
@ -194,8 +196,9 @@ selectFunctionConnection mkRootFieldName sourceInfo fi@FunctionInfo {..} descrip
|
|||||||
-- | Computed field parser
|
-- | Computed field parser
|
||||||
computedFieldPG ::
|
computedFieldPG ::
|
||||||
forall pgKind r m n.
|
forall pgKind r m n.
|
||||||
MonadBuildSchema ('Postgres pgKind) r m n =>
|
( MonadBuildSchema ('Postgres pgKind) r m n,
|
||||||
BackendTableSelectSchema ('Postgres pgKind) =>
|
BackendTableSelectSchema ('Postgres pgKind)
|
||||||
|
) =>
|
||||||
SourceInfo ('Postgres pgKind) ->
|
SourceInfo ('Postgres pgKind) ->
|
||||||
ComputedFieldInfo ('Postgres pgKind) ->
|
ComputedFieldInfo ('Postgres pgKind) ->
|
||||||
TableName ('Postgres pgKind) ->
|
TableName ('Postgres pgKind) ->
|
||||||
|
@ -340,7 +340,7 @@ type ComparisonExp b = OpExpG b (UnpreparedValue b)
|
|||||||
--
|
--
|
||||||
-- * Pro: You can specify both shared and diverging behavior.
|
-- * Pro: You can specify both shared and diverging behavior.
|
||||||
-- * Pro: You can specify a lot of behavior implicitly, i.e. it's easy to write.
|
-- * Pro: You can specify a lot of behavior implicitly, i.e. it's easy to write.
|
||||||
-- * Con: You can specify a lot of behavior implicitly, i.e. it's hard do
|
-- * Con: You can specify a lot of behavior implicitly, i.e. it's hard to
|
||||||
-- understand without tracing through implementations.
|
-- understand without tracing through implementations.
|
||||||
-- * Con: You get a proliferation of type class methods and it's difficult to
|
-- * Con: You get a proliferation of type class methods and it's difficult to
|
||||||
-- understand how they fit together.
|
-- understand how they fit together.
|
||||||
@ -355,7 +355,7 @@ type ComparisonExp b = OpExpG b (UnpreparedValue b)
|
|||||||
-- instead of other type class methods.
|
-- instead of other type class methods.
|
||||||
--
|
--
|
||||||
-- When we do this, the function call sites (which will often be in @instance
|
-- When we do this, the function call sites (which will often be in @instance
|
||||||
-- BackendSchema ...@) becomes the centralised place where we decide which behavior
|
-- BackendSchema ...@) become the centralised places where we decide which behavior
|
||||||
-- variation to follow.
|
-- variation to follow.
|
||||||
--
|
--
|
||||||
-- When faced with answering the question of "what does this method do, and how does
|
-- When faced with answering the question of "what does this method do, and how does
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
module Hasura.GraphQL.Schema.BoolExp
|
module Hasura.GraphQL.Schema.BoolExp
|
||||||
( boolExp,
|
( AggregationPredicatesSchema (..),
|
||||||
|
boolExp,
|
||||||
mkBoolOperator,
|
mkBoolOperator,
|
||||||
equalityOperators,
|
equalityOperators,
|
||||||
comparisonOperators,
|
comparisonOperators,
|
||||||
@ -15,7 +16,7 @@ import Data.Text.Casing qualified as C
|
|||||||
import Data.Text.Extended
|
import Data.Text.Extended
|
||||||
import Hasura.GraphQL.Parser.Class
|
import Hasura.GraphQL.Parser.Class
|
||||||
import Hasura.GraphQL.Schema.Backend
|
import Hasura.GraphQL.Schema.Backend
|
||||||
import Hasura.GraphQL.Schema.Common (SchemaContext (..), askTableInfo, partialSQLExpToUnpreparedValue, retrieve)
|
import Hasura.GraphQL.Schema.Common (MonadBuildSchemaBase, SchemaContext (..), askTableInfo, partialSQLExpToUnpreparedValue, retrieve)
|
||||||
import Hasura.GraphQL.Schema.NamingCase
|
import Hasura.GraphQL.Schema.NamingCase
|
||||||
import Hasura.GraphQL.Schema.Options qualified as Options
|
import Hasura.GraphQL.Schema.Options qualified as Options
|
||||||
import Hasura.GraphQL.Schema.Parser
|
import Hasura.GraphQL.Schema.Parser
|
||||||
@ -39,8 +40,35 @@ import Hasura.RQL.Types.SchemaCache hiding (askTableInfo)
|
|||||||
import Hasura.RQL.Types.Source
|
import Hasura.RQL.Types.Source
|
||||||
import Hasura.RQL.Types.SourceCustomization
|
import Hasura.RQL.Types.SourceCustomization
|
||||||
import Hasura.RQL.Types.Table
|
import Hasura.RQL.Types.Table
|
||||||
|
import Hasura.SQL.Backend (BackendType)
|
||||||
import Language.GraphQL.Draft.Syntax qualified as G
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
|
|
||||||
|
-- | Backends implement this type class to specify the schema of
|
||||||
|
-- aggregation predicates.
|
||||||
|
--
|
||||||
|
-- The default implementation results in a parser that does not parse anything.
|
||||||
|
--
|
||||||
|
-- The scope of this class is local to the function 'boolExp'. In particular,
|
||||||
|
-- methods in `class BackendSchema` and `type MonadBuildSchema` should *NOT*
|
||||||
|
-- include this class as a constraint.
|
||||||
|
class AggregationPredicatesSchema (b :: BackendType) where
|
||||||
|
aggregationPredicatesParser ::
|
||||||
|
forall r m n.
|
||||||
|
MonadBuildSchemaBase r m n =>
|
||||||
|
SourceInfo b ->
|
||||||
|
TableInfo b ->
|
||||||
|
m (Maybe (InputFieldsParser n [AggregationPredicates b (UnpreparedValue b)]))
|
||||||
|
|
||||||
|
-- Overlapping instance for backends that do not implement Aggregation Predicates.
|
||||||
|
instance {-# OVERLAPPABLE #-} (AggregationPredicates b ~ Const Void) => AggregationPredicatesSchema (b :: BackendType) where
|
||||||
|
aggregationPredicatesParser ::
|
||||||
|
forall r m n.
|
||||||
|
(MonadBuildSchemaBase r m n) =>
|
||||||
|
SourceInfo b ->
|
||||||
|
TableInfo b ->
|
||||||
|
m (Maybe (InputFieldsParser n [AggregationPredicates b (UnpreparedValue b)]))
|
||||||
|
aggregationPredicatesParser _ _ = return Nothing
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- > input type_bool_exp {
|
-- > input type_bool_exp {
|
||||||
-- > _or: [type_bool_exp!]
|
-- > _or: [type_bool_exp!]
|
||||||
@ -51,7 +79,7 @@ import Language.GraphQL.Draft.Syntax qualified as G
|
|||||||
-- > }
|
-- > }
|
||||||
boolExp ::
|
boolExp ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
MonadBuildSchema b r m n =>
|
(MonadBuildSchema b r m n, AggregationPredicatesSchema b) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableInfo b ->
|
TableInfo b ->
|
||||||
m (Parser 'Input n (AnnBoolExp b (UnpreparedValue b)))
|
m (Parser 'Input n (AnnBoolExp b (UnpreparedValue b)))
|
||||||
@ -66,10 +94,12 @@ boolExp sourceInfo tableInfo = P.memoizeOn 'boolExp (_siName sourceInfo, tableNa
|
|||||||
|
|
||||||
fieldInfos <- tableSelectFields sourceInfo tableInfo
|
fieldInfos <- tableSelectFields sourceInfo tableInfo
|
||||||
tableFieldParsers <- catMaybes <$> traverse mkField fieldInfos
|
tableFieldParsers <- catMaybes <$> traverse mkField fieldInfos
|
||||||
|
-- TODO: This naming is somewhat unsatifactory..
|
||||||
|
aggregationPredicatesParser' <- fromMaybe (pure []) <$> aggregationPredicatesParser sourceInfo tableInfo
|
||||||
recur <- boolExp sourceInfo tableInfo
|
recur <- boolExp sourceInfo tableInfo
|
||||||
-- Bafflingly, ApplicativeDo doesn’t work if we inline this definition (I
|
-- Bafflingly, ApplicativeDo doesn’t work if we inline this definition (I
|
||||||
-- think the TH splices throw it off), so we have to define it separately.
|
-- think the TH splices throw it off), so we have to define it separately.
|
||||||
let specialFieldParsers =
|
let connectiveFieldParsers =
|
||||||
[ P.fieldOptional Name.__or Nothing (BoolOr <$> P.list recur),
|
[ P.fieldOptional Name.__or Nothing (BoolOr <$> P.list recur),
|
||||||
P.fieldOptional Name.__and Nothing (BoolAnd <$> P.list recur),
|
P.fieldOptional Name.__and Nothing (BoolAnd <$> P.list recur),
|
||||||
P.fieldOptional Name.__not Nothing (BoolNot <$> recur)
|
P.fieldOptional Name.__not Nothing (BoolNot <$> recur)
|
||||||
@ -78,8 +108,9 @@ boolExp sourceInfo tableInfo = P.memoizeOn 'boolExp (_siName sourceInfo, tableNa
|
|||||||
pure $
|
pure $
|
||||||
BoolAnd <$> P.object name (Just description) do
|
BoolAnd <$> P.object name (Just description) do
|
||||||
tableFields <- map BoolField . catMaybes <$> sequenceA tableFieldParsers
|
tableFields <- map BoolField . catMaybes <$> sequenceA tableFieldParsers
|
||||||
specialFields <- catMaybes <$> sequenceA specialFieldParsers
|
specialFields <- catMaybes <$> sequenceA connectiveFieldParsers
|
||||||
pure (tableFields ++ specialFields)
|
aggregationPredicateFields <- map (BoolField . AVAggregationPredicates) <$> aggregationPredicatesParser'
|
||||||
|
pure (tableFields ++ specialFields ++ aggregationPredicateFields)
|
||||||
where
|
where
|
||||||
tableName = tableInfoName tableInfo
|
tableName = tableInfoName tableInfo
|
||||||
|
|
||||||
|
@ -0,0 +1,167 @@
|
|||||||
|
{-# LANGUAGE ApplicativeDo #-}
|
||||||
|
|
||||||
|
-- | This module defines the schema aspect of the default implementation of
|
||||||
|
-- aggregation predicates.
|
||||||
|
module Hasura.GraphQL.Schema.BoolExp.AggregationPredicates
|
||||||
|
( defaultAggregationPredicatesParser,
|
||||||
|
|
||||||
|
-- * Data types describing aggregation functions supported by a backend
|
||||||
|
FunctionSignature (..),
|
||||||
|
ArgumentsSignature (..),
|
||||||
|
ArgumentSignature (..),
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Functor.Compose
|
||||||
|
import Data.List.NonEmpty qualified as NE
|
||||||
|
import Hasura.GraphQL.Parser qualified as P
|
||||||
|
import Hasura.GraphQL.Schema.Backend
|
||||||
|
import Hasura.GraphQL.Schema.BoolExp
|
||||||
|
import Hasura.GraphQL.Schema.Common (MonadBuildSchemaBase, askTableInfo, textToName)
|
||||||
|
import Hasura.GraphQL.Schema.Parser
|
||||||
|
( InputFieldsParser,
|
||||||
|
Kind (..),
|
||||||
|
Parser,
|
||||||
|
)
|
||||||
|
import Hasura.GraphQL.Schema.Table
|
||||||
|
import Hasura.Name qualified as Name
|
||||||
|
import Hasura.Prelude
|
||||||
|
import Hasura.RQL.IR.BoolExp.AggregationPredicates
|
||||||
|
import Hasura.RQL.IR.Value
|
||||||
|
import Hasura.RQL.Types.Backend qualified as B
|
||||||
|
import Hasura.RQL.Types.Column
|
||||||
|
import Hasura.RQL.Types.Common (relNameToTxt)
|
||||||
|
import Hasura.RQL.Types.Relationships.Local
|
||||||
|
import Hasura.RQL.Types.SchemaCache hiding (askTableInfo)
|
||||||
|
import Hasura.RQL.Types.Source
|
||||||
|
import Hasura.RQL.Types.Table
|
||||||
|
import Hasura.SQL.Backend (BackendType)
|
||||||
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
|
|
||||||
|
-- | This function is meant to serve as the default schema for Aggregation
|
||||||
|
-- Predicates represented in the IR by the type
|
||||||
|
-- 'Hasura.RQL.IR.BoolExp.AggregationPredicates.AggregationPredicates'.
|
||||||
|
defaultAggregationPredicatesParser ::
|
||||||
|
forall b r m n.
|
||||||
|
( MonadBuildSchemaBase r m n,
|
||||||
|
BackendSchema b,
|
||||||
|
AggregationPredicatesSchema b
|
||||||
|
) =>
|
||||||
|
[FunctionSignature b] ->
|
||||||
|
SourceInfo b ->
|
||||||
|
TableInfo b ->
|
||||||
|
m (Maybe (InputFieldsParser n [AggregationPredicatesImplementation b (UnpreparedValue b)]))
|
||||||
|
defaultAggregationPredicatesParser aggFns si ti = runMaybeT do
|
||||||
|
arrayRelationships <- fails $ return $ nonEmpty $ tableArrayRelationships ti
|
||||||
|
aggregationFunctions <- fails $ return $ nonEmpty aggFns
|
||||||
|
|
||||||
|
buildAnyOptionalFields $
|
||||||
|
arrayRelationships <&> \rel -> do
|
||||||
|
relTable <- askTableInfo si (riRTable rel)
|
||||||
|
relGqlName <- textToName $ relNameToTxt $ riName rel
|
||||||
|
typeGqlName <- (<> Name.__ <> relGqlName) <$> getTableGQLName relTable
|
||||||
|
|
||||||
|
-- We only make a field for aggregations over a relation if at least
|
||||||
|
-- some aggregation predicates are callable.
|
||||||
|
relAggregateField rel relGqlName typeGqlName
|
||||||
|
-- We only return an InputFieldsParser for aggregation predicates,
|
||||||
|
-- if we parse at least one aggregation predicate
|
||||||
|
<$> buildAnyOptionalFields
|
||||||
|
( aggregationFunctions <&> \FunctionSignature {..} -> do
|
||||||
|
aggPredicateField fnGQLName typeGqlName <$> unfuse do
|
||||||
|
aggPredArguments <-
|
||||||
|
-- We only include an aggregation predicate if we are able to
|
||||||
|
-- access columns all its arguments. This might fail due to
|
||||||
|
-- permissions or due to no columns of suitable types
|
||||||
|
-- existing on the table.
|
||||||
|
case fnArguments of
|
||||||
|
ArgumentsStar ->
|
||||||
|
maybe AggregationPredicateArgumentsStar AggregationPredicateArguments . nonEmpty
|
||||||
|
<$> fuse (fieldOptionalDefault Name._arguments Nothing [] . P.list <$> fails (tableSelectColumnsEnum si relTable))
|
||||||
|
Arguments args ->
|
||||||
|
AggregationPredicateArguments
|
||||||
|
<$> fuse
|
||||||
|
( P.field Name._arguments Nothing
|
||||||
|
. P.object (typeGqlName <> Name.__ <> fnGQLName <> Name.__ <> Name._arguments) Nothing
|
||||||
|
<$> buildAllFieldsNE
|
||||||
|
( args `for` \ArgumentSignature {..} ->
|
||||||
|
P.field argName Nothing <$> fails (tableSelectColumnsPredEnum (== (ColumnScalar argType)) relGqlName si relTable)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
aggPredDistinct <- fuse $ return $ fieldOptionalDefault Name._distinct Nothing False P.boolean
|
||||||
|
let aggPredFunctionName = fnName
|
||||||
|
aggPredPredicate <- fuse $ P.field Name._predicate Nothing <$> lift (comparisonExps @b (ColumnScalar fnReturnType))
|
||||||
|
aggPredFilter <- fuse $ P.fieldOptional Name._filter Nothing <$> lift (boolExp si relTable)
|
||||||
|
pure $ AggregationPredicate {..}
|
||||||
|
)
|
||||||
|
where
|
||||||
|
-- Input field of the aggregation predicates for one array relation.
|
||||||
|
relAggregateField ::
|
||||||
|
RelInfo b ->
|
||||||
|
G.Name ->
|
||||||
|
G.Name ->
|
||||||
|
(InputFieldsParser n [AggregationPredicate b (UnpreparedValue b)]) ->
|
||||||
|
(InputFieldsParser n (Maybe (AggregationPredicatesImplementation b (UnpreparedValue b))))
|
||||||
|
relAggregateField rel typeGqlName relGqlName =
|
||||||
|
P.fieldOptional (relGqlName <> Name.__ <> Name._aggregate) Nothing
|
||||||
|
. P.object typeGqlName Nothing
|
||||||
|
. fmap (AggregationPredicatesImplementation rel)
|
||||||
|
|
||||||
|
-- Input field for a single aggregation predicate.
|
||||||
|
aggPredicateField ::
|
||||||
|
G.Name ->
|
||||||
|
G.Name ->
|
||||||
|
InputFieldsParser n (AggregationPredicate b (UnpreparedValue b)) ->
|
||||||
|
InputFieldsParser n (Maybe (AggregationPredicate b (UnpreparedValue b)))
|
||||||
|
aggPredicateField fnGQLName typeGqlName =
|
||||||
|
P.fieldOptional fnGQLName Nothing . P.object (typeGqlName <> Name.__ <> fnGQLName) Nothing
|
||||||
|
|
||||||
|
buildAnyOptionalFields :: NonEmpty (MaybeT m (InputFieldsParser n (Maybe c))) -> MaybeT m (InputFieldsParser n [c])
|
||||||
|
buildAnyOptionalFields = fmap collectOptionalFields . collectBranchesNE
|
||||||
|
where
|
||||||
|
-- Collect a non-empty list of optional input field parsers into one input field
|
||||||
|
-- parser parsing a list of the specified values.
|
||||||
|
collectOptionalFields :: NonEmpty (InputFieldsParser n (Maybe a)) -> InputFieldsParser n [a]
|
||||||
|
collectOptionalFields = fmap (catMaybes . NE.toList) . sequenceA
|
||||||
|
|
||||||
|
buildAllFieldsNE :: MaybeT m (NonEmpty (InputFieldsParser n c)) -> MaybeT m (InputFieldsParser n (NonEmpty c))
|
||||||
|
buildAllFieldsNE = fmap sequenceA
|
||||||
|
|
||||||
|
-- Collect all the non-failed branches, failing if all branches failed.
|
||||||
|
collectBranchesNE :: forall f a. Applicative f => NonEmpty (MaybeT f a) -> MaybeT f (NonEmpty a)
|
||||||
|
collectBranchesNE xs = MaybeT $ NE.nonEmpty . catMaybes . NE.toList <$> sequenceA (xs <&> runMaybeT)
|
||||||
|
|
||||||
|
-- Mark a computation as potentially failing.
|
||||||
|
fails :: m (Maybe a) -> MaybeT m a
|
||||||
|
fails = MaybeT
|
||||||
|
|
||||||
|
-- Compose our monad with InputFieldsParser into one fused Applicative that
|
||||||
|
-- acts on the parsed values directly.
|
||||||
|
fuse :: MaybeT m (InputFieldsParser n a) -> Compose (MaybeT m) (InputFieldsParser n) a
|
||||||
|
fuse = Compose
|
||||||
|
|
||||||
|
-- The inverse of 'fuse'.
|
||||||
|
unfuse :: Compose (MaybeT m) (InputFieldsParser n) a -> MaybeT m (InputFieldsParser n a)
|
||||||
|
unfuse = getCompose
|
||||||
|
|
||||||
|
-- Optional input field with a default value when the field is elided or null.
|
||||||
|
fieldOptionalDefault ::
|
||||||
|
forall k a. ('Input P.<: k) => G.Name -> Maybe G.Description -> a -> Parser k n a -> InputFieldsParser n a
|
||||||
|
fieldOptionalDefault n d a p = fromMaybe a <$> P.fieldOptional n d p
|
||||||
|
|
||||||
|
data FunctionSignature (b :: BackendType) = FunctionSignature
|
||||||
|
{ fnName :: Text,
|
||||||
|
fnGQLName :: G.Name,
|
||||||
|
fnArguments :: ArgumentsSignature b,
|
||||||
|
fnReturnType :: B.ScalarType b
|
||||||
|
}
|
||||||
|
|
||||||
|
data ArgumentsSignature (b :: BackendType)
|
||||||
|
= ArgumentsStar
|
||||||
|
| Arguments (NonEmpty (ArgumentSignature b))
|
||||||
|
|
||||||
|
data ArgumentSignature (b :: BackendType) = ArgumentSignature
|
||||||
|
{ argType :: B.ScalarType b,
|
||||||
|
argName :: G.Name
|
||||||
|
}
|
@ -59,6 +59,7 @@ import Data.Text.Casing qualified as C
|
|||||||
import Data.Text.Extended
|
import Data.Text.Extended
|
||||||
import Hasura.GraphQL.ApolloFederation
|
import Hasura.GraphQL.ApolloFederation
|
||||||
import Hasura.GraphQL.Schema.Backend (BackendTableSelectSchema (..), MonadBuildSchema)
|
import Hasura.GraphQL.Schema.Backend (BackendTableSelectSchema (..), MonadBuildSchema)
|
||||||
|
import Hasura.GraphQL.Schema.BoolExp (AggregationPredicatesSchema)
|
||||||
import Hasura.GraphQL.Schema.Common
|
import Hasura.GraphQL.Schema.Common
|
||||||
import Hasura.GraphQL.Schema.Mutation
|
import Hasura.GraphQL.Schema.Mutation
|
||||||
import Hasura.GraphQL.Schema.NamingCase
|
import Hasura.GraphQL.Schema.NamingCase
|
||||||
@ -104,6 +105,7 @@ setFieldNameCase tCase tInfo crf getFieldName tableName =
|
|||||||
buildTableQueryAndSubscriptionFields ::
|
buildTableQueryAndSubscriptionFields ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
|
AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b
|
||||||
) =>
|
) =>
|
||||||
MkRootFieldName ->
|
MkRootFieldName ->
|
||||||
@ -200,6 +202,7 @@ buildTableQueryAndSubscriptionFields mkRootFieldName sourceInfo tableName tableI
|
|||||||
buildTableStreamingSubscriptionFields ::
|
buildTableStreamingSubscriptionFields ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
|
AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b
|
||||||
) =>
|
) =>
|
||||||
MkRootFieldName ->
|
MkRootFieldName ->
|
||||||
@ -276,6 +279,7 @@ buildTableInsertMutationFields backendInsertAction mkRootFieldName scenario sour
|
|||||||
buildTableUpdateMutationFields ::
|
buildTableUpdateMutationFields ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
|
AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b
|
||||||
) =>
|
) =>
|
||||||
-- | an action that builds @BackendUpdate@ with the
|
-- | an action that builds @BackendUpdate@ with the
|
||||||
@ -319,6 +323,7 @@ buildTableUpdateMutationFields mkBackendUpdate mkRootFieldName scenario sourceIn
|
|||||||
buildTableDeleteMutationFields ::
|
buildTableDeleteMutationFields ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
|
AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b
|
||||||
) =>
|
) =>
|
||||||
MkRootFieldName ->
|
MkRootFieldName ->
|
||||||
|
@ -353,6 +353,7 @@ mkInsertObject objects tableInfo backendInsert insertPerms updatePerms =
|
|||||||
deleteFromTable ::
|
deleteFromTable ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
|
AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b
|
||||||
) =>
|
) =>
|
||||||
Scenario ->
|
Scenario ->
|
||||||
|
@ -149,7 +149,8 @@ defaultSelectTable sourceInfo tableInfo fieldName description = runMaybeT do
|
|||||||
selectTableConnection ::
|
selectTableConnection ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b,
|
||||||
|
AggregationPredicatesSchema b
|
||||||
) =>
|
) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
-- | table info
|
-- | table info
|
||||||
@ -368,9 +369,10 @@ cause errors on the client side, for the following reasons:
|
|||||||
-- > }
|
-- > }
|
||||||
defaultTableSelectionSet ::
|
defaultTableSelectionSet ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b,
|
BackendTableSelectSchema b,
|
||||||
Eq (AnnBoolExp b (IR.UnpreparedValue b))
|
Eq (AnnBoolExp b (IR.UnpreparedValue b)),
|
||||||
|
MonadBuildSchema b r m n
|
||||||
) =>
|
) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableInfo b ->
|
TableInfo b ->
|
||||||
@ -582,7 +584,7 @@ tableConnectionSelectionSet sourceInfo tableInfo = runMaybeT do
|
|||||||
-- > where: table_bool_exp
|
-- > where: table_bool_exp
|
||||||
defaultTableArgs ::
|
defaultTableArgs ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
MonadBuildSchema b r m n =>
|
(MonadBuildSchema b r m n, AggregationPredicatesSchema b) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableInfo b ->
|
TableInfo b ->
|
||||||
m (InputFieldsParser n (SelectArgs b))
|
m (InputFieldsParser n (SelectArgs b))
|
||||||
@ -630,7 +632,9 @@ defaultTableArgs sourceInfo tableInfo = do
|
|||||||
-- > where: table_bool_exp
|
-- > where: table_bool_exp
|
||||||
tableWhereArg ::
|
tableWhereArg ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
MonadBuildSchema b r m n =>
|
( AggregationPredicatesSchema b,
|
||||||
|
MonadBuildSchema b r m n
|
||||||
|
) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableInfo b ->
|
TableInfo b ->
|
||||||
m (InputFieldsParser n (Maybe (IR.AnnBoolExp b (IR.UnpreparedValue b))))
|
m (InputFieldsParser n (Maybe (IR.AnnBoolExp b (IR.UnpreparedValue b))))
|
||||||
@ -723,7 +727,7 @@ tableOffsetArg =
|
|||||||
-- > after: String
|
-- > after: String
|
||||||
tableConnectionArgs ::
|
tableConnectionArgs ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
MonadBuildSchema b r m n =>
|
(MonadBuildSchema b r m n, AggregationPredicatesSchema b) =>
|
||||||
PrimaryKeyColumns b ->
|
PrimaryKeyColumns b ->
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableInfo b ->
|
TableInfo b ->
|
||||||
@ -971,9 +975,10 @@ tableAggregationFields sourceInfo tableInfo =
|
|||||||
-- > field_name(arg_name: arg_type, ...): field_type
|
-- > field_name(arg_name: arg_type, ...): field_type
|
||||||
fieldSelection ::
|
fieldSelection ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b,
|
BackendTableSelectSchema b,
|
||||||
Eq (AnnBoolExp b (IR.UnpreparedValue b))
|
Eq (AnnBoolExp b (IR.UnpreparedValue b)),
|
||||||
|
MonadBuildSchema b r m n
|
||||||
) =>
|
) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableName b ->
|
TableName b ->
|
||||||
@ -1124,9 +1129,10 @@ join) satisfies `p(T)`.
|
|||||||
-- | Field parsers for a table relationship
|
-- | Field parsers for a table relationship
|
||||||
relationshipField ::
|
relationshipField ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b,
|
BackendTableSelectSchema b,
|
||||||
Eq (AnnBoolExp b (IR.UnpreparedValue b))
|
Eq (AnnBoolExp b (IR.UnpreparedValue b)),
|
||||||
|
MonadBuildSchema b r m n
|
||||||
) =>
|
) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableName b ->
|
TableName b ->
|
||||||
|
@ -14,6 +14,7 @@ import Data.Text.Extended ((<>>))
|
|||||||
import Hasura.Base.Error (QErr)
|
import Hasura.Base.Error (QErr)
|
||||||
import Hasura.GraphQL.Parser.Class
|
import Hasura.GraphQL.Parser.Class
|
||||||
import Hasura.GraphQL.Schema.Backend
|
import Hasura.GraphQL.Schema.Backend
|
||||||
|
import Hasura.GraphQL.Schema.BoolExp (AggregationPredicatesSchema)
|
||||||
import Hasura.GraphQL.Schema.Common
|
import Hasura.GraphQL.Schema.Common
|
||||||
import Hasura.GraphQL.Schema.NamingCase
|
import Hasura.GraphQL.Schema.NamingCase
|
||||||
import Hasura.GraphQL.Schema.Options qualified as Options
|
import Hasura.GraphQL.Schema.Options qualified as Options
|
||||||
@ -218,7 +219,9 @@ tableStreamCursorArg sourceInfo tableInfo = do
|
|||||||
-- > table_stream (cursor: [table_stream_cursor_input]!, batch_size: Int!, where: table_bool_exp)
|
-- > table_stream (cursor: [table_stream_cursor_input]!, batch_size: Int!, where: table_bool_exp)
|
||||||
tableStreamArguments ::
|
tableStreamArguments ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
MonadBuildSchema b r m n =>
|
( AggregationPredicatesSchema b,
|
||||||
|
MonadBuildSchema b r m n
|
||||||
|
) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableInfo b ->
|
TableInfo b ->
|
||||||
m (InputFieldsParser n (SelectStreamArgs b))
|
m (InputFieldsParser n (SelectStreamArgs b))
|
||||||
@ -241,6 +244,7 @@ tableStreamArguments sourceInfo tableInfo = do
|
|||||||
selectStreamTable ::
|
selectStreamTable ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
|
AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b
|
||||||
) =>
|
) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
module Hasura.GraphQL.Schema.Table
|
module Hasura.GraphQL.Schema.Table
|
||||||
( getTableGQLName,
|
( getTableGQLName,
|
||||||
tableSelectColumnsEnum,
|
tableSelectColumnsEnum,
|
||||||
|
tableSelectColumnsPredEnum,
|
||||||
tableUpdateColumnsEnum,
|
tableUpdateColumnsEnum,
|
||||||
updateColumnsPlaceholderParser,
|
updateColumnsPlaceholderParser,
|
||||||
tableSelectPermissions,
|
tableSelectPermissions,
|
||||||
@ -16,6 +17,7 @@ where
|
|||||||
import Data.Has
|
import Data.Has
|
||||||
import Data.HashMap.Strict qualified as Map
|
import Data.HashMap.Strict qualified as Map
|
||||||
import Data.HashSet qualified as Set
|
import Data.HashSet qualified as Set
|
||||||
|
import Data.Text (pack)
|
||||||
import Data.Text.Casing qualified as C
|
import Data.Text.Casing qualified as C
|
||||||
import Data.Text.Extended
|
import Data.Text.Extended
|
||||||
import Hasura.Base.Error (QErr)
|
import Hasura.Base.Error (QErr)
|
||||||
@ -83,7 +85,7 @@ getTableIdentifierName tableInfo =
|
|||||||
-- permissions for.
|
-- permissions for.
|
||||||
tableSelectColumnsEnum ::
|
tableSelectColumnsEnum ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
MonadBuildSchema b r m n =>
|
(Backend b, MonadBuildSchemaBase r m n) =>
|
||||||
SourceInfo b ->
|
SourceInfo b ->
|
||||||
TableInfo b ->
|
TableInfo b ->
|
||||||
m (Maybe (Parser 'Both n (Column b)))
|
m (Maybe (Parser 'Both n (Column b)))
|
||||||
@ -108,6 +110,42 @@ tableSelectColumnsEnum sourceInfo tableInfo = do
|
|||||||
define name =
|
define name =
|
||||||
P.Definition name (Just $ G.Description "column name") Nothing [] P.EnumValueInfo
|
P.Definition name (Just $ G.Description "column name") Nothing [] P.EnumValueInfo
|
||||||
|
|
||||||
|
-- | Table select columns enum of a certain type.
|
||||||
|
--
|
||||||
|
-- Parser for an enum type that matches, of a given table, certain columns which
|
||||||
|
-- satisfy a predicate. Used as a parameter for aggregation predicate
|
||||||
|
-- arguments, among others. Maps to the table_select_column object.
|
||||||
|
--
|
||||||
|
-- Return Nothing if there's no column the current user has "select"
|
||||||
|
-- permissions for.
|
||||||
|
tableSelectColumnsPredEnum ::
|
||||||
|
forall b r m n.
|
||||||
|
MonadBuildSchema b r m n =>
|
||||||
|
(ColumnType b -> Bool) ->
|
||||||
|
G.Name ->
|
||||||
|
SourceInfo b ->
|
||||||
|
TableInfo b ->
|
||||||
|
m (Maybe (Parser 'Both n (Column b)))
|
||||||
|
tableSelectColumnsPredEnum columnPredicate predName sourceInfo tableInfo = do
|
||||||
|
tableGQLName <- getTableGQLName @b tableInfo
|
||||||
|
columns <- filter (columnPredicate . ciType) <$> tableSelectColumns sourceInfo tableInfo
|
||||||
|
enumName <- mkTypename $ tableGQLName <> Name.__select_column <> Name.__ <> predName
|
||||||
|
let description =
|
||||||
|
Just $
|
||||||
|
G.Description $
|
||||||
|
pack ("select " ++ show predName ++ "columns of table ") <>> tableInfoName tableInfo
|
||||||
|
pure $
|
||||||
|
P.enum enumName description
|
||||||
|
<$> nonEmpty
|
||||||
|
[ ( define $ ciName column,
|
||||||
|
ciColumn column
|
||||||
|
)
|
||||||
|
| column <- columns
|
||||||
|
]
|
||||||
|
where
|
||||||
|
define name =
|
||||||
|
P.Definition name (Just $ G.Description "column name") Nothing [] P.EnumValueInfo
|
||||||
|
|
||||||
-- | Table update columns enum
|
-- | Table update columns enum
|
||||||
--
|
--
|
||||||
-- Parser for an enum type that matches the columns of the given
|
-- Parser for an enum type that matches the columns of the given
|
||||||
|
@ -25,7 +25,7 @@ import Data.Text.Extended (toTxt, (<>>))
|
|||||||
import Hasura.Base.Error (QErr)
|
import Hasura.Base.Error (QErr)
|
||||||
import Hasura.Base.ToErrorValue
|
import Hasura.Base.ToErrorValue
|
||||||
import Hasura.GraphQL.Schema.Backend (BackendSchema (..), BackendTableSelectSchema (..), MonadBuildSchema, columnParser)
|
import Hasura.GraphQL.Schema.Backend (BackendSchema (..), BackendTableSelectSchema (..), MonadBuildSchema, columnParser)
|
||||||
import Hasura.GraphQL.Schema.BoolExp (boolExp)
|
import Hasura.GraphQL.Schema.BoolExp (AggregationPredicatesSchema, boolExp)
|
||||||
import Hasura.GraphQL.Schema.Common (Scenario (..), SchemaContext (..), mapField, partialSQLExpToUnpreparedValue, retrieve)
|
import Hasura.GraphQL.Schema.Common (Scenario (..), SchemaContext (..), mapField, partialSQLExpToUnpreparedValue, retrieve)
|
||||||
import Hasura.GraphQL.Schema.Mutation (mutationSelectionSet, primaryKeysArguments)
|
import Hasura.GraphQL.Schema.Mutation (mutationSelectionSet, primaryKeysArguments)
|
||||||
import Hasura.GraphQL.Schema.NamingCase
|
import Hasura.GraphQL.Schema.NamingCase
|
||||||
@ -256,6 +256,7 @@ incOp = UpdateOperator {..}
|
|||||||
updateTable ::
|
updateTable ::
|
||||||
forall b r m n.
|
forall b r m n.
|
||||||
( MonadBuildSchema b r m n,
|
( MonadBuildSchema b r m n,
|
||||||
|
AggregationPredicatesSchema b,
|
||||||
BackendTableSelectSchema b
|
BackendTableSelectSchema b
|
||||||
) =>
|
) =>
|
||||||
-- | backend-specific data needed to perform an update mutation
|
-- | backend-specific data needed to perform an update mutation
|
||||||
|
@ -606,3 +606,14 @@ __Entity = [G.name|_Entity|]
|
|||||||
|
|
||||||
__entities :: G.Name
|
__entities :: G.Name
|
||||||
__entities = [G.name|_entities|]
|
__entities = [G.name|_entities|]
|
||||||
|
|
||||||
|
-- * Aggregation Predicates
|
||||||
|
|
||||||
|
_arguments :: G.Name
|
||||||
|
_arguments = [G.name|arguments|]
|
||||||
|
|
||||||
|
_predicate :: G.Name
|
||||||
|
_predicate = [G.name|predicate|]
|
||||||
|
|
||||||
|
_filter :: G.Name
|
||||||
|
_filter = [G.name|filter|]
|
||||||
|
@ -60,6 +60,7 @@ module Hasura.RQL.Types.Table
|
|||||||
sortCols,
|
sortCols,
|
||||||
tableInfoName,
|
tableInfoName,
|
||||||
getRolePermInfo,
|
getRolePermInfo,
|
||||||
|
tableArrayRelationships,
|
||||||
tcCustomName,
|
tcCustomName,
|
||||||
tcCustomRootFields,
|
tcCustomRootFields,
|
||||||
tcComment,
|
tcComment,
|
||||||
@ -929,6 +930,9 @@ tiName = tiCoreInfo . tciName
|
|||||||
tableInfoName :: TableInfo b -> TableName b
|
tableInfoName :: TableInfo b -> TableName b
|
||||||
tableInfoName = view tiName
|
tableInfoName = view tiName
|
||||||
|
|
||||||
|
tableArrayRelationships :: TableInfo b -> [RelInfo b]
|
||||||
|
tableArrayRelationships ti = [rel | rel <- getRels . _tciFieldInfoMap . _tiCoreInfo $ ti, riType rel == ArrRel]
|
||||||
|
|
||||||
getRolePermInfo :: RoleName -> TableInfo b -> RolePermInfo b
|
getRolePermInfo :: RoleName -> TableInfo b -> RolePermInfo b
|
||||||
getRolePermInfo role tableInfo
|
getRolePermInfo role tableInfo
|
||||||
| role == adminRoleName = _tiAdminRolePermInfo tableInfo
|
| role == adminRoleName = _tiAdminRolePermInfo tableInfo
|
||||||
|
Loading…
Reference in New Issue
Block a user