Commit 00a5db6a authored by charlie ablett's avatar charlie ablett

Expose blocked_by_count in issues in GraphQL

- Add changelog and docs
parent 2329d705
......@@ -6909,6 +6909,11 @@ type EpicIssue implements CurrentUserTodos & Noteable {
"""
blocked: Boolean!
"""
Count of issues blocking this issue
"""
blockedByCount: Int
"""
Timestamp of when the issue was closed
"""
......@@ -9172,6 +9177,11 @@ type Issue implements CurrentUserTodos & Noteable {
"""
blocked: Boolean!
"""
Count of issues blocking this issue
"""
blockedByCount: Int
"""
Timestamp of when the issue was closed
"""
......
......@@ -19037,6 +19037,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "blockedByCount",
"description": "Count of issues blocking this issue",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "closedAt",
"description": "Timestamp of when the issue was closed",
......@@ -24965,6 +24979,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "blockedByCount",
"description": "Count of issues blocking this issue",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "closedAt",
"description": "Timestamp of when the issue was closed",
......@@ -1115,6 +1115,7 @@ Relationship between an epic and an issue.
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
| `blockedByCount` | Int | Count of issues blocking this issue |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
......@@ -1310,6 +1311,7 @@ Represents a recorded measurement (object count) for the Admins.
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
| `blockedByCount` | Int | Count of issues blocking this issue |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
......
......@@ -20,7 +20,17 @@ module EE
field :blocked, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates the issue is blocked',
resolve: -> (obj, _args, ctx) {
::Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate.new(ctx, obj.id)
::Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate.new(ctx, obj.id) do |count|
(count || 0) > 0
end
}
field :blocked_by_count, GraphQL::INT_TYPE, null: true,
description: 'Count of issues blocking this issue',
resolve: -> (obj, _args, ctx) {
::Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate.new(ctx, obj.id) do |count|
count || 0
end
}
field :health_status, ::Types::HealthStatusEnum, null: true,
......
---
title: Expose blocked issue count in GraphQL
merge_request: 46303
author:
type: added
......@@ -7,8 +7,9 @@ module Gitlab
class LazyBlockAggregate
attr_reader :issue_id, :lazy_state
def initialize(query_ctx, issue_id)
def initialize(query_ctx, issue_id, &block)
@issue_id = issue_id
@block = block
# Initialize the loading state for this query,
# or get the previously-initiated state
......@@ -27,7 +28,11 @@ module Gitlab
load_records_into_loaded_objects
end
!!@lazy_state[:loaded_objects][@issue_id]
result = @lazy_state[:loaded_objects][@issue_id]
return @block.call(result) if @block
result
end
private
......@@ -36,10 +41,10 @@ module Gitlab
# The record hasn't been loaded yet, so
# hit the database with all pending IDs to prevent N+1
pending_ids = @lazy_state[:pending_ids].to_a
blocked = IssueLink.blocked_issues_for_collection(pending_ids).compact.flatten
blocked_data = IssueLink.blocked_issues_for_collection(pending_ids).compact.flatten
blocked.each do |o|
@lazy_state[:loaded_objects][o.blocked_issue_id] = true
blocked_data.each do |blocked|
@lazy_state[:loaded_objects][blocked.blocked_issue_id] = blocked.count
end
@lazy_state[:pending_ids].clear
......
......@@ -8,6 +8,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
it { expect(described_class).to have_graphql_field(:weight) }
it { expect(described_class).to have_graphql_field(:health_status) }
it { expect(described_class).to have_graphql_field(:blocked) }
it { expect(described_class).to have_graphql_field(:blocked_by_count) }
it { expect(described_class).to have_graphql_field(:sla_due_at) }
context 'N+1 queries' do
......
......@@ -23,18 +23,36 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do
describe '#block_aggregate' do
subject { described_class.new(query_ctx, issue_id) }
# We cannot directly stub IssueLink, otherwise we get a strange RSpec error
let(:issue_link) { class_double('IssueLink').as_stubbed_const }
let(:fake_state) do
{ pending_ids: Set.new, loaded_objects: {} }
end
before do
subject.instance_variable_set(:@lazy_state, fake_state)
end
context 'when there is a block provided' do
subject do
described_class.new(query_ctx, issue_id) do |result|
result.do_thing
end
end
it 'calls the block' do
expect(fake_state[:loaded_objects][issue_id]).to receive(:do_thing)
subject.block_aggregate
end
end
context 'if the record has already been loaded' do
let(:fake_state) do
{ pending_ids: Set.new, loaded_objects: { issue_id => true } }
{ pending_ids: Set.new, loaded_objects: { issue_id => double(count: 10) } }
end
it 'does not make the query again' do
# We cannot directly stub IssueLink, otherwise we get a strange RSpec error
issue_link = class_double('IssueLink').as_stubbed_const
expect(issue_link).not_to receive(:blocked_issues_for_collection)
subject.block_aggregate
......@@ -55,8 +73,6 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do
end
before do
# We cannot directly stub IssueLink, otherwise we get a strange RSpec error
issue_link = class_double('IssueLink').as_stubbed_const
expect(issue_link).to receive(:blocked_issues_for_collection).and_return(fake_data)
end
......
......@@ -56,9 +56,11 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:blocking_issue1) { create(:issue, project: project) }
let_it_be(:blocked_issue2) { create(:issue, project: project) }
let_it_be(:blocking_issue2) { create(:issue, :confidential, project: project) }
let_it_be(:blocking_issue3) { create(:issue, project: project) }
let_it_be(:issue_link1) { create(:issue_link, source: blocked_issue1, target: blocking_issue1, link_type: 'is_blocked_by') }
let_it_be(:issue_link2) { create(:issue_link, source: blocking_issue2, target: blocked_issue2, link_type: 'blocks') }
let_it_be(:issue_link3) { create(:issue_link, source: blocking_issue3, target: blocked_issue2, link_type: 'blocks') }
let(:query) do
graphql_query_for('project', { fullPath: project.full_path }, query_graphql_field('issues', {}, issue_links_aggregates_query))
......@@ -73,6 +75,7 @@ RSpec.describe 'getting an issue list for a project' do
nodes {
id
blocked
blockedByCount
}
QUERY
end
......@@ -95,19 +98,21 @@ RSpec.describe 'getting an issue list for a project' do
post_graphql(single_issue_query, current_user: current_user)
end
it 'returns the correct results', :aggregate_failures do
it 'returns the correct result', :aggregate_failures do
check_result(blocked_issue1, true, 1)
check_result(blocked_issue2, true, 2)
check_result(blocking_issue1, false, 0)
check_result(blocking_issue2, false, 0)
end
def check_result(issue, expected_blocked, expected_blocked_count)
post_graphql(query, current_user: current_user)
result = graphql_data.dig('project', 'issues', 'nodes')
nodes = graphql_data.dig('project', 'issues', 'nodes')
node = nodes.find { |r| r['id'] == issue.to_global_id.to_s }
expect(find_result(result, blocked_issue1)).to eq true
expect(find_result(result, blocked_issue2)).to eq true
expect(find_result(result, blocking_issue1)).to eq false
expect(find_result(result, blocking_issue2)).to eq false
expect(node['blocked']).to eq expected_blocked
expect(node['blockedByCount']).to eq expected_blocked_count
end
end
def find_result(result, issue)
result.find { |r| r['id'] == issue.to_global_id.to_s }['blocked']
end
end
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