mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 12:31:52 +03:00
512a4dbb92
### Description This PR changes all the schema code to operate in a specific `SchemaT` monad, rather than in an arbitrary `m` monad. `SchemaT` is intended to be used opaquely with `runSourceSchema` and `runRemoteSchema`. The main goal of this is to allow a different reader context per part of the schema: this PR also minimizes the contexts. This means that we no longer require `SchemaOptions` when building remote schemas' schema, and this PR therefore removes a lot of dummy / placeholder values accordingly. ### Performance and stacking This PR has been through several iterations. #5339 was the original version, that accomplished the same thing by stacking readers on top of the stack at every remote relationship boundary. This raised performance concerns, and @0x777 confirmed with an ad-hoc test that in some extreme cases we could see up to a 10% performance impact. This version, while more verbose, allows us to unstack / re-stack the readers, and avoid that problem. #5517 adds a new benchmark set to be able to automatically measure this on every PR. ### Remaining work - [x] a comment (or perhaps even a Note?) should be added to `SchemaT` - [x] we probably want for #5517 to be merged first so that we can confirm the lack of performance penalty PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5458 GitOrigin-RevId: e06b83d90da475f745b838f1fd8f8b4d9d3f4b10
352 lines
14 KiB
Haskell
352 lines
14 KiB
Haskell
{-# LANGUAGE ApplicativeDo #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
|
|
-- | This module provides common building blocks for composing Schema Parsers
|
|
-- used in the schema of Update Mutations.
|
|
module Hasura.GraphQL.Schema.Update
|
|
( UpdateOperator (..),
|
|
updateOperator,
|
|
buildUpdateOperators,
|
|
presetColumns,
|
|
setOp,
|
|
incOp,
|
|
updateTable,
|
|
updateTableByPk,
|
|
mkUpdateObject,
|
|
)
|
|
where
|
|
|
|
import Data.Has (Has (getter))
|
|
import Data.HashMap.Strict qualified as M
|
|
import Data.HashMap.Strict.Extended qualified as M
|
|
import Data.List.NonEmpty qualified as NE
|
|
import Data.Text.Casing (GQLNameIdentifier, fromAutogeneratedName)
|
|
import Data.Text.Extended (toTxt, (<>>))
|
|
import Hasura.Base.ToErrorValue
|
|
import Hasura.GraphQL.Schema.Backend (BackendSchema (..), BackendTableSelectSchema (..), MonadBuildSchema, columnParser)
|
|
import Hasura.GraphQL.Schema.BoolExp (AggregationPredicatesSchema, boolExp)
|
|
import Hasura.GraphQL.Schema.Common
|
|
import Hasura.GraphQL.Schema.Mutation (mutationSelectionSet, primaryKeysArguments)
|
|
import Hasura.GraphQL.Schema.NamingCase
|
|
import Hasura.GraphQL.Schema.Parser qualified as P
|
|
import Hasura.GraphQL.Schema.Table (getTableIdentifierName, tableColumns, tableUpdateColumns)
|
|
import Hasura.GraphQL.Schema.Typename
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.IR.BoolExp (AnnBoolExp, annBoolExpTrue)
|
|
import Hasura.RQL.IR.Returning (MutationOutputG (..))
|
|
import Hasura.RQL.IR.Root (RemoteRelationshipField)
|
|
import Hasura.RQL.IR.Update (AnnotatedUpdateG (..))
|
|
import Hasura.RQL.IR.Value
|
|
import Hasura.RQL.Types.Backend (Backend (..))
|
|
import Hasura.RQL.Types.Column (ColumnInfo (..), isNumCol)
|
|
import Hasura.RQL.Types.Metadata.Object
|
|
import Hasura.RQL.Types.Source
|
|
import Hasura.RQL.Types.SourceCustomization (applyFieldNameCaseIdentifier, applyTypeNameCaseIdentifier, mkTableOperatorInputTypeName, mkTablePkColumnsInputTypeName)
|
|
import Hasura.RQL.Types.Table
|
|
import Hasura.SQL.AnyBackend qualified as AB
|
|
import Language.GraphQL.Draft.Syntax (Description (..), Name (..), Nullability (..), litName)
|
|
|
|
-- | @UpdateOperator b m n op@ represents one single update operator for a
|
|
-- backend @b@.
|
|
--
|
|
-- The type variable @op@ is the backend-specific data type that represents
|
|
-- update operators, typically in the form of a sum-type with an
|
|
-- @UnpreparedValue b@ in each constructor.
|
|
--
|
|
-- The @UpdateOperator b m n@ is a @Functor@. There exist building blocks of
|
|
-- common update operators (such as 'setOp', etc.) which have @op ~
|
|
-- UnpreparedValue b@. The Functor instance lets you wrap the generic update
|
|
-- operators in backend-specific tags.
|
|
data UpdateOperator b r m n op = UpdateOperator
|
|
{ updateOperatorApplicableColumn :: ColumnInfo b -> Bool,
|
|
updateOperatorParser ::
|
|
GQLNameIdentifier ->
|
|
TableName b ->
|
|
NonEmpty (ColumnInfo b) ->
|
|
SchemaT r m (P.InputFieldsParser n (HashMap (Column b) op))
|
|
}
|
|
deriving (Functor)
|
|
|
|
-- | The top-level component for building update operators parsers.
|
|
--
|
|
-- * It implements the @preset@ functionality from Update Permissions (see
|
|
-- <https://hasura.io/docs/latest/graphql/core/auth/authorization/permission-rules.html#column-presets
|
|
-- Permissions user docs>). Use the 'presetColumns' function to extract those from the update permissions.
|
|
-- * It validates that that the update fields parsed are sound when taken as a
|
|
-- whole, i.e. that some changes are actually specified (either in the
|
|
-- mutation query text or in update preset columns) and that each column is
|
|
-- only used in one operator.
|
|
buildUpdateOperators ::
|
|
forall b r m n op.
|
|
MonadBuildSchema b r m n =>
|
|
-- | Columns with @preset@ expressions
|
|
(HashMap (Column b) op) ->
|
|
-- | Update operators to include in the Schema
|
|
[UpdateOperator b r m n op] ->
|
|
TableInfo b ->
|
|
SchemaT r m (P.InputFieldsParser n (HashMap (Column b) op))
|
|
buildUpdateOperators presetCols ops tableInfo = do
|
|
parsers :: P.InputFieldsParser n [HashMap (Column b) op] <-
|
|
sequenceA . catMaybes <$> traverse (runUpdateOperator tableInfo) ops
|
|
pure $
|
|
parsers
|
|
`P.bindFields` ( \opExps -> do
|
|
let withPreset = presetCols : opExps
|
|
mergeDisjoint @b withPreset
|
|
)
|
|
|
|
-- | The columns that have 'preset' definitions applied to them. (see
|
|
-- <https://hasura.io/docs/latest/graphql/core/auth/authorization/permission-rules.html#column-presets
|
|
-- Permissions user docs>)
|
|
presetColumns :: UpdPermInfo b -> HashMap (Column b) (UnpreparedValue b)
|
|
presetColumns = fmap partialSQLExpToUnpreparedValue . upiSet
|
|
|
|
-- | Produce an InputFieldsParser from an UpdateOperator, but only if the operator
|
|
-- applies to the table (i.e., it admits a non-empty column set).
|
|
runUpdateOperator ::
|
|
forall b r m n op.
|
|
MonadBuildSchema b r m n =>
|
|
TableInfo b ->
|
|
UpdateOperator b r m n op ->
|
|
SchemaT
|
|
r
|
|
m
|
|
( Maybe
|
|
( P.InputFieldsParser
|
|
n
|
|
(HashMap (Column b) op)
|
|
)
|
|
)
|
|
runUpdateOperator tableInfo UpdateOperator {..} = do
|
|
let tableName = tableInfoName tableInfo
|
|
tableGQLName <- getTableIdentifierName tableInfo
|
|
roleName <- retrieve scRole
|
|
let columns = tableUpdateColumns roleName tableInfo
|
|
|
|
let applicableCols :: Maybe (NonEmpty (ColumnInfo b)) =
|
|
nonEmpty . filter updateOperatorApplicableColumn $ columns
|
|
|
|
(sequenceA :: Maybe (SchemaT r m a) -> SchemaT r m (Maybe a))
|
|
(applicableCols <&> updateOperatorParser tableGQLName tableName)
|
|
|
|
-- | Merge the results of parsed update operators. Throws an error if the same
|
|
-- column has been specified in multiple operators.
|
|
mergeDisjoint ::
|
|
forall b m t.
|
|
(Backend b, P.MonadParse m) =>
|
|
[HashMap (Column b) t] ->
|
|
m (HashMap (Column b) t)
|
|
mergeDisjoint parsedResults = do
|
|
let unioned = M.unionsAll parsedResults
|
|
duplicates = M.keys $ M.filter (not . null . NE.tail) unioned
|
|
|
|
unless (null duplicates) $
|
|
P.parseError
|
|
( "Column found in multiple operators: "
|
|
<> toErrorValue duplicates
|
|
<> "."
|
|
)
|
|
|
|
return $ M.map NE.head unioned
|
|
|
|
-- | Construct a parser for a single update operator.
|
|
--
|
|
-- @updateOperator _ "op" fp MkOp ["col1","col2"]@ gives a parser that accepts
|
|
-- objects in the shape of:
|
|
--
|
|
-- > op: {
|
|
-- > col1: "x",
|
|
-- > col2: "y"
|
|
-- > }
|
|
--
|
|
-- And (morally) parses into values:
|
|
--
|
|
-- > M.fromList [("col1", MkOp (fp "x")), ("col2", MkOp (fp "y"))]
|
|
updateOperator ::
|
|
forall n r m b a.
|
|
MonadBuildSchema b r m n =>
|
|
GQLNameIdentifier ->
|
|
GQLNameIdentifier ->
|
|
GQLNameIdentifier ->
|
|
(ColumnInfo b -> SchemaT r m (P.Parser 'P.Both n a)) ->
|
|
NonEmpty (ColumnInfo b) ->
|
|
Description ->
|
|
Description ->
|
|
SchemaT r m (P.InputFieldsParser n (HashMap (Column b) a))
|
|
updateOperator tableGQLName opName opFieldName mkParser columns opDesc objDesc = do
|
|
tCase <- asks getter
|
|
fieldParsers :: NonEmpty (P.InputFieldsParser n (Maybe (Column b, a))) <-
|
|
for columns \columnInfo -> do
|
|
let fieldName = ciName columnInfo
|
|
fieldDesc = ciDescription columnInfo
|
|
fieldParser <- mkParser columnInfo
|
|
pure $
|
|
P.fieldOptional fieldName fieldDesc fieldParser
|
|
`mapField` \value -> (ciColumn columnInfo, value)
|
|
|
|
objName <- mkTypename $ applyTypeNameCaseIdentifier tCase $ mkTableOperatorInputTypeName tableGQLName opName
|
|
pure $
|
|
fmap (M.fromList . (fold :: Maybe [(Column b, a)] -> [(Column b, a)])) $
|
|
P.fieldOptional (applyFieldNameCaseIdentifier tCase opFieldName) (Just opDesc) $
|
|
P.object objName (Just objDesc) $
|
|
(catMaybes . toList) <$> sequenceA fieldParsers
|
|
{-# ANN updateOperator ("HLint: ignore Use tuple-section" :: String) #-}
|
|
|
|
setOp ::
|
|
forall b n r m.
|
|
MonadBuildSchema b r m n =>
|
|
UpdateOperator b r m n (UnpreparedValue b)
|
|
setOp = UpdateOperator {..}
|
|
where
|
|
updateOperatorApplicableColumn = const True
|
|
|
|
updateOperatorParser tableGQLName tableName columns = do
|
|
let typedParser columnInfo =
|
|
fmap mkParameter
|
|
<$> columnParser
|
|
(ciType columnInfo)
|
|
(Nullability $ ciIsNullable columnInfo)
|
|
|
|
updateOperator
|
|
tableGQLName
|
|
(fromAutogeneratedName $$(litName "set"))
|
|
(fromAutogeneratedName $$(litName "_set"))
|
|
typedParser
|
|
columns
|
|
"sets the columns of the filtered rows to the given values"
|
|
(Description $ "input type for updating data in table " <>> tableName)
|
|
|
|
incOp ::
|
|
forall b m n r.
|
|
MonadBuildSchema b r m n =>
|
|
UpdateOperator b r m n (UnpreparedValue b)
|
|
incOp = UpdateOperator {..}
|
|
where
|
|
updateOperatorApplicableColumn = isNumCol
|
|
|
|
updateOperatorParser tableGQLName tableName columns = do
|
|
let typedParser columnInfo =
|
|
fmap mkParameter
|
|
<$> columnParser
|
|
(ciType columnInfo)
|
|
(Nullability $ ciIsNullable columnInfo)
|
|
|
|
updateOperator
|
|
tableGQLName
|
|
(fromAutogeneratedName $$(litName "inc"))
|
|
(fromAutogeneratedName $$(litName "_inc"))
|
|
typedParser
|
|
columns
|
|
"increments the numeric columns with given value of the filtered values"
|
|
(Description $ "input type for incrementing numeric columns in table " <>> tableName)
|
|
|
|
-- | Construct a root field, normally called update_tablename, that can be used
|
|
-- to update rows in a DB table specified by filters. Only returns a parser if
|
|
-- there are columns the user is allowed to update; otherwise returns Nothing.
|
|
updateTable ::
|
|
forall b r m n.
|
|
( MonadBuildSchema b r m n,
|
|
AggregationPredicatesSchema b,
|
|
BackendTableSelectSchema b
|
|
) =>
|
|
-- | backend-specific data needed to perform an update mutation
|
|
P.InputFieldsParser n (BackendUpdate b (UnpreparedValue b)) ->
|
|
Scenario ->
|
|
-- | table source
|
|
SourceInfo b ->
|
|
-- | table info
|
|
TableInfo b ->
|
|
-- | field display name
|
|
Name ->
|
|
-- | field description, if any
|
|
Maybe Description ->
|
|
SchemaT r m (Maybe (P.FieldParser n (AnnotatedUpdateG b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))))
|
|
updateTable backendUpdate scenario sourceInfo tableInfo fieldName description = runMaybeT do
|
|
let columns = tableColumns tableInfo
|
|
whereName = $$(litName "where")
|
|
whereDesc = "filter the rows which have to be updated"
|
|
viewInfo = _tciViewInfo $ _tiCoreInfo tableInfo
|
|
guard $ isMutable viIsUpdatable viewInfo
|
|
roleName <- retrieve scRole
|
|
updatePerms <- hoistMaybe $ _permUpd $ getRolePermInfo roleName tableInfo
|
|
-- If we're in a frontend scenario, we should not include backend_only updates
|
|
-- For more info see Note [Backend only permissions]
|
|
guard $ not $ scenario == Frontend && upiBackendOnly updatePerms
|
|
whereArg <- lift $ P.field whereName (Just whereDesc) <$> boolExp sourceInfo tableInfo
|
|
selection <- lift $ mutationSelectionSet sourceInfo tableInfo
|
|
tCase <- asks getter
|
|
let argsParser = liftA2 (,) backendUpdate whereArg
|
|
pure $
|
|
P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $
|
|
P.subselection fieldName description argsParser selection
|
|
<&> mkUpdateObject tableName columns updatePerms (Just tCase) . fmap MOutMultirowFields
|
|
where
|
|
sourceName = _siName sourceInfo
|
|
tableName = tableInfoName tableInfo
|
|
|
|
-- | Construct a root field, normally called 'update_tablename_by_pk', that can be used
|
|
-- to update a single in a DB table, specified by primary key. Only returns a
|
|
-- parser if there are columns the user is allowed to update and if the user has
|
|
-- select permissions on all primary keys; otherwise returns Nothing.
|
|
updateTableByPk ::
|
|
forall b r m n.
|
|
MonadBuildSchema b r m n =>
|
|
BackendTableSelectSchema b =>
|
|
-- | backend-specific data needed to perform an update mutation
|
|
P.InputFieldsParser n (BackendUpdate b (UnpreparedValue b)) ->
|
|
Scenario ->
|
|
-- | table source
|
|
SourceInfo b ->
|
|
-- | table info
|
|
TableInfo b ->
|
|
-- | field display name
|
|
Name ->
|
|
-- | field description, if any
|
|
Maybe Description ->
|
|
SchemaT r m (Maybe (P.FieldParser n (AnnotatedUpdateG b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))))
|
|
updateTableByPk backendUpdate scenario sourceInfo tableInfo fieldName description = runMaybeT $ do
|
|
let columns = tableColumns tableInfo
|
|
viewInfo = _tciViewInfo $ _tiCoreInfo tableInfo
|
|
guard $ isMutable viIsUpdatable viewInfo
|
|
roleName <- retrieve scRole
|
|
updatePerms <- hoistMaybe $ _permUpd $ getRolePermInfo roleName tableInfo
|
|
-- If we're in a frontend scenario, we should not include backend_only updates
|
|
-- For more info see Note [Backend only permissions]
|
|
guard $ not $ scenario == Frontend && upiBackendOnly updatePerms
|
|
pkArgs <- MaybeT $ primaryKeysArguments tableInfo
|
|
selection <- MaybeT $ tableSelectionSet sourceInfo tableInfo
|
|
tCase <- asks getter
|
|
lift $ do
|
|
tableGQLName <- getTableIdentifierName tableInfo
|
|
pkObjectName <- mkTypename $ applyTypeNameCaseIdentifier tCase $ mkTablePkColumnsInputTypeName tableGQLName
|
|
let pkFieldName = $$(litName "pk_columns")
|
|
pkObjectDesc = Description $ "primary key columns input for table: " <> toTxt tableName
|
|
pkParser = P.object pkObjectName (Just pkObjectDesc) pkArgs
|
|
argsParser = (,) <$> backendUpdate <*> P.field pkFieldName Nothing pkParser
|
|
pure $
|
|
P.setFieldParserOrigin (MOSourceObjId sourceName (AB.mkAnyBackend $ SMOTable @b tableName)) $
|
|
P.subselection fieldName description argsParser selection
|
|
<&> mkUpdateObject tableName columns updatePerms (Just tCase) . fmap MOutSinglerowObject
|
|
where
|
|
sourceName = _siName sourceInfo
|
|
tableName = tableInfoName tableInfo
|
|
|
|
mkUpdateObject ::
|
|
Backend b =>
|
|
TableName b ->
|
|
[ColumnInfo b] ->
|
|
UpdPermInfo b ->
|
|
(Maybe NamingCase) ->
|
|
( ( BackendUpdate b (UnpreparedValue b),
|
|
AnnBoolExp b (UnpreparedValue b)
|
|
),
|
|
MutationOutputG b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b)
|
|
) ->
|
|
AnnotatedUpdateG b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b)
|
|
mkUpdateObject _auTable _auAllCols updatePerms _auNamingConvention ((_auBackend, whereExp), _auOutput) =
|
|
AnnotatedUpdateG {..}
|
|
where
|
|
permissionFilter = fmap partialSQLExpToUnpreparedValue <$> upiFilter updatePerms
|
|
_auWhere = (permissionFilter, whereExp)
|
|
_auCheck = maybe annBoolExpTrue ((fmap . fmap) partialSQLExpToUnpreparedValue) $ upiCheck updatePerms
|