diff options
Diffstat (limited to 'app/graphql/mutations')
18 files changed, 441 insertions, 73 deletions
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb new file mode 100644 index 00000000000..d4bf47af4cf --- /dev/null +++ b/app/graphql/mutations/boards/issues/issue_move_list.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Issues + class IssueMoveList < Mutations::Issues::Base + graphql_name 'IssueMoveList' + + argument :board_id, GraphQL::ID_TYPE, + required: true, + loads: Types::BoardType, + description: 'Global ID of the board that the issue is in' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Project the issue to mutate is in' + + argument :iid, GraphQL::STRING_TYPE, + required: true, + description: 'IID of the issue to mutate' + + argument :from_list_id, GraphQL::ID_TYPE, + required: false, + description: 'ID of the board list that the issue will be moved from' + + argument :to_list_id, GraphQL::ID_TYPE, + required: false, + description: 'ID of the board list that the issue will be moved to' + + argument :move_before_id, GraphQL::ID_TYPE, + required: false, + description: 'ID of issue before which the current issue will be positioned at' + + argument :move_after_id, GraphQL::ID_TYPE, + required: false, + description: 'ID of issue after which the current issue will be positioned at' + + def ready?(**args) + if move_arguments(args).blank? + raise Gitlab::Graphql::Errors::ArgumentError, + 'At least one of the arguments fromListId, toListId, afterId or beforeId is required' + end + + if move_list_arguments(args).one? + raise Gitlab::Graphql::Errors::ArgumentError, + 'Both fromListId and toListId must be present' + end + + super + end + + def resolve(board:, **args) + raise_resource_not_available_error! unless board + authorize_board!(board) + + issue = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args)) + + move_issue(board, issue, move_params) + + { + issue: issue.reset, + errors: issue.errors.full_messages + } + end + + private + + def move_issue(board, issue, move_params) + service = ::Boards::Issues::MoveService.new(board.resource_parent, current_user, move_params) + + service.execute(issue) + end + + def move_list_arguments(args) + args.slice(:from_list_id, :to_list_id) + end + + def move_arguments(args) + args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id) + end + + def authorize_board!(board) + return if Ability.allowed?(current_user, :read_board, board.resource_parent) + + raise_resource_not_available_error! + end + end + end + end +end diff --git a/app/graphql/mutations/boards/lists/base.rb b/app/graphql/mutations/boards/lists/base.rb new file mode 100644 index 00000000000..34b271ba3b8 --- /dev/null +++ b/app/graphql/mutations/boards/lists/base.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Lists + class Base < BaseMutation + include Mutations::ResolvesIssuable + + argument :board_id, ::Types::GlobalIDType[::Board], + required: true, + description: 'The Global ID of the issue board to mutate' + + field :list, + Types::BoardListType, + null: true, + description: 'List of the issue board' + + authorize :admin_list + + private + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Board) + end + end + end + end +end diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb new file mode 100644 index 00000000000..4f545709ee9 --- /dev/null +++ b/app/graphql/mutations/boards/lists/create.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Lists + class Create < Base + graphql_name 'BoardListCreate' + + argument :backlog, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Create the backlog list' + + argument :label_id, ::Types::GlobalIDType[::Label], + required: false, + description: 'ID of an existing label' + + def ready?(**args) + if args.slice(*mutually_exclusive_args).size != 1 + arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ') + raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required" + end + + super + end + + def resolve(**args) + board = authorized_find!(id: args[:board_id]) + params = create_list_params(args) + + authorize_list_type_resource!(board, params) + + list = create_list(board, params) + + { + list: list.valid? ? list : nil, + errors: errors_on_object(list) + } + end + + private + + def authorize_list_type_resource!(board, params) + return unless params[:label_id] + + labels = ::Labels::AvailableLabelsService.new(current_user, board.resource_parent, params) + .filter_labels_ids_in_param(:label_id) + + unless labels.present? + raise Gitlab::Graphql::Errors::ArgumentError, 'Label not found!' + end + end + + def create_list(board, params) + create_list_service = + ::Boards::Lists::CreateService.new(board.resource_parent, current_user, params) + + create_list_service.execute(board) + end + + def create_list_params(args) + params = args.slice(*mutually_exclusive_args).with_indifferent_access + params[:label_id] = GitlabSchema.parse_gid(params[:label_id]).model_id if params[:label_id] + + params + end + + def mutually_exclusive_args + [:backlog, :label_id] + end + end + end + end +end diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb new file mode 100644 index 00000000000..7efed3058b3 --- /dev/null +++ b/app/graphql/mutations/boards/lists/update.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Lists + class Update < BaseMutation + graphql_name 'UpdateBoardList' + + argument :list_id, GraphQL::ID_TYPE, + required: true, + loads: Types::BoardListType, + description: 'Global ID of the list.' + + argument :position, GraphQL::INT_TYPE, + required: false, + description: 'Position of list within the board' + + argument :collapsed, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Indicates if list is collapsed for this user' + + field :list, + Types::BoardListType, + null: true, + description: 'Mutated list' + + def resolve(list: nil, **args) + raise_resource_not_available_error! unless can_read_list?(list) + update_result = update_list(list, args) + + { + list: update_result[:list], + errors: list.errors.full_messages + } + end + + private + + def update_list(list, args) + service = ::Boards::Lists::UpdateService.new(list.board, current_user, args) + service.execute(list) + end + + def can_read_list?(list) + return false unless list.present? + + Ability.allowed?(current_user, :read_list, list.board) + end + end + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb new file mode 100644 index 00000000000..f6f4b744f4e --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/assignable.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Mutations + module Assignable + extend ActiveSupport::Concern + + included do + argument :assignee_usernames, + [GraphQL::STRING_TYPE], + required: true, + description: 'The usernames to assign to the resource. Replaces existing assignees by default.' + + argument :operation_mode, + Types::MutationOperationModeEnum, + required: false, + description: 'The operation to perform. Defaults to REPLACE.' + end + + def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace]) + resource = authorized_find!(project_path: project_path, iid: iid) + + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098') if resource.is_a?(MergeRequest) + + update_service_class.new( + resource.project, + current_user, + assignee_ids: assignee_ids(resource, assignee_usernames, operation_mode) + ).execute(resource) + + { + resource.class.name.underscore.to_sym => resource, + errors: errors_on_object(resource) + } + end + + private + + def assignee_ids(resource, usernames, operation_mode) + assignee_ids = [] + assignee_ids += resource.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode) + user_ids = UsersFinder.new(current_user, username: usernames).execute.map(&:id) + + if operation_mode == Types::MutationOperationModeEnum.enum[:remove] + assignee_ids -= user_ids + else + assignee_ids |= user_ids + end + + assignee_ids + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/resolves_subscription.rb b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb new file mode 100644 index 00000000000..e8c5d0d404d --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutations + module ResolvesSubscription + extend ActiveSupport::Concern + included do + argument :subscribed_state, + GraphQL::BOOLEAN_TYPE, + required: true, + description: 'The desired state of the subscription' + end + + def resolve(project_path:, iid:, subscribed_state:) + resource = authorized_find!(project_path: project_path, iid: iid) + project = resource.project + + resource.set_subscription(current_user, subscribed_state, project) + + { + resource.class.name.underscore.to_sym => resource, + errors: errors_on_object(resource) + } + end + end +end diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb new file mode 100644 index 00000000000..0b654447844 --- /dev/null +++ b/app/graphql/mutations/design_management/move.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Mutations + module DesignManagement + class Move < ::Mutations::BaseMutation + graphql_name "DesignManagementMove" + + DesignID = ::Types::GlobalIDType[::DesignManagement::Design] + + argument :id, DesignID, required: true, as: :current_design, + description: "ID of the design to move" + + argument :previous, DesignID, required: false, as: :previous_design, + description: "ID of the immediately preceding design" + + argument :next, DesignID, required: false, as: :next_design, + description: "ID of the immediately following design" + + field :design_collection, Types::DesignManagement::DesignCollectionType, + null: true, + description: "The current state of the collection" + + def ready(*) + raise ::Gitlab::Graphql::Errors::ResourceNotAvailable unless ::Feature.enabled?(:reorder_designs, default_enabled: true) + end + + def resolve(**args) + service = ::DesignManagement::MoveDesignsService.new(current_user, parameters(args)) + + { design_collection: service.collection, errors: service.execute.errors } + end + + private + + def parameters(**args) + args.transform_values { |id| GitlabSchema.find_by_gid(id) }.transform_values(&:sync).tap do |hash| + hash.each { |k, design| not_found(args[k]) unless current_user.can?(:read_design, design) } + end + end + + def not_found(gid) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{gid}" + end + end + end +end diff --git a/app/graphql/mutations/issues/base.rb b/app/graphql/mutations/issues/base.rb index 7c545c3eb00..529d48f3cd0 100644 --- a/app/graphql/mutations/issues/base.rb +++ b/app/graphql/mutations/issues/base.rb @@ -11,7 +11,7 @@ module Mutations argument :iid, GraphQL::STRING_TYPE, required: true, - description: "The iid of the issue to mutate" + description: "The IID of the issue to mutate" field :issue, Types::IssueType, diff --git a/app/graphql/mutations/issues/set_assignees.rb b/app/graphql/mutations/issues/set_assignees.rb new file mode 100644 index 00000000000..a4d1c755b53 --- /dev/null +++ b/app/graphql/mutations/issues/set_assignees.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class SetAssignees < Base + graphql_name 'IssueSetAssignees' + + include Assignable + + def update_service_class + ::Issues::UpdateService + end + end + end +end diff --git a/app/graphql/mutations/issues/set_subscription.rb b/app/graphql/mutations/issues/set_subscription.rb new file mode 100644 index 00000000000..a04c8f5ba2d --- /dev/null +++ b/app/graphql/mutations/issues/set_subscription.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class SetSubscription < Base + graphql_name 'IssueSetSubscription' + + include ResolvesSubscription + end + end +end diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb index 7f6d9b0f988..cc03d32731b 100644 --- a/app/graphql/mutations/issues/update.rb +++ b/app/graphql/mutations/issues/update.rb @@ -25,6 +25,27 @@ module Mutations required: false, description: copy_field_description(Types::IssueType, :confidential) + argument :locked, + GraphQL::BOOLEAN_TYPE, + as: :discussion_locked, + required: false, + description: copy_field_description(Types::IssueType, :discussion_locked) + + argument :add_label_ids, + [GraphQL::ID_TYPE], + required: false, + description: 'The IDs of labels to be added to the issue.' + + argument :remove_label_ids, + [GraphQL::ID_TYPE], + required: false, + description: 'The IDs of labels to be removed from the issue.' + + argument :milestone_id, + GraphQL::ID_TYPE, + required: false, + description: 'The ID of the milestone to be assigned, milestone will be removed if set to null.' + def resolve(project_path:, iid:, **args) issue = authorized_find!(project_path: project_path, iid: iid) project = issue.project diff --git a/app/graphql/mutations/merge_requests/create.rb b/app/graphql/mutations/merge_requests/create.rb index e210987f259..fd2cd58a5ee 100644 --- a/app/graphql/mutations/merge_requests/create.rb +++ b/app/graphql/mutations/merge_requests/create.rb @@ -27,6 +27,10 @@ module Mutations required: false, description: copy_field_description(Types::MergeRequestType, :description) + argument :labels, [GraphQL::STRING_TYPE], + required: false, + description: copy_field_description(Types::MergeRequestType, :labels) + field :merge_request, Types::MergeRequestType, null: true, @@ -34,18 +38,11 @@ module Mutations authorize :create_merge_request_from - def resolve(project_path:, title:, source_branch:, target_branch:, description: nil) + def resolve(project_path:, **attributes) project = authorized_find!(full_path: project_path) + params = attributes.merge(author_id: current_user.id) - attributes = { - title: title, - source_branch: source_branch, - target_branch: target_branch, - author_id: current_user.id, - description: description - } - - merge_request = ::MergeRequests::CreateService.new(project, current_user, attributes).execute + merge_request = ::MergeRequests::CreateService.new(project, current_user, params).execute { merge_request: merge_request.valid? ? merge_request : nil, diff --git a/app/graphql/mutations/merge_requests/set_assignees.rb b/app/graphql/mutations/merge_requests/set_assignees.rb index de244b62d0f..548c6b55a85 100644 --- a/app/graphql/mutations/merge_requests/set_assignees.rb +++ b/app/graphql/mutations/merge_requests/set_assignees.rb @@ -5,43 +5,10 @@ module Mutations class SetAssignees < Base graphql_name 'MergeRequestSetAssignees' - argument :assignee_usernames, - [GraphQL::STRING_TYPE], - required: true, - description: <<~DESC - The usernames to assign to the merge request. Replaces existing assignees by default. - DESC + include Assignable - argument :operation_mode, - Types::MutationOperationModeEnum, - required: false, - description: <<~DESC - The operation to perform. Defaults to REPLACE. - DESC - - def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace]) - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098') - - merge_request = authorized_find!(project_path: project_path, iid: iid) - project = merge_request.project - - assignee_ids = [] - assignee_ids += merge_request.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode) - user_ids = UsersFinder.new(current_user, username: assignee_usernames).execute.map(&:id) - - if operation_mode == Types::MutationOperationModeEnum.enum[:remove] - assignee_ids -= user_ids - else - assignee_ids |= user_ids - end - - ::MergeRequests::UpdateService.new(project, current_user, assignee_ids: assignee_ids) - .execute(merge_request) - - { - merge_request: merge_request, - errors: errors_on_object(merge_request) - } + def update_service_class + ::MergeRequests::UpdateService end end end diff --git a/app/graphql/mutations/merge_requests/set_subscription.rb b/app/graphql/mutations/merge_requests/set_subscription.rb index 1535481ab37..7d3c40185c9 100644 --- a/app/graphql/mutations/merge_requests/set_subscription.rb +++ b/app/graphql/mutations/merge_requests/set_subscription.rb @@ -5,22 +5,7 @@ module Mutations class SetSubscription < Base graphql_name 'MergeRequestSetSubscription' - argument :subscribed_state, - GraphQL::BOOLEAN_TYPE, - required: true, - description: 'The desired state of the subscription' - - def resolve(project_path:, iid:, subscribed_state:) - merge_request = authorized_find!(project_path: project_path, iid: iid) - project = merge_request.project - - merge_request.set_subscription(current_user, subscribed_state, project) - - { - merge_request: merge_request, - errors: errors_on_object(merge_request) - } - end + include ResolvesSubscription end end end diff --git a/app/graphql/mutations/notes/update/base.rb b/app/graphql/mutations/notes/update/base.rb index 9a53337f253..8a2a78a29ec 100644 --- a/app/graphql/mutations/notes/update/base.rb +++ b/app/graphql/mutations/notes/update/base.rb @@ -40,7 +40,7 @@ module Mutations end def note_params(_note, args) - { note: args[:body] }.compact + { note: args[:body], confidential: args[:confidential] }.compact end end end diff --git a/app/graphql/mutations/notes/update/note.rb b/app/graphql/mutations/notes/update/note.rb index 03a174fc8d9..ca97dad6ded 100644 --- a/app/graphql/mutations/notes/update/note.rb +++ b/app/graphql/mutations/notes/update/note.rb @@ -8,9 +8,14 @@ module Mutations argument :body, GraphQL::STRING_TYPE, - required: true, + required: false, description: copy_field_description(Types::Notes::NoteType, :body) + argument :confidential, + GraphQL::BOOLEAN_TYPE, + required: false, + description: 'The confidentiality flag of a note. Default is false.' + private def pre_update_checks!(note, _args) diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb index 89c21486a74..a068fd806f5 100644 --- a/app/graphql/mutations/snippets/create.rb +++ b/app/graphql/mutations/snippets/create.rb @@ -40,8 +40,8 @@ module Mutations required: false, description: 'The paths to files uploaded in the snippet description' - argument :files, [Types::Snippets::FileInputType], - description: "The snippet files to create", + argument :blob_actions, [Types::Snippets::BlobActionInputType], + description: 'Actions to perform over the snippet repository and blobs', required: false def resolve(args) @@ -85,9 +85,9 @@ module Mutations def create_params(args) args.tap do |create_args| - # We need to rename `files` into `snippet_actions` because + # We need to rename `blob_actions` into `snippet_actions` because # it's the expected key param - create_args[:snippet_actions] = create_args.delete(:files)&.map(&:to_h) + create_args[:snippet_actions] = create_args.delete(:blob_actions)&.map(&:to_h) # We need to rename `uploaded_files` into `files` because # it's the expected key param diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb index 8890158b0df..6ff632ec008 100644 --- a/app/graphql/mutations/snippets/update.rb +++ b/app/graphql/mutations/snippets/update.rb @@ -30,8 +30,8 @@ module Mutations description: 'The visibility level of the snippet', required: false - argument :files, [Types::Snippets::FileInputType], - description: 'The snippet files to update', + argument :blob_actions, [Types::Snippets::BlobActionInputType], + description: 'Actions to perform over the snippet repository and blobs', required: false def resolve(args) @@ -56,9 +56,9 @@ module Mutations def update_params(args) args.tap do |update_args| - # We need to rename `files` into `snippet_actions` because + # We need to rename `blob_actions` into `snippet_actions` because # it's the expected key param - update_args[:snippet_actions] = update_args.delete(:files)&.map(&:to_h) + update_args[:snippet_actions] = update_args.delete(:blob_actions)&.map(&:to_h) end end end |