2022-03-10 18:25:25 +03:00
|
|
|
-- | How to construct and execute a call to a source for a remote join.
|
|
|
|
--
|
|
|
|
-- There are three steps required to do this:
|
|
|
|
-- - construct the execution step for that source join
|
|
|
|
-- - execute that GraphQL query over the network
|
|
|
|
-- - build a join index of the variables out of the response
|
|
|
|
--
|
|
|
|
-- This can be done as one function, but we also export the individual steps for
|
|
|
|
-- debugging / test purposes. We congregate all intermediary state in the opaque
|
|
|
|
-- 'SourceJoinCall' type.
|
|
|
|
module Hasura.GraphQL.Execute.RemoteJoin.Source
|
|
|
|
( -- * Executing a remote join
|
|
|
|
makeSourceJoinCall,
|
|
|
|
|
|
|
|
-- * Individual steps
|
|
|
|
SourceJoinCall (..),
|
|
|
|
buildSourceJoinCall,
|
|
|
|
buildJoinIndex,
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import Data.Aeson qualified as J
|
2022-06-08 18:31:28 +03:00
|
|
|
import Data.Aeson.Key qualified as K
|
|
|
|
import Data.Aeson.KeyMap qualified as KM
|
2022-03-10 18:25:25 +03:00
|
|
|
import Data.Aeson.Ordered qualified as AO
|
|
|
|
import Data.Aeson.Ordered qualified as JO
|
2022-06-08 18:31:28 +03:00
|
|
|
import Data.Bifunctor (bimap)
|
2022-03-10 18:25:25 +03:00
|
|
|
import Data.ByteString.Lazy qualified as BL
|
2023-04-26 18:42:13 +03:00
|
|
|
import Data.HashMap.Strict.Extended qualified as HashMap
|
2022-03-10 18:25:25 +03:00
|
|
|
import Data.IntMap.Strict qualified as IntMap
|
|
|
|
import Data.List.NonEmpty qualified as NE
|
|
|
|
import Data.Scientific qualified as Scientific
|
|
|
|
import Data.Text qualified as T
|
2023-05-31 08:47:40 +03:00
|
|
|
import Data.Text.Extended ((<<>), (<>>))
|
2022-03-10 18:25:25 +03:00
|
|
|
import Data.Text.Read qualified as TR
|
|
|
|
import Hasura.Base.Error
|
|
|
|
import Hasura.GraphQL.Execute.Backend qualified as EB
|
|
|
|
import Hasura.GraphQL.Execute.Instances ()
|
|
|
|
import Hasura.GraphQL.Execute.RemoteJoin.Types
|
|
|
|
import Hasura.GraphQL.Namespace
|
|
|
|
import Hasura.GraphQL.Transport.Instances ()
|
|
|
|
import Hasura.Prelude
|
2023-03-31 00:18:11 +03:00
|
|
|
import Hasura.QueryTags
|
2022-04-27 16:57:28 +03:00
|
|
|
import Hasura.RQL.Types.Backend
|
|
|
|
import Hasura.RQL.Types.Common
|
2022-03-10 18:25:25 +03:00
|
|
|
import Hasura.SQL.AnyBackend qualified as AB
|
|
|
|
import Hasura.Session
|
2023-05-31 08:47:40 +03:00
|
|
|
import Hasura.Tracing (MonadTrace)
|
|
|
|
import Hasura.Tracing qualified as Tracing
|
2022-03-10 18:25:25 +03:00
|
|
|
import Language.GraphQL.Draft.Syntax qualified as G
|
2023-01-25 10:12:53 +03:00
|
|
|
import Network.HTTP.Types qualified as HTTP
|
2022-03-10 18:25:25 +03:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Executing a remote join
|
|
|
|
|
|
|
|
-- | Construct and execute a call to a source for a remote join.
|
|
|
|
makeSourceJoinCall ::
|
2023-05-31 08:47:40 +03:00
|
|
|
(MonadQueryTags m, MonadError QErr m, MonadTrace m, MonadIO m) =>
|
2022-03-10 18:25:25 +03:00
|
|
|
-- | Function to dispatch a request to a source.
|
|
|
|
(AB.AnyBackend SourceJoinCall -> m BL.ByteString) ->
|
|
|
|
-- | User information.
|
|
|
|
UserInfo ->
|
|
|
|
-- | Remote join information.
|
|
|
|
AB.AnyBackend RemoteSourceJoin ->
|
|
|
|
-- | Name of the field from the join arguments.
|
|
|
|
FieldName ->
|
|
|
|
-- | Mapping from 'JoinArgumentId' to its corresponding 'JoinArgument'.
|
|
|
|
IntMap.IntMap JoinArgument ->
|
2023-01-25 10:12:53 +03:00
|
|
|
[HTTP.Header] ->
|
|
|
|
Maybe G.Name ->
|
2022-03-10 18:25:25 +03:00
|
|
|
-- | The resulting join index (see 'buildJoinIndex') if any.
|
|
|
|
m (Maybe (IntMap.IntMap AO.Value))
|
2023-05-31 08:47:40 +03:00
|
|
|
makeSourceJoinCall networkFunction userInfo remoteSourceJoin jaFieldName joinArguments reqHeaders operationName =
|
|
|
|
Tracing.newSpan ("Remote join to data source " <> sourceName <<> " for field " <>> jaFieldName) do
|
|
|
|
-- step 1: create the SourceJoinCall
|
|
|
|
-- maybeSourceCall <-
|
|
|
|
-- AB.dispatchAnyBackend @EB.BackendExecute remoteSourceJoin \(sjc :: SourceJoinCall b) ->
|
|
|
|
-- buildSourceJoinCall @b userInfo jaFieldName joinArguments sjc
|
|
|
|
maybeSourceCall <-
|
|
|
|
AB.dispatchAnyBackend @EB.BackendExecute remoteSourceJoin
|
|
|
|
$ buildSourceJoinCall userInfo jaFieldName joinArguments reqHeaders operationName
|
|
|
|
-- if there actually is a remote call:
|
|
|
|
for maybeSourceCall \sourceCall -> do
|
|
|
|
-- step 2: send this call over the network
|
|
|
|
sourceResponse <- networkFunction sourceCall
|
|
|
|
-- step 3: build the join index
|
|
|
|
Tracing.newSpan "Build remote join index"
|
|
|
|
$ buildJoinIndex sourceResponse
|
|
|
|
where
|
|
|
|
sourceName :: SourceName
|
|
|
|
sourceName = AB.dispatchAnyBackend @Backend remoteSourceJoin _rsjSource
|
2022-03-10 18:25:25 +03:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Internal representation
|
|
|
|
|
|
|
|
-- | Intermediate type that contains all the necessary information to perform a
|
|
|
|
-- call to a database to perform a join.
|
|
|
|
data SourceJoinCall b = SourceJoinCall
|
|
|
|
{ _sjcRootFieldAlias :: RootFieldAlias,
|
|
|
|
_sjcSourceConfig :: SourceConfig b,
|
|
|
|
_sjcStepInfo :: EB.DBStepInfo b
|
|
|
|
}
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Step 1: building the source call
|
|
|
|
|
|
|
|
buildSourceJoinCall ::
|
2023-05-31 08:47:40 +03:00
|
|
|
forall b m.
|
|
|
|
(EB.BackendExecute b, MonadQueryTags m, MonadError QErr m, MonadTrace m, MonadIO m) =>
|
2022-03-10 18:25:25 +03:00
|
|
|
UserInfo ->
|
|
|
|
FieldName ->
|
|
|
|
IntMap.IntMap JoinArgument ->
|
2023-01-25 10:12:53 +03:00
|
|
|
[HTTP.Header] ->
|
|
|
|
Maybe G.Name ->
|
2022-03-10 18:25:25 +03:00
|
|
|
RemoteSourceJoin b ->
|
|
|
|
m (Maybe (AB.AnyBackend SourceJoinCall))
|
2023-04-13 04:29:15 +03:00
|
|
|
buildSourceJoinCall userInfo jaFieldName joinArguments reqHeaders operationName remoteSourceJoin = do
|
2023-05-31 08:47:40 +03:00
|
|
|
Tracing.newSpan "Resolve execution step for remote join field" do
|
|
|
|
let rows =
|
|
|
|
IntMap.toList joinArguments <&> \(argumentId, argument) ->
|
|
|
|
KM.insert "__argument_id__" (J.toJSON argumentId)
|
|
|
|
$ KM.fromList
|
|
|
|
$ map (bimap (K.fromText . getFieldNameTxt) JO.fromOrdered)
|
|
|
|
$ HashMap.toList
|
|
|
|
$ unJoinArgument argument
|
|
|
|
rowSchema = fmap snd (_rsjJoinColumns remoteSourceJoin)
|
|
|
|
for (NE.nonEmpty rows) $ \nonEmptyRows -> do
|
|
|
|
let sourceConfig = _rsjSourceConfig remoteSourceJoin
|
|
|
|
Tracing.attachSourceConfigAttributes @b sourceConfig
|
|
|
|
stepInfo <-
|
|
|
|
EB.mkDBRemoteRelationshipPlan
|
|
|
|
userInfo
|
|
|
|
(_rsjSource remoteSourceJoin)
|
|
|
|
sourceConfig
|
|
|
|
nonEmptyRows
|
|
|
|
rowSchema
|
|
|
|
(FieldName "__argument_id__")
|
|
|
|
(FieldName "f", _rsjRelationship remoteSourceJoin)
|
|
|
|
reqHeaders
|
|
|
|
operationName
|
|
|
|
(_rsjStringifyNum remoteSourceJoin)
|
|
|
|
-- This should never fail, as field names in remote relationships are
|
|
|
|
-- validated when building the schema cache.
|
|
|
|
fieldName <-
|
|
|
|
G.mkName (getFieldNameTxt jaFieldName)
|
|
|
|
`onNothing` throw500 ("'" <> getFieldNameTxt jaFieldName <> "' is not a valid GraphQL name")
|
|
|
|
-- NOTE: We're making an assumption that the 'FieldName' propagated upwards
|
|
|
|
-- from 'collectJoinArguments' is reasonable to use for logging.
|
|
|
|
let rootFieldAlias = mkUnNamespacedRootFieldAlias fieldName
|
|
|
|
pure
|
|
|
|
$ AB.mkAnyBackend
|
|
|
|
$ SourceJoinCall rootFieldAlias sourceConfig stepInfo
|
2022-03-10 18:25:25 +03:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Step 3: extracting the join index
|
|
|
|
|
|
|
|
-- | Construct a join index from the 'EncJSON' response from the source.
|
|
|
|
--
|
|
|
|
-- Unlike with remote schemas, we can make assumptions about the shape of the
|
|
|
|
-- result, instead of having to keep track of the path within the answer. This
|
|
|
|
-- function therefore enforces that the answer has the shape we expect, and
|
|
|
|
-- throws a 'QErr' if it doesn't.
|
|
|
|
buildJoinIndex :: (MonadError QErr m) => BL.ByteString -> m (IntMap.IntMap JO.Value)
|
|
|
|
buildJoinIndex response = do
|
|
|
|
json <-
|
2022-12-19 17:03:13 +03:00
|
|
|
JO.eitherDecode response `onLeft` \err ->
|
2022-03-10 18:25:25 +03:00
|
|
|
throwInvalidJsonErr $ T.pack err
|
|
|
|
case json of
|
|
|
|
JO.Array arr -> fmap IntMap.fromList $ for (toList arr) \case
|
|
|
|
JO.Object obj -> do
|
|
|
|
argumentResult <-
|
|
|
|
JO.lookup "f" obj
|
|
|
|
`onNothing` throwMissingRelationshipDataErr
|
|
|
|
argumentIdValue <-
|
|
|
|
JO.lookup "__argument_id__" obj
|
|
|
|
`onNothing` throwMissingArgumentIdErr
|
|
|
|
argumentId <-
|
|
|
|
case argumentIdValue of
|
|
|
|
JO.Number n ->
|
|
|
|
Scientific.toBoundedInteger n
|
|
|
|
`onNothing` throwInvalidArgumentIdValueErr
|
|
|
|
JO.String s ->
|
|
|
|
intFromText s
|
|
|
|
`onNothing` throwInvalidArgumentIdValueErr
|
|
|
|
_ -> throwInvalidArgumentIdValueErr
|
|
|
|
pure (argumentId, argumentResult)
|
|
|
|
_ -> throwNoNestedObjectErr
|
|
|
|
_ -> throwNoListOfObjectsErr
|
|
|
|
where
|
|
|
|
intFromText txt = case TR.decimal txt of
|
|
|
|
Right (i, "") -> pure i
|
|
|
|
_ -> Nothing
|
|
|
|
throwInvalidJsonErr errMsg =
|
2023-05-24 16:51:56 +03:00
|
|
|
throw500
|
|
|
|
$ "failed to decode JSON response from the source: "
|
|
|
|
<> errMsg
|
2022-03-10 18:25:25 +03:00
|
|
|
throwMissingRelationshipDataErr =
|
2023-05-24 16:51:56 +03:00
|
|
|
throw500
|
|
|
|
$ "cannot find relationship data (aliased as 'f') within the source \
|
|
|
|
\response"
|
2022-03-10 18:25:25 +03:00
|
|
|
throwMissingArgumentIdErr =
|
2023-05-24 16:51:56 +03:00
|
|
|
throw500
|
|
|
|
$ "cannot find '__argument_id__' within the source response"
|
2022-03-10 18:25:25 +03:00
|
|
|
throwInvalidArgumentIdValueErr =
|
|
|
|
throw500 $ "expected 'argument_id' to get parsed as backend integer type"
|
|
|
|
throwNoNestedObjectErr =
|
2023-05-24 16:51:56 +03:00
|
|
|
throw500
|
|
|
|
$ "expected an object one level deep in the remote schema's response, \
|
|
|
|
\but found an array/scalar value instead"
|
2022-03-10 18:25:25 +03:00
|
|
|
throwNoListOfObjectsErr =
|
2023-05-24 16:51:56 +03:00
|
|
|
throw500
|
|
|
|
$ "expected a list of objects in the remote schema's response, but found \
|
|
|
|
\an object/scalar value instead"
|