Remove label issue and MR counts from default API responses

These counts significantly increase the load time for these
requests. Users can now opt in to receiving the counts by setting
`with_counts=true` in requests. This is a breaking change, but hopefully
a fairly minor one.
This commit is contained in:
Sean McGivern 2019-08-06 17:27:46 +01:00
parent 2608732271
commit e6dc5168b8
14 changed files with 158 additions and 85 deletions

View File

@ -0,0 +1,5 @@
---
title: Remove counts from default labels API responses
merge_request: 31543
author:
type: changed

View File

@ -12,12 +12,13 @@ Get all labels for a given group.
GET /groups/:id/labels GET /groups/:id/labels
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31543))_ |
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/labels?with_counts=true
``` ```
Example response: Example response:

View File

@ -8,12 +8,13 @@ Get all labels for a given project.
GET /projects/:id/labels GET /projects/:id/labels
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- | | --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `with_counts` | boolean | no | Whether or not to include issue and merge request counts. Defaults to `false`. _([Introduced in GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31543))_ |
```bash ```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/labels curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/labels?with_counts=true
``` ```
Example response: Example response:

View File

@ -1085,16 +1085,18 @@ module API
end end
class Label < LabelBasic class Label < LabelBasic
expose :open_issues_count do |label, options| with_options if: lambda { |_, options| options[:with_counts] } do
label.open_issues_count(options[:current_user]) expose :open_issues_count do |label, options|
end label.open_issues_count(options[:current_user])
end
expose :closed_issues_count do |label, options| expose :closed_issues_count do |label, options|
label.closed_issues_count(options[:current_user]) label.closed_issues_count(options[:current_user])
end end
expose :open_merge_requests_count do |label, options| expose :open_merge_requests_count do |label, options|
label.open_merge_requests_count(options[:current_user]) label.open_merge_requests_count(options[:current_user])
end
end end
expose :subscribed do |label, options| expose :subscribed do |label, options|

View File

@ -16,6 +16,8 @@ module API
success Entities::GroupLabel success Entities::GroupLabel
end end
params do params do
optional :with_counts, type: Boolean, default: false,
desc: 'Include issue and merge request counts'
use :pagination use :pagination
end end
get ':id/labels' do get ':id/labels' do

View File

@ -19,7 +19,11 @@ module API
end end
def get_labels(parent, entity) def get_labels(parent, entity)
present paginate(available_labels_for(parent)), with: entity, current_user: current_user, parent: parent present paginate(available_labels_for(parent)),
with: entity,
current_user: current_user,
parent: parent,
with_counts: params[:with_counts]
end end
def create_label(parent, entity) def create_label(parent, entity)

View File

@ -15,6 +15,8 @@ module API
success Entities::ProjectLabel success Entities::ProjectLabel
end end
params do params do
optional :with_counts, type: Boolean, default: false,
desc: 'Include issue and merge request counts'
use :pagination use :pagination
end end
get ':id/labels' do get ':id/labels' do

View File

@ -1,19 +0,0 @@
{
"type": "array",
"items": {
"type": "object",
"properties" : {
"id" : { "type": "integer" },
"name" : { "type": "string "},
"color" : { "type": "string "},
"text_color" : { "type": "string "},
"description" : { "type": "string "},
"open_issues_count" : { "type": "integer "},
"closed_issues_count" : { "type": "integer "},
"open_merge_requests_count" : { "type": "integer "},
"subscribed" : { "type": "boolean" },
"priority" : { "type": "null" }
},
"additionalProperties": false
}
}

View File

@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"color": { "type": "string" },
"text_color": { "type": "string" },
"description": { "type": ["string", "null"] },
"subscribed": { "type": "boolean" }
}
}

View File

@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"allOf": [
{ "$ref": "label.json" },
{
"type": "object",
"properties": {
"open_issues_count": { "type": "integer" },
"closed_issues_count": { "type": "integer" },
"open_merge_requests_count": { "type": "integer" }
}
}
]
}
}

View File

@ -0,0 +1,15 @@
{
"type": "object",
"properties": {
"allOf": [
{ "$ref": "label.json" },
{
"type": "object",
"properties": {
"priority": { "type": ["integer", "null"] },
"is_project_label": { "type": "boolean" }
}
}
]
}
}

View File

@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"allOf": [
{ "$ref": "project_label.json" },
{ "$ref": "label_with_counts.json" }
]
}
}

View File

@ -14,12 +14,25 @@ describe API::GroupLabels do
get api("/groups/#{group.id}/labels", user) get api("/groups/#{group.id}/labels", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/group_labels')
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label'))
expect(json_response.size).to eq(2) expect(json_response.size).to eq(2)
expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug') expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug')
end end
context 'when the with_counts parameter is set' do
it 'includes counts in the response' do
get api("/groups/#{group.id}/labels", user), params: { with_counts: true }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response).to all(match_schema('public_api/v4/labels/label_with_counts'))
expect(json_response.size).to eq(2)
expect(json_response.map { |r| r['open_issues_count'] }).to contain_exactly(0, 0)
end
end
end end
describe 'POST /groups/:id/labels' do describe 'POST /groups/:id/labels' do

View File

@ -11,65 +11,76 @@ describe API::Labels do
end end
describe 'GET /projects/:id/labels' do describe 'GET /projects/:id/labels' do
let(:group) { create(:group) }
let!(:group_label) { create(:group_label, title: 'feature', group: group) }
before do
project.update!(group: group)
end
it 'returns all available labels to the project' do it 'returns all available labels to the project' do
group = create(:group)
group_label = create(:group_label, title: 'feature', group: group)
project.update(group: group)
create(:labeled_issue, project: project, labels: [group_label], author: user)
create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
expected_keys = %w(
id name color text_color description
open_issues_count closed_issues_count open_merge_requests_count
subscribed priority is_project_label
)
get api("/projects/#{project.id}/labels", user) get api("/projects/#{project.id}/labels", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to all(match_schema('public_api/v4/labels/project_label'))
expect(json_response.size).to eq(3) expect(json_response.size).to eq(3)
expect(json_response.first.keys).to match_array expected_keys
expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name]) expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
end
label1_response = json_response.find { |l| l['name'] == label1.title } context 'when the with_counts parameter is set' do
group_label_response = json_response.find { |l| l['name'] == group_label.title } before do
priority_label_response = json_response.find { |l| l['name'] == priority_label.title } create(:labeled_issue, project: project, labels: [group_label], author: user)
create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
end
expect(label1_response['open_issues_count']).to eq(0) it 'includes counts in the response' do
expect(label1_response['closed_issues_count']).to eq(1) get api("/projects/#{project.id}/labels", user), params: { with_counts: true }
expect(label1_response['open_merge_requests_count']).to eq(0)
expect(label1_response['name']).to eq(label1.name)
expect(label1_response['color']).to be_present
expect(label1_response['text_color']).to be_present
expect(label1_response['description']).to be_nil
expect(label1_response['priority']).to be_nil
expect(label1_response['subscribed']).to be_falsey
expect(label1_response['is_project_label']).to be_truthy
expect(group_label_response['open_issues_count']).to eq(1) expect(response).to have_gitlab_http_status(200)
expect(group_label_response['closed_issues_count']).to eq(0) expect(response).to include_pagination_headers
expect(group_label_response['open_merge_requests_count']).to eq(0) expect(json_response).to all(match_schema('public_api/v4/labels/project_label_with_counts'))
expect(group_label_response['name']).to eq(group_label.name) expect(json_response.size).to eq(3)
expect(group_label_response['color']).to be_present expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
expect(group_label_response['text_color']).to be_present
expect(group_label_response['description']).to be_nil
expect(group_label_response['priority']).to be_nil
expect(group_label_response['subscribed']).to be_falsey
expect(group_label_response['is_project_label']).to be_falsey
expect(priority_label_response['open_issues_count']).to eq(0) label1_response = json_response.find { |l| l['name'] == label1.title }
expect(priority_label_response['closed_issues_count']).to eq(0) group_label_response = json_response.find { |l| l['name'] == group_label.title }
expect(priority_label_response['open_merge_requests_count']).to eq(1) priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
expect(priority_label_response['name']).to eq(priority_label.name)
expect(priority_label_response['color']).to be_present expect(label1_response).to include('open_issues_count' => 0,
expect(priority_label_response['text_color']).to be_present 'closed_issues_count' => 1,
expect(priority_label_response['description']).to be_nil 'open_merge_requests_count' => 0,
expect(priority_label_response['priority']).to eq(3) 'name' => label1.name,
expect(priority_label_response['subscribed']).to be_falsey 'description' => nil,
expect(priority_label_response['is_project_label']).to be_truthy 'color' => a_string_matching(/^#\h{6}$/),
'text_color' => a_string_matching(/^#\h{6}$/),
'priority' => nil,
'subscribed' => false,
'is_project_label' => true)
expect(group_label_response).to include('open_issues_count' => 1,
'closed_issues_count' => 0,
'open_merge_requests_count' => 0,
'name' => group_label.name,
'description' => nil,
'color' => a_string_matching(/^#\h{6}$/),
'text_color' => a_string_matching(/^#\h{6}$/),
'priority' => nil,
'subscribed' => false,
'is_project_label' => false)
expect(priority_label_response).to include('open_issues_count' => 0,
'closed_issues_count' => 0,
'open_merge_requests_count' => 1,
'name' => priority_label.name,
'description' => nil,
'color' => a_string_matching(/^#\h{6}$/),
'text_color' => a_string_matching(/^#\h{6}$/),
'priority' => 3,
'subscribed' => false,
'is_project_label' => true)
end
end end
end end