Skip to content
Snippets Groups Projects
Commit 11f85ae8 authored by Phil Hughes's avatar Phil Hughes Committed by Bob Van Landuyt
Browse files

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.
parent 2cc6e6ff
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -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
 
Loading
Loading
@@ -38,6 +33,43 @@ def execute
 
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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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])
Loading
Loading
@@ -54,7 +65,7 @@ def max_query_depth(ctx)
if current_user
AUTHENTICATED_MAX_DEPTH
else
ANONYMOUS_MAX_DEPTH
DEFAULT_MAX_DEPTH
end
end
end
Loading
Loading
---
title: Support multiplex GraphQL queries
merge_request: 28273
author:
type: added
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -56,10 +56,10 @@
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
Loading
Loading
Loading
Loading
@@ -3,41 +3,82 @@
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
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
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)
subject
 
expect(graphql_errors).to eq(errors)
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
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
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" } }
]
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
 
post_graphql(query)
# 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(graphql_errors).to be_nil
# 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
Loading
Loading
# 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
Loading
Loading
@@ -134,6 +134,10 @@ def attributes_to_graphql(attributes)
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
Loading
Loading
@@ -147,7 +151,14 @@ def graphql_data
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)
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment