diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /app/graphql | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) | |
download | gitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'app/graphql')
91 files changed, 1210 insertions, 226 deletions
diff --git a/app/graphql/batch_loaders/merge_request_diff_summary_batch_loader.rb b/app/graphql/batch_loaders/merge_request_diff_summary_batch_loader.rb new file mode 100644 index 00000000000..9b8737a6703 --- /dev/null +++ b/app/graphql/batch_loaders/merge_request_diff_summary_batch_loader.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module BatchLoaders + class MergeRequestDiffSummaryBatchLoader + NIL_STATS = { additions: 0, deletions: 0, file_count: 0 }.freeze + + def self.load_for(merge_request) + BatchLoader::GraphQL.for(merge_request).batch(key: :diff_stats_summary) do |merge_requests, loader, args| + Preloaders::MergeRequestDiffPreloader.new(merge_requests).preload_all + + merge_requests.each do |merge_request| + metrics = merge_request.metrics + + summary = if metrics && metrics.added_lines && metrics.removed_lines + { additions: metrics.added_lines, deletions: metrics.removed_lines, file_count: merge_request.merge_request_diff&.files_count || 0 } + elsif merge_request.diff_stats.blank? + NIL_STATS + else + merge_request.diff_stats.each_with_object(NIL_STATS.dup) do |status, summary| + summary.merge!(additions: status.additions, deletions: status.deletions, file_count: 1) { |_, x, y| x + y } + end + end + + loader.call(merge_request, summary) + end + end + end + end +end diff --git a/app/graphql/mutations/award_emojis/base.rb b/app/graphql/mutations/award_emojis/base.rb index 583744c3884..df6b883529e 100644 --- a/app/graphql/mutations/award_emojis/base.rb +++ b/app/graphql/mutations/award_emojis/base.rb @@ -6,7 +6,7 @@ module Mutations authorize :award_emoji argument :awardable_id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Awardable], required: true, description: 'The global id of the awardable resource' @@ -23,7 +23,10 @@ module Mutations private def find_object(id:) - GitlabSchema.object_from_id(id) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Awardable].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) end # Called by mutations methods after performing an authorization check diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index 577f10545b3..ac5ddc5bd4c 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -4,6 +4,7 @@ module Mutations class BaseMutation < GraphQL::Schema::RelayClassicMutation prepend Gitlab::Graphql::Authorize::AuthorizeResource prepend Gitlab::Graphql::CopyFieldDescription + prepend ::Gitlab::Graphql::GlobalIDCompatibility ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance' diff --git a/app/graphql/mutations/boards/create.rb b/app/graphql/mutations/boards/create.rb new file mode 100644 index 00000000000..e381205242e --- /dev/null +++ b/app/graphql/mutations/boards/create.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Mutations + module Boards + class Create < ::Mutations::BaseMutation + include Mutations::ResolvesGroup + include ResolvesProject + + graphql_name 'CreateBoard' + + field :board, + Types::BoardType, + 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, + description: 'The board name.' + argument :assignee_id, + GraphQL::STRING_TYPE, + required: false, + description: 'The ID of the user to be assigned to the board.' + argument :milestone_id, + GraphQL::ID_TYPE, + required: false, + description: 'The ID of the milestone to be assigned to the board.' + argument :weight, + GraphQL::BOOLEAN_TYPE, + required: false, + description: 'The weight of the board.' + argument :label_ids, + [GraphQL::ID_TYPE], + 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) + response = ::Boards::CreateService.new(board_parent, current_user, args).execute + + { + board: response.payload, + 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/destroy.rb b/app/graphql/mutations/boards/lists/destroy.rb new file mode 100644 index 00000000000..61ffae7c047 --- /dev/null +++ b/app/graphql/mutations/boards/lists/destroy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Mutations + module Boards + module Lists + class Destroy < ::Mutations::BaseMutation + graphql_name 'DestroyBoardList' + + field :list, + Types::BoardListType, + null: true, + description: 'The list after mutation.' + + argument :list_id, ::Types::GlobalIDType[::List], + required: true, + loads: Types::BoardListType, + description: 'Global ID of the list to destroy. Only label lists are accepted.' + + def resolve(list:) + raise_resource_not_available_error! unless can_admin_list?(list) + + response = ::Boards::Lists::DestroyService.new(list.board.resource_parent, current_user) + .execute(list) + + { + list: response.success? ? nil : list, + errors: response.errors + } + end + + private + + def can_admin_list?(list) + return false unless list.present? + + Ability.allowed?(current_user, :admin_list, list.board) + end + end + end + end +end diff --git a/app/graphql/mutations/ci/base.rb b/app/graphql/mutations/ci/base.rb index 09df4487a50..aaece2a3021 100644 --- a/app/graphql/mutations/ci/base.rb +++ b/app/graphql/mutations/ci/base.rb @@ -3,13 +3,18 @@ module Mutations module Ci class Base < BaseMutation - argument :id, ::Types::GlobalIDType[::Ci::Pipeline], + PipelineID = ::Types::GlobalIDType[::Ci::Pipeline] + + argument :id, PipelineID, required: true, description: 'The id of the pipeline to mutate' 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 = PipelineID.coerce_isolated_input(id) GlobalID::Locator.locate(id) end end diff --git a/app/graphql/mutations/concerns/mutations/spammable_mutation_fields.rb b/app/graphql/mutations/concerns/mutations/spammable_mutation_fields.rb new file mode 100644 index 00000000000..7aef55f8011 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/spammable_mutation_fields.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module SpammableMutationFields + extend ActiveSupport::Concern + + included do + field :spam, + GraphQL::BOOLEAN_TYPE, + null: true, + description: 'Indicates whether the operation returns a record detected as spam' + end + + def with_spam_params(&block) + request = Feature.enabled?(:snippet_spam) ? context[:request] : nil + + yield.merge({ api: true, request: request }) + end + + def with_spam_fields(spammable, &block) + { spam: spammable.spam? }.merge!(yield) + end + end +end diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb index 6126af8b68b..aed4cfec0fd 100644 --- a/app/graphql/mutations/design_management/move.rb +++ b/app/graphql/mutations/design_management/move.rb @@ -21,7 +21,7 @@ module Mutations description: "The current state of the collection" def resolve(**args) - service = ::DesignManagement::MoveDesignsService.new(current_user, parameters(args)) + service = ::DesignManagement::MoveDesignsService.new(current_user, parameters(**args)) { design_collection: service.collection, errors: service.execute.errors } end @@ -29,11 +29,18 @@ module Mutations private def parameters(**args) - args.transform_values { |id| GitlabSchema.find_by_gid(id) }.transform_values(&:sync).tap do |hash| + args.transform_values { |id| find_design(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 find_design(id) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = DesignID.coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + def not_found(gid) raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{gid}" end diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb index 41fd22c6b55..4492da74706 100644 --- a/app/graphql/mutations/discussions/toggle_resolve.rb +++ b/app/graphql/mutations/discussions/toggle_resolve.rb @@ -8,7 +8,7 @@ module Mutations description 'Toggles the resolved state of a discussion' argument :id, - GraphQL::ID_TYPE, + Types::GlobalIDType[Discussion], required: true, description: 'The global id of the discussion' @@ -54,7 +54,10 @@ module Mutations end def find_object(id:) - GitlabSchema.object_from_id(id, expected_type: ::Discussion) + # TODO: remove explicit coercion once compatibility layer has been removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = Types::GlobalIDType[Discussion].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) end def resolve!(discussion) diff --git a/app/graphql/mutations/issues/common_mutation_arguments.rb b/app/graphql/mutations/issues/common_mutation_arguments.rb new file mode 100644 index 00000000000..4b5b246281f --- /dev/null +++ b/app/graphql/mutations/issues/common_mutation_arguments.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mutations + module Issues + module CommonMutationArguments + extend ActiveSupport::Concern + + included do + argument :description, GraphQL::STRING_TYPE, + required: false, + description: copy_field_description(Types::IssueType, :description) + + argument :due_date, GraphQL::Types::ISO8601Date, + required: false, + description: copy_field_description(Types::IssueType, :due_date) + + argument :confidential, GraphQL::BOOLEAN_TYPE, + 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) + end + end + end +end diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb new file mode 100644 index 00000000000..1454916bc77 --- /dev/null +++ b/app/graphql/mutations/issues/create.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class Create < BaseMutation + include ResolvesProject + graphql_name 'CreateIssue' + + authorize :create_issue + + include CommonMutationArguments + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Project full path the issue is associated with' + + argument :iid, GraphQL::INT_TYPE, + required: false, + description: 'The IID (internal ID) of a project issue. Only admins and project owners can modify' + + argument :title, GraphQL::STRING_TYPE, + required: true, + description: copy_field_description(Types::IssueType, :title) + + argument :milestone_id, ::Types::GlobalIDType[::Milestone], + required: false, + description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null' + + argument :labels, [GraphQL::STRING_TYPE], + required: false, + description: copy_field_description(Types::IssueType, :labels) + + argument :label_ids, [::Types::GlobalIDType[::Label]], + required: false, + description: 'The IDs of labels to be added to the issue' + + argument :created_at, Types::TimeType, + required: false, + description: 'Timestamp when the issue was created. Available only for admins and project owners' + + argument :merge_request_to_resolve_discussions_of, ::Types::GlobalIDType[::MergeRequest], + required: false, + description: 'The IID of a merge request for which to resolve discussions' + + argument :discussion_to_resolve, GraphQL::STRING_TYPE, + required: false, + description: 'The ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`' + + argument :assignee_ids, [::Types::GlobalIDType[::User]], + required: false, + description: 'The array of user IDs to assign to the issue' + + field :issue, + Types::IssueType, + null: true, + description: 'The issue after mutation' + + def ready?(**args) + if args.slice(*mutually_exclusive_label_args).size > 1 + arg_str = mutually_exclusive_label_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 + + if args[:discussion_to_resolve].present? && args[:merge_request_to_resolve_discussions_of].blank? + raise Gitlab::Graphql::Errors::ArgumentError, + 'to resolve a discussion please also provide `merge_request_to_resolve_discussions_of` parameter' + end + + super + end + + def resolve(project_path:, **attributes) + project = authorized_find!(full_path: project_path) + params = build_create_issue_params(attributes.merge(author_id: current_user.id)) + + issue = ::Issues::CreateService.new(project, current_user, params).execute + + if issue.spam? + issue.errors.add(:base, 'Spam detected.') + end + + { + issue: issue.valid? ? issue : nil, + errors: errors_on_object(issue) + } + end + + private + + def build_create_issue_params(params) + params[:milestone_id] &&= params[:milestone_id]&.model_id + params[:assignee_ids] &&= params[:assignee_ids].map { |assignee_id| assignee_id&.model_id } + params[:label_ids] &&= params[:label_ids].map { |label_id| label_id&.model_id } + + params + end + + def mutually_exclusive_label_args + [:labels, :label_ids] + end + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end + +Mutations::Issues::Create.prepend_if_ee('::EE::Mutations::Issues::Create') diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb new file mode 100644 index 00000000000..e6971c9df8c --- /dev/null +++ b/app/graphql/mutations/issues/move.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class Move < Base + graphql_name 'IssueMove' + + argument :target_project_path, + GraphQL::ID_TYPE, + required: true, + description: 'The project to move the issue to' + + def resolve(project_path:, iid:, target_project_path:) + Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/267762') + + issue = authorized_find!(project_path: project_path, iid: iid) + source_project = issue.project + target_project = resolve_project(full_path: target_project_path).sync + + begin + moved_issue = ::Issues::MoveService.new(source_project, current_user).execute(issue, target_project) + rescue ::Issues::MoveService::MoveError => error + errors = error.message + end + + { + issue: moved_issue, + errors: Array.wrap(errors) + } + end + end + end +end diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb index cc03d32731b..9b216b31f9b 100644 --- a/app/graphql/mutations/issues/update.rb +++ b/app/graphql/mutations/issues/update.rb @@ -5,46 +5,27 @@ module Mutations class Update < Base graphql_name 'UpdateIssue' - argument :title, - GraphQL::STRING_TYPE, - required: false, - description: copy_field_description(Types::IssueType, :title) + include CommonMutationArguments - argument :description, - GraphQL::STRING_TYPE, - required: false, - description: copy_field_description(Types::IssueType, :description) - - argument :due_date, - Types::TimeType, - required: false, - description: copy_field_description(Types::IssueType, :due_date) - - argument :confidential, - GraphQL::BOOLEAN_TYPE, + argument :title, GraphQL::STRING_TYPE, required: false, - description: copy_field_description(Types::IssueType, :confidential) + description: copy_field_description(Types::IssueType, :title) - argument :locked, - GraphQL::BOOLEAN_TYPE, - as: :discussion_locked, + argument :milestone_id, GraphQL::ID_TYPE, required: false, - description: copy_field_description(Types::IssueType, :discussion_locked) + description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null' - argument :add_label_ids, - [GraphQL::ID_TYPE], + argument :add_label_ids, [GraphQL::ID_TYPE], required: false, - description: 'The IDs of labels to be added to the issue.' + description: 'The IDs of labels to be added to the issue' - argument :remove_label_ids, - [GraphQL::ID_TYPE], + argument :remove_label_ids, [GraphQL::ID_TYPE], required: false, - description: 'The IDs of labels to be removed from the issue.' + 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.' + argument :state_event, Types::IssueStateEventEnum, + description: 'Close or reopen an issue', + required: false def resolve(project_path:, iid:, **args) issue = authorized_find!(project_path: project_path, iid: iid) diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb index b3412dd9ed2..abcb1bda1f3 100644 --- a/app/graphql/mutations/merge_requests/set_milestone.rb +++ b/app/graphql/mutations/merge_requests/set_milestone.rb @@ -6,7 +6,7 @@ module Mutations graphql_name 'MergeRequestSetMilestone' argument :milestone_id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Milestone], required: false, loads: Types::MilestoneType, description: <<~DESC diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb index f99688aeac6..b064f55825f 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb @@ -18,12 +18,12 @@ module Mutations description: 'The created annotation' argument :environment_id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Environment], required: false, description: 'The global id of the environment to add an annotation to' argument :cluster_id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Clusters::Cluster], required: false, description: 'The global id of the cluster to add an annotation to' @@ -80,11 +80,11 @@ module Mutations raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR end - super(args) + super(**args) end def find_object(id:) - GitlabSchema.object_from_id(id) + GitlabSchema.find_by_gid(id) end def annotation_create_params(args) @@ -96,7 +96,16 @@ module Mutations end def annotation_source(args) - annotation_source_id = args[:cluster_id] || args[:environment_id] + # TODO: remove these lines when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + annotation_source_id = if args[:cluster_id] + ::Types::GlobalIDType[::Clusters::Cluster].coerce_isolated_input(args[:cluster_id]) + else + ::Types::GlobalIDType[::Environment].coerce_isolated_input(args[:environment_id]) + end + + # TODO: uncomment following line once lines above are removed + # annotation_source_id = args[:cluster_id] || args[:environment_id] authorized_find!(id: annotation_source_id) end end diff --git a/app/graphql/mutations/notes/base.rb b/app/graphql/mutations/notes/base.rb index 31dabc0a660..f2678211335 100644 --- a/app/graphql/mutations/notes/base.rb +++ b/app/graphql/mutations/notes/base.rb @@ -11,21 +11,10 @@ module Mutations private def find_object(id:) - GitlabSchema.object_from_id(id) - end - - def check_object_is_noteable!(object) - unless object.is_a?(Noteable) - raise Gitlab::Graphql::Errors::ResourceNotAvailable, - 'Cannot add notes to this resource' - end - end - - def check_object_is_note!(object) - unless object.is_a?(Note) - raise Gitlab::Graphql::Errors::ResourceNotAvailable, - 'Resource is not a note' - end + # TODO: remove explicit coercion once compatibility layer has been removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Note].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) end end end diff --git a/app/graphql/mutations/notes/create/base.rb b/app/graphql/mutations/notes/create/base.rb index f081eac368e..3cfdaf84760 100644 --- a/app/graphql/mutations/notes/create/base.rb +++ b/app/graphql/mutations/notes/create/base.rb @@ -9,7 +9,7 @@ module Mutations authorize :create_note argument :noteable_id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Noteable], required: true, description: 'The global id of the resource to add a note to' @@ -26,8 +26,6 @@ module Mutations def resolve(args) noteable = authorized_find!(id: args[:noteable_id]) - check_object_is_noteable!(noteable) - note = ::Notes::CreateService.new( noteable.project, current_user, @@ -42,6 +40,13 @@ module Mutations private + def find_object(id:) + # TODO: remove explicit coercion once compatibility layer has been removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Noteable].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + def create_note_params(noteable, args) { noteable: noteable, diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb index 5236e48026e..e97037171f7 100644 --- a/app/graphql/mutations/notes/create/note.rb +++ b/app/graphql/mutations/notes/create/note.rb @@ -7,7 +7,7 @@ module Mutations graphql_name 'CreateNote' argument :discussion_id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Discussion], required: false, description: 'The global id of the discussion this note is in reply to' @@ -17,7 +17,11 @@ module Mutations discussion_id = nil if args[:discussion_id] - discussion = GitlabSchema.object_from_id(args[:discussion_id]) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + discussion_gid = ::Types::GlobalIDType[::Discussion].coerce_isolated_input(args[:discussion_id]) + discussion = GitlabSchema.find_by_gid(discussion_gid) + authorize_discussion!(discussion) discussion_id = discussion.id diff --git a/app/graphql/mutations/notes/destroy.rb b/app/graphql/mutations/notes/destroy.rb index a81322bc9b7..63e5eeb5ecf 100644 --- a/app/graphql/mutations/notes/destroy.rb +++ b/app/graphql/mutations/notes/destroy.rb @@ -8,15 +8,13 @@ module Mutations authorize :admin_note argument :id, - GraphQL::ID_TYPE, - required: true, - description: 'The global id of the note to destroy' + ::Types::GlobalIDType[::Note], + required: true, + description: 'The global id of the note to destroy' def resolve(id:) note = authorized_find!(id: id) - check_object_is_note!(note) - ::Notes::DestroyService.new(note.project, current_user).execute(note) { diff --git a/app/graphql/mutations/notes/update/base.rb b/app/graphql/mutations/notes/update/base.rb index 8a2a78a29ec..1d5738ada77 100644 --- a/app/graphql/mutations/notes/update/base.rb +++ b/app/graphql/mutations/notes/update/base.rb @@ -9,7 +9,7 @@ module Mutations authorize :admin_note argument :id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Note], required: true, description: 'The global id of the note to update' diff --git a/app/graphql/mutations/notes/update/image_diff_note.rb b/app/graphql/mutations/notes/update/image_diff_note.rb index 7aad3af1e04..ef70a8d2bf4 100644 --- a/app/graphql/mutations/notes/update/image_diff_note.rb +++ b/app/graphql/mutations/notes/update/image_diff_note.rb @@ -28,12 +28,12 @@ module Mutations 'body or position arguments are required' end - super(args) + super(**args) end private - def pre_update_checks!(note, args) + def pre_update_checks!(note, _args) unless note.is_a?(DiffNote) && note.position.on_image? raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Resource is not an ImageDiffNote' diff --git a/app/graphql/mutations/notes/update/note.rb b/app/graphql/mutations/notes/update/note.rb index ca97dad6ded..73b9b9bc49a 100644 --- a/app/graphql/mutations/notes/update/note.rb +++ b/app/graphql/mutations/notes/update/note.rb @@ -18,8 +18,8 @@ module Mutations private - def pre_update_checks!(note, _args) - check_object_is_note!(note) + def pre_update_checks!(_note, _args) + # no-op end end end diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb index a8aeb15afcd..37c0f80310c 100644 --- a/app/graphql/mutations/snippets/create.rb +++ b/app/graphql/mutations/snippets/create.rb @@ -3,6 +3,7 @@ module Mutations module Snippets class Create < BaseMutation + include SpammableMutationFields include ResolvesProject graphql_name 'CreateSnippet' @@ -56,10 +57,12 @@ module Mutations ::Gitlab::UsageDataCounters::EditorUniqueCounter.track_snippet_editor_edit_action(author: current_user) end - { - snippet: service_response.success? ? snippet : nil, - errors: errors_on_object(snippet) - } + with_spam_fields(snippet) do + { + snippet: service_response.success? ? snippet : nil, + errors: errors_on_object(snippet) + } + end end private @@ -81,14 +84,16 @@ module Mutations end def create_params(args) - args.tap do |create_args| - # We need to rename `blob_actions` into `snippet_actions` because - # it's the expected key param - 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 - create_args[:files] = create_args.delete(:uploaded_files) + with_spam_params do + args.tap do |create_args| + # We need to rename `blob_actions` into `snippet_actions` because + # it's the expected key param + 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 + create_args[:files] = create_args.delete(:uploaded_files) + end end end end diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb index d0db5fa2eb9..74266880806 100644 --- a/app/graphql/mutations/snippets/update.rb +++ b/app/graphql/mutations/snippets/update.rb @@ -3,6 +3,8 @@ module Mutations module Snippets class Update < Base + include SpammableMutationFields + graphql_name 'UpdateSnippet' argument :id, @@ -39,10 +41,12 @@ module Mutations ::Gitlab::UsageDataCounters::EditorUniqueCounter.track_snippet_editor_edit_action(author: current_user) end - { - snippet: result.success? ? snippet : snippet.reset, - errors: errors_on_object(snippet) - } + with_spam_fields(snippet) do + { + snippet: result.success? ? snippet : snippet.reset, + errors: errors_on_object(snippet) + } + end end private @@ -52,10 +56,12 @@ module Mutations end def update_params(args) - args.tap do |update_args| - # We need to rename `blob_actions` into `snippet_actions` because - # it's the expected key param - update_args[:snippet_actions] = update_args.delete(:blob_actions)&.map(&:to_h) + with_spam_params do + args.tap do |update_args| + # We need to rename `blob_actions` into `snippet_actions` because + # it's the expected key param + update_args[:snippet_actions] = update_args.delete(:blob_actions)&.map(&:to_h) + end end end end diff --git a/app/graphql/mutations/todos/base.rb b/app/graphql/mutations/todos/base.rb index 2a72019fbac..6db863796bc 100644 --- a/app/graphql/mutations/todos/base.rb +++ b/app/graphql/mutations/todos/base.rb @@ -6,7 +6,10 @@ module Mutations private def find_object(id:) - GitlabSchema.object_from_id(id) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Todo].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) end def map_to_global_ids(ids) @@ -16,7 +19,7 @@ module Mutations end def to_global_id(id) - ::URI::GID.build(app: GlobalID.app, model_name: Todo.name, model_id: id, params: nil).to_s + Gitlab::GlobalId.as_global_id(id, model_name: Todo.name).to_s end end end diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index 748e02d8782..3d73022f266 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -8,7 +8,7 @@ module Mutations authorize :update_todo argument :id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Todo], required: true, description: 'The global id of the todo to mark as done' diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb index a0a1772db0a..7c8f92d32f5 100644 --- a/app/graphql/mutations/todos/restore.rb +++ b/app/graphql/mutations/todos/restore.rb @@ -8,7 +8,7 @@ module Mutations authorize :update_todo argument :id, - GraphQL::ID_TYPE, + ::Types::GlobalIDType[::Todo], required: true, description: 'The global id of the todo to restore' diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index c5e2750768c..ea5f5414134 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -8,7 +8,7 @@ module Mutations MAX_UPDATE_AMOUNT = 50 argument :ids, - [GraphQL::ID_TYPE], + [::Types::GlobalIDType[::Todo]], required: true, description: 'The global ids of the todos to restore (a maximum of 50 is supported at once)' @@ -37,24 +37,18 @@ module Mutations private def gids_of(ids) - ids.map { |id| ::URI::GID.build(app: GlobalID.app, model_name: Todo.name, model_id: id, params: nil).to_s } + 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| - parsed_gid = ::URI::GID.parse(gid) - parsed_gid.model_id.to_i if accessible_todo?(parsed_gid) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + gid = ::Types::GlobalIDType[::Todo].coerce_isolated_input(gid) + gid.model_id.to_i end.compact end - def accessible_todo?(gid) - gid.app == GlobalID.app && todo?(gid) - end - - def todo?(gid) - GlobalID.parse(gid)&.model_class&.ancestors&.include?(Todo) - end - def raise_too_many_todos_requested_error raise Gitlab::Graphql::Errors::ArgumentError, 'Too many todos requested.' end diff --git a/app/graphql/queries/repository/path_last_commit.query.graphql b/app/graphql/queries/repository/path_last_commit.query.graphql new file mode 100644 index 00000000000..d845f7c6224 --- /dev/null +++ b/app/graphql/queries/repository/path_last_commit.query.graphql @@ -0,0 +1,47 @@ +query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { + project(fullPath: $projectPath) { + __typename + repository { + __typename + tree(path: $path, ref: $ref) { + __typename + lastCommit { + __typename + sha + title + titleHtml + descriptionHtml + message + webPath + authoredDate + authorName + authorGravatar + author { + __typename + name + avatarUrl + webPath + } + signatureHtml + pipelines(ref: $ref, first: 1) { + __typename + edges { + __typename + node { + __typename + detailedStatus { + __typename + detailsPath + icon + tooltip + text + group + } + } + } + } + } + } + } + } +} diff --git a/app/graphql/resolvers/alert_management/alert_resolver.rb b/app/graphql/resolvers/alert_management/alert_resolver.rb index 71a7615685a..dc9b1dbb5f4 100644 --- a/app/graphql/resolvers/alert_management/alert_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_resolver.rb @@ -22,6 +22,10 @@ module Resolvers description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', required: false + argument :assignee_username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of a user assigned to the issue' + type Types::AlertManagement::AlertType, null: true def resolve_with_lookahead(**args) diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb index a45de21002f..96ea4610aff 100644 --- a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb @@ -9,6 +9,10 @@ module Resolvers description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', required: false + argument :assignee_username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of a user assigned to the issue' + def resolve(**args) ::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args) end diff --git a/app/graphql/resolvers/assigned_merge_requests_resolver.rb b/app/graphql/resolvers/assigned_merge_requests_resolver.rb index fa08b142a7e..172a8e298ad 100644 --- a/app/graphql/resolvers/assigned_merge_requests_resolver.rb +++ b/app/graphql/resolvers/assigned_merge_requests_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class AssignedMergeRequestsResolver < UserMergeRequestsResolver + accept_author + def user_role :assignee end diff --git a/app/graphql/resolvers/authored_merge_requests_resolver.rb b/app/graphql/resolvers/authored_merge_requests_resolver.rb index e19bc9e8715..bc796f8685a 100644 --- a/app/graphql/resolvers/authored_merge_requests_resolver.rb +++ b/app/graphql/resolvers/authored_merge_requests_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class AuthoredMergeRequestsResolver < UserMergeRequestsResolver + accept_assignee + def user_role :author end diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 791c6eab42f..2b8854fb4d0 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -4,6 +4,9 @@ module Resolvers class BaseResolver < GraphQL::Schema::Resolver extend ::Gitlab::Utils::Override include ::Gitlab::Utils::StrongMemoize + include ::Gitlab::Graphql::GlobalIDCompatibility + + argument_class ::Types::BaseArgument def self.single @single ||= Class.new(self) do diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb index dba9f99edeb..3421e1024c0 100644 --- a/app/graphql/resolvers/board_list_issues_resolver.rb +++ b/app/graphql/resolvers/board_list_issues_resolver.rb @@ -14,7 +14,7 @@ module Resolvers def resolve(**args) filter_params = issue_filters(args[:filters]).merge(board_id: list.board.id, id: list.id) - service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params) + service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params) Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute) end diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb index b1d43934f24..3384b37e2ce 100644 --- a/app/graphql/resolvers/board_lists_resolver.rb +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -2,6 +2,7 @@ module Resolvers class BoardListsResolver < BaseResolver + include BoardIssueFilterable include Gitlab::Graphql::Authorize::AuthorizeResource type Types::BoardListType, null: true @@ -10,12 +11,17 @@ module Resolvers required: false, description: 'Find a list by its global ID' + argument :issue_filters, Types::Boards::BoardIssueInputType, + required: false, + description: 'Filters applied when getting issue metadata in the board list' + alias_method :board, :object - def resolve(lookahead: nil, id: nil) + def resolve(lookahead: nil, id: nil, issue_filters: {}) authorize!(board) lists = board_lists(id) + context.scoped_set!(:issue_filters, issue_filters(issue_filters)) if load_preferences?(lookahead) List.preload_preferences_for_user(lists, context[:current_user]) @@ -27,7 +33,7 @@ module Resolvers private def board_lists(id) - service = Boards::Lists::ListService.new( + service = ::Boards::Lists::ListService.new( board.resource_parent, context[:current_user], list_id: extract_list_id(id) diff --git a/app/graphql/resolvers/board_resolver.rb b/app/graphql/resolvers/board_resolver.rb new file mode 100644 index 00000000000..517f4e514c9 --- /dev/null +++ b/app/graphql/resolvers/board_resolver.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Resolvers + class BoardResolver < BaseResolver.single + alias_method :parent, :synchronized_object + + type Types::BoardType, null: true + + argument :id, ::Types::GlobalIDType[::Board], + required: true, + description: 'The board\'s ID' + + def resolve(id: nil) + return unless parent + + ::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false).first + rescue ActiveRecord::RecordNotFound + nil + end + + private + + def extract_board_id(gid) + GitlabSchema.parse_gid(gid, expected_type: ::Board).model_id + end + end +end diff --git a/app/graphql/resolvers/boards_resolver.rb b/app/graphql/resolvers/boards_resolver.rb index eceb5b38031..82efd92d33f 100644 --- a/app/graphql/resolvers/boards_resolver.rb +++ b/app/graphql/resolvers/boards_resolver.rb @@ -16,7 +16,7 @@ module Resolvers return Board.none unless parent - Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false) + ::Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false) rescue ActiveRecord::RecordNotFound Board.none end diff --git a/app/graphql/resolvers/ci/runner_platforms_resolver.rb b/app/graphql/resolvers/ci/runner_platforms_resolver.rb new file mode 100644 index 00000000000..9677c5139b4 --- /dev/null +++ b/app/graphql/resolvers/ci/runner_platforms_resolver.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerPlatformsResolver < BaseResolver + type Types::Ci::RunnerPlatformType, null: false + + def resolve(**args) + runner_instructions.map do |platform, data| + { + name: platform, human_readable_name: data[:human_readable_name], + architectures: parse_architectures(data[:download_locations]) + } + end + end + + private + + def runner_instructions + Gitlab::Ci::RunnerInstructions::OS.merge(Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS) + end + + def parse_architectures(download_locations) + download_locations&.map do |architecture, download_location| + { name: architecture, download_location: download_location } + end + end + end + end +end diff --git a/app/graphql/resolvers/concerns/group_issuable_resolver.rb b/app/graphql/resolvers/concerns/group_issuable_resolver.rb new file mode 100644 index 00000000000..49a79683e9f --- /dev/null +++ b/app/graphql/resolvers/concerns/group_issuable_resolver.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module GroupIssuableResolver + extend ActiveSupport::Concern + + class_methods do + def include_subgroups(name_of_things) + argument :include_subgroups, GraphQL::BOOLEAN_TYPE, + required: false, + default_value: false, + description: "Include #{name_of_things} belonging to subgroups" + end + end +end diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 2b14d8275d1..fe6fa0bb262 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -18,9 +18,15 @@ module IssueResolverArguments argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, required: false, description: 'Milestone applied to this issue' + argument :author_username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of the author of the issue' argument :assignee_username, GraphQL::STRING_TYPE, required: false, description: 'Username of a user assigned to the issue' + argument :assignee_usernames, [GraphQL::STRING_TYPE], + required: false, + description: 'Usernames of users 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' diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb index e7230287e13..61f23920ebb 100644 --- a/app/graphql/resolvers/concerns/looks_ahead.rb +++ b/app/graphql/resolvers/concerns/looks_ahead.rb @@ -3,8 +3,6 @@ module LooksAhead extend ActiveSupport::Concern - FEATURE_FLAG = :graphql_lookahead_support - included do attr_accessor :lookahead end @@ -16,8 +14,6 @@ module LooksAhead end def apply_lookahead(query) - return query unless Feature.enabled?(FEATURE_FLAG) - selection = node_selection includes = preloads.each.flat_map do |name, requirements| diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index 0c01efd4f9a..ab83476ddea 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -12,7 +12,7 @@ module ResolvesMergeRequests def resolve_with_lookahead(**args) mr_finder = MergeRequestsFinder.new(current_user, args.compact) - finder = Gitlab::Graphql::Loaders::IssuableLoader.new(project, mr_finder) + finder = Gitlab::Graphql::Loaders::IssuableLoader.new(mr_parent, mr_finder) select_result(finder.batching_find_all { |query| apply_lookahead(query) }) end @@ -29,6 +29,10 @@ module ResolvesMergeRequests private + def mr_parent + project + end + def unconditional_includes [:target_project] end @@ -40,7 +44,8 @@ module ResolvesMergeRequests author: [:author], merged_at: [:metrics], commit_count: [:metrics], - approved_by: [:approver_users], + diff_stats_summary: [:metrics], + approved_by: [:approved_by_users], milestone: [:milestone], head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }] } diff --git a/app/graphql/resolvers/concerns/time_frame_arguments.rb b/app/graphql/resolvers/concerns/time_frame_arguments.rb index ef333dd05a5..94bfe6f7f9f 100644 --- a/app/graphql/resolvers/concerns/time_frame_arguments.rb +++ b/app/graphql/resolvers/concerns/time_frame_arguments.rb @@ -3,21 +3,33 @@ module TimeFrameArguments extend ActiveSupport::Concern + OVERLAPPING_TIMEFRAME_DESC = 'List items overlapping a time frame defined by startDate..endDate (if one date is provided, both must be present)' + included do argument :start_date, Types::TimeType, required: false, - description: 'List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)' + description: OVERLAPPING_TIMEFRAME_DESC, + deprecated: { reason: 'Use timeframe.start', milestone: '13.5' } argument :end_date, Types::TimeType, required: false, - description: 'List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)' + description: OVERLAPPING_TIMEFRAME_DESC, + deprecated: { reason: 'Use timeframe.end', milestone: '13.5' } + + argument :timeframe, Types::TimeframeInputType, + required: false, + description: 'List items overlapping the given timeframe' end + # TODO: remove when the start_date and end_date arguments are removed def validate_timeframe_params!(args) - return unless args[:start_date].present? || args[:end_date].present? + return unless %i[start_date end_date timeframe].any? { |k| args[k].present? } + return if args[:timeframe] && %i[start_date end_date].all? { |k| args[k].nil? } error_message = - if args[:start_date].nil? || args[:end_date].nil? + if args[:timeframe].present? + "startDate and endDate are deprecated in favor of timeframe. Please use only timeframe." + elsif args[:start_date].nil? || args[:end_date].nil? "Both startDate and endDate must be present." elsif args[:start_date] > args[:end_date] "startDate is after endDate" diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb index ac51011eea8..1fa6c78e730 100644 --- a/app/graphql/resolvers/group_issues_resolver.rb +++ b/app/graphql/resolvers/group_issues_resolver.rb @@ -2,9 +2,8 @@ module Resolvers class GroupIssuesResolver < IssuesResolver - argument :include_subgroups, GraphQL::BOOLEAN_TYPE, - required: false, - default_value: false, - description: 'Include issues belonging to subgroups.' + include GroupIssuableResolver + + include_subgroups 'issues' end end diff --git a/app/graphql/resolvers/group_merge_requests_resolver.rb b/app/graphql/resolvers/group_merge_requests_resolver.rb new file mode 100644 index 00000000000..5ee72e3f781 --- /dev/null +++ b/app/graphql/resolvers/group_merge_requests_resolver.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resolvers + class GroupMergeRequestsResolver < MergeRequestsResolver + include GroupIssuableResolver + + alias_method :group, :synchronized_object + + include_subgroups 'merge requests' + accept_assignee + accept_author + + def project + nil + end + + def mr_parent + group + end + + def no_results_possible?(args) + group.nil? || some_argument_is_empty?(args) + end + end +end diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index 677f84e5795..cb4a76243ae 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -6,6 +6,18 @@ module Resolvers alias_method :project, :synchronized_object + def self.accept_assignee + argument :assignee_username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of the assignee' + end + + def self.accept_author + argument :author_username, GraphQL::STRING_TYPE, + required: false, + description: 'Username of the author' + end + argument :iids, [GraphQL::STRING_TYPE], required: false, description: 'Array of IIDs of merge requests, for example `[1, 2]`' diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 5f80506c01b..84712b674db 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -13,6 +13,18 @@ module Resolvers required: false, description: 'Filter milestones by state' + argument :title, GraphQL::STRING_TYPE, + required: false, + description: 'The title of the milestone' + + argument :search_title, GraphQL::STRING_TYPE, + required: false, + description: 'A search string for the title' + + argument :containing_date, Types::TimeType, + required: false, + description: 'A date that the milestone contains' + type Types::MilestoneType, null: true def resolve(**args) @@ -29,9 +41,18 @@ module Resolvers { ids: parse_gids(args[:ids]), state: args[:state] || 'all', - start_date: args[:start_date], - end_date: args[:end_date] - }.merge(parent_id_parameters(args)) + title: args[:title], + search_title: args[:search_title], + containing_date: args[:containing_date] + }.merge!(timeframe_parameters(args)).merge!(parent_id_parameters(args)) + end + + def timeframe_parameters(args) + if args[:timeframe] + args[:timeframe].transform_keys { |k| :"#{k}_date" } + else + args.slice(:start_date, :end_date) + end end def parent diff --git a/app/graphql/resolvers/project_merge_requests_resolver.rb b/app/graphql/resolvers/project_merge_requests_resolver.rb index 0526ccd315f..ba13cb6e52c 100644 --- a/app/graphql/resolvers/project_merge_requests_resolver.rb +++ b/app/graphql/resolvers/project_merge_requests_resolver.rb @@ -2,11 +2,7 @@ module Resolvers class ProjectMergeRequestsResolver < MergeRequestsResolver - argument :assignee_username, GraphQL::STRING_TYPE, - required: false, - description: 'Username of the assignee' - argument :author_username, GraphQL::STRING_TYPE, - required: false, - description: 'Username of the author' + accept_assignee + accept_author end end diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb index ed382ac82d0..d017f973e17 100644 --- a/app/graphql/resolvers/projects/jira_projects_resolver.rb +++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb @@ -22,7 +22,7 @@ module Resolvers 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 }) + **args.merge({ max_page_size: projects_array.size }) ) else raise Gitlab::Graphql::Errors::BaseError, response.message diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb index 3bbadf87a71..69438229a50 100644 --- a/app/graphql/resolvers/projects_resolver.rb +++ b/app/graphql/resolvers/projects_resolver.rb @@ -13,8 +13,16 @@ module Resolvers description: 'Search query for project name, path, or description' argument :ids, [GraphQL::ID_TYPE], - required: false, - description: 'Filter projects by IDs' + required: false, + description: 'Filter projects by IDs' + + argument :search_namespaces, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Include namespace in project search' + + argument :sort, GraphQL::STRING_TYPE, + required: false, + description: 'Sort order of results' def resolve(**args) ProjectsFinder @@ -28,7 +36,9 @@ module Resolvers { without_deleted: true, non_public: params[:membership], - search: params[:search] + search: params[:search], + search_namespaces: params[:search_namespaces], + sort: params[:sort] }.compact end diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb new file mode 100644 index 00000000000..dc28358cab6 --- /dev/null +++ b/app/graphql/resolvers/snippets/blobs_resolver.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Resolvers + module Snippets + class BlobsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + alias_method :snippet, :object + + argument :paths, [GraphQL::STRING_TYPE], + required: false, + description: 'Paths of the blobs' + + def resolve(**args) + authorize!(snippet) + + return [snippet.blob] if snippet.empty_repo? + + paths = Array(args.fetch(:paths, [])) + + if paths.empty? + snippet.blobs + else + snippet.repository.blobs_at(transformed_blob_paths(paths)) + end + end + + def authorized_resource?(snippet) + Ability.allowed?(context[:current_user], :read_snippet, snippet) + end + + private + + def transformed_blob_paths(paths) + ref = snippet.default_branch + paths.map { |path| [ref, path] } + end + end + end +end diff --git a/app/graphql/resolvers/terraform/states_resolver.rb b/app/graphql/resolvers/terraform/states_resolver.rb new file mode 100644 index 00000000000..38b26a948b1 --- /dev/null +++ b/app/graphql/resolvers/terraform/states_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + module Terraform + class StatesResolver < BaseResolver + type Types::Terraform::StateType, null: true + + alias_method :project, :object + + def resolve(**args) + return ::Terraform::State.none unless can_read_terraform_states? + + project.terraform_states.ordered_by_name + end + + private + + def can_read_terraform_states? + current_user.can?(:read_terraform_state, project) + end + end + end +end diff --git a/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb b/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb index 13c67442c2e..c6ca5963588 100644 --- a/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb +++ b/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb @@ -8,12 +8,16 @@ module Types graphql_name 'MeasurementIdentifier' description 'Possible identifier types for a measurement' - value 'PROJECTS', 'Project count', value: :projects - value 'USERS', 'User count', value: :users - value 'ISSUES', 'Issue count', value: :issues - value 'MERGE_REQUESTS', 'Merge request count', value: :merge_requests - value 'GROUPS', 'Group count', value: :groups - value 'PIPELINES', 'Pipeline count', value: :pipelines + value 'PROJECTS', 'Project count', value: 'projects' + value 'USERS', 'User count', value: 'users' + value 'ISSUES', 'Issue count', value: 'issues' + value 'MERGE_REQUESTS', 'Merge request count', value: 'merge_requests' + value 'GROUPS', 'Group count', value: 'groups' + value 'PIPELINES', 'Pipeline count', value: 'pipelines' + value 'PIPELINES_SUCCEEDED', 'Pipeline count with success status', value: 'pipelines_succeeded' + value 'PIPELINES_FAILED', 'Pipeline count with failed status', value: 'pipelines_failed' + value 'PIPELINES_CANCELED', 'Pipeline count with canceled status', value: 'pipelines_canceled' + value 'PIPELINES_SKIPPED', 'Pipeline count with skipped status', value: 'pipelines_skipped' end end end diff --git a/app/graphql/types/alert_management/alert_status_counts_type.rb b/app/graphql/types/alert_management/alert_status_counts_type.rb index f80b289eabc..a84be705445 100644 --- a/app/graphql/types/alert_management/alert_status_counts_type.rb +++ b/app/graphql/types/alert_management/alert_status_counts_type.rb @@ -9,11 +9,11 @@ module Types authorize :read_alert_management_alert - ::Gitlab::AlertManagement::AlertStatusCounts::STATUSES.each_key do |status| + ::AlertManagement::Alert.status_names.each do |status| field status, GraphQL::INT_TYPE, null: true, - description: "Number of alerts with status #{status.upcase} for the project" + description: "Number of alerts with status #{status.to_s.upcase} for the project" end field :open, diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index 2da97030b88..623762de208 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -40,7 +40,8 @@ module Types field :status, AlertManagement::StatusEnum, null: true, - description: 'Status of the alert' + description: 'Status of the alert', + method: :status_name field :service, GraphQL::STRING_TYPE, @@ -67,6 +68,11 @@ module Types null: true, description: 'Timestamp the alert ended' + field :environment, + Types::EnvironmentType, + null: true, + description: 'Environment for the alert' + field :event_count, GraphQL::INT_TYPE, null: true, diff --git a/app/graphql/types/alert_management/status_enum.rb b/app/graphql/types/alert_management/status_enum.rb index 4ff6c4a9505..9d2c7316254 100644 --- a/app/graphql/types/alert_management/status_enum.rb +++ b/app/graphql/types/alert_management/status_enum.rb @@ -6,8 +6,8 @@ module Types graphql_name 'AlertManagementStatus' description 'Alert status values' - ::AlertManagement::Alert::STATUSES.each do |name, value| - value name.upcase, value: value, description: "#{name.to_s.titleize} status" + ::AlertManagement::Alert.status_names.each do |status| + value status.to_s.upcase, value: status, description: "#{status.to_s.titleize} status" end end end diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb new file mode 100644 index 00000000000..11774d0b59d --- /dev/null +++ b/app/graphql/types/base_argument.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class BaseArgument < GraphQL::Schema::Argument + include GitlabStyleDeprecations + + def initialize(*args, **kwargs, &block) + kwargs = gitlab_deprecation(kwargs) + kwargs.delete(:deprecation_reason) + + super(*args, **kwargs, &block) + end + end +end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 1e72a4cddf5..5c8aabfe163 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -5,6 +5,8 @@ module Types prepend Gitlab::Graphql::Authorize include GitlabStyleDeprecations + argument_class ::Types::BaseArgument + DEFAULT_COMPLEXITY = 1 def initialize(*args, **kwargs, &block) diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb index 24faf1fe8bc..6ee76b0d1f1 100644 --- a/app/graphql/types/board_list_type.rb +++ b/app/graphql/types/board_list_type.rb @@ -32,17 +32,14 @@ module Types metadata[:size] end - def total_weight - metadata[:total_weight] - end - def metadata strong_memoize(:metadata) do list = self.object user = context[:current_user] + params = (context[:issue_filters] || {}).merge(board_id: list.board_id, id: list.id) ::Boards::Issues::ListService - .new(list.board.resource_parent, user, board_id: list.board_id, id: list.id) + .new(list.board.resource_parent, user, params) .metadata end end diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb index 90b5283fc9a..f4a50115ee6 100644 --- a/app/graphql/types/ci/detailed_status_type.rb +++ b/app/graphql/types/ci/detailed_status_type.rb @@ -6,24 +6,39 @@ module Types class DetailedStatusType < BaseObject graphql_name 'DetailedStatus' - field :group, GraphQL::STRING_TYPE, null: false, - description: 'Group of the pipeline status' - field :icon, GraphQL::STRING_TYPE, null: false, - description: 'Icon of the pipeline status' - field :favicon, GraphQL::STRING_TYPE, null: false, - description: 'Favicon of the pipeline status' - field :details_path, GraphQL::STRING_TYPE, null: false, - description: 'Path of the details for the pipeline status' - field :has_details, GraphQL::BOOLEAN_TYPE, null: false, - description: 'Indicates if the pipeline status has further details', + field :group, GraphQL::STRING_TYPE, null: true, + description: 'Group of the status' + field :icon, GraphQL::STRING_TYPE, null: true, + description: 'Icon of the status' + field :favicon, GraphQL::STRING_TYPE, null: true, + description: 'Favicon of the status' + field :details_path, GraphQL::STRING_TYPE, null: true, + description: 'Path of the details for the status' + field :has_details, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Indicates if the status has further details', method: :has_details? - field :label, GraphQL::STRING_TYPE, null: false, - description: 'Label of the pipeline status' - field :text, GraphQL::STRING_TYPE, null: false, - description: 'Text of the pipeline status' - field :tooltip, GraphQL::STRING_TYPE, null: false, - description: 'Tooltip associated with the pipeline status', + field :label, GraphQL::STRING_TYPE, null: true, + description: 'Label of the status' + field :text, GraphQL::STRING_TYPE, null: true, + description: 'Text of the status' + field :tooltip, GraphQL::STRING_TYPE, null: true, + description: 'Tooltip associated with the status', method: :status_tooltip + field :action, Types::Ci::StatusActionType, null: true, + description: 'Action information for the status. This includes method, button title, icon, path, and title', + resolve: -> (obj, _args, _ctx) { + if obj.has_action? + { + button_title: obj.action_button_title, + icon: obj.icon, + method: obj.action_method, + path: obj.action_path, + title: obj.action_title + } + else + nil + 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 index 04c0eb93068..d930ae311b7 100644 --- a/app/graphql/types/ci/group_type.rb +++ b/app/graphql/types/ci/group_type.rb @@ -12,6 +12,9 @@ module Types description: 'Size of the group' field :jobs, Ci::JobType.connection_type, null: true, description: 'Jobs in group' + field :detailed_status, Types::Ci::DetailedStatusType, null: true, + description: 'Detailed status of the group', + resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } end end end diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 4c18f3ffd52..0ee1ad47b62 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -10,6 +10,11 @@ module Types description: 'Name of the job' field :needs, JobType.connection_type, null: true, description: 'Builds that must complete before the jobs run' + field :detailed_status, Types::Ci::DetailedStatusType, null: true, + description: 'Detailed status of the job', + resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } + field :scheduled_at, Types::TimeType, null: true, + description: 'Schedule for the build' end end end diff --git a/app/graphql/types/ci/runner_architecture_type.rb b/app/graphql/types/ci/runner_architecture_type.rb new file mode 100644 index 00000000000..526348abd9d --- /dev/null +++ b/app/graphql/types/ci/runner_architecture_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RunnerArchitectureType < BaseObject + graphql_name 'RunnerArchitecture' + + field :name, GraphQL::STRING_TYPE, null: false, + description: 'Name of the runner platform architecture' + field :download_location, GraphQL::STRING_TYPE, null: false, + description: 'Download location for the runner for the platform architecture' + end + end +end diff --git a/app/graphql/types/ci/runner_platform_type.rb b/app/graphql/types/ci/runner_platform_type.rb new file mode 100644 index 00000000000..64719bc4908 --- /dev/null +++ b/app/graphql/types/ci/runner_platform_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RunnerPlatformType < BaseObject + graphql_name 'RunnerPlatform' + + field :name, GraphQL::STRING_TYPE, null: false, + description: 'Name slug of the runner platform' + field :human_readable_name, GraphQL::STRING_TYPE, null: false, + description: 'Human readable name of the runner platform' + field :architectures, Types::Ci::RunnerArchitectureType.connection_type, null: true, + description: 'Runner architectures supported for the platform' + end + end +end diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb index 278c4d4d748..fc2c72d0d06 100644 --- a/app/graphql/types/ci/stage_type.rb +++ b/app/graphql/types/ci/stage_type.rb @@ -10,6 +10,9 @@ module Types description: 'Name of the stage' field :groups, Ci::GroupType.connection_type, null: true, description: 'Group of jobs for the stage' + field :detailed_status, Types::Ci::DetailedStatusType, null: true, + description: 'Detailed status of the stage', + resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } end end end diff --git a/app/graphql/types/ci/status_action_type.rb b/app/graphql/types/ci/status_action_type.rb new file mode 100644 index 00000000000..08cbb6d3b59 --- /dev/null +++ b/app/graphql/types/ci/status_action_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class StatusActionType < BaseObject + graphql_name 'StatusAction' + + field :button_title, GraphQL::STRING_TYPE, null: true, + description: 'Title for the button, for example: Retry this job' + field :icon, GraphQL::STRING_TYPE, null: true, + description: 'Icon used in the action button' + field :method, GraphQL::STRING_TYPE, null: true, + description: 'Method for the action, for example: :post', + resolver_method: :action_method + field :path, GraphQL::STRING_TYPE, null: true, + description: 'Path for the action' + field :title, GraphQL::STRING_TYPE, null: true, + description: 'Title for the action, for example: Retry' + + def action_method + object[:method] + end + end + end +end diff --git a/app/graphql/types/date_type.rb b/app/graphql/types/date_type.rb new file mode 100644 index 00000000000..7129b75b8bb --- /dev/null +++ b/app/graphql/types/date_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + class DateType < BaseScalar + graphql_name 'Date' + description 'Date represented in ISO 8601' + + def self.coerce_input(value, ctx) + return if value.nil? + + Date.iso8601(value) + rescue ArgumentError, TypeError => e + raise GraphQL::CoercionError, e.message + end + + def self.coerce_result(value, ctx) + return if value.nil? + + value.to_date.iso8601 + end + end +end diff --git a/app/graphql/types/design_management/design_collection_copy_state_enum.rb b/app/graphql/types/design_management/design_collection_copy_state_enum.rb new file mode 100644 index 00000000000..7e7303c50ef --- /dev/null +++ b/app/graphql/types/design_management/design_collection_copy_state_enum.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module DesignManagement + class DesignCollectionCopyStateEnum < BaseEnum + graphql_name 'DesignCollectionCopyState' + description 'Copy state of a DesignCollection' + + DESCRIPTION_VARIANTS = { + in_progress: 'is being copied', + error: 'encountered an error during a copy', + ready: 'has no copy in progress' + }.freeze + + def self.description_variant(copy_state) + DESCRIPTION_VARIANTS[copy_state.to_sym] || + (raise ArgumentError, "Unknown copy state: #{copy_state}") + end + + ::DesignManagement::DesignCollection.state_machines[:copy_state].states.keys.each do |copy_state| + value copy_state.upcase, + value: copy_state.to_s, + description: "The DesignCollection #{description_variant(copy_state)}" + end + end + end +end diff --git a/app/graphql/types/design_management/design_collection_type.rb b/app/graphql/types/design_management/design_collection_type.rb index 904fb270e11..9af1f4db425 100644 --- a/app/graphql/types/design_management/design_collection_type.rb +++ b/app/graphql/types/design_management/design_collection_type.rb @@ -39,6 +39,10 @@ module Types null: true, resolver: ::Resolvers::DesignManagement::DesignResolver, description: 'Find a specific design' + + field :copy_state, ::Types::DesignManagement::DesignCollectionCopyStateEnum, + null: true, + description: 'Copy state of the design collection' end end end diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb index 4e11a7aaf09..bab22015dc4 100644 --- a/app/graphql/types/design_management/design_type.rb +++ b/app/graphql/types/design_management/design_type.rb @@ -30,7 +30,7 @@ module Types # most recent `Version` for an issue Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'stateful_version', object.issue_id, version_gid]) do if version_gid - GitlabSchema.object_from_id(version_gid)&.sync + GitlabSchema.object_from_id(version_gid, expected_type: ::DesignManagement::Version)&.sync else object.issue.design_versions.most_recent end diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 239b26f9c38..e4631a4a903 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -5,6 +5,8 @@ module Types graphql_name 'Environment' description 'Describes where code is deployed for a project' + present_using ::EnvironmentPresenter + authorize :read_environment field :name, GraphQL::STRING_TYPE, null: false, @@ -16,6 +18,10 @@ module Types field :state, GraphQL::STRING_TYPE, null: false, description: 'State of the environment, for example: available/stopped' + field :path, GraphQL::STRING_TYPE, null: true, + description: 'The path to the environment. Will always return null ' \ + 'if `expose_environment_path_in_alert_details` feature flag is disabled' + field :metrics_dashboard, Types::Metrics::DashboardType, null: true, description: 'Metrics dashboard schema for the environment', resolver: Resolvers::Metrics::DashboardResolver @@ -23,6 +29,6 @@ module Types 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.' + 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/global_id_type.rb b/app/graphql/types/global_id_type.rb index a3964ba83e1..9ae9ba32c13 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -1,5 +1,21 @@ # frozen_string_literal: true +module GraphQLExtensions + module ScalarExtensions + # Allow ID to unify with GlobalID Types + def ==(other) + if name == 'ID' && other.is_a?(self.class) && + other.type_class.ancestors.include?(::Types::GlobalIDType) + return true + end + + super + end + end +end + +::GraphQL::ScalarType.prepend(GraphQLExtensions::ScalarExtensions) + module Types class GlobalIDType < BaseScalar graphql_name 'GlobalID' diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 60b2e3c7b6e..199cc0308c5 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -46,9 +46,15 @@ module Types field :issues, Types::IssueType.connection_type, null: true, - description: 'Issues of the group', + description: 'Issues for projects in this group', resolver: Resolvers::GroupIssuesResolver + field :merge_requests, + Types::MergeRequestType.connection_type, + null: true, + description: 'Merge requests for projects in this group', + resolver: Resolvers::GroupMergeRequestsResolver + field :milestones, Types::MilestoneType.connection_type, null: true, description: 'Milestones of the group', resolver: Resolvers::GroupMilestonesResolver @@ -64,7 +70,7 @@ module Types Types::BoardType, null: true, description: 'A single board of the group', - resolver: Resolvers::BoardsResolver.single + resolver: Resolvers::BoardResolver field :label, Types::LabelType, diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index e458d6e02c5..08762264b1b 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -8,6 +8,8 @@ module Types value 'DUE_DATE_ASC', 'Due date by ascending order', value: :due_date_asc value 'DUE_DATE_DESC', 'Due date by descending order', value: :due_date_desc value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: :relative_position_asc + value 'SEVERITY_ASC', 'Severity from less critical to more critical', value: :severity_asc + value 'SEVERITY_DESC', 'Severity from more critical to less critical', value: :severity_desc end end diff --git a/app/graphql/types/issue_state_event_enum.rb b/app/graphql/types/issue_state_event_enum.rb new file mode 100644 index 00000000000..6a9d840831d --- /dev/null +++ b/app/graphql/types/issue_state_event_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class IssueStateEventEnum < BaseEnum + graphql_name 'IssueStateEvent' + description 'Values for issue state events' + + value 'REOPEN', 'Reopens the issue', value: 'reopen' + value 'CLOSE', 'Closes the issue', value: 'close' + end +end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index d6253f74ce5..487508f448f 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -36,8 +36,7 @@ module Types end field :author, Types::UserType, null: false, - description: 'User that created the issue', - resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find } + description: 'User that created the issue' field :assignees, Types::UserType.connection_type, null: true, description: 'Assignees of the issue' @@ -45,16 +44,14 @@ module Types field :labels, Types::LabelType.connection_type, null: true, description: 'Labels of the issue' field :milestone, Types::MilestoneType, null: true, - description: 'Milestone of the issue', - resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find } + description: 'Milestone of the issue' field :due_date, Types::TimeType, null: true, description: 'Due date of the issue' field :confidential, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates the issue is confidential' field :discussion_locked, GraphQL::BOOLEAN_TYPE, null: false, - description: 'Indicates discussion is locked on the issue', - resolve: -> (obj, _args, _ctx) { !!obj.discussion_locked } + description: 'Indicates discussion is locked on the issue' field :upvotes, GraphQL::INT_TYPE, null: false, description: 'Number of upvotes the issue has received' @@ -108,6 +105,18 @@ module Types field :severity, Types::IssuableSeverityEnum, null: true, description: 'Severity level of the incident' + + def author + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find + end + + def milestone + Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, object.milestone_id).find + end + + def discussion_locked + !!object.discussion_locked + end end end diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb index 738a00ad616..28dee2a9db5 100644 --- a/app/graphql/types/label_type.rb +++ b/app/graphql/types/label_type.rb @@ -4,6 +4,8 @@ module Types class LabelType < BaseObject graphql_name 'Label' + connection_type_class(Types::CountableConnectionType) + authorize :read_label field :id, GraphQL::ID_TYPE, null: false, diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 56c88491684..372aeac055b 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -152,6 +152,25 @@ module Types field :auto_merge_enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates if auto merge is enabled for the merge request' + field :approved_by, Types::UserType.connection_type, null: true, + description: 'Users who approved the merge request' + + def approved_by + object.approved_by_users + end + + # rubocop: disable CodeReuse/ActiveRecord + def user_notes_count + BatchLoader::GraphQL.for(object.id).batch(key: :merge_request_user_notes_count) do |ids, loader, args| + counts = Note.where(noteable_type: 'MergeRequest', noteable_id: ids).user.group(:noteable_id).count + + ids.each do |id| + loader.call(id, counts[id] || 0) + end + end + end + # rubocop: enable CodeReuse/ActiveRecord + def diff_stats(path: nil) stats = Array.wrap(object.diff_stats&.to_a) @@ -163,21 +182,12 @@ module Types end def diff_stats_summary - nil_stats = { additions: 0, deletions: 0, file_count: 0 } - return nil_stats unless object.diff_stats.present? - - object.diff_stats.each_with_object(nil_stats) do |status, hash| - hash.merge!(additions: status.additions, deletions: status.deletions, file_count: 1) { |_, x, y| x + y } - end + BatchLoaders::MergeRequestDiffSummaryBatchLoader.load_for(object) 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 b2732d83aac..3f48e7b4a16 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -14,13 +14,16 @@ module Types mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle + mount_mutation Mutations::Boards::Create mount_mutation Mutations::Boards::Destroy mount_mutation Mutations::Boards::Issues::IssueMoveList mount_mutation Mutations::Boards::Lists::Create mount_mutation Mutations::Boards::Lists::Update + mount_mutation Mutations::Boards::Lists::Destroy 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::Create mount_mutation Mutations::Issues::SetAssignees mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetLocked @@ -28,6 +31,7 @@ module Types mount_mutation Mutations::Issues::SetSeverity mount_mutation Mutations::Issues::SetSubscription mount_mutation Mutations::Issues::Update + mount_mutation Mutations::Issues::Move mount_mutation Mutations::MergeRequests::Create mount_mutation Mutations::MergeRequests::Update mount_mutation Mutations::MergeRequests::SetLabels @@ -71,4 +75,5 @@ module Types end ::Types::MutationType.prepend(::Types::DeprecatedMutations) +::Types::MutationType.prepend_if_ee('EE::Types::DeprecatedMutations') ::Types::MutationType.prepend_if_ee('::EE::Types::MutationType') diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb index 3a16d54f9cd..602634d9292 100644 --- a/app/graphql/types/notes/noteable_type.rb +++ b/app/graphql/types/notes/noteable_type.rb @@ -8,24 +8,24 @@ module Types field :notes, Types::Notes::NoteType.connection_type, null: false, description: "All notes on this noteable" field :discussions, Types::Notes::DiscussionType.connection_type, null: false, description: "All discussions on this noteable" - definition_methods do - def resolve_type(object, context) - case object - when Issue - Types::IssueType - when MergeRequest - Types::MergeRequestType - when Snippet - Types::SnippetType - when ::DesignManagement::Design - Types::DesignManagement::DesignType - when ::AlertManagement::Alert - Types::AlertManagement::AlertType - else - raise "Unknown GraphQL type for #{object}" - end + def self.resolve_type(object, context) + case object + when Issue + Types::IssueType + when MergeRequest + Types::MergeRequestType + when Snippet + Types::SnippetType + when ::DesignManagement::Design + Types::DesignManagement::DesignType + when ::AlertManagement::Alert + Types::AlertManagement::AlertType + else + raise "Unknown GraphQL type for #{object}" end end end end end + +Types::Notes::NoteableType.prepend_if_ee('::EE::Types::Notes::NoteableType') diff --git a/app/graphql/types/package_type_enum.rb b/app/graphql/types/package_type_enum.rb index bc03b8f5f8b..6f50c166da3 100644 --- a/app/graphql/types/package_type_enum.rb +++ b/app/graphql/types/package_type_enum.rb @@ -2,8 +2,14 @@ module Types class PackageTypeEnum < BaseEnum + PACKAGE_TYPE_NAMES = { + pypi: 'PyPI', + npm: 'NPM' + }.freeze + ::Packages::Package.package_types.keys.each do |package_type| - value package_type.to_s.upcase, "Packages from the #{package_type} package manager", value: package_type.to_s + type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize) + value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s end end end diff --git a/app/graphql/types/project_member_type.rb b/app/graphql/types/project_member_type.rb index f08781238d0..01731531ae2 100644 --- a/app/graphql/types/project_member_type.rb +++ b/app/graphql/types/project_member_type.rb @@ -12,7 +12,10 @@ module Types authorize :read_project field :project, Types::ProjectType, null: true, - description: 'Project that User is a member of', - resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.source_id).find } + description: 'Project that User is a member of' + + def project + Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.source_id).find + end end end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 0fd54af1538..c7fc193abe8 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -234,7 +234,7 @@ module Types Types::BoardType, null: true, description: 'A single board of the project', - resolver: Resolvers::BoardsResolver.single + resolver: Resolvers::BoardResolver field :jira_imports, Types::JiraImportType.connection_type, @@ -294,6 +294,12 @@ module Types description: 'Title of the label' end + field :terraform_states, + Types::Terraform::StateType.connection_type, + null: true, + description: 'Terraform states associated with the project', + resolver: Resolvers::Terraform::StatesResolver + def label(title:) BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args| LabelsFinder diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 447ac63a294..bd4b53bdaa7 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -49,8 +49,7 @@ module Types field :milestone, ::Types::MilestoneType, null: true, - description: 'Find a milestone', - resolve: -> (_obj, args, _ctx) { GitlabSchema.find_by_gid(args[:id]) } do + description: 'Find a milestone' do argument :id, ::Types::GlobalIDType[Milestone], required: true, description: 'Find a milestone by its ID' @@ -81,12 +80,26 @@ module Types description: 'Get statistics on the instance', resolver: Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver + field :runner_platforms, Types::Ci::RunnerPlatformType.connection_type, + null: true, description: 'Supported runner platforms', + resolver: Resolvers::Ci::RunnerPlatformsResolver + def design_management DesignManagementObject.new(nil) end def issue(id:) - GitlabSchema.object_from_id(id, expected_type: ::Issue) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Issue].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + + def milestone(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) end end end diff --git a/app/graphql/types/range_input_type.rb b/app/graphql/types/range_input_type.rb new file mode 100644 index 00000000000..766e523a99e --- /dev/null +++ b/app/graphql/types/range_input_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class RangeInputType < BaseInputObject + def self.[](type, closed = true) + @subtypes ||= {} + + @subtypes[[type, closed]] ||= Class.new(self) do + argument :start, type, + required: closed, + description: 'The start of the range' + + argument :end, type, + required: closed, + description: 'The end of the range' + end + end + + def prepare + if self[:end] && self[:start] && self[:end] < self[:start] + raise ::Gitlab::Graphql::Errors::ArgumentError, 'start must be before end' + end + + to_h + end + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb index 3acc1d9ca44..224e8c7ee03 100644 --- a/app/graphql/types/root_storage_statistics_type.rb +++ b/app/graphql/types/root_storage_statistics_type.rb @@ -13,5 +13,6 @@ module Types field :packages_size, GraphQL::FLOAT_TYPE, null: false, description: 'The packages size in bytes' field :wiki_size, GraphQL::FLOAT_TYPE, null: false, description: 'The wiki size in bytes' field :snippets_size, GraphQL::FLOAT_TYPE, null: false, description: 'The snippets size in bytes' + field :pipeline_artifacts_size, GraphQL::FLOAT_TYPE, null: false, description: 'The CI pipeline artifacts size in bytes' end end diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb index db98e62c10a..495c25c1776 100644 --- a/app/graphql/types/snippet_type.rb +++ b/app/graphql/types/snippet_type.rb @@ -24,16 +24,14 @@ module Types field :project, Types::ProjectType, description: 'The project the snippet is associated with', null: true, - authorize: :read_project, - resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, snippet.project_id).find } + authorize: :read_project # Author can be nil in some scenarios. For example, # when the admin setting restricted visibility # level is set to public field :author, Types::UserType, description: 'The owner of the snippet', - null: true, - resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, snippet.author_id).find } + null: true field :file_name, GraphQL::STRING_TYPE, description: 'File Name of the snippet', @@ -69,10 +67,11 @@ module Types null: false, deprecated: { reason: 'Use `blobs`', milestone: '13.3' } - field :blobs, type: [Types::Snippets::BlobType], + field :blobs, type: Types::Snippets::BlobType.connection_type, description: 'Snippet blobs', calls_gitaly: true, - null: false + null: true, + resolver: Resolvers::Snippets::BlobsResolver field :ssh_url_to_repo, type: GraphQL::STRING_TYPE, description: 'SSH URL to the snippet repository', @@ -85,5 +84,13 @@ module Types null: true markdown_field :description_html, null: true, method: :description + + def author + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find + end + + def project + Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find + end end end diff --git a/app/graphql/types/sort_enum.rb b/app/graphql/types/sort_enum.rb index 3245cb33e0d..d0a6eecb672 100644 --- a/app/graphql/types/sort_enum.rb +++ b/app/graphql/types/sort_enum.rb @@ -5,9 +5,16 @@ module Types graphql_name 'Sort' description 'Common sort values' - value 'updated_desc', 'Updated at descending order' - value 'updated_asc', 'Updated at ascending order' - value 'created_desc', 'Created at descending order' - value 'created_asc', 'Created at ascending order' + # Deprecated, as we prefer uppercase enums + # https://gitlab.com/groups/gitlab-org/-/epics/1838 + value 'updated_desc', 'Updated at descending order', deprecated: { reason: 'Use UPDATED_DESC', milestone: '13.5' } + value 'updated_asc', 'Updated at ascending order', deprecated: { reason: 'Use UPDATED_ASC', milestone: '13.5' } + value 'created_desc', 'Created at descending order', deprecated: { reason: 'Use CREATED_DESC', milestone: '13.5' } + value 'created_asc', 'Created at ascending order', deprecated: { reason: 'Use CREATED_ASC', milestone: '13.5' } + + value 'UPDATED_DESC', 'Updated at descending order', value: :updated_desc + value 'UPDATED_ASC', 'Updated at ascending order', value: :updated_asc + value 'CREATED_DESC', 'Created at descending order', value: :created_desc + value 'CREATED_ASC', 'Created at ascending order', value: :created_asc end end diff --git a/app/graphql/types/terraform/state_type.rb b/app/graphql/types/terraform/state_type.rb new file mode 100644 index 00000000000..f25f3a7789b --- /dev/null +++ b/app/graphql/types/terraform/state_type.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Types + module Terraform + class StateType < BaseObject + graphql_name 'TerraformState' + + authorize :read_terraform_state + + field :id, GraphQL::ID_TYPE, + null: false, + description: 'ID of the Terraform state' + + field :name, GraphQL::STRING_TYPE, + null: false, + description: 'Name of the Terraform state' + + field :locked_by_user, Types::UserType, + null: true, + authorize: :read_user, + description: 'The user currently holding a lock on the Terraform state', + resolve: -> (state, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, state.locked_by_user_id).find } + + field :locked_at, Types::TimeType, + null: true, + description: 'Timestamp the Terraform state was locked' + + field :created_at, Types::TimeType, + null: false, + description: 'Timestamp the Terraform state was created' + + field :updated_at, Types::TimeType, + null: false, + description: 'Timestamp the Terraform state was updated' + end + end +end diff --git a/app/graphql/types/timeframe_input_type.rb b/app/graphql/types/timeframe_input_type.rb new file mode 100644 index 00000000000..79c1bc5cf01 --- /dev/null +++ b/app/graphql/types/timeframe_input_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class TimeframeInputType < RangeInputType[::Types::DateType] + graphql_name 'Timeframe' + description 'A time-frame defined as a closed inclusive range of two dates' + end + # rubocop: enable Graphql/AuthorizeTypes +end |