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:
parent
2cc6e6ff26
commit
11f85ae8c3
8 changed files with 183 additions and 36 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
5
changelogs/unreleased/bvl-graphql-multiplex.yml
Normal file
5
changelogs/unreleased/bvl-graphql-multiplex.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support multiplex GraphQL queries
|
||||
merge_request: 28273
|
||||
author:
|
||||
type: added
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
39
spec/requests/api/graphql/multiplexed_queries_spec.rb
Normal file
39
spec/requests/api/graphql/multiplexed_queries_spec.rb
Normal 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
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue