2021-08-05 00:23:33 +03:00
|
|
|
{-# LANGUAGE DeriveAnyClass #-}
|
|
|
|
|
2020-10-07 13:23:17 +03:00
|
|
|
module Hasura.GraphQL.Execute.Remote
|
|
|
|
( buildExecStepRemote
|
2021-01-19 23:56:53 +03:00
|
|
|
, collectVariablesFromSelectionSet
|
2020-12-21 12:11:37 +03:00
|
|
|
, collectVariables
|
|
|
|
, resolveRemoteVariable
|
|
|
|
, resolveRemoteField
|
2021-05-24 23:12:53 +03:00
|
|
|
, runVariableCache
|
2020-10-07 13:23:17 +03:00
|
|
|
) where
|
|
|
|
|
|
|
|
import Hasura.Prelude
|
|
|
|
|
2020-12-21 12:11:37 +03:00
|
|
|
import qualified Data.Aeson as J
|
2020-10-07 13:23:17 +03:00
|
|
|
import qualified Data.HashMap.Strict as Map
|
2020-12-28 15:56:00 +03:00
|
|
|
import qualified Data.HashSet as Set
|
2020-12-21 12:11:37 +03:00
|
|
|
import qualified Data.Text as T
|
2020-10-07 13:23:17 +03:00
|
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
2020-12-21 12:11:37 +03:00
|
|
|
|
|
|
|
import Data.Text.Extended
|
2020-10-07 13:23:17 +03:00
|
|
|
|
|
|
|
import qualified Hasura.GraphQL.Transport.HTTP.Protocol as GH
|
|
|
|
|
2021-05-11 18:18:31 +03:00
|
|
|
import Hasura.Base.Error
|
2021-06-11 06:26:50 +03:00
|
|
|
import Hasura.GraphQL.Execute.Backend
|
2020-12-21 12:11:37 +03:00
|
|
|
import Hasura.GraphQL.Parser
|
2021-02-20 16:45:49 +03:00
|
|
|
import Hasura.GraphQL.Transport.HTTP.Protocol
|
2020-10-07 13:23:17 +03:00
|
|
|
import Hasura.RQL.Types
|
2020-12-21 12:11:37 +03:00
|
|
|
import Hasura.Session
|
|
|
|
|
2021-02-12 06:04:09 +03:00
|
|
|
|
2020-12-21 12:11:37 +03:00
|
|
|
mkVariableDefinitionAndValue :: Variable -> (G.VariableDefinition, (G.Name, J.Value))
|
|
|
|
mkVariableDefinitionAndValue var@(Variable varInfo gType varValue) =
|
|
|
|
(varDefn, (varName, varJSONValue))
|
|
|
|
where
|
|
|
|
varName = getName var
|
|
|
|
|
|
|
|
varDefn = G.VariableDefinition varName gType defaultVal
|
|
|
|
|
|
|
|
defaultVal =
|
|
|
|
case varInfo of
|
2020-12-28 15:56:00 +03:00
|
|
|
VIRequired _ -> Nothing
|
2020-12-21 12:11:37 +03:00
|
|
|
VIOptional _ val -> Just val
|
|
|
|
|
|
|
|
varJSONValue =
|
|
|
|
case varValue of
|
2020-12-28 15:56:00 +03:00
|
|
|
JSONValue v -> v
|
2020-12-21 12:11:37 +03:00
|
|
|
GraphQLValue val -> graphQLValueToJSON val
|
|
|
|
|
|
|
|
unresolveVariables
|
|
|
|
:: forall fragments
|
|
|
|
. Functor fragments
|
|
|
|
=> G.SelectionSet fragments Variable
|
|
|
|
-> G.SelectionSet fragments G.Name
|
|
|
|
unresolveVariables =
|
|
|
|
fmap (fmap (getName . vInfo))
|
|
|
|
|
2020-10-07 13:23:17 +03:00
|
|
|
collectVariables
|
|
|
|
:: forall fragments var
|
|
|
|
. (Foldable fragments, Hashable var, Eq var)
|
|
|
|
=> G.SelectionSet fragments var
|
|
|
|
-> Set.HashSet var
|
|
|
|
collectVariables =
|
|
|
|
Set.unions . fmap (foldMap Set.singleton)
|
|
|
|
|
2021-01-19 23:56:53 +03:00
|
|
|
collectVariablesFromSelectionSet
|
|
|
|
:: G.SelectionSet G.NoFragments Variable
|
|
|
|
-> [(G.VariableDefinition, (G.Name, J.Value))]
|
|
|
|
collectVariablesFromSelectionSet =
|
|
|
|
map mkVariableDefinitionAndValue . Set.toList . collectVariables
|
|
|
|
|
2021-02-20 16:45:49 +03:00
|
|
|
|
2020-10-07 13:23:17 +03:00
|
|
|
buildExecStepRemote
|
2021-02-20 16:45:49 +03:00
|
|
|
:: RemoteSchemaInfo
|
2021-07-30 14:33:06 +03:00
|
|
|
-> RemoteResultCustomizer
|
2020-10-07 13:23:17 +03:00
|
|
|
-> G.OperationType
|
2020-12-21 12:11:37 +03:00
|
|
|
-> G.SelectionSet G.NoFragments Variable
|
2021-02-20 16:45:49 +03:00
|
|
|
-> ExecutionStep
|
2021-07-30 14:33:06 +03:00
|
|
|
buildExecStepRemote remoteSchemaInfo resultCustomizer tp selSet =
|
2020-12-21 12:11:37 +03:00
|
|
|
let unresolvedSelSet = unresolveVariables selSet
|
|
|
|
allVars = map mkVariableDefinitionAndValue $ Set.toList $ collectVariables selSet
|
|
|
|
varValues = Map.fromList $ map snd allVars
|
|
|
|
varValsM = bool (Just varValues) Nothing $ Map.null varValues
|
|
|
|
varDefs = map fst allVars
|
|
|
|
_grQuery = G.TypedOperationDefinition tp Nothing varDefs [] unresolvedSelSet
|
|
|
|
_grVariables = varValsM
|
2021-07-30 14:33:06 +03:00
|
|
|
in ExecStepRemote remoteSchemaInfo resultCustomizer GH.GQLReq{_grOperationName = Nothing, ..}
|
2020-12-21 12:11:37 +03:00
|
|
|
|
2021-08-05 00:23:33 +03:00
|
|
|
-- | Association between keys uniquely identifying some remote JSON variable and
|
|
|
|
-- an 'Int' identifier that will be used to construct a valid variable name to
|
|
|
|
-- be used in a GraphQL query.
|
|
|
|
newtype RemoteJSONVariableMap =
|
|
|
|
RemoteJSONVariableMap (HashMap RemoteJSONVariableKey Int)
|
|
|
|
deriving newtype (Eq, Monoid, Semigroup)
|
|
|
|
|
|
|
|
-- | A unique identifier for some remote JSON variable whose name will need to
|
|
|
|
-- be substituted when constructing a GraphQL query.
|
|
|
|
--
|
|
|
|
-- For a detailed explanation of this behavior, see the following comment:
|
|
|
|
-- https://github.com/hasura/graphql-engine/issues/7170#issuecomment-880838970
|
|
|
|
data RemoteJSONVariableKey = RemoteJSONVariableKey !G.GType !J.Value
|
|
|
|
deriving stock (Eq, Generic)
|
|
|
|
deriving anyclass (Hashable)
|
2020-12-21 12:11:37 +03:00
|
|
|
|
2021-08-05 00:23:33 +03:00
|
|
|
-- | Resolves a `RemoteSchemaVariable` into a GraphQL `Variable`.
|
|
|
|
--
|
|
|
|
-- A `RemoteSchemaVariable` can either be a query variable (i.e. a variable
|
|
|
|
-- provided in the query) or it can be a `SessionPresetVariable` (in which case
|
|
|
|
-- we look up the value of the session variable and coerce it into the
|
|
|
|
-- appropriate type and then construct the GraphQL 'Variable').
|
2020-12-21 12:11:37 +03:00
|
|
|
--
|
2021-08-05 00:23:33 +03:00
|
|
|
-- NOTE: The session variable preset is a hard preset (i.e. if the session
|
|
|
|
-- variable doesn't exist, an error will be thrown).
|
2020-12-21 12:11:37 +03:00
|
|
|
--
|
2021-08-05 00:23:33 +03:00
|
|
|
-- The name of the GraphQL variable generated will be a GraphQL-ized version of
|
|
|
|
-- the session variable (i.e. '-' will be replaced with '_'), since session
|
|
|
|
-- variables are not valid GraphQL names.
|
2020-12-21 12:11:37 +03:00
|
|
|
--
|
2021-08-05 00:23:33 +03:00
|
|
|
-- Additionally, we need to handle partially traversed JSON values; likewise, we
|
|
|
|
-- create a new variable out of thin air.
|
2020-12-21 12:11:37 +03:00
|
|
|
--
|
2021-05-24 23:12:53 +03:00
|
|
|
-- For example, considering the following schema for a role:
|
|
|
|
--
|
|
|
|
-- input UserName {
|
|
|
|
-- firstName : String! @preset(value:"Foo")
|
|
|
|
-- lastName : String!
|
|
|
|
-- }
|
|
|
|
--
|
2020-12-21 12:11:37 +03:00
|
|
|
-- type Query {
|
2021-05-24 23:12:53 +03:00
|
|
|
-- user(
|
|
|
|
-- user_id: Int! @preset(value:"x-hasura-user-id")
|
|
|
|
-- user_name: UserName!
|
|
|
|
-- ): User
|
2020-12-21 12:11:37 +03:00
|
|
|
-- }
|
|
|
|
--
|
2021-05-24 23:12:53 +03:00
|
|
|
-- and the incoming query to the graphql-engine is:
|
2020-12-21 12:11:37 +03:00
|
|
|
--
|
2021-05-24 23:12:53 +03:00
|
|
|
-- query($foo: UserName!) {
|
|
|
|
-- user(user_name: $foo) { id name }
|
2020-12-21 12:11:37 +03:00
|
|
|
-- }
|
|
|
|
--
|
2021-05-24 23:12:53 +03:00
|
|
|
-- with variables:
|
|
|
|
--
|
|
|
|
-- { "foo": {"lastName": "Bar"} }
|
2020-12-21 12:11:37 +03:00
|
|
|
--
|
2021-05-24 23:12:53 +03:00
|
|
|
--
|
2021-08-05 00:23:33 +03:00
|
|
|
-- After resolving the session argument presets, the query that will be sent to
|
|
|
|
-- the remote server will be:
|
2021-05-24 23:12:53 +03:00
|
|
|
--
|
|
|
|
-- query ($x_hasura_user_id: Int!, $hasura_json_var_1: String!) {
|
|
|
|
-- user (user_id: $x_hasura_user_id, user_name: {firstName: "Foo", lastName: $hasura_json_var_1}) {
|
|
|
|
-- id
|
|
|
|
-- name
|
2020-12-21 12:11:37 +03:00
|
|
|
-- }
|
2021-05-24 23:12:53 +03:00
|
|
|
-- }
|
2020-12-21 12:11:37 +03:00
|
|
|
--
|
|
|
|
resolveRemoteVariable
|
|
|
|
:: (MonadError QErr m)
|
|
|
|
=> UserInfo
|
|
|
|
-> RemoteSchemaVariable
|
2021-08-05 00:23:33 +03:00
|
|
|
-> StateT RemoteJSONVariableMap m Variable
|
2020-12-21 12:11:37 +03:00
|
|
|
resolveRemoteVariable userInfo = \case
|
|
|
|
SessionPresetVariable sessionVar typeName presetInfo -> do
|
|
|
|
sessionVarVal <- onNothing (getSessionVariableValue sessionVar $ _uiSession userInfo)
|
|
|
|
$ throw400 NotFound $ sessionVar <<> " session variable expected, but not found"
|
|
|
|
let varName = sessionVariableToGraphQLName sessionVar
|
|
|
|
coercedValue <-
|
|
|
|
case presetInfo of
|
|
|
|
SessionArgumentPresetScalar ->
|
|
|
|
case G.unName typeName of
|
|
|
|
"Int" ->
|
|
|
|
case readMaybe $ T.unpack sessionVarVal of
|
|
|
|
Nothing -> throw400 CoercionError $ sessionVarVal <<> " cannot be coerced into an Int value"
|
|
|
|
Just i -> pure $ G.VInt i
|
|
|
|
"Boolean" ->
|
|
|
|
if | sessionVarVal `elem` ["true", "false"] ->
|
|
|
|
pure $ G.VBoolean $ "true" == sessionVarVal
|
|
|
|
| otherwise ->
|
|
|
|
throw400 CoercionError $ sessionVarVal <<> " cannot be coerced into a Boolean value"
|
|
|
|
"Float" ->
|
|
|
|
case readMaybe $ T.unpack sessionVarVal of
|
|
|
|
Nothing ->
|
|
|
|
throw400 CoercionError $ sessionVarVal <<> " cannot be coerced into a Float value"
|
|
|
|
Just i -> pure $ G.VFloat i
|
|
|
|
-- The `String`,`ID` and the default case all use the same code. But,
|
|
|
|
-- it will be better to not merge all of them into the default case
|
|
|
|
-- because it will be helpful to know how all the built-in scalars
|
|
|
|
-- are handled
|
|
|
|
"String" -> pure $ G.VString sessionVarVal
|
|
|
|
"ID" -> pure $ G.VString sessionVarVal
|
|
|
|
-- When we encounter a custom scalar, we just pass it as a string
|
|
|
|
_ -> pure $ G.VString sessionVarVal
|
|
|
|
SessionArgumentPresetEnum enumVals -> do
|
|
|
|
sessionVarEnumVal <-
|
|
|
|
G.EnumValue <$>
|
|
|
|
onNothing
|
|
|
|
(G.mkName sessionVarVal)
|
|
|
|
(throw400 CoercionError $ sessionVarVal <<> " is not a valid GraphQL name")
|
|
|
|
case sessionVarEnumVal `Set.member` enumVals of
|
|
|
|
True -> pure $ G.VEnum sessionVarEnumVal
|
|
|
|
False -> throw400 CoercionError $ sessionVarEnumVal <<> " is not one of the valid enum values"
|
|
|
|
-- nullability is false, because we treat presets as hard presets
|
|
|
|
let variableGType = G.TypeNamed (G.Nullability False) typeName
|
|
|
|
pure $ Variable (VIRequired varName) variableGType (GraphQLValue coercedValue)
|
2021-05-24 23:12:53 +03:00
|
|
|
RemoteJSONValue gtype jsonValue -> do
|
2021-08-05 00:23:33 +03:00
|
|
|
let key = RemoteJSONVariableKey gtype jsonValue
|
|
|
|
varMap <- gets coerce
|
|
|
|
index <- Map.lookup key varMap `onNothing` do
|
|
|
|
let i = Map.size varMap + 1
|
|
|
|
put . coerce $ Map.insert key i varMap
|
2021-05-24 23:12:53 +03:00
|
|
|
pure i
|
|
|
|
let varName = G.unsafeMkName $ "hasura_json_var_" <> tshow index
|
|
|
|
pure $ Variable (VIRequired varName) gtype $ JSONValue jsonValue
|
2020-12-21 12:11:37 +03:00
|
|
|
QueryVariable variable -> pure variable
|
|
|
|
|
2021-08-05 00:23:33 +03:00
|
|
|
-- | TODO: Documentation.
|
2020-12-21 12:11:37 +03:00
|
|
|
resolveRemoteField
|
2021-07-30 14:33:06 +03:00
|
|
|
:: (MonadError QErr m, Traversable f)
|
2020-12-21 12:11:37 +03:00
|
|
|
=> UserInfo
|
2021-07-30 14:33:06 +03:00
|
|
|
-> RemoteFieldG f RemoteSchemaVariable
|
2021-08-05 00:23:33 +03:00
|
|
|
-> StateT RemoteJSONVariableMap m (RemoteFieldG f Variable)
|
2020-12-21 12:11:37 +03:00
|
|
|
resolveRemoteField userInfo = traverse (resolveRemoteVariable userInfo)
|
2021-05-24 23:12:53 +03:00
|
|
|
|
2021-08-05 00:23:33 +03:00
|
|
|
-- | TODO: Documentation.
|
2021-05-24 23:12:53 +03:00
|
|
|
runVariableCache
|
|
|
|
:: Monad m
|
2021-08-05 00:23:33 +03:00
|
|
|
=> StateT RemoteJSONVariableMap m a
|
2021-05-24 23:12:53 +03:00
|
|
|
-> m a
|
|
|
|
runVariableCache = flip evalStateT mempty
|