Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support batched queries (fix #1812) #3490

Merged
merged 5 commits into from
Dec 20, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/graphql/manual/api-reference/graphql-api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,60 @@ The following types of requests can be made using the GraphQL API:
- :doc:`Query / Subscription <query>`
- :doc:`Mutation <mutation>`

Batching operations
-------------------

The GraphQL API provides support for batched operations (which can be a combination of queries and mutations).
The endpoint will accept an array of operations in place of a single operation, and return an array of corresponding
responses.

**Example:** using a client which supports batching (such as Apollo Client), we can send two
query operations in one request:

.. graphiql::
:view_only:
:query:
query first {
author(where: {id: {_eq: 1}}) {
id
name
}
}
query second {
author(where: {id: {_eq: 2}}) {
id
name
}
}
:response:
[
{
"data": {
"author": [
{
"id": 1,
"name": "Justin"
}
]
}
},
{
"data": {
"author": [
{
"id": 2,
"name": "Beltran"
}
]
}
}
]


.. toctree::
:maxdepth: 1
:hidden:

Query / Subscription <query>
Mutation <mutation>
Batching operations <batching>
24 changes: 23 additions & 1 deletion server/src-lib/Hasura/GraphQL/Transport/HTTP.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Hasura.GraphQL.Transport.HTTP
( runGQ
, runGQBatched
) where

import qualified Network.HTTP.Types as N
Expand All @@ -24,7 +25,7 @@ runGQ
=> RequestId
-> UserInfo
-> [N.Header]
-> GQLReqUnparsed
-> GQLReq GQLQueryText
-> m (HttpResponse EncJSON)
runGQ reqId userInfo reqHdrs req = do
E.ExecutionCtx _ sqlGenCtx pgExecCtx planCache sc scVer _ enableAL <- ask
Expand All @@ -36,6 +37,27 @@ runGQ reqId userInfo reqHdrs req = do
E.GExPRemote rsi opDef ->
E.execRemoteGQ reqId userInfo reqHdrs req rsi opDef

runGQBatched
:: ( MonadIO m
, MonadError QErr m
, MonadReader E.ExecutionCtx m
)
=> RequestId
-> UserInfo
-> [N.Header]
-> GQLBatchedReqs GQLQueryText
-> m (HttpResponse EncJSON)
runGQBatched reqId userInfo reqHdrs reqs =
case reqs of
GQLSingleRequest req ->
runGQ reqId userInfo reqHdrs req
GQLBatchedReqs batch -> do
-- It's unclear what we should do if we receive multiple
-- responses with distinct headers, so just do the simplest thing
-- in this case, and don't forward any.
let removeHeaders = flip HttpResponse Nothing . encJFromList . map _hrBody
removeHeaders <$> traverse (runGQ reqId userInfo reqHdrs) batch

runHasuraGQ
:: ( MonadIO m
, MonadError QErr m
Expand Down
19 changes: 19 additions & 0 deletions server/src-lib/Hasura/GraphQL/Transport/HTTP/Protocol.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Hasura.GraphQL.Transport.HTTP.Protocol
( GQLReq(..)
, GQLBatchedReqs(..)
, GQLReqUnparsed
, GQLReqParsed
, toParsed
Expand Down Expand Up @@ -64,6 +65,24 @@ $(J.deriveJSON (J.aesonDrop 3 J.camelCase){J.omitNothingFields=True}

instance (Hashable a) => Hashable (GQLReq a)

-- | Batched queries are sent as a JSON array of
-- 'GQLReq' records. This newtype exists to support
-- the unusual JSON encoding.
--
-- See <https://github.com/hasura/graphql-engine/issues/1812>.
data GQLBatchedReqs a
= GQLSingleRequest (GQLReq a)
| GQLBatchedReqs [GQLReq a]
deriving (Show, Eq, Generic)

instance J.ToJSON a => J.ToJSON (GQLBatchedReqs a) where
toJSON (GQLSingleRequest q) = J.toJSON q
toJSON (GQLBatchedReqs qs) = J.toJSON qs

instance J.FromJSON a => J.FromJSON (GQLBatchedReqs a) where
parseJSON arr@J.Array{} = GQLBatchedReqs <$> J.parseJSON arr
parseJSON other = GQLSingleRequest <$> J.parseJSON other

newtype GQLQueryText
= GQLQueryText
{ _unGQLQueryText :: Text
Expand Down
6 changes: 3 additions & 3 deletions server/src-lib/Hasura/Server/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ v1QueryHandler query = do
instanceId <- scInstanceId . hcServerCtx <$> ask
runQuery pgExecCtx instanceId userInfo schemaCache httpMgr sqlGenCtx (SystemDefined False) query

v1Alpha1GQHandler :: (MonadIO m) => GH.GQLReqUnparsed -> Handler m (HttpResponse EncJSON)
v1Alpha1GQHandler :: (MonadIO m) => GH.GQLBatchedReqs GH.GQLQueryText -> Handler m (HttpResponse EncJSON)
v1Alpha1GQHandler query = do
userInfo <- asks hcUser
reqHeaders <- asks hcReqHeaders
Expand All @@ -312,11 +312,11 @@ v1Alpha1GQHandler query = do
requestId <- asks hcRequestId
let execCtx = E.ExecutionCtx logger sqlGenCtx pgExecCtx planCache
sc scVer manager enableAL
flip runReaderT execCtx $ GH.runGQ requestId userInfo reqHeaders query
flip runReaderT execCtx $ GH.runGQBatched requestId userInfo reqHeaders query

v1GQHandler
:: (MonadIO m)
=> GH.GQLReqUnparsed
=> GH.GQLBatchedReqs GH.GQLQueryText
-> Handler m (HttpResponse EncJSON)
v1GQHandler = v1Alpha1GQHandler

Expand Down
2 changes: 1 addition & 1 deletion server/src-lib/Hasura/Server/Context.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ data HttpResponse a
= HttpResponse
{ _hrBody :: !a
, _hrHeaders :: !(Maybe Headers)
}
} deriving (Functor, Foldable, Traversable)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
description: GraphQL query to test batching in the style of Apollo
url: /v1/graphql
status: 200
response:
- data:
user:
- id: '1'
- id: '2'
- data:
author:
- id: 1
- id: 2
query:
- query: |
query {
user {
id
}
}
- query: |
query {
author {
id
}
}
4 changes: 4 additions & 0 deletions server/tests-py/test_graphql_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def test_select_query_invalid_escape_sequence(self, hge_ctx, transport):
transport = 'http'
check_query_f(hge_ctx, self.dir() + "/select_query_invalid_escape_sequence.yaml", transport)

def test_select_query_batching(self, hge_ctx, transport):
transport = 'http'
check_query_f(hge_ctx, self.dir() + "/select_query_batching.yaml", transport)

@classmethod
def dir(cls):
return 'queries/graphql_query/basic'
Expand Down
3 changes: 2 additions & 1 deletion server/tests-py/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,8 @@ def go(result_node, selset):
# Copy-pasta from: https://stackoverflow.com/q/12734517/176841
def stringify_keys(d):
"""Convert a dict's keys to strings if they are not."""
for key in d.keys():
if isinstance(d, dict):
for key in d.keys():
# check inner dict
if isinstance(d[key], dict):
value = stringify_keys(d[key])
Expand Down