graphql-engine/server/src-lib/Hasura/RQL/DML/Update.hs
Vamshi Surabhi b84db36ebb
allow custom mutations through actions (#3042)
* basic doc for actions

* custom_types, sync and async actions

* switch to graphql-parser-hs on github

* update docs

* metadata import/export

* webhook calls are now supported

* relationships in sync actions

* initialise.sql is now in sync with the migration file

* fix metadata tests

* allow specifying arguments of actions

* fix blacklist check on check_build_worthiness job

* track custom_types and actions related tables

* handlers are now triggered on async actions

* default to pgjson unless a field is involved in relationships, for generating definition list

* use 'true' for action filter for non admin role

* fix create_action_permission sql query

* drop permissions when dropping an action

* add a hdb_role view (and relationships) to fetch all roles in the system

* rename 'webhook' key in action definition to 'handler'

* allow templating actions wehook URLs with env vars

* add 'update_action' /v1/query type

* allow forwarding client headers by setting `forward_client_headers` in action definition

* add 'headers' configuration in action definition

* handle webhook error response based on status codes

* support array relationships for custom types

* implement single row mutation, see https://github.com/hasura/graphql-engine/issues/3731

* single row mutation: rename 'pk_columns' -> 'columns' and no-op refactor

* use top level primary key inputs for delete_by_pk & account select permissions for single row mutations

* use only REST semantics to resolve the webhook response

* use 'pk_columns' instead of 'columns' for update_by_pk input

* add python basic tests for single row mutations

* add action context (name) in webhook payload

* Async action response is accessible for non admin roles only if
  the request session vars equals to action's

* clean nulls, empty arrays for actions, custom types in export metadata

* async action mutation returns only the UUID of the action

* unit tests for URL template parser

* Basic sync actions python tests

* fix output in async query & add async tests

* add admin secret header in async actions python test

* document async action architecture in Resolve/Action.hs file

* support actions returning array of objects

* tests for list type response actions

* update docs with actions and custom types metadata API reference

* update actions python tests as per #f8e1330

Co-authored-by: Tirumarai Selvan <tirumarai.selvan@gmail.com>
Co-authored-by: Aravind Shankar <face11301@gmail.com>
Co-authored-by: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com>
2020-02-13 23:08:23 +05:30

239 lines
7.3 KiB
Haskell

module Hasura.RQL.DML.Update
( validateUpdateQueryWith
, validateUpdateQuery
, AnnUpdG(..)
, traverseAnnUpd
, AnnUpd
, updateQueryToTx
, runUpdate
) where
import Data.Aeson.Types
import Instances.TH.Lift ()
import qualified Data.HashMap.Strict as M
import qualified Data.Sequence as DS
import Hasura.EncJSON
import Hasura.Prelude
import Hasura.RQL.DML.Insert (insertCheckExpr)
import Hasura.RQL.DML.Internal
import Hasura.RQL.DML.Mutation
import Hasura.RQL.DML.Returning
import Hasura.RQL.GBoolExp
import Hasura.RQL.Instances ()
import Hasura.RQL.Types
import Hasura.SQL.Types
import qualified Database.PG.Query as Q
import qualified Hasura.SQL.DML as S
data AnnUpdG v
= AnnUpd
{ uqp1Table :: !QualifiedTable
, uqp1SetExps :: ![(PGCol, v)]
, uqp1Where :: !(AnnBoolExp v, AnnBoolExp v)
, upq1Check :: !(AnnBoolExp v)
-- we don't prepare the arguments for returning
-- however the session variable can still be
-- converted as desired
, uqp1Output :: !(MutationOutputG v)
, uqp1AllCols :: ![PGColumnInfo]
} deriving (Show, Eq)
traverseAnnUpd
:: (Applicative f)
=> (a -> f b)
-> AnnUpdG a
-> f (AnnUpdG b)
traverseAnnUpd f annUpd =
AnnUpd tn
<$> traverse (traverse f) setExps
<*> ((,) <$> traverseAnnBoolExp f whr <*> traverseAnnBoolExp f fltr)
<*> traverseAnnBoolExp f chk
<*> traverseMutationOutput f mutOutput
<*> pure allCols
where
AnnUpd tn setExps (whr, fltr) chk mutOutput allCols = annUpd
type AnnUpd = AnnUpdG S.SQLExp
mkUpdateCTE
:: AnnUpd -> S.CTE
mkUpdateCTE (AnnUpd tn setExps (permFltr, wc) chk _ _) =
S.CTEUpdate update
where
update =
S.SQLUpdate tn setExp Nothing tableFltr
. Just
. S.RetExp
$ [ S.selectStar
, S.Extractor (insertCheckExpr "update check constraint failed" checkExpr) Nothing
]
setExp = S.SetExp $ map S.SetExpItem setExps
tableFltr = Just $ S.WhereFrag tableFltrExpr
tableFltrExpr = toSQLBoolExp (S.QualTable tn) $ andAnnBoolExps permFltr wc
checkExpr = toSQLBoolExp (S.QualTable tn) chk
convInc
:: (QErrM m)
=> (PGColumnType -> Value -> m S.SQLExp)
-> PGCol
-> PGColumnType
-> Value
-> m (PGCol, S.SQLExp)
convInc f col colType val = do
prepExp <- f colType val
return (col, S.SEOpApp S.incOp [S.mkSIdenExp col, prepExp])
convMul
:: (QErrM m)
=> (PGColumnType -> Value -> m S.SQLExp)
-> PGCol
-> PGColumnType
-> Value
-> m (PGCol, S.SQLExp)
convMul f col colType val = do
prepExp <- f colType val
return (col, S.SEOpApp S.mulOp [S.mkSIdenExp col, prepExp])
convSet
:: (QErrM m)
=> (PGColumnType -> Value -> m S.SQLExp)
-> PGCol
-> PGColumnType
-> Value
-> m (PGCol, S.SQLExp)
convSet f col colType val = do
prepExp <- f colType val
return (col, prepExp)
convDefault :: (Monad m) => PGCol -> PGColumnType -> () -> m (PGCol, S.SQLExp)
convDefault col _ _ = return (col, S.SEUnsafe "DEFAULT")
convOp
:: (UserInfoM m, QErrM m)
=> FieldInfoMap FieldInfo
-> [PGCol]
-> UpdPermInfo
-> [(PGCol, a)]
-> (PGCol -> PGColumnType -> a -> m (PGCol, S.SQLExp))
-> m [(PGCol, S.SQLExp)]
convOp fieldInfoMap preSetCols updPerm objs conv =
forM objs $ \(pgCol, a) -> do
-- if column has predefined value then throw error
when (pgCol `elem` preSetCols) $ throwNotUpdErr pgCol
checkPermOnCol PTUpdate allowedCols pgCol
colType <- askPGType fieldInfoMap pgCol relWhenPgErr
res <- conv pgCol colType a
-- build a set expression's entry
withPathK (getPGColTxt pgCol) $ return res
where
allowedCols = upiCols updPerm
relWhenPgErr = "relationships can't be updated"
throwNotUpdErr c = do
role <- userRole <$> askUserInfo
throw400 NotSupported $ "column " <> c <<> " is not updatable"
<> " for role " <> role <<> "; its value is predefined in permission"
validateUpdateQueryWith
:: (UserInfoM m, QErrM m, CacheRM m)
=> SessVarBldr m
-> (PGColumnType -> Value -> m S.SQLExp)
-> UpdateQuery
-> m AnnUpd
validateUpdateQueryWith sessVarBldr prepValBldr uq = do
let tableName = uqTable uq
tableInfo <- withPathK "table" $ askTabInfo tableName
let coreInfo = _tiCoreInfo tableInfo
-- If it is view then check if it is updatable
mutableView tableName viIsUpdatable
(_tciViewInfo coreInfo) "updatable"
-- Check if the role has update permissions
updPerm <- askUpdPermInfo tableInfo
-- Check if all dependent headers are present
validateHeaders $ upiRequiredHeaders updPerm
-- Check if select is allowed
selPerm <- modifyErr (<> selNecessaryMsg) $
askSelPermInfo tableInfo
let fieldInfoMap = _tciFieldInfoMap coreInfo
allCols = getCols fieldInfoMap
preSetObj = upiSet updPerm
preSetCols = M.keys preSetObj
-- convert the object to SQL set expression
setItems <- withPathK "$set" $
convOp fieldInfoMap preSetCols updPerm (M.toList $ uqSet uq) $ convSet prepValBldr
incItems <- withPathK "$inc" $
convOp fieldInfoMap preSetCols updPerm (M.toList $ uqInc uq) $ convInc prepValBldr
mulItems <- withPathK "$mul" $
convOp fieldInfoMap preSetCols updPerm (M.toList $ uqMul uq) $ convMul prepValBldr
defItems <- withPathK "$default" $
convOp fieldInfoMap preSetCols updPerm (zip (uqDefault uq) [()..]) convDefault
-- convert the returning cols into sql returing exp
mAnnRetCols <- forM mRetCols $ \retCols ->
withPathK "returning" $ checkRetCols fieldInfoMap selPerm retCols
resolvedPreSetItems <- M.toList <$>
mapM (convPartialSQLExp sessVarBldr) preSetObj
let setExpItems = resolvedPreSetItems ++ setItems ++ incItems ++
mulItems ++ defItems
when (null setExpItems) $
throw400 UnexpectedPayload "atleast one of $set, $inc, $mul has to be present"
-- convert the where clause
annSQLBoolExp <- withPathK "where" $
convBoolExp fieldInfoMap selPerm (uqWhere uq) sessVarBldr prepValBldr
resolvedUpdFltr <- convAnnBoolExpPartialSQL sessVarBldr $
upiFilter updPerm
resolvedUpdCheck <- fromMaybe gBoolExpTrue <$>
traverse (convAnnBoolExpPartialSQL sessVarBldr)
(upiCheck updPerm)
return $ AnnUpd
tableName
setExpItems
(resolvedUpdFltr, annSQLBoolExp)
resolvedUpdCheck
(mkDefaultMutFlds mAnnRetCols)
allCols
where
mRetCols = uqReturning uq
selNecessaryMsg =
"; \"update\" is only allowed if the role "
<> "has \"select\" permission as \"where\" can't be used "
<> "without \"select\" permission on the table"
validateUpdateQuery
:: (QErrM m, UserInfoM m, CacheRM m)
=> UpdateQuery -> m (AnnUpd, DS.Seq Q.PrepArg)
validateUpdateQuery =
runDMLP1T . validateUpdateQueryWith sessVarFromCurrentSetting binRHSBuilder
updateQueryToTx
:: Bool -> (AnnUpd, DS.Seq Q.PrepArg) -> Q.TxE QErr EncJSON
updateQueryToTx strfyNum (u, p) =
runMutation $ Mutation (uqp1Table u) (updateCTE, p)
(uqp1Output u) (uqp1AllCols u) strfyNum
where
updateCTE = mkUpdateCTE u
runUpdate
:: (QErrM m, UserInfoM m, CacheRM m, MonadTx m, HasSQLGenCtx m)
=> UpdateQuery -> m EncJSON
runUpdate q = do
strfyNum <- stringifyNum <$> askSQLGenCtx
validateUpdateQuery q >>= liftTx . updateQueryToTx strfyNum