Files
graphql-engine/server/src-lib/Hasura/SQL/Value.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

252 lines
8.7 KiB
Haskell

module Hasura.SQL.Value
( PGScalarValue(..)
, pgColValueToInt
, pgScalarValueToJson
, withConstructorFn
, parsePGValue
, TxtEncodedPGVal(..)
, txtEncodedPGVal
, binEncoder
, txtEncoder
, toBinaryValue
, toTxtValue
, toPrepParam
) where
import Hasura.SQL.GeoJSON
import Hasura.SQL.Time
import Hasura.SQL.Types
import qualified Database.PG.Query as Q
import qualified Database.PG.Query.PTI as PTI
import qualified Hasura.SQL.DML as S
import Data.Aeson
import Data.Int
import Data.Scientific
import Data.Time
import Hasura.Prelude
import qualified Data.Aeson.Text as AE
import qualified Data.Aeson.Types as AT
import qualified Data.ByteString as B
import qualified Data.Text as T
import qualified Data.Text.Conversions as TC
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as TL
import qualified Data.UUID as UUID
import qualified Database.PostgreSQL.LibPQ as PQ
import qualified PostgreSQL.Binary.Encoding as PE
newtype RasterWKB
= RasterWKB { getRasterWKB :: TC.Base16 B.ByteString }
deriving (Show, Eq)
instance FromJSON RasterWKB where
parseJSON = \case
String t -> case TC.fromText t of
Just v -> return $ RasterWKB v
Nothing -> fail
"invalid hexadecimal representation of raster well known binary format"
_ -> fail "expecting String for raster"
instance ToJSON RasterWKB where
toJSON = toJSON . TC.toText . getRasterWKB
-- Binary value. Used in prepared sq
data PGScalarValue
= PGValInteger !Int32
| PGValSmallInt !Int16
| PGValBigInt !Int64
| PGValFloat !Float
| PGValDouble !Double
| PGValNumeric !Scientific
| PGValBoolean !Bool
| PGValChar !Char
| PGValVarchar !T.Text
| PGValText !T.Text
| PGValCitext !T.Text
| PGValDate !Day
| PGValTimeStampTZ !UTCTime
| PGValTimeTZ !ZonedTimeOfDay
| PGNull !PGScalarType
| PGValJSON !Q.JSON
| PGValJSONB !Q.JSONB
| PGValGeo !GeometryWithCRS
| PGValRaster !RasterWKB
| PGValUUID !UUID.UUID
| PGValUnknown !T.Text
deriving (Show, Eq)
pgScalarValueToJson :: PGScalarValue -> Value
pgScalarValueToJson = \case
PGValInteger i -> toJSON i
PGValSmallInt i -> toJSON i
PGValBigInt i -> toJSON i
PGValFloat f -> toJSON f
PGValDouble d -> toJSON d
PGValNumeric sc -> toJSON sc
PGValBoolean b -> toJSON b
PGValChar t -> toJSON t
PGValVarchar t -> toJSON t
PGValText t -> toJSON t
PGValCitext t -> toJSON t
PGValDate d -> toJSON d
PGValTimeStampTZ u ->
toJSON $ formatTime defaultTimeLocale "%FT%T%QZ" u
PGValTimeTZ (ZonedTimeOfDay tod tz) ->
toJSON (show tod ++ timeZoneOffsetString tz)
PGNull _ -> Null
PGValJSON (Q.JSON j) -> j
PGValJSONB (Q.JSONB j) -> j
PGValGeo o -> toJSON o
PGValRaster r -> toJSON r
PGValUUID u -> toJSON u
PGValUnknown t -> toJSON t
pgColValueToInt :: PGScalarValue -> Maybe Int
pgColValueToInt (PGValInteger i) = Just $ fromIntegral i
pgColValueToInt (PGValSmallInt i) = Just $ fromIntegral i
pgColValueToInt (PGValBigInt i) = Just $ fromIntegral i
pgColValueToInt _ = Nothing
withConstructorFn :: PGScalarType -> S.SQLExp -> S.SQLExp
withConstructorFn ty v
| isGeoType ty = S.SEFnApp "ST_GeomFromGeoJSON" [v] Nothing
| ty == PGRaster = S.SEFnApp "ST_RastFromHexWKB" [v] Nothing
| otherwise = v
parsePGValue :: PGScalarType -> Value -> AT.Parser PGScalarValue
parsePGValue ty val = case (ty, val) of
(_ , Null) -> pure $ PGNull ty
(PGUnknown _, String t) -> pure $ PGValUnknown t
(PGRaster , _) -> parseTyped -- strictly parse raster value
(_ , String t) -> parseTyped <|> pure (PGValUnknown t)
(_ , _) -> parseTyped
where
parseTyped = case ty of
PGSmallInt -> PGValSmallInt <$> parseJSON val
PGInteger -> PGValInteger <$> parseJSON val
PGBigInt -> PGValBigInt <$> parseJSON val
PGSerial -> PGValInteger <$> parseJSON val
PGBigSerial -> PGValBigInt <$> parseJSON val
PGFloat -> PGValFloat <$> parseJSON val
PGDouble -> PGValDouble <$> parseJSON val
PGNumeric -> PGValNumeric <$> parseJSON val
PGBoolean -> PGValBoolean <$> parseJSON val
PGChar -> PGValChar <$> parseJSON val
PGVarchar -> PGValVarchar <$> parseJSON val
PGText -> PGValText <$> parseJSON val
PGCitext -> PGValCitext <$> parseJSON val
PGDate -> PGValDate <$> parseJSON val
PGTimeStampTZ -> PGValTimeStampTZ <$> parseJSON val
PGTimeTZ -> PGValTimeTZ <$> parseJSON val
PGJSON -> PGValJSON . Q.JSON <$> parseJSON val
PGJSONB -> PGValJSONB . Q.JSONB <$> parseJSON val
PGGeometry -> PGValGeo <$> parseJSON val
PGGeography -> PGValGeo <$> parseJSON val
PGRaster -> PGValRaster <$> parseJSON val
PGUUID -> PGValUUID <$> parseJSON val
PGUnknown tyName ->
fail $ "A string is expected for type : " ++ T.unpack tyName
data TxtEncodedPGVal
= TENull
| TELit !Text
deriving (Show, Eq, Generic)
instance Hashable TxtEncodedPGVal
instance ToJSON TxtEncodedPGVal where
toJSON = \case
TENull -> Null
TELit t -> String t
instance FromJSON TxtEncodedPGVal where
parseJSON Null = pure TENull
parseJSON (String t) = pure $ TELit t
parseJSON v = AT.typeMismatch "String" v
txtEncodedPGVal :: PGScalarValue -> TxtEncodedPGVal
txtEncodedPGVal colVal = case colVal of
PGValInteger i -> TELit $ T.pack $ show i
PGValSmallInt i -> TELit $ T.pack $ show i
PGValBigInt i -> TELit $ T.pack $ show i
PGValFloat f -> TELit $ T.pack $ show f
PGValDouble d -> TELit $ T.pack $ show d
PGValNumeric sc -> TELit $ T.pack $ show sc
PGValBoolean b -> TELit $ bool "false" "true" b
PGValChar t -> TELit $ T.pack $ show t
PGValVarchar t -> TELit t
PGValText t -> TELit t
PGValCitext t -> TELit t
PGValDate d -> TELit $ T.pack $ showGregorian d
PGValTimeStampTZ u ->
TELit $ T.pack $ formatTime defaultTimeLocale "%FT%T%QZ" u
PGValTimeTZ (ZonedTimeOfDay tod tz) ->
TELit $ T.pack (show tod ++ timeZoneOffsetString tz)
PGNull _ ->
TENull
PGValJSON (Q.JSON j) -> TELit $ TL.toStrict $
AE.encodeToLazyText j
PGValJSONB (Q.JSONB j) -> TELit $ TL.toStrict $
AE.encodeToLazyText j
PGValGeo o -> TELit $ TL.toStrict $
AE.encodeToLazyText o
PGValRaster r -> TELit $ TC.toText $ getRasterWKB r
PGValUUID u -> TELit $ UUID.toText u
PGValUnknown t -> TELit t
binEncoder :: PGScalarValue -> Q.PrepArg
binEncoder colVal = case colVal of
PGValInteger i -> Q.toPrepVal i
PGValSmallInt i -> Q.toPrepVal i
PGValBigInt i -> Q.toPrepVal i
PGValFloat f -> Q.toPrepVal f
PGValDouble d -> Q.toPrepVal d
PGValNumeric sc -> Q.toPrepVal sc
PGValBoolean b -> Q.toPrepVal b
PGValChar t -> Q.toPrepVal t
PGValVarchar t -> Q.toPrepVal t
PGValText t -> Q.toPrepVal t
PGValCitext t -> Q.toPrepVal t
PGValDate d -> Q.toPrepVal d
PGValTimeStampTZ u -> Q.toPrepVal u
PGValTimeTZ (ZonedTimeOfDay t z) -> Q.toPrepValHelper PTI.timetz PE.timetz_int (t, z)
PGNull ty -> (pgTypeOid ty, Nothing)
PGValJSON u -> Q.toPrepVal u
PGValJSONB u -> Q.toPrepVal u
PGValGeo o -> Q.toPrepVal $ TL.toStrict $ AE.encodeToLazyText o
PGValRaster r -> Q.toPrepVal $ TC.toText $ getRasterWKB r
PGValUUID u -> Q.toPrepVal u
PGValUnknown t -> (PTI.auto, Just (TE.encodeUtf8 t, PQ.Text))
txtEncoder :: PGScalarValue -> S.SQLExp
txtEncoder colVal = case txtEncodedPGVal colVal of
TENull -> S.SENull
TELit t -> S.SELit t
{- Note [Type casting prepared params]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Prepared values are passed to Postgres via text encoding. Explicit type cast for prepared params
is needed to distinguish the column types. For example, the parameter for citext column type is
generated as ($i)::citext where 'i' is parameter position (integer).
Also see https://github.com/hasura/graphql-engine/issues/2818
-}
toPrepParam :: Int -> PGScalarType -> S.SQLExp
toPrepParam i ty =
-- See Note [Type casting prepared params] above
S.withTyAnn ty . withConstructorFn ty $ S.SEPrep i
toBinaryValue :: WithScalarType PGScalarValue -> Q.PrepArg
toBinaryValue = binEncoder . pstValue
toTxtValue :: WithScalarType PGScalarValue -> S.SQLExp
toTxtValue (WithScalarType ty val) =
S.withTyAnn ty . withConstructorFn ty $ txtEncoder val