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.
This commit is contained in:
Phil Hughes 2019-05-09 10:27:07 +01:00 committed by Bob Van Landuyt
parent 2cc6e6ff26
commit 11f85ae8c3
8 changed files with 183 additions and 36 deletions

View file

@ -16,13 +16,8 @@ class GraphqlController < ApplicationController
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
def execute
variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: current_user
}
result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
result = multiplex? ? execute_multiplex : execute_query
render json: result
end
@ -38,6 +33,43 @@ class GraphqlController < ApplicationController
private
def execute_multiplex
GitlabSchema.multiplex(multiplex_queries, context: context)
end
def execute_query
variables = build_variables(params[:variables])
operation_name = params[:operationName]
GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
end
def query
params[:query]
end
def multiplex_queries
params[:_json].map do |single_query_info|
{
query: single_query_info[:query],
variables: build_variables(single_query_info[:variables]),
operation_name: single_query_info[:operationName]
}
end
end
def context
@context ||= { current_user: current_user }
end
def build_variables(variable_info)
Gitlab::Graphql::Variables.new(variable_info).to_h
end
def multiplex?
params[:_json].present?
end
def authorize_access_api!
access_denied!("API not accessible for user.") unless can?(current_user, :access_api)
end

View file

@ -7,7 +7,7 @@ class GitlabSchema < GraphQL::Schema
AUTHENTICATED_COMPLEXITY = 250
ADMIN_COMPLEXITY = 300
ANONYMOUS_MAX_DEPTH = 10
DEFAULT_MAX_DEPTH = 10
AUTHENTICATED_MAX_DEPTH = 15
use BatchLoader::GraphQL
@ -23,10 +23,21 @@ class GitlabSchema < GraphQL::Schema
default_max_page_size 100
max_complexity DEFAULT_MAX_COMPLEXITY
max_depth DEFAULT_MAX_DEPTH
mutation(Types::MutationType)
class << self
def multiplex(queries, **kwargs)
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
queries.each do |query|
query[:max_depth] = max_query_depth(kwargs[:context])
end
super(queries, **kwargs)
end
def execute(query_str = nil, **kwargs)
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
kwargs[:max_depth] ||= max_query_depth(kwargs[:context])
@ -54,7 +65,7 @@ class GitlabSchema < GraphQL::Schema
if current_user
AUTHENTICATED_MAX_DEPTH
else
ANONYMOUS_MAX_DEPTH
DEFAULT_MAX_DEPTH
end
end
end

View file

@ -0,0 +1,5 @@
---
title: Support multiplex GraphQL queries
merge_request: 28273
author:
type: added

View file

@ -48,6 +48,14 @@ A first iteration of a GraphQL API includes the following queries
1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID.
1. `group` : Only basic group information is currently supported.
### Multiplex queries
GitLab supports batching queries into a single request using
[apollo-link-batch-http](https://www.apollographql.com/docs/link/links/batch-http). More
info about multiplexed queries is also available for
[graphql-ruby](https://graphql-ruby.org/queries/multiplex.html) the
library GitLab uses on the backend.
## GraphiQL
The API can be explored by using the GraphiQL IDE, it is available on your

View file

@ -56,10 +56,10 @@ describe GitlabSchema do
described_class.execute('query', context: {})
end
it 'returns ANONYMOUS_MAX_DEPTH' do
it 'returns DEFAULT_MAX_DEPTH' do
expect(GraphQL::Schema)
.to receive(:execute)
.with('query', hash_including(max_depth: GitlabSchema::ANONYMOUS_MAX_DEPTH))
.with('query', hash_including(max_depth: GitlabSchema::DEFAULT_MAX_DEPTH))
described_class.execute('query', context: {})
end

View file

@ -3,41 +3,82 @@ require 'spec_helper'
describe 'GitlabSchema configurations' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
let(:query) { graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) }
let(:current_user) { create(:user) }
let(:project) { create(:project) }
describe '#max_complexity' do
context 'when complexity is too high' do
it 'shows an error' do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
shared_examples 'imposing query limits' do
describe '#max_complexity' do
context 'when complexity is too high' do
it 'shows an error' do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
post_graphql(query, current_user: nil)
subject
expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1')
expect(graphql_errors.flatten.first['message']).to include('which exceeds max complexity of 1')
end
end
end
describe '#max_depth' do
context 'when query depth is too high' do
it 'shows error' do
errors = { "message" => "Query has depth of 2, which exceeds max depth of 1" }
allow(GitlabSchema).to receive(:max_query_depth).and_return 1
subject
expect(graphql_errors.flatten).to include(errors)
end
end
context 'when query depth is within range' do
it 'has no error' do
allow(GitlabSchema).to receive(:max_query_depth).and_return 5
subject
expect(Array.wrap(graphql_errors).compact).to be_empty
end
end
end
end
describe '#max_depth' do
context 'when query depth is too high' do
it 'shows error' do
errors = [{ "message" => "Query has depth of 2, which exceeds max depth of 1" }]
allow(GitlabSchema).to receive(:max_query_depth).and_return 1
post_graphql(query)
expect(graphql_errors).to eq(errors)
end
context 'regular queries' do
subject do
query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description))
post_graphql(query)
end
context 'when query depth is within range' do
it 'has no error' do
allow(GitlabSchema).to receive(:max_query_depth).and_return 5
it_behaves_like 'imposing query limits'
end
post_graphql(query)
context 'multiplexed queries' do
subject do
queries = [
{ query: graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) },
{ query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }
]
expect(graphql_errors).to be_nil
post_multiplex(queries)
end
it_behaves_like 'imposing query limits' do
it "fails all queries when only one of the queries is too complex" do
# The `project` query above has a complexity of 5
allow(GitlabSchema).to receive(:max_query_complexity).and_return 4
subject
# Expect a response for each query, even though it will be empty
expect(json_response.size).to eq(2)
json_response.each do |single_query_response|
expect(single_query_response).not_to have_key('data')
end
# Expect errors for each query
expect(graphql_errors.size).to eq(2)
graphql_errors.each do |single_query_errors|
expect(single_query_errors.first['message']).to include('which exceeds max complexity of 4')
end
end
end
end

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Multiplexed queries' do
include GraphqlHelpers
it 'returns responses for multiple queries' do
queries = [
{ query: 'query($text: String) { echo(text: $text) }',
variables: { 'text' => 'Hello' } },
{ query: 'query($text: String) { echo(text: $text) }',
variables: { 'text' => 'World' } }
]
post_multiplex(queries)
first_response = json_response.first['data']['echo']
second_response = json_response.last['data']['echo']
expect(first_response).to eq('nil says: Hello')
expect(second_response).to eq('nil says: World')
end
it 'returns error and data combinations' do
queries = [
{ query: 'query($text: String) { broken query }' },
{ query: 'query working($text: String) { echo(text: $text) }',
variables: { 'text' => 'World' } }
]
post_multiplex(queries)
first_response = json_response.first['errors']
second_response = json_response.last['data']['echo']
expect(first_response).not_to be_empty
expect(second_response).to eq('nil says: World')
end
end

View file

@ -134,6 +134,10 @@ module GraphqlHelpers
end.join(", ")
end
def post_multiplex(queries, current_user: nil, headers: {})
post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {})
post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
end
@ -147,7 +151,14 @@ module GraphqlHelpers
end
def graphql_errors
json_response['errors']
case json_response
when Hash # regular query
json_response['errors']
when Array # multiplexed queries
json_response.map { |response| response['errors'] }
else
raise "Unkown GraphQL response type #{json_response.class}"
end
end
def graphql_mutation_response(mutation_name)