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