Commit 95b16248 authored by Mario Celi's avatar Mario Celi

Add IterationCadenceDestroy mutation

- Change issues/merge_requests sprint FK from delete cascade to nullify
- Add mutation to GQL API
- Add changelog entry for EE
- Add specs
parent 0327e789
---
title: Add support to destroy iteration cadences in GraphQL
merge_request: 59060
author:
type: added
# frozen_string_literal: true
class UpdateIssuesIterationForeignKey < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key_if_exists(:issues, column: :sprint_id)
end
add_concurrent_foreign_key(:issues, :sprints, column: :sprint_id, on_delete: :nullify)
end
def down
with_lock_retries do
remove_foreign_key_if_exists(:issues, column: :sprint_id)
end
add_concurrent_foreign_key(:issues, :sprints, column: :sprint_id, on_delete: :cascade)
end
end
# frozen_string_literal: true
class UpdateMergeRequestsIterationForeignKey < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key_if_exists(:merge_requests, column: :sprint_id)
end
add_concurrent_foreign_key(:merge_requests, :sprints, column: :sprint_id, on_delete: :nullify)
end
def down
with_lock_retries do
remove_foreign_key_if_exists(:merge_requests, column: :sprint_id)
end
add_concurrent_foreign_key(:merge_requests, :sprints, column: :sprint_id, on_delete: :cascade)
end
end
145782c0cb0d24617e0e43c43f49a0f1d4033df3f303e4d4085e586c48e2408e
\ No newline at end of file
62842b9e9753b7880e980b0a16335e7d00bdce8b7b42d94b1ba26828724c01dd
\ No newline at end of file
......@@ -24839,7 +24839,7 @@ ALTER TABLE ONLY ci_builds
ADD CONSTRAINT fk_3a9eaa254d FOREIGN KEY (stage_id) REFERENCES ci_stages(id) ON DELETE CASCADE;
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_3b8c72ea56 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE CASCADE;
ADD CONSTRAINT fk_3b8c72ea56 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE SET NULL;
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_3c1fd1cccc FOREIGN KEY (due_date_sourcing_milestone_id) REFERENCES milestones(id) ON DELETE SET NULL;
......@@ -24983,7 +24983,7 @@ ALTER TABLE ONLY labels
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_7e85395a64 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE CASCADE;
ADD CONSTRAINT fk_7e85395a64 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE SET NULL;
ALTER TABLE ONLY merge_request_metrics
ADD CONSTRAINT fk_7f28d925f3 FOREIGN KEY (merged_by_id) REFERENCES users(id) ON DELETE SET NULL;
......@@ -2437,6 +2437,25 @@ Input type: `IterationCadenceCreateInput`
| <a id="mutationiterationcadencecreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationiterationcadencecreateiterationcadence"></a>`iterationCadence` | [`IterationCadence`](#iterationcadence) | The created iteration cadence. |
### `Mutation.iterationCadenceDestroy`
Input type: `IterationCadenceDestroyInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationiterationcadencedestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationiterationcadencedestroyid"></a>`id` | [`IterationsCadenceID!`](#iterationscadenceid) | Global ID of the iteration cadence. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationiterationcadencedestroyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationiterationcadencedestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationiterationcadencedestroygroup"></a>`group` | [`Group!`](#group) | Group the iteration cadence belongs to. |
### `Mutation.iterationCadenceUpdate`
Input type: `IterationCadenceUpdateInput`
......
......@@ -27,6 +27,7 @@ module EE
mount_mutation ::Mutations::Iterations::Update
mount_mutation ::Mutations::Iterations::Cadences::Create
mount_mutation ::Mutations::Iterations::Cadences::Update
mount_mutation ::Mutations::Iterations::Cadences::Destroy
mount_mutation ::Mutations::RequirementsManagement::CreateRequirement
mount_mutation ::Mutations::RequirementsManagement::ExportRequirements
mount_mutation ::Mutations::RequirementsManagement::UpdateRequirement
......
# frozen_string_literal: true
module Mutations
module Iterations
module Cadences
class Destroy < BaseMutation
graphql_name 'IterationCadenceDestroy'
authorize :admin_iteration_cadence
argument :id, ::Types::GlobalIDType[::Iterations::Cadence], required: true,
description: copy_field_description(Types::Iterations::CadenceType, :id)
field :group, ::Types::GroupType, null: false, description: 'Group the iteration cadence belongs to.'
def resolve(id:)
iteration_cadence = authorized_find!(id: id)
response = ::Iterations::Cadences::DestroyService.new(iteration_cadence, current_user).execute
{
group: response.payload[:group],
errors: response.errors
}
end
private
def find_object(id:)
# TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Iterations::Cadence].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end
end
......@@ -25,6 +25,7 @@ module EE
has_many :labels, through: :board_labels
scope :with_associations, -> { preload(:destroyable_lists, :labels, :assignee) }
scope :in_iterations, ->(iterations) { where(iteration: iterations) }
end
override :scoped?
......
# frozen_string_literal: true
module Iterations
module Cadences
class DestroyService
include Gitlab::Allowable
def initialize(iteration_cadence, user)
@iteration_cadence = iteration_cadence
@group = iteration_cadence.group
@current_user = user
end
def execute
response_payload = { group: @group }
return ::ServiceResponse.error(message: _('Operation not allowed'), payload: response_payload, http_status: 403) unless can_destroy_iteration_cadence?
if destroy_and_remove_references
::ServiceResponse.success(payload: response_payload.merge(iteration_cadence: iteration_cadence))
else
::ServiceResponse.error(message: iteration_cadence.errors.full_messages, payload: response_payload, http_status: 422)
end
end
private
attr_reader :iteration_cadence, :current_user, :group
def can_destroy_iteration_cadence?
group.iteration_cadences_feature_flag_enabled? &&
group.licensed_feature_available?(:iterations) &&
can?(current_user, :admin_iteration_cadence, iteration_cadence)
end
def destroy_and_remove_references
ApplicationRecord.transaction do
Board.in_iterations(iteration_cadence.iterations).update_all(iteration_id: nil) && iteration_cadence.destroy
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Destroying an iteration cadence' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:iteration_cadence, refind: true) { create(:iterations_cadence, group: group) }
let(:params) do
{ id: iteration_cadence.to_global_id.to_s }
end
let(:mutation) do
graphql_mutation(:iteration_cadence_destroy, params)
end
def mutation_response
graphql_mutation_response(:iteration_cadence_destroy)
end
context 'when the user does not have permission' do
before do
stub_licensed_features(iterations: true)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the user has permission' do
before do
group.add_developer(current_user)
end
context 'when iterations feature is disabled' do
before do
stub_licensed_features(iterations: false)
end
it_behaves_like 'a mutation that returns top-level errors', errors: [
'The resource that you are attempting to access does not exist or you don\'t have permission to ' \
'perform this action'
]
end
context 'when iterations feature is enabled' do
before do
stub_licensed_features(iterations: true)
end
it 'destroys the iteration cadence', :aggregate_failures do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(Iterations::Cadence, :count).by(-1)
expect(mutation_response).to include('group' => hash_including('id' => group.to_global_id.to_s))
end
context 'when iteration_cadences feature flag is disabled' do
before do
stub_feature_flags(iteration_cadences: false)
end
it_behaves_like 'a mutation that returns errors in the response', errors: ["Operation not allowed"]
end
context 'when required arguments are missing' do
let(:params) { {} }
it 'returns error about required argument' do
post_graphql_mutation(mutation, current_user: current_user)
expect_graphql_errors_to_include(/was provided invalid value for id \(Expected value to not be null\)/)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Iterations::Cadences::DestroyService do
subject(:results) { described_class.new(iteration_cadence, user).execute }
let_it_be(:group, refind: true) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:iteration_cadence, refind: true) { create(:iterations_cadence, group: group, start_date: Date.today, duration_in_weeks: 1, iterations_in_advance: 2) }
let_it_be(:iteration) { create(:started_iteration, group: group, start_date: 2.days.ago, due_date: 5.days.from_now) }
let_it_be(:iteration_list, refind: true) { create(:iteration_list, iteration: iteration) }
let_it_be(:iteration_event, refind: true) { create(:resource_iteration_event, iteration: iteration) }
let_it_be(:board) { create(:board, iteration: iteration, group: group) }
let_it_be(:issue) { create(:issue, namespace: group, iteration: iteration) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, iteration: iteration) }
RSpec.shared_examples 'cadence destroy fails with message' do |message:|
it { is_expected.to be_error }
it 'returns not allowed message' do
expect(results.message).to eq(message)
end
end
describe '#execute' do
context 'when iterations feature enabled' do
before do
stub_licensed_features(iterations: true)
end
context 'when user is authorized' do
before do
group.add_developer(user)
end
it { is_expected.to be_success }
it 'destroys the cadence and associated records' do
expect do
results
board.reload
issue.reload
merge_request.reload
end.to change(Iterations::Cadence, :count).by(-1).and(
change(List, :count).by(-1)
).and(
change(ResourceIterationEvent, :count).by(-1)
).and(
change(Iteration, :count).by(-1)
).and(
change(board, :iteration_id).from(iteration.id).to(nil)
).and(
change(issue, :iteration).from(iteration).to(nil)
).and(
change(merge_request, :iteration).from(iteration).to(nil)
)
end
it 'returns the cadence as part of the response' do
expect(results.payload[:iteration_cadence]).to eq(iteration_cadence)
end
end
context 'when user is not authorized' do
it_behaves_like 'cadence destroy fails with message', message: 'Operation not allowed'
end
end
context 'when iterations feature disabled' do
before do
stub_licensed_features(iterations: false)
end
context 'when user is authorized' do
before do
group.add_developer(user)
end
it_behaves_like 'cadence destroy fails with message', message: 'Operation not allowed'
end
context 'when user is not authorized' do
it_behaves_like 'cadence destroy fails with message', message: 'Operation not allowed'
end
end
context 'when iteration cadences feature flag disabled' do
before do
stub_licensed_features(iterations: true)
stub_feature_flags(iteration_cadences: false)
end
context 'when user is authorized' do
before do
group.add_developer(user)
end
it_behaves_like 'cadence destroy fails with message', message: 'Operation not allowed'
end
context 'when user is not authorized' do
it_behaves_like 'cadence destroy fails with message', message: 'Operation not allowed'
end
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