diff options
Diffstat (limited to 'app/graphql/mutations')
37 files changed, 816 insertions, 86 deletions
diff --git a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb index a3a421f8938..17f9b5b5637 100644 --- a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb +++ b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb @@ -33,9 +33,9 @@ module Mutations super end - def resolve(args) + def resolve(queue_name:, **args) { - result: Gitlab::SidekiqQueue.new(args[:queue_name]).drop_jobs!(args, timeout: 30), + result: Gitlab::SidekiqQueue.new(queue_name).drop_jobs!(args, timeout: 30), errors: [] } rescue Gitlab::SidekiqQueue::NoMetadataError @@ -44,7 +44,7 @@ module Mutations errors: ['No metadata provided'] } rescue Gitlab::SidekiqQueue::InvalidQueueError - raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Queue #{args[:queue_name]} not found" + raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Queue #{queue_name} not found" end end end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index 0ccfcf34180..81d5ee95f06 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -4,7 +4,6 @@ module Mutations module AlertManagement class Base < BaseMutation include Gitlab::Utils::UsageData - include ResolvesProject argument :project_path, GraphQL::ID_TYPE, required: true, @@ -33,13 +32,12 @@ module Mutations private - def find_object(project_path:, iid:) - project = resolve_project(full_path: project_path) + def find_object(project_path:, **args) + project = Project.find_by_full_path(project_path) return unless project - resolver = Resolvers::AlertManagement::AlertResolver.single.new(object: project, context: context, field: nil) - resolver.resolve(iid: iid) + ::AlertManagement::AlertsFinder.new(current_user, project, args).execute.first end end end diff --git a/app/graphql/mutations/alert_management/http_integration/create.rb b/app/graphql/mutations/alert_management/http_integration/create.rb new file mode 100644 index 00000000000..ddb75e66bb4 --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/create.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class Create < HttpIntegrationBase + include ResolvesProject + + graphql_name 'HttpIntegrationCreate' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'The project to create the integration in' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'The name of the integration' + + argument :active, GraphQL::BOOLEAN_TYPE, + required: true, + description: 'Whether the integration is receiving alerts' + + def resolve(args) + project = authorized_find!(full_path: args[:project_path]) + + response ::AlertManagement::HttpIntegrations::CreateService.new( + project, + current_user, + args.slice(:name, :active) + ).execute + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/destroy.rb b/app/graphql/mutations/alert_management/http_integration/destroy.rb new file mode 100644 index 00000000000..0f478760aab --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/destroy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class Destroy < HttpIntegrationBase + graphql_name 'HttpIntegrationDestroy' + + argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], + required: true, + description: "The id of the integration to remove" + + def resolve(id:) + integration = authorized_find!(id: id) + + response ::AlertManagement::HttpIntegrations::DestroyService.new( + integration, + current_user + ).execute + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb new file mode 100644 index 00000000000..d328eabf244 --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class HttpIntegrationBase < BaseMutation + field :integration, + Types::AlertManagement::HttpIntegrationType, + null: true, + description: "The HTTP integration" + + authorize :admin_operations + + private + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_class: ::AlertManagement::HttpIntegration) + end + + def response(result) + { + integration: result.payload[:integration], + errors: result.errors + } + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/reset_token.rb b/app/graphql/mutations/alert_management/http_integration/reset_token.rb new file mode 100644 index 00000000000..eefab156825 --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/reset_token.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class ResetToken < HttpIntegrationBase + graphql_name 'HttpIntegrationResetToken' + + argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], + required: true, + description: "The id of the integration to mutate" + + def resolve(id:) + integration = authorized_find!(id: id) + + response ::AlertManagement::HttpIntegrations::UpdateService.new( + integration, + current_user, + regenerate_token: true + ).execute + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/update.rb b/app/graphql/mutations/alert_management/http_integration/update.rb new file mode 100644 index 00000000000..309c45b04ac --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/update.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class Update < HttpIntegrationBase + graphql_name 'HttpIntegrationUpdate' + + argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], + required: true, + description: "The id of the integration to mutate" + + argument :name, GraphQL::STRING_TYPE, + required: false, + description: "The name of the integration" + + argument :active, GraphQL::BOOLEAN_TYPE, + required: false, + description: "Whether the integration is receiving alerts" + + def resolve(args) + integration = authorized_find!(id: args[:id]) + + response ::AlertManagement::HttpIntegrations::UpdateService.new( + integration, + current_user, + args.slice(:name, :active) + ).execute + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/create.rb b/app/graphql/mutations/alert_management/prometheus_integration/create.rb new file mode 100644 index 00000000000..935ec53795c --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/create.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class Create < PrometheusIntegrationBase + include ResolvesProject + + graphql_name 'PrometheusIntegrationCreate' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'The project to create the integration in' + + argument :active, GraphQL::BOOLEAN_TYPE, + required: true, + description: 'Whether the integration is receiving alerts' + + argument :api_url, GraphQL::STRING_TYPE, + required: true, + description: 'Endpoint at which prometheus can be queried' + + def resolve(args) + project = authorized_find!(full_path: args[:project_path]) + + return integration_exists if project.prometheus_service + + result = ::Projects::Operations::UpdateService.new( + project, + current_user, + **integration_attributes(args), + **token_attributes + ).execute + + response(project.prometheus_service, result) + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + + def integration_exists + response(nil, message: _('Multiple Prometheus integrations are not supported')) + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb new file mode 100644 index 00000000000..6b690ac239a --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class PrometheusIntegrationBase < BaseMutation + field :integration, + Types::AlertManagement::PrometheusIntegrationType, + null: true, + description: "The newly created integration" + + authorize :admin_project + + private + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_class: ::PrometheusService) + end + + def response(integration, result) + { + integration: integration, + errors: Array(result[:message]) + } + end + + def integration_attributes(args) + { + prometheus_integration_attributes: { + manual_configuration: args[:active], + api_url: args[:api_url] + }.compact + } + end + + def token_attributes + { alerting_setting_attributes: { regenerate_token: true } } + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb new file mode 100644 index 00000000000..745ac51f6e3 --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class ResetToken < PrometheusIntegrationBase + graphql_name 'PrometheusIntegrationResetToken' + + argument :id, Types::GlobalIDType[::PrometheusService], + required: true, + description: "The id of the integration to mutate" + + def resolve(id:) + integration = authorized_find!(id: id) + + result = ::Projects::Operations::UpdateService.new( + integration.project, + current_user, + token_attributes + ).execute + + response integration, result + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/update.rb b/app/graphql/mutations/alert_management/prometheus_integration/update.rb new file mode 100644 index 00000000000..1f0dea119c5 --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/update.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class Update < PrometheusIntegrationBase + graphql_name 'PrometheusIntegrationUpdate' + + argument :id, Types::GlobalIDType[::PrometheusService], + required: true, + description: "The id of the integration to mutate" + + argument :active, GraphQL::BOOLEAN_TYPE, + required: false, + description: "Whether the integration is receiving alerts" + + argument :api_url, GraphQL::STRING_TYPE, + required: false, + description: "Endpoint at which prometheus can be queried" + + def resolve(args) + integration = authorized_find!(id: args[:id]) + + result = ::Projects::Operations::UpdateService.new( + integration.project, + current_user, + integration_attributes(args) + ).execute + + response integration.reset, result + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb index 1e14bae048a..74185dca529 100644 --- a/app/graphql/mutations/alert_management/update_alert_status.rb +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -9,9 +9,9 @@ module Mutations required: true, description: 'The status to set the alert' - def resolve(args) - alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) - result = update_status(alert, args[:status]) + def resolve(project_path:, iid:, status:) + alert = authorized_find!(project_path: project_path, iid: iid) + result = update_status(alert, status) track_usage_event(:incident_management_alert_status_changed, current_user.id) diff --git a/app/graphql/mutations/boards/create.rb b/app/graphql/mutations/boards/create.rb index e381205242e..ebbd19930ec 100644 --- a/app/graphql/mutations/boards/create.rb +++ b/app/graphql/mutations/boards/create.rb @@ -3,8 +3,7 @@ module Mutations module Boards class Create < ::Mutations::BaseMutation - include Mutations::ResolvesGroup - include ResolvesProject + include Mutations::ResolvesResourceParent graphql_name 'CreateBoard' @@ -13,12 +12,6 @@ module Mutations null: true, description: 'The board after mutation.' - argument :project_path, GraphQL::ID_TYPE, - required: false, - description: 'The project full path the board is associated with.' - argument :group_path, GraphQL::ID_TYPE, - required: false, - description: 'The group full path the board is associated with.' argument :name, GraphQL::STRING_TYPE, required: false, @@ -28,7 +21,7 @@ module Mutations required: false, description: 'The ID of the user to be assigned to the board.' argument :milestone_id, - GraphQL::ID_TYPE, + Types::GlobalIDType[Milestone], required: false, description: 'The ID of the milestone to be assigned to the board.' argument :weight, @@ -36,17 +29,14 @@ module Mutations required: false, description: 'The weight of the board.' argument :label_ids, - [GraphQL::ID_TYPE], + [Types::GlobalIDType[Label]], required: false, description: 'The IDs of labels to be added to the board.' authorize :admin_board def resolve(args) - group_path = args.delete(:group_path) - project_path = args.delete(:project_path) - - board_parent = authorized_find!(group_path: group_path, project_path: project_path) + board_parent = authorized_resource_parent_find!(args) response = ::Boards::CreateService.new(board_parent, current_user, args).execute { @@ -54,25 +44,6 @@ module Mutations errors: response.errors } end - - def ready?(**args) - if args.values_at(:project_path, :group_path).compact.blank? - raise Gitlab::Graphql::Errors::ArgumentError, - 'group_path or project_path arguments are required' - end - - super - end - - private - - def find_object(group_path: nil, project_path: nil) - if group_path - resolve_group(full_path: group_path) - else - resolve_project(full_path: project_path) - end - end end end end diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb index 7efed3058b3..14502b5174f 100644 --- a/app/graphql/mutations/boards/lists/update.rb +++ b/app/graphql/mutations/boards/lists/update.rb @@ -6,7 +6,7 @@ module Mutations class Update < BaseMutation graphql_name 'UpdateBoardList' - argument :list_id, GraphQL::ID_TYPE, + argument :list_id, Types::GlobalIDType[List], required: true, loads: Types::BoardListType, description: 'Global ID of the list.' diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb index 9ed1bb819c8..2b9107350fd 100644 --- a/app/graphql/mutations/commits/create.rb +++ b/app/graphql/mutations/commits/create.rb @@ -13,7 +13,11 @@ module Mutations argument :branch, GraphQL::STRING_TYPE, required: true, - description: 'Name of the branch' + description: 'Name of the branch to commit into, it can be a new branch' + + argument :start_branch, GraphQL::STRING_TYPE, + required: false, + description: 'If on a new branch, name of the original branch' argument :message, GraphQL::STRING_TYPE, @@ -32,13 +36,13 @@ module Mutations authorize :push_code - def resolve(project_path:, branch:, message:, actions:) + def resolve(project_path:, branch:, message:, actions:, **args) project = authorized_find!(full_path: project_path) attributes = { commit_message: message, branch_name: branch, - start_branch: branch, + start_branch: args[:start_branch] || branch, actions: actions.map { |action| action.to_h } } diff --git a/app/graphql/mutations/concerns/mutations/package_eventable.rb b/app/graphql/mutations/concerns/mutations/package_eventable.rb new file mode 100644 index 00000000000..86fd7b9a88a --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/package_eventable.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Mutations + module PackageEventable + extend ActiveSupport::Concern + + private + + def track_event(event, scope) + ::Packages::CreateEventService.new(nil, current_user, event_name: event, scope: scope).execute + ::Gitlab::Tracking.event(event.to_s, scope.to_s) + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb new file mode 100644 index 00000000000..04a9abf9529 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Mutations + module ResolvesResourceParent + extend ActiveSupport::Concern + include Mutations::ResolvesGroup + include ResolvesProject + + included do + argument :project_path, GraphQL::ID_TYPE, + required: false, + description: 'The project full path the resource is associated with' + + argument :group_path, GraphQL::ID_TYPE, + required: false, + description: 'The group full path the resource is associated with' + end + + def ready?(**args) + unless args[:project_path].present? ^ args[:group_path].present? + raise Gitlab::Graphql::Errors::ArgumentError, + 'Exactly one of group_path or project_path arguments is required' + end + + super + end + + private + + def authorized_resource_parent_find!(args) + authorized_find!(project_path: args.delete(:project_path), + group_path: args.delete(:group_path)) + end + + def find_object(project_path: nil, group_path: nil) + if group_path.present? + resolve_group(full_path: group_path) + else + resolve_project(full_path: project_path) + end + end + end +end diff --git a/app/graphql/mutations/container_repositories/destroy.rb b/app/graphql/mutations/container_repositories/destroy.rb new file mode 100644 index 00000000000..8312193147f --- /dev/null +++ b/app/graphql/mutations/container_repositories/destroy.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Mutations + module ContainerRepositories + class Destroy < Mutations::BaseMutation + include ::Mutations::PackageEventable + + graphql_name 'DestroyContainerRepository' + + authorize :destroy_container_image + + argument :id, + ::Types::GlobalIDType[::ContainerRepository], + required: true, + description: 'ID of the container repository.' + + field :container_repository, + Types::ContainerRepositoryType, + null: false, + description: 'The container repository policy after scheduling the deletion.' + + def resolve(id:) + container_repository = authorized_find!(id: id) + + container_repository.delete_scheduled! + DeleteContainerRepositoryWorker.perform_async(current_user.id, container_repository.id) + track_event(:delete_repository, :container) + + { + container_repository: container_repository, + errors: [] + } + end + + private + + def find_object(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::ContainerRepository].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/mutations/custom_emoji/create.rb b/app/graphql/mutations/custom_emoji/create.rb new file mode 100644 index 00000000000..d912a29d12e --- /dev/null +++ b/app/graphql/mutations/custom_emoji/create.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module CustomEmoji + class Create < BaseMutation + include Mutations::ResolvesGroup + + graphql_name 'CreateCustomEmoji' + + authorize :create_custom_emoji + + field :custom_emoji, + Types::CustomEmojiType, + null: true, + description: 'The new custom emoji' + + argument :group_path, GraphQL::ID_TYPE, + required: true, + description: 'Namespace full path the emoji is associated with' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the emoji' + + argument :url, GraphQL::STRING_TYPE, + required: true, + as: :file, + description: 'Location of the emoji file' + + def resolve(group_path:, **args) + group = authorized_find!(group_path: group_path) + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911#note_444682238 + args[:external] = true + + custom_emoji = group.custom_emoji.create(args) + + { + custom_emoji: custom_emoji.valid? ? custom_emoji : nil, + errors: errors_on_object(custom_emoji) + } + end + + private + + def find_object(group_path:) + resolve_group(full_path: group_path) + end + end + end +end diff --git a/app/graphql/mutations/labels/create.rb b/app/graphql/mutations/labels/create.rb new file mode 100644 index 00000000000..cb03651618e --- /dev/null +++ b/app/graphql/mutations/labels/create.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Mutations + module Labels + class Create < BaseMutation + include Mutations::ResolvesResourceParent + + graphql_name 'LabelCreate' + + field :label, + Types::LabelType, + null: true, + description: 'The label after mutation' + + argument :title, GraphQL::STRING_TYPE, + required: true, + description: 'Title of the label' + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: 'Description of the label' + + argument :color, GraphQL::STRING_TYPE, + required: false, + default_value: Label::DEFAULT_COLOR, + description: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords" + + authorize :admin_label + + def resolve(args) + parent = authorized_resource_parent_find!(args) + parent_key = parent.is_a?(Project) ? :project : :group + + label = ::Labels::CreateService.new(args).execute(parent_key => parent) + + { + label: label.persisted? ? label : nil, + errors: errors_on_object(label) + } + end + end + end +end diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb index c1e45808593..712c68c9425 100644 --- a/app/graphql/mutations/merge_requests/set_labels.rb +++ b/app/graphql/mutations/merge_requests/set_labels.rb @@ -6,7 +6,7 @@ module Mutations graphql_name 'MergeRequestSetLabels' argument :label_ids, - [GraphQL::ID_TYPE], + [::Types::GlobalIDType[Label]], required: true, description: <<~DESC The Label IDs to set. Replaces existing labels by default. @@ -23,10 +23,11 @@ module Mutations merge_request = authorized_find!(project_path: project_path, iid: iid) project = merge_request.project - label_ids = label_ids - .map { |gid| GlobalID.parse(gid) } - .select(&method(:label_descendant?)) - .map(&:model_id) # MergeRequests::UpdateService expects integers + # TODO: remove this line when the compatibility layer is removed: + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + label_ids = label_ids.map { |id| ::Types::GlobalIDType[::Label].coerce_isolated_input(id) } + # MergeRequests::UpdateService expects integers + label_ids = label_ids.compact.map(&:model_id) attribute_name = case operation_mode when Types::MutationOperationModeEnum.enum[:append] @@ -45,10 +46,6 @@ module Mutations errors: errors_on_object(merge_request) } end - - def label_descendant?(gid) - gid&.model_class&.ancestors&.include?(Label) - end end end end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb index 6e183e78d9b..d6731dfcafd 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb @@ -9,8 +9,7 @@ module Mutations authorize :delete_metrics_dashboard_annotation - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Metrics::Dashboard::Annotation], required: true, description: 'The global ID of the annotation to delete' diff --git a/app/graphql/mutations/notes/reposition_image_diff_note.rb b/app/graphql/mutations/notes/reposition_image_diff_note.rb new file mode 100644 index 00000000000..0d88bcd9a30 --- /dev/null +++ b/app/graphql/mutations/notes/reposition_image_diff_note.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Mutations + module Notes + # This mutation differs from the update note mutations as it checks the + # `reposition_note` permission, and doesn't allow updating a note's `body`. + class RepositionImageDiffNote < Mutations::Notes::Base + graphql_name 'RepositionImageDiffNote' + + description 'Repositions a DiffNote on an image (a `Note` where the `position.positionType` is `"image"`)' + + authorize :reposition_note + + argument :id, + Types::GlobalIDType[DiffNote], + loads: Types::Notes::NoteType, + as: :note, + required: true, + description: 'The global id of the DiffNote to update' + + argument :position, + Types::Notes::UpdateDiffImagePositionInputType, + required: true, + description: copy_field_description(Types::Notes::NoteType, :position) + + def resolve(note:, position:) + authorize!(note) + + pre_update_checks!(note, position) + + updated_note = ::Notes::UpdateService.new( + note.project, + current_user, + note_params(note.position, position) + ).execute(note) + + { + note: updated_note.reset, + errors: errors_on_object(updated_note) + } + end + + private + + # An ImageDiffNote does not exist as a class itself, but is instead + # just a `DiffNote` with a particular kind of `Gitlab::Diff::Position`. + # In addition to accepting a `DiffNote` Global ID we also need to + # perform this check. + def pre_update_checks!(note, position) + unless note.position&.on_image? + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + 'Resource is not an ImageDiffNote' + end + end + + def note_params(old_position, new_position) + position = old_position.to_h.merge(new_position) + + { + position: Gitlab::Diff::Position.new(position) + } + end + end + end +end diff --git a/app/graphql/mutations/notes/update/image_diff_note.rb b/app/graphql/mutations/notes/update/image_diff_note.rb index ef70a8d2bf4..f4533cd9edb 100644 --- a/app/graphql/mutations/notes/update/image_diff_note.rb +++ b/app/graphql/mutations/notes/update/image_diff_note.rb @@ -47,12 +47,11 @@ module Mutations end def position_params(note, args) - new_position = args[:position]&.to_h&.compact - return unless new_position + return unless args[:position] original_position = note.position.to_h - Gitlab::Diff::Position.new(original_position.merge(new_position)) + Gitlab::Diff::Position.new(original_position.merge(args[:position])) end end end diff --git a/app/graphql/mutations/releases/base.rb b/app/graphql/mutations/releases/base.rb new file mode 100644 index 00000000000..d53cfbe6a11 --- /dev/null +++ b/app/graphql/mutations/releases/base.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Mutations + module Releases + class Base < BaseMutation + include ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Full path of the project the release is associated with' + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb new file mode 100644 index 00000000000..57c1541c368 --- /dev/null +++ b/app/graphql/mutations/releases/create.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Mutations + module Releases + class Create < Base + graphql_name 'ReleaseCreate' + + field :release, + Types::ReleaseType, + null: true, + description: 'The release after mutation' + + argument :tag_name, GraphQL::STRING_TYPE, + required: true, as: :tag, + description: 'Name of the tag to associate with the release' + + argument :ref, GraphQL::STRING_TYPE, + required: false, + description: 'The commit SHA or branch name to use if creating a new tag' + + argument :name, GraphQL::STRING_TYPE, + required: false, + description: 'Name of the release' + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: 'Description (also known as "release notes") of the release' + + argument :released_at, Types::TimeType, + required: false, + description: 'The date when the release will be/was ready. Defaults to the current time.' + + argument :milestones, [GraphQL::STRING_TYPE], + required: false, + description: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.' + + argument :assets, Types::ReleaseAssetsInputType, + required: false, + description: 'Assets associated to the release' + + authorize :create_release + + def resolve(project_path:, milestones: nil, assets: nil, **scalars) + project = authorized_find!(full_path: project_path) + + params = { + **scalars, + milestones: milestones.presence || [], + assets: assets.to_h + }.with_indifferent_access + + result = ::Releases::CreateService.new(project, current_user, params).execute + + if result[:status] == :success + { + release: result[:release], + errors: [] + } + else + { + release: nil, + errors: [result[:message]] + } + end + end + end + end +end diff --git a/app/graphql/mutations/snippets/destroy.rb b/app/graphql/mutations/snippets/destroy.rb index dc9a1e82575..4915d7dd77a 100644 --- a/app/graphql/mutations/snippets/destroy.rb +++ b/app/graphql/mutations/snippets/destroy.rb @@ -7,8 +7,7 @@ module Mutations ERROR_MSG = 'Error deleting the snippet' - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Snippet], required: true, description: 'The global id of the snippet to destroy' diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb index 8cfbbae7c08..d6b96c699c0 100644 --- a/app/graphql/mutations/snippets/mark_as_spam.rb +++ b/app/graphql/mutations/snippets/mark_as_spam.rb @@ -5,8 +5,7 @@ module Mutations class MarkAsSpam < Base graphql_name 'MarkAsSpamSnippet' - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Snippet], required: true, description: 'The global id of the snippet to update' diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb index 74266880806..bcaa807e4c1 100644 --- a/app/graphql/mutations/snippets/update.rb +++ b/app/graphql/mutations/snippets/update.rb @@ -7,8 +7,7 @@ module Mutations graphql_name 'UpdateSnippet' - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Snippet], required: true, description: 'The global id of the snippet to update' diff --git a/app/graphql/mutations/terraform/state/base.rb b/app/graphql/mutations/terraform/state/base.rb new file mode 100644 index 00000000000..b1721c784b1 --- /dev/null +++ b/app/graphql/mutations/terraform/state/base.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Base < BaseMutation + authorize :admin_terraform_state + + argument :id, + Types::GlobalIDType[::Terraform::State], + required: true, + description: 'Global ID of the Terraform state' + + private + + def find_object(id:) + GitlabSchema.find_by_gid(id) + end + end + end + end +end diff --git a/app/graphql/mutations/terraform/state/delete.rb b/app/graphql/mutations/terraform/state/delete.rb new file mode 100644 index 00000000000..f08219cb395 --- /dev/null +++ b/app/graphql/mutations/terraform/state/delete.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Delete < Base + graphql_name 'TerraformStateDelete' + + def resolve(id:) + state = authorized_find!(id: id) + state.destroy + + { errors: errors_on_object(state) } + end + end + end + end +end diff --git a/app/graphql/mutations/terraform/state/lock.rb b/app/graphql/mutations/terraform/state/lock.rb new file mode 100644 index 00000000000..d22c8de2560 --- /dev/null +++ b/app/graphql/mutations/terraform/state/lock.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Lock < Base + graphql_name 'TerraformStateLock' + + def resolve(id:) + state = authorized_find!(id: id) + + if state.locked? + state.errors.add(:base, 'state is already locked') + else + state.update(lock_xid: lock_xid, locked_by_user: current_user, locked_at: Time.current) + end + + { errors: errors_on_object(state) } + end + + private + + def lock_xid + SecureRandom.uuid + end + end + end + end +end diff --git a/app/graphql/mutations/terraform/state/unlock.rb b/app/graphql/mutations/terraform/state/unlock.rb new file mode 100644 index 00000000000..0818dbd7fb3 --- /dev/null +++ b/app/graphql/mutations/terraform/state/unlock.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Unlock < Base + graphql_name 'TerraformStateUnlock' + + def resolve(id:) + state = authorized_find!(id: id) + state.update(lock_xid: nil, locked_by_user: nil, locked_at: nil) + + { errors: errors_on_object(state) } + end + end + end + end +end diff --git a/app/graphql/mutations/todos/base.rb b/app/graphql/mutations/todos/base.rb index 6db863796bc..4dab3bbc3f4 100644 --- a/app/graphql/mutations/todos/base.rb +++ b/app/graphql/mutations/todos/base.rb @@ -11,16 +11,6 @@ module Mutations id = ::Types::GlobalIDType[::Todo].coerce_isolated_input(id) GitlabSchema.find_by_gid(id) end - - def map_to_global_ids(ids) - return [] if ids.blank? - - ids.map { |id| to_global_id(id) } - end - - def to_global_id(id) - Gitlab::GlobalId.as_global_id(id, model_name: Todo.name).to_s - end end end end diff --git a/app/graphql/mutations/todos/create.rb b/app/graphql/mutations/todos/create.rb new file mode 100644 index 00000000000..53c88696fdd --- /dev/null +++ b/app/graphql/mutations/todos/create.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mutations + module Todos + class Create < ::Mutations::Todos::Base + graphql_name 'TodoCreate' + + authorize :create_todo + + argument :target_id, + Types::GlobalIDType[Todoable], + required: true, + description: "The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported" + + field :todo, Types::TodoType, + null: true, + description: 'The to-do created' + + def resolve(target_id:) + id = ::Types::GlobalIDType[Todoable].coerce_isolated_input(target_id) + target = authorized_find!(id) + + todo = TodoService.new.mark_todo(target, current_user)&.first + errors = errors_on_object(todo) if todo + + { + todo: todo, + errors: errors + } + end + + private + + def find_object(id) + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb index 8b53658ddd5..97bbbeeaa2f 100644 --- a/app/graphql/mutations/todos/mark_all_done.rb +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -8,7 +8,7 @@ module Mutations authorize :update_user field :updated_ids, - [GraphQL::ID_TYPE], + [::Types::GlobalIDType[::Todo]], null: false, deprecated: { reason: 'Use todos', milestone: '13.2' }, description: 'Ids of the updated todos' @@ -23,7 +23,7 @@ module Mutations updated_ids = mark_all_todos_done { - updated_ids: map_to_global_ids(updated_ids), + updated_ids: updated_ids, todos: Todo.id_in(updated_ids), errors: [] } diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index ea5f5414134..9e0a95c48ec 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -12,7 +12,7 @@ module Mutations required: true, description: 'The global ids of the todos to restore (a maximum of 50 is supported at once)' - field :updated_ids, [GraphQL::ID_TYPE], + field :updated_ids, [::Types::GlobalIDType[Todo]], null: false, description: 'The ids of the updated todo items', deprecated: { reason: 'Use todos', milestone: '13.2' } @@ -28,7 +28,7 @@ module Mutations updated_ids = restore(todos) { - updated_ids: gids_of(updated_ids), + updated_ids: updated_ids, todos: Todo.id_in(updated_ids), errors: errors_on_objects(todos) } @@ -36,10 +36,6 @@ module Mutations private - def gids_of(ids) - ids.map { |id| Gitlab::GlobalId.as_global_id(id, model_name: Todo.name).to_s } - end - def model_ids_of(ids) ids.map do |gid| # TODO: remove this line when the compatibility layer is removed |