diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 8af0db644dd8e305a797f549b441cbfe512f0818..545d82b8f369b9730f1ebd735d71baba274f80e4 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -79,8 +79,14 @@ class QueryType < ::Types::BaseObject field :issue, Types::IssueType, null: true, - description: 'Find an Issue.' do - argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue.' + description: 'Find an issue.' do + argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the issue.' + end + + field :merge_request, Types::MergeRequestType, + null: true, + description: 'Find a merge request.' do + argument :id, ::Types::GlobalIDType[::MergeRequest], required: true, description: 'The global ID of the merge request.' end field :instance_statistics_measurements, @@ -119,6 +125,13 @@ def issue(id:) GitlabSchema.find_by_gid(id) end + def merge_request(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::MergeRequest].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + def milestone(id:) # TODO: remove this line when the compatibility layer is removed # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 diff --git a/changelogs/unreleased/graphql-expose-merge-request-at-root.yml b/changelogs/unreleased/graphql-expose-merge-request-at-root.yml new file mode 100644 index 0000000000000000000000000000000000000000..8bc84565c0106c0c2adf601b3cf4ba9b0d0a6660 --- /dev/null +++ b/changelogs/unreleased/graphql-expose-merge-request-at-root.yml @@ -0,0 +1,5 @@ +--- +title: Allow merge request search via GraphQL +merge_request: 60190 +author: Lee Tickett @leetickett +type: added diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 765478c73cd3d944cc0cc58cb9882811a5207e1b..8d80b69a3bbc326838c831a322f8f554ff849ccd 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -165,7 +165,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ### `Query.issue` -Find an Issue. +Find an issue. Returns [`Issue`](#issue). @@ -173,7 +173,7 @@ Returns [`Issue`](#issue). | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="queryissueid"></a>`id` | [`IssueID!`](#issueid) | The global ID of the Issue. | +| <a id="queryissueid"></a>`id` | [`IssueID!`](#issueid) | The global ID of the issue. | ### `Query.iteration` @@ -197,6 +197,18 @@ This field returns a [connection](#connections). It accepts the four standard [pagination arguments](#connection-pagination-arguments): `before: String`, `after: String`, `first: Int`, `last: Int`. +### `Query.mergeRequest` + +Find a merge request. + +Returns [`MergeRequest`](#mergerequest). + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="querymergerequestid"></a>`id` | [`MergeRequestID!`](#mergerequestid) | The global ID of the merge request. | + ### `Query.metadata` Metadata about GitLab. diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index d3dcdd260b0ed7fa26ca1c1a86635feb3726aa89..a877e19c06968284511673eb95950100dccad4b4 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -21,6 +21,7 @@ user users issue + merge_request usage_trends_measurements runner_platforms ] @@ -60,11 +61,21 @@ describe 'issue field' do subject { described_class.fields['issue'] } - it 'returns issue' do + it "finds an issue by it's gid" do + is_expected.to have_graphql_arguments(:id) is_expected.to have_graphql_type(Types::IssueType) end end + describe 'merge_request field' do + subject { described_class.fields['mergeRequest'] } + + it "finds a merge_request by it's gid" do + is_expected.to have_graphql_arguments(:id) + is_expected.to have_graphql_type(Types::MergeRequestType) + end + end + describe 'usage_trends_measurements field' do subject { described_class.fields['usageTrendsMeasurements'] } diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb index e8b8caf6c2da632f43d371ef386fd47b7bbf7533..42ca3348384abb3c9750e2ec3bd60b2826946511 100644 --- a/spec/requests/api/graphql/issue/issue_spec.rb +++ b/spec/requests/api/graphql/issue/issue_spec.rb @@ -76,7 +76,7 @@ def query(fields) post_graphql(query, current_user: current_user) end - it "returns the Issue and field #{params['field']}" do + it "returns the issue and field #{params['field']}" do expect(issue_data.keys).to eq([field]) end end @@ -86,7 +86,7 @@ def query(fields) context 'when selecting multiple fields' do let(:issue_fields) { ['title', 'description', 'updatedBy { username }'] } - it 'returns the Issue with the specified fields' do + it 'returns the issue with the specified fields' do post_graphql(query, current_user: current_user) expect(issue_data.keys).to eq %w[title description updatedBy] @@ -115,7 +115,7 @@ def query(fields) end end - context 'when passed a non-Issue gid' do + context 'when passed a non-issue gid' do let(:mr) { create(:merge_request) } it 'returns an error' do diff --git a/spec/requests/api/graphql/merge_request/merge_request_spec.rb b/spec/requests/api/graphql/merge_request/merge_request_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..75dd01a07630a7566d75028cc3f617af37254f35 --- /dev/null +++ b/spec/requests/api/graphql/merge_request/merge_request_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.merge_request(id)' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :empty_repo) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:current_user) { create(:user) } + let_it_be(:merge_request_params) { { 'id' => merge_request.to_global_id.to_s } } + + let(:merge_request_data) { graphql_data['mergeRequest'] } + let(:merge_request_fields) { all_graphql_fields_for('MergeRequest'.classify) } + + let(:query) do + graphql_query_for('mergeRequest', merge_request_params, merge_request_fields) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + it_behaves_like 'a noteable graphql type we can query' do + let(:noteable) { merge_request } + let(:project) { merge_request.project } + let(:path_to_noteable) { [:merge_request] } + + before do + project.add_reporter(current_user) + end + + def query(fields) + graphql_query_for('mergeRequest', merge_request_params, fields) + end + end + + context 'when the user does not have access to the merge request' do + it 'returns nil' do + post_graphql(query) + + expect(merge_request_data).to be nil + end + end + + context 'when the user does have access' do + before do + project.add_reporter(current_user) + end + + it 'returns the merge request' do + post_graphql(query, current_user: current_user) + + expect(merge_request_data).to include( + 'title' => merge_request.title, + 'description' => merge_request.description + ) + end + + context 'when selecting any single field' do + where(:field) do + scalar_fields_of('MergeRequest').map { |name| [name] } + end + + with_them do + it_behaves_like 'a working graphql query' do + let(:merge_request_fields) do + field + end + + before do + post_graphql(query, current_user: current_user) + end + + it "returns the merge request and field #{params['field']}" do + expect(merge_request_data.keys).to eq([field]) + end + end + end + end + + context 'when selecting multiple fields' do + let(:merge_request_fields) { ['title', 'description', 'author { username }'] } + + it 'returns the merge request with the specified fields' do + post_graphql(query, current_user: current_user) + + expect(merge_request_data.keys).to eq %w[title description author] + expect(merge_request_data['title']).to eq(merge_request.title) + expect(merge_request_data['description']).to eq(merge_request.description) + expect(merge_request_data['author']['username']).to eq(merge_request.author.username) + end + end + + context 'when passed a non-merge request gid' do + let(:issue) { create(:issue) } + + it 'returns an error' do + gid = issue.to_global_id.to_s + merge_request_params['id'] = gid + + post_graphql(query, current_user: current_user) + + expect(graphql_errors).not_to be nil + expect(graphql_errors.first['message']).to eq("\"#{gid}\" does not represent an instance of MergeRequest") + end + end + end +end