diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs index d1d3ec559f7fb..898968a06b94d 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Context.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Context.hs @@ -3,7 +3,6 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} module Hasura.GraphQL.Resolve.Context ( FieldMap @@ -26,9 +25,6 @@ module Hasura.GraphQL.Resolve.Context , module Hasura.GraphQL.Utils ) where -import Data.Aeson -import Data.Aeson.Casing -import Data.Aeson.TH import Data.Has import Hasura.Prelude @@ -68,13 +64,6 @@ type OrdByResolveCtxElem = (PGColInfo, OrdTy, NullsOrder) type OrdByResolveCtx = Map.HashMap (G.NamedType, G.EnumValue) OrdByResolveCtxElem -data InsertTxConflictCtx - = InsertTxConflictCtx - { itcAction :: !ConflictAction - , itcConstraint :: !(Maybe ConstraintName) - } deriving (Show, Eq) -$(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''InsertTxConflictCtx) - getFldInfo :: (MonadError QErr m, MonadReader r m, Has FieldMap r) => G.NamedType -> G.Name -> m (Either PGColInfo (RelInfo, S.BoolExp, Maybe Int, Bool)) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs index 75ba3b06972ae..0fc4f13325a40 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs @@ -13,11 +13,7 @@ module Hasura.GraphQL.Resolve.Mutation import Data.Has import Hasura.Prelude -import qualified Data.Aeson.Text as AT -import qualified Data.ByteString.Builder as BB import qualified Data.HashMap.Strict as Map -import qualified Data.Text.Lazy as LT -import qualified Database.PG.Query as Q import qualified Language.GraphQL.Draft.Syntax as G import qualified Hasura.RQL.DML.Delete as RD @@ -73,12 +69,10 @@ convertRowObj val = let prepExp = fromMaybe (S.SEUnsafe "NULL") prepExpM return (PGCol $ G.unName k, prepExp) -type ConflictCtx = (ConflictAction, Maybe ConstraintName) - mkConflictClause :: (MonadError QErr m) => [PGCol] - -> ConflictCtx + -> RI.ConflictCtx -> m RI.ConflictClauseP1 mkConflictClause cols (act, conM) = case (act , conM) of (CAIgnore, Nothing) -> return $ RI.CP1DoNothing Nothing @@ -112,7 +106,7 @@ parseConstraint obj = do parseOnConflict :: (MonadError QErr m) - => AnnGValue -> m ConflictCtx + => AnnGValue -> m RI.ConflictCtx parseOnConflict val = flip withObject val $ \_ obj -> do action <- parseAction obj @@ -129,12 +123,13 @@ convertInsert role (tn, vn) tableCols fld = do rows <- withArg arguments "objects" asRowExps conflictCtxM <- withPathK "on_conflict" $ withArgM arguments "on_conflict" parseOnConflict + onConflictM <- mapM (mkConflictClause tableCols) conflictCtxM mutFlds <- convertMutResp (_fType fld) $ _fSelSet fld args <- get + let p1Query = RI.InsertQueryP1 tn vn tableCols rows onConflictM mutFlds + p1 = (p1Query, args) return $ - bool (nonAdminInsert args rows conflictCtxM mutFlds) - (adminInsert args rows conflictCtxM mutFlds) - $ isAdmin role + bool (RI.nonAdminInsert p1) (RI.insertP2 p1) $ isAdmin role where arguments = _fArguments fld asRowExps = withArray (const $ mapM rowExpWithDefaults) @@ -144,27 +139,6 @@ convertInsert role (tn, vn) tableCols fld = do defVals = Map.fromList $ zip tableCols (repeat $ S.SEUnsafe "DEFAULT") - adminInsert args rows conflictCtxM mutFlds = do - onConflictM <- mapM (mkConflictClause tableCols) conflictCtxM - let p1 = RI.InsertQueryP1 tn vn tableCols rows onConflictM mutFlds - RI.insertP2 (p1, args) - - nonAdminInsert args rows conflictCtxM mutFlds = do - mapM_ (mkConflictClause tableCols) conflictCtxM - setConflictCtxTx conflictCtxM - let p1 = RI.InsertQueryP1 tn vn tableCols rows Nothing mutFlds - RI.insertP2 (p1, args) - - setConflictCtxTx conflictCtxM = do - let t = maybe "null" conflictCtxToJSON conflictCtxM - setVal = toSQL $ S.SELit t - setVar = BB.string7 "SET LOCAL hasura.conflict_clause = " - q = Q.fromBuilder $ setVar <> setVal - Q.unitQE defaultTxErrorHandler q () False - - conflictCtxToJSON (act, constrM) = - LT.toStrict $ AT.encodeToLazyText $ InsertTxConflictCtx act constrM - type ApplySQLOp = (PGCol, S.SQLExp) -> S.SQLExp rhsExpOp :: S.SQLOp -> S.AnnType -> ApplySQLOp diff --git a/server/src-lib/Hasura/RQL/DML/Insert.hs b/server/src-lib/Hasura/RQL/DML/Insert.hs index 316dd42c377ff..e9d96fbc318f4 100644 --- a/server/src-lib/Hasura/RQL/DML/Insert.hs +++ b/server/src-lib/Hasura/RQL/DML/Insert.hs @@ -8,8 +8,11 @@ module Hasura.RQL.DML.Insert where import Data.Aeson.Types import Instances.TH.Lift () +import qualified Data.Aeson.Text as AT +import qualified Data.ByteString.Builder as BB import qualified Data.HashMap.Strict as HM import qualified Data.Sequence as DS +import qualified Data.Text.Lazy as LT import Hasura.Prelude import Hasura.RQL.DML.Internal @@ -172,6 +175,7 @@ convInsertQuery objsParser prepFn (InsertQuery tableName val oC mRetCols) = do unless (ipiAllowUpsert insPerm) $ throw400 PermissionDenied $ "upsert is not allowed for role" <>> roleName buildConflictClause tableInfo c + return $ InsertQueryP1 tableName insView insCols insTuples conflictClause mutFlds @@ -198,11 +202,50 @@ insertP2 (u, p) = where insertSQL = toSQL $ mkSQLInsert u +type ConflictCtx = (ConflictAction, Maybe ConstraintName) + +nonAdminInsert :: (InsertQueryP1, DS.Seq Q.PrepArg) -> Q.TxE QErr RespBody +nonAdminInsert (insQueryP1, args) = do + conflictCtxM <- mapM extractConflictCtx conflictClauseP1 + setConflictCtx conflictCtxM + insertP2 (withoutConflictClause, args) + where + withoutConflictClause = insQueryP1{iqp1Conflict=Nothing} + conflictClauseP1 = iqp1Conflict insQueryP1 + +extractConflictCtx :: (MonadError QErr m) => ConflictClauseP1 -> m ConflictCtx +extractConflictCtx cp = + case cp of + (CP1DoNothing mConflictTar) -> do + mConstraintName <- mapM extractConstraintName mConflictTar + return (CAIgnore, mConstraintName) + (CP1Update conflictTar _) -> do + constraintName <- extractConstraintName conflictTar + return (CAUpdate, Just constraintName) + where + extractConstraintName (Constraint cn) = return cn + extractConstraintName _ = throw400 NotSupported + "\"constraint_on\" not supported for non admin insert. use \"constraint\" instead" + +setConflictCtx :: Maybe ConflictCtx -> Q.TxE QErr () +setConflictCtx conflictCtxM = do + let t = maybe "null" conflictCtxToJSON conflictCtxM + setVal = toSQL $ S.SELit t + setVar = BB.string7 "SET LOCAL hasura.conflict_clause = " + q = Q.fromBuilder $ setVar <> setVal + Q.unitQE defaultTxErrorHandler q () False + where + conflictCtxToJSON (act, constrM) = + LT.toStrict $ AT.encodeToLazyText $ InsertTxConflictCtx act constrM + instance HDBQuery InsertQuery where type Phase1Res InsertQuery = (InsertQueryP1, DS.Seq Q.PrepArg) phaseOne = convInsQ - phaseTwo _ = liftTx . insertP2 + phaseTwo _ p1Res = do + role <- userRole <$> ask + liftTx $ + bool (nonAdminInsert p1Res) (insertP2 p1Res) $ isAdmin role schemaCachePolicy = SCPNoChange diff --git a/server/src-lib/Hasura/RQL/Types/DML.hs b/server/src-lib/Hasura/RQL/Types/DML.hs index 8fcd69a2169fc..9a4ccff63554c 100644 --- a/server/src-lib/Hasura/RQL/Types/DML.hs +++ b/server/src-lib/Hasura/RQL/Types/DML.hs @@ -29,6 +29,8 @@ module Hasura.RQL.Types.DML , ConflictAction(..) , ConstraintOn(..) + , InsertTxConflictCtx(..) + , UpdVals , UpdateQuery(..) @@ -268,6 +270,13 @@ data InsertQuery $(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''InsertQuery) +data InsertTxConflictCtx + = InsertTxConflictCtx + { itcAction :: !ConflictAction + , itcConstraint :: !(Maybe ConstraintName) + } deriving (Show, Eq) +$(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''InsertTxConflictCtx) + type UpdVals = M.HashMap PGCol Value data UpdateQuery diff --git a/server/test/Spec.hs b/server/test/Spec.hs index a579b65ffb73e..09543ddfcc918 100644 --- a/server/test/Spec.hs +++ b/server/test/Spec.hs @@ -43,6 +43,10 @@ querySpecFiles = , "create_address_resident_relationship_error.yaml" , "create_user_permission_address.yaml" , "create_author_permission_role_admin_error.yaml" + , "create_user_permission_test_table.yaml" + , "all_json_queries.yaml" + , "upsert_role_user.yaml" + , "upsert_role_user_error.yaml" ] gqlSpecFiles :: [FilePath] diff --git a/server/test/testcases/all_json_queries.yaml b/server/test/testcases/all_json_queries.yaml new file mode 100644 index 0000000000000..8e16752f64fe4 --- /dev/null +++ b/server/test/testcases/all_json_queries.yaml @@ -0,0 +1,56 @@ +description: Select, Insert, Upsert, Update and Delete JSON queries +url: /v1/query +status: 200 +query: + type: bulk + args: + - type: select + args: + table: author + columns: + - id + - name + - type: insert + args: + table: test_table + objects: + - name: erlich + age: 30 + - name: gilfoyle + age: 27 + returing: + - id + - name + - age + - type: update + args: + table: test_table + where: + id: 1 + $set: + age: 31 + returing: + - id + - name + - age + - type: insert + args: + table: test_table + objects: + - id: 2 + name: galvin + age: 45 + on_conflict: + constraint_on: + - id + action: update + returing: + - id + - name + - age + - type: delete + args: + table: test_table + where: + id: 2 + diff --git a/server/test/testcases/create_tables.yaml b/server/test/testcases/create_tables.yaml index a4a8d42c74371..4f6fc2c359df3 100644 --- a/server/test/testcases/create_tables.yaml +++ b/server/test/testcases/create_tables.yaml @@ -58,3 +58,11 @@ query: placed TIMESTAMPTZ NOT NULL, shipped TIMESTAMPTZ ) + - type: run_sql + args: + sql: | + CREATE TABLE test_table ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + age INTEGER NOT NULL + ) diff --git a/server/test/testcases/create_user_permission_test_table.yaml b/server/test/testcases/create_user_permission_test_table.yaml new file mode 100644 index 0000000000000..4dc5a2ddfebf4 --- /dev/null +++ b/server/test/testcases/create_user_permission_test_table.yaml @@ -0,0 +1,12 @@ +description: Create a insert permission on test_table for user role +url: /v1/query +status: 200 +query: + type: create_insert_permission + args: + table: test_table + role: user + permission: + check: + id: X-Hasura-User-Id + allow_upsert: true diff --git a/server/test/testcases/track_tables.yaml b/server/test/testcases/track_tables.yaml index 20099336deecf..4749906313c85 100644 --- a/server/test/testcases/track_tables.yaml +++ b/server/test/testcases/track_tables.yaml @@ -28,4 +28,8 @@ query: args: schema: public name: orders + - type: track_table + args: + schema: public + name: test_table diff --git a/server/test/testcases/upsert_role_user.yaml b/server/test/testcases/upsert_role_user.yaml new file mode 100644 index 0000000000000..9b1e8a60b8838 --- /dev/null +++ b/server/test/testcases/upsert_role_user.yaml @@ -0,0 +1,17 @@ +description: upsert into test_table with user role +url: v1/query +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '1' +status: 200 +query: + type: insert + args: + table: test_table + objects: + - id: 1 + name: monika + age: 25 + on_conflict: + constraint: test_table_pkey + action: update diff --git a/server/test/testcases/upsert_role_user_error.yaml b/server/test/testcases/upsert_role_user_error.yaml new file mode 100644 index 0000000000000..735a6154f77d6 --- /dev/null +++ b/server/test/testcases/upsert_role_user_error.yaml @@ -0,0 +1,19 @@ +description: Upsert on test_table as user role with constraint on columns (error) +url: /v1/query +headers: + X-Hasura-Role: user + X-Hasura-User-Id: '1' +status: 400 +query: + type: insert + args: + table: test_table + objects: + - id: 1 + name: monika + age: 25 + on_conflict: + constraint_on: + - id + action: ignore +