diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 18:42:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 18:42:06 +0000 |
commit | 6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch) | |
tree | 78be5963ec075d80116a932011d695dd33910b4e /app/graphql | |
parent | 1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff) | |
download | gitlab-ce-6e4e1050d9dba2b7b2523fdd1768823ab85feef4.tar.gz |
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'app/graphql')
71 files changed, 1050 insertions, 367 deletions
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 592167a633b..d8967da9f57 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -48,6 +48,13 @@ class GitlabSchema < GraphQL::Schema super(query_str, **kwargs) end + def get_type(type_name) + # This is a backwards compatibility hack to work around an accidentally + # released argument typed as EEIterationID + type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID') + super(type_name) + end + def id_from_object(object, _type = nil, _ctx = nil) unless object.respond_to?(:to_global_id) # This is an error in our schema and needs to be solved. So raise a @@ -77,6 +84,8 @@ class GitlabSchema < GraphQL::Schema # will be called. # * All other classes will use `GlobalID#find` def find_by_gid(gid) + return unless gid + if gid.model_class < ApplicationRecord Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find elsif gid.model_class.respond_to?(:lazy_find) @@ -142,6 +151,13 @@ class GitlabSchema < GraphQL::Schema end end end + + # This is a backwards compatibility hack to work around an accidentally + # released argument typed as EE{Type}ID + def get_type(type_name) + type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID') + super(type_name) + end end GitlabSchema.prepend_if_ee('EE::GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule 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 diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb new file mode 100644 index 00000000000..a7cc367379d --- /dev/null +++ b/app/graphql/resolvers/board_list_issues_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + class BoardListIssuesResolver < BaseResolver + type Types::IssueType, null: true + + alias_method :list, :object + + def resolve(**args) + service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], { board_id: list.board.id, id: list.id }) + Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute) + end + + # https://gitlab.com/gitlab-org/gitlab/-/issues/235681 + def self.complexity_multiplier(args) + 0.005 + end + end +end diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb index f8d62ba86af..b1d43934f24 100644 --- a/app/graphql/resolvers/board_lists_resolver.rb +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -6,12 +6,16 @@ module Resolvers type Types::BoardListType, null: true + argument :id, GraphQL::ID_TYPE, + required: false, + description: 'Find a list by its global ID' + alias_method :board, :object - def resolve(lookahead: nil) + def resolve(lookahead: nil, id: nil) authorize!(board) - lists = board_lists + lists = board_lists(id) if load_preferences?(lookahead) List.preload_preferences_for_user(lists, context[:current_user]) @@ -22,8 +26,13 @@ module Resolvers private - def board_lists - service = Boards::Lists::ListService.new(board.resource_parent, context[:current_user]) + def board_lists(id) + service = Boards::Lists::ListService.new( + board.resource_parent, + context[:current_user], + list_id: extract_list_id(id) + ) + service.execute(board, create_default_lists: false) end @@ -34,5 +43,11 @@ module Resolvers def load_preferences?(lookahead) lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed) end + + def extract_list_id(gid) + return unless gid.present? + + GitlabSchema.parse_gid(gid, expected_type: ::List).model_id + end end end diff --git a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb new file mode 100644 index 00000000000..f9817d8b97b --- /dev/null +++ b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class PipelineStagesResolver < BaseResolver + include LooksAhead + + alias_method :pipeline, :object + + def resolve_with_lookahead + apply_lookahead(pipeline.stages) + end + + def preloads + { + statuses: [:needs] + } + end + end + end +end diff --git a/app/graphql/resolvers/ci_configuration/sast_resolver.rb b/app/graphql/resolvers/ci_configuration/sast_resolver.rb deleted file mode 100644 index e8c42076ea2..00000000000 --- a/app/graphql/resolvers/ci_configuration/sast_resolver.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require "json" - -module Resolvers - module CiConfiguration - class SastResolver < BaseResolver - SAST_UI_SCHEMA_PATH = 'app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json' - - type ::Types::CiConfiguration::Sast::Type, null: true - - def resolve(**args) - Gitlab::Json.parse(File.read(Rails.root.join(SAST_UI_SCHEMA_PATH))) - end - end - end -end diff --git a/app/graphql/resolvers/concerns/issue_resolver_fields.rb b/app/graphql/resolvers/concerns/issue_resolver_fields.rb new file mode 100644 index 00000000000..bf2f510dd89 --- /dev/null +++ b/app/graphql/resolvers/concerns/issue_resolver_fields.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module IssueResolverFields + extend ActiveSupport::Concern + + prepended do + argument :iid, GraphQL::STRING_TYPE, + required: false, + description: 'IID of the issue. For example, "1"' + argument :iids, [GraphQL::STRING_TYPE], + required: false, + description: 'List of IIDs of issues. For example, [1, 2]' + argument :label_name, GraphQL::STRING_TYPE.to_list_type, + required: false, + description: 'Labels applied to this issue' + argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, + required: false, + description: 'Milestone applied to this issue' + argument :assignee_username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of a user assigned to the issue' + argument :assignee_id, GraphQL::STRING_TYPE, + required: false, + description: 'ID of a user assigned to the issues, "none" and "any" values supported' + argument :created_before, Types::TimeType, + required: false, + description: 'Issues created before this date' + argument :created_after, Types::TimeType, + required: false, + description: 'Issues created after this date' + argument :updated_before, Types::TimeType, + required: false, + description: 'Issues updated before this date' + argument :updated_after, Types::TimeType, + required: false, + description: 'Issues updated after this date' + argument :closed_before, Types::TimeType, + required: false, + description: 'Issues closed before this date' + argument :closed_after, Types::TimeType, + required: false, + description: 'Issues closed after this date' + argument :search, GraphQL::STRING_TYPE, + required: false, + description: 'Search query for issue title or description' + argument :types, [Types::IssueTypeEnum], + as: :issue_types, + description: 'Filter issues by the given issue types', + required: false + end + + def resolve(**args) + # The project could have been loaded in batch by `BatchLoader`. + # At this point we need the `id` of the project to query for issues, so + # make sure it's loaded and not `nil` before continuing. + parent = object.respond_to?(:sync) ? object.sync : object + return Issue.none if parent.nil? + + # Will need to be made group & namespace aware with + # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 + args[:iids] ||= [args.delete(:iid)].compact if args[:iid] + args[:attempt_project_search_optimizations] = true if args[:search].present? + + finder = IssuesFinder.new(current_user, args) + + continue_issue_resolve(parent, finder, **args) + end + + class_methods do + def resolver_complexity(args, child_complexity:) + complexity = super + complexity += 2 if args[:labelName] + + complexity + end + end +end diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index 7ed88be52b9..0c01efd4f9a 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -38,6 +38,9 @@ module ResolvesMergeRequests assignees: [:assignees], labels: [:labels], author: [:author], + merged_at: [:metrics], + commit_count: [:metrics], + approved_by: [:approver_users], milestone: [:milestone], head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }] } diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb index 81f94d5cb30..955ea6304e0 100644 --- a/app/graphql/resolvers/design_management/designs_resolver.rb +++ b/app/graphql/resolvers/design_management/designs_resolver.rb @@ -27,19 +27,20 @@ module Resolvers current_user, ids: design_ids(ids), filenames: filenames, - visible_at_version: version(at_version), - order: :id + visible_at_version: version(at_version) ).execute end private def version(at_version) - GitlabSchema.object_from_id(at_version)&.sync if at_version + return unless at_version + + GitlabSchema.object_from_id(at_version, expected_type: ::DesignManagement::Version)&.sync end def design_ids(ids) - ids&.map { |id| GlobalID.parse(id).model_id } + ids&.map { |id| GlobalID.parse(id, expected_type: ::DesignManagement::Design).model_id } end def issue diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb new file mode 100644 index 00000000000..ac51011eea8 --- /dev/null +++ b/app/graphql/resolvers/group_issues_resolver.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Resolvers + class GroupIssuesResolver < IssuesResolver + argument :include_subgroups, GraphQL::BOOLEAN_TYPE, + required: false, + default_value: false, + description: 'Include issues belonging to subgroups.' + end +end diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb new file mode 100644 index 00000000000..8d34cea4fa1 --- /dev/null +++ b/app/graphql/resolvers/group_milestones_resolver.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Resolvers + class GroupMilestonesResolver < MilestonesResolver + argument :include_descendants, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Also return milestones in all subgroups and subprojects' + + private + + def parent_id_parameters(args) + return { group_ids: parent.id } unless args[:include_descendants].present? + + { + group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id), + project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user) + } + end + + def group_projects + GroupProjectsFinder.new( + group: parent, + current_user: current_user, + options: { include_subgroups: true } + ).execute + end + end +end diff --git a/app/graphql/resolvers/issue_status_counts_resolver.rb b/app/graphql/resolvers/issue_status_counts_resolver.rb new file mode 100644 index 00000000000..466ca538467 --- /dev/null +++ b/app/graphql/resolvers/issue_status_counts_resolver.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Resolvers + class IssueStatusCountsResolver < BaseResolver + prepend IssueResolverFields + + type Types::IssueStatusCountsType, null: true + + def continue_issue_resolve(parent, finder, **args) + Gitlab::IssuablesCountForState.new(finder, parent) + end + end +end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 9d0535a208f..e2874f6643c 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -2,49 +2,11 @@ module Resolvers class IssuesResolver < BaseResolver - argument :iid, GraphQL::STRING_TYPE, - required: false, - description: 'IID of the issue. For example, "1"' + prepend IssueResolverFields - argument :iids, [GraphQL::STRING_TYPE], - required: false, - description: 'List of IIDs of issues. For example, [1, 2]' argument :state, Types::IssuableStateEnum, required: false, description: 'Current state of this issue' - argument :label_name, GraphQL::STRING_TYPE.to_list_type, - required: false, - description: 'Labels applied to this issue' - argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, - required: false, - description: 'Milestones applied to this issue' - argument :assignee_username, GraphQL::STRING_TYPE, - required: false, - description: 'Username of a user assigned to the issues' - argument :assignee_id, GraphQL::STRING_TYPE, - required: false, - description: 'ID of a user assigned to the issues, "none" and "any" values supported' - argument :created_before, Types::TimeType, - required: false, - description: 'Issues created before this date' - argument :created_after, Types::TimeType, - required: false, - description: 'Issues created after this date' - argument :updated_before, Types::TimeType, - required: false, - description: 'Issues updated before this date' - argument :updated_after, Types::TimeType, - required: false, - description: 'Issues updated after this date' - argument :closed_before, Types::TimeType, - required: false, - description: 'Issues closed before this date' - argument :closed_after, Types::TimeType, - required: false, - description: 'Issues closed after this date' - argument :search, GraphQL::STRING_TYPE, - required: false, - description: 'Search query for issue title or description' argument :sort, Types::IssueSortEnum, description: 'Sort issues by this criteria', required: false, @@ -56,19 +18,7 @@ module Resolvers label_priority_asc label_priority_desc milestone_due_asc milestone_due_desc].freeze - def resolve(**args) - # The project could have been loaded in batch by `BatchLoader`. - # At this point we need the `id` of the project to query for issues, so - # make sure it's loaded and not `nil` before continuing. - parent = object.respond_to?(:sync) ? object.sync : object - return Issue.none if parent.nil? - - # Will need to be be made group & namespace aware with - # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 - args[:iids] ||= [args.delete(:iid)].compact if args[:iid] - args[:attempt_project_search_optimizations] = true if args[:search].present? - - finder = IssuesFinder.new(current_user, args) + def continue_issue_resolve(parent, finder, **args) issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all if non_stable_cursor_sort?(args[:sort]) @@ -80,13 +30,6 @@ module Resolvers end end - def self.resolver_complexity(args, child_complexity:) - complexity = super - complexity += 2 if args[:labelName] - - complexity - end - def non_stable_cursor_sort?(sort) NON_STABLE_CURSOR_SORTS.include?(sort) end diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index 3aa52341eec..d15a1ede6fe 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -28,6 +28,12 @@ module Resolvers required: false, as: :label_name, description: 'Array of label names. All resolved merge requests will have all of these labels.' + argument :merged_after, Types::TimeType, + required: false, + description: 'Merge requests merged after this date' + argument :merged_before, Types::TimeType, + required: false, + description: 'Merge requests merged before this date' def self.single ::Resolvers::MergeRequestResolver diff --git a/app/graphql/resolvers/milestone_resolver.rb b/app/graphql/resolvers/milestone_resolver.rb deleted file mode 100644 index bcfbc63c31f..00000000000 --- a/app/graphql/resolvers/milestone_resolver.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - class MilestoneResolver < BaseResolver - include Gitlab::Graphql::Authorize::AuthorizeResource - include TimeFrameArguments - - argument :state, Types::MilestoneStateEnum, - required: false, - description: 'Filter milestones by state' - - argument :include_descendants, GraphQL::BOOLEAN_TYPE, - required: false, - description: 'Return also milestones in all subgroups and subprojects' - - type Types::MilestoneType, null: true - - def resolve(**args) - validate_timeframe_params!(args) - - authorize! - - MilestonesFinder.new(milestones_finder_params(args)).execute - end - - private - - def milestones_finder_params(args) - { - state: args[:state] || 'all', - start_date: args[:start_date], - end_date: args[:end_date] - }.merge(parent_id_parameter(args)) - end - - def parent - @parent ||= object.respond_to?(:sync) ? object.sync : object - end - - def parent_id_parameter(args) - if parent.is_a?(Group) - group_parameters(args) - elsif parent.is_a?(Project) - { project_ids: parent.id } - end - end - - # MilestonesFinder does not check for current_user permissions, - # so for now we need to keep it here. - def authorize! - Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error! - end - - def group_parameters(args) - return { group_ids: parent.id } unless args[:include_descendants].present? - - { - group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id), - project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user) - } - end - - def group_projects - GroupProjectsFinder.new( - group: parent, - current_user: current_user, - options: { include_subgroups: true } - ).execute - end - end -end diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb new file mode 100644 index 00000000000..5f80506c01b --- /dev/null +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Resolvers + class MilestonesResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include TimeFrameArguments + + argument :ids, [GraphQL::ID_TYPE], + required: false, + description: 'Array of global milestone IDs, e.g., "gid://gitlab/Milestone/1"' + + argument :state, Types::MilestoneStateEnum, + required: false, + description: 'Filter milestones by state' + + type Types::MilestoneType, null: true + + def resolve(**args) + validate_timeframe_params!(args) + + authorize! + + MilestonesFinder.new(milestones_finder_params(args)).execute + end + + private + + def milestones_finder_params(args) + { + ids: parse_gids(args[:ids]), + state: args[:state] || 'all', + start_date: args[:start_date], + end_date: args[:end_date] + }.merge(parent_id_parameters(args)) + end + + def parent + synchronized_object + end + + def parent_id_parameters(args) + raise NotImplementedError + end + + # MilestonesFinder does not check for current_user permissions, + # so for now we need to keep it here. + def authorize! + Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error! + end + + def parse_gids(gids) + gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: Milestone).model_id } + end + end +end diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb new file mode 100644 index 00000000000..976fc300b87 --- /dev/null +++ b/app/graphql/resolvers/project_milestones_resolver.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Resolvers + class ProjectMilestonesResolver < MilestonesResolver + argument :include_ancestors, GraphQL::BOOLEAN_TYPE, + required: false, + description: "Also return milestones in the project's parent group and its ancestors" + + private + + def parent_id_parameters(args) + return { project_ids: parent.id } unless args[:include_ancestors].present? && parent.group.present? + + { + group_ids: parent.group.self_and_ancestors.select(:id), + project_ids: parent.id + } + end + end +end diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb index 5bafe3dd140..181c1e77109 100644 --- a/app/graphql/resolvers/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -10,7 +10,7 @@ module Resolvers def resolve(iid:) BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader, args| - args[:key].ci_pipelines.for_iid(iids).each { |pl| loader.call(pl.iid.to_s, pl) } + args[:key].all_pipelines.for_iid(iids).each { |pl| loader.call(pl.iid.to_s, pl) } end end end diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb index 2dc712128cc..ed382ac82d0 100644 --- a/app/graphql/resolvers/projects/jira_projects_resolver.rb +++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb @@ -16,7 +16,14 @@ module Resolvers response = jira_projects(name: name) if response.success? - response.payload[:projects] + projects_array = response.payload[:projects] + + GraphQL::Pagination::ArrayConnection.new( + projects_array, + # override default max_page_size to whatever the size of the response is, + # see https://gitlab.com/gitlab-org/gitlab/-/issues/231394 + args.merge({ max_page_size: projects_array.size }) + ) else raise Gitlab::Graphql::Errors::BaseError, response.message end diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb index cff65321dc0..bd5f8f274cd 100644 --- a/app/graphql/resolvers/todo_resolver.rb +++ b/app/graphql/resolvers/todo_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class TodoResolver < BaseResolver type Types::TodoType, null: true - alias_method :user, :object + alias_method :target, :object argument :action, [Types::TodoActionEnum], required: false, @@ -31,9 +31,10 @@ module Resolvers description: 'The type of the todo' def resolve(**args) - return Todo.none if user != context[:current_user] + return Todo.none unless current_user.present? && target.present? + return Todo.none if target.is_a?(User) && target != current_user - TodosFinder.new(user, todo_finder_params(args)).execute + TodosFinder.new(current_user, todo_finder_params(args)).execute end private @@ -46,6 +47,15 @@ module Resolvers author_id: args[:author_id], action_id: args[:action], project_id: args[:project_id] + }.merge(target_params) + end + + def target_params + return {} unless TodosFinder::TODO_TYPES.include?(target.class.name) + + { + type: target.class.name, + target_id: target.id } end end diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index 089d2426158..1a0b0685ffe 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -71,7 +71,7 @@ module Types description: 'Number of events of this alert', method: :events - field :details, + field :details, # rubocop:disable Graphql/JSONType GraphQL::Types::JSON, null: true, description: 'Alert details' @@ -94,8 +94,28 @@ module Types field :metrics_dashboard_url, GraphQL::STRING_TYPE, null: true, - description: 'URL for metrics embed for the alert', - resolve: -> (alert, _args, _context) { alert.present.metrics_dashboard_url } + description: 'URL for metrics embed for the alert' + + field :runbook, + GraphQL::STRING_TYPE, + null: true, + description: 'Runbook for the alert as defined in alert details' + + field :todos, + Types::TodoType.connection_type, + null: true, + description: 'Todos of the current user for the alert', + resolver: Resolvers::TodoResolver + + field :details_url, + GraphQL::STRING_TYPE, + null: false, + description: 'The URL of the alert detail page' + + field :prometheus_alert, + Types::PrometheusAlertType, + null: true, + description: 'The alert condition for Prometheus' def notes object.ordered_notes diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb index e94ff898807..70c0794fc90 100644 --- a/app/graphql/types/board_list_type.rb +++ b/app/graphql/types/board_list_type.rb @@ -3,6 +3,8 @@ module Types # rubocop: disable Graphql/AuthorizeTypes class BoardListType < BaseObject + include Gitlab::Utils::StrongMemoize + graphql_name 'BoardList' description 'Represents a list for an issue board' @@ -19,6 +21,31 @@ module Types field :collapsed, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if list is collapsed for this user', resolve: -> (list, _args, ctx) { list.collapsed?(ctx[:current_user]) } + field :issues_count, GraphQL::INT_TYPE, null: true, + description: 'Count of issues in the list' + + field :issues, ::Types::IssueType.connection_type, null: true, + description: 'Board issues', + resolver: ::Resolvers::BoardListIssuesResolver + + def issues_count + metadata[:size] + end + + def total_weight + metadata[:total_weight] + end + + def metadata + strong_memoize(:metadata) do + list = self.object + user = context[:current_user] + + Boards::Issues::ListService + .new(list.board.resource_parent, user, board_id: list.board_id, id: list.id) + .metadata + end + end end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/ci/group_type.rb b/app/graphql/types/ci/group_type.rb new file mode 100644 index 00000000000..04c0eb93068 --- /dev/null +++ b/app/graphql/types/ci/group_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class GroupType < BaseObject + graphql_name 'CiGroup' + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the job group' + field :size, GraphQL::INT_TYPE, null: true, + description: 'Size of the group' + field :jobs, Ci::JobType.connection_type, null: true, + description: 'Jobs in group' + end + end +end diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb new file mode 100644 index 00000000000..4c18f3ffd52 --- /dev/null +++ b/app/graphql/types/ci/job_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class JobType < BaseObject + graphql_name 'CiJob' + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the job' + field :needs, JobType.connection_type, null: true, + description: 'Builds that must complete before the jobs run' + end + end +end diff --git a/app/graphql/types/ci/pipeline_config_source_enum.rb b/app/graphql/types/ci/pipeline_config_source_enum.rb new file mode 100644 index 00000000000..48f88c133b4 --- /dev/null +++ b/app/graphql/types/ci/pipeline_config_source_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module Ci + class PipelineConfigSourceEnum < BaseEnum + ::Ci::PipelineEnums.config_sources.keys.each do |state_symbol| + value state_symbol.to_s.upcase, value: state_symbol.to_s + end + end + end +end diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 32050766e5b..82a9f8495ce 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -5,6 +5,8 @@ module Types class PipelineType < BaseObject graphql_name 'Pipeline' + connection_type_class(Types::CountableConnectionType) + authorize :read_pipeline expose_permissions Types::PermissionTypes::Ci::Pipeline @@ -23,6 +25,8 @@ module Types field :detailed_status, Types::Ci::DetailedStatusType, null: false, description: 'Detailed status of the pipeline', resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } + field :config_source, PipelineConfigSourceEnum, null: true, + description: "Config source of the pipeline (#{::Ci::PipelineEnums.config_sources.keys.join(', ').upcase})" field :duration, GraphQL::INT_TYPE, null: true, description: 'Duration of the pipeline in seconds' field :coverage, GraphQL::FLOAT_TYPE, null: true, @@ -37,8 +41,13 @@ module Types description: "Timestamp of the pipeline's completion" field :committed_at, Types::TimeType, null: true, description: "Timestamp of the pipeline's commit" - - # TODO: Add triggering user as a type + field :stages, Types::Ci::StageType.connection_type, null: true, + description: 'Stages of the pipeline', + extras: [:lookahead], + resolver: Resolvers::Ci::PipelineStagesResolver + field :user, Types::UserType, null: true, + description: 'Pipeline user', + resolve: -> (pipeline, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, pipeline.user_id).find } end end end diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb new file mode 100644 index 00000000000..278c4d4d748 --- /dev/null +++ b/app/graphql/types/ci/stage_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class StageType < BaseObject + graphql_name 'CiStage' + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the stage' + field :groups, Ci::GroupType.connection_type, null: true, + description: 'Group of jobs for the stage' + end + end +end diff --git a/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb b/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb deleted file mode 100644 index ccd1c7dd0eb..00000000000 --- a/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Types - module CiConfiguration - module Sast - # rubocop: disable Graphql/AuthorizeTypes - class AnalyzersEntityType < BaseObject - graphql_name 'SastCiConfigurationAnalyzersEntity' - description 'Represents an analyzer entity in SAST CI configuration' - - field :name, GraphQL::STRING_TYPE, null: true, - description: 'Name of the analyzer.' - - field :label, GraphQL::STRING_TYPE, null: true, - description: 'Analyzer label used in the config UI.' - - field :enabled, GraphQL::BOOLEAN_TYPE, null: true, - description: 'Indicates whether an analyzer is enabled.' - - field :description, GraphQL::STRING_TYPE, null: true, - description: 'Analyzer description that is displayed on the form.' - end - end - end -end diff --git a/app/graphql/types/ci_configuration/sast/entity_type.rb b/app/graphql/types/ci_configuration/sast/entity_type.rb deleted file mode 100644 index b61b582ad20..00000000000 --- a/app/graphql/types/ci_configuration/sast/entity_type.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Types - module CiConfiguration - module Sast - # rubocop: disable Graphql/AuthorizeTypes - class EntityType < BaseObject - graphql_name 'SastCiConfigurationEntity' - description 'Represents an entity in SAST CI configuration' - - field :field, GraphQL::STRING_TYPE, null: true, - description: 'CI keyword of entity.' - - field :label, GraphQL::STRING_TYPE, null: true, - description: 'Label for entity used in the form.' - - field :type, GraphQL::STRING_TYPE, null: true, - description: 'Type of the field value.' - - field :options, ::Types::CiConfiguration::Sast::OptionsEntityType.connection_type, null: true, - description: 'Different possible values of the field.' - - field :default_value, GraphQL::STRING_TYPE, null: true, - description: 'Default value that is used if value is empty.' - - field :description, GraphQL::STRING_TYPE, null: true, - description: 'Entity description that is displayed on the form.' - - field :value, GraphQL::STRING_TYPE, null: true, - description: 'Current value of the entity.' - end - end - end -end diff --git a/app/graphql/types/ci_configuration/sast/options_entity_type.rb b/app/graphql/types/ci_configuration/sast/options_entity_type.rb deleted file mode 100644 index 86d104a7fda..00000000000 --- a/app/graphql/types/ci_configuration/sast/options_entity_type.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Types - module CiConfiguration - module Sast - # rubocop: disable Graphql/AuthorizeTypes - class OptionsEntityType < BaseObject - graphql_name 'SastCiConfigurationOptionsEntity' - description 'Represents an entity for options in SAST CI configuration' - - field :label, GraphQL::STRING_TYPE, null: true, - description: 'Label of option entity.' - - field :value, GraphQL::STRING_TYPE, null: true, - description: 'Value of option entity.' - end - end - end -end diff --git a/app/graphql/types/ci_configuration/sast/type.rb b/app/graphql/types/ci_configuration/sast/type.rb deleted file mode 100644 index 35d11584ac7..00000000000 --- a/app/graphql/types/ci_configuration/sast/type.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Types - module CiConfiguration - module Sast - # rubocop: disable Graphql/AuthorizeTypes - class Type < BaseObject - graphql_name 'SastCiConfiguration' - description 'Represents a CI configuration of SAST' - - field :global, ::Types::CiConfiguration::Sast::EntityType.connection_type, null: true, - description: 'List of global entities related to SAST configuration.' - - field :pipeline, ::Types::CiConfiguration::Sast::EntityType.connection_type, null: true, - description: 'List of pipeline entities related to SAST configuration.' - - field :analyzers, ::Types::CiConfiguration::Sast::AnalyzersEntityType.connection_type, null: true, - description: 'List of analyzers entities attached to SAST configuration.' - end - end - end -end diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb index be5165da545..dd4b4c3b114 100644 --- a/app/graphql/types/commit_type.rb +++ b/app/graphql/types/commit_type.rb @@ -17,12 +17,15 @@ module Types markdown_field :title_html, null: true field :description, type: GraphQL::STRING_TYPE, null: true, description: 'Description of the commit message' + markdown_field :description_html, null: true field :message, type: GraphQL::STRING_TYPE, null: true, description: 'Raw commit message' field :authored_date, type: Types::TimeType, null: true, description: 'Timestamp of when the commit was authored' field :web_url, type: GraphQL::STRING_TYPE, null: false, description: 'Web URL of the commit' + field :web_path, type: GraphQL::STRING_TYPE, null: false, + description: 'Web path of the commit' field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true, description: 'Rendered HTML of the commit signature' field :author_name, type: GraphQL::STRING_TYPE, null: true, diff --git a/app/graphql/types/countable_connection_type.rb b/app/graphql/types/countable_connection_type.rb new file mode 100644 index 00000000000..2538366b786 --- /dev/null +++ b/app/graphql/types/countable_connection_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class CountableConnectionType < GraphQL::Types::Relay::BaseConnection + field :count, Integer, null: false, + description: 'Total count of collection' + + def count + # rubocop: disable CodeReuse/ActiveRecord + relation = object.items + + # sometimes relation is an Array + relation = relation.reorder(nil) if relation.respond_to?(:reorder) + # rubocop: enable CodeReuse/ActiveRecord + + if relation.try(:group_values)&.present? + relation.size.keys.size + else + relation.size + end + end + end +end diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 34a90006d03..239b26f9c38 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -19,5 +19,10 @@ module Types field :metrics_dashboard, Types::Metrics::DashboardType, null: true, description: 'Metrics dashboard schema for the environment', resolver: Resolvers::Metrics::DashboardResolver + + field :latest_opened_most_severe_alert, + Types::AlertManagement::AlertType, + null: true, + description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.' end end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index fd7d9a9ba3d..cc8cd7c01f9 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -47,11 +47,11 @@ module Types Types::IssueType.connection_type, null: true, description: 'Issues of the group', - resolver: Resolvers::IssuesResolver + resolver: Resolvers::GroupIssuesResolver field :milestones, Types::MilestoneType.connection_type, null: true, - description: 'Find milestones', - resolver: Resolvers::MilestoneResolver + description: 'Milestones of the group', + resolver: Resolvers::GroupMilestonesResolver field :boards, Types::BoardType.connection_type, diff --git a/app/graphql/types/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb index f2f6d6c6cab..543b7f8e5b2 100644 --- a/app/graphql/types/issuable_state_enum.rb +++ b/app/graphql/types/issuable_state_enum.rb @@ -8,5 +8,6 @@ module Types value 'opened' value 'closed' value 'locked' + value 'all' end end diff --git a/app/graphql/types/issue_connection_type.rb b/app/graphql/types/issue_connection_type.rb deleted file mode 100644 index beed392f01a..00000000000 --- a/app/graphql/types/issue_connection_type.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Types - # rubocop: disable Graphql/AuthorizeTypes - class IssueConnectionType < GraphQL::Types::Relay::BaseConnection - field :count, Integer, null: false, - description: 'Total count of collection' - - def count - object.items.size - end - end -end diff --git a/app/graphql/types/issue_status_counts_type.rb b/app/graphql/types/issue_status_counts_type.rb new file mode 100644 index 00000000000..f2b1ba8e655 --- /dev/null +++ b/app/graphql/types/issue_status_counts_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Types + class IssueStatusCountsType < BaseObject + graphql_name 'IssueStatusCountsType' + description "Represents total number of issues for the represented statuses." + + authorize :read_issue + + def self.available_issue_states + @available_issue_states ||= Issue.available_states.keys.push('all') + end + + ::Gitlab::IssuablesCountForState::STATES.each do |state| + next unless available_issue_states.include?(state.downcase) + + field state, + GraphQL::INT_TYPE, + null: true, + description: "Number of issues with status #{state.upcase} for the project" + end + end +end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 9baa0018999..0a73ce95424 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -4,7 +4,7 @@ module Types class IssueType < BaseObject graphql_name 'Issue' - connection_type_class(Types::IssueConnectionType) + connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) @@ -97,6 +97,10 @@ module Types field :design_collection, Types::DesignManagement::DesignCollectionType, null: true, description: 'Collection of design images associated with this issue' + + field :type, Types::IssueTypeEnum, null: true, + method: :issue_type, + description: 'Type of the issue' end end diff --git a/app/graphql/types/issue_type_enum.rb b/app/graphql/types/issue_type_enum.rb new file mode 100644 index 00000000000..7dc45f78c99 --- /dev/null +++ b/app/graphql/types/issue_type_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class IssueTypeEnum < BaseEnum + graphql_name 'IssueType' + description 'Issue type' + + ::Issue.issue_types.keys.each do |issue_type| + value issue_type.upcase, value: issue_type, description: "#{issue_type.titleize} issue type" + end + end +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index c194b467363..01b02b7976f 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -4,6 +4,8 @@ module Types class MergeRequestType < BaseObject graphql_name 'MergeRequest' + connection_type_class(Types::CountableConnectionType) + implements(Types::Notes::NoteableType) authorize :read_merge_request @@ -141,6 +143,8 @@ module Types end field :task_completion_status, Types::TaskCompletionStatus, null: false, description: Types::TaskCompletionStatus.description + field :commit_count, GraphQL::INT_TYPE, null: true, + description: 'Number of commits in the merge request' def diff_stats(path: nil) stats = Array.wrap(object.diff_stats&.to_a) @@ -160,5 +164,14 @@ module Types hash.merge!(additions: status.additions, deletions: status.deletions, file_count: 1) { |_, x, y| x + y } end end + + def commit_count + object&.metrics&.commits_count + end + + def approvers + object.approver_users + end end end +Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType') diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 49d51b626b2..e143d14676e 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -14,12 +14,17 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle + mount_mutation Mutations::Boards::Issues::IssueMoveList + mount_mutation Mutations::Boards::Lists::Create + mount_mutation Mutations::Boards::Lists::Update mount_mutation Mutations::Branches::Create, calls_gitaly: true mount_mutation Mutations::Commits::Create, calls_gitaly: true mount_mutation Mutations::Discussions::ToggleResolve + mount_mutation Mutations::Issues::SetAssignees mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetLocked mount_mutation Mutations::Issues::SetDueDate + mount_mutation Mutations::Issues::SetSubscription mount_mutation Mutations::Issues::Update mount_mutation Mutations::MergeRequests::Create mount_mutation Mutations::MergeRequests::Update @@ -55,6 +60,7 @@ module Types mount_mutation Mutations::JiraImport::ImportUsers mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true + mount_mutation Mutations::DesignManagement::Move mount_mutation Mutations::ContainerExpirationPolicies::Update end end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 2251a0f4e0c..5562db69de6 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -148,6 +148,16 @@ module Types description: 'Issues of the project', resolver: Resolvers::IssuesResolver + field :issue_status_counts, + Types::IssueStatusCountsType, + null: true, + description: 'Counts of issues by status for the project', + resolver: Resolvers::IssueStatusCountsResolver + + field :milestones, Types::MilestoneType.connection_type, null: true, + description: 'Milestones of the project', + resolver: Resolvers::ProjectMilestonesResolver + field :project_members, Types::ProjectMemberType.connection_type, description: 'Members of the project', @@ -159,9 +169,11 @@ module Types description: 'Environments of the project', resolver: Resolvers::EnvironmentsResolver - field :sast_ci_configuration, ::Types::CiConfiguration::Sast::Type, null: true, - description: 'SAST CI configuration for the project', - resolver: ::Resolvers::CiConfiguration::SastResolver + field :environment, + Types::EnvironmentType, + null: true, + description: 'A single environment of the project', + resolver: Resolvers::EnvironmentsResolver.single field :issue, Types::IssueType, diff --git a/app/graphql/types/projects/services/jira_service_type.rb b/app/graphql/types/projects/services/jira_service_type.rb index 8bf85a14cbf..cb0712249e3 100644 --- a/app/graphql/types/projects/services/jira_service_type.rb +++ b/app/graphql/types/projects/services/jira_service_type.rb @@ -13,8 +13,6 @@ module Types field :projects, Types::Projects::Services::JiraProjectType.connection_type, null: true, - connection: false, - extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension], description: 'List of all Jira projects fetched through Jira REST API', resolver: Resolvers::Projects::JiraProjectsResolver end diff --git a/app/graphql/types/prometheus_alert_type.rb b/app/graphql/types/prometheus_alert_type.rb new file mode 100644 index 00000000000..1d09a8dbeb7 --- /dev/null +++ b/app/graphql/types/prometheus_alert_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + class PrometheusAlertType < BaseObject + graphql_name 'PrometheusAlert' + description 'The alert condition for Prometheus' + + authorize :read_prometheus_alerts + + present_using PrometheusAlertPresenter + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the alert condition' + + field :humanized_text, + GraphQL::STRING_TYPE, + null: false, + description: 'The human-readable text of the alert condition' + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index b4cbd96bfdb..c04f4da70cf 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -47,6 +47,15 @@ module Types null: false, description: 'Fields related to design management' + field :milestone, ::Types::MilestoneType, + null: true, + description: 'Find a milestone', + resolve: -> (_obj, args, _ctx) { GitlabSchema.find_by_gid(args[:id]) } do + argument :id, ::Types::GlobalIDType[Milestone], + required: true, + description: 'Find a milestone by its ID' + end + field :user, Types::UserType, null: true, description: 'Find a user', diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb index 73ca3425ded..db98e62c10a 100644 --- a/app/graphql/types/snippet_type.rb +++ b/app/graphql/types/snippet_type.rb @@ -66,7 +66,8 @@ module Types field :blob, type: Types::Snippets::BlobType, description: 'Snippet blob', calls_gitaly: true, - null: false + null: false, + deprecated: { reason: 'Use `blobs`', milestone: '13.3' } field :blobs, type: [Types::Snippets::BlobType], description: 'Snippet blobs', diff --git a/app/graphql/types/snippets/file_input_action_enum.rb b/app/graphql/types/snippets/blob_action_enum.rb index 7785853f3a8..e3f89920f16 100644 --- a/app/graphql/types/snippets/file_input_action_enum.rb +++ b/app/graphql/types/snippets/blob_action_enum.rb @@ -2,9 +2,9 @@ module Types module Snippets - class FileInputActionEnum < BaseEnum - graphql_name 'SnippetFileInputActionEnum' - description 'Type of a snippet file input action' + class BlobActionEnum < BaseEnum + graphql_name 'SnippetBlobActionEnum' + description 'Type of a snippet blob input action' value 'create', value: :create value 'update', value: :update diff --git a/app/graphql/types/snippets/file_input_type.rb b/app/graphql/types/snippets/blob_action_input_type.rb index 85a02c8f493..ccb6ae3f2c1 100644 --- a/app/graphql/types/snippets/file_input_type.rb +++ b/app/graphql/types/snippets/blob_action_input_type.rb @@ -2,11 +2,11 @@ module Types module Snippets - class FileInputType < BaseInputObject # rubocop:disable Graphql/AuthorizeTypes - graphql_name 'SnippetFileInputType' + class BlobActionInputType < BaseInputObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'SnippetBlobActionInputType' description 'Represents an action to perform over a snippet file' - argument :action, Types::Snippets::FileInputActionEnum, + argument :action, Types::Snippets::BlobActionEnum, description: 'Type of input action', required: true diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb index f045a50e672..c31e4873df0 100644 --- a/app/graphql/types/time_type.rb +++ b/app/graphql/types/time_type.rb @@ -7,6 +7,8 @@ module Types def self.coerce_input(value, ctx) Time.parse(value) + rescue ArgumentError, TypeError => e + raise GraphQL::CoercionError, e.message end def self.coerce_result(value, ctx) diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb index 36cae756a0d..cc6bf7b4f00 100644 --- a/app/graphql/types/tree/blob_type.rb +++ b/app/graphql/types/tree/blob_type.rb @@ -12,6 +12,8 @@ module Types field :web_url, GraphQL::STRING_TYPE, null: true, description: 'Web URL of the blob' + field :web_path, GraphQL::STRING_TYPE, null: true, + description: 'Web path of the blob' field :lfs_oid, GraphQL::STRING_TYPE, null: true, description: 'LFS ID of the blob', resolve: -> (blob, args, ctx) do diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb index 81a7a7e66ae..aff2e025761 100644 --- a/app/graphql/types/tree/tree_entry_type.rb +++ b/app/graphql/types/tree/tree_entry_type.rb @@ -13,6 +13,8 @@ module Types field :web_url, GraphQL::STRING_TYPE, null: true, description: 'Web URL for the tree entry (directory)' + field :web_path, GraphQL::STRING_TYPE, null: true, + description: 'Web path for the tree entry (directory)' end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/user_status_type.rb b/app/graphql/types/user_status_type.rb new file mode 100644 index 00000000000..ff277c1f8e8 --- /dev/null +++ b/app/graphql/types/user_status_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class UserStatusType < BaseObject + graphql_name 'UserStatus' + + markdown_field :message_html, null: true, + description: 'HTML of the user status message' + field :message, GraphQL::STRING_TYPE, null: true, + description: 'User status message' + field :emoji, GraphQL::STRING_TYPE, null: true, + description: 'String representation of emoji' + end +end diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index ab3c84ea539..cb3575b41d1 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -18,16 +18,22 @@ module Types description: 'Human-readable name of the user' field :state, Types::UserStateEnum, null: false, description: 'State of the user' + field :email, GraphQL::STRING_TYPE, null: true, + description: 'User email' field :avatar_url, GraphQL::STRING_TYPE, null: true, description: "URL of the user's avatar" field :web_url, GraphQL::STRING_TYPE, null: false, description: 'Web URL of the user' + field :web_path, GraphQL::STRING_TYPE, null: false, + description: 'Web path of the user' field :todos, Types::TodoType.connection_type, null: false, resolver: Resolvers::TodoResolver, description: 'Todos of the user' field :group_memberships, Types::GroupMemberType.connection_type, null: true, description: 'Group memberships of the user', method: :group_members + field :status, Types::UserStatusType, null: true, + description: 'User status' field :project_memberships, Types::ProjectMemberType.connection_type, null: true, description: 'Project memberships of the user', method: :project_members |