Commit 91cb8558 authored by Etienne Baqué's avatar Etienne Baqué

Merge branch '321097-add-segment-bulk-mutations' into 'master'

Add new bulk create API endpoint for Devops adoption

See merge request gitlab-org/gitlab!54813
parents 7dea43b7 185e73c7
......@@ -85890,4 +85890,4 @@
]
}
}
}
\ No newline at end of file
}
......@@ -757,6 +757,16 @@ Autogenerated return type of BoardListUpdateLimitMetrics.
| `commit` | Commit | Commit for the branch. |
| `name` | String! | Name of the branch. |
### BulkFindOrCreateDevopsAdoptionSegmentsPayload
Autogenerated return type of BulkFindOrCreateDevopsAdoptionSegments.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `segments` | DevopsAdoptionSegment! => Array | Created segments after mutation. |
### BurnupChartDailyTotals
Represents the total number of issues and their weights for a particular day.
......
......@@ -63,7 +63,7 @@ export default {
} = await this.$apollo.mutate({
mutation: deleteDevopsAdoptionSegmentMutation,
variables: {
id,
id: [id],
},
update(store) {
deleteSegmentFromCache(store, id);
......
mutation($id: AnalyticsDevopsAdoptionSegmentID!) {
mutation($id: [AnalyticsDevopsAdoptionSegmentID!]!) {
deleteDevopsAdoptionSegment(input: { id: $id }) {
errors
}
......
......@@ -59,6 +59,7 @@ module EE
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
mount_mutation ::Mutations::QualityManagement::TestCases::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Create
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOrCreate
mount_mutation ::Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Create
mount_mutation ::Mutations::IncidentManagement::OncallSchedule::Update
......
# frozen_string_literal: true
module Mutations
module Admin
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreate < BaseMutation
include Mixins::CommonMethods
graphql_name 'BulkFindOrCreateDevopsAdoptionSegments'
argument :namespace_ids, [::Types::GlobalIDType[::Namespace]],
required: true,
description: 'List of Namespace IDs for the segments.'
field :segments,
[::Types::Admin::Analytics::DevopsAdoption::SegmentType],
null: true,
description: 'Created segments after mutation.'
def resolve(namespace_ids:, **)
namespaces = GlobalID::Locator.locate_many(namespace_ids)
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkFindOrCreateService
.new(current_user: current_user, params: { namespaces: namespaces })
segments = service.execute.payload.fetch(:segments)
{
segments: segments.select(&:persisted?),
errors: segments.sum { |segment| errors_on_object(segment) }
}
end
end
end
end
end
end
end
end
......@@ -22,11 +22,14 @@ module Mutations
def resolve(namespace_id:, **)
namespace = namespace_id.find
response = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
.execute
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::CreateService
.new(current_user: current_user, params: { namespace: namespace })
resolve_segment(response)
response = service.execute
resolve_segment(response)
end
end
end
end
......
......@@ -10,16 +10,23 @@ module Mutations
graphql_name 'DeleteDevopsAdoptionSegment'
argument :id, ::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment],
argument :id, [::Types::GlobalIDType[::Analytics::DevopsAdoption::Segment]],
required: true,
description: "ID of the segment."
description: "One or many IDs of the segments to delete."
def resolve(id:, **)
response = ::Analytics::DevopsAdoption::Segments::DeleteService
.new(segment: id.find, current_user: current_user)
.execute
segments = GlobalID::Locator.locate_many(id)
{ errors: errors_on_object(response.payload[:segment]) }
with_authorization_handler do
service = ::Analytics::DevopsAdoption::Segments::BulkDeleteService
.new(segments: segments, current_user: current_user)
response = service.execute
errors = response.payload[:segments].sum { |segment| errors_on_object(segment) }
{ errors: errors }
end
end
end
end
......
......@@ -13,11 +13,7 @@ module Mutations
def ready?(**args)
unless License.feature_available?(:instance_level_devops_adoption)
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, FEATURE_UNAVAILABLE_MESSAGE
end
unless current_user&.admin?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, ADMIN_MESSAGE
raise_resource_not_available_error!(FEATURE_UNAVAILABLE_MESSAGE)
end
super
......@@ -33,6 +29,16 @@ module Mutations
errors: errors_on_object(segment)
}
end
def with_authorization_handler
yield
rescue ::Analytics::DevopsAdoption::Segments::AuthorizationError => e
handle_unauthorized!(e)
end
def handle_unauthorized!(_exception)
raise_resource_not_available_error!(ADMIN_MESSAGE)
end
end
end
end
......
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class AuthorizationError < StandardError
attr_reader :service
def initialize(service, *args)
@service = service
super(*args)
end
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class BulkDeleteService
include CommonMethods
def initialize(segments:, current_user:)
@segments = segments
@current_user = current_user
end
def execute
authorize!
result = nil
ActiveRecord::Base.transaction do
segments.each do |segment|
response = delete_segment(segment)
if response.error?
result = ServiceResponse.error(message: response.message, payload: response_payload)
raise ActiveRecord::Rollback
end
end
result = ServiceResponse.success(payload: response_payload)
end
result
end
private
attr_reader :segments, :current_user
def response_payload
{ segments: segments }
end
def delete_segment(segment)
DeleteService.new(current_user: current_user, segment: segment).execute
end
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class BulkFindOrCreateService
include CommonMethods
def initialize(params: {}, current_user:)
@params = params
@current_user = current_user
end
def execute
authorize!
segments = params[:namespaces].map do |namespace|
response = FindOrCreateService
.new(current_user: current_user, params: { namespace: namespace })
.execute
response.payload[:segment]
end
ServiceResponse.success(payload: { segments: segments })
end
private
attr_reader :params, :current_user
end
end
end
end
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
module CommonMethods
include Gitlab::Allowable
def authorize!
unless can?(current_user, :manage_devops_adoption_segments, :global)
raise AuthorizationError.new(self, 'Forbidden')
end
end
end
end
end
end
......@@ -4,7 +4,7 @@ module Analytics
module DevopsAdoption
module Segments
class CreateService
include Gitlab::Allowable
include CommonMethods
def initialize(segment: Analytics::DevopsAdoption::Segment.new, params: {}, current_user:)
@segment = segment
......@@ -13,9 +13,7 @@ module Analytics
end
def execute
unless can?(current_user, :manage_devops_adoption_segments, :global)
return ServiceResponse.error(message: 'Forbidden', payload: response_payload)
end
authorize!
segment.assign_attributes(attributes)
......
......@@ -4,7 +4,7 @@ module Analytics
module DevopsAdoption
module Segments
class DeleteService
include Gitlab::Allowable
include CommonMethods
def initialize(segment:, current_user:)
@segment = segment
......@@ -12,12 +12,11 @@ module Analytics
end
def execute
unless can?(current_user, :manage_devops_adoption_segments, :global)
return ServiceResponse.error(message: 'Forbidden', payload: response_payload)
end
authorize!
begin
segment.destroy!
ServiceResponse.success(payload: response_payload)
rescue ActiveRecord::RecordNotDestroyed
ServiceResponse.error(message: 'Devops Adoption Segment deletion error', payload: response_payload)
......
# frozen_string_literal: true
module Analytics
module DevopsAdoption
module Segments
class FindOrCreateService
include CommonMethods
def initialize(params: {}, current_user:)
@params = params
@current_user = current_user
end
def execute
authorize!
segment = Analytics::DevopsAdoption::Segment.find_by_namespace_id(namespace_id)
if segment
ServiceResponse.success(payload: { segment: segment })
else
CreateService.new(current_user: current_user, params: params).execute
end
end
private
attr_reader :params, :current_user
def namespace_id
params.fetch(:namespace_id, params[:namespace]&.id)
end
end
end
end
end
---
title: Add new bulk create API endpoint for Devops adoption
merge_request: 54813
author:
type: other
......@@ -120,7 +120,7 @@ describe('DevopsAdoptionDeleteModal', () => {
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
id: devopsAdoptionSegmentsData.nodes[0].id,
id: [devopsAdoptionSegmentsData.nodes[0].id],
},
}),
);
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::BulkFindOrCreate do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:group) { create(:group, name: 'aaaa') }
let_it_be(:group2) { create(:group, name: 'bbbb') }
let_it_be(:group3) { create(:group, name: 'cccc') }
let_it_be(:existing_segment) { create :devops_adoption_segment, namespace: group3 }
let(:variables) { { namespace_ids: [group.to_gid.to_s, group2.to_gid.to_s, group3.to_gid.to_s] } }
let(:mutation) do
graphql_mutation(:bulk_find_or_create_devops_adoption_segments, variables) do
<<-QL.strip_heredoc
clientMutationId
errors
segments {
id
namespace {
id
name
}
}
QL
end
end
def mutation_response
graphql_mutation_response(:bulk_find_or_create_devops_adoption_segments)
end
before do
stub_licensed_features(instance_level_devops_adoption: true)
end
it_behaves_like 'DevOps Adoption top level errors'
it 'creates the segment for each passed namespace or returns existing segment' do
post_graphql_mutation(mutation, current_user: admin)
expect(mutation_response['errors']).to be_empty
segments = mutation_response['segments']
expect(segments.map { |s| s['namespace']['name'] }).to match_array(%w[aaaa bbbb cccc])
expect(segments.map { |s| s['id'] }).to include(existing_segment.to_gid.to_s)
expect(::Analytics::DevopsAdoption::Segment.joins(:namespace)
.where(namespaces: { name: %w[aaaa bbbb cccc] }).count).to eq(3)
end
end
......@@ -42,5 +42,6 @@ RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Create do
segment = mutation_response['segment']
expect(segment['namespace']['name']).to eq('bbbb')
expect(::Analytics::DevopsAdoption::Segment.joins(:namespace).where(namespaces: { name: 'bbbb' }).count).to eq(1)
end
end
......@@ -7,7 +7,7 @@ RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete do
let_it_be(:admin) { create(:admin) }
let(:segment) { create(:devops_adoption_segment) }
let!(:segment) { create(:devops_adoption_segment) }
let(:variables) { { id: segment.to_gid.to_s } }
let(:mutation) do
......@@ -35,4 +35,19 @@ RSpec.describe Mutations::Admin::Analytics::DevopsAdoption::Segments::Delete do
expect(mutation_response['errors']).to be_empty
expect(::Analytics::DevopsAdoption::Segment.find_by_id(segment.id)).to eq(nil)
end
context 'with bulk ids' do
let!(:segment2) { create(:devops_adoption_segment) }
let!(:segment3) { create(:devops_adoption_segment) }
let(:variables) { { id: [segment.to_gid.to_s, segment2.to_gid.to_s] } }
it 'deletes the segments specified for deletion' do
post_graphql_mutation(mutation, current_user: admin)
expect(mutation_response['errors']).to be_empty
expect(::Analytics::DevopsAdoption::Segment.where(id: [segment.id, segment2.id, segment3.id]))
.to match_array([segment3])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::BulkDeleteService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let(:segment) { create(:devops_adoption_segment, namespace: group) }
let(:segment2) { create(:devops_adoption_segment) }
subject { described_class.new(segments: [segment, segment2], current_user: user).execute }
before do
enable_admin_mode!(user)
end
it 'deletes the segments' do
expect(subject).to be_success
expect(segment).not_to be_persisted
expect(segment2).not_to be_persisted
end
context 'when deletion fails' do
it 'keeps records and returns error response' do
expect(segment).to receive(:destroy).and_raise(ActiveRecord::RecordNotDestroyed)
expect(subject).to be_error
expect(subject.message).to eq('Devops Adoption Segment deletion error')
expect(segment).to be_persisted
expect(segment2).to be_persisted
end
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::BulkFindOrCreateService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let_it_be(:group2) { create(:group) }
let(:params) { { namespaces: [group, group2] } }
let!(:segment) { create :devops_adoption_segment, namespace: group }
subject { described_class.new(params: params, current_user: user).execute }
before do
enable_admin_mode!(user)
end
context 'for admins' do
it 'returns existing segments for namespaces and creates new one if none exists' do
expect do
subject
end.to change { ::Analytics::DevopsAdoption::Segment.count }.by(1)
expect(subject.payload.fetch(:segments)).to include(segment)
end
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
end
......@@ -5,16 +5,16 @@ require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::CreateService do
include AdminModeHelper
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let(:params) { { namespace: group } }
let(:segment) { subject.payload[:segment] }
subject { described_class.new(params: params, current_user: admin).execute }
subject { described_class.new(params: params, current_user: user).execute }
before do
enable_admin_mode!(admin)
enable_admin_mode!(user)
end
it 'persists the segment' do
......@@ -30,18 +30,6 @@ RSpec.describe Analytics::DevopsAdoption::Segments::CreateService do
expect(Analytics::DevopsAdoption::CreateSnapshotWorker).to have_received(:perform_async).with(Analytics::DevopsAdoption::Segment.last.id)
end
context 'when user is not an admin' do
let(:user) { build(:user) }
subject { described_class.new(params: params, current_user: user).execute }
it 'does not persist the segment' do
expect(subject).to be_error
expect(subject.message).to eq('Forbidden')
expect(segment).not_to be_persisted
end
end
context 'when namespace is not given' do
before do
params.delete(:namespace)
......@@ -53,4 +41,14 @@ RSpec.describe Analytics::DevopsAdoption::Segments::CreateService do
expect(segment.namespace).to be_nil
end
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
end
......@@ -29,12 +29,13 @@ RSpec.describe Analytics::DevopsAdoption::Segments::DeleteService do
end
end
context 'when the user is not admin' do
let(:user) { build(:user) }
context 'for non-admins' do
let_it_be(:user) { build(:user) }
it 'returns error response' do
expect(subject).to be_error
expect(subject.message).to eq('Forbidden')
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segments::FindOrCreateService do
include AdminModeHelper
let_it_be(:user) { create(:user, :admin) }
let_it_be(:group) { create(:group) }
let(:params) { { namespace: group } }
let(:segment) { subject.payload[:segment] }
subject { described_class.new(params: params, current_user: user).execute }
before do
enable_admin_mode!(user)
end
context 'for admins' do
context 'when segment for given namespace already exists' do
let!(:segment) { create :devops_adoption_segment, namespace: group }
it 'returns existing segment' do
expect do
subject
end.not_to change { Analytics::DevopsAdoption::Segment.count }
expect(subject.payload.fetch(:segment)).to eq(segment)
end
end
context 'when segment for given namespace does not exist' do
it 'calls for segment creation' do
expect_next_instance_of(Analytics::DevopsAdoption::Segments::CreateService, current_user: user, params: { namespace: group }) do |instance|
expect(instance).to receive(:execute).and_return('create_response')
end
expect(subject).to eq 'create_response'
end
end
end
context 'for non-admins' do
let_it_be(:user) { build(:user) }
it 'returns forbidden error' do
expect do
subject
end.to raise_error(Analytics::DevopsAdoption::Segments::AuthorizationError)
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