Skip to content

Commit 11f85ae

Browse files
iamphillReprazent
authored andcommitted
Enables GraphQL batch requests
Enabling GraphQL batch requests allows for multiple queries to be sent in 1 request reducing the amount of requests we send to the server. Responses come come back in the same order as the queries were provided.
1 parent 2cc6e6f commit 11f85ae

File tree

8 files changed

+181
-34
lines changed

8 files changed

+181
-34
lines changed

app/controllers/graphql_controller.rb

+39-7
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,8 @@ class GraphqlController < ApplicationController
1616
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
1717

1818
def execute
19-
variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
20-
query = params[:query]
21-
operation_name = params[:operationName]
22-
context = {
23-
current_user: current_user
24-
}
25-
result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
19+
result = multiplex? ? execute_multiplex : execute_query
20+
2621
render json: result
2722
end
2823

@@ -38,6 +33,43 @@ def execute
3833

3934
private
4035

36+
def execute_multiplex
37+
GitlabSchema.multiplex(multiplex_queries, context: context)
38+
end
39+
40+
def execute_query
41+
variables = build_variables(params[:variables])
42+
operation_name = params[:operationName]
43+
44+
GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
45+
end
46+
47+
def query
48+
params[:query]
49+
end
50+
51+
def multiplex_queries
52+
params[:_json].map do |single_query_info|
53+
{
54+
query: single_query_info[:query],
55+
variables: build_variables(single_query_info[:variables]),
56+
operation_name: single_query_info[:operationName]
57+
}
58+
end
59+
end
60+
61+
def context
62+
@context ||= { current_user: current_user }
63+
end
64+
65+
def build_variables(variable_info)
66+
Gitlab::Graphql::Variables.new(variable_info).to_h
67+
end
68+
69+
def multiplex?
70+
params[:_json].present?
71+
end
72+
4173
def authorize_access_api!
4274
access_denied!("API not accessible for user.") unless can?(current_user, :access_api)
4375
end

app/graphql/gitlab_schema.rb

+13-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class GitlabSchema < GraphQL::Schema
77
AUTHENTICATED_COMPLEXITY = 250
88
ADMIN_COMPLEXITY = 300
99

10-
ANONYMOUS_MAX_DEPTH = 10
10+
DEFAULT_MAX_DEPTH = 10
1111
AUTHENTICATED_MAX_DEPTH = 15
1212

1313
use BatchLoader::GraphQL
@@ -23,10 +23,21 @@ class GitlabSchema < GraphQL::Schema
2323
default_max_page_size 100
2424

2525
max_complexity DEFAULT_MAX_COMPLEXITY
26+
max_depth DEFAULT_MAX_DEPTH
2627

2728
mutation(Types::MutationType)
2829

2930
class << self
31+
def multiplex(queries, **kwargs)
32+
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
33+
34+
queries.each do |query|
35+
query[:max_depth] = max_query_depth(kwargs[:context])
36+
end
37+
38+
super(queries, **kwargs)
39+
end
40+
3041
def execute(query_str = nil, **kwargs)
3142
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
3243
kwargs[:max_depth] ||= max_query_depth(kwargs[:context])
@@ -54,7 +65,7 @@ def max_query_depth(ctx)
5465
if current_user
5566
AUTHENTICATED_MAX_DEPTH
5667
else
57-
ANONYMOUS_MAX_DEPTH
68+
DEFAULT_MAX_DEPTH
5869
end
5970
end
6071
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: Support multiplex GraphQL queries
3+
merge_request: 28273
4+
author:
5+
type: added

doc/api/graphql/index.md

+8
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ A first iteration of a GraphQL API includes the following queries
4848
1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID.
4949
1. `group` : Only basic group information is currently supported.
5050

51+
### Multiplex queries
52+
53+
GitLab supports batching queries into a single request using
54+
[apollo-link-batch-http](https://www.apollographql.com/docs/link/links/batch-http). More
55+
info about multiplexed queries is also available for
56+
[graphql-ruby](https://graphql-ruby.org/queries/multiplex.html) the
57+
library GitLab uses on the backend.
58+
5159
## GraphiQL
5260

5361
The API can be explored by using the GraphiQL IDE, it is available on your

spec/graphql/gitlab_schema_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@
5656
described_class.execute('query', context: {})
5757
end
5858

59-
it 'returns ANONYMOUS_MAX_DEPTH' do
59+
it 'returns DEFAULT_MAX_DEPTH' do
6060
expect(GraphQL::Schema)
6161
.to receive(:execute)
62-
.with('query', hash_including(max_depth: GitlabSchema::ANONYMOUS_MAX_DEPTH))
62+
.with('query', hash_including(max_depth: GitlabSchema::DEFAULT_MAX_DEPTH))
6363

6464
described_class.execute('query', context: {})
6565
end

spec/requests/api/graphql/gitlab_schema_spec.rb

+63-22
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,82 @@
33
describe 'GitlabSchema configurations' do
44
include GraphqlHelpers
55

6-
let(:project) { create(:project, :repository) }
7-
let(:query) { graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) }
8-
let(:current_user) { create(:user) }
6+
let(:project) { create(:project) }
97

10-
describe '#max_complexity' do
11-
context 'when complexity is too high' do
12-
it 'shows an error' do
13-
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
8+
shared_examples 'imposing query limits' do
9+
describe '#max_complexity' do
10+
context 'when complexity is too high' do
11+
it 'shows an error' do
12+
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
1413

15-
post_graphql(query, current_user: nil)
14+
subject
1615

17-
expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1')
16+
expect(graphql_errors.flatten.first['message']).to include('which exceeds max complexity of 1')
17+
end
1818
end
1919
end
20-
end
2120

22-
describe '#max_depth' do
23-
context 'when query depth is too high' do
24-
it 'shows error' do
25-
errors = [{ "message" => "Query has depth of 2, which exceeds max depth of 1" }]
26-
allow(GitlabSchema).to receive(:max_query_depth).and_return 1
21+
describe '#max_depth' do
22+
context 'when query depth is too high' do
23+
it 'shows error' do
24+
errors = { "message" => "Query has depth of 2, which exceeds max depth of 1" }
25+
allow(GitlabSchema).to receive(:max_query_depth).and_return 1
2726

28-
post_graphql(query)
27+
subject
2928

30-
expect(graphql_errors).to eq(errors)
29+
expect(graphql_errors.flatten).to include(errors)
30+
end
3131
end
32+
33+
context 'when query depth is within range' do
34+
it 'has no error' do
35+
allow(GitlabSchema).to receive(:max_query_depth).and_return 5
36+
37+
subject
38+
39+
expect(Array.wrap(graphql_errors).compact).to be_empty
40+
end
41+
end
42+
end
43+
end
44+
45+
context 'regular queries' do
46+
subject do
47+
query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description))
48+
post_graphql(query)
3249
end
3350

34-
context 'when query depth is within range' do
35-
it 'has no error' do
36-
allow(GitlabSchema).to receive(:max_query_depth).and_return 5
51+
it_behaves_like 'imposing query limits'
52+
end
53+
54+
context 'multiplexed queries' do
55+
subject do
56+
queries = [
57+
{ query: graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) },
58+
{ query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }
59+
]
60+
61+
post_multiplex(queries)
62+
end
63+
64+
it_behaves_like 'imposing query limits' do
65+
it "fails all queries when only one of the queries is too complex" do
66+
# The `project` query above has a complexity of 5
67+
allow(GitlabSchema).to receive(:max_query_complexity).and_return 4
68+
69+
subject
3770

38-
post_graphql(query)
71+
# Expect a response for each query, even though it will be empty
72+
expect(json_response.size).to eq(2)
73+
json_response.each do |single_query_response|
74+
expect(single_query_response).not_to have_key('data')
75+
end
3976

40-
expect(graphql_errors).to be_nil
77+
# Expect errors for each query
78+
expect(graphql_errors.size).to eq(2)
79+
graphql_errors.each do |single_query_errors|
80+
expect(single_query_errors.first['message']).to include('which exceeds max complexity of 4')
81+
end
4182
end
4283
end
4384
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
require 'spec_helper'
3+
4+
describe 'Multiplexed queries' do
5+
include GraphqlHelpers
6+
7+
it 'returns responses for multiple queries' do
8+
queries = [
9+
{ query: 'query($text: String) { echo(text: $text) }',
10+
variables: { 'text' => 'Hello' } },
11+
{ query: 'query($text: String) { echo(text: $text) }',
12+
variables: { 'text' => 'World' } }
13+
]
14+
15+
post_multiplex(queries)
16+
17+
first_response = json_response.first['data']['echo']
18+
second_response = json_response.last['data']['echo']
19+
20+
expect(first_response).to eq('nil says: Hello')
21+
expect(second_response).to eq('nil says: World')
22+
end
23+
24+
it 'returns error and data combinations' do
25+
queries = [
26+
{ query: 'query($text: String) { broken query }' },
27+
{ query: 'query working($text: String) { echo(text: $text) }',
28+
variables: { 'text' => 'World' } }
29+
]
30+
31+
post_multiplex(queries)
32+
33+
first_response = json_response.first['errors']
34+
second_response = json_response.last['data']['echo']
35+
36+
expect(first_response).not_to be_empty
37+
expect(second_response).to eq('nil says: World')
38+
end
39+
end

spec/support/helpers/graphql_helpers.rb

+12-1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ def attributes_to_graphql(attributes)
134134
end.join(", ")
135135
end
136136

137+
def post_multiplex(queries, current_user: nil, headers: {})
138+
post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
139+
end
140+
137141
def post_graphql(query, current_user: nil, variables: nil, headers: {})
138142
post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
139143
end
@@ -147,7 +151,14 @@ def graphql_data
147151
end
148152

149153
def graphql_errors
150-
json_response['errors']
154+
case json_response
155+
when Hash # regular query
156+
json_response['errors']
157+
when Array # multiplexed queries
158+
json_response.map { |response| response['errors'] }
159+
else
160+
raise "Unkown GraphQL response type #{json_response.class}"
161+
end
151162
end
152163

153164
def graphql_mutation_response(mutation_name)

0 commit comments

Comments
 (0)