Commit 9403b1d9 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Allow querying a single MR within a project

This allows the user to get a single MR nested in a GraphQL project
query.

Since we need the full path and the iid anyway, this makes more sense
than having a root query that needs the full path as well.
parent d85929d7
module Resolvers module Resolvers
class MergeRequestResolver < BaseResolver class MergeRequestResolver < BaseResolver
prepend FullPathResolver
type Types::MergeRequestType, null: true
argument :iid, GraphQL::ID_TYPE, argument :iid, GraphQL::ID_TYPE,
required: true, required: true,
description: 'The IID of the merge request, e.g., "1"' description: 'The IID of the merge request, e.g., "1"'
def resolve(full_path:, iid:) type Types::MergeRequestType, null: true
project = model_by_full_path(Project, full_path)
alias_method :project, :object
def resolve(iid:)
return unless project.present? return unless project.present?
BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader| BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
......
...@@ -61,5 +61,12 @@ module Types ...@@ -61,5 +61,12 @@ module Types
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :merge_request,
Types::MergeRequestType,
null: true,
resolver: Resolvers::MergeRequestResolver do
authorize :read_merge_request
end
end end
end end
...@@ -9,13 +9,6 @@ module Types ...@@ -9,13 +9,6 @@ module Types
authorize :read_project authorize :read_project
end end
field :merge_request, Types::MergeRequestType,
null: true,
resolver: Resolvers::MergeRequestResolver,
description: "Find a merge request" do
authorize :read_merge_request
end
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end end
end end
---
title: Allow querying a single merge request within a project
merge_request: 19853
author:
type: changed
...@@ -29,9 +29,7 @@ curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://g ...@@ -29,9 +29,7 @@ curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://g
## Available queries ## Available queries
A first iteration of a GraphQL API includes only 2 queries: `project` and A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID.
`merge_request` and only returns scalar fields, or fields of the type `Project`
or `MergeRequest`.
## GraphiQL ## GraphiQL
......
...@@ -10,49 +10,36 @@ describe Resolvers::MergeRequestResolver do ...@@ -10,49 +10,36 @@ describe Resolvers::MergeRequestResolver do
set(:other_project) { create(:project, :repository) } set(:other_project) { create(:project, :repository) }
set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) } set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
let(:full_path) { project.full_path }
let(:iid_1) { merge_request_1.iid } let(:iid_1) { merge_request_1.iid }
let(:iid_2) { merge_request_2.iid } let(:iid_2) { merge_request_2.iid }
let(:other_full_path) { other_project.full_path }
let(:other_iid) { other_merge_request.iid } let(:other_iid) { other_merge_request.iid }
describe '#resolve' do describe '#resolve' do
it 'batch-resolves merge requests by target project full path and IID' do it 'batch-resolves merge requests by target project full path and IID' do
path = full_path # avoid database query
result = batch(max_queries: 2) do result = batch(max_queries: 2) do
[resolve_mr(path, iid_1), resolve_mr(path, iid_2)] [resolve_mr(project, iid_1), resolve_mr(project, iid_2)]
end end
expect(result).to contain_exactly(merge_request_1, merge_request_2) expect(result).to contain_exactly(merge_request_1, merge_request_2)
end end
it 'can batch-resolve merge requests from different projects' do it 'can batch-resolve merge requests from different projects' do
path = project.full_path # avoid database queries
other_path = other_full_path
result = batch(max_queries: 3) do result = batch(max_queries: 3) do
[resolve_mr(path, iid_1), resolve_mr(path, iid_2), resolve_mr(other_path, other_iid)] [resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)]
end end
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
end end
it 'resolves an unknown iid to nil' do it 'resolves an unknown iid to nil' do
result = batch { resolve_mr(full_path, -1) } result = batch { resolve_mr(project, -1) }
expect(result).to be_nil
end
it 'resolves a known iid for an unknown full_path to nil' do
result = batch { resolve_mr('unknown/project', iid_1) }
expect(result).to be_nil expect(result).to be_nil
end end
end end
def resolve_mr(full_path, iid) def resolve_mr(project, iid)
resolve(described_class, args: { full_path: full_path, iid: iid }) resolve(described_class, obj: project, args: { iid: iid })
end end
end end
...@@ -2,4 +2,13 @@ require 'spec_helper' ...@@ -2,4 +2,13 @@ require 'spec_helper'
describe GitlabSchema.types['Project'] do describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') } it { expect(described_class.graphql_name).to eq('Project') }
describe 'nested merge request' do
it { expect(described_class).to have_graphql_field(:merge_request) }
it 'authorizes the merge request' do
expect(described_class.fields['mergeRequest'])
.to require_graphql_authorizations(:read_merge_request)
end
end
end end
...@@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do ...@@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query') expect(described_class.graphql_name).to eq('Query')
end end
it { is_expected.to have_graphql_fields(:project, :merge_request, :echo) } it { is_expected.to have_graphql_fields(:project, :echo) }
describe 'project field' do describe 'project field' do
subject { described_class.fields['project'] } subject { described_class.fields['project'] }
...@@ -20,18 +20,4 @@ describe GitlabSchema.types['Query'] do ...@@ -20,18 +20,4 @@ describe GitlabSchema.types['Query'] do
is_expected.to require_graphql_authorizations(:read_project) is_expected.to require_graphql_authorizations(:read_project)
end end
end end
describe 'merge_request field' do
subject { described_class.fields['mergeRequest'] }
it 'finds MRs by project and IID' do
is_expected.to have_graphql_arguments(:full_path, :iid)
is_expected.to have_graphql_type(Types::MergeRequestType)
is_expected.to have_graphql_resolver(Resolvers::MergeRequestResolver)
end
it 'authorizes with read_merge_request' do
is_expected.to require_graphql_authorizations(:read_merge_request)
end
end
end end
require 'spec_helper'
describe 'getting merge request information' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:current_user) { create(:user) }
let(:query) do
attributes = {
'fullPath' => merge_request.project.full_path,
'iid' => merge_request.iid
}
graphql_query_for('mergeRequest', attributes)
end
context 'when the user has access to the merge request' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it 'returns the merge request' do
expect(graphql_data['mergeRequest']).not_to be_nil
end
# This is a field coming from the `MergeRequestPresenter`
it 'includes a web_url' do
expect(graphql_data['mergeRequest']['webUrl']).to be_present
end
it_behaves_like 'a working graphql query'
end
context 'when the user does not have access to the merge request' do
before do
post_graphql(query, current_user: current_user)
end
it 'returns an empty field' do
post_graphql(query, current_user: current_user)
expect(graphql_data['mergeRequest']).to be_nil
end
it_behaves_like 'a working graphql query'
end
end
...@@ -13,27 +13,76 @@ describe 'getting project information' do ...@@ -13,27 +13,76 @@ describe 'getting project information' do
context 'when the user has access to the project' do context 'when the user has access to the project' do
before do before do
project.add_developer(current_user) project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end end
it 'includes the project' do it 'includes the project' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']).not_to be_nil expect(graphql_data['project']).not_to be_nil
end end
it_behaves_like 'a working graphql query' it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end end
context 'when the user does not have access to the project' do context 'when requesting a nested merge request' do
let(:merge_request) { create(:merge_request, source_project: project) }
let(:merge_request_graphql_data) { graphql_data['project']['mergeRequest'] }
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('mergeRequest', iid: merge_request.iid)
)
end
it_behaves_like 'a working graphql query' do
before do before do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
end end
end
it 'contains merge request information' do
post_graphql(query, current_user: current_user)
expect(merge_request_graphql_data).not_to be_nil
end
# This is a field coming from the `MergeRequestPresenter`
it 'includes a web_url' do
post_graphql(query, current_user: current_user)
expect(merge_request_graphql_data['webUrl']).to be_present
end
context 'when the user does not have access to the merge request' do
let(:project) { create(:project, :public, :repository) }
it 'returns nil' do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
post_graphql(query)
expect(merge_request_graphql_data).to be_nil
end
end
end
end
context 'when the user does not have access to the project' do
it 'returns an empty field' do it 'returns an empty field' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
expect(graphql_data['project']).to be_nil expect(graphql_data['project']).to be_nil
end end
it_behaves_like 'a working graphql query' it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
end end
end end
...@@ -34,14 +34,20 @@ module GraphqlHelpers ...@@ -34,14 +34,20 @@ module GraphqlHelpers
end end
def graphql_query_for(name, attributes = {}, fields = nil) def graphql_query_for(name, attributes = {}, fields = nil)
<<~QUERY
{
#{query_graphql_field(name, attributes, fields)}
}
QUERY
end
def query_graphql_field(name, attributes = {}, fields = nil)
fields ||= all_graphql_fields_for(name.classify) fields ||= all_graphql_fields_for(name.classify)
attributes = attributes_to_graphql(attributes) attributes = attributes_to_graphql(attributes)
<<~QUERY <<~QUERY
{
#{name}(#{attributes}) { #{name}(#{attributes}) {
#{fields} #{fields}
} }
}
QUERY QUERY
end end
...@@ -50,12 +56,15 @@ module GraphqlHelpers ...@@ -50,12 +56,15 @@ module GraphqlHelpers
return "" unless type return "" unless type
type.fields.map do |name, field| type.fields.map do |name, field|
# We can't guess arguments, so skip fields that require them
next if field.arguments.any?
if scalar?(field) if scalar?(field)
name name
else else
"#{name} { #{all_graphql_fields_for(field_type(field))} }" "#{name} { #{all_graphql_fields_for(field_type(field))} }"
end end
end.join("\n") end.compact.join("\n")
end end
def attributes_to_graphql(attributes) def attributes_to_graphql(attributes)
......
...@@ -13,6 +13,12 @@ RSpec::Matchers.define :have_graphql_fields do |*expected| ...@@ -13,6 +13,12 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
end end
end end
RSpec::Matchers.define :have_graphql_field do |field_name|
match do |kls|
expect(kls.fields.keys).to include(GraphqlHelpers.fieldnamerize(field_name))
end
end
RSpec::Matchers.define :have_graphql_arguments do |*expected| RSpec::Matchers.define :have_graphql_arguments do |*expected|
include GraphqlHelpers include GraphqlHelpers
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment