2023-01-17 03:33:56 +03:00
module Hasura.Backends.DataConnector.Plan.MutationPlan
( mkMutationPlan ,
)
where
import Control.Monad.Trans.Writer.CPS qualified as CPS
import Data.Aeson qualified as J
import Data.Aeson.Encoding qualified as JE
import Data.Has ( Has , modifier )
import Data.HashMap.Strict qualified as HashMap
import Data.Text.Extended ( toTxt )
import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Backends.DataConnector.Adapter.Types
2023-01-20 09:16:39 +03:00
import Hasura.Backends.DataConnector.Adapter.Types.Mutations
2023-01-17 03:33:56 +03:00
import Hasura.Backends.DataConnector.Plan.Common
import Hasura.Backends.DataConnector.Plan.QueryPlan ( reshapeAnnFields , translateAnnFields )
import Hasura.Base.Error ( Code ( .. ) , QErr , throw400 , throw500 )
import Hasura.Prelude
2023-01-20 09:16:39 +03:00
import Hasura.RQL.IR.BoolExp ( GBoolExp ( .. ) )
2023-01-17 03:33:56 +03:00
import Hasura.RQL.IR.Delete
import Hasura.RQL.IR.Insert hiding ( Single )
import Hasura.RQL.IR.Returning
import Hasura.RQL.IR.Root
import Hasura.RQL.IR.Select
import Hasura.RQL.IR.Update
2023-01-20 09:16:39 +03:00
import Hasura.RQL.IR.Update.Batch
2023-01-17 03:33:56 +03:00
import Hasura.RQL.IR.Value
import Hasura.RQL.Types.Column
import Hasura.RQL.Types.Common
import Hasura.SQL.Backend ( BackendType ( .. ) )
import Hasura.Session ( SessionVariables )
import Language.GraphQL.Draft.Syntax qualified as G
import Witch qualified
--------------------------------------------------------------------------------
newtype TableInsertSchemas = TableInsertSchemas
{ unTableInsertSchemas :: HashMap API . TableName ( HashMap API . FieldName API . InsertFieldSchema ) }
deriving stock ( Eq , Show )
instance Semigroup TableInsertSchemas where
( TableInsertSchemas l ) <> ( TableInsertSchemas r ) = TableInsertSchemas $ HashMap . unionWith HashMap . union l r
instance Monoid TableInsertSchemas where
mempty = TableInsertSchemas mempty
recordTableInsertSchema ::
( Has TableInsertSchemas writerOutput ,
Monoid writerOutput ,
MonadError QErr m
) =>
API . TableName ->
HashMap API . FieldName API . InsertFieldSchema ->
CPS . WriterT writerOutput m ()
recordTableInsertSchema tableName fieldSchemas =
let newTableSchema = TableInsertSchemas $ HashMap . singleton tableName fieldSchemas
in CPS . tell $ modifier ( const newTableSchema ) mempty
--------------------------------------------------------------------------------
mkMutationPlan ::
MonadError QErr m =>
SessionVariables ->
MutationDB 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
m ( Plan API . MutationRequest API . MutationResponse )
mkMutationPlan sessionVariables mutationDB = do
request <- translateMutationDB sessionVariables mutationDB
pure $ Plan request ( reshapeResponseToMutationGqlShape mutationDB )
translateMutationDB ::
MonadError QErr m =>
SessionVariables ->
MutationDB 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
m API . MutationRequest
translateMutationDB sessionVariables = \ case
MDBInsert insert -> do
( insertOperation , ( tableRelationships , tableInsertSchemas ) ) <- CPS . runWriterT $ translateInsert sessionVariables insert
let apiTableRelationships = uncurry API . TableRelationships <$> HashMap . toList ( unTableRelationships tableRelationships )
let apiTableInsertSchema = uncurry API . TableInsertSchema <$> HashMap . toList ( unTableInsertSchemas tableInsertSchemas )
pure $
API . MutationRequest
{ _mrTableRelationships = apiTableRelationships ,
_mrInsertSchema = apiTableInsertSchema ,
_mrOperations = [ API . InsertOperation insertOperation ]
}
2023-01-20 09:16:39 +03:00
MDBUpdate update -> do
( updateOperations , tableRelationships ) <- CPS . runWriterT $ translateUpdate sessionVariables update
let apiTableRelationships = uncurry API . TableRelationships <$> HashMap . toList ( unTableRelationships tableRelationships )
pure $
API . MutationRequest
{ _mrTableRelationships = apiTableRelationships ,
_mrInsertSchema = [] ,
_mrOperations = API . UpdateOperation <$> updateOperations
}
2023-01-24 07:17:31 +03:00
MDBDelete delete -> do
( deleteOperation , tableRelationships ) <- CPS . runWriterT $ translateDelete sessionVariables delete
let apiTableRelationships = uncurry API . TableRelationships <$> HashMap . toList ( unTableRelationships tableRelationships )
pure $
API . MutationRequest
{ _mrTableRelationships = apiTableRelationships ,
_mrInsertSchema = [] ,
_mrOperations = [ API . DeleteOperation deleteOperation ]
}
2023-01-17 03:33:56 +03:00
MDBFunction _returnsSet _select ->
throw400 NotSupported " translateMutationDB: function mutations not implemented for the Data Connector backend. "
translateInsert ::
MonadError QErr m =>
SessionVariables ->
AnnotatedInsert 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
CPS . WriterT ( TableRelationships , TableInsertSchemas ) m API . InsertMutationOperation
translateInsert sessionVariables AnnotatedInsert { .. } = do
rows <- traverse ( translateInsertRow sessionVariables tableName _aiTableColumns _aiPresetValues ) _aiInsertObject
postInsertCheck <- translateBoolExpToExpression sessionVariables tableName insertCheckCondition
returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _aiOutput
pure $
API . InsertMutationOperation
{ API . _imoTable = tableName ,
API . _imoRows = rows ,
API . _imoPostInsertCheck = postInsertCheck ,
API . _imoReturningFields = HashMap . mapKeys ( API . FieldName . getFieldNameTxt ) returningFields
}
where
tableName = Witch . from _aiTableName
2023-01-24 07:17:31 +03:00
-- Update check condition must be used once upserts are supported
( insertCheckCondition , _updateCheckCondition ) = _aiCheckCondition
2023-01-17 03:33:56 +03:00
AnnotatedInsertData { .. } = _aiData
translateInsertRow ::
( Has TableInsertSchemas writerOutput ,
Monoid writerOutput ,
MonadError QErr m
) =>
SessionVariables ->
API . TableName ->
[ ColumnInfo 'DataConnector ] ->
HashMap ColumnName ( UnpreparedValue 'DataConnector ) ->
AnnotatedInsertRow 'DataConnector ( UnpreparedValue 'DataConnector ) ->
CPS . WriterT writerOutput m API . RowObject
translateInsertRow sessionVariables tableName tableColumns defaultColumnValues insertRow = do
columnSchemasAndValues <- lift $ forM ( HashMap . toList columnUnpreparedValues ) $ \ ( columnName , columnValue ) -> do
fieldName <-
case find ( \ ColumnInfo { .. } -> ciColumn == columnName ) tableColumns of
Just ColumnInfo { .. } -> pure . API . FieldName $ G . unName ciName
Nothing -> throw500 $ " Can't find column " <> toTxt columnName <> " in table schema for " <> API . tableNameToText tableName
preparedLiteral <- prepareLiteral sessionVariables columnValue
( scalarType , value ) <-
case preparedLiteral of
ValueLiteral scalarType value -> pure ( scalarType , value )
ArrayLiteral _scalarType _values -> throw400 NotSupported " translateInsertRow: Array literals are not supported as column insert values "
let columnInsertSchema = API . ColumnInsertSchema ( Witch . from columnName ) ( Witch . from scalarType )
pure ( fieldName , columnInsertSchema , value )
let fieldSchemas =
columnSchemasAndValues
& fmap ( \ ( fieldName , columnInsertSchema , _ ) -> ( fieldName , API . ColumnInsert columnInsertSchema ) )
& HashMap . fromList
recordTableInsertSchema tableName fieldSchemas
let rowObject =
columnSchemasAndValues
& fmap ( \ ( fieldName , _ , value ) -> ( fieldName , API . mkColumnInsertFieldValue value ) )
& HashMap . fromList
& API . RowObject
pure rowObject
where
columnUnpreparedValues :: HashMap ColumnName ( UnpreparedValue 'DataConnector )
columnUnpreparedValues = HashMap . union rowColumnValues defaultColumnValues
rowColumnValues :: HashMap ColumnName ( UnpreparedValue 'DataConnector )
rowColumnValues =
insertRow
& fmap ( \ ( AIColumn columnNameAndValue ) -> columnNameAndValue )
& HashMap . fromList
2023-01-20 09:16:39 +03:00
translateUpdate ::
2023-01-17 03:33:56 +03:00
MonadError QErr m =>
SessionVariables ->
2023-01-20 09:16:39 +03:00
AnnotatedUpdateG 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
CPS . WriterT TableRelationships m [ API . UpdateMutationOperation ]
translateUpdate sessionVariables annUpdate @ AnnotatedUpdateG { .. } = do
case _auUpdateVariant of
SingleBatch batch -> ( : [] ) <$> translateUpdateBatch sessionVariables annUpdate batch
MultipleBatches batches -> traverse ( translateUpdateBatch sessionVariables annUpdate ) batches
translateUpdateBatch ::
MonadError QErr m =>
SessionVariables ->
AnnotatedUpdateG 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
UpdateBatch 'DataConnector UpdateOperator ( UnpreparedValue 'DataConnector ) ->
CPS . WriterT TableRelationships m API . UpdateMutationOperation
translateUpdateBatch sessionVariables AnnotatedUpdateG { .. } UpdateBatch { .. } = do
updates <- lift $ translateUpdateOperations sessionVariables _ubOperations
whereExp <- translateBoolExpToExpression sessionVariables tableName ( BoolAnd [ _auUpdatePermissions , _ubWhere ] )
postUpdateCheck <- translateBoolExpToExpression sessionVariables tableName _auCheck
returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _auOutput
pure $
API . UpdateMutationOperation
{ API . _umoTable = tableName ,
API . _umoWhere = whereExp ,
API . _umoUpdates = updates ,
API . _umoPostUpdateCheck = postUpdateCheck ,
API . _umoReturningFields = HashMap . mapKeys ( API . FieldName . getFieldNameTxt ) returningFields
}
where
tableName = Witch . from _auTable
translateUpdateOperations ::
forall m .
MonadError QErr m =>
SessionVariables ->
HashMap ColumnName ( UpdateOperator ( UnpreparedValue 'DataConnector ) ) ->
m [ API . RowUpdate ]
translateUpdateOperations sessionVariables columnUpdates =
forM ( HashMap . toList columnUpdates ) $ \ ( columnName , updateOperator ) -> do
let ( mkRowUpdate , value ) =
case updateOperator of
UpdateSet value' -> ( API . SetColumn , value' )
UpdateCustomOperator operatorName value' -> ( API . CustomUpdateColumnOperator operatorName , value' )
( scalarType , literalValue ) <- prepareAndExtractLiteralValue value
let operatorValue = API . RowColumnOperatorValue ( Witch . from columnName ) literalValue ( Witch . from scalarType )
pure $ mkRowUpdate operatorValue
where
prepareAndExtractLiteralValue :: UnpreparedValue 'DataConnector -> m ( ScalarType , J . Value )
prepareAndExtractLiteralValue unpreparedValue = do
preparedLiteral <- prepareLiteral sessionVariables unpreparedValue
case preparedLiteral of
ValueLiteral scalarType value -> pure ( scalarType , value )
ArrayLiteral _scalarType _values -> throw400 NotSupported " translateUpdateOperations: Array literals are not supported as column update values "
2023-01-24 07:17:31 +03:00
translateDelete ::
MonadError QErr m =>
SessionVariables ->
AnnDelG 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
CPS . WriterT TableRelationships m API . DeleteMutationOperation
translateDelete sessionVariables AnnDel { .. } = do
whereExp <- translateBoolExpToExpression sessionVariables tableName ( BoolAnd [ permissionFilter , whereClause ] )
returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _adOutput
pure $
API . DeleteMutationOperation
{ API . _dmoTable = tableName ,
API . _dmoWhere = whereExp ,
API . _dmoReturningFields = HashMap . mapKeys ( API . FieldName . getFieldNameTxt ) returningFields
}
where
tableName = Witch . from _adTable
( permissionFilter , whereClause ) = _adWhere
2023-01-20 09:16:39 +03:00
translateMutationOutputToReturningFields ::
( MonadError QErr m ,
Has TableRelationships writerOutput ,
Monoid writerOutput
) =>
SessionVariables ->
2023-01-17 03:33:56 +03:00
API . TableName ->
MutationOutputG 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
2023-01-20 09:16:39 +03:00
CPS . WriterT writerOutput m ( HashMap FieldName API . Field )
2023-01-17 03:33:56 +03:00
translateMutationOutputToReturningFields sessionVariables tableName = \ case
MOutSinglerowObject annFields ->
translateAnnFields sessionVariables noPrefix tableName annFields
MOutMultirowFields mutFields ->
HashMap . unions <$> traverse ( uncurry $ translateMutField sessionVariables tableName ) mutFields
translateMutField ::
2023-01-20 09:16:39 +03:00
( MonadError QErr m ,
Has TableRelationships writerOutput ,
Monoid writerOutput
) =>
2023-01-17 03:33:56 +03:00
SessionVariables ->
API . TableName ->
FieldName ->
MutFldG 'DataConnector Void ( UnpreparedValue 'DataConnector ) ->
2023-01-20 09:16:39 +03:00
CPS . WriterT writerOutput m ( HashMap FieldName API . Field )
2023-01-17 03:33:56 +03:00
translateMutField sessionVariables tableName fieldName = \ case
MCount ->
-- All mutation operations in a request return their affected rows count.
-- The count can just be added to the response JSON during agent response reshaping
pure mempty
MExp _text ->
-- We ignore literal text fields (we don't send them to the data connector agent)
-- and add them back to the response JSON when we reshape what the agent returns
-- to us
pure mempty
MRet annFields ->
translateAnnFields sessionVariables ( prefixWith fieldName ) tableName annFields
--------------------------------------------------------------------------------
reshapeResponseToMutationGqlShape ::
MonadError QErr m =>
MutationDB 'DataConnector Void v ->
API . MutationResponse ->
m J . Encoding
2023-01-20 09:16:39 +03:00
reshapeResponseToMutationGqlShape mutationDb mutationResponse = do
case mutationDb of
MDBInsert AnnotatedInsert { .. } ->
reshapeOutputForSingleBatchOperation _aiOutput mutationResponse
MDBUpdate AnnotatedUpdateG { .. } ->
case _auUpdateVariant of
SingleBatch _batch ->
reshapeOutputForSingleBatchOperation _auOutput mutationResponse
MultipleBatches batches ->
let outputs = replicate ( length batches ) _auOutput
in reshapeOutputForMultipleBatchOperation outputs mutationResponse
MDBDelete AnnDel { .. } ->
reshapeOutputForSingleBatchOperation _adOutput mutationResponse
MDBFunction _returnsSet _select ->
throw400 NotSupported " reshapeResponseToMutationGqlShape: function mutations not implemented for the Data Connector backend. "
reshapeOutputForSingleBatchOperation ::
MonadError QErr m =>
MutationOutputG 'DataConnector Void v ->
API . MutationResponse ->
m J . Encoding
reshapeOutputForSingleBatchOperation mutationOutput API . MutationResponse { .. } = do
2023-01-17 03:33:56 +03:00
mutationOperationResult <-
listToMaybe _mrOperationResults
` onNothing ` throw500 " Unable to find expected mutation operation results "
2023-01-20 09:16:39 +03:00
reshapeMutationOutput mutationOutput mutationOperationResult
2023-01-17 03:33:56 +03:00
2023-01-20 09:16:39 +03:00
reshapeOutputForMultipleBatchOperation ::
MonadError QErr m =>
[ MutationOutputG 'DataConnector Void v ] ->
API . MutationResponse ->
m J . Encoding
reshapeOutputForMultipleBatchOperation mutationOutputs API . MutationResponse { .. } = do
unless ( operationResultCount >= requiredResultCount ) $
throw500 ( " Data Connector agent returned " <> tshow operationResultCount <> " mutation operation results where at least " <> tshow requiredResultCount <> " was expected " )
2023-01-17 03:33:56 +03:00
2023-01-20 09:16:39 +03:00
reshapedResults <-
zip mutationOutputs _mrOperationResults
& traverse ( uncurry reshapeMutationOutput )
pure $ JE . list id reshapedResults
where
requiredResultCount = length mutationOutputs
operationResultCount = length _mrOperationResults
2023-01-17 03:33:56 +03:00
reshapeMutationOutput ::
MonadError QErr m =>
MutationOutputG 'DataConnector Void v ->
API . MutationOperationResults ->
m J . Encoding
reshapeMutationOutput mutationOutput mutationOperationResults =
case mutationOutput of
MOutSinglerowObject annFields -> reshapeReturningRows Single noPrefix annFields mutationOperationResults
MOutMultirowFields mutFields -> reshapeMutFields mutFields mutationOperationResults
reshapeReturningRows ::
MonadError QErr m =>
Cardinality ->
FieldPrefix ->
AnnFieldsG 'DataConnector Void v ->
API . MutationOperationResults ->
m J . Encoding
reshapeReturningRows cardinality fieldNamePrefix annFields API . MutationOperationResults { .. } =
case cardinality of
Single ->
case rows of
[] -> pure $ J . toEncoding J . Null
[ singleRow ] -> reshapeAnnFields fieldNamePrefix annFields singleRow
_multipleRows ->
throw500 " Data Connector agent returned multiple rows in a mutation operation result when only one was expected "
Many -> do
reshapedRows <- traverse ( reshapeAnnFields fieldNamePrefix annFields ) rows
pure $ JE . list id reshapedRows
where
rows = fromMaybe mempty _morReturning
reshapeMutFields ::
MonadError QErr m =>
MutFldsG 'DataConnector Void v ->
API . MutationOperationResults ->
m J . Encoding
reshapeMutFields mutFields mutationOperationResults @ API . MutationOperationResults { .. } = do
reshapedMutFields <- forM mutFields $ \ ( fieldName @ ( FieldName fieldNameText ) , mutField ) -> do
reshapedFieldValue <-
case mutField of
MCount -> pure $ JE . int _morAffectedRows
MExp literalText -> pure $ JE . text literalText
MRet annFields -> reshapeReturningRows Many ( prefixWith fieldName ) annFields mutationOperationResults
pure ( fieldNameText , reshapedFieldValue )
pure $ encodeAssocListAsObject reshapedMutFields