graphql-engine/server/src-lib/Hasura/GraphQL/Schema/BoolExp/AggregationPredicates.hs
Antoine Leblanc 42e5205eb5 server: reduce schema contexts to the bare minimum
### Description

This monster of a PR took way too long. As the title suggests, it reduces the schema context carried in the readers to the very strict minimum. In practice, that means that to build a source, we only require:
  - the global `SchemaContext`
  - the global `SchemaOptions` (soon to be renamed `SchemaSourceOptions`)
  - that source's `SourceInfo`

Furthermore, _we no longer carry "default" customization options throughout the schema_. All customization information is extracted from the `SourceInfo`, when required. This prevents an entire category of bugs we had previously encountered, such as parts of the code using uninitialized / unupdated customization info.

In turn, this meant that we could remove the explicit threading of the `SourceInfo` throughout the schema, since it is now always available through the reader context.

Finally, this meant making a few adjustments to relay and actions as well, such as the introduction of a new separate "context" for actions, and a change to how we create some of the action-specific postgres scalar parsers.

I'll highlight with review comments the areas of interest.

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6709
GitOrigin-RevId: ea80fddcb24e2513779dd04b0b700a55f0028dd1
2022-11-17 10:35:54 +00:00

206 lines
9.2 KiB
Haskell

{-# 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
import Hasura.GraphQL.Schema.Options (IncludeAggregationPredicates (..))
import Hasura.GraphQL.Schema.Options qualified as Options
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 qualified as IR
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.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.
( MonadBuildSchema b r m n,
AggregationPredicatesSchema b
) =>
[FunctionSignature b] ->
TableInfo b ->
SchemaT r m (Maybe (InputFieldsParser n [AggregationPredicatesImplementation b (UnpreparedValue b)]))
defaultAggregationPredicatesParser aggFns ti = runMaybeT do
-- Check in schema options whether we should include aggregation predicates
include <- retrieve Options.soIncludeAggregationPredicates
case include of
IncludeAggregationPredicates -> return ()
Don'tIncludeAggregationPredicates -> fails $ return Nothing
arrayRelationships <- fails $ return $ nonEmpty $ tableArrayRelationships ti
aggregationFunctions <- fails $ return $ nonEmpty aggFns
roleName <- retrieve scRole
collectOptionalFieldsNE . succeedingBranchesNE $
arrayRelationships <&> \rel -> do
relTable <- askTableInfo $ riRTable rel
selectPermissions <- hoistMaybe $ tableSelectPermissions roleName relTable
guard $ spiAllowAgg selectPermissions
let rowPermissions = fmap partialSQLExpToUnpreparedValue <$> spiFilter selectPermissions
relGqlName <- textToName $ relNameToTxt $ riName rel
typeGqlName <- (<> Name.__ <> Name._aggregate_bool_exp) <$> getTableGQLName relTable
-- We only make a field for aggregations over a relation if at least
-- some aggregation predicates are callable.
relAggregateField rel relGqlName typeGqlName rowPermissions
-- We only return an InputFieldsParser for aggregation predicates,
-- if we parse at least one aggregation predicate
<$> (collectOptionalFieldsNE . succeedingBranchesNE)
( aggregationFunctions <&> \FunctionSignature {..} -> do
let relFunGqlName = typeGqlName <> Name.__ <> fnGQLName <> Name.__ <> Name._arguments <> Name.__ <> Name._columns
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 relTable))
SingleArgument typ ->
AggregationPredicateArguments . (NE.:| [])
<$> fuse
( P.field Name._arguments Nothing
<$> fails (tableSelectColumnsPredEnum (== (ColumnScalar typ)) relFunGqlName relTable)
)
Arguments args ->
AggregationPredicateArguments
<$> fuse
( P.field Name._arguments Nothing
. P.object (typeGqlName <> Name.__ <> fnGQLName <> Name.__ <> Name._arguments) Nothing
<$> collectFieldsNE
( args `for` \ArgumentSignature {..} ->
P.field argName Nothing <$> fails (tableSelectColumnsPredEnum (== (ColumnScalar argType)) relFunGqlName 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 relTable)
pure $ AggregationPredicate {..}
)
where
-- Input field of the aggregation predicates for one array relation.
relAggregateField ::
RelInfo b ->
G.Name ->
G.Name ->
(IR.AnnBoolExp b (UnpreparedValue b)) ->
(InputFieldsParser n [AggregationPredicate b (UnpreparedValue b)]) ->
(InputFieldsParser n (Maybe (AggregationPredicatesImplementation b (UnpreparedValue b))))
relAggregateField rel relGqlName typeGqlName rowPermissions =
P.fieldOptional (relGqlName <> Name.__ <> Name._aggregate) Nothing
. P.object typeGqlName Nothing
. fmap (AggregationPredicatesImplementation rel rowPermissions)
. ( `P.bindFields`
\case
[predicate] -> pure predicate
_ -> P.parseError "exactly one predicate should be specified"
)
-- 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
-- Collect all non-failing branches of optional field parsers.
-- Fails only when all branches fail.
-- buildAnyOptionalFields ::
-- Applicative f =>
-- NonEmpty (MaybeT f (InputFieldsParser n (Maybe c))) ->
-- MaybeT f (InputFieldsParser n [c])
-- buildAnyOptionalFields = fmap collectOptionalFields . succeedingBranchesNE
-- where
-- Collect all the non-failed branches, failing if all branches failed.
succeedingBranchesNE :: forall f a. Applicative f => NonEmpty (MaybeT f a) -> MaybeT f (NonEmpty a)
succeedingBranchesNE xs = MaybeT $ NE.nonEmpty . catMaybes . NE.toList <$> sequenceA (xs <&> runMaybeT)
-- Collect a non-empty list of input field parsers into one input field
-- parser parsing a non-empty list of the specified values.
collectFieldsNE ::
Functor f =>
MaybeT f (NonEmpty (InputFieldsParser n c)) ->
MaybeT f (InputFieldsParser n (NonEmpty c))
collectFieldsNE = fmap sequenceA
-- Collect a non-empty list of optional input field parsers into one input field
-- parser parsing a list of the specified values.
collectOptionalFieldsNE ::
Functor f =>
MaybeT f (NonEmpty (InputFieldsParser n (Maybe a))) ->
MaybeT f (InputFieldsParser n [a])
collectOptionalFieldsNE = fmap $ fmap (catMaybes . NE.toList) . sequenceA
-- Mark a computation as potentially failing.
fails :: f (Maybe a) -> MaybeT f a
fails = MaybeT
-- Compose our monad with InputFieldsParser into one fused Applicative that
-- acts on the parsed values directly.
fuse :: MaybeT f (InputFieldsParser n a) -> Compose (MaybeT f) (InputFieldsParser n) a
fuse = Compose
-- The inverse of 'fuse'.
unfuse :: Compose (MaybeT f) (InputFieldsParser n) a -> MaybeT f (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
| SingleArgument (B.ScalarType b)
| Arguments (NonEmpty (ArgumentSignature b))
data ArgumentSignature (b :: BackendType) = ArgumentSignature
{ argType :: B.ScalarType b,
argName :: G.Name
}