mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
Enable remote joins from remote schemas in the execution engine.
### Description This PR adds the ability to perform remote joins from remote schemas in the engine. To do so, we alter the definition of an `ExecutionStep` targeting a remote schema: the `ExecStepRemote` constructor now expects a `Maybe RemoteJoins`. This new argument is used when processing the execution step, in the transport layer (either `Transport.HTTP` or `Transport.WebSocket`). For this `Maybe RemoteJoins` to be extracted from a parsed query, this PR also extends the `Execute.RemoteJoin.Collect` module, to implement "collection" from a selection set. Not only do those new functions extract the remote joins, but they also apply all necessary transformations to the selection sets (such as inserting the necessary "phantom" fields used as join keys). Finally in `Execute.RemoteJoin.Join`, we make two changes. First, we now always look for nested remote joins, regardless of whether the join we just performed went to a source or a remote schema; and second we adapt our join tree logic according to the special cases that were added to deal with remote server edge cases. Additionally, this PR refactors / cleans / documents `Execute.RemoteJoin.RemoteServer`. This is not required as part of this change and could be moved to a separate PR if needed (a similar cleanup of `Join` is done independently in #3894). It also introduces a draft of a new documentation page for this project, that will be refined in the release PR that ships the feature (either #3069 or a copy of it). While this PR extends the engine, it doesn't plug such relationships in the schema, meaning that, as of this PR, the new code paths in `Join` are technically unreachable. Adding the corresponding schema code and, ultimately, enabling the metadata API will be done in subsequent PRs. ### Keeping track of concrete type names The main change this PR makes to the existing `Join` code is to handle a new reserved field we sometimes use when targeting remote servers: the `__hasura_internal_typename` field. In short, a GraphQL selection set can sometimes "branch" based on the concrete "runtime type" of the object on which the selection happens: ```graphql query { author(id: 53478) { ... on Writer { name articles { title } } ... on Artist { name articles { title } } } } ``` If both of those `articles` are remote joins, we need to be able, when we get the answer, to differentiate between the two different cases. We do this by asking for `__typename`, to be able to decide if we're in the `Writer` or the `Artist` branch of the query. To avoid further processing / customization of results, we only insert this `__hasura_internal_typename: __typename` field in the query in the case of unions of interfaces AND if we have the guarantee that we will processing the request as part of the remote joins "folding": that is, if there's any remote join in this branch in the tree. Otherwise, we don't insert the field, and we leave that part of the response untouched. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3810 GitOrigin-RevId: 89aaf16274d68e26ad3730b80c2d2fdc2896b96c
This commit is contained in:
parent
9a96e7d165
commit
6e1761f8f9
@ -23,6 +23,7 @@ code; we automatically update it on every push to a branch of interest:
|
|||||||
|
|
||||||
* how to understand our [GraphQL schema](schema.md)
|
* how to understand our [GraphQL schema](schema.md)
|
||||||
* how to perform [database migrations](migration-guidelines.md)
|
* how to perform [database migrations](migration-guidelines.md)
|
||||||
|
* how the [execution of remote joins](remote_joins_execution.md) works
|
||||||
|
|
||||||
TODO: we can either list individual topics here, or point to architecture/ and have a README file there.
|
TODO: we can either list individual topics here, or point to architecture/ and have a README file there.
|
||||||
|
|
||||||
|
162
server/documentation/remote_joins_execution.md
Normal file
162
server/documentation/remote_joins_execution.md
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
## Table of contents
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please make sure you update the table of contents when modifying this file. If
|
||||||
|
you're using emacs, you can automatically do so using the command mentioned in
|
||||||
|
the generated comment below (provided by the package markdown-toc), but it will
|
||||||
|
use a slightly different format and you will have to fix the differences
|
||||||
|
manually.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
|
||||||
|
|
||||||
|
- [The join tree](#the-join-tree)
|
||||||
|
- [Collect](#collect)
|
||||||
|
- [Join](#join)
|
||||||
|
- [Ambiguous schemas](#ambiguous-schemas)
|
||||||
|
|
||||||
|
<!-- markdown-toc end -->
|
||||||
|
|
||||||
|
## Executing remote joins
|
||||||
|
|
||||||
|
When a request has been parsed, and is ready to be executed, we start by
|
||||||
|
building a `JoinTree`: a structure close to a [prefix
|
||||||
|
tree](https://en.wikipedia.org/wiki/Trie), containing all the paths in the
|
||||||
|
response that will require remote joins. We call this phase the
|
||||||
|
[collection](#collection) phase: it constructs the build tree, and transforms
|
||||||
|
the request as needed.
|
||||||
|
|
||||||
|
After executing the core step of the request, if there is indeed a join tree,
|
||||||
|
then we start the [join](#join) phase: we fold that tree, expending the response
|
||||||
|
with the result of each subsequent request.
|
||||||
|
|
||||||
|
### The join tree
|
||||||
|
|
||||||
|
As mentioned, the join tree is almost like a prefix tree; the key difference is
|
||||||
|
that we don't store values at arbitrary points of the tree, only at the
|
||||||
|
leaves. Furthermore, while most prefix trees are indexed by character, in our
|
||||||
|
case we index joins by the *path through the response*.
|
||||||
|
|
||||||
|
For instance, imagine that we send the following request:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
authors {
|
||||||
|
name
|
||||||
|
articles { # remote join
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
the join tree we would emit would have the following shape:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
(Nothing, authors):
|
||||||
|
(Nothing, articles): <join information>
|
||||||
|
```
|
||||||
|
|
||||||
|
Recursively, all the way down, each join information might contain its own join
|
||||||
|
tree if there are any nested remote relationship.
|
||||||
|
|
||||||
|
Each key in this join tree is a pair: it contains the name of the field, but
|
||||||
|
also contains an optional type information: this is used to deal with [ambiguous
|
||||||
|
schemas](#ambiguous-schemas).
|
||||||
|
|
||||||
|
### Collect
|
||||||
|
|
||||||
|
Implemented in
|
||||||
|
[Hasura.GraphQL.Execute.RemoteJoin.Collect](https://github.com/hasura/graphql-engine/blob/master/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Collect.hs),
|
||||||
|
this phase identifies the remote joins in a request, and transforms the request
|
||||||
|
accordingly. If a selection set contains a field that is a remote join, we alter
|
||||||
|
the selection set:
|
||||||
|
- the field that maps to a remote join is replaced by a placeholder value, so
|
||||||
|
that we can keep track of the order in the selection set (since that order
|
||||||
|
must not be altered)
|
||||||
|
- we add "phantom fields": fields that were not requested by the user, but
|
||||||
|
that we need to include, as they are the keys on which the join is performed
|
||||||
|
|
||||||
|
In the case where the request goes to a remote schema, we might need additional
|
||||||
|
transformations (see the section on [ambiguous schemas](#ambiguous-schemas)).
|
||||||
|
|
||||||
|
From a practical perspective, the collection is a glorified `traverse`,
|
||||||
|
operating in the `Collector` monad, which itself is a `Writer` monad: whenever
|
||||||
|
we encounter a remote join, we `tell` it to the collector, and continue our
|
||||||
|
traversal. Every time we traverse a field, we use `censor` to wrap the resulting
|
||||||
|
joins in a sub-tree. Remote joins are aggregated using the `Semigroup` instance
|
||||||
|
of `JoinTree`.
|
||||||
|
|
||||||
|
### Join
|
||||||
|
|
||||||
|
Implemented in
|
||||||
|
[Hasura.GraphQL.Execute.RemoteJoin.Join](https://github.com/hasura/graphql-engine/blob/master/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs),
|
||||||
|
we post-process the root request by "folding" the tree of joins: we traverse the
|
||||||
|
join tree alongside the response: for each field in the response that maps to a
|
||||||
|
leaf of the join tree, we recursively do the same thing: issue a query, traverse
|
||||||
|
its own join tree... and on the way back, we replace the value of field by the
|
||||||
|
result of the join.
|
||||||
|
|
||||||
|
Depending on whether the target is a remote schema or a local source, we call
|
||||||
|
either `makeRemoteSchemaJoinCall` or `makeSourceJoinCall`, defined in
|
||||||
|
[Hasura.GraphQL.Execute.RemoteJoin.RemoteServer](https://github.com/hasura/graphql-engine/blob/master/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/RemoteServer.hs)
|
||||||
|
and
|
||||||
|
[Hasura.GraphQL.Execute.RemoteJoin.Source](https://github.com/hasura/graphql-engine/blob/master/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Source.hs)
|
||||||
|
respectively.
|
||||||
|
|
||||||
|
### Ambiguous schemas
|
||||||
|
|
||||||
|
This process is made more complicated by the fact that remote schemas, via
|
||||||
|
unions and interfaces, can be ambiguous. Consider the following request:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
node(id: $some_id) {
|
||||||
|
... on Article {
|
||||||
|
# foo is a field, returns data of type `t`
|
||||||
|
foo {
|
||||||
|
# r1 is a REMOTE relationship, returns data of type `u`
|
||||||
|
bar: r1 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Author {
|
||||||
|
id
|
||||||
|
# foo is a field, returns data of type `t`
|
||||||
|
foo {
|
||||||
|
# r2 is a REMOTE relationship, returns data of type `u`
|
||||||
|
bar: r2 {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are several complications with this request:
|
||||||
|
- there are two remote joins that would need to be at the same point in the
|
||||||
|
join tree, `node.foo.bar`;
|
||||||
|
- we need to identify which of the two relationships it is when processing the
|
||||||
|
joins; but we can't do so using information about `foo`, since its
|
||||||
|
`__typename` will be `t` in both cases.
|
||||||
|
|
||||||
|
To fix this, we have altered the join tree: instead of using the field name as
|
||||||
|
key at each level, we use instead a combination of optional type name and field
|
||||||
|
name. We identify as "ambiguous" all selection sets of a union or an interface
|
||||||
|
that either directly contain remote joins, or whose subselections contain remote
|
||||||
|
joins. Whenever we encounter such a selection set, we use its type name in the
|
||||||
|
corresponding keys in the join tree, and we add one more phantom field to the
|
||||||
|
selection set: `__hasura_internal_typename`, which extracts the `__typename`.
|
||||||
|
|
||||||
|
When processing the joins, we look for the presence of this field: if it is
|
||||||
|
there, we remove it from the response, and we do the join tree lookup using its
|
||||||
|
value, instead of using `Nothing`.
|
||||||
|
|
||||||
|
In practice, the join tree for the aforementioned query would therefore be:
|
||||||
|
```yaml
|
||||||
|
(Nothing, node):
|
||||||
|
(Article, foo):
|
||||||
|
(Nothing, bar): <join info>
|
||||||
|
(Author, foo):
|
||||||
|
(Nothing, bar): <join info>
|
||||||
|
```
|
@ -12,6 +12,10 @@ module Data.HashMap.Strict.NonEmpty
|
|||||||
-- * Basic interface
|
-- * Basic interface
|
||||||
lookup,
|
lookup,
|
||||||
(!?),
|
(!?),
|
||||||
|
keys,
|
||||||
|
|
||||||
|
-- * Compose
|
||||||
|
unionWith,
|
||||||
|
|
||||||
-- * Transformations
|
-- * Transformations
|
||||||
mapKeys,
|
mapKeys,
|
||||||
@ -72,6 +76,19 @@ lookup k (NEHashMap m) = M.lookup k m
|
|||||||
(!?) :: (Eq k, Hashable k) => NEHashMap k v -> k -> Maybe v
|
(!?) :: (Eq k, Hashable k) => NEHashMap k v -> k -> Maybe v
|
||||||
(!?) = flip lookup
|
(!?) = flip lookup
|
||||||
|
|
||||||
|
-- | Return a list of this map's keys.
|
||||||
|
keys :: NEHashMap k v -> [k]
|
||||||
|
keys = M.keys . unNEHashMap
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | The union of two maps.
|
||||||
|
--
|
||||||
|
-- If a key occurs in both maps, the provided function (first argument) will be
|
||||||
|
-- used to compute the result.
|
||||||
|
unionWith :: (Eq k, Hashable k) => (v -> v -> v) -> NEHashMap k v -> NEHashMap k v -> NEHashMap k v
|
||||||
|
unionWith fun (NEHashMap m1) (NEHashMap m2) = NEHashMap $ M.unionWith fun m1 m2
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
-- | @'mapKeys' f s@ is the map obtained by applying @f@ to each key of @s@.
|
-- | @'mapKeys' f s@ is the map obtained by applying @f@ to each key of @s@.
|
||||||
|
@ -240,6 +240,7 @@ data ExecutionStep where
|
|||||||
!RemoteSchemaInfo ->
|
!RemoteSchemaInfo ->
|
||||||
!ResultCustomizer ->
|
!ResultCustomizer ->
|
||||||
!GH.GQLReqOutgoing ->
|
!GH.GQLReqOutgoing ->
|
||||||
|
Maybe RemoteJoins ->
|
||||||
ExecutionStep
|
ExecutionStep
|
||||||
-- | Output a plain JSON object
|
-- | Output a plain JSON object
|
||||||
ExecStepRaw ::
|
ExecStepRaw ::
|
||||||
|
@ -128,7 +128,9 @@ convertMutationSelectionSet
|
|||||||
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
||||||
RFRemote remoteField -> do
|
RFRemote remoteField -> do
|
||||||
RemoteSchemaRootField remoteSchemaInfo resultCustomizer resolvedRemoteField <- runVariableCache $ resolveRemoteField userInfo remoteField
|
RemoteSchemaRootField remoteSchemaInfo resultCustomizer resolvedRemoteField <- runVariableCache $ resolveRemoteField userInfo remoteField
|
||||||
pure $ buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeMutation [G.SelectionField $ convertGraphQLField resolvedRemoteField] (GH._grOperationName gqlUnparsed)
|
let (noRelsRemoteField, remoteJoins) = RJ.getRemoteJoinsGraphQLField resolvedRemoteField
|
||||||
|
pure $
|
||||||
|
buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeMutation noRelsRemoteField remoteJoins (GH._grOperationName gqlUnparsed)
|
||||||
RFAction action -> do
|
RFAction action -> do
|
||||||
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionMutation action
|
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionMutation action
|
||||||
(actionName, _fch) <- pure $ case noRelsDBAST of
|
(actionName, _fch) <- pure $ case noRelsDBAST of
|
||||||
|
@ -114,7 +114,8 @@ convertQuerySelSet
|
|||||||
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
||||||
RFRemote rf -> do
|
RFRemote rf -> do
|
||||||
RemoteSchemaRootField remoteSchemaInfo resultCustomizer remoteField <- runVariableCache $ for rf $ resolveRemoteVariable userInfo
|
RemoteSchemaRootField remoteSchemaInfo resultCustomizer remoteField <- runVariableCache $ for rf $ resolveRemoteVariable userInfo
|
||||||
pure $ buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeQuery [G.SelectionField $ convertGraphQLField remoteField] (GH._grOperationName gqlUnparsed)
|
let (noRelsRemoteField, remoteJoins) = RJ.getRemoteJoinsGraphQLField remoteField
|
||||||
|
pure $ buildExecStepRemote remoteSchemaInfo resultCustomizer G.OperationTypeQuery noRelsRemoteField remoteJoins (GH._grOperationName gqlUnparsed)
|
||||||
RFAction action -> do
|
RFAction action -> do
|
||||||
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionQuery action
|
let (noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsActionQuery action
|
||||||
(actionExecution, actionName, fch) <- pure $ case noRelsDBAST of
|
(actionExecution, actionName, fch) <- pure $ case noRelsDBAST of
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
module Hasura.GraphQL.Execute.Remote
|
module Hasura.GraphQL.Execute.Remote
|
||||||
( buildExecStepRemote,
|
( buildExecStepRemote,
|
||||||
collectVariablesFromSelectionSet,
|
getVariableDefinitionAndValue,
|
||||||
collectVariables,
|
|
||||||
resolveRemoteVariable,
|
resolveRemoteVariable,
|
||||||
resolveRemoteField,
|
resolveRemoteField,
|
||||||
runVariableCache,
|
runVariableCache,
|
||||||
@ -17,17 +16,18 @@ import Data.Text qualified as T
|
|||||||
import Data.Text.Extended
|
import Data.Text.Extended
|
||||||
import Hasura.Base.Error
|
import Hasura.Base.Error
|
||||||
import Hasura.GraphQL.Execute.Backend
|
import Hasura.GraphQL.Execute.Backend
|
||||||
|
import Hasura.GraphQL.Execute.RemoteJoin.Types (RemoteJoins)
|
||||||
import Hasura.GraphQL.Parser
|
import Hasura.GraphQL.Parser
|
||||||
import Hasura.GraphQL.Transport.HTTP.Protocol
|
import Hasura.GraphQL.Transport.HTTP.Protocol
|
||||||
import Hasura.GraphQL.Transport.HTTP.Protocol qualified as GH
|
import Hasura.GraphQL.Transport.HTTP.Protocol qualified as GH
|
||||||
import Hasura.Prelude
|
import Hasura.Prelude
|
||||||
import Hasura.RQL.IR.RemoteSchema
|
import Hasura.RQL.IR.RemoteSchema qualified as IR
|
||||||
import Hasura.RQL.Types
|
import Hasura.RQL.Types
|
||||||
import Hasura.Session
|
import Hasura.Session
|
||||||
import Language.GraphQL.Draft.Syntax qualified as G
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
|
|
||||||
mkVariableDefinitionAndValue :: Variable -> (G.VariableDefinition, (G.Name, J.Value))
|
getVariableDefinitionAndValue :: Variable -> (G.VariableDefinition, (G.Name, J.Value))
|
||||||
mkVariableDefinitionAndValue var@(Variable varInfo gType varValue) =
|
getVariableDefinitionAndValue var@(Variable varInfo gType varValue) =
|
||||||
(varDefn, (varName, varJSONValue))
|
(varDefn, (varName, varJSONValue))
|
||||||
where
|
where
|
||||||
varName = getName var
|
varName = getName var
|
||||||
@ -60,29 +60,25 @@ collectVariables ::
|
|||||||
collectVariables =
|
collectVariables =
|
||||||
Set.unions . fmap (foldMap Set.singleton)
|
Set.unions . fmap (foldMap Set.singleton)
|
||||||
|
|
||||||
collectVariablesFromSelectionSet ::
|
|
||||||
G.SelectionSet G.NoFragments Variable ->
|
|
||||||
[(G.VariableDefinition, (G.Name, J.Value))]
|
|
||||||
collectVariablesFromSelectionSet =
|
|
||||||
map mkVariableDefinitionAndValue . Set.toList . collectVariables
|
|
||||||
|
|
||||||
buildExecStepRemote ::
|
buildExecStepRemote ::
|
||||||
RemoteSchemaInfo ->
|
RemoteSchemaInfo ->
|
||||||
ResultCustomizer ->
|
ResultCustomizer ->
|
||||||
G.OperationType ->
|
G.OperationType ->
|
||||||
G.SelectionSet G.NoFragments Variable ->
|
IR.GraphQLField Void Variable ->
|
||||||
|
Maybe RemoteJoins ->
|
||||||
Maybe OperationName ->
|
Maybe OperationName ->
|
||||||
ExecutionStep
|
ExecutionStep
|
||||||
buildExecStepRemote remoteSchemaInfo resultCustomizer tp selSet operationName =
|
buildExecStepRemote remoteSchemaInfo resultCustomizer tp rootField remoteJoins operationName =
|
||||||
let unresolvedSelSet = unresolveVariables selSet
|
let selSet = [G.SelectionField $ IR.convertGraphQLField rootField]
|
||||||
allVars = map mkVariableDefinitionAndValue $ Set.toList $ collectVariables selSet
|
unresolvedSelSet = unresolveVariables selSet
|
||||||
|
allVars = map getVariableDefinitionAndValue $ Set.toList $ collectVariables selSet
|
||||||
varValues = Map.fromList $ map snd allVars
|
varValues = Map.fromList $ map snd allVars
|
||||||
varValsM = bool (Just varValues) Nothing $ Map.null varValues
|
varValsM = bool (Just varValues) Nothing $ Map.null varValues
|
||||||
varDefs = map fst allVars
|
varDefs = map fst allVars
|
||||||
_grQuery = G.TypedOperationDefinition tp (_unOperationName <$> operationName) varDefs [] unresolvedSelSet
|
_grQuery = G.TypedOperationDefinition tp (_unOperationName <$> operationName) varDefs [] unresolvedSelSet
|
||||||
_grVariables = varValsM
|
_grVariables = varValsM
|
||||||
_grOperationName = operationName
|
_grOperationName = operationName
|
||||||
in ExecStepRemote remoteSchemaInfo resultCustomizer GH.GQLReq {..}
|
in ExecStepRemote remoteSchemaInfo resultCustomizer GH.GQLReq {..} remoteJoins
|
||||||
|
|
||||||
-- | Association between keys uniquely identifying some remote JSON variable and
|
-- | Association between keys uniquely identifying some remote JSON variable and
|
||||||
-- an 'Int' identifier that will be used to construct a valid variable name to
|
-- an 'Int' identifier that will be used to construct a valid variable name to
|
||||||
@ -223,8 +219,8 @@ resolveRemoteVariable userInfo = \case
|
|||||||
resolveRemoteField ::
|
resolveRemoteField ::
|
||||||
(MonadError QErr m) =>
|
(MonadError QErr m) =>
|
||||||
UserInfo ->
|
UserInfo ->
|
||||||
RemoteSchemaRootField Void RemoteSchemaVariable ->
|
IR.RemoteSchemaRootField r RemoteSchemaVariable ->
|
||||||
StateT RemoteJSONVariableMap m (RemoteSchemaRootField Void Variable)
|
StateT RemoteJSONVariableMap m (IR.RemoteSchemaRootField r Variable)
|
||||||
resolveRemoteField userInfo = traverse (resolveRemoteVariable userInfo)
|
resolveRemoteField userInfo = traverse (resolveRemoteVariable userInfo)
|
||||||
|
|
||||||
-- | TODO: Documentation.
|
-- | TODO: Documentation.
|
||||||
|
@ -4,12 +4,14 @@ module Hasura.GraphQL.Execute.RemoteJoin.Collect
|
|||||||
getRemoteJoinsMutationDB,
|
getRemoteJoinsMutationDB,
|
||||||
getRemoteJoinsActionQuery,
|
getRemoteJoinsActionQuery,
|
||||||
getRemoteJoinsActionMutation,
|
getRemoteJoinsActionMutation,
|
||||||
|
getRemoteJoinsGraphQLField,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Lens (Traversal', preview, _2)
|
import Control.Lens (Traversal', preview, (^?), _2)
|
||||||
import Control.Monad.Writer
|
import Control.Monad.Writer
|
||||||
import Data.HashMap.Strict qualified as Map
|
import Data.HashMap.Strict qualified as Map
|
||||||
|
import Data.HashMap.Strict.InsOrd qualified as OMap
|
||||||
import Data.HashMap.Strict.NonEmpty (NEHashMap)
|
import Data.HashMap.Strict.NonEmpty (NEHashMap)
|
||||||
import Data.HashMap.Strict.NonEmpty qualified as NEMap
|
import Data.HashMap.Strict.NonEmpty qualified as NEMap
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
@ -63,15 +65,15 @@ newtype Collector a = Collector {runCollector :: (a, Maybe RemoteJoins)}
|
|||||||
|
|
||||||
-- | Collect some remote joins appearing at the given field names in the current
|
-- | Collect some remote joins appearing at the given field names in the current
|
||||||
-- context.
|
-- context.
|
||||||
collect :: NEHashMap FieldName RemoteJoin -> Collector ()
|
collect :: NEHashMap QualifiedFieldName RemoteJoin -> Collector ()
|
||||||
collect = tell . Just . JoinTree . fmap Leaf
|
collect = tell . Just . JoinTree . fmap Leaf
|
||||||
|
|
||||||
-- | Keep track of the given field name in the current path from the root of the
|
-- | Keep track of the given field name in the current path from the root of the
|
||||||
-- selection set.
|
-- selection set.
|
||||||
withField :: FieldName -> Collector a -> Collector a
|
withField :: Maybe Text -> Text -> Collector a -> Collector a
|
||||||
withField name = censor (fmap wrap)
|
withField typeName fieldName = censor (fmap wrap)
|
||||||
where
|
where
|
||||||
wrap rjs = JoinTree $ NEMap.singleton name (Tree rjs)
|
wrap rjs = JoinTree $ NEMap.singleton (QualifiedFieldName typeName fieldName) (Tree rjs)
|
||||||
|
|
||||||
-- | Collects remote joins from the AST and also adds the necessary join fields
|
-- | Collects remote joins from the AST and also adds the necessary join fields
|
||||||
getRemoteJoins ::
|
getRemoteJoins ::
|
||||||
@ -123,7 +125,7 @@ getRemoteJoinsMutationOutput =
|
|||||||
MOutSinglerowObject <$> transformAnnFields annFields
|
MOutSinglerowObject <$> transformAnnFields annFields
|
||||||
where
|
where
|
||||||
transfromMutationFields fields =
|
transfromMutationFields fields =
|
||||||
for fields $ \(fieldName, field') -> withField fieldName do
|
for fields $ \(fieldName, field') -> withField Nothing (getFieldNameTxt fieldName) do
|
||||||
(fieldName,) <$> case field' of
|
(fieldName,) <$> case field' of
|
||||||
MCount -> pure MCount
|
MCount -> pure MCount
|
||||||
MExp t -> pure $ MExp t
|
MExp t -> pure $ MExp t
|
||||||
@ -186,7 +188,7 @@ getRemoteJoinsActionQuery = \case
|
|||||||
in (async {_aaaqFields = fields'}, remoteJoins)
|
in (async {_aaaqFields = fields'}, remoteJoins)
|
||||||
|
|
||||||
transformAsyncFields fields =
|
transformAsyncFields fields =
|
||||||
for fields $ \(fieldName, field) -> withField fieldName do
|
for fields $ \(fieldName, field) -> withField Nothing (getFieldNameTxt fieldName) do
|
||||||
(fieldName,) <$> case field of
|
(fieldName,) <$> case field of
|
||||||
AsyncTypename t -> pure $ AsyncTypename t
|
AsyncTypename t -> pure $ AsyncTypename t
|
||||||
AsyncOutput outputFields ->
|
AsyncOutput outputFields ->
|
||||||
@ -217,7 +219,7 @@ transformAggregateSelect ::
|
|||||||
Collector (AnnAggregateSelectG b Void (UnpreparedValue b))
|
Collector (AnnAggregateSelectG b Void (UnpreparedValue b))
|
||||||
transformAggregateSelect select@AnnSelectG {_asnFields = aggFields} = do
|
transformAggregateSelect select@AnnSelectG {_asnFields = aggFields} = do
|
||||||
transformedFields <- for aggFields \(fieldName, aggField) ->
|
transformedFields <- for aggFields \(fieldName, aggField) ->
|
||||||
withField fieldName $ case aggField of
|
withField Nothing (getFieldNameTxt fieldName) $ case aggField of
|
||||||
TAFAgg agg -> pure (fieldName, TAFAgg agg)
|
TAFAgg agg -> pure (fieldName, TAFAgg agg)
|
||||||
TAFExp t -> pure (fieldName, TAFExp t)
|
TAFExp t -> pure (fieldName, TAFExp t)
|
||||||
TAFNodes nodesAgg annFields -> do
|
TAFNodes nodesAgg annFields -> do
|
||||||
@ -232,7 +234,7 @@ transformConnectionSelect ::
|
|||||||
Collector (ConnectionSelect b Void (UnpreparedValue b))
|
Collector (ConnectionSelect b Void (UnpreparedValue b))
|
||||||
transformConnectionSelect connSelect@ConnectionSelect {..} = do
|
transformConnectionSelect connSelect@ConnectionSelect {..} = do
|
||||||
transformedFields <- for (_asnFields _csSelect) \(fieldName, connField) ->
|
transformedFields <- for (_asnFields _csSelect) \(fieldName, connField) ->
|
||||||
withField fieldName $ case connField of
|
withField Nothing (getFieldNameTxt fieldName) $ case connField of
|
||||||
ConnectionTypename t -> pure (fieldName, ConnectionTypename t)
|
ConnectionTypename t -> pure (fieldName, ConnectionTypename t)
|
||||||
ConnectionPageInfo p -> pure (fieldName, ConnectionPageInfo p)
|
ConnectionPageInfo p -> pure (fieldName, ConnectionPageInfo p)
|
||||||
ConnectionEdges edges -> do
|
ConnectionEdges edges -> do
|
||||||
@ -246,7 +248,7 @@ transformConnectionSelect connSelect@ConnectionSelect {..} = do
|
|||||||
[(FieldName, EdgeField b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))] ->
|
[(FieldName, EdgeField b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))] ->
|
||||||
Collector [(FieldName, EdgeField b Void (UnpreparedValue b))]
|
Collector [(FieldName, EdgeField b Void (UnpreparedValue b))]
|
||||||
transformEdges edgeFields = for edgeFields \(fieldName, edgeField) ->
|
transformEdges edgeFields = for edgeFields \(fieldName, edgeField) ->
|
||||||
withField fieldName $ case edgeField of
|
withField Nothing (getFieldNameTxt fieldName) $ case edgeField of
|
||||||
EdgeTypename t -> pure (fieldName, EdgeTypename t)
|
EdgeTypename t -> pure (fieldName, EdgeTypename t)
|
||||||
EdgeCursor -> pure (fieldName, EdgeCursor)
|
EdgeCursor -> pure (fieldName, EdgeCursor)
|
||||||
EdgeNode annFields -> do
|
EdgeNode annFields -> do
|
||||||
@ -274,16 +276,17 @@ createRemoteJoin joinColumnAliases remoteRelationship =
|
|||||||
case remoteRelationship of
|
case remoteRelationship of
|
||||||
RemoteSchemaField RemoteSchemaSelect {..} ->
|
RemoteSchemaField RemoteSchemaSelect {..} ->
|
||||||
let inputArgsToMap = Map.fromList . map (_rfaArgument &&& _rfaValue)
|
let inputArgsToMap = Map.fromList . map (_rfaArgument &&& _rfaValue)
|
||||||
|
(transformedSchemaRelationship, schemaRelationshipJoins) =
|
||||||
|
getRemoteJoinsGraphQLSelectionSet _rselSelection
|
||||||
remoteJoin =
|
remoteJoin =
|
||||||
RemoteJoinRemoteSchema $
|
RemoteSchemaJoin
|
||||||
RemoteSchemaJoin
|
(inputArgsToMap _rselArgs)
|
||||||
(inputArgsToMap _rselArgs)
|
_rselResultCustomizer
|
||||||
_rselResultCustomizer
|
transformedSchemaRelationship
|
||||||
(convertSelectionSet _rselSelection)
|
joinColumnAliases
|
||||||
joinColumnAliases
|
_rselFieldCall
|
||||||
_rselFieldCall
|
_rselRemoteSchema
|
||||||
_rselRemoteSchema
|
in RemoteJoinRemoteSchema remoteJoin schemaRelationshipJoins
|
||||||
in remoteJoin
|
|
||||||
RemoteSourceField anySourceSelect ->
|
RemoteSourceField anySourceSelect ->
|
||||||
AB.dispatchAnyBackend @Backend anySourceSelect \RemoteSourceSelect {..} ->
|
AB.dispatchAnyBackend @Backend anySourceSelect \RemoteSourceSelect {..} ->
|
||||||
let (transformedSourceRelationship, sourceRelationshipJoins) =
|
let (transformedSourceRelationship, sourceRelationshipJoins) =
|
||||||
@ -317,7 +320,7 @@ transformAnnFields ::
|
|||||||
transformAnnFields fields = do
|
transformAnnFields fields = do
|
||||||
-- Produces a list of transformed fields that may or may not have an
|
-- Produces a list of transformed fields that may or may not have an
|
||||||
-- associated remote join.
|
-- associated remote join.
|
||||||
annotatedFields <- for fields \(fieldName, field') -> withField fieldName do
|
annotatedFields <- for fields \(fieldName, field') -> withField Nothing (getFieldNameTxt fieldName) do
|
||||||
(fieldName,) <$> case field' of
|
(fieldName,) <$> case field' of
|
||||||
-- AnnFields which do not need to be transformed.
|
-- AnnFields which do not need to be transformed.
|
||||||
AFNodeId x qt pkeys -> pure (AFNodeId x qt pkeys, Nothing)
|
AFNodeId x qt pkeys -> pure (AFNodeId x qt pkeys, Nothing)
|
||||||
@ -358,7 +361,7 @@ transformAnnFields fields = do
|
|||||||
let transformedFields = (fmap . fmap) fst annotatedFields
|
let transformedFields = (fmap . fmap) fst annotatedFields
|
||||||
remoteJoins =
|
remoteJoins =
|
||||||
annotatedFields & mapMaybe \(fieldName, (_, mRemoteJoin)) ->
|
annotatedFields & mapMaybe \(fieldName, (_, mRemoteJoin)) ->
|
||||||
(fieldName,) <$> mRemoteJoin
|
(QualifiedFieldName Nothing (getFieldNameTxt fieldName),) <$> mRemoteJoin
|
||||||
|
|
||||||
case NEMap.fromList remoteJoins of
|
case NEMap.fromList remoteJoins of
|
||||||
Nothing -> pure transformedFields
|
Nothing -> pure transformedFields
|
||||||
@ -459,7 +462,7 @@ transformActionFields ::
|
|||||||
transformActionFields fields = do
|
transformActionFields fields = do
|
||||||
-- Produces a list of transformed fields that may or may not have an
|
-- Produces a list of transformed fields that may or may not have an
|
||||||
-- associated remote join.
|
-- associated remote join.
|
||||||
annotatedFields <- for fields \(fieldName, field') -> withField fieldName do
|
annotatedFields <- for fields \(fieldName, field') -> withField Nothing (getFieldNameTxt fieldName) do
|
||||||
(fieldName,) <$> case field' of
|
(fieldName,) <$> case field' of
|
||||||
-- ActionFields which do not need to be transformed.
|
-- ActionFields which do not need to be transformed.
|
||||||
ACFScalar c -> pure (ACFScalar c, Nothing)
|
ACFScalar c -> pure (ACFScalar c, Nothing)
|
||||||
@ -480,7 +483,7 @@ transformActionFields fields = do
|
|||||||
let transformedFields = (fmap . fmap) fst annotatedFields
|
let transformedFields = (fmap . fmap) fst annotatedFields
|
||||||
remoteJoins =
|
remoteJoins =
|
||||||
annotatedFields & mapMaybe \(fieldName, (_, mRemoteJoin)) ->
|
annotatedFields & mapMaybe \(fieldName, (_, mRemoteJoin)) ->
|
||||||
(fieldName,) <$> mRemoteJoin
|
(QualifiedFieldName Nothing (getFieldNameTxt fieldName),) <$> mRemoteJoin
|
||||||
|
|
||||||
case NEMap.fromList remoteJoins of
|
case NEMap.fromList remoteJoins of
|
||||||
Nothing -> pure transformedFields
|
Nothing -> pure transformedFields
|
||||||
@ -571,3 +574,130 @@ getRemoteJoinsSourceRelation = runCollector . transformSourceRelation
|
|||||||
SourceRelationshipArray <$> transformSelect simpleSelect
|
SourceRelationshipArray <$> transformSelect simpleSelect
|
||||||
SourceRelationshipArrayAggregate aggregateSelect ->
|
SourceRelationshipArrayAggregate aggregateSelect ->
|
||||||
SourceRelationshipArrayAggregate <$> transformAggregateSelect aggregateSelect
|
SourceRelationshipArrayAggregate <$> transformAggregateSelect aggregateSelect
|
||||||
|
|
||||||
|
transformGraphQLSelectionSet ::
|
||||||
|
SelectionSet (RemoteRelationshipField UnpreparedValue) var ->
|
||||||
|
Collector (SelectionSet Void var)
|
||||||
|
transformGraphQLSelectionSet = \case
|
||||||
|
SelectionSetNone -> pure SelectionSetNone
|
||||||
|
SelectionSetObject s -> SelectionSetObject <$> transformObjectSelectionSet Nothing s
|
||||||
|
SelectionSetUnion s -> SelectionSetUnion <$> transformAbstractTypeSelectionSet s
|
||||||
|
SelectionSetInterface s -> SelectionSetInterface <$> transformAbstractTypeSelectionSet s
|
||||||
|
where
|
||||||
|
transformAbstractTypeSelectionSet DeduplicatedSelectionSet {..} = do
|
||||||
|
transformedMemberSelectionSets <-
|
||||||
|
_atssMemberSelectionSets & Map.traverseWithKey \typeName objectSelectionSet ->
|
||||||
|
transformObjectSelectionSet (Just typeName) objectSelectionSet
|
||||||
|
pure
|
||||||
|
DeduplicatedSelectionSet
|
||||||
|
{ _atssMemberSelectionSets = transformedMemberSelectionSets,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
|
||||||
|
transformObjectSelectionSet ::
|
||||||
|
-- | The type name on which this selection set is defined; this is only
|
||||||
|
-- expected to be provided for unions and interfaces, not for regular objects,
|
||||||
|
-- as this is used to determine whether a selection set is potentially
|
||||||
|
-- "ambiguous" or not, and regular objects cannot. This will be used as the
|
||||||
|
-- type name in the 'QualifiedFieldName' key of the join tree if this
|
||||||
|
-- selection set or its subselections contain remote joins.
|
||||||
|
Maybe G.Name ->
|
||||||
|
ObjectSelectionSet (RemoteRelationshipField UnpreparedValue) var ->
|
||||||
|
Collector (ObjectSelectionSet Void var)
|
||||||
|
transformObjectSelectionSet typename selectionSet = do
|
||||||
|
-- we need to keep track of whether any subfield contained a remote join
|
||||||
|
(annotatedFields, subfieldsContainRemoteJoins) <-
|
||||||
|
listens isJust $
|
||||||
|
flip OMap.traverseWithKey selectionSet \alias field ->
|
||||||
|
withField (G.unName <$> typename) (G.unName alias) do
|
||||||
|
case field of
|
||||||
|
FieldGraphQL f -> (,Nothing) <$> transformGraphQLField f
|
||||||
|
FieldRemote SchemaRemoteRelationshipSelect {..} -> do
|
||||||
|
pure
|
||||||
|
( mkPlaceholderField alias,
|
||||||
|
Just $ createRemoteJoin joinColumnAliases _srrsRelationship
|
||||||
|
)
|
||||||
|
let internalTypeAlias = $$(G.litName "__hasura_internal_typename")
|
||||||
|
remoteJoins = OMap.mapMaybe snd annotatedFields
|
||||||
|
additionalFields =
|
||||||
|
if
|
||||||
|
| isJust typename && (not (null remoteJoins) || subfieldsContainRemoteJoins) ->
|
||||||
|
-- We are in a situation in which the type name matters, and we know
|
||||||
|
-- that there is at least one remote join in this part of tree, meaning
|
||||||
|
-- we might need to branch on the typename when traversing the join
|
||||||
|
-- tree: we insert a custom field that will return the type name.
|
||||||
|
OMap.singleton internalTypeAlias $
|
||||||
|
mkGraphQLField
|
||||||
|
(Just internalTypeAlias)
|
||||||
|
$$(G.litName "__typename")
|
||||||
|
mempty
|
||||||
|
mempty
|
||||||
|
SelectionSetNone
|
||||||
|
| otherwise ->
|
||||||
|
-- Either the typename doesn't matter, or this tree doesn't have remote
|
||||||
|
-- joins; this selection set isn't "ambiguous".
|
||||||
|
mempty
|
||||||
|
transformedFields = fmap fst annotatedFields <> additionalFields
|
||||||
|
case NEMap.fromList $ OMap.toList remoteJoins of
|
||||||
|
Nothing -> pure $ fmap FieldGraphQL transformedFields
|
||||||
|
Just neRemoteJoins -> do
|
||||||
|
collect $ NEMap.mapKeys (\fieldGName -> QualifiedFieldName (G.unName <$> typename) (G.unName fieldGName)) neRemoteJoins
|
||||||
|
pure $
|
||||||
|
fmap
|
||||||
|
FieldGraphQL
|
||||||
|
(transformedFields <> OMap.fromList [(_fAlias fld, fld) | fld <- toList phantomFields])
|
||||||
|
where
|
||||||
|
nameToField = FieldName . G.unName
|
||||||
|
allAliases = map (nameToField . fst) $ OMap.toList selectionSet
|
||||||
|
|
||||||
|
mkPlaceholderField alias =
|
||||||
|
mkGraphQLField (Just alias) $$(G.litName "__typename") mempty mempty SelectionSetNone
|
||||||
|
|
||||||
|
-- A map of graphql scalar fields (without any arguments) to their aliases
|
||||||
|
-- in the selection set. We do not yet support lhs join fields which take
|
||||||
|
-- arguments. To be consistent with that, we ignore fields with arguments
|
||||||
|
noArgsGraphQLFields =
|
||||||
|
Map.fromList $
|
||||||
|
flip mapMaybe (OMap.toList selectionSet) \(alias, field) -> case field of
|
||||||
|
FieldGraphQL f ->
|
||||||
|
if null (_fArguments f)
|
||||||
|
then Just (_fName f, FieldName $ G.unName alias)
|
||||||
|
else Nothing
|
||||||
|
FieldRemote _ -> Nothing
|
||||||
|
|
||||||
|
annotateLHSJoinField fieldName lhsJoinField =
|
||||||
|
let columnAlias =
|
||||||
|
getJoinColumnAlias fieldName lhsJoinField noArgsGraphQLFields allAliases
|
||||||
|
in ( mkGraphQLField
|
||||||
|
(Just $ G.unsafeMkName $ getFieldNameTxt $ getAliasFieldName columnAlias)
|
||||||
|
lhsJoinField
|
||||||
|
mempty
|
||||||
|
mempty
|
||||||
|
SelectionSetNone,
|
||||||
|
columnAlias
|
||||||
|
)
|
||||||
|
|
||||||
|
(joinColumnAliases, phantomFields) =
|
||||||
|
let lhsJoinFields =
|
||||||
|
Map.unions $ map _srrsLHSJoinFields $ mapMaybe (^? _FieldRemote) $ toList selectionSet
|
||||||
|
annotatedJoinColumns = Map.mapWithKey annotateLHSJoinField lhsJoinFields
|
||||||
|
in (fmap snd annotatedJoinColumns, fmap fst annotatedJoinColumns)
|
||||||
|
|
||||||
|
transformGraphQLField ::
|
||||||
|
GraphQLField (RemoteRelationshipField UnpreparedValue) var ->
|
||||||
|
Collector (GraphQLField Void var)
|
||||||
|
transformGraphQLField GraphQLField {..} = do
|
||||||
|
transformedSelectionSet <- transformGraphQLSelectionSet _fSelectionSet
|
||||||
|
pure $ GraphQLField {_fSelectionSet = transformedSelectionSet, ..}
|
||||||
|
|
||||||
|
getRemoteJoinsGraphQLSelectionSet ::
|
||||||
|
SelectionSet (RemoteRelationshipField UnpreparedValue) var ->
|
||||||
|
(SelectionSet Void var, Maybe RemoteJoins)
|
||||||
|
getRemoteJoinsGraphQLSelectionSet =
|
||||||
|
runCollector . transformGraphQLSelectionSet
|
||||||
|
|
||||||
|
getRemoteJoinsGraphQLField ::
|
||||||
|
GraphQLField (RemoteRelationshipField UnpreparedValue) var ->
|
||||||
|
(GraphQLField Void var, Maybe RemoteJoins)
|
||||||
|
getRemoteJoinsGraphQLField =
|
||||||
|
runCollector . transformGraphQLField
|
||||||
|
@ -3,6 +3,7 @@ module Hasura.GraphQL.Execute.RemoteJoin.Join
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
import Control.Lens (view, _3)
|
||||||
import Data.Aeson qualified as J
|
import Data.Aeson qualified as J
|
||||||
import Data.Aeson.Ordered qualified as JO
|
import Data.Aeson.Ordered qualified as JO
|
||||||
import Data.Environment qualified as Env
|
import Data.Environment qualified as Env
|
||||||
@ -24,6 +25,7 @@ import Hasura.GraphQL.Execute.RemoteJoin.RemoteSchema qualified as RS
|
|||||||
import Hasura.GraphQL.Execute.RemoteJoin.Types
|
import Hasura.GraphQL.Execute.RemoteJoin.Types
|
||||||
import Hasura.GraphQL.Logging (MonadQueryLog)
|
import Hasura.GraphQL.Logging (MonadQueryLog)
|
||||||
import Hasura.GraphQL.Namespace
|
import Hasura.GraphQL.Namespace
|
||||||
|
import Hasura.GraphQL.RemoteServer (execRemoteGQ)
|
||||||
import Hasura.GraphQL.Transport.Backend qualified as TB
|
import Hasura.GraphQL.Transport.Backend qualified as TB
|
||||||
import Hasura.GraphQL.Transport.HTTP.Protocol (GQLReqUnparsed)
|
import Hasura.GraphQL.Transport.HTTP.Protocol (GQLReqUnparsed)
|
||||||
import Hasura.GraphQL.Transport.Instances ()
|
import Hasura.GraphQL.Transport.Instances ()
|
||||||
@ -97,21 +99,17 @@ processRemoteJoins_ ::
|
|||||||
RemoteJoins ->
|
RemoteJoins ->
|
||||||
GQLReqUnparsed ->
|
GQLReqUnparsed ->
|
||||||
m (f JO.Value)
|
m (f JO.Value)
|
||||||
processRemoteJoins_ requestId logger env manager reqHdrs userInfo lhs joinTree gqlreq = do
|
processRemoteJoins_ requestId logger env manager requestHeaders userInfo lhs joinTree gqlreq = do
|
||||||
(compositeValue, joins) <- collectJoinArguments (assignJoinIds joinTree) lhs
|
(compositeValue, joins) <- collectJoinArguments (assignJoinIds joinTree) lhs
|
||||||
joinIndices <- fmap IntMap.catMaybes $
|
joinIndices <- fmap IntMap.catMaybes $
|
||||||
for joins $ \JoinArguments {..} -> do
|
for joins $ \JoinArguments {..} -> do
|
||||||
let joinArguments = IntMap.fromList $ map swap $ Map.toList _jalArguments
|
let joinArguments = IntMap.fromList $ map swap $ Map.toList _jalArguments
|
||||||
case _jalJoin of
|
previousStep <- case _jalJoin of
|
||||||
RemoteJoinRemoteSchema remoteSchemaJoin -> do
|
RemoteJoinRemoteSchema remoteSchemaJoin childJoinTree -> do
|
||||||
-- construct a remote call for
|
let remoteSchemaInfo = rsDef $ _rsjRemoteSchema remoteSchemaJoin
|
||||||
remoteCall <- RS.buildRemoteSchemaCall userInfo remoteSchemaJoin joinArguments
|
networkCall = fmap (view _3) . execRemoteGQ env manager userInfo requestHeaders remoteSchemaInfo
|
||||||
-- A remote call could be Nothing if there are no join arguments
|
maybeJoinIndex <- RS.makeRemoteSchemaJoinCall networkCall userInfo remoteSchemaJoin joinArguments
|
||||||
for remoteCall $ \rsc@(RS.RemoteSchemaCall _ _ _ responsePaths) -> do
|
pure $ fmap (childJoinTree,) maybeJoinIndex
|
||||||
remoteResponse <-
|
|
||||||
RS.getRemoteSchemaResponse env manager reqHdrs userInfo rsc
|
|
||||||
-- extract the join values from the remote's response
|
|
||||||
RS.buildJoinIndex remoteResponse responsePaths
|
|
||||||
RemoteJoinSource sourceJoin childJoinTree -> AB.dispatchAnyBackend @TB.BackendTransport sourceJoin \(RemoteSourceJoin {..} :: RemoteSourceJoin b) -> do
|
RemoteJoinSource sourceJoin childJoinTree -> AB.dispatchAnyBackend @TB.BackendTransport sourceJoin \(RemoteSourceJoin {..} :: RemoteSourceJoin b) -> do
|
||||||
let rows = flip map (IntMap.toList joinArguments) $ \(argumentId, argument) ->
|
let rows = flip map (IntMap.toList joinArguments) $ \(argumentId, argument) ->
|
||||||
Map.insert "__argument_id__" (J.toJSON argumentId) $
|
Map.insert "__argument_id__" (J.toJSON argumentId) $
|
||||||
@ -150,26 +148,26 @@ processRemoteJoins_ requestId logger env manager reqHdrs userInfo lhs joinTree g
|
|||||||
_rsjSourceConfig
|
_rsjSourceConfig
|
||||||
(EB.dbsiAction stepInfo)
|
(EB.dbsiAction stepInfo)
|
||||||
(EB.dbsiPreparedQuery stepInfo)
|
(EB.dbsiPreparedQuery stepInfo)
|
||||||
|
(childJoinTree,) <$> buildSourceDataJoinIndex sourceResponse
|
||||||
preRemoteJoinResults <- buildSourceDataJoinIndex sourceResponse
|
for previousStep $ \(childJoinTree, joinIndex) -> do
|
||||||
forRemoteJoins childJoinTree preRemoteJoinResults $ \childRemoteJoins -> do
|
forRemoteJoins childJoinTree joinIndex $ \childRemoteJoins -> do
|
||||||
results <-
|
results <-
|
||||||
processRemoteJoins_
|
processRemoteJoins_
|
||||||
requestId
|
requestId
|
||||||
logger
|
logger
|
||||||
env
|
env
|
||||||
manager
|
manager
|
||||||
reqHdrs
|
requestHeaders
|
||||||
userInfo
|
userInfo
|
||||||
(IntMap.elems preRemoteJoinResults)
|
(IntMap.elems joinIndex)
|
||||||
childRemoteJoins
|
childRemoteJoins
|
||||||
gqlreq
|
gqlreq
|
||||||
pure $ IntMap.fromAscList $ zip (IntMap.keys preRemoteJoinResults) results
|
pure $ IntMap.fromAscList $ zip (IntMap.keys joinIndex) results
|
||||||
|
|
||||||
joinResults joinIndices compositeValue
|
joinResults joinIndices compositeValue
|
||||||
|
|
||||||
-- | Attempt to construct a 'JoinIndex' from some 'EncJSON' source response.
|
-- | Attempt to construct a 'JoinIndex' from some 'EncJSON' source response.
|
||||||
buildSourceDataJoinIndex :: (MonadError QErr m) => EncJSON -> m JoinIndex
|
buildSourceDataJoinIndex :: (MonadError QErr m) => EncJSON -> m (IntMap.IntMap JO.Value)
|
||||||
buildSourceDataJoinIndex response = do
|
buildSourceDataJoinIndex response = do
|
||||||
json <-
|
json <-
|
||||||
JO.eitherDecode (encJToLBS response) `onLeft` \err ->
|
JO.eitherDecode (encJToLBS response) `onLeft` \err ->
|
||||||
@ -379,22 +377,36 @@ collectJoinArguments joinTree lhs = do
|
|||||||
m
|
m
|
||||||
(InsOrdHashMap Text (CompositeValue ReplacementToken))
|
(InsOrdHashMap Text (CompositeValue ReplacementToken))
|
||||||
traverseObject joinTree_ object = do
|
traverseObject joinTree_ object = do
|
||||||
let phantomFields =
|
let joinTreeNodes = unJoinTree joinTree_
|
||||||
|
phantomFields =
|
||||||
HS.fromList $
|
HS.fromList $
|
||||||
map getFieldNameTxt $
|
map getFieldNameTxt $
|
||||||
concatMap (getPhantomFields . snd) $ toList joinTree_
|
concatMap (getPhantomFields . snd) $ toList joinTree_
|
||||||
|
|
||||||
joinTreeNodes =
|
-- If we need the typename to disambiguate branches in the join tree, it
|
||||||
Map.mapKeys getFieldNameTxt $
|
-- will be present in the answer as a placeholder internal field.
|
||||||
NEMap.toHashMap $
|
--
|
||||||
unJoinTree joinTree_
|
-- We currently have no way of checking whether we explicitly requested
|
||||||
|
-- that field, and it would be possible for a malicious user to attempt to
|
||||||
|
-- spoof that value by explicitly requesting a value they control.
|
||||||
|
-- However, there's no actual risk: we only use that value for lookups
|
||||||
|
-- inside the join tree, and if we didn't request this field, the keys in
|
||||||
|
-- the join tree map will explicitly require a typename NOT to be
|
||||||
|
-- provided. Meaning that any spoofing attempt will just, at worst, result
|
||||||
|
-- in remote joins not being performed.
|
||||||
|
--
|
||||||
|
-- We always remove that key from the resulting object.
|
||||||
|
joinTypeName <- case JO.lookup "__hasura_internal_typename" object of
|
||||||
|
Nothing -> pure Nothing
|
||||||
|
Just (JO.String typename) -> pure $ Just typename
|
||||||
|
Just value -> throw500 $ "The reserved __hasura_internal_typename field contains an unexpected value: " <> tshow value
|
||||||
|
|
||||||
-- during this traversal we assume that the remote join column has some
|
-- during this traversal we assume that the remote join column has some
|
||||||
-- placeholder value in the response. If this weren't present it would
|
-- placeholder value in the response. If this weren't present it would
|
||||||
-- involve a lot more book-keeping to preserve the order of the original
|
-- involve a lot more book-keeping to preserve the order of the original
|
||||||
-- selection set in the response
|
-- selection set in the response
|
||||||
compositeObject <- for (JO.toList object) $ \(fieldName, value_) ->
|
compositeObject <- for (JO.toList object) $ \(fieldName, value_) ->
|
||||||
(fieldName,) <$> case Map.lookup fieldName joinTreeNodes of
|
(fieldName,) <$> case NEMap.lookup (QualifiedFieldName joinTypeName fieldName) joinTreeNodes of
|
||||||
Just (Leaf (joinId, remoteJoin)) -> do
|
Just (Leaf (joinId, remoteJoin)) -> do
|
||||||
joinArgument <- forM (getJoinColumnMapping remoteJoin) $ \alias -> do
|
joinArgument <- forM (getJoinColumnMapping remoteJoin) $ \alias -> do
|
||||||
let aliasTxt = getFieldNameTxt $ getAliasFieldName alias
|
let aliasTxt = getFieldNameTxt $ getAliasFieldName alias
|
||||||
@ -410,7 +422,7 @@ collectJoinArguments joinTree lhs = do
|
|||||||
Just (Tree joinSubTree) ->
|
Just (Tree joinSubTree) ->
|
||||||
Just <$> traverseValue joinSubTree value_
|
Just <$> traverseValue joinSubTree value_
|
||||||
Nothing ->
|
Nothing ->
|
||||||
if HS.member fieldName phantomFields
|
if HS.member fieldName phantomFields || fieldName == "__hasura_internal_typename"
|
||||||
then pure Nothing
|
then pure Nothing
|
||||||
else pure $ Just $ CVOrdValue value_
|
else pure $ Just $ CVOrdValue value_
|
||||||
|
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
|
-- | How to construct and execute a call to a remote schema for a remote join.
|
||||||
|
--
|
||||||
|
-- There are three steps required to do this:
|
||||||
|
-- 1. construct the call: given the requested fields, the phantom fields, the
|
||||||
|
-- values extracted by the LHS, construct a GraphQL query
|
||||||
|
-- 2. execute that GraphQL query over the network
|
||||||
|
-- 3. build a 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
|
||||||
|
-- 'RemoteSchemaCall' type.
|
||||||
module Hasura.GraphQL.Execute.RemoteJoin.RemoteSchema
|
module Hasura.GraphQL.Execute.RemoteJoin.RemoteSchema
|
||||||
( buildRemoteSchemaCall,
|
( -- * Executing a remote join
|
||||||
RemoteSchemaCall (..),
|
makeRemoteSchemaJoinCall,
|
||||||
getRemoteSchemaResponse,
|
|
||||||
|
-- * Individual steps
|
||||||
|
RemoteSchemaCall,
|
||||||
|
buildRemoteSchemaCall,
|
||||||
|
executeRemoteSchemaCall,
|
||||||
buildJoinIndex,
|
buildJoinIndex,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@ -9,96 +24,103 @@ where
|
|||||||
import Control.Lens (view, _2, _3)
|
import Control.Lens (view, _2, _3)
|
||||||
import Data.Aeson qualified as A
|
import Data.Aeson qualified as A
|
||||||
import Data.Aeson.Ordered qualified as AO
|
import Data.Aeson.Ordered qualified as AO
|
||||||
import Data.Environment qualified as Env
|
import Data.ByteString.Lazy qualified as BL
|
||||||
import Data.HashMap.Strict qualified as Map
|
import Data.HashMap.Strict qualified as Map
|
||||||
import Data.IntMap.Strict qualified as IntMap
|
import Data.IntMap.Strict qualified as IntMap
|
||||||
import Data.List.NonEmpty qualified as NE
|
import Data.List.NonEmpty qualified as NE
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import Data.Text.Extended (commaSeparated, toTxt, (<<>))
|
import Data.Text.Extended (commaSeparated, toTxt, (<<>))
|
||||||
import Data.Validation (Validation (..), toEither)
|
import Data.Validation (Validation (..), toEither)
|
||||||
|
import GHC.Stack (HasCallStack)
|
||||||
import Hasura.Base.Error
|
import Hasura.Base.Error
|
||||||
import Hasura.GraphQL.Execute.Remote
|
import Hasura.GraphQL.Execute.Remote
|
||||||
( collectVariablesFromSelectionSet,
|
( getVariableDefinitionAndValue,
|
||||||
resolveRemoteVariable,
|
resolveRemoteVariable,
|
||||||
runVariableCache,
|
runVariableCache,
|
||||||
)
|
)
|
||||||
import Hasura.GraphQL.Execute.RemoteJoin.Types
|
import Hasura.GraphQL.Execute.RemoteJoin.Types
|
||||||
import Hasura.GraphQL.Parser qualified as P
|
import Hasura.GraphQL.Parser qualified as P
|
||||||
import Hasura.GraphQL.RemoteServer (execRemoteGQ)
|
|
||||||
import Hasura.GraphQL.Transport.HTTP.Protocol (GQLReq (..), GQLReqOutgoing)
|
import Hasura.GraphQL.Transport.HTTP.Protocol (GQLReq (..), GQLReqOutgoing)
|
||||||
import Hasura.Prelude
|
import Hasura.Prelude
|
||||||
|
import Hasura.RQL.IR.RemoteSchema (convertSelectionSet)
|
||||||
import Hasura.RQL.Types
|
import Hasura.RQL.Types
|
||||||
import Hasura.Session
|
import Hasura.Session
|
||||||
import Hasura.Tracing qualified as Tracing
|
|
||||||
import Language.GraphQL.Draft.Syntax qualified as G
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
import Network.HTTP.Client qualified as HTTP
|
|
||||||
import Network.HTTP.Types qualified as HTTP
|
|
||||||
|
|
||||||
-- XXX(jkachmar): Think about reworking 'ResponsePath' to be 'Alias, Maybe [G.Name]'
|
-------------------------------------------------------------------------------
|
||||||
|
-- Executing a remote join
|
||||||
|
|
||||||
|
-- | Construct and execute a call to a remote schema for a remote join.
|
||||||
|
makeRemoteSchemaJoinCall ::
|
||||||
|
(MonadError QErr m) =>
|
||||||
|
-- | Function to send a request over the network.
|
||||||
|
(GQLReqOutgoing -> m BL.ByteString) ->
|
||||||
|
-- | User information.
|
||||||
|
UserInfo ->
|
||||||
|
-- | Information about that remote join.
|
||||||
|
RemoteSchemaJoin ->
|
||||||
|
-- | Mapping from 'JoinArgumentId' to its corresponding 'JoinArgument'.
|
||||||
|
IntMap.IntMap JoinArgument ->
|
||||||
|
-- | The resulting join index (see 'buildJoinIndex') if any.
|
||||||
|
m (Maybe (IntMap.IntMap AO.Value))
|
||||||
|
makeRemoteSchemaJoinCall networkFunction userInfo remoteSchemaJoin joinArguments = do
|
||||||
|
-- step 1: construct the internal intermediary representation
|
||||||
|
maybeRemoteCall <- buildRemoteSchemaCall remoteSchemaJoin joinArguments userInfo
|
||||||
|
-- if there actually is a remote call:
|
||||||
|
for maybeRemoteCall \remoteCall -> do
|
||||||
|
-- step 2: execute it over the network
|
||||||
|
responseValue <- executeRemoteSchemaCall networkFunction remoteCall
|
||||||
|
-- step 3: build the join index
|
||||||
|
buildJoinIndex remoteCall responseValue
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Internal representation
|
||||||
|
|
||||||
|
-- | Intermediate type containing all of the information required to perform
|
||||||
|
-- a remote schema call, constructed from the static join information.
|
||||||
|
data RemoteSchemaCall = RemoteSchemaCall
|
||||||
|
{ rscCustomizer :: ResultCustomizer,
|
||||||
|
rscGQLRequest :: GQLReqOutgoing,
|
||||||
|
rscResponsePaths :: IntMap.IntMap ResponsePath
|
||||||
|
}
|
||||||
|
|
||||||
-- | Used to extract the value from a remote schema response.
|
-- | Used to extract the value from a remote schema response.
|
||||||
--
|
--
|
||||||
-- For example: if a remote relationship is defined to retrieve data from some
|
-- For example: if a remote relationship is defined to retrieve data from some
|
||||||
-- deeply nested field, this is the path towards that deeply nested field.
|
-- deeply nested field, this is the path towards that deeply nested field.
|
||||||
newtype ResponsePath = ResponsePath (NE.NonEmpty G.Name)
|
newtype ResponsePath = ResponsePath (NE.NonEmpty G.Name)
|
||||||
-- (Alias, Maybe [G.Name])
|
|
||||||
deriving stock (Eq, Show)
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
-- | The name that we generate when performing a remote join, which shall always
|
-------------------------------------------------------------------------------
|
||||||
-- be the first field in a 'ResponsePath'.
|
-- Step 1: building the remote call
|
||||||
type Alias = G.Name
|
|
||||||
|
|
||||||
-- NOTE: Ideally this should be done at the remote relationship validation
|
-- | Constructs a 'RemoteSchemaCall' from some static information, such as the
|
||||||
-- layer.
|
-- definition of the join, and dynamic information such as the user's
|
||||||
--
|
-- information and the map of join arguments.
|
||||||
-- When validating remote relationships, we should store the validated names so
|
|
||||||
-- that we don't need to continually re-validate them downstream.
|
|
||||||
parseGraphQLName :: (MonadError QErr m) => Text -> m G.Name
|
|
||||||
parseGraphQLName txt =
|
|
||||||
G.mkName txt `onNothing` (throw400 RemoteSchemaError $ errMsg)
|
|
||||||
where
|
|
||||||
errMsg = txt <> " is not a valid GraphQL name"
|
|
||||||
|
|
||||||
-- | Intermediate type containing all of the information required to perform
|
|
||||||
-- a remote schema call.
|
|
||||||
--
|
|
||||||
-- See 'buildRemoteSchemaCall' for details.
|
|
||||||
data RemoteSchemaCall = RemoteSchemaCall
|
|
||||||
{ _rscInfo :: !RemoteSchemaInfo,
|
|
||||||
_rscCustomizer :: !ResultCustomizer,
|
|
||||||
_rscGQLRequest :: !GQLReqOutgoing,
|
|
||||||
_rscResponsePaths :: !(IntMap.IntMap ResponsePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Constructs an outgoing response from the remote relationships definition
|
|
||||||
-- (i.e. 'RemoteSchemaJoin') and the arguments collected from the database's
|
|
||||||
-- response.
|
|
||||||
--
|
|
||||||
-- NOTE: We need to pass along some additional information with the raw outgoing
|
|
||||||
-- GraphQL request, hence the 'RemoteSchemaCall' type.
|
|
||||||
buildRemoteSchemaCall ::
|
buildRemoteSchemaCall ::
|
||||||
(MonadError QErr m) =>
|
(MonadError QErr m) =>
|
||||||
UserInfo ->
|
|
||||||
RemoteSchemaJoin ->
|
RemoteSchemaJoin ->
|
||||||
IntMap.IntMap JoinArgument ->
|
IntMap.IntMap JoinArgument ->
|
||||||
|
UserInfo ->
|
||||||
m (Maybe RemoteSchemaCall)
|
m (Maybe RemoteSchemaCall)
|
||||||
buildRemoteSchemaCall userInfo RemoteSchemaJoin {..} arguments = do
|
buildRemoteSchemaCall RemoteSchemaJoin {..} arguments userInfo = do
|
||||||
-- for each join argument, we generate a unique field, with the alias
|
-- for each join argument, we generate a unique field, with the alias
|
||||||
-- "f" <> argumentId
|
-- "f" <> argumentId
|
||||||
fields <- flip IntMap.traverseWithKey arguments $ \argumentId (JoinArgument argument) -> do
|
fields <- flip IntMap.traverseWithKey arguments $ \argumentId (JoinArgument argument) -> do
|
||||||
graphqlArgs <- fmap Map.fromList $
|
graphqlArgs <- fmap Map.fromList $
|
||||||
for (Map.toList argument) $
|
for (Map.toList argument) \(FieldName columnName, value) -> do
|
||||||
\(FieldName columnName, value) ->
|
graphQLName <- parseGraphQLName columnName
|
||||||
(,) <$> parseGraphQLName columnName <*> ordJSONValueToGValue value
|
graphQLValue <- ordJSONValueToGValue value
|
||||||
|
pure (graphQLName, graphQLValue)
|
||||||
-- Creating the alias should never fail.
|
-- Creating the alias should never fail.
|
||||||
let aliasText = T.pack $ "f" <> show argumentId
|
let aliasText = T.pack $ "f" <> show argumentId
|
||||||
alias <-
|
alias <-
|
||||||
G.mkName aliasText
|
G.mkName aliasText
|
||||||
`onNothing` throw500 ("'" <> aliasText <> "' is not a valid GraphQL name!")
|
`onNothing` throw500 ("'" <> aliasText <> "' is not a valid GraphQL name!")
|
||||||
let responsePath = alias NE.:| map fcName (toList $ NE.tail _rsjFieldCall)
|
let responsePath = alias NE.:| fmap fcName (NE.tail _rsjFieldCall)
|
||||||
rootField = fcName $ NE.head _rsjFieldCall
|
rootField = fcName $ NE.head _rsjFieldCall
|
||||||
resultCustomizer = applyAliasMapping (singletonAliasMapping rootField alias) _rsjResultCustomizer
|
resultCustomizer = applyAliasMapping (singletonAliasMapping rootField alias) _rsjResultCustomizer
|
||||||
gqlField <- fieldCallsToField _rsjArgs graphqlArgs _rsjSelSet alias _rsjFieldCall
|
gqlField <- fieldCallsToField _rsjArgs graphqlArgs (convertSelectionSet _rsjSelSet) alias _rsjFieldCall
|
||||||
pure (gqlField, responsePath, resultCustomizer)
|
pure (gqlField, responsePath, resultCustomizer)
|
||||||
|
|
||||||
-- this constructs the actual GraphQL Request that can be sent to the remote
|
-- this constructs the actual GraphQL Request that can be sent to the remote
|
||||||
@ -108,136 +130,7 @@ buildRemoteSchemaCall userInfo RemoteSchemaJoin {..} arguments = do
|
|||||||
\(field, _, _) -> traverse (resolveRemoteVariable userInfo) field
|
\(field, _, _) -> traverse (resolveRemoteVariable userInfo) field
|
||||||
let customizer = foldMap (view _3) fields
|
let customizer = foldMap (view _3) fields
|
||||||
responsePath = fmap (ResponsePath . view _2) fields
|
responsePath = fmap (ResponsePath . view _2) fields
|
||||||
pure $ RemoteSchemaCall _rsjRemoteSchema customizer gqlRequest responsePath
|
pure $ RemoteSchemaCall customizer gqlRequest responsePath
|
||||||
|
|
||||||
-- | Construct a 'JoinIndex' from the remote source's 'AO.Value' response.
|
|
||||||
--
|
|
||||||
-- If the response does not have value at any of the provided 'ResponsePath's,
|
|
||||||
-- throw a generic 'QErr'.
|
|
||||||
--
|
|
||||||
-- NOTE(jkachmar): If we switch to an 'Applicative' validator, we can collect
|
|
||||||
-- more than one missing 'ResponsePath's (rather than short-circuiting on the
|
|
||||||
-- first missing value).
|
|
||||||
buildJoinIndex ::
|
|
||||||
(Monad m, MonadError QErr m) =>
|
|
||||||
AO.Object ->
|
|
||||||
IntMap.IntMap ResponsePath ->
|
|
||||||
m JoinIndex
|
|
||||||
buildJoinIndex response responsePaths =
|
|
||||||
for responsePaths $ \path -> extractAtPath (AO.Object response) path
|
|
||||||
|
|
||||||
getRemoteSchemaResponse ::
|
|
||||||
( MonadError QErr m,
|
|
||||||
MonadIO m,
|
|
||||||
Tracing.MonadTrace m
|
|
||||||
) =>
|
|
||||||
Env.Environment ->
|
|
||||||
HTTP.Manager ->
|
|
||||||
[HTTP.Header] ->
|
|
||||||
UserInfo ->
|
|
||||||
RemoteSchemaCall ->
|
|
||||||
m AO.Object
|
|
||||||
getRemoteSchemaResponse env manager requestHeaders userInfo (RemoteSchemaCall rsi customizer req _) = do
|
|
||||||
(_, _, respBody) <- execRemoteGQ env manager userInfo requestHeaders (rsDef rsi) req
|
|
||||||
resp <-
|
|
||||||
AO.eitherDecode respBody
|
|
||||||
`onLeft` (\e -> throw500 $ "Remote server response is not valid JSON: " <> T.pack e)
|
|
||||||
respObj <- AO.asObject resp `onLeft` throw500
|
|
||||||
let errors = AO.lookup "errors" respObj
|
|
||||||
if
|
|
||||||
| isNothing errors || errors == Just AO.Null ->
|
|
||||||
case AO.lookup "data" respObj of
|
|
||||||
Nothing -> throw500 "\"data\" field not found in remote response"
|
|
||||||
Just v ->
|
|
||||||
let v' = applyResultCustomizer customizer v
|
|
||||||
in AO.asObject v' `onLeft` throw500
|
|
||||||
| otherwise ->
|
|
||||||
throwError
|
|
||||||
(err400 Unexpected "Errors from remote server")
|
|
||||||
{ qeInternal = Just $ ExtraInternal $ A.object ["errors" A..= (AO.fromOrdered <$> errors)]
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Attempt to extract a deeply nested value from a remote source's 'AO.Value'
|
|
||||||
-- response, according to the JSON path provided by 'ResponsePath'.
|
|
||||||
extractAtPath ::
|
|
||||||
forall m.
|
|
||||||
MonadError QErr m =>
|
|
||||||
AO.Value ->
|
|
||||||
ResponsePath ->
|
|
||||||
m AO.Value
|
|
||||||
extractAtPath initValue (ResponsePath rPath) =
|
|
||||||
go initValue (map G.unName . NE.toList $ rPath)
|
|
||||||
where
|
|
||||||
go :: AO.Value -> [Text] -> m AO.Value
|
|
||||||
go value path = case path of
|
|
||||||
[] -> pure value
|
|
||||||
k : ks -> case value of
|
|
||||||
AO.Object obj -> do
|
|
||||||
objValue <-
|
|
||||||
AO.lookup k obj
|
|
||||||
`onNothing` throw500 ("failed to lookup key '" <> toTxt k <> "' in response")
|
|
||||||
go objValue ks
|
|
||||||
_ ->
|
|
||||||
throw500 $
|
|
||||||
"unexpected non-object json value found while path not empty: "
|
|
||||||
<> commaSeparated path
|
|
||||||
|
|
||||||
ordJSONValueToGValue :: (MonadError QErr n) => AO.Value -> n (G.Value Void)
|
|
||||||
ordJSONValueToGValue =
|
|
||||||
either (throw400 ValidationFailed) pure . P.jsonToGraphQL . AO.fromOrdered
|
|
||||||
|
|
||||||
convertFieldWithVariablesToName :: G.Field G.NoFragments P.Variable -> G.Field G.NoFragments G.Name
|
|
||||||
convertFieldWithVariablesToName = fmap P.getName
|
|
||||||
|
|
||||||
inputValueToJSON :: P.InputValue Void -> A.Value
|
|
||||||
inputValueToJSON = \case
|
|
||||||
P.JSONValue j -> j
|
|
||||||
P.GraphQLValue g -> graphQLValueToJSON g
|
|
||||||
|
|
||||||
-- | TODO: Documentation.
|
|
||||||
collectVariablesFromValue ::
|
|
||||||
G.Value P.Variable -> HashMap G.VariableDefinition A.Value
|
|
||||||
collectVariablesFromValue = foldMap' \var@(P.Variable _ gType val) ->
|
|
||||||
let name = P.getName var
|
|
||||||
jsonVal = inputValueToJSON val
|
|
||||||
defaultVal = getDefaultValue val
|
|
||||||
in Map.singleton (G.VariableDefinition name gType defaultVal) jsonVal
|
|
||||||
where
|
|
||||||
getDefaultValue :: P.InputValue Void -> Maybe (G.Value Void)
|
|
||||||
getDefaultValue = \case
|
|
||||||
P.JSONValue _ -> Nothing
|
|
||||||
P.GraphQLValue g -> Just g
|
|
||||||
|
|
||||||
-- | TODO: Documentation.
|
|
||||||
collectVariablesFromField ::
|
|
||||||
G.Field G.NoFragments P.Variable -> HashMap G.VariableDefinition A.Value
|
|
||||||
collectVariablesFromField (G.Field _ _ arguments _ selSet) =
|
|
||||||
let argumentVariables = fmap collectVariablesFromValue arguments
|
|
||||||
selSetVariables =
|
|
||||||
(fmap . fmap) snd $ collectVariablesFromSelectionSet selSet
|
|
||||||
in fold' (Map.elems argumentVariables) <> Map.fromList selSetVariables
|
|
||||||
|
|
||||||
-- | TODO: Documentation.
|
|
||||||
--
|
|
||||||
-- Extension of the documentation required for 'collectVariablesFromField' and
|
|
||||||
-- 'collectVariablesFromValue'.
|
|
||||||
fieldsToRequest :: NonEmpty (G.Field G.NoFragments P.Variable) -> GQLReqOutgoing
|
|
||||||
fieldsToRequest gFields =
|
|
||||||
let variableInfos = foldMap collectVariablesFromField gFields
|
|
||||||
in GQLReq
|
|
||||||
{ _grOperationName = Nothing,
|
|
||||||
_grVariables =
|
|
||||||
mapKeys G._vdName variableInfos <$ guard (not $ Map.null variableInfos),
|
|
||||||
_grQuery =
|
|
||||||
G.TypedOperationDefinition
|
|
||||||
{ G._todSelectionSet =
|
|
||||||
NE.toList $ G.SelectionField . convertFieldWithVariablesToName <$> gFields,
|
|
||||||
G._todVariableDefinitions = Map.keys variableInfos,
|
|
||||||
G._todType = G.OperationTypeQuery,
|
|
||||||
G._todName = Nothing,
|
|
||||||
G._todDirectives = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Fold nested 'FieldCall's into a bare 'Field', inserting the passed
|
-- | Fold nested 'FieldCall's into a bare 'Field', inserting the passed
|
||||||
-- selection set at the leaf of the tree we construct.
|
-- selection set at the leaf of the tree we construct.
|
||||||
@ -251,7 +144,7 @@ fieldCallsToField ::
|
|||||||
-- | Inserted at leaf of nested FieldCalls
|
-- | Inserted at leaf of nested FieldCalls
|
||||||
G.SelectionSet G.NoFragments RemoteSchemaVariable ->
|
G.SelectionSet G.NoFragments RemoteSchemaVariable ->
|
||||||
-- | Top-level name to set for this Field
|
-- | Top-level name to set for this Field
|
||||||
Alias ->
|
G.Name ->
|
||||||
NonEmpty FieldCall ->
|
NonEmpty FieldCall ->
|
||||||
m (G.Field G.NoFragments RemoteSchemaVariable)
|
m (G.Field G.NoFragments RemoteSchemaVariable)
|
||||||
fieldCallsToField rrArguments variables finalSelSet topAlias =
|
fieldCallsToField rrArguments variables finalSelSet topAlias =
|
||||||
@ -269,7 +162,7 @@ fieldCallsToField rrArguments variables finalSelSet topAlias =
|
|||||||
Nothing ->
|
Nothing ->
|
||||||
let arguments =
|
let arguments =
|
||||||
Map.unionWith
|
Map.unionWith
|
||||||
mergeValue
|
combineValues
|
||||||
graphQLarguments
|
graphQLarguments
|
||||||
-- converting (G.Value Void) -> (G.Value Variable) to merge the
|
-- converting (G.Value Void) -> (G.Value Variable) to merge the
|
||||||
-- 'rrArguments' with the 'variables'
|
-- 'rrArguments' with the 'variables'
|
||||||
@ -290,54 +183,165 @@ fieldCallsToField rrArguments variables finalSelSet topAlias =
|
|||||||
-- FIXME: check that this is correct!
|
-- FIXME: check that this is correct!
|
||||||
throw500 "internal error: encountered an already expanded variable when folding remote field arguments"
|
throw500 "internal error: encountered an already expanded variable when folding remote field arguments"
|
||||||
|
|
||||||
-- FIXME: better error message
|
-- | Create an argument map using the inputs taken from the left hand side.
|
||||||
|
|
||||||
-- This is a kind of "deep merge".
|
|
||||||
-- For e.g. suppose the input argument of the remote field is something like:
|
|
||||||
-- `where: { id : 1}`
|
|
||||||
-- And during execution, client also gives the input arg: `where: {name: "tiru"}`
|
|
||||||
-- We need to merge the input argument to where: {id : 1, name: "tiru"}
|
|
||||||
mergeValue :: G.Value RemoteSchemaVariable -> G.Value RemoteSchemaVariable -> G.Value RemoteSchemaVariable
|
|
||||||
mergeValue lVal rVal = case (lVal, rVal) of
|
|
||||||
(G.VList l, G.VList r) ->
|
|
||||||
G.VList $ l <> r
|
|
||||||
(G.VObject l, G.VObject r) ->
|
|
||||||
G.VObject $ Map.unionWith mergeValue l r
|
|
||||||
(_, _) ->
|
|
||||||
error $
|
|
||||||
"can only merge a list with another list or an "
|
|
||||||
<> "object with another object"
|
|
||||||
|
|
||||||
-- | Create an argument map using the inputs taken from the hasura database.
|
|
||||||
createArguments ::
|
createArguments ::
|
||||||
(MonadError QErr m) =>
|
(MonadError QErr m) =>
|
||||||
Map.HashMap G.Name (G.Value Void) ->
|
Map.HashMap G.Name (G.Value Void) ->
|
||||||
RemoteArguments ->
|
RemoteArguments ->
|
||||||
m (HashMap G.Name (G.Value Void))
|
m (HashMap G.Name (G.Value Void))
|
||||||
createArguments variables (RemoteArguments arguments) =
|
createArguments variables (RemoteArguments arguments) =
|
||||||
toEither (substituteVariables variables arguments)
|
toEither (traverse substituteVariables arguments)
|
||||||
`onLeft` (\errors -> throw400 Unexpected $ "Found errors: " <> commaSeparated errors)
|
`onLeft` (\errors -> throw400 Unexpected $ "Found errors: " <> commaSeparated errors)
|
||||||
|
|
||||||
-- | Substitute values in the argument list.
|
|
||||||
substituteVariables ::
|
|
||||||
-- | Values of the variables to substitute.
|
|
||||||
HashMap G.Name (G.Value Void) ->
|
|
||||||
-- | Template which contains the variables.
|
|
||||||
HashMap G.Name (G.Value G.Name) ->
|
|
||||||
Validation [Text] (HashMap G.Name (G.Value Void))
|
|
||||||
substituteVariables values = traverse go
|
|
||||||
where
|
where
|
||||||
go = \case
|
substituteVariables = \case
|
||||||
G.VVariable variableName ->
|
G.VVariable variableName ->
|
||||||
Map.lookup variableName values
|
Map.lookup variableName variables
|
||||||
`onNothing` Failure ["Value for variable " <> variableName <<> " not provided"]
|
`onNothing` Failure ["Value for variable " <> variableName <<> " not provided"]
|
||||||
G.VList listValue ->
|
G.VList listValue ->
|
||||||
fmap G.VList (traverse go listValue)
|
fmap G.VList (traverse substituteVariables listValue)
|
||||||
G.VObject objectValue ->
|
G.VObject objectValue ->
|
||||||
fmap G.VObject (traverse go objectValue)
|
fmap G.VObject (traverse substituteVariables objectValue)
|
||||||
G.VInt i -> pure $ G.VInt i
|
G.VInt i -> pure $ G.VInt i
|
||||||
G.VFloat d -> pure $ G.VFloat d
|
G.VFloat d -> pure $ G.VFloat d
|
||||||
G.VString txt -> pure $ G.VString txt
|
G.VString txt -> pure $ G.VString txt
|
||||||
G.VEnum e -> pure $ G.VEnum e
|
G.VEnum e -> pure $ G.VEnum e
|
||||||
G.VBoolean b -> pure $ G.VBoolean b
|
G.VBoolean b -> pure $ G.VBoolean b
|
||||||
G.VNull -> pure $ G.VNull
|
G.VNull -> pure $ G.VNull
|
||||||
|
|
||||||
|
-- | Combine two GraphQL values together.
|
||||||
|
--
|
||||||
|
-- This is used to combine different input arguments into one. This function can
|
||||||
|
-- only combine objects or lists pairwise, and fails if it has to combine any
|
||||||
|
-- other combination of values.
|
||||||
|
--
|
||||||
|
-- >>> combineValues (Object (fromList [("id", Number 1)]) (Object (fromList [("name", String "foo")])
|
||||||
|
-- Object (fromList [("id", Number 1), ("name", String "foo")])
|
||||||
|
--
|
||||||
|
-- NOTE: this function *panics* if it fails.
|
||||||
|
combineValues ::
|
||||||
|
HasCallStack => G.Value RemoteSchemaVariable -> G.Value RemoteSchemaVariable -> G.Value RemoteSchemaVariable
|
||||||
|
combineValues (G.VList l) (G.VList r) = G.VList $ l <> r
|
||||||
|
combineValues (G.VObject l) (G.VObject r) = G.VObject $ Map.unionWith combineValues l r
|
||||||
|
combineValues l r =
|
||||||
|
error $
|
||||||
|
"combineValues: cannot combine values (" <> show l <> ") and (" <> show r
|
||||||
|
<> "); \
|
||||||
|
\lists can only be merged with lists, objects can only be merged with objects"
|
||||||
|
|
||||||
|
-- | Craft a GraphQL query document from the list of fields.
|
||||||
|
fieldsToRequest :: NonEmpty (G.Field G.NoFragments P.Variable) -> GQLReqOutgoing
|
||||||
|
fieldsToRequest gFields =
|
||||||
|
GQLReq
|
||||||
|
{ _grOperationName = Nothing,
|
||||||
|
_grVariables =
|
||||||
|
if Map.null variableInfos
|
||||||
|
then Nothing
|
||||||
|
else Just $ mapKeys G._vdName variableInfos,
|
||||||
|
_grQuery =
|
||||||
|
G.TypedOperationDefinition
|
||||||
|
{ G._todSelectionSet =
|
||||||
|
-- convert from Field Variable to Field Name
|
||||||
|
NE.toList $ G.SelectionField . fmap P.getName <$> gFields,
|
||||||
|
G._todVariableDefinitions = Map.keys variableInfos,
|
||||||
|
G._todType = G.OperationTypeQuery,
|
||||||
|
G._todName = Nothing,
|
||||||
|
G._todDirectives = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
where
|
||||||
|
variableInfos :: HashMap G.VariableDefinition A.Value
|
||||||
|
variableInfos = Map.fromList $ concatMap (foldMap getVariableInfo) gFields
|
||||||
|
getVariableInfo :: P.Variable -> [(G.VariableDefinition, A.Value)]
|
||||||
|
getVariableInfo = pure . fmap snd . getVariableDefinitionAndValue
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
-- Step 2: sending the call over the network
|
||||||
|
|
||||||
|
-- | Sends the call over the network, and parse the resulting ByteString.
|
||||||
|
executeRemoteSchemaCall ::
|
||||||
|
(MonadError QErr m) =>
|
||||||
|
-- | Function to send a request over the network.
|
||||||
|
(GQLReqOutgoing -> m BL.ByteString) ->
|
||||||
|
-- | Information about that call.
|
||||||
|
RemoteSchemaCall ->
|
||||||
|
-- | Resulting JSON object
|
||||||
|
m AO.Object
|
||||||
|
executeRemoteSchemaCall networkFunction (RemoteSchemaCall customizer request _) = do
|
||||||
|
responseBody <- networkFunction request
|
||||||
|
responseJSON <-
|
||||||
|
AO.eitherDecode responseBody
|
||||||
|
`onLeft` (\e -> throw500 $ "Remote server response is not valid JSON: " <> T.pack e)
|
||||||
|
responseObject <- AO.asObject responseJSON `onLeft` throw500
|
||||||
|
let errors = AO.lookup "errors" responseObject
|
||||||
|
if
|
||||||
|
| isNothing errors || errors == Just AO.Null ->
|
||||||
|
case AO.lookup "data" responseObject of
|
||||||
|
Nothing -> throw500 "\"data\" field not found in remote response"
|
||||||
|
Just v ->
|
||||||
|
let v' = applyResultCustomizer customizer v
|
||||||
|
in AO.asObject v' `onLeft` throw500
|
||||||
|
| otherwise ->
|
||||||
|
throwError
|
||||||
|
(err400 Unexpected "Errors from remote server")
|
||||||
|
{ qeInternal = Just $ ExtraInternal $ A.object ["errors" A..= (AO.fromOrdered <$> errors)]
|
||||||
|
}
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Step 3: extracting the join index
|
||||||
|
|
||||||
|
-- | Construct a join index from the remote source's 'AO.Value' response.
|
||||||
|
--
|
||||||
|
-- This function extracts from the 'RemoteJoinCall' a mapping from
|
||||||
|
-- 'JoinArgumentId' to 'ResponsePath': from an integer that uniquely identifies
|
||||||
|
-- a join argument to the "path" at which we expect that value in the
|
||||||
|
-- response. With it, and with the actual reponse JSON value obtained from the
|
||||||
|
-- remote server, it constructs a corresponding mapping of, for each argument,
|
||||||
|
-- its extracted value.
|
||||||
|
--
|
||||||
|
-- If the response does not have value at any of the provided 'ResponsePath's,
|
||||||
|
-- throw a generic 'QErr'.
|
||||||
|
--
|
||||||
|
-- NOTE(jkachmar): If we switch to an 'Applicative' validator, we can collect
|
||||||
|
-- more than one missing 'ResponsePath's (rather than short-circuiting on the
|
||||||
|
-- first missing value).
|
||||||
|
buildJoinIndex ::
|
||||||
|
forall m.
|
||||||
|
(MonadError QErr m) =>
|
||||||
|
RemoteSchemaCall ->
|
||||||
|
AO.Object ->
|
||||||
|
m (IntMap.IntMap AO.Value)
|
||||||
|
buildJoinIndex RemoteSchemaCall {..} response =
|
||||||
|
for rscResponsePaths $ \(ResponsePath path) ->
|
||||||
|
go (AO.Object response) (map G.unName . NE.toList $ path)
|
||||||
|
where
|
||||||
|
go :: AO.Value -> [Text] -> m AO.Value
|
||||||
|
go value path = case path of
|
||||||
|
[] -> pure value
|
||||||
|
k : ks -> case value of
|
||||||
|
AO.Object obj -> do
|
||||||
|
objValue <-
|
||||||
|
AO.lookup k obj
|
||||||
|
`onNothing` throw500 ("failed to lookup key '" <> toTxt k <> "' in response")
|
||||||
|
go objValue ks
|
||||||
|
_ ->
|
||||||
|
throw500 $
|
||||||
|
"unexpected non-object json value found while path not empty: "
|
||||||
|
<> commaSeparated path
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Local helpers
|
||||||
|
|
||||||
|
-- NOTE: Ideally this should be done at the remote relationship validation
|
||||||
|
-- layer.
|
||||||
|
--
|
||||||
|
-- When validating remote relationships, we should store the validated names so
|
||||||
|
-- that we don't need to continually re-validate them downstream.
|
||||||
|
parseGraphQLName :: (MonadError QErr m) => Text -> m G.Name
|
||||||
|
parseGraphQLName txt =
|
||||||
|
G.mkName txt `onNothing` (throw400 RemoteSchemaError $ errMsg)
|
||||||
|
where
|
||||||
|
errMsg = txt <> " is not a valid GraphQL name"
|
||||||
|
|
||||||
|
ordJSONValueToGValue :: (MonadError QErr n) => AO.Value -> n (G.Value Void)
|
||||||
|
ordJSONValueToGValue =
|
||||||
|
either (throw400 ValidationFailed) pure . P.jsonToGraphQL . AO.fromOrdered
|
||||||
|
@ -2,21 +2,30 @@
|
|||||||
{-# LANGUAGE UndecidableInstances #-}
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
|
||||||
module Hasura.GraphQL.Execute.RemoteJoin.Types
|
module Hasura.GraphQL.Execute.RemoteJoin.Types
|
||||||
( RemoteJoin (..),
|
( -- * Remote joins tree
|
||||||
getPhantomFields,
|
|
||||||
getJoinColumnMapping,
|
|
||||||
getRemoteSchemaJoins,
|
|
||||||
RemoteSchemaJoin (..),
|
|
||||||
RemoteSourceJoin (..),
|
|
||||||
RemoteJoins,
|
|
||||||
JoinColumnAlias (..),
|
|
||||||
getAliasFieldName,
|
|
||||||
JoinTree (..),
|
JoinTree (..),
|
||||||
JoinNode (..),
|
JoinNode (..),
|
||||||
|
RemoteJoins,
|
||||||
|
QualifiedFieldName (..),
|
||||||
|
getRemoteSchemaJoins,
|
||||||
|
|
||||||
|
-- * Individual join information
|
||||||
|
RemoteJoin (..),
|
||||||
JoinCallId,
|
JoinCallId,
|
||||||
|
JoinColumnAlias (..),
|
||||||
|
getAliasFieldName,
|
||||||
|
getPhantomFields,
|
||||||
|
getJoinColumnMapping,
|
||||||
|
|
||||||
|
-- * Join to source
|
||||||
|
RemoteSourceJoin (..),
|
||||||
|
|
||||||
|
-- * Join to schema
|
||||||
|
RemoteSchemaJoin (..),
|
||||||
|
|
||||||
|
-- * Join arguments
|
||||||
JoinArgumentId,
|
JoinArgumentId,
|
||||||
JoinArgument (..),
|
JoinArgument (..),
|
||||||
JoinIndex,
|
|
||||||
JoinArguments (..),
|
JoinArguments (..),
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
@ -24,15 +33,18 @@ where
|
|||||||
import Control.Lens (view, _1)
|
import Control.Lens (view, _1)
|
||||||
import Data.Aeson.Ordered qualified as AO
|
import Data.Aeson.Ordered qualified as AO
|
||||||
import Data.HashMap.Strict qualified as Map
|
import Data.HashMap.Strict qualified as Map
|
||||||
import Data.HashMap.Strict.NonEmpty qualified as Map
|
import Data.HashMap.Strict.NonEmpty qualified as NEMap
|
||||||
import Data.IntMap.Strict qualified as IntMap
|
|
||||||
import Hasura.GraphQL.Parser qualified as P
|
import Hasura.GraphQL.Parser qualified as P
|
||||||
import Hasura.Prelude
|
import Hasura.Prelude
|
||||||
|
import Hasura.RQL.IR.RemoteSchema qualified as IR
|
||||||
import Hasura.RQL.IR.Select qualified as IR
|
import Hasura.RQL.IR.Select qualified as IR
|
||||||
import Hasura.RQL.Types
|
import Hasura.RQL.Types
|
||||||
import Hasura.SQL.AnyBackend qualified as AB
|
import Hasura.SQL.AnyBackend qualified as AB
|
||||||
import Language.GraphQL.Draft.Syntax qualified as G
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Remote joins tree
|
||||||
|
|
||||||
-- | A JoinTree represents the set of operations that need to be executed to
|
-- | A JoinTree represents the set of operations that need to be executed to
|
||||||
-- enrich the response of a source with data from remote sources. A tree
|
-- enrich the response of a source with data from remote sources. A tree
|
||||||
-- structure is used to capture the locations in the response where the join
|
-- structure is used to capture the locations in the response where the join
|
||||||
@ -62,33 +74,102 @@ import Language.GraphQL.Draft.Syntax qualified as G
|
|||||||
-- Note that the same join tree will be emitted even if 'city' is of type
|
-- Note that the same join tree will be emitted even if 'city' is of type
|
||||||
-- '[City]' and 'state' is of type [State], we currently do not capture any
|
-- '[City]' and 'state' is of type [State], we currently do not capture any
|
||||||
-- information if any of the fields in the path expect json arrays. It is
|
-- information if any of the fields in the path expect json arrays. It is
|
||||||
-- similar in spirit to a GraphQL selection set in this regard
|
-- similar in spirit to a GraphQL selection set in this regard.
|
||||||
newtype JoinTree a = JoinTree {unJoinTree :: Map.NEHashMap FieldName (JoinNode a)}
|
--
|
||||||
deriving stock (Eq, Foldable, Functor, Generic, Traversable, Show)
|
-- This structure is somewhat similar to a prefix tree such as 'Data.Trie.Trie',
|
||||||
|
-- but has two additional guarantees:
|
||||||
|
-- - a 'JoinTree' is never empty,
|
||||||
|
-- - there cannot exist a pair of values for which one's prefix key is a
|
||||||
|
-- subset of the other: every value is effectively a leaf.
|
||||||
|
newtype JoinTree a = JoinTree {unJoinTree :: NEMap.NEHashMap QualifiedFieldName (JoinNode a)}
|
||||||
|
deriving stock (Show, Eq, Functor, Foldable, Traversable, Generic)
|
||||||
deriving newtype (Semigroup)
|
deriving newtype (Semigroup)
|
||||||
|
|
||||||
|
-- | A field name annotated with an optional type name.
|
||||||
|
--
|
||||||
|
-- To deal with ambiguous join paths, such as those that emerge from GraphQL
|
||||||
|
-- interfaces or GraphQL unions, we do not just keep track of the fields' name,
|
||||||
|
-- but also, optionally, of their type. Whenever a selection set is deemed
|
||||||
|
-- ambiguous, we insert a reserved field in the query to retrieve the typename,
|
||||||
|
-- @__hasura_internal_typename@; when traversing the join tree, if that key is
|
||||||
|
-- present, then we use it alongside the field name when querying the join tree
|
||||||
|
-- (see @traverseObject@ in the @Join@ module).
|
||||||
|
--
|
||||||
|
-- We use 'Text' for the representation of the field name instead of
|
||||||
|
-- 'FieldName', for simplicity: the join tree is only meant to be queried using
|
||||||
|
-- the values we get in the reponse, which will be unrestricted text.
|
||||||
|
data QualifiedFieldName = QualifiedFieldName
|
||||||
|
{ _qfTypeName :: Maybe Text,
|
||||||
|
_qfFieldName :: Text
|
||||||
|
}
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (Hashable)
|
||||||
|
|
||||||
|
-- | Each leaf associates a mapping from typename to actual join info.
|
||||||
|
-- This allows to disambiguate between different remote joins with the same name
|
||||||
|
-- in a given selection set, which might happen with union or interface
|
||||||
|
-- fragments.
|
||||||
data JoinNode a
|
data JoinNode a
|
||||||
= Leaf a
|
= Leaf a
|
||||||
| Tree !(JoinTree a)
|
| Tree (JoinTree a)
|
||||||
deriving stock (Eq, Foldable, Functor, Generic, Traversable, Show)
|
deriving stock (Eq, Foldable, Functor, Generic, Traversable, Show)
|
||||||
|
|
||||||
type RemoteJoins = JoinTree RemoteJoin
|
type RemoteJoins = JoinTree RemoteJoin
|
||||||
|
|
||||||
-- | TODO(jkachmar): Documentation
|
-- | Collect all the remote joins to a remote schema from a join tree.
|
||||||
|
getRemoteSchemaJoins :: RemoteJoins -> [RemoteSchemaJoin]
|
||||||
|
getRemoteSchemaJoins = concatMap getRemoteSchemaJoin . toList
|
||||||
|
where
|
||||||
|
getRemoteSchemaJoin :: RemoteJoin -> [RemoteSchemaJoin]
|
||||||
|
getRemoteSchemaJoin = \case
|
||||||
|
RemoteJoinSource _ remoteJoins -> maybe [] getRemoteSchemaJoins remoteJoins
|
||||||
|
RemoteJoinRemoteSchema s remoteJoins -> s : maybe [] getRemoteSchemaJoins remoteJoins
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
-- Individual join information
|
||||||
|
|
||||||
|
-- | An individual join entry point in a 'JoinTree'.
|
||||||
|
--
|
||||||
|
-- Either a join against a source, or against a remote schema. In either case,
|
||||||
|
-- the constructor will contain that particular join's information (a
|
||||||
|
-- 'RemoteSourceJoin' or 'RemoteSchemaJoin' respectively) and, recursively, the
|
||||||
|
-- set of follow-up 'RemoteJoins' from that target, if any.
|
||||||
data RemoteJoin
|
data RemoteJoin
|
||||||
= RemoteJoinSource !(AB.AnyBackend RemoteSourceJoin) !(Maybe RemoteJoins)
|
= RemoteJoinSource (AB.AnyBackend RemoteSourceJoin) (Maybe RemoteJoins)
|
||||||
| RemoteJoinRemoteSchema !RemoteSchemaJoin
|
| RemoteJoinRemoteSchema RemoteSchemaJoin (Maybe RemoteJoins)
|
||||||
deriving stock (Eq, Generic)
|
deriving stock (Eq, Generic)
|
||||||
|
|
||||||
-- | This collects all the remote joins from a join tree
|
-- | A unique id that gets assigned to each 'RemoteJoin' (this is to avoid the
|
||||||
getRemoteSchemaJoins :: RemoteJoins -> [RemoteSchemaJoin]
|
-- requirement of Ord/Hashable implementation for RemoteJoin)
|
||||||
getRemoteSchemaJoins = mapMaybe getRemoteSchemaJoin . toList
|
type JoinCallId = Int
|
||||||
where
|
|
||||||
getRemoteSchemaJoin :: RemoteJoin -> Maybe RemoteSchemaJoin
|
|
||||||
getRemoteSchemaJoin = \case
|
|
||||||
RemoteJoinSource _ _ -> Nothing
|
|
||||||
RemoteJoinRemoteSchema s -> Just s
|
|
||||||
|
|
||||||
|
-- | Disambiguates between 'FieldName's which are provided as part of the
|
||||||
|
-- GraphQL selection provided by the user (i.e. 'JCSelected') and those which
|
||||||
|
-- we need to retreive data but which are not expressly requested (i.e.
|
||||||
|
-- 'JCPhantom').
|
||||||
|
--
|
||||||
|
-- After processing the remote join, we remove all phantom 'FieldName's and
|
||||||
|
-- only return those which fall under the 'JCSelected' branch of this type.
|
||||||
|
data JoinColumnAlias
|
||||||
|
= -- | This fieldname is already part of the response.
|
||||||
|
JCSelected !FieldName
|
||||||
|
| -- | This is explicitly added for the join.
|
||||||
|
--
|
||||||
|
-- Such keys will have to be removed from the response eventually.
|
||||||
|
JCPhantom !FieldName
|
||||||
|
deriving stock (Eq, Generic, Show)
|
||||||
|
deriving anyclass (Hashable)
|
||||||
|
|
||||||
|
-- | Extracts the field name from the 'JoinColumnAlias', regardless of whether
|
||||||
|
-- the field is requested by the user of a "phantom" field.
|
||||||
|
getAliasFieldName :: JoinColumnAlias -> FieldName
|
||||||
|
getAliasFieldName = \case
|
||||||
|
JCSelected f -> f
|
||||||
|
JCPhantom f -> f
|
||||||
|
|
||||||
|
-- | Extracts the list of phantom field names out of a given 'RemoteJoin',
|
||||||
|
-- i.e. the name of the fields that must be part of the query but were not
|
||||||
|
-- requested by the user.
|
||||||
getPhantomFields :: RemoteJoin -> [FieldName]
|
getPhantomFields :: RemoteJoin -> [FieldName]
|
||||||
getPhantomFields =
|
getPhantomFields =
|
||||||
mapMaybe getPhantomFieldName . Map.elems . getJoinColumnMapping
|
mapMaybe getPhantomFieldName . Map.elems . getJoinColumnMapping
|
||||||
@ -98,16 +179,27 @@ getPhantomFields =
|
|||||||
JCSelected _ -> Nothing
|
JCSelected _ -> Nothing
|
||||||
JCPhantom f -> Just f
|
JCPhantom f -> Just f
|
||||||
|
|
||||||
|
-- | Extracts an abstracted field mapping for a particular 'RemoteJoin', using a
|
||||||
|
-- common representation.
|
||||||
|
--
|
||||||
|
-- The RHS of the mapping uses 'JoinColumnAlias' instead of 'FieldName' to
|
||||||
|
-- differentiate between selected fields and phantom fields (see
|
||||||
|
-- 'JoinColumnAlias').
|
||||||
getJoinColumnMapping :: RemoteJoin -> Map.HashMap FieldName JoinColumnAlias
|
getJoinColumnMapping :: RemoteJoin -> Map.HashMap FieldName JoinColumnAlias
|
||||||
getJoinColumnMapping = \case
|
getJoinColumnMapping = \case
|
||||||
RemoteJoinSource sourceJoin _ -> AB.runBackend
|
RemoteJoinSource sourceJoin _ -> AB.runBackend
|
||||||
sourceJoin
|
sourceJoin
|
||||||
\RemoteSourceJoin {_rsjJoinColumns} ->
|
\RemoteSourceJoin {_rsjJoinColumns} ->
|
||||||
fmap (view _1) _rsjJoinColumns
|
fmap (view _1) _rsjJoinColumns
|
||||||
RemoteJoinRemoteSchema RemoteSchemaJoin {_rsjJoinColumnAliases} ->
|
RemoteJoinRemoteSchema RemoteSchemaJoin {_rsjJoinColumnAliases} _ ->
|
||||||
_rsjJoinColumnAliases
|
_rsjJoinColumnAliases
|
||||||
|
|
||||||
-- | TODO(jkachmar): Documentation.
|
-------------------------------------------------------------------------------
|
||||||
|
-- Join to source
|
||||||
|
|
||||||
|
-- | A 'RemoteSourceJoin' contains all the contextual information required for
|
||||||
|
-- the execution of a join against a source, translated from the IR's
|
||||||
|
-- representation of a selection (see 'AnnFieldG').
|
||||||
data RemoteSourceJoin b = RemoteSourceJoin
|
data RemoteSourceJoin b = RemoteSourceJoin
|
||||||
{ _rsjSource :: !SourceName,
|
{ _rsjSource :: !SourceName,
|
||||||
_rsjSourceConfig :: !(SourceConfig b),
|
_rsjSourceConfig :: !(SourceConfig b),
|
||||||
@ -131,37 +223,19 @@ deriving instance
|
|||||||
) =>
|
) =>
|
||||||
Eq (RemoteSourceJoin b)
|
Eq (RemoteSourceJoin b)
|
||||||
|
|
||||||
-- | Disambiguates between 'FieldName's which are provided as part of the
|
-------------------------------------------------------------------------------
|
||||||
-- GraphQL selection provided by the user (i.e. 'JCSelected') and those which
|
-- Join to schema
|
||||||
-- we need to retreive data but which are not expressly requested (i.e.
|
|
||||||
-- 'JCPhantom').
|
|
||||||
--
|
|
||||||
-- After processing the remote join, we remove all phantom 'FieldName's and
|
|
||||||
-- only return those which fall under the 'JCSelected' branch of this type.
|
|
||||||
data JoinColumnAlias
|
|
||||||
= -- | This fieldname is already part of the response.
|
|
||||||
JCSelected !FieldName
|
|
||||||
| -- | This is explicitly added for the join.
|
|
||||||
--
|
|
||||||
-- Such keys will have to be removed from the response eventually.
|
|
||||||
JCPhantom !FieldName
|
|
||||||
deriving stock (Eq, Generic, Show)
|
|
||||||
deriving anyclass (Hashable)
|
|
||||||
|
|
||||||
getAliasFieldName :: JoinColumnAlias -> FieldName
|
-- | A 'RemoteSchemaJoin' contains all the contextual information required for
|
||||||
getAliasFieldName = \case
|
-- the execution of a join against a remote schema, translated from the IR's
|
||||||
JCSelected f -> f
|
-- representation of a selection (see 'AnnFieldG').
|
||||||
JCPhantom f -> f
|
|
||||||
|
|
||||||
-- | A 'RemoteSchemaJoin' represents the context of a remote relationship to be
|
|
||||||
-- extracted from 'AnnFieldG's.
|
|
||||||
data RemoteSchemaJoin = RemoteSchemaJoin
|
data RemoteSchemaJoin = RemoteSchemaJoin
|
||||||
{ -- | User-provided arguments with variables.
|
{ -- | User-provided arguments with variables.
|
||||||
_rsjArgs :: !(Map.HashMap G.Name (P.InputValue RemoteSchemaVariable)),
|
_rsjArgs :: !(Map.HashMap G.Name (P.InputValue RemoteSchemaVariable)),
|
||||||
-- | Customizer for JSON result from the remote server.
|
-- | Customizer for JSON result from the remote server.
|
||||||
_rsjResultCustomizer :: !ResultCustomizer,
|
_rsjResultCustomizer :: !ResultCustomizer,
|
||||||
-- | User-provided selection set of remote field.
|
-- | User-provided selection set of remote field.
|
||||||
_rsjSelSet :: !(G.SelectionSet G.NoFragments RemoteSchemaVariable),
|
_rsjSelSet :: !(IR.SelectionSet Void RemoteSchemaVariable),
|
||||||
-- | A map of the join column to its alias in the response
|
-- | A map of the join column to its alias in the response
|
||||||
_rsjJoinColumnAliases :: !(Map.HashMap FieldName JoinColumnAlias),
|
_rsjJoinColumnAliases :: !(Map.HashMap FieldName JoinColumnAlias),
|
||||||
-- | Remote server fields.
|
-- | Remote server fields.
|
||||||
@ -177,9 +251,8 @@ instance Eq RemoteSchemaJoin where
|
|||||||
(==) = on (==) \RemoteSchemaJoin {..} ->
|
(==) = on (==) \RemoteSchemaJoin {..} ->
|
||||||
(_rsjArgs, _rsjSelSet, _rsjJoinColumnAliases, _rsjFieldCall, _rsjRemoteSchema)
|
(_rsjArgs, _rsjSelSet, _rsjJoinColumnAliases, _rsjFieldCall, _rsjRemoteSchema)
|
||||||
|
|
||||||
-- | A unique id that gets assigned to each 'RemoteJoin' (this is to avoid the
|
-------------------------------------------------------------------------------
|
||||||
-- requirement of Ord/Hashable implementation for RemoteJoin)
|
-- Join arguments
|
||||||
type JoinCallId = Int
|
|
||||||
|
|
||||||
-- | A map of fieldname to values extracted from each LHS row/object
|
-- | A map of fieldname to values extracted from each LHS row/object
|
||||||
--
|
--
|
||||||
@ -195,9 +268,6 @@ newtype JoinArgument = JoinArgument {unJoinArgument :: Map.HashMap FieldName AO.
|
|||||||
-- | A unique id assigned to each join argument
|
-- | A unique id assigned to each join argument
|
||||||
type JoinArgumentId = Int
|
type JoinArgumentId = Int
|
||||||
|
|
||||||
-- | A map of JoinArgumentId to its value fetched from the RHS source of a join
|
|
||||||
type JoinIndex = IntMap.IntMap AO.Value
|
|
||||||
|
|
||||||
data JoinArguments = JoinArguments
|
data JoinArguments = JoinArguments
|
||||||
{ -- | The 'RemoteJoin' associated with the join arguments within this
|
{ -- | The 'RemoteJoin' associated with the join arguments within this
|
||||||
-- structure.
|
-- structure.
|
||||||
|
@ -243,12 +243,12 @@ buildRoleContext
|
|||||||
where
|
where
|
||||||
getQueryRemotes ::
|
getQueryRemotes ::
|
||||||
[ParsedIntrospection] ->
|
[ParsedIntrospection] ->
|
||||||
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))]
|
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))]
|
||||||
getQueryRemotes = concatMap piQuery
|
getQueryRemotes = concatMap piQuery
|
||||||
|
|
||||||
getMutationRemotes ::
|
getMutationRemotes ::
|
||||||
[ParsedIntrospection] ->
|
[ParsedIntrospection] ->
|
||||||
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))]
|
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))]
|
||||||
getMutationRemotes = concatMap (concat . piMutation)
|
getMutationRemotes = concatMap (concat . piMutation)
|
||||||
|
|
||||||
buildSource ::
|
buildSource ::
|
||||||
@ -429,8 +429,8 @@ unauthenticatedContext ::
|
|||||||
( MonadError QErr m,
|
( MonadError QErr m,
|
||||||
MonadIO m
|
MonadIO m
|
||||||
) =>
|
) =>
|
||||||
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))] ->
|
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))] ->
|
||||||
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))] ->
|
[P.FieldParser (P.ParseT Identity) (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))] ->
|
||||||
RemoteSchemaPermsCtx ->
|
RemoteSchemaPermsCtx ->
|
||||||
m GQLContext
|
m GQLContext
|
||||||
unauthenticatedContext adminQueryRemotes adminMutationRemotes remoteSchemaPermsCtx = P.runSchemaT $ do
|
unauthenticatedContext adminQueryRemotes adminMutationRemotes remoteSchemaPermsCtx = P.runSchemaT $ do
|
||||||
@ -613,7 +613,7 @@ buildQueryParser ::
|
|||||||
Has CustomizeRemoteFieldName r
|
Has CustomizeRemoteFieldName r
|
||||||
) =>
|
) =>
|
||||||
[P.FieldParser n (NamespacedField (QueryRootField UnpreparedValue))] ->
|
[P.FieldParser n (NamespacedField (QueryRootField UnpreparedValue))] ->
|
||||||
[P.FieldParser n (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))] ->
|
[P.FieldParser n (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))] ->
|
||||||
[ActionInfo] ->
|
[ActionInfo] ->
|
||||||
AnnotatedCustomTypes ->
|
AnnotatedCustomTypes ->
|
||||||
Maybe (Parser 'Output n (RootFieldMap (MutationRootField UnpreparedValue))) ->
|
Maybe (Parser 'Output n (RootFieldMap (MutationRootField UnpreparedValue))) ->
|
||||||
@ -711,7 +711,7 @@ buildMutationParser ::
|
|||||||
Has MkRootFieldName r,
|
Has MkRootFieldName r,
|
||||||
Has CustomizeRemoteFieldName r
|
Has CustomizeRemoteFieldName r
|
||||||
) =>
|
) =>
|
||||||
[P.FieldParser n (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))] ->
|
[P.FieldParser n (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))] ->
|
||||||
[ActionInfo] ->
|
[ActionInfo] ->
|
||||||
AnnotatedCustomTypes ->
|
AnnotatedCustomTypes ->
|
||||||
[P.FieldParser n (NamespacedField (MutationRootField UnpreparedValue))] ->
|
[P.FieldParser n (NamespacedField (MutationRootField UnpreparedValue))] ->
|
||||||
|
@ -24,9 +24,10 @@ import Hasura.GraphQL.Parser.Internal.Parser qualified as P
|
|||||||
import Hasura.GraphQL.Parser.Internal.TypeChecking qualified as P
|
import Hasura.GraphQL.Parser.Internal.TypeChecking qualified as P
|
||||||
import Hasura.Prelude
|
import Hasura.Prelude
|
||||||
import Hasura.RQL.IR.RemoteSchema qualified as IR
|
import Hasura.RQL.IR.RemoteSchema qualified as IR
|
||||||
|
import Hasura.RQL.IR.Root qualified as IR
|
||||||
import Hasura.RQL.Types.RemoteSchema
|
import Hasura.RQL.Types.RemoteSchema
|
||||||
import Hasura.RQL.Types.ResultCustomization
|
import Hasura.RQL.Types.ResultCustomization
|
||||||
import Hasura.RQL.Types.SchemaCache (IntrospectionResult (IntrospectionResult, irMutationRoot, irQueryRoot, irSubscriptionRoot), ParsedIntrospectionG (..))
|
import Hasura.RQL.Types.SchemaCache
|
||||||
import Hasura.RQL.Types.SourceCustomization
|
import Hasura.RQL.Types.SourceCustomization
|
||||||
import Language.GraphQL.Draft.Syntax qualified as G
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ buildRemoteParser introspectionResult remoteSchemaInfo@RemoteSchemaInfo {..} = d
|
|||||||
|
|
||||||
makeResultCustomizer ::
|
makeResultCustomizer ::
|
||||||
RemoteSchemaCustomizer ->
|
RemoteSchemaCustomizer ->
|
||||||
IR.GraphQLField Void RemoteSchemaVariable ->
|
IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable ->
|
||||||
ResultCustomizer
|
ResultCustomizer
|
||||||
makeResultCustomizer remoteSchemaCustomizer IR.GraphQLField {..} =
|
makeResultCustomizer remoteSchemaCustomizer IR.GraphQLField {..} =
|
||||||
modifyFieldByName _fAlias $
|
modifyFieldByName _fAlias $
|
||||||
@ -61,16 +62,21 @@ makeResultCustomizer remoteSchemaCustomizer IR.GraphQLField {..} =
|
|||||||
else resultCustomizerFromSelection _fSelectionSet
|
else resultCustomizerFromSelection _fSelectionSet
|
||||||
where
|
where
|
||||||
resultCustomizerFromSelection ::
|
resultCustomizerFromSelection ::
|
||||||
IR.SelectionSet Void RemoteSchemaVariable -> ResultCustomizer
|
IR.SelectionSet (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable -> ResultCustomizer
|
||||||
resultCustomizerFromSelection = \case
|
resultCustomizerFromSelection = \case
|
||||||
IR.SelectionSetObject s -> foldMap customizeField s
|
IR.SelectionSetObject s -> foldMap customizeField s
|
||||||
IR.SelectionSetUnion s -> foldMap (foldMap customizeField) $ IR._atssMemberSelectionSets s
|
IR.SelectionSetUnion s -> foldMap (foldMap customizeField) $ IR._atssMemberSelectionSets s
|
||||||
IR.SelectionSetInterface s -> foldMap (foldMap customizeField) $ IR._atssMemberSelectionSets s
|
IR.SelectionSetInterface s -> foldMap (foldMap customizeField) $ IR._atssMemberSelectionSets s
|
||||||
IR.SelectionSetNone -> mempty
|
IR.SelectionSetNone -> mempty
|
||||||
|
|
||||||
customizeField :: IR.Field Void RemoteSchemaVariable -> ResultCustomizer
|
customizeField :: IR.Field (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable -> ResultCustomizer
|
||||||
customizeField = \case
|
customizeField = \case
|
||||||
IR.FieldGraphQL f -> makeResultCustomizer remoteSchemaCustomizer f
|
IR.FieldGraphQL f -> makeResultCustomizer remoteSchemaCustomizer f
|
||||||
|
-- we do not traverse the remote because that part of the response is
|
||||||
|
-- never exists in the response by a remote schema - it is only added
|
||||||
|
-- later by the remote joins execution engine, which in turn would have
|
||||||
|
-- been processed by its own result customizer if applicable
|
||||||
|
IR.FieldRemote _ -> mempty
|
||||||
|
|
||||||
buildRawRemoteParser ::
|
buildRawRemoteParser ::
|
||||||
forall r m n.
|
forall r m n.
|
||||||
@ -79,9 +85,9 @@ buildRawRemoteParser ::
|
|||||||
RemoteSchemaInfo ->
|
RemoteSchemaInfo ->
|
||||||
-- | parsers for, respectively: queries, mutations, and subscriptions
|
-- | parsers for, respectively: queries, mutations, and subscriptions
|
||||||
m
|
m
|
||||||
( [P.FieldParser n (IR.RemoteSchemaRootField Void RemoteSchemaVariable)],
|
( [P.FieldParser n (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)],
|
||||||
Maybe [P.FieldParser n (IR.RemoteSchemaRootField Void RemoteSchemaVariable)],
|
Maybe [P.FieldParser n (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)],
|
||||||
Maybe [P.FieldParser n (IR.RemoteSchemaRootField Void RemoteSchemaVariable)]
|
Maybe [P.FieldParser n (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)]
|
||||||
)
|
)
|
||||||
buildRawRemoteParser (IntrospectionResult sdoc queryRoot mutationRoot subscriptionRoot) info = do
|
buildRawRemoteParser (IntrospectionResult sdoc queryRoot mutationRoot subscriptionRoot) info = do
|
||||||
queryT <- makeParsers queryRoot
|
queryT <- makeParsers queryRoot
|
||||||
@ -89,21 +95,21 @@ buildRawRemoteParser (IntrospectionResult sdoc queryRoot mutationRoot subscripti
|
|||||||
subscriptionT <- makeNonQueryRootFieldParser subscriptionRoot $$(G.litName "Subscription")
|
subscriptionT <- makeNonQueryRootFieldParser subscriptionRoot $$(G.litName "Subscription")
|
||||||
return (queryT, mutationT, subscriptionT)
|
return (queryT, mutationT, subscriptionT)
|
||||||
where
|
where
|
||||||
makeFieldParser :: G.Name -> G.FieldDefinition RemoteSchemaInputValueDefinition -> m (P.FieldParser n (IR.RemoteSchemaRootField Void RemoteSchemaVariable))
|
makeFieldParser :: G.Name -> G.FieldDefinition RemoteSchemaInputValueDefinition -> m (P.FieldParser n (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
makeFieldParser rootTypeName fieldDef =
|
makeFieldParser rootTypeName fieldDef =
|
||||||
fmap makeRemoteField <$> remoteFieldFromDefinition sdoc rootTypeName fieldDef
|
fmap makeRemoteField <$> remoteFieldFromDefinition sdoc rootTypeName fieldDef
|
||||||
|
|
||||||
makeRemoteField :: IR.GraphQLField Void RemoteSchemaVariable -> (IR.RemoteSchemaRootField Void RemoteSchemaVariable)
|
makeRemoteField :: IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable -> (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)
|
||||||
makeRemoteField fld = IR.RemoteSchemaRootField info (makeResultCustomizer (rsCustomizer info) fld) fld
|
makeRemoteField fld = IR.RemoteSchemaRootField info (makeResultCustomizer (rsCustomizer info) fld) fld
|
||||||
|
|
||||||
makeParsers :: G.Name -> m [P.FieldParser n (IR.RemoteSchemaRootField Void RemoteSchemaVariable)]
|
makeParsers :: G.Name -> m [P.FieldParser n (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)]
|
||||||
makeParsers rootName =
|
makeParsers rootName =
|
||||||
case lookupType sdoc rootName of
|
case lookupType sdoc rootName of
|
||||||
Just (G.TypeDefinitionObject o) ->
|
Just (G.TypeDefinitionObject o) ->
|
||||||
traverse (makeFieldParser rootName) $ G._otdFieldsDefinition o
|
traverse (makeFieldParser rootName) $ G._otdFieldsDefinition o
|
||||||
_ -> throw400 Unexpected $ rootName <<> " has to be an object type"
|
_ -> throw400 Unexpected $ rootName <<> " has to be an object type"
|
||||||
|
|
||||||
makeNonQueryRootFieldParser :: Maybe G.Name -> G.Name -> m (Maybe [P.FieldParser n (IR.RemoteSchemaRootField Void RemoteSchemaVariable)])
|
makeNonQueryRootFieldParser :: Maybe G.Name -> G.Name -> m (Maybe [P.FieldParser n (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)])
|
||||||
makeNonQueryRootFieldParser userProvidedRootName defaultRootName =
|
makeNonQueryRootFieldParser userProvidedRootName defaultRootName =
|
||||||
case userProvidedRootName of
|
case userProvidedRootName of
|
||||||
Just _rootName -> traverse makeParsers userProvidedRootName
|
Just _rootName -> traverse makeParsers userProvidedRootName
|
||||||
@ -550,7 +556,7 @@ remoteSchemaObject ::
|
|||||||
MonadBuildRemoteSchema r m n =>
|
MonadBuildRemoteSchema r m n =>
|
||||||
RemoteSchemaIntrospection ->
|
RemoteSchemaIntrospection ->
|
||||||
G.ObjectTypeDefinition RemoteSchemaInputValueDefinition ->
|
G.ObjectTypeDefinition RemoteSchemaInputValueDefinition ->
|
||||||
m (Parser 'Output n (IR.ObjectSelectionSet Void RemoteSchemaVariable))
|
m (Parser 'Output n (IR.ObjectSelectionSet (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
remoteSchemaObject schemaDoc defn@(G.ObjectTypeDefinition description name interfaces _directives subFields) =
|
remoteSchemaObject schemaDoc defn@(G.ObjectTypeDefinition description name interfaces _directives subFields) =
|
||||||
P.memoizeOn 'remoteSchemaObject defn do
|
P.memoizeOn 'remoteSchemaObject defn do
|
||||||
subFieldParsers <- traverse (remoteFieldFromDefinition schemaDoc name) subFields
|
subFieldParsers <- traverse (remoteFieldFromDefinition schemaDoc name) subFields
|
||||||
@ -730,7 +736,7 @@ remoteSchemaInterface ::
|
|||||||
MonadBuildRemoteSchema r m n =>
|
MonadBuildRemoteSchema r m n =>
|
||||||
RemoteSchemaIntrospection ->
|
RemoteSchemaIntrospection ->
|
||||||
G.InterfaceTypeDefinition [G.Name] RemoteSchemaInputValueDefinition ->
|
G.InterfaceTypeDefinition [G.Name] RemoteSchemaInputValueDefinition ->
|
||||||
m (Parser 'Output n (IR.DeduplicatedSelectionSet Void RemoteSchemaVariable))
|
m (Parser 'Output n (IR.DeduplicatedSelectionSet (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
remoteSchemaInterface schemaDoc defn@(G.InterfaceTypeDefinition description name _directives fields possibleTypes) =
|
remoteSchemaInterface schemaDoc defn@(G.InterfaceTypeDefinition description name _directives fields possibleTypes) =
|
||||||
P.memoizeOn 'remoteSchemaObject defn do
|
P.memoizeOn 'remoteSchemaObject defn do
|
||||||
subFieldParsers <- traverse (remoteFieldFromDefinition schemaDoc name) fields
|
subFieldParsers <- traverse (remoteFieldFromDefinition schemaDoc name) fields
|
||||||
@ -770,7 +776,7 @@ remoteSchemaUnion ::
|
|||||||
MonadBuildRemoteSchema r m n =>
|
MonadBuildRemoteSchema r m n =>
|
||||||
RemoteSchemaIntrospection ->
|
RemoteSchemaIntrospection ->
|
||||||
G.UnionTypeDefinition ->
|
G.UnionTypeDefinition ->
|
||||||
m (Parser 'Output n (IR.DeduplicatedSelectionSet Void RemoteSchemaVariable))
|
m (Parser 'Output n (IR.DeduplicatedSelectionSet (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
remoteSchemaUnion schemaDoc defn@(G.UnionTypeDefinition description name _directives objectNames) =
|
remoteSchemaUnion schemaDoc defn@(G.UnionTypeDefinition description name _directives objectNames) =
|
||||||
P.memoizeOn 'remoteSchemaObject defn do
|
P.memoizeOn 'remoteSchemaObject defn do
|
||||||
objs <- traverse (getObjectParser schemaDoc getObject) objectNames
|
objs <- traverse (getObjectParser schemaDoc getObject) objectNames
|
||||||
@ -800,7 +806,7 @@ remoteFieldFromDefinition ::
|
|||||||
RemoteSchemaIntrospection ->
|
RemoteSchemaIntrospection ->
|
||||||
G.Name ->
|
G.Name ->
|
||||||
G.FieldDefinition RemoteSchemaInputValueDefinition ->
|
G.FieldDefinition RemoteSchemaInputValueDefinition ->
|
||||||
m (FieldParser n (IR.GraphQLField Void RemoteSchemaVariable))
|
m (FieldParser n (IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
remoteFieldFromDefinition schemaDoc parentTypeName (G.FieldDefinition description name argsDefinition gType _) =
|
remoteFieldFromDefinition schemaDoc parentTypeName (G.FieldDefinition description name argsDefinition gType _) =
|
||||||
convertType gType
|
convertType gType
|
||||||
where
|
where
|
||||||
@ -815,7 +821,7 @@ remoteFieldFromDefinition schemaDoc parentTypeName (G.FieldDefinition descriptio
|
|||||||
-- TODO add directives, deprecation
|
-- TODO add directives, deprecation
|
||||||
convertType ::
|
convertType ::
|
||||||
G.GType ->
|
G.GType ->
|
||||||
m (FieldParser n (IR.GraphQLField Void RemoteSchemaVariable))
|
m (FieldParser n (IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
convertType = \case
|
convertType = \case
|
||||||
G.TypeNamed (G.Nullability True) fieldTypeName ->
|
G.TypeNamed (G.Nullability True) fieldTypeName ->
|
||||||
P.nullableField <$> remoteFieldFromName schemaDoc parentTypeName name description fieldTypeName argsDefinition
|
P.nullableField <$> remoteFieldFromName schemaDoc parentTypeName name description fieldTypeName argsDefinition
|
||||||
@ -837,7 +843,7 @@ remoteFieldFromName ::
|
|||||||
Maybe G.Description ->
|
Maybe G.Description ->
|
||||||
G.Name ->
|
G.Name ->
|
||||||
G.ArgumentsDefinition RemoteSchemaInputValueDefinition ->
|
G.ArgumentsDefinition RemoteSchemaInputValueDefinition ->
|
||||||
m (FieldParser n (IR.GraphQLField Void RemoteSchemaVariable))
|
m (FieldParser n (IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
remoteFieldFromName sdoc parentTypeName fieldName description fieldTypeName argsDefns =
|
remoteFieldFromName sdoc parentTypeName fieldName description fieldTypeName argsDefns =
|
||||||
case lookupType sdoc fieldTypeName of
|
case lookupType sdoc fieldTypeName of
|
||||||
Nothing -> throw400 RemoteSchemaError $ "Could not find type with name " <>> fieldTypeName
|
Nothing -> throw400 RemoteSchemaError $ "Could not find type with name " <>> fieldTypeName
|
||||||
@ -855,7 +861,7 @@ remoteField ::
|
|||||||
Maybe G.Description ->
|
Maybe G.Description ->
|
||||||
G.ArgumentsDefinition RemoteSchemaInputValueDefinition ->
|
G.ArgumentsDefinition RemoteSchemaInputValueDefinition ->
|
||||||
G.TypeDefinition [G.Name] RemoteSchemaInputValueDefinition ->
|
G.TypeDefinition [G.Name] RemoteSchemaInputValueDefinition ->
|
||||||
m (FieldParser n (IR.GraphQLField Void RemoteSchemaVariable))
|
m (FieldParser n (IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
remoteField sdoc parentTypeName fieldName description argsDefn typeDefn = do
|
remoteField sdoc parentTypeName fieldName description argsDefn typeDefn = do
|
||||||
-- TODO add directives
|
-- TODO add directives
|
||||||
argsParser <- argumentsParser argsDefn sdoc
|
argsParser <- argumentsParser argsDefn sdoc
|
||||||
@ -884,8 +890,8 @@ remoteField sdoc parentTypeName fieldName description argsDefn typeDefn = do
|
|||||||
Maybe G.Name ->
|
Maybe G.Name ->
|
||||||
G.Name ->
|
G.Name ->
|
||||||
HashMap G.Name (G.Value RemoteSchemaVariable) ->
|
HashMap G.Name (G.Value RemoteSchemaVariable) ->
|
||||||
IR.SelectionSet Void RemoteSchemaVariable ->
|
IR.SelectionSet (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable ->
|
||||||
IR.GraphQLField Void RemoteSchemaVariable
|
IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable
|
||||||
mkField alias customizedFieldName args selSet =
|
mkField alias customizedFieldName args selSet =
|
||||||
-- If there's no alias then use customizedFieldName as the alias so the
|
-- If there's no alias then use customizedFieldName as the alias so the
|
||||||
-- correctly customized field name will be returned from the remote server.
|
-- correctly customized field name will be returned from the remote server.
|
||||||
@ -896,7 +902,7 @@ remoteField sdoc parentTypeName fieldName description argsDefn typeDefn = do
|
|||||||
G.Name ->
|
G.Name ->
|
||||||
InputFieldsParser n (Altered, HashMap G.Name (G.Value RemoteSchemaVariable)) ->
|
InputFieldsParser n (Altered, HashMap G.Name (G.Value RemoteSchemaVariable)) ->
|
||||||
Parser 'Both n () ->
|
Parser 'Both n () ->
|
||||||
FieldParser n (IR.GraphQLField Void RemoteSchemaVariable)
|
FieldParser n (IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)
|
||||||
mkFieldParserWithoutSelectionSet customizedFieldName argsParser outputParser =
|
mkFieldParserWithoutSelectionSet customizedFieldName argsParser outputParser =
|
||||||
P.rawSelection customizedFieldName description argsParser outputParser
|
P.rawSelection customizedFieldName description argsParser outputParser
|
||||||
<&> \(alias, _, (_, args)) -> mkField alias customizedFieldName args IR.SelectionSetNone
|
<&> \(alias, _, (_, args)) -> mkField alias customizedFieldName args IR.SelectionSetNone
|
||||||
@ -904,8 +910,8 @@ remoteField sdoc parentTypeName fieldName description argsDefn typeDefn = do
|
|||||||
mkFieldParserWithSelectionSet ::
|
mkFieldParserWithSelectionSet ::
|
||||||
G.Name ->
|
G.Name ->
|
||||||
InputFieldsParser n (Altered, HashMap G.Name (G.Value RemoteSchemaVariable)) ->
|
InputFieldsParser n (Altered, HashMap G.Name (G.Value RemoteSchemaVariable)) ->
|
||||||
Parser 'Output n (IR.SelectionSet Void RemoteSchemaVariable) ->
|
Parser 'Output n (IR.SelectionSet (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable) ->
|
||||||
FieldParser n (IR.GraphQLField Void RemoteSchemaVariable)
|
FieldParser n (IR.GraphQLField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)
|
||||||
mkFieldParserWithSelectionSet customizedFieldName argsParser outputParser =
|
mkFieldParserWithSelectionSet customizedFieldName argsParser outputParser =
|
||||||
P.rawSubselection customizedFieldName description argsParser outputParser
|
P.rawSubselection customizedFieldName description argsParser outputParser
|
||||||
<&> \(alias, _, (_, args), selSet) -> mkField alias customizedFieldName args selSet
|
<&> \(alias, _, (_, args), selSet) -> mkField alias customizedFieldName args selSet
|
||||||
@ -921,7 +927,7 @@ getObjectParser ::
|
|||||||
RemoteSchemaIntrospection ->
|
RemoteSchemaIntrospection ->
|
||||||
(G.Name -> m (G.ObjectTypeDefinition RemoteSchemaInputValueDefinition)) ->
|
(G.Name -> m (G.ObjectTypeDefinition RemoteSchemaInputValueDefinition)) ->
|
||||||
G.Name ->
|
G.Name ->
|
||||||
m (Parser 'Output n (G.Name, IR.ObjectSelectionSet Void RemoteSchemaVariable))
|
m (Parser 'Output n (G.Name, IR.ObjectSelectionSet (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
getObjectParser schemaDoc getObject objName = do
|
getObjectParser schemaDoc getObject objName = do
|
||||||
obj <- remoteSchemaObject schemaDoc =<< getObject objName
|
obj <- remoteSchemaObject schemaDoc =<< getObject objName
|
||||||
return $ (objName,) <$> obj
|
return $ (objName,) <$> obj
|
||||||
@ -931,8 +937,8 @@ customizeRemoteNamespace ::
|
|||||||
(MonadParse n) =>
|
(MonadParse n) =>
|
||||||
RemoteSchemaInfo ->
|
RemoteSchemaInfo ->
|
||||||
G.Name ->
|
G.Name ->
|
||||||
[P.FieldParser n (IR.RemoteSchemaRootField Void RemoteSchemaVariable)] ->
|
[P.FieldParser n (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)] ->
|
||||||
[P.FieldParser n (NamespacedField (IR.RemoteSchemaRootField Void RemoteSchemaVariable))]
|
[P.FieldParser n (NamespacedField (IR.RemoteSchemaRootField (IR.RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))]
|
||||||
customizeRemoteNamespace remoteSchemaInfo@RemoteSchemaInfo {..} rootTypeName fieldParsers =
|
customizeRemoteNamespace remoteSchemaInfo@RemoteSchemaInfo {..} rootTypeName fieldParsers =
|
||||||
customizeNamespace (_rscNamespaceFieldName rsCustomizer) fromParsedSelection mkNamespaceTypename fieldParsers
|
customizeNamespace (_rscNamespaceFieldName rsCustomizer) fromParsedSelection mkNamespaceTypename fieldParsers
|
||||||
where
|
where
|
||||||
|
@ -60,7 +60,7 @@ remoteRelationshipToSchemaField ::
|
|||||||
(MonadBuildSchemaBase r m n) =>
|
(MonadBuildSchemaBase r m n) =>
|
||||||
Map.HashMap FieldName lhsJoinField ->
|
Map.HashMap FieldName lhsJoinField ->
|
||||||
RemoteSchemaFieldInfo ->
|
RemoteSchemaFieldInfo ->
|
||||||
m (Maybe (FieldParser n IR.RemoteSchemaSelect))
|
m (Maybe (FieldParser n (IR.RemoteSchemaSelect (IR.RemoteRelationshipField UnpreparedValue))))
|
||||||
remoteRelationshipToSchemaField lhsFields RemoteSchemaFieldInfo {..} = runMaybeT do
|
remoteRelationshipToSchemaField lhsFields RemoteSchemaFieldInfo {..} = runMaybeT do
|
||||||
remoteRelationshipQueryCtx <- asks $ qcRemoteRelationshipContext . getter
|
remoteRelationshipQueryCtx <- asks $ qcRemoteRelationshipContext . getter
|
||||||
RemoteRelationshipQueryContext roleIntrospectionResultOriginal _ remoteSchemaCustomizer <-
|
RemoteRelationshipQueryContext roleIntrospectionResultOriginal _ remoteSchemaCustomizer <-
|
||||||
|
@ -242,7 +242,7 @@ runSessVarPred = filterSessionVariables . fromMaybe (\_ _ -> False) . unSessVarP
|
|||||||
filterVariablesFromQuery ::
|
filterVariablesFromQuery ::
|
||||||
[ RootField
|
[ RootField
|
||||||
(QueryDBRoot (RemoteRelationshipField UnpreparedValue) UnpreparedValue)
|
(QueryDBRoot (RemoteRelationshipField UnpreparedValue) UnpreparedValue)
|
||||||
(RemoteSchemaRootField Void RemoteSchemaVariable)
|
(RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)
|
||||||
(ActionQuery (RemoteRelationshipField UnpreparedValue))
|
(ActionQuery (RemoteRelationshipField UnpreparedValue))
|
||||||
d
|
d
|
||||||
] ->
|
] ->
|
||||||
@ -432,9 +432,9 @@ runGQ env logger reqId userInfo ipAddress reqHeaders queryType reqUnparsed = do
|
|||||||
finalResponse <-
|
finalResponse <-
|
||||||
RJ.processRemoteJoins reqId logger env httpManager reqHeaders userInfo resp remoteJoins reqUnparsed
|
RJ.processRemoteJoins reqId logger env httpManager reqHeaders userInfo resp remoteJoins reqUnparsed
|
||||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
||||||
E.ExecStepRemote rsi resultCustomizer gqlReq -> do
|
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||||
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema
|
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema
|
||||||
runRemoteGQ httpManager fieldName rsi resultCustomizer gqlReq
|
runRemoteGQ httpManager fieldName rsi resultCustomizer gqlReq remoteJoins
|
||||||
E.ExecStepAction aep _ remoteJoins -> do
|
E.ExecStepAction aep _ remoteJoins -> do
|
||||||
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindAction
|
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindAction
|
||||||
(time, resp) <- doQErr $ do
|
(time, resp) <- doQErr $ do
|
||||||
@ -462,9 +462,9 @@ runGQ env logger reqId userInfo ipAddress reqHeaders queryType reqUnparsed = do
|
|||||||
finalResponse <-
|
finalResponse <-
|
||||||
RJ.processRemoteJoins reqId logger env httpManager reqHeaders userInfo resp remoteJoins reqUnparsed
|
RJ.processRemoteJoins reqId logger env httpManager reqHeaders userInfo resp remoteJoins reqUnparsed
|
||||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse responseHeaders
|
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse responseHeaders
|
||||||
E.ExecStepRemote rsi resultCustomizer gqlReq -> do
|
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||||
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema
|
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema
|
||||||
runRemoteGQ httpManager fieldName rsi resultCustomizer gqlReq
|
runRemoteGQ httpManager fieldName rsi resultCustomizer gqlReq remoteJoins
|
||||||
E.ExecStepAction aep _ remoteJoins -> do
|
E.ExecStepAction aep _ remoteJoins -> do
|
||||||
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindAction
|
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindAction
|
||||||
(time, (resp, hdrs)) <- doQErr $ do
|
(time, (resp, hdrs)) <- doQErr $ do
|
||||||
@ -477,12 +477,25 @@ runGQ env logger reqId userInfo ipAddress reqHeaders queryType reqUnparsed = do
|
|||||||
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindIntrospection
|
logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindIntrospection
|
||||||
buildRaw json
|
buildRaw json
|
||||||
|
|
||||||
runRemoteGQ httpManager fieldName rsi resultCustomizer gqlReq = do
|
runRemoteGQ httpManager fieldName rsi resultCustomizer gqlReq remoteJoins = do
|
||||||
(telemTimeIO_DT, remoteResponseHeaders, resp) <-
|
(telemTimeIO_DT, remoteResponseHeaders, resp) <-
|
||||||
doQErr $ E.execRemoteGQ env httpManager userInfo reqHeaders (rsDef rsi) gqlReq
|
doQErr $ E.execRemoteGQ env httpManager userInfo reqHeaders (rsDef rsi) gqlReq
|
||||||
value <- extractFieldFromResponse fieldName resultCustomizer resp
|
value <- extractFieldFromResponse fieldName resultCustomizer resp
|
||||||
|
finalResponse <-
|
||||||
|
doQErr $
|
||||||
|
RJ.processRemoteJoins
|
||||||
|
reqId
|
||||||
|
logger
|
||||||
|
env
|
||||||
|
httpManager
|
||||||
|
reqHeaders
|
||||||
|
userInfo
|
||||||
|
-- TODO: avoid encode and decode here
|
||||||
|
(encJFromOrderedValue value)
|
||||||
|
remoteJoins
|
||||||
|
reqUnparsed
|
||||||
let filteredHeaders = filter ((== "Set-Cookie") . fst) remoteResponseHeaders
|
let filteredHeaders = filter ((== "Set-Cookie") . fst) remoteResponseHeaders
|
||||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Remote (encJFromOrderedValue value) filteredHeaders
|
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Remote finalResponse filteredHeaders
|
||||||
|
|
||||||
cacheAccess ::
|
cacheAccess ::
|
||||||
GQLReqParsed ->
|
GQLReqParsed ->
|
||||||
|
@ -500,9 +500,9 @@ onStart env enabledLogTypes serverEnv wsConn (StartMsg opId q) onMessageActions
|
|||||||
finalResponse <-
|
finalResponse <-
|
||||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||||
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse []
|
||||||
E.ExecStepRemote rsi resultCustomizer gqlReq -> do
|
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
||||||
runRemoteGQ fieldName userInfo reqHdrs rsi resultCustomizer gqlReq
|
runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins
|
||||||
E.ExecStepAction actionExecPlan _ remoteJoins -> do
|
E.ExecStepAction actionExecPlan _ remoteJoins -> do
|
||||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction
|
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction
|
||||||
(time, (resp, _)) <- doQErr $ do
|
(time, (resp, _)) <- doQErr $ do
|
||||||
@ -577,9 +577,9 @@ onStart env enabledLogTypes serverEnv wsConn (StartMsg opId q) onMessageActions
|
|||||||
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
RJ.processRemoteJoins requestId logger env httpMgr reqHdrs userInfo resp remoteJoins q
|
||||||
pure (time, (finalResponse, hdrs))
|
pure (time, (finalResponse, hdrs))
|
||||||
pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs
|
pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs
|
||||||
E.ExecStepRemote rsi resultCustomizer gqlReq -> do
|
E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do
|
||||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema
|
||||||
runRemoteGQ fieldName userInfo reqHdrs rsi resultCustomizer gqlReq
|
runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins
|
||||||
E.ExecStepRaw json -> do
|
E.ExecStepRaw json -> do
|
||||||
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection
|
logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection
|
||||||
buildRaw json
|
buildRaw json
|
||||||
@ -707,19 +707,35 @@ onStart env enabledLogTypes serverEnv wsConn (StartMsg opId q) onMessageActions
|
|||||||
Telem.recordTimingMetric Telem.RequestDimensions {..} Telem.RequestTimings {..}
|
Telem.recordTimingMetric Telem.RequestDimensions {..} Telem.RequestTimings {..}
|
||||||
|
|
||||||
runRemoteGQ ::
|
runRemoteGQ ::
|
||||||
|
RequestId ->
|
||||||
|
GQLReqUnparsed ->
|
||||||
RootFieldAlias ->
|
RootFieldAlias ->
|
||||||
UserInfo ->
|
UserInfo ->
|
||||||
[HTTP.Header] ->
|
[HTTP.Header] ->
|
||||||
RemoteSchemaInfo ->
|
RemoteSchemaInfo ->
|
||||||
ResultCustomizer ->
|
ResultCustomizer ->
|
||||||
GQLReqOutgoing ->
|
GQLReqOutgoing ->
|
||||||
|
Maybe RJ.RemoteJoins ->
|
||||||
ExceptT (Either GQExecError QErr) (ExceptT () m) AnnotatedResponsePart
|
ExceptT (Either GQExecError QErr) (ExceptT () m) AnnotatedResponsePart
|
||||||
runRemoteGQ fieldName userInfo reqHdrs rsi resultCustomizer gqlReq = do
|
runRemoteGQ requestId reqUnparsed fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins = do
|
||||||
(telemTimeIO_DT, _respHdrs, resp) <-
|
(telemTimeIO_DT, _respHdrs, resp) <-
|
||||||
doQErr $
|
doQErr $
|
||||||
E.execRemoteGQ env httpMgr userInfo reqHdrs (rsDef rsi) gqlReq
|
E.execRemoteGQ env httpMgr userInfo reqHdrs (rsDef rsi) gqlReq
|
||||||
value <- mapExceptT lift $ extractFieldFromResponse fieldName resultCustomizer resp
|
value <- mapExceptT lift $ extractFieldFromResponse fieldName resultCustomizer resp
|
||||||
return $ AnnotatedResponsePart telemTimeIO_DT Telem.Remote (encJFromOrderedValue value) []
|
finalResponse <-
|
||||||
|
doQErr $
|
||||||
|
RJ.processRemoteJoins
|
||||||
|
requestId
|
||||||
|
logger
|
||||||
|
env
|
||||||
|
httpMgr
|
||||||
|
reqHdrs
|
||||||
|
userInfo
|
||||||
|
-- TODO: avoid encode and decode here
|
||||||
|
(encJFromOrderedValue value)
|
||||||
|
remoteJoins
|
||||||
|
reqUnparsed
|
||||||
|
return $ AnnotatedResponsePart telemTimeIO_DT Telem.Remote finalResponse []
|
||||||
|
|
||||||
WSServerEnv
|
WSServerEnv
|
||||||
logger
|
logger
|
||||||
|
@ -160,10 +160,10 @@ data RemoteFieldArgument = RemoteFieldArgument
|
|||||||
}
|
}
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
data RemoteSchemaSelect = RemoteSchemaSelect
|
data RemoteSchemaSelect r = RemoteSchemaSelect
|
||||||
{ _rselArgs :: [RemoteFieldArgument],
|
{ _rselArgs :: [RemoteFieldArgument],
|
||||||
_rselResultCustomizer :: ResultCustomizer,
|
_rselResultCustomizer :: ResultCustomizer,
|
||||||
_rselSelection :: SelectionSet Void RemoteSchemaVariable,
|
_rselSelection :: SelectionSet r RemoteSchemaVariable,
|
||||||
_rselFieldCall :: NonEmpty FieldCall,
|
_rselFieldCall :: NonEmpty FieldCall,
|
||||||
_rselRemoteSchema :: RemoteSchemaInfo
|
_rselRemoteSchema :: RemoteSchemaInfo
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ newtype MutationDBRoot r v b = MDBR (MutationDB b r (v b))
|
|||||||
-- either a remote schema or a database's table. See RemoteSourceSelect for
|
-- either a remote schema or a database's table. See RemoteSourceSelect for
|
||||||
-- explanation on 'vf'.
|
-- explanation on 'vf'.
|
||||||
data RemoteRelationshipField vf
|
data RemoteRelationshipField vf
|
||||||
= RemoteSchemaField RemoteSchemaSelect
|
= RemoteSchemaField (RemoteSchemaSelect (RemoteRelationshipField vf))
|
||||||
| -- | AnyBackend is used here to capture a relationship to an arbitrary target
|
| -- | AnyBackend is used here to capture a relationship to an arbitrary target
|
||||||
RemoteSourceField (AB.AnyBackend (RemoteSourceSelect (RemoteRelationshipField vf) vf))
|
RemoteSourceField (AB.AnyBackend (RemoteSourceSelect (RemoteRelationshipField vf) vf))
|
||||||
|
|
||||||
@ -88,14 +88,14 @@ type MutationActionRoot v =
|
|||||||
type QueryRootField v =
|
type QueryRootField v =
|
||||||
RootField
|
RootField
|
||||||
(QueryDBRoot (RemoteRelationshipField v) v)
|
(QueryDBRoot (RemoteRelationshipField v) v)
|
||||||
(RemoteSchemaRootField Void RQL.RemoteSchemaVariable)
|
(RemoteSchemaRootField (RemoteRelationshipField v) RQL.RemoteSchemaVariable)
|
||||||
(QueryActionRoot v)
|
(QueryActionRoot v)
|
||||||
JO.Value
|
JO.Value
|
||||||
|
|
||||||
type MutationRootField v =
|
type MutationRootField v =
|
||||||
RootField
|
RootField
|
||||||
(MutationDBRoot (RemoteRelationshipField v) v)
|
(MutationDBRoot (RemoteRelationshipField v) v)
|
||||||
(RemoteSchemaRootField Void RQL.RemoteSchemaVariable)
|
(RemoteSchemaRootField (RemoteRelationshipField v) RQL.RemoteSchemaVariable)
|
||||||
(MutationActionRoot v)
|
(MutationActionRoot v)
|
||||||
JO.Value
|
JO.Value
|
||||||
|
|
||||||
|
@ -129,6 +129,7 @@ import Hasura.Base.Error
|
|||||||
import Hasura.GraphQL.Context (GQLContext, RoleContext)
|
import Hasura.GraphQL.Context (GQLContext, RoleContext)
|
||||||
import Hasura.GraphQL.Namespace
|
import Hasura.GraphQL.Namespace
|
||||||
import Hasura.GraphQL.Parser qualified as P
|
import Hasura.GraphQL.Parser qualified as P
|
||||||
|
import Hasura.GraphQL.Parser.Column (UnpreparedValue)
|
||||||
import Hasura.Incremental
|
import Hasura.Incremental
|
||||||
( Cacheable,
|
( Cacheable,
|
||||||
Dependency,
|
Dependency,
|
||||||
@ -139,6 +140,7 @@ import Hasura.Prelude
|
|||||||
import Hasura.RQL.DDL.Webhook.Transform
|
import Hasura.RQL.DDL.Webhook.Transform
|
||||||
import Hasura.RQL.IR.BoolExp
|
import Hasura.RQL.IR.BoolExp
|
||||||
import Hasura.RQL.IR.RemoteSchema
|
import Hasura.RQL.IR.RemoteSchema
|
||||||
|
import Hasura.RQL.IR.Root
|
||||||
import Hasura.RQL.Types.Action
|
import Hasura.RQL.Types.Action
|
||||||
import Hasura.RQL.Types.Allowlist
|
import Hasura.RQL.Types.Allowlist
|
||||||
import Hasura.RQL.Types.ApiLimit
|
import Hasura.RQL.Types.ApiLimit
|
||||||
@ -227,9 +229,9 @@ data IntrospectionResult = IntrospectionResult
|
|||||||
instance Cacheable IntrospectionResult
|
instance Cacheable IntrospectionResult
|
||||||
|
|
||||||
data ParsedIntrospectionG m = ParsedIntrospection
|
data ParsedIntrospectionG m = ParsedIntrospection
|
||||||
{ piQuery :: [P.FieldParser m (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))],
|
{ piQuery :: [P.FieldParser m (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))],
|
||||||
piMutation :: Maybe [P.FieldParser m (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))],
|
piMutation :: Maybe [P.FieldParser m (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))],
|
||||||
piSubscription :: Maybe [P.FieldParser m (NamespacedField (RemoteSchemaRootField Void RemoteSchemaVariable))]
|
piSubscription :: Maybe [P.FieldParser m (NamespacedField (RemoteSchemaRootField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParsedIntrospection = ParsedIntrospectionG (P.ParseT Identity)
|
type ParsedIntrospection = ParsedIntrospectionG (P.ParseT Identity)
|
||||||
|
@ -12,15 +12,20 @@ import Hasura.GraphQL.Execute.Inline
|
|||||||
import Hasura.GraphQL.Execute.Remote (resolveRemoteVariable, runVariableCache)
|
import Hasura.GraphQL.Execute.Remote (resolveRemoteVariable, runVariableCache)
|
||||||
import Hasura.GraphQL.Execute.Resolve
|
import Hasura.GraphQL.Execute.Resolve
|
||||||
import Hasura.GraphQL.Namespace
|
import Hasura.GraphQL.Namespace
|
||||||
|
import Hasura.GraphQL.Parser.Column (UnpreparedValue)
|
||||||
import Hasura.GraphQL.Parser.Internal.Parser qualified as P
|
import Hasura.GraphQL.Parser.Internal.Parser qualified as P
|
||||||
import Hasura.GraphQL.Parser.Schema
|
import Hasura.GraphQL.Parser.Schema
|
||||||
import Hasura.GraphQL.Parser.TestUtils
|
import Hasura.GraphQL.Parser.TestUtils
|
||||||
import Hasura.GraphQL.RemoteServer (identityCustomizer)
|
import Hasura.GraphQL.RemoteServer (identityCustomizer)
|
||||||
|
import Hasura.GraphQL.Schema.Common
|
||||||
import Hasura.GraphQL.Schema.Remote
|
import Hasura.GraphQL.Schema.Remote
|
||||||
import Hasura.Prelude
|
import Hasura.Prelude
|
||||||
import Hasura.RQL.IR.RemoteSchema
|
import Hasura.RQL.IR.RemoteSchema
|
||||||
|
import Hasura.RQL.IR.Root
|
||||||
import Hasura.RQL.Types.RemoteSchema
|
import Hasura.RQL.Types.RemoteSchema
|
||||||
import Hasura.RQL.Types.SchemaCache
|
import Hasura.RQL.Types.SchemaCache
|
||||||
|
import Hasura.RQL.Types.Source
|
||||||
|
import Hasura.RQL.Types.SourceCustomization
|
||||||
import Hasura.Session
|
import Hasura.Session
|
||||||
import Language.GraphQL.Draft.Parser qualified as G
|
import Language.GraphQL.Draft.Parser qualified as G
|
||||||
import Language.GraphQL.Draft.Syntax qualified as G
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
@ -98,16 +103,26 @@ mkTestVariableValues vars = runIdentity $
|
|||||||
|
|
||||||
buildQueryParsers ::
|
buildQueryParsers ::
|
||||||
RemoteSchemaIntrospection ->
|
RemoteSchemaIntrospection ->
|
||||||
IO (P.FieldParser TestMonad (G.Field G.NoFragments RemoteSchemaVariable))
|
IO (P.FieldParser TestMonad (GraphQLField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable))
|
||||||
buildQueryParsers introspection = do
|
buildQueryParsers introspection = do
|
||||||
let introResult = IntrospectionResult introspection $$(G.litName "Query") Nothing Nothing
|
let introResult = IntrospectionResult introspection $$(G.litName "Query") Nothing Nothing
|
||||||
|
remoteSchemaInfo = RemoteSchemaInfo (ValidatedRemoteSchemaDef N.nullURI [] False 60 Nothing) identityCustomizer
|
||||||
|
sourceContext =
|
||||||
|
-- without relationships to sources, this won't be evaluated
|
||||||
|
( adminRoleName :: RoleName,
|
||||||
|
mempty :: SourceCache,
|
||||||
|
undefined :: QueryContext,
|
||||||
|
mempty :: CustomizeRemoteFieldName,
|
||||||
|
mempty :: RemoteSchemaMap,
|
||||||
|
mempty :: MkTypename,
|
||||||
|
mempty :: MkRootFieldName
|
||||||
|
)
|
||||||
ParsedIntrospection query _ _ <-
|
ParsedIntrospection query _ _ <-
|
||||||
runError $
|
runError $
|
||||||
buildRemoteParser introResult $
|
buildRemoteParser introResult remoteSchemaInfo
|
||||||
RemoteSchemaInfo (ValidatedRemoteSchemaDef N.nullURI [] False 60 Nothing) identityCustomizer
|
|
||||||
pure $
|
pure $
|
||||||
head query <&> \case
|
head query <&> \case
|
||||||
NotNamespaced remoteFld -> convertGraphQLField $ _rfField remoteFld
|
NotNamespaced remoteFld -> _rfField remoteFld
|
||||||
Namespaced _ ->
|
Namespaced _ ->
|
||||||
-- Shouldn't happen if we're using identityCustomizer
|
-- Shouldn't happen if we're using identityCustomizer
|
||||||
-- TODO: add some tests for remote schema customization
|
-- TODO: add some tests for remote schema customization
|
||||||
@ -132,7 +147,7 @@ run ::
|
|||||||
Text ->
|
Text ->
|
||||||
-- | variables
|
-- | variables
|
||||||
LBS.ByteString ->
|
LBS.ByteString ->
|
||||||
IO (G.Field G.NoFragments RemoteSchemaVariable)
|
IO (GraphQLField (RemoteRelationshipField UnpreparedValue) RemoteSchemaVariable)
|
||||||
run schema query variables = do
|
run schema query variables = do
|
||||||
parser <- buildQueryParsers $ mkTestRemoteSchema schema
|
parser <- buildQueryParsers $ mkTestRemoteSchema schema
|
||||||
pure $
|
pure $
|
||||||
@ -192,7 +207,7 @@ query($a: A!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
let arg = head $ M.toList $ G._fArguments field
|
let arg = head $ M.toList $ _fArguments field
|
||||||
arg
|
arg
|
||||||
`shouldBe` ( $$(G.litName "a"),
|
`shouldBe` ( $$(G.litName "a"),
|
||||||
-- the parser did not create a new JSON variable, and forwarded the query variable unmodified
|
-- the parser did not create a new JSON variable, and forwarded the query variable unmodified
|
||||||
@ -246,7 +261,7 @@ query($a: A) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
let arg = head $ M.toList $ G._fArguments field
|
let arg = head $ M.toList $ _fArguments field
|
||||||
arg
|
arg
|
||||||
`shouldBe` ( $$(G.litName "a"),
|
`shouldBe` ( $$(G.litName "a"),
|
||||||
-- fieldOptional has peeled the variable; all we see is a JSON blob, and in doubt
|
-- fieldOptional has peeled the variable; all we see is a JSON blob, and in doubt
|
||||||
@ -300,7 +315,7 @@ query($a: A!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
let arg = head $ M.toList $ G._fArguments field
|
let arg = head $ M.toList $ _fArguments field
|
||||||
arg
|
arg
|
||||||
`shouldBe` ( $$(G.litName "a"),
|
`shouldBe` ( $$(G.litName "a"),
|
||||||
-- the preset has caused partial variable expansion, only up to where it's needed
|
-- the preset has caused partial variable expansion, only up to where it's needed
|
||||||
@ -333,7 +348,7 @@ testVariableSubstitutionCollision = it "ensures that remote variables are de-dup
|
|||||||
. traverse (resolveRemoteVariable dummyUserInfo)
|
. traverse (resolveRemoteVariable dummyUserInfo)
|
||||||
$ field
|
$ field
|
||||||
let variableNames =
|
let variableNames =
|
||||||
eField ^.. _Right . to G._fArguments . traverse . _VVariable . to vInfo . to getName . to G.unName
|
eField ^.. _Right . to _fArguments . traverse . _VVariable . to vInfo . to getName . to G.unName
|
||||||
variableNames `shouldBe` ["hasura_json_var_1", "hasura_json_var_2"]
|
variableNames `shouldBe` ["hasura_json_var_1", "hasura_json_var_2"]
|
||||||
where
|
where
|
||||||
-- A schema whose values are representable as collections of JSON values.
|
-- A schema whose values are representable as collections of JSON values.
|
||||||
|
Loading…
Reference in New Issue
Block a user