diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /app/graphql | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/graphql')
183 files changed, 2691 insertions, 621 deletions
diff --git a/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb b/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb index c1b35d3eaf7..4ce9baff8cb 100644 --- a/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb +++ b/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb @@ -1,21 +1,25 @@ # frozen_string_literal: true module BatchLoaders - module AwardEmojiVotesBatchLoader - private + class AwardEmojiVotesBatchLoader + def self.load_upvotes(object, awardable_class: nil) + load_votes_for(object, AwardEmoji::UPVOTE_NAME, awardable_class: awardable_class) + end + + def self.load_downvotes(object, awardable_class: nil) + load_votes_for(object, AwardEmoji::DOWNVOTE_NAME, awardable_class: awardable_class) + end - def load_votes(object, vote_type) - BatchLoader::GraphQL.for(object.id).batch(key: "#{object.issuing_parent_id}-#{vote_type}") do |ids, loader, args| - counts = AwardEmoji.votes_for_collection(ids, object.class.name).named(vote_type).index_by(&:awardable_id) + def self.load_votes_for(object, vote_type, awardable_class: nil) + awardable_class ||= object.class.name + + BatchLoader::GraphQL.for(object.id).batch(key: "#{object.issuing_parent_id}-#{vote_type}") do |ids, loader, _args| + counts = AwardEmoji.votes_for_collection(ids, awardable_class).named(vote_type).index_by(&:awardable_id) ids.each do |id| loader.call(id, counts[id]&.count || 0) end end end - - def authorized_resource?(object) - Ability.allowed?(current_user, "read_#{object.to_ability_name}".to_sym, object) - end end end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 37adf4c2d3b..eed7959a2f1 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -20,7 +20,7 @@ class GitlabSchema < GraphQL::Schema use Gitlab::Graphql::GenericTracing use Gitlab::Graphql::Tracers::TimerTracer - use GraphQL::Subscriptions::ActionCableSubscriptions + use Gitlab::Graphql::Subscriptions::ActionCableWithLoadBalancing use BatchLoader::GraphQL use Gitlab::Graphql::Pagination::Connections use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb index 89656f1e018..d1798d2ade7 100644 --- a/app/graphql/graphql_triggers.rb +++ b/app/graphql/graphql_triggers.rb @@ -2,60 +2,62 @@ module GraphqlTriggers def self.issuable_assignees_updated(issuable) - GitlabSchema.subscriptions.trigger('issuableAssigneesUpdated', { issuable_id: issuable.to_gid }, issuable) + GitlabSchema.subscriptions.trigger(:issuable_assignees_updated, { issuable_id: issuable.to_gid }, issuable) end def self.issue_crm_contacts_updated(issue) - GitlabSchema.subscriptions.trigger('issueCrmContactsUpdated', { issuable_id: issue.to_gid }, issue) + GitlabSchema.subscriptions.trigger(:issue_crm_contacts_updated, { issuable_id: issue.to_gid }, issue) end def self.issuable_title_updated(issuable) - GitlabSchema.subscriptions.trigger('issuableTitleUpdated', { issuable_id: issuable.to_gid }, issuable) + GitlabSchema.subscriptions.trigger(:issuable_title_updated, { issuable_id: issuable.to_gid }, issuable) end def self.issuable_description_updated(issuable) - GitlabSchema.subscriptions.trigger('issuableDescriptionUpdated', { issuable_id: issuable.to_gid }, issuable) + GitlabSchema.subscriptions.trigger(:issuable_description_updated, { issuable_id: issuable.to_gid }, issuable) end def self.issuable_labels_updated(issuable) - GitlabSchema.subscriptions.trigger('issuableLabelsUpdated', { issuable_id: issuable.to_gid }, issuable) + GitlabSchema.subscriptions.trigger(:issuable_labels_updated, { issuable_id: issuable.to_gid }, issuable) end def self.issuable_dates_updated(issuable) - GitlabSchema.subscriptions.trigger('issuableDatesUpdated', { issuable_id: issuable.to_gid }, issuable) + GitlabSchema.subscriptions.trigger(:issuable_dates_updated, { issuable_id: issuable.to_gid }, issuable) end def self.issuable_milestone_updated(issuable) - GitlabSchema.subscriptions.trigger('issuableMilestoneUpdated', { issuable_id: issuable.to_gid }, issuable) + GitlabSchema.subscriptions.trigger(:issuable_milestone_updated, { issuable_id: issuable.to_gid }, issuable) end def self.work_item_note_created(work_item_gid, note_data) - GitlabSchema.subscriptions.trigger('workItemNoteCreated', { noteable_id: work_item_gid }, note_data) + GitlabSchema.subscriptions.trigger(:work_item_note_created, { noteable_id: work_item_gid }, note_data) end def self.work_item_note_deleted(work_item_gid, note_data) - GitlabSchema.subscriptions.trigger('workItemNoteDeleted', { noteable_id: work_item_gid }, note_data) + GitlabSchema.subscriptions.trigger(:work_item_note_deleted, { noteable_id: work_item_gid }, note_data) end def self.work_item_note_updated(work_item_gid, note_data) - GitlabSchema.subscriptions.trigger('workItemNoteUpdated', { noteable_id: work_item_gid }, note_data) + GitlabSchema.subscriptions.trigger(:work_item_note_updated, { noteable_id: work_item_gid }, note_data) end def self.merge_request_reviewers_updated(merge_request) GitlabSchema.subscriptions.trigger( - 'mergeRequestReviewersUpdated', { issuable_id: merge_request.to_gid }, merge_request + :merge_request_reviewers_updated, { issuable_id: merge_request.to_gid }, merge_request ) end def self.merge_request_merge_status_updated(merge_request) + return unless Feature.enabled?(:realtime_mr_status_change, merge_request.project) + GitlabSchema.subscriptions.trigger( - 'mergeRequestMergeStatusUpdated', { issuable_id: merge_request.to_gid }, merge_request + :merge_request_merge_status_updated, { issuable_id: merge_request.to_gid }, merge_request ) end def self.merge_request_approval_state_updated(merge_request) GitlabSchema.subscriptions.trigger( - 'mergeRequestApprovalStateUpdated', { issuable_id: merge_request.to_gid }, merge_request + :merge_request_approval_state_updated, { issuable_id: merge_request.to_gid }, merge_request ) end end diff --git a/app/graphql/mutations/achievements/award.rb b/app/graphql/mutations/achievements/award.rb new file mode 100644 index 00000000000..b486049594d --- /dev/null +++ b/app/graphql/mutations/achievements/award.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Mutations + module Achievements + class Award < BaseMutation + graphql_name 'AchievementsAward' + + include Gitlab::Graphql::Authorize::AuthorizeResource + + field :user_achievement, + ::Types::Achievements::UserAchievementType, + null: true, + description: 'Achievement award.' + + argument :achievement_id, ::Types::GlobalIDType[::Achievements::Achievement], + required: true, + description: 'Global ID of the achievement being awarded.' + + argument :user_id, ::Types::GlobalIDType[::User], + required: true, + description: 'Global ID of the user being awarded the achievement.' + + authorize :award_achievement + + def resolve(args) + achievement = authorized_find!(id: args[:achievement_id]) + + recipient_id = args[:user_id].model_id + result = ::Achievements::AwardService.new(current_user, achievement.id, recipient_id).execute + { user_achievement: result.payload, errors: result.errors } + end + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Achievements::Achievement) + end + end + end +end diff --git a/app/graphql/mutations/achievements/delete.rb b/app/graphql/mutations/achievements/delete.rb new file mode 100644 index 00000000000..0b510b44b4e --- /dev/null +++ b/app/graphql/mutations/achievements/delete.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Achievements + class Delete < BaseMutation + graphql_name 'AchievementsDelete' + + include Gitlab::Graphql::Authorize::AuthorizeResource + + field :achievement, + ::Types::Achievements::AchievementType, + null: true, + description: 'Achievement.' + + argument :achievement_id, ::Types::GlobalIDType[::Achievements::Achievement], + required: true, + description: 'Global ID of the achievement being deleted.' + + authorize :admin_achievement + + def resolve(args) + achievement = authorized_find!(id: args[:achievement_id]) + + result = ::Achievements::DestroyService.new(current_user, achievement).execute + { achievement: result.payload, errors: result.errors } + end + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Achievements::Achievement) + end + end + end +end diff --git a/app/graphql/mutations/achievements/revoke.rb b/app/graphql/mutations/achievements/revoke.rb new file mode 100644 index 00000000000..9d21b1c3741 --- /dev/null +++ b/app/graphql/mutations/achievements/revoke.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Achievements + class Revoke < BaseMutation + graphql_name 'AchievementsRevoke' + + include Gitlab::Graphql::Authorize::AuthorizeResource + + field :user_achievement, + ::Types::Achievements::UserAchievementType, + null: true, + description: 'Achievement award.' + + argument :user_achievement_id, ::Types::GlobalIDType[::Achievements::UserAchievement], + required: true, + description: 'Global ID of the user achievement being revoked.' + + authorize :award_achievement + + def resolve(args) + user_achievement = authorized_find!(id: args[:user_achievement_id]) + + result = ::Achievements::RevokeService.new(current_user, user_achievement).execute + { user_achievement: result.payload, errors: result.errors } + end + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Achievements::UserAchievement) + end + end + end +end diff --git a/app/graphql/mutations/achievements/update.rb b/app/graphql/mutations/achievements/update.rb new file mode 100644 index 00000000000..2a9e6580629 --- /dev/null +++ b/app/graphql/mutations/achievements/update.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Mutations + module Achievements + class Update < BaseMutation + graphql_name 'AchievementsUpdate' + + include Gitlab::Graphql::Authorize::AuthorizeResource + + field :achievement, + ::Types::Achievements::AchievementType, + null: true, + description: 'Achievement.' + + argument :achievement_id, ::Types::GlobalIDType[::Achievements::Achievement], + required: true, + description: 'Global ID of the achievement being updated.' + + argument :name, GraphQL::Types::String, + required: false, + description: 'Name for the achievement.' + + argument :avatar, ApolloUploadServer::Upload, + required: false, + description: 'Avatar for the achievement.' + + argument :description, GraphQL::Types::String, + required: false, + description: 'Description of or notes for the achievement.' + + authorize :admin_achievement + + def resolve(args) + achievement = authorized_find!(id: args[:achievement_id]) + + args.delete(:achievement_id) + result = ::Achievements::UpdateService.new(current_user, achievement, args).execute + { achievement: result.payload, errors: result.errors } + end + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::Achievements::Achievement) + end + end + end +end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index 2eef6bb9db7..771ace5510f 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -45,8 +45,6 @@ module Mutations namespace = project.namespace track_usage_event(event, current_user.id) - return unless Feature.enabled?(:route_hll_to_snowplow_phase2, namespace) - Gitlab::Tracking.event( self.class.to_s, event, diff --git a/app/graphql/mutations/award_emojis/base.rb b/app/graphql/mutations/award_emojis/base.rb index dc2d46269e6..65065de0de4 100644 --- a/app/graphql/mutations/award_emojis/base.rb +++ b/app/graphql/mutations/award_emojis/base.rb @@ -3,8 +3,6 @@ module Mutations module AwardEmojis class Base < BaseMutation - include ::Mutations::FindsByGid - NOT_EMOJI_AWARDABLE = 'You cannot award emoji to this resource.' authorize :award_emoji diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index 5f98b222099..994668b5f8f 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -28,7 +28,7 @@ module Mutations end def ready?(**args) - raise_resource_not_available_error! ERROR_MESSAGE if Gitlab::Database.read_only? + raise_resource_not_available_error!(ERROR_MESSAGE) if read_only? missing_args = self.class.arguments.values .reject { |arg| arg.accepts?(args.fetch(arg.keyword, :not_given)) } @@ -39,6 +39,10 @@ module Mutations true end + def read_only? + Gitlab::Database.read_only? + end + def load_application_object(argument, id, context) ::Gitlab::Graphql::Lazy.new { super } end diff --git a/app/graphql/mutations/boards/update.rb b/app/graphql/mutations/boards/update.rb index 7cfce9d2d91..f611608d1b6 100644 --- a/app/graphql/mutations/boards/update.rb +++ b/app/graphql/mutations/boards/update.rb @@ -29,12 +29,6 @@ module Mutations errors: errors_on_object(board) } end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/ci/ci_cd_settings_update.rb b/app/graphql/mutations/ci/ci_cd_settings_update.rb index 98b8e9567e7..a7d99d2a496 100644 --- a/app/graphql/mutations/ci/ci_cd_settings_update.rb +++ b/app/graphql/mutations/ci/ci_cd_settings_update.rb @@ -2,9 +2,16 @@ module Mutations module Ci - # TODO: Remove in 16.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87002 + # TODO: Remove after 16.0, see https://gitlab.com/gitlab-org/gitlab/-/issues/361801#note_1373963840 class CiCdSettingsUpdate < ProjectCiCdSettingsUpdate graphql_name 'CiCdSettingsUpdate' + + def ready?(**args) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, '`remove_cicd_settings_update` feature flag is enabled.' \ + if Feature.enabled?(:remove_cicd_settings_update) + + super + end end end end diff --git a/app/graphql/mutations/ci/job_artifact/bulk_destroy.rb b/app/graphql/mutations/ci/job_artifact/bulk_destroy.rb new file mode 100644 index 00000000000..53036496de4 --- /dev/null +++ b/app/graphql/mutations/ci/job_artifact/bulk_destroy.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module JobArtifact + class BulkDestroy < BaseMutation + graphql_name 'BulkDestroyJobArtifacts' + + authorize :destroy_artifacts + + ArtifactId = ::Types::GlobalIDType[::Ci::JobArtifact] + ProjectId = ::Types::GlobalIDType[::Project] + + argument :ids, [ArtifactId], + required: true, + description: 'Global IDs of the job artifacts to destroy.', + prepare: ->(global_ids, _ctx) { GitlabSchema.parse_gids(global_ids, expected_type: ::Ci::JobArtifact) } + + argument :project_id, ProjectId, + required: true, + description: 'Global Project ID of the job artifacts to destroy. Incompatible with projectPath.' + + field :destroyed_count, ::GraphQL::Types::Int, + null: true, + description: 'Number of job artifacts deleted.' + + field :destroyed_ids, [ArtifactId], + null: true, + description: 'IDs of job artifacts that were deleted.' + + def find_object(id:) + GlobalID::Locator.locate(id) + end + + def resolve(**args) + ids = args[:ids] + project_id = args[:project_id] + + project = authorized_find!(id: project_id) + + if Feature.disabled?(:ci_job_artifact_bulk_destroy, project) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + '`ci_job_artifact_bulk_destroy` feature flag is disabled.' + end + + raise Gitlab::Graphql::Errors::ArgumentError, 'IDs array of job artifacts can not be empty' if ids.empty? + + result = ::Ci::JobArtifacts::BulkDeleteByProjectService.new( + job_artifact_ids: model_ids_of(ids), + current_user: current_user, + project: project + ).execute + + if result.success? + result.payload.slice(:destroyed_count, :destroyed_ids).merge(errors: []) + else + { errors: result.errors } + end + end + + private + + def model_ids_of(global_ids) + global_ids.filter_map { |gid| gid.model_id.to_i } + end + end + end + end +end diff --git a/app/graphql/mutations/ci/job_token_scope/add_project.rb b/app/graphql/mutations/ci/job_token_scope/add_project.rb index 6f0f87b47a1..6071d6750c2 100644 --- a/app/graphql/mutations/ci/job_token_scope/add_project.rb +++ b/app/graphql/mutations/ci/job_token_scope/add_project.rb @@ -21,16 +21,28 @@ module Mutations argument :direction, ::Types::Ci::JobTokenScope::DirectionEnum, required: false, - description: 'Direction of access, which defaults to outbound.' + deprecated: { + reason: 'Outbound job token scope is being removed. This field can now only be set to INBOUND', + milestone: '16.0' + }, + description: 'Direction of access, which defaults to INBOUND.' field :ci_job_token_scope, Types::Ci::JobTokenScopeType, null: true, description: "CI job token's access scope." - def resolve(project_path:, target_project_path:, direction: :outbound) + def resolve(project_path:, target_project_path:, direction: nil) project = authorized_find!(project_path) target_project = Project.find_by_full_path(target_project_path) + frozen_outbound = project.frozen_outbound_job_token_scopes? + + if direction == :outbound && frozen_outbound + raise Gitlab::Graphql::Errors::ArgumentError, 'direction: OUTBOUND scope entries can only be removed. ' \ + 'Only INBOUND scope can be expanded.' + end + + direction ||= frozen_outbound ? :inbound : :outbound result = ::Ci::JobTokenScope::AddProjectService .new(project, current_user) diff --git a/app/graphql/mutations/ci/pipeline_schedule/take_ownership.rb b/app/graphql/mutations/ci/pipeline_schedule/take_ownership.rb index 2e4312f0045..d71ef738cab 100644 --- a/app/graphql/mutations/ci/pipeline_schedule/take_ownership.rb +++ b/app/graphql/mutations/ci/pipeline_schedule/take_ownership.rb @@ -6,7 +6,7 @@ module Mutations class TakeOwnership < Base graphql_name 'PipelineScheduleTakeOwnership' - authorize :take_ownership_pipeline_schedule + authorize :admin_pipeline_schedule field :pipeline_schedule, Types::Ci::PipelineScheduleType, diff --git a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb index d214aa46cfc..d4e55fd1792 100644 --- a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb +++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb @@ -19,6 +19,10 @@ module Mutations argument :job_token_scope_enabled, GraphQL::Types::Boolean, required: false, + deprecated: { + reason: 'Outbound job token scope is being removed. This field can now only be set to false', + milestone: '16.0' + }, description: 'Indicates CI/CD job tokens generated in this project ' \ 'have restricted access to other projects.' @@ -27,10 +31,6 @@ module Mutations description: 'Indicates CI/CD job tokens generated in other projects ' \ 'have restricted access to this project.' - argument :opt_in_jwt, GraphQL::Types::Boolean, - required: false, - description: 'When disabled, the JSON Web Token is always available in all jobs in the pipeline.' - field :ci_cd_settings, Types::Ci::CiCdSettingType, null: false, @@ -39,7 +39,9 @@ module Mutations def resolve(full_path:, **args) project = authorized_find!(full_path) - args.delete(:inbound_job_token_scope_enabled) unless Feature.enabled?(:ci_inbound_job_token_scope, project) + if args[:job_token_scope_enabled] && project.frozen_outbound_job_token_scopes? + raise Gitlab::Graphql::Errors::ArgumentError, 'job_token_scope_enabled can only be set to false' + end settings = project.ci_cd_settings settings.update(args) diff --git a/app/graphql/mutations/ci/runner/common_mutation_arguments.rb b/app/graphql/mutations/ci/runner/common_mutation_arguments.rb new file mode 100644 index 00000000000..f4fbd0a38c7 --- /dev/null +++ b/app/graphql/mutations/ci/runner/common_mutation_arguments.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Runner + module CommonMutationArguments + extend ActiveSupport::Concern + + included do + argument :description, GraphQL::Types::String, + required: false, + description: 'Description of the runner.' + + argument :maintenance_note, GraphQL::Types::String, + required: false, + description: 'Runner\'s maintenance notes.' + + argument :maximum_timeout, GraphQL::Types::Int, + required: false, + description: 'Maximum timeout (in seconds) for jobs processed by the runner.' + + argument :access_level, ::Types::Ci::RunnerAccessLevelEnum, + required: false, + description: 'Access level of the runner.' + + argument :paused, GraphQL::Types::Boolean, + required: false, + description: 'Indicates the runner is not allowed to receive jobs.' + + argument :locked, GraphQL::Types::Boolean, + required: false, + description: 'Indicates the runner is locked.' + + argument :run_untagged, GraphQL::Types::Boolean, + required: false, + description: 'Indicates the runner is able to run untagged jobs.' + + argument :tag_list, [GraphQL::Types::String], + required: false, + description: 'Tags associated with the runner.' + end + end + end + end +end diff --git a/app/graphql/mutations/ci/runner/create.rb b/app/graphql/mutations/ci/runner/create.rb new file mode 100644 index 00000000000..7eca6c27d10 --- /dev/null +++ b/app/graphql/mutations/ci/runner/create.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Runner + class Create < BaseMutation + graphql_name 'RunnerCreate' + + authorize :create_runner + + include Mutations::Ci::Runner::CommonMutationArguments + + argument :runner_type, ::Types::Ci::RunnerTypeEnum, + required: true, + description: 'Type of the runner to create.' + + argument :group_id, ::Types::GlobalIDType[Group], + required: false, + description: 'Global ID of the group that the runner is created in (valid only for group runner).' + + argument :project_id, ::Types::GlobalIDType[Project], + required: false, + description: 'Global ID of the project that the runner is created in (valid only for project runner).' + + field :runner, + Types::Ci::RunnerType, + null: true, + description: 'Runner after mutation.' + + def ready?(**args) + case args[:runner_type] + when 'group_type' + raise Gitlab::Graphql::Errors::ArgumentError, '`group_id` is missing' unless args[:group_id].present? + when 'project_type' + raise Gitlab::Graphql::Errors::ArgumentError, '`project_id` is missing' unless args[:project_id].present? + end + + parse_gid(**args) + + check_feature_flag(**args) + + super + end + + def resolve(**args) + case args[:runner_type] + when 'group_type', 'project_type' + args[:scope] = authorized_find!(**args) + args.except!(:group_id, :project_id) + else + raise_resource_not_available_error! unless current_user.can?(:create_instance_runner) + end + + response = { runner: nil, errors: [] } + result = ::Ci::Runners::CreateRunnerService.new(user: current_user, params: args).execute + + if result.success? + response[:runner] = result.payload[:runner] + else + response[:errors] = result.errors + end + + response + end + + private + + def find_object(**args) + obj = parse_gid(**args) + + GitlabSchema.find_by_gid(obj) if obj + end + + def parse_gid(runner_type:, **args) + case runner_type + when 'group_type' + GitlabSchema.parse_gid(args[:group_id], expected_type: ::Group) + when 'project_type' + GitlabSchema.parse_gid(args[:project_id], expected_type: ::Project) + end + end + + def check_feature_flag(**args) + case args[:runner_type] + when 'instance_type' + if Feature.disabled?(:create_runner_workflow_for_admin, current_user) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + '`create_runner_workflow_for_admin` feature flag is disabled.' + end + when 'group_type' + namespace = find_object(**args).sync + if Feature.disabled?(:create_runner_workflow_for_namespace, namespace) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + '`create_runner_workflow_for_namespace` feature flag is disabled.' + end + when 'project_type' + project = find_object(**args).sync + if project && Feature.disabled?(:create_runner_workflow_for_namespace, project.namespace) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + '`create_runner_workflow_for_namespace` feature flag is disabled.' + end + end + end + end + end + end +end diff --git a/app/graphql/mutations/ci/runner/delete.rb b/app/graphql/mutations/ci/runner/delete.rb index db68914a4eb..ba309ca754d 100644 --- a/app/graphql/mutations/ci/runner/delete.rb +++ b/app/graphql/mutations/ci/runner/delete.rb @@ -15,16 +15,12 @@ module Mutations description: 'ID of the runner to delete.' def resolve(id:, **runner_attrs) - runner = authorized_find!(id) + runner = authorized_find!(id: id) ::Ci::Runners::UnregisterRunnerService.new(runner, current_user).execute { errors: runner.errors.full_messages } end - - def find_object(id) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb index 4f0bf19f09c..da28397bb71 100644 --- a/app/graphql/mutations/ci/runner/update.rb +++ b/app/graphql/mutations/ci/runner/update.rb @@ -8,53 +8,23 @@ module Mutations authorize :update_runner + include Mutations::Ci::Runner::CommonMutationArguments + RunnerID = ::Types::GlobalIDType[::Ci::Runner] argument :id, RunnerID, required: true, description: 'ID of the runner to update.' - argument :description, GraphQL::Types::String, - required: false, - description: 'Description of the runner.' - - argument :maintenance_note, GraphQL::Types::String, - required: false, - description: 'Runner\'s maintenance notes.' - - argument :maximum_timeout, GraphQL::Types::Int, - required: false, - description: 'Maximum timeout (in seconds) for jobs processed by the runner.' - - argument :access_level, ::Types::Ci::RunnerAccessLevelEnum, - required: false, - description: 'Access level of the runner.' - argument :active, GraphQL::Types::Boolean, required: false, description: 'Indicates the runner is allowed to receive jobs.', deprecated: { reason: :renamed, replacement: 'paused', milestone: '14.8' } - argument :paused, GraphQL::Types::Boolean, - required: false, - description: 'Indicates the runner is not allowed to receive jobs.' - - argument :locked, GraphQL::Types::Boolean, - required: false, - description: 'Indicates the runner is locked.' - - argument :run_untagged, GraphQL::Types::Boolean, - required: false, - description: 'Indicates the runner is able to run untagged jobs.' - - argument :tag_list, [GraphQL::Types::String], - required: false, - description: 'Tags associated with the runner.' - argument :associated_projects, [::Types::GlobalIDType[::Project]], required: false, description: 'Projects associated with the runner. Available only for project runners.', - prepare: ->(global_ids, ctx) { global_ids&.filter_map { |gid| gid.model_id.to_i } } + prepare: ->(global_ids, _ctx) { global_ids&.filter_map { |gid| gid.model_id.to_i } } field :runner, Types::Ci::RunnerType, @@ -62,7 +32,7 @@ module Mutations description: 'Runner after mutation.' def resolve(id:, **runner_attrs) - runner = authorized_find!(id) + runner = authorized_find!(id: id) associated_projects_ids = runner_attrs.delete(:associated_projects) @@ -75,10 +45,6 @@ module Mutations response end - def find_object(id) - GitlabSchema.find_by_gid(id) - end - private def associate_runner_projects(response, runner, associated_project_ids) diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb index c10e1633350..e717ff4d798 100644 --- a/app/graphql/mutations/clusters/agent_tokens/create.rb +++ b/app/graphql/mutations/clusters/agent_tokens/create.rb @@ -40,9 +40,9 @@ module Mutations result = ::Clusters::AgentTokens::CreateService .new( - container: cluster_agent.project, + agent: cluster_agent, current_user: current_user, - params: args.merge(agent_id: cluster_agent.id) + params: args ) .execute @@ -54,12 +54,6 @@ module Mutations errors: Array.wrap(result.message) } end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/clusters/agent_tokens/revoke.rb b/app/graphql/mutations/clusters/agent_tokens/revoke.rb index 974db976f1d..c4187746464 100644 --- a/app/graphql/mutations/clusters/agent_tokens/revoke.rb +++ b/app/graphql/mutations/clusters/agent_tokens/revoke.rb @@ -16,15 +16,10 @@ module Mutations def resolve(id:) token = authorized_find!(id: id) - token.update(status: token.class.statuses[:revoked]) - { errors: errors_on_object(token) } - end + ::Clusters::AgentTokens::RevokeService.new(token: token, current_user: current_user).execute - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) + { errors: errors_on_object(token) } end end end diff --git a/app/graphql/mutations/clusters/agents/delete.rb b/app/graphql/mutations/clusters/agents/delete.rb index fb482e02794..ddb4e36a68e 100644 --- a/app/graphql/mutations/clusters/agents/delete.rb +++ b/app/graphql/mutations/clusters/agents/delete.rb @@ -24,12 +24,6 @@ module Mutations errors: Array.wrap(result.message) } end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/concerns/mutations/finds_by_gid.rb b/app/graphql/mutations/concerns/mutations/finds_by_gid.rb deleted file mode 100644 index 157f87a413d..00000000000 --- a/app/graphql/mutations/concerns/mutations/finds_by_gid.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module FindsByGid - def find_object(id:) - GitlabSchema.find_by_gid(id) - end - end -end diff --git a/app/graphql/mutations/concerns/mutations/finds_namespace.rb b/app/graphql/mutations/concerns/mutations/finds_namespace.rb new file mode 100644 index 00000000000..bc9dfbcffe5 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/finds_namespace.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Mutations + module FindsNamespace + private + + def find_object(full_path) + Routable.find_by_full_path(full_path) + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/spam_protection.rb b/app/graphql/mutations/concerns/mutations/spam_protection.rb index e61f66c02a5..4c1ce0c8c5e 100644 --- a/app/graphql/mutations/concerns/mutations/spam_protection.rb +++ b/app/graphql/mutations/concerns/mutations/spam_protection.rb @@ -26,8 +26,6 @@ module Mutations elsif fields[:needs_captcha_response] fields.delete :spam raise NeedsCaptchaResponseError.new(NEEDS_CAPTCHA_RESPONSE_MESSAGE, extensions: fields) - else - nil end end end diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb index 6738f268e92..f009abdba70 100644 --- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb +++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb @@ -36,6 +36,18 @@ module Mutations argument :milestone_widget, ::Types::WorkItems::Widgets::MilestoneInputType, required: false, description: 'Input for milestone widget.' + argument :notifications_widget, + ::Types::WorkItems::Widgets::NotificationsUpdateInputType, + required: false, + description: 'Input for notifications widget.' + argument :current_user_todos_widget, + ::Types::WorkItems::Widgets::CurrentUserTodosInputType, + required: false, + description: 'Input for to-dos widget.' + argument :award_emoji_widget, + ::Types::WorkItems::Widgets::AwardEmojiUpdateInputType, + required: false, + description: 'Input for award emoji widget.' end end end diff --git a/app/graphql/mutations/container_repositories/destroy_base.rb b/app/graphql/mutations/container_repositories/destroy_base.rb index 1c2c4d87a5f..46851c15702 100644 --- a/app/graphql/mutations/container_repositories/destroy_base.rb +++ b/app/graphql/mutations/container_repositories/destroy_base.rb @@ -4,12 +4,6 @@ module Mutations module ContainerRepositories class DestroyBase < Mutations::BaseMutation include ::Mutations::PackageEventable - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/design_management/update.rb b/app/graphql/mutations/design_management/update.rb new file mode 100644 index 00000000000..67732b70f29 --- /dev/null +++ b/app/graphql/mutations/design_management/update.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module DesignManagement + class Update < ::Mutations::BaseMutation + graphql_name "DesignManagementUpdate" + + authorize :update_design + + argument :id, ::Types::GlobalIDType[::DesignManagement::Design], + required: true, + description: "ID of the design to update." + + argument :description, GraphQL::Types::String, + required: false, + description: copy_field_description(Types::DesignManagement::DesignType, :description) + + field :design, Types::DesignManagement::DesignType, + null: false, + description: "Updated design." + + def resolve(id:, description:) + design = authorized_find!(id: id) + design.update(description: description) + + { + design: design.reset, + errors: errors_on_object(design) + } + end + end + end +end diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb index fce6e4f416f..dc5731add3a 100644 --- a/app/graphql/mutations/discussions/toggle_resolve.rb +++ b/app/graphql/mutations/discussions/toggle_resolve.rb @@ -53,10 +53,6 @@ module Mutations end end - def find_object(id:) - GitlabSchema.find_by_gid(id) - end - def resolve!(discussion) ::Discussions::ResolveService.new( discussion.project, diff --git a/app/graphql/mutations/environments/canary_ingress/update.rb b/app/graphql/mutations/environments/canary_ingress/update.rb index 1cddfdd815b..43e9b6c0881 100644 --- a/app/graphql/mutations/environments/canary_ingress/update.rb +++ b/app/graphql/mutations/environments/canary_ingress/update.rb @@ -35,10 +35,6 @@ module Mutations { errors: Array.wrap(result[:message]) } end - def find_object(id:) - GitlabSchema.find_by_gid(id) - end - private def certificate_based_clusters_enabled? diff --git a/app/graphql/mutations/environments/stop.rb b/app/graphql/mutations/environments/stop.rb new file mode 100644 index 00000000000..b0b87b9b534 --- /dev/null +++ b/app/graphql/mutations/environments/stop.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mutations + module Environments + class Stop < ::Mutations::BaseMutation + graphql_name 'EnvironmentStop' + description 'Stop an environment.' + + authorize :stop_environment + + argument :id, + ::Types::GlobalIDType[::Environment], + required: true, + description: 'Global ID of the environment to stop.' + + argument :force, + GraphQL::Types::Boolean, + required: false, + description: 'Force environment to stop without executing on_stop actions.' + + field :environment, + Types::EnvironmentType, + null: true, + description: 'Environment after attempt to stop.' + + def resolve(id:, **kwargs) + environment = authorized_find!(id: id) + + response = ::Environments::StopService.new(environment.project, current_user, kwargs).execute(environment) + + if response.success? + { environment: response.payload[:environment], errors: [] } + else + { environment: response.payload[:environment], errors: response.errors } + end + end + end + end +end diff --git a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb index bb1da9278ff..c29dc98c872 100644 --- a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb +++ b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb @@ -35,7 +35,7 @@ module Mutations end def authorize!(object) - raise_noteable_not_incident! if object && !object.try(:incident?) + raise_noteable_not_incident! if object && !object.try(:incident_type_issue?) super end diff --git a/app/graphql/mutations/issues/bulk_update.rb b/app/graphql/mutations/issues/bulk_update.rb index 3d80f119079..9c9dd3cf2fc 100644 --- a/app/graphql/mutations/issues/bulk_update.rb +++ b/app/graphql/mutations/issues/bulk_update.rb @@ -14,7 +14,8 @@ module Mutations argument :parent_id, ::Types::GlobalIDType[::IssueParent], required: true, - description: 'Global ID of the parent that the bulk update will be scoped to . ' \ + description: 'Global ID of the parent to which the bulk update will be scoped. ' \ + 'The parent can be a project **(FREE)** or a group **(PREMIUM)**. ' \ 'Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`.' argument :ids, [::Types::GlobalIDType[::Issue]], @@ -31,6 +32,22 @@ module Mutations required: false, description: 'Global ID of the milestone that will be assigned to the issues.' + argument :state_event, Types::IssueStateEventEnum, + description: 'Close or reopen an issue.', + required: false + + argument :add_label_ids, [::Types::GlobalIDType[::Label]], + description: 'Global ID array of the labels that will be added to the issues. ', + required: false + + argument :remove_label_ids, [::Types::GlobalIDType[::Label]], + description: 'Global ID array of the labels that will be removed from the issues. ', + required: false + + argument :subscription_event, Types::IssuableSubscriptionEventEnum, + description: 'Subscribe to or unsubscribe from issue notifications.', + required: false + field :updated_issue_count, GraphQL::Types::Int, null: true, description: 'Number of issues that were successfully updated.' @@ -74,7 +91,7 @@ module Mutations end def prepared_params(attributes, ids) - prepared = { issuable_ids: model_ids_from(ids).uniq } + prepared = attributes.except(*global_id_arguments).merge(issuable_ids: model_ids_from(ids).uniq) global_id_arguments.each do |argument| next unless attributes.key?(argument) @@ -92,7 +109,7 @@ module Mutations end def global_id_arguments - %i[assignee_ids milestone_id] + %i[assignee_ids milestone_id add_label_ids remove_label_ids] end def model_ids_from(attributes) diff --git a/app/graphql/mutations/members/bulk_update_base.rb b/app/graphql/mutations/members/bulk_update_base.rb new file mode 100644 index 00000000000..1e0208e864d --- /dev/null +++ b/app/graphql/mutations/members/bulk_update_base.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Mutations + module Members + class BulkUpdateBase < BaseMutation + include ::API::Helpers::MembersHelpers + + argument :user_ids, + [::Types::GlobalIDType[::User]], + required: true, + description: 'Global IDs of the members.' + + argument :access_level, + ::Types::MemberAccessLevelEnum, + required: true, + description: 'Access level to update the members to.' + + argument :expires_at, + Types::TimeType, + required: false, + description: 'Date and time the membership expires.' + + MAX_MEMBERS_UPDATE_LIMIT = 50 + MAX_MEMBERS_UPDATE_ERROR = "Count of members to be updated should be less than #{MAX_MEMBERS_UPDATE_LIMIT}." + .freeze + INVALID_MEMBERS_ERROR = 'Only access level of direct members can be updated.' + + def resolve(**args) + result = ::Members::UpdateService + .new(current_user, args.except(:user_ids, source_id_param_name)) + .execute(@updatable_members) + + { + source_members_key => result[:members], + errors: Array.wrap(result[:message]) + } + rescue Gitlab::Access::AccessDeniedError + { + errors: ["Unable to update members, please check user permissions."] + } + end + + private + + def ready?(**args) + source = authorized_find!(source_id: args[source_id_param_name]) + user_ids = args.fetch(:user_ids, {}).map(&:model_id) + @updatable_members = only_direct_members(source, user_ids) + + if @updatable_members.size > MAX_MEMBERS_UPDATE_LIMIT + raise Gitlab::Graphql::Errors::InvalidMemberCountError, MAX_MEMBERS_UPDATE_ERROR + end + + if @updatable_members.size != user_ids.size + raise Gitlab::Graphql::Errors::InvalidMembersError, INVALID_MEMBERS_ERROR + end + + super + end + + def find_object(source_id:) + GitlabSchema.object_from_id(source_id, expected_type: source_type) + end + + def only_direct_members(source, user_ids) + source_members(source) + .with_user(user_ids) + .to_a + end + + def source_id_param_name + "#{source_name}_id".to_sym + end + + def source_members_key + "#{source_name}_members".to_sym + end + + def source_name + source_type.name.downcase + end + + def source_type + raise NotImplementedError + end + end + end +end diff --git a/app/graphql/mutations/members/groups/bulk_update.rb b/app/graphql/mutations/members/groups/bulk_update.rb index d0b19bd9634..fe3c7521c20 100644 --- a/app/graphql/mutations/members/groups/bulk_update.rb +++ b/app/graphql/mutations/members/groups/bulk_update.rb @@ -3,81 +3,22 @@ module Mutations module Members module Groups - class BulkUpdate < ::Mutations::BaseMutation + class BulkUpdate < BulkUpdateBase graphql_name 'GroupMemberBulkUpdate' - - include Gitlab::Utils::StrongMemoize - authorize :admin_group_member field :group_members, - [Types::GroupMemberType], - null: true, - description: 'Group members after mutation.' + [Types::GroupMemberType], + null: true, + description: 'Group members after mutation.' argument :group_id, - ::Types::GlobalIDType[::Group], - required: true, - description: 'Global ID of the group.' - - argument :user_ids, - [::Types::GlobalIDType[::User]], - required: true, - description: 'Global IDs of the group members.' - - argument :access_level, - ::Types::MemberAccessLevelEnum, - required: true, - description: 'Access level to update the members to.' - - argument :expires_at, - Types::TimeType, - required: false, - description: 'Date and time the membership expires.' - - MAX_MEMBERS_UPDATE_LIMIT = 50 - MAX_MEMBERS_UPDATE_ERROR = "Count of members to be updated should be less than #{MAX_MEMBERS_UPDATE_LIMIT}." - INVALID_MEMBERS_ERROR = 'Only access level of direct members can be updated.' - - def resolve(group_id:, **args) - result = ::Members::UpdateService.new(current_user, args.except(:user_ids)).execute(@updatable_group_members) - - { - group_members: result[:members], - errors: Array.wrap(result[:message]) - } - rescue Gitlab::Access::AccessDeniedError - { - errors: ["Unable to update members, please check user permissions."] - } - end - - private - - def ready?(**args) - group = authorized_find!(group_id: args[:group_id]) - user_ids = args.fetch(:user_ids, {}).map(&:model_id) - @updatable_group_members = only_direct_group_members(group, user_ids) - - if @updatable_group_members.size > MAX_MEMBERS_UPDATE_LIMIT - raise Gitlab::Graphql::Errors::InvalidMemberCountError, MAX_MEMBERS_UPDATE_ERROR - end - - if @updatable_group_members.size != user_ids.size - raise Gitlab::Graphql::Errors::InvalidMembersError, INVALID_MEMBERS_ERROR - end - - super - end - - def find_object(group_id:) - GitlabSchema.object_from_id(group_id, expected_type: ::Group) - end + ::Types::GlobalIDType[::Group], + required: true, + description: 'Global ID of the group.' - def only_direct_group_members(group, user_ids) - group - .members - .with_user(user_ids).to_a + def source_type + ::Group end end end diff --git a/app/graphql/mutations/members/projects/bulk_update.rb b/app/graphql/mutations/members/projects/bulk_update.rb new file mode 100644 index 00000000000..9bf7968670e --- /dev/null +++ b/app/graphql/mutations/members/projects/bulk_update.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module Members + module Projects + class BulkUpdate < BulkUpdateBase + graphql_name 'ProjectMemberBulkUpdate' + description 'Updates multiple members of a project. ' \ + 'To use this mutation, you must have at least the Maintainer role.' + + authorize :admin_project_member + + field :project_members, + [Types::ProjectMemberType], + null: true, + description: 'Project members after mutation.' + + argument :project_id, + ::Types::GlobalIDType[::Project], + required: true, + description: 'Global ID of the project.' + + def source_type + ::Project + end + end + end + end +end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb index 2e7c0c5a2f9..296efa19bb7 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb @@ -10,7 +10,7 @@ module Mutations ANNOTATION_SOURCE_ARGUMENT_ERROR = 'Either a cluster or environment global id is required' INVALID_ANNOTATION_SOURCE_ERROR = 'Invalid cluster or environment id' - authorize :create_metrics_dashboard_annotation + authorize :admin_metrics_dashboard_annotation field :annotation, Types::Metrics::Dashboards::AnnotationType, @@ -75,6 +75,8 @@ module Mutations private def ready?(**args) + raise_resource_not_available_error! if Feature.enabled?(:remove_monitor_metrics) + # Raise error if both cluster_id and environment_id are present or neither is present unless args[:cluster_id].present? ^ args[:environment_id].present? raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR @@ -83,10 +85,6 @@ module Mutations super(**args) end - def find_object(id:) - GitlabSchema.find_by_gid(id) - end - def annotation_create_params(args) annotation_source = AnnotationSource.new(object: annotation_source(args)) diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb index e0fadff13d4..32047cda213 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb @@ -7,13 +7,15 @@ module Mutations class Delete < Base graphql_name 'DeleteAnnotation' - authorize :delete_metrics_dashboard_annotation + authorize :admin_metrics_dashboard_annotation argument :id, ::Types::GlobalIDType[::Metrics::Dashboard::Annotation], required: true, description: 'Global ID of the annotation to delete.' def resolve(id:) + raise_resource_not_available_error! if Feature.enabled?(:remove_monitor_metrics) + annotation = authorized_find!(id: id) result = ::Metrics::Dashboard::Annotations::DeleteService.new(context[:current_user], annotation).execute diff --git a/app/graphql/mutations/notes/base.rb b/app/graphql/mutations/notes/base.rb index fb74805db17..d656835c335 100644 --- a/app/graphql/mutations/notes/base.rb +++ b/app/graphql/mutations/notes/base.rb @@ -13,12 +13,6 @@ module Mutations Types::Notes::NoteType, null: true, description: 'Note after mutation.' - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/notes/create/base.rb b/app/graphql/mutations/notes/create/base.rb index f48e62af767..69cd1426218 100644 --- a/app/graphql/mutations/notes/create/base.rb +++ b/app/graphql/mutations/notes/create/base.rb @@ -47,10 +47,6 @@ module Mutations private - def find_object(id:) - GitlabSchema.find_by_gid(id) - end - def create_note_params(noteable, args) { noteable: noteable, diff --git a/app/graphql/mutations/packages/destroy.rb b/app/graphql/mutations/packages/destroy.rb index a398b1ff9dc..95832ec8b85 100644 --- a/app/graphql/mutations/packages/destroy.rb +++ b/app/graphql/mutations/packages/destroy.rb @@ -23,12 +23,6 @@ module Mutations errors: errors } end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/packages/destroy_file.rb b/app/graphql/mutations/packages/destroy_file.rb index f2a8f2b853a..c7dd2df704e 100644 --- a/app/graphql/mutations/packages/destroy_file.rb +++ b/app/graphql/mutations/packages/destroy_file.rb @@ -21,12 +21,6 @@ module Mutations { errors: package_file.errors.full_messages } end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/projects/sync_fork.rb b/app/graphql/mutations/projects/sync_fork.rb new file mode 100644 index 00000000000..4520f6388c5 --- /dev/null +++ b/app/graphql/mutations/projects/sync_fork.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Mutations + module Projects + class SyncFork < BaseMutation + graphql_name 'ProjectSyncFork' + + include FindsProject + + argument :project_path, GraphQL::Types::ID, + required: true, + description: 'Full path of the project to initialize.' + + argument :target_branch, GraphQL::Types::String, + required: true, + description: 'Ref of the fork to fetch into.' + + field :details, Types::Projects::ForkDetailsType, + null: true, + description: 'Updated fork details.' + + def resolve(project_path:, target_branch:) + project = authorized_find!(project_path, target_branch) + + return respond(nil, ['Feature flag is disabled']) unless Feature.enabled?(:synchronize_fork, + project.fork_source) + + return respond(nil, ['Target branch does not exist']) unless project.repository.branch_exists?(target_branch) + + details_resolver = Resolvers::Projects::ForkDetailsResolver.new(object: project, context: context, field: nil) + details = details_resolver.resolve(ref: target_branch) + + return respond(nil, ['This branch of this project cannot be updated from the upstream']) unless details + + enqueue_sync_fork(project, target_branch, details) + end + + def enqueue_sync_fork(project, target_branch, details) + return respond(details, []) if details.counts[:behind] == 0 + + if details.has_conflicts? + return respond(details, ['The synchronization cannot happen due to the merge conflict']) + end + + return respond(details, ['This service has been called too many times.']) if rate_limit_throttled?(project) + return respond(details, ['Another fork sync is already in progress']) unless details.exclusive_lease.try_obtain + + ::Projects::Forks::SyncWorker.perform_async(project.id, current_user.id, target_branch) # rubocop:disable CodeReuse/Worker + + respond(details, []) + end + + def rate_limit_throttled?(project) + Gitlab::ApplicationRateLimiter.throttled?(:project_fork_sync, scope: [project, current_user]) + end + + def respond(details, errors) + { details: details, errors: errors } + end + + def authorized_find!(project_path, target_branch) + project = find_object(project_path) + + return project if ::Gitlab::UserAccess.new(current_user, container: project).can_push_to_branch?(target_branch) + + raise_resource_not_available_error! + end + end + end +end diff --git a/app/graphql/mutations/release_asset_links/create.rb b/app/graphql/mutations/release_asset_links/create.rb index f6445514ce9..bda998764b9 100644 --- a/app/graphql/mutations/release_asset_links/create.rb +++ b/app/graphql/mutations/release_asset_links/create.rb @@ -36,13 +36,15 @@ module Mutations raise_resource_not_available_error! end - new_link = release.links.create(link_attrs) - - unless new_link.persisted? - return { link: nil, errors: new_link.errors.full_messages } + result = ::Releases::Links::CreateService + .new(release, current_user, link_attrs) + .execute + + if result.success? + { link: result.payload[:link], errors: [] } + else + { link: nil, errors: result.message } end - - { link: new_link, errors: [] } end end end diff --git a/app/graphql/mutations/release_asset_links/delete.rb b/app/graphql/mutations/release_asset_links/delete.rb index 91fa74859f6..891d8e5a4d8 100644 --- a/app/graphql/mutations/release_asset_links/delete.rb +++ b/app/graphql/mutations/release_asset_links/delete.rb @@ -19,17 +19,17 @@ module Mutations description: 'Deleted release asset link.' def resolve(id:) - link = authorized_find!(id) + link = authorized_find!(id: id) - unless link.destroy - return { link: nil, errors: link.errors.full_messages } - end - - { link: link, errors: [] } - end + result = ::Releases::Links::DestroyService + .new(link.release, current_user) + .execute(link) - def find_object(id) - GitlabSchema.find_by_gid(id) + if result.success? + { link: result.payload[:link], errors: [] } + else + { link: nil, errors: result.message } + end end end end diff --git a/app/graphql/mutations/release_asset_links/update.rb b/app/graphql/mutations/release_asset_links/update.rb index f9368927371..3df2d28b88c 100644 --- a/app/graphql/mutations/release_asset_links/update.rb +++ b/app/graphql/mutations/release_asset_links/update.rb @@ -44,17 +44,17 @@ module Mutations end def resolve(id:, **link_attrs) - link = authorized_find!(id) + link = authorized_find!(id: id) - unless link.update(link_attrs) - return { link: nil, errors: link.errors.full_messages } - end - - { link: link, errors: [] } - end + result = ::Releases::Links::UpdateService + .new(link.release, current_user, link_attrs) + .execute(link) - def find_object(id) - GitlabSchema.find_by_gid(id) + if result.success? + { link: result.payload[:link], errors: [] } + else + { link: nil, errors: result.message } + end end end end diff --git a/app/graphql/mutations/terraform/state/base.rb b/app/graphql/mutations/terraform/state/base.rb index 01f69934ea3..9a264836ef5 100644 --- a/app/graphql/mutations/terraform/state/base.rb +++ b/app/graphql/mutations/terraform/state/base.rb @@ -10,12 +10,6 @@ module Mutations Types::GlobalIDType[::Terraform::State], required: true, description: 'Global ID of the Terraform state.' - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/todos/base.rb b/app/graphql/mutations/todos/base.rb deleted file mode 100644 index 9a94c5d1e6d..00000000000 --- a/app/graphql/mutations/todos/base.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Todos - class Base < ::Mutations::BaseMutation - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end - end - end -end diff --git a/app/graphql/mutations/todos/create.rb b/app/graphql/mutations/todos/create.rb index 489d2f490ff..8a0906da724 100644 --- a/app/graphql/mutations/todos/create.rb +++ b/app/graphql/mutations/todos/create.rb @@ -2,7 +2,7 @@ module Mutations module Todos - class Create < ::Mutations::Todos::Base + class Create < ::Mutations::BaseMutation graphql_name 'TodoCreate' authorize :create_todo @@ -17,7 +17,7 @@ module Mutations description: 'To-do item created.' def resolve(target_id:) - target = authorized_find!(target_id) + target = authorized_find!(id: target_id) todo = TodoService.new.mark_todo(target, current_user)&.first errors = errors_on_object(todo) if todo @@ -27,12 +27,6 @@ module Mutations errors: errors } end - - private - - def find_object(id) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb index fe4023515a4..7f8d15e033a 100644 --- a/app/graphql/mutations/todos/mark_all_done.rb +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -2,7 +2,7 @@ module Mutations module Todos - class MarkAllDone < ::Mutations::Todos::Base + class MarkAllDone < ::Mutations::BaseMutation graphql_name 'TodosMarkAllDone' authorize :update_user diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index 4fecba55242..05d69fbc969 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -2,7 +2,7 @@ module Mutations module Todos - class MarkDone < ::Mutations::Todos::Base + class MarkDone < ::Mutations::BaseMutation graphql_name 'TodoMarkDone' authorize :update_todo diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb index def24cb71bc..a169ec58a9a 100644 --- a/app/graphql/mutations/todos/restore.rb +++ b/app/graphql/mutations/todos/restore.rb @@ -2,7 +2,7 @@ module Mutations module Todos - class Restore < ::Mutations::Todos::Base + class Restore < ::Mutations::BaseMutation graphql_name 'TodoRestore' authorize :update_todo diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index f2f944860c2..106ba18b852 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -2,7 +2,7 @@ module Mutations module Todos - class RestoreMany < ::Mutations::Todos::Base + class RestoreMany < ::Mutations::BaseMutation graphql_name 'TodoRestoreMany' MAX_UPDATE_AMOUNT = 50 diff --git a/app/graphql/mutations/user_preferences/update.rb b/app/graphql/mutations/user_preferences/update.rb index c92c6d725b7..16c7b37532c 100644 --- a/app/graphql/mutations/user_preferences/update.rb +++ b/app/graphql/mutations/user_preferences/update.rb @@ -8,6 +8,9 @@ module Mutations argument :issues_sort, Types::IssueSortEnum, required: false, description: 'Sort order for issue lists.' + argument :visibility_pipeline_id_type, Types::VisibilityPipelineIdTypeEnum, + required: false, + description: 'Determines whether the pipeline list shows ID or IID.' field :user_preferences, Types::UserPreferencesType, diff --git a/app/graphql/mutations/work_items/convert.rb b/app/graphql/mutations/work_items/convert.rb new file mode 100644 index 00000000000..83bca56d900 --- /dev/null +++ b/app/graphql/mutations/work_items/convert.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Mutations + module WorkItems + class Convert < BaseMutation + graphql_name 'WorkItemConvert' + description "Converts the work item to a new type" + + include Mutations::SpamProtection + + authorize :update_work_item + + argument :id, ::Types::GlobalIDType[::WorkItem], + required: true, + description: 'Global ID of the work item.' + argument :work_item_type_id, ::Types::GlobalIDType[::WorkItems::Type], + required: true, + description: 'Global ID of the new work item type.' + + field :work_item, Types::WorkItemType, + null: true, + description: 'Updated work item.' + + def resolve(attributes) + work_item = authorized_find!(id: attributes[:id]) + + work_item_type = find_work_item_type!(attributes[:work_item_type_id]) + authorize_work_item_type!(work_item, work_item_type) + + spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) + + update_result = ::WorkItems::UpdateService.new( + container: work_item.project, + current_user: current_user, + params: { work_item_type: work_item_type, issue_type: work_item_type.base_type }, + spam_params: spam_params + ).execute(work_item) + + check_spam_action_response!(work_item) + + { + work_item: (update_result[:work_item] if update_result[:status] == :success), + errors: Array.wrap(update_result[:message]) + } + end + + private + + def find_work_item_type!(gid) + work_item_type = ::WorkItems::Type.find_by_id(gid.model_id) + + return work_item_type if work_item_type.present? + + message = format(_('Work Item type with id %{id} was not found'), id: gid.model_id) + raise_resource_not_available_error! message + end + + def authorize_work_item_type!(work_item, work_item_type) + return if current_user.can?(:"create_#{work_item_type.base_type}", work_item) + + message = format(_('You are not allowed to change the Work Item type to %{name}.'), name: work_item_type.name) + raise_resource_not_available_error! message + end + + def find_object(id:) + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb index 9f124de7ab2..dfd2d5d1f88 100644 --- a/app/graphql/mutations/work_items/create.rb +++ b/app/graphql/mutations/work_items/create.rb @@ -6,13 +6,15 @@ module Mutations graphql_name 'WorkItemCreate' include Mutations::SpamProtection - include FindsProject + include FindsNamespace include Mutations::WorkItems::Widgetable description "Creates a work item." authorize :create_work_item + MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR = 'Please provide either projectPath or namespacePath argument, but not both.' + argument :confidential, GraphQL::Types::Boolean, required: false, description: 'Sets the work item confidentiality.' @@ -25,9 +27,16 @@ module Mutations argument :milestone_widget, ::Types::WorkItems::Widgets::MilestoneInputType, required: false, description: 'Input for milestone widget.' + argument :namespace_path, GraphQL::Types::ID, + required: false, + description: 'Full path of the namespace(project or group) the work item is created in.' argument :project_path, GraphQL::Types::ID, - required: true, - description: 'Full path of the project the work item is associated with.' + required: false, + description: 'Full path of the project the work item is associated with.', + deprecated: { + reason: 'Please use namespace_path instead. That will cover for both projects and groups', + milestone: '15.10' + } argument :title, GraphQL::Types::String, required: true, description: copy_field_description(Types::WorkItemType, :title) @@ -39,8 +48,17 @@ module Mutations null: true, description: 'Created work item.' - def resolve(project_path:, **attributes) - project = authorized_find!(project_path) + def ready?(**args) + if args.slice(:project_path, :namespace_path)&.length != 1 + raise Gitlab::Graphql::Errors::ArgumentError, MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR + end + + super + end + + def resolve(project_path: nil, namespace_path: nil, **attributes) + container_path = project_path || namespace_path + container = authorized_find!(container_path) spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) params = global_id_compatibility_params(attributes).merge(author_id: current_user.id) @@ -48,7 +66,7 @@ module Mutations widget_params = extract_widget_params!(type, params) create_result = ::WorkItems::CreateService.new( - container: project, + container: container, current_user: current_user, params: params, spam_params: spam_params, diff --git a/app/graphql/mutations/work_items/create_from_task.rb b/app/graphql/mutations/work_items/create_from_task.rb index 4ef8269a42f..23ae09b23fd 100644 --- a/app/graphql/mutations/work_items/create_from_task.rb +++ b/app/graphql/mutations/work_items/create_from_task.rb @@ -46,12 +46,6 @@ module Mutations response end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/work_items/delete.rb b/app/graphql/mutations/work_items/delete.rb index ec0244fa65e..bce59448412 100644 --- a/app/graphql/mutations/work_items/delete.rb +++ b/app/graphql/mutations/work_items/delete.rb @@ -29,12 +29,6 @@ module Mutations errors: result.errors } end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/work_items/delete_task.rb b/app/graphql/mutations/work_items/delete_task.rb index 47ab3748ab4..b13d7e2e3bf 100644 --- a/app/graphql/mutations/work_items/delete_task.rb +++ b/app/graphql/mutations/work_items/delete_task.rb @@ -53,11 +53,6 @@ module Mutations raise_resource_not_available_error! end end - - # method used by `authorized_find!(id: id)` - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/mutations/work_items/export.rb b/app/graphql/mutations/work_items/export.rb new file mode 100644 index 00000000000..a3c7bae5571 --- /dev/null +++ b/app/graphql/mutations/work_items/export.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Mutations + module WorkItems + class Export < BaseMutation + graphql_name 'WorkItemExport' + + include FindsProject + include ::WorkItems::SharedFilterArguments + include ::SearchArguments + + authorize :export_work_items + + argument :project_path, + GraphQL::Types::ID, + required: true, + description: 'Full project path.' + + argument :selected_fields, + [::Types::WorkItems::AvailableExportFieldsEnum], + required: false, + description: 'List of selected fields to be exported. Omit to export all available fields.' + + field :message, GraphQL::Types::String, + null: true, + description: 'Export request result message.' + + def resolve(args) + project_path = args.delete(:project_path) + project = authorized_find!(project_path) + + check_export_available_for!(project) + + # rubocop:disable CodeReuse/Worker + IssuableExportCsvWorker.perform_async(:work_item, current_user.id, project.id, args) + # rubocop:enable CodeReuse/Worker + + { + message: format(_('Your CSV export request has succeeded. The result will be emailed to %{email}.'), + email: current_user.notification_email_or_default), + errors: [] + } + end + + def check_export_available_for!(project) + return if Feature.enabled?(:import_export_work_items_csv, project) + + error = '`import_export_work_items_csv` feature flag is disabled.' + + raise Gitlab::Graphql::Errors::ResourceNotAvailable, error + end + end + end +end diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb index db6af38d82e..3fd0f5aab62 100644 --- a/app/graphql/mutations/work_items/update.rb +++ b/app/graphql/mutations/work_items/update.rb @@ -17,6 +17,8 @@ module Mutations description: 'Updated work item.' def resolve(id:, **attributes) + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/408575') + work_item = authorized_find!(id: id) spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) @@ -42,10 +44,6 @@ module Mutations private - def find_object(id:) - GitlabSchema.find_by_gid(id) - end - def interpret_quick_actions!(work_item, current_user, widget_params, attributes = {}) return unless work_item.work_item_type.widgets.include?(::WorkItems::Widgets::Description) @@ -60,21 +58,10 @@ module Mutations description_param[:description] = description if description && description != original_description - # Widgets have a set of quick action params that they must process. - # Map them to widget_params so they can be picked up by widget services. - work_item.work_item_type.widgets - .filter { |widget| widget.respond_to?(:quick_action_params) } - .each do |widget| - widget.quick_action_params - .filter { |param_name| command_params.key?(param_name) } - .each do |param_name| - widget_params[widget.api_symbol] ||= {} - widget_params[widget.api_symbol][param_name] = command_params.delete(param_name) - end - end - - # The command_params not processed by widgets (e.g. title) should be placed in 'attributes'. - attributes.merge!(command_params || {}) + parsed_params = work_item.transform_quick_action_params(command_params) + + widget_params.merge!(parsed_params[:widgets]) + attributes.merge!(parsed_params[:common]) end end end diff --git a/app/graphql/queries/repository/path_last_commit.query.graphql b/app/graphql/queries/repository/path_last_commit.query.graphql index 914be3a72c1..facbf1555fc 100644 --- a/app/graphql/queries/repository/path_last_commit.query.graphql +++ b/app/graphql/queries/repository/path_last_commit.query.graphql @@ -27,7 +27,30 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { avatarUrl webPath } - signatureHtml + signature { + __typename + ... on GpgSignature { + gpgKeyPrimaryKeyid + verificationStatus + } + ... on X509Signature { + verificationStatus + x509Certificate { + id + subject + subjectKeyIdentifier + x509Issuer { + id + subject + subjectKeyIdentifier + } + } + } + ... on SshSignature { + verificationStatus + keyFingerprintSha256 + } + } pipelines(ref: $ref, first: 1) { __typename edges { diff --git a/app/graphql/resolvers/achievements/achievements_resolver.rb b/app/graphql/resolvers/achievements/achievements_resolver.rb new file mode 100644 index 00000000000..eb3f6eaf92e --- /dev/null +++ b/app/graphql/resolvers/achievements/achievements_resolver.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Resolvers + module Achievements + class AchievementsResolver < BaseResolver + include LooksAhead + + type ::Types::Achievements::AchievementType.connection_type, null: true + + argument :ids, [::Types::GlobalIDType[::Achievements::Achievement]], + required: false, + description: 'Filter achievements by IDs.' + + alias_method :namespace, :object + + def resolve_with_lookahead(**args) + return ::Achievements::Achievement.none if Feature.disabled?(:achievements, namespace) + + params = {} + params[:ids] = args[:ids].map(&:model_id) if args[:ids].present? + + achievements = ::Achievements::AchievementsFinder.new(namespace, params).execute + apply_lookahead(achievements) + end + + private + + def preloads + { + user_achievements: [{ user_achievements: [:user, :awarded_by_user, :revoked_by_user] }] + } + end + end + end +end diff --git a/app/graphql/resolvers/achievements/user_achievements_resolver.rb b/app/graphql/resolvers/achievements/user_achievements_resolver.rb new file mode 100644 index 00000000000..77fb15c3d93 --- /dev/null +++ b/app/graphql/resolvers/achievements/user_achievements_resolver.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Resolvers + module Achievements + class UserAchievementsResolver < BaseResolver + include LooksAhead + + type ::Types::Achievements::UserAchievementType.connection_type, null: true + + def resolve_with_lookahead + user_achievements = object.user_achievements.not_revoked.order_by_id_asc + + apply_lookahead(user_achievements) + end + + private + + def unconditional_includes + [ + { achievement: [:namespace] } + ] + end + + def preloads + { + user: [:user], + awarded_by_user: [:awarded_by_user], + revoked_by_user: [:revoked_by_user] + } + end + end + end +end diff --git a/app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb new file mode 100644 index 00000000000..82d38ff89d9 --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/base_count_resolver.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Resolvers + module Analytics + module CycleAnalytics + class BaseCountResolver < BaseResolver + type Types::Analytics::CycleAnalytics::MetricType, null: true + + argument :from, Types::TimeType, + required: true, + description: 'Timestamp marking the start date and time.' + + argument :to, Types::TimeType, + required: true, + description: 'Timestamp marking the end date and time.' + + def ready?(**args) + start_date = args[:from] + end_date = args[:to] + + if start_date >= end_date + raise Gitlab::Graphql::Errors::ArgumentError, + '`from` argument must be before `to` argument' + end + + max_days = Gitlab::Analytics::CycleAnalytics::RequestParams::MAX_RANGE_DAYS + + if (end_date.beginning_of_day - start_date.beginning_of_day) > max_days + raise Gitlab::Graphql::Errors::ArgumentError, + "Max of #{max_days.inspect} timespan is allowed" + end + + super + end + end + end + end +end diff --git a/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb new file mode 100644 index 00000000000..8128023aecb --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Resolvers + module Analytics + module CycleAnalytics + class BaseIssueResolver < BaseCountResolver + type Types::Analytics::CycleAnalytics::MetricType, null: true + + argument :assignee_usernames, [GraphQL::Types::String], + required: false, + description: 'Usernames of users assigned to the issue.' + + argument :author_username, GraphQL::Types::String, + required: false, + description: 'Username of the author of the issue.' + + argument :milestone_title, GraphQL::Types::String, + required: false, + description: 'Milestone applied to the issue.' + + argument :label_names, [GraphQL::Types::String], + required: false, + description: 'Labels applied to the issue.' + + def finder_params + { project_id: object.project.id } + end + + # :project level: no customization, returning the original resolver + # :group level: add the project_ids argument + def self.[](context = :project) + case context + when :project + self + when :group + Class.new(self) do + argument :project_ids, [GraphQL::Types::ID], + required: false, + description: 'Project IDs within the group hierarchy.' + + define_method :finder_params do + { group_id: object.id, include_subgroups: true } + end + end + end + end + end + end + end +end diff --git a/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb new file mode 100644 index 00000000000..51a1afdd5ab --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/deployment_count_resolver.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# rubocop:disable Graphql/ResolverType (inherited from Resolvers::Analytics::CycleAnalytics::BaseCountResolver) +module Resolvers + module Analytics + module CycleAnalytics + class DeploymentCountResolver < BaseCountResolver + def resolve(**args) + value = count(args) + { + value: value, + title: n_('Deploy', 'Deploys', value.to_i), + identifier: 'deploys', + links: [] + } + end + + private + + def count(args) + finder = DeploymentsFinder.new({ + finished_after: args[:from], + finished_before: args[:to], + project: object.project, + status: :success, + order_by: :finished_at + }) + + finder.execute.count + end + + # :project level: no customization, returning the original resolver + # :group level: add the project_ids argument + def self.[](context = :project) + case context + when :project + self + when :group + Class.new(self) do + argument :project_ids, [GraphQL::Types::ID], + required: false, + description: 'Project IDs within the group hierarchy.' + end + + end + end + end + end + end +end +# rubocop:enable Graphql/ResolverType + +mod = Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver +mod.prepend_mod_with('Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver') diff --git a/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb new file mode 100644 index 00000000000..fd20800ee16 --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# rubocop:disable Graphql/ResolverType (inherited from Resolvers::Analytics::CycleAnalytics::BaseIssueResolver) +module Resolvers + module Analytics + module CycleAnalytics + class IssueCountResolver < BaseIssueResolver + def resolve(**args) + value = IssuesFinder + .new(current_user, process_params(args)) + .execute + .count + + { + value: value, + title: n_('New Issue', 'New Issues', value), + identifier: 'issues', + links: [] + } + end + + private + + def process_params(params) + params[:assignee_username] = params.delete(:assignee_usernames) if params[:assignee_usernames] + params[:label_name] = params.delete(:label_names) if params[:label_names] + params[:created_after] = params.delete(:from) + params[:created_before] = params.delete(:to) + params[:projects] = params[:project_ids] if params[:project_ids] + + params.merge(finder_params) + end + end + end + end +end +# rubocop:enable Graphql/ResolverType diff --git a/app/graphql/resolvers/award_emoji/base_votes_count_resolver.rb b/app/graphql/resolvers/award_emoji/base_votes_count_resolver.rb new file mode 100644 index 00000000000..406c52eb0d5 --- /dev/null +++ b/app/graphql/resolvers/award_emoji/base_votes_count_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + module AwardEmoji + class BaseVotesCountResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type GraphQL::Types::Int, null: true + + private + + def authorized_resource?(object) + Ability.allowed?(current_user, "read_#{object.to_ability_name}".to_sym, object) + end + + def votes_batch_loader + BatchLoaders::AwardEmojiVotesBatchLoader + end + end + end +end diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb index fb5fa4465f9..0b8180dbce7 100644 --- a/app/graphql/resolvers/blobs_resolver.rb +++ b/app/graphql/resolvers/blobs_resolver.rb @@ -38,7 +38,7 @@ module Resolvers private def validate_ref(ref) - unless Gitlab::GitRefValidator.validate(ref) + unless Gitlab::GitRefValidator.validate(ref, skip_head_ref_check: true) raise Gitlab::Graphql::Errors::ArgumentError, 'Ref is not valid' end end diff --git a/app/graphql/resolvers/ci/all_jobs_resolver.rb b/app/graphql/resolvers/ci/all_jobs_resolver.rb index d918bed9f57..5d0193e0e1c 100644 --- a/app/graphql/resolvers/ci/all_jobs_resolver.rb +++ b/app/graphql/resolvers/ci/all_jobs_resolver.rb @@ -3,14 +3,37 @@ module Resolvers module Ci class AllJobsResolver < BaseResolver + include LooksAhead + type ::Types::Ci::JobType.connection_type, null: true argument :statuses, [::Types::Ci::JobStatusEnum], required: false, description: 'Filter jobs by status.' - def resolve(statuses: nil) - ::Ci::JobsFinder.new(current_user: current_user, params: { scope: statuses }).execute + def resolve_with_lookahead(statuses: nil) + jobs = ::Ci::JobsFinder.new(current_user: current_user, params: { scope: statuses }).execute + + apply_lookahead(jobs) + end + + private + + def preloads + { + previous_stage_jobs_or_needs: [:needs, :pipeline], + artifacts: [:job_artifacts], + pipeline: [:user], + kind: [:metadata], + retryable: [:metadata], + project: [{ project: [:route, { namespace: [:route] }] }], + commit_path: [:pipeline, { project: { namespace: [:route] } }], + ref_path: [{ project: [:route, { namespace: [:route] }] }], + browse_artifacts_path: [{ project: { namespace: [:route] } }], + play_path: [{ project: { namespace: [:route] } }], + web_path: [{ project: { namespace: [:route] } }], + tags: [:tags] + } end end end diff --git a/app/graphql/resolvers/ci/inherited_variables_resolver.rb b/app/graphql/resolvers/ci/inherited_variables_resolver.rb new file mode 100644 index 00000000000..01f966942a4 --- /dev/null +++ b/app/graphql/resolvers/ci/inherited_variables_resolver.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class InheritedVariablesResolver < BaseResolver + type Types::Ci::ProjectVariableType.connection_type, null: true + + def resolve + object.group&.self_and_ancestors&.flat_map(&:variables) || [] + end + end + end +end diff --git a/app/graphql/resolvers/ci/jobs_resolver.rb b/app/graphql/resolvers/ci/jobs_resolver.rb index 31cc350f331..95816a1ac1a 100644 --- a/app/graphql/resolvers/ci/jobs_resolver.rb +++ b/app/graphql/resolvers/ci/jobs_resolver.rb @@ -23,12 +23,17 @@ module Resolvers required: false, description: 'Filter jobs by when they are executed.' - def resolve(statuses: nil, security_report_types: [], retried: nil, when_executed: nil) + argument :job_kind, ::Types::Ci::JobKindEnum, + required: false, + description: 'Filter jobs by kind.' + + def resolve(statuses: nil, security_report_types: [], retried: nil, when_executed: nil, job_kind: nil) jobs = init_collection(security_report_types) jobs = jobs.with_status(statuses) if statuses.present? jobs = jobs.retried if retried jobs = jobs.with_when_executed(when_executed) if when_executed.present? jobs = jobs.latest if retried == false + jobs = jobs.with_type(job_kind) if job_kind jobs end diff --git a/app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb b/app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb index 35d30827561..561c61e3b27 100644 --- a/app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb +++ b/app/graphql/resolvers/ci/pipeline_job_artifacts_resolver.rb @@ -15,7 +15,7 @@ module Resolvers def find_job_artifacts BatchLoader::GraphQL.for(pipeline).batch do |pipelines, loader| - ActiveRecord::Associations::Preloader.new.preload(pipelines, :job_artifacts) # rubocop: disable CodeReuse/ActiveRecord + ActiveRecord::Associations::Preloader.new(records: pipelines, associations: :job_artifacts).call # rubocop: disable CodeReuse/ActiveRecord pipelines.each { |pl| loader.call(pl, pl.job_artifacts) } end diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb index 467a3525867..9fe25a4d13d 100644 --- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb +++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb @@ -36,7 +36,11 @@ module Resolvers { pipeline: [:merge_request] }, { project: [:route, { namespace: :route }] } ], - commit_path: [:pipeline, { project: [:route, { namespace: [:route] }] }], + commit_path: [:pipeline, { project: { namespace: [:route] } }], + ref_path: [{ project: [:route, { namespace: [:route] }] }], + browse_artifacts_path: [{ project: { namespace: [:route] } }], + play_path: [{ project: { namespace: [:route] } }], + web_path: [{ project: { namespace: [:route] } }], short_sha: [:pipeline], tags: [:tags] } diff --git a/app/graphql/resolvers/ci/runner_projects_resolver.rb b/app/graphql/resolvers/ci/runner_projects_resolver.rb index 2a2d63f85de..c5037965e20 100644 --- a/app/graphql/resolvers/ci/runner_projects_resolver.rb +++ b/app/graphql/resolvers/ci/runner_projects_resolver.rb @@ -15,10 +15,10 @@ module Resolvers argument :sort, GraphQL::Types::String, required: false, - default_value: 'id_asc', # TODO: Remove in %16.0 and move :sort to ProjectSearchArguments, see https://gitlab.com/gitlab-org/gitlab/-/issues/372117 + default_value: 'id_asc', # TODO: Remove in %17.0 and move :sort to ProjectSearchArguments, see https://gitlab.com/gitlab-org/gitlab/-/issues/372117 deprecated: { - reason: 'Default sort order will change in 16.0. ' \ - 'Specify `"id_asc"` if query results\' order is important', + reason: 'Default sort order will change in GitLab 17.0. ' \ + 'Specify `"id_asc"` if you require the query results to be ordered by ascending IDs', milestone: '15.4' }, description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \ @@ -34,25 +34,30 @@ module Resolvers .where(runner_id: runner_ids) .pluck(:runner_id, :project_id) - project_ids = plucked_runner_and_project_ids.collect { |_runner_id, project_id| project_id }.uniq + unique_project_ids = plucked_runner_and_project_ids.collect { |_runner_id, project_id| project_id }.uniq projects = ProjectsFinder .new(current_user: current_user, params: project_finder_params(args), - project_ids_relation: project_ids) + project_ids_relation: unique_project_ids) .execute projects = apply_lookahead(projects) Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute + sorted_project_ids = projects.map(&:id) projects_by_id = projects.index_by(&:id) # In plucked_runner_and_project_ids, first() represents the runner ID, and second() the project ID, # so let's group the project IDs by runner ID - runner_project_ids_by_runner_id = + project_ids_by_runner_id = plucked_runner_and_project_ids .group_by(&:first) - .transform_values { |values| values.map(&:second).filter_map { |project_id| projects_by_id[project_id] } } + .transform_values { |runner_id_and_project_id| runner_id_and_project_id.map(&:second) } + # Reorder the project IDs according to the order in sorted_project_ids + sorted_project_ids_by_runner_id = + project_ids_by_runner_id.transform_values { |project_ids| sorted_project_ids.intersection(project_ids) } runner_ids.each do |runner_id| - runner_projects = runner_project_ids_by_runner_id[runner_id] || [] + runner_project_ids = sorted_project_ids_by_runner_id[runner_id] || [] + runner_projects = runner_project_ids.map { |id| projects_by_id[id] } loader.call(runner_id, runner_projects) end @@ -68,9 +73,9 @@ module Resolvers def preloads super.merge({ - full_path: [:route, { namespace: [:route] }], - web_url: [:route, { namespace: [:route] }] - }) + full_path: [:route, { namespace: [:route] }], + web_url: [:route, { namespace: [:route] }] + }) end end end diff --git a/app/graphql/resolvers/ci/runner_resolver.rb b/app/graphql/resolvers/ci/runner_resolver.rb index ca94e28b2e9..4250b069d20 100644 --- a/app/graphql/resolvers/ci/runner_resolver.rb +++ b/app/graphql/resolvers/ci/runner_resolver.rb @@ -22,11 +22,15 @@ module Resolvers def find_runner(id:) runner_id = GitlabSchema.parse_gid(id, expected_type: ::Ci::Runner).model_id.to_i - preload_tag_list = lookahead.selects?(:tag_list) + key = { + preload_tag_list: lookahead.selects?(:tag_list), + preload_creator: lookahead.selects?(:created_by) + } - BatchLoader::GraphQL.for(runner_id).batch(key: { preload_tag_list: preload_tag_list }) do |ids, loader, batch| + BatchLoader::GraphQL.for(runner_id).batch(key: key) do |ids, loader, batch| results = ::Ci::Runner.id_in(ids) results = results.with_tags if batch[:key][:preload_tag_list] + results = results.with_creator if batch[:key][:preload_creator] results.each { |record| loader.call(record.id, record) } end diff --git a/app/graphql/resolvers/ci/runner_status_resolver.rb b/app/graphql/resolvers/ci/runner_status_resolver.rb index 447ab306ba7..c8f47b16f75 100644 --- a/app/graphql/resolvers/ci/runner_status_resolver.rb +++ b/app/graphql/resolvers/ci/runner_status_resolver.rb @@ -12,17 +12,16 @@ module Resolvers argument :legacy_mode, type: GraphQL::Types::String, - default_value: '14.5', + default_value: nil, required: false, - description: 'Compatibility mode. A null value turns off compatibility mode.', + description: 'No-op, left for compatibility.', deprecated: { - reason: 'Will be removed in 17.0. In GitLab 16.0 and later, ' \ - 'the field will act as if `legacyMode` is null', + reason: 'Will be removed in 17.0', milestone: '15.0' } - def resolve(legacy_mode:, **args) - runner.status(legacy_mode) + def resolve(**args) + runner.status end end end diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb index b52a4cc0ab4..735e38c1a5c 100644 --- a/app/graphql/resolvers/ci/runners_resolver.rb +++ b/app/graphql/resolvers/ci/runners_resolver.rb @@ -61,9 +61,7 @@ module Resolvers upgrade_status: params[:upgrade_status], search: params[:search], sort: params[:sort]&.to_s, - preload: { - tag_name: node_selection&.selects?(:tag_list) - } + preload: false # we'll handle preloading ourselves }.compact .merge(parent_param) end @@ -79,6 +77,31 @@ module Resolvers def parent object.respond_to?(:sync) ? object.sync : object end + + def preloads + super.merge({ + created_by: [:creator], + tag_list: [:tags] + }) + end + + def nested_preloads + { + created_by: { + creator: { + full_path: [:route], + web_path: [:route], + web_url: [:route] + } + }, + owner_project: { + owner_project: { + full_path: [:route, { namespace: [:route] }], + web_url: [:route, { namespace: [:route] }] + } + } + } + end end end end diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb index b7355a1752e..0b9422db2a9 100644 --- a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb +++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb @@ -9,12 +9,8 @@ module Resolvers delegate :project, to: :agent - argument :status, Types::Clusters::AgentTokenStatusEnum, - required: false, - description: 'Status of the token.' - - def resolve(**args) - ::Clusters::AgentTokensFinder.new(agent, current_user, args).execute + def resolve(**_args) + ::Clusters::AgentTokensFinder.new(agent, current_user, status: :active).execute end end end diff --git a/app/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver.rb b/app/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver.rb new file mode 100644 index 00000000000..c36338439e6 --- /dev/null +++ b/app/graphql/resolvers/clusters/agents/authorizations/ci_access_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + module Clusters + module Agents + module Authorizations + class CiAccessResolver < BaseResolver + type Types::Clusters::Agents::Authorizations::CiAccessType, null: true + + alias_method :project, :object + + def resolve(*) + ::Clusters::Agents::Authorizations::CiAccess::Finder.new(project).execute + end + end + end + end + end +end diff --git a/app/graphql/resolvers/clusters/agents/authorizations/user_access_resolver.rb b/app/graphql/resolvers/clusters/agents/authorizations/user_access_resolver.rb new file mode 100644 index 00000000000..280db570aa6 --- /dev/null +++ b/app/graphql/resolvers/clusters/agents/authorizations/user_access_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + module Clusters + module Agents + module Authorizations + class UserAccessResolver < BaseResolver + type Types::Clusters::Agents::Authorizations::UserAccessType, null: true + + alias_method :project, :object + + def resolve(*) + ::Clusters::Agents::Authorizations::UserAccess::Finder.new(current_user, project: project).execute + end + end + end + end + end +end diff --git a/app/graphql/resolvers/clusters/agents_resolver.rb b/app/graphql/resolvers/clusters/agents_resolver.rb index 0b9eb361dbd..f138ad2b510 100644 --- a/app/graphql/resolvers/clusters/agents_resolver.rb +++ b/app/graphql/resolvers/clusters/agents_resolver.rb @@ -28,7 +28,7 @@ module Resolvers def preloads { activity_events: { activity_events: [:user, agent_token: :agent] }, - tokens: :agent_tokens + tokens: :active_agent_tokens } end end diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index c68e120ee24..b9326015ac0 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -40,6 +40,7 @@ module ResolvesMergeRequests def preloads { assignees: [:assignees], + award_emoji: { award_emoji: [:awardable] }, reviewers: [:reviewers], participants: MergeRequest.participant_includes, author: [:author], diff --git a/app/graphql/resolvers/concerns/time_frame_arguments.rb b/app/graphql/resolvers/concerns/time_frame_arguments.rb index 87b7a96045c..c26898bb2f1 100644 --- a/app/graphql/resolvers/concerns/time_frame_arguments.rb +++ b/app/graphql/resolvers/concerns/time_frame_arguments.rb @@ -3,51 +3,15 @@ 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: OVERLAPPING_TIMEFRAME_DESC, - deprecated: { reason: 'Use timeframe.start', milestone: '13.5' } - - argument :end_date, Types::TimeType, - required: false, - 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 %i[start_date end_date timeframe].any? { |k| args[k].present? } - - # the timeframe is passed in as a TimeframeInputType - timeframe = args[:timeframe].to_h if args[:timeframe] - return if timeframe && %i[start_date end_date].all? { |k| args[k].nil? } - - error_message = - if 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" - end - - if error_message - raise Gitlab::Graphql::Errors::ArgumentError, error_message - end - end - def transform_timeframe_parameters(args) - if args[:timeframe] - args[:timeframe].to_h.transform_keys { |k| :"#{k}_date" } - else - args.slice(:start_date, :end_date) - end + return {} unless args[:timeframe] + + args[:timeframe].to_h.transform_keys { |k| :"#{k}_date" } end end diff --git a/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb new file mode 100644 index 00000000000..ecb105a64d0 --- /dev/null +++ b/app/graphql/resolvers/concerns/work_items/shared_filter_arguments.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module WorkItems + module SharedFilterArguments + extend ActiveSupport::Concern + + included do + argument :author_username, + GraphQL::Types::String, + required: false, + description: 'Filter work items by author username.', + alpha: { milestone: '15.9' } + argument :iids, + [GraphQL::Types::String], + required: false, + description: 'List of IIDs of work items. For example, `["1", "2"]`.' + argument :state, + Types::IssuableStateEnum, + required: false, + description: 'Current state of the work item.' + argument :types, + [Types::IssueTypeEnum], + as: :issue_types, + description: 'Filter work items by the given work item types.', + required: false + end + end +end diff --git a/app/graphql/resolvers/data_transfer/data_transfer_arguments.rb b/app/graphql/resolvers/data_transfer/data_transfer_arguments.rb new file mode 100644 index 00000000000..da75a78b2ac --- /dev/null +++ b/app/graphql/resolvers/data_transfer/data_transfer_arguments.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + module DataTransfer + module DataTransferArguments + extend ActiveSupport::Concern + + included do + argument :from, Types::DateType, + description: + 'Retain egress data for one year. Data for the current month will increase dynamically as egress occurs.', + required: false + argument :to, Types::DateType, + description: 'End date for the data.', + required: false + end + end + end +end diff --git a/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb new file mode 100644 index 00000000000..83bb144017c --- /dev/null +++ b/app/graphql/resolvers/data_transfer/group_data_transfer_resolver.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Resolvers + module DataTransfer + class GroupDataTransferResolver < BaseResolver + include DataTransferArguments + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorizes_object! + authorize :read_usage_quotas + + type Types::DataTransfer::GroupDataTransferType, null: false + + alias_method :group, :object + + def resolve(**args) + return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, group) + + results = if Feature.enabled?(:data_transfer_monitoring_mock_data, group) + ::DataTransfer::MockedTransferFinder.new.execute + else + ::DataTransfer::GroupDataTransferFinder.new( + group: group, + from: args[:from], + to: args[:to], + user: current_user + ).execute.map(&:attributes) + end + + { egress_nodes: results.to_a } + end + end + end +end diff --git a/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb new file mode 100644 index 00000000000..c3296f7d4c3 --- /dev/null +++ b/app/graphql/resolvers/data_transfer/project_data_transfer_resolver.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Resolvers + module DataTransfer + class ProjectDataTransferResolver < BaseResolver + include DataTransferArguments + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorizes_object! + authorize :read_usage_quotas + + type Types::DataTransfer::ProjectDataTransferType, null: false + + alias_method :project, :object + + def resolve(**args) + return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, project.group) + + results = if Feature.enabled?(:data_transfer_monitoring_mock_data, project.group) + ::DataTransfer::MockedTransferFinder.new.execute + else + ::DataTransfer::ProjectDataTransferFinder.new( + project: project, + from: args[:from], + to: args[:to], + user: current_user + ).execute + end + + { egress_nodes: results } + end + end + end +end diff --git a/app/graphql/resolvers/data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer_resolver.rb deleted file mode 100644 index 1a240d2811f..00000000000 --- a/app/graphql/resolvers/data_transfer_resolver.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Resolvers - class DataTransferResolver < BaseResolver - argument :from, Types::DateType, - description: 'Retain egress data for 1 year. Current month will increase dynamically as egress occurs.', - required: false - argument :to, Types::DateType, - description: 'End date for the data.', - required: false - - type ::Types::DataTransfer::BaseType, null: false - - def self.source - raise NotImplementedError - end - - def self.project - Class.new(self) do - type Types::DataTransfer::ProjectDataTransferType, null: false - - def self.source - "Project" - end - end - end - - def self.group - Class.new(self) do - type Types::DataTransfer::GroupDataTransferType, null: false - - def self.source - "Group" - end - end - end - - def resolve(**_args) - return unless Feature.enabled?(:data_transfer_monitoring) - - start_date = Date.new(2023, 0o1, 0o1) - date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') } - - nodes = 0.upto(3).map do |i| - { - date: date_for_index.call(i), - repository_egress: 250_000, - artifacts_egress: 250_000, - packages_egress: 250_000, - registry_egress: 250_000 - } - end - - { egress_nodes: nodes } - end - end -end diff --git a/app/graphql/resolvers/design_management/version_resolver.rb b/app/graphql/resolvers/design_management/version_resolver.rb index 7895981d67c..0d2479ded40 100644 --- a/app/graphql/resolvers/design_management/version_resolver.rb +++ b/app/graphql/resolvers/design_management/version_resolver.rb @@ -16,10 +16,6 @@ module Resolvers def resolve(id:) authorized_find!(id: id) end - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/resolvers/down_votes_count_resolver.rb b/app/graphql/resolvers/down_votes_count_resolver.rb index 0e7772f988a..5f5340578cd 100644 --- a/app/graphql/resolvers/down_votes_count_resolver.rb +++ b/app/graphql/resolvers/down_votes_count_resolver.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true module Resolvers - class DownVotesCountResolver < BaseResolver - include Gitlab::Graphql::Authorize::AuthorizeResource - include BatchLoaders::AwardEmojiVotesBatchLoader - + class DownVotesCountResolver < Resolvers::AwardEmoji::BaseVotesCountResolver type GraphQL::Types::Int, null: true def resolve authorize!(object) - load_votes(object, AwardEmoji::DOWNVOTE_NAME) + votes_batch_loader.load_downvotes(object) end end end diff --git a/app/graphql/resolvers/group_labels_resolver.rb b/app/graphql/resolvers/group_labels_resolver.rb index a22fa9761d6..b88f62b984a 100644 --- a/app/graphql/resolvers/group_labels_resolver.rb +++ b/app/graphql/resolvers/group_labels_resolver.rb @@ -13,5 +13,9 @@ module Resolvers required: false, description: 'Include only group level labels.', default_value: false + + before_connection_authorization do |nodes, current_user| + Preloaders::LabelsPreloader.new(nodes, current_user).preload_all + end end end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index bbf45efa33e..17e3e159a5b 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -2,14 +2,19 @@ module Resolvers class IssuesResolver < Issues::BaseResolver + extend ::Gitlab::Utils::Override prepend ::Issues::LookAheadPreloads include ::Issues::SortArguments - NON_FILTER_ARGUMENTS = %i[sort lookahead].freeze + NON_FILTER_ARGUMENTS = %i[sort lookahead include_archived].freeze + argument :include_archived, GraphQL::Types::Boolean, + required: false, + default_value: false, + description: 'Whether to include issues from archived projects. Defaults to `false`.' argument :state, Types::IssuableStateEnum, - required: false, - description: 'Current state of this issue.' + required: false, + description: 'Current state of this issue.' # see app/graphql/types/issue_connection.rb type 'Types::IssueConnection', null: true @@ -44,6 +49,13 @@ module Resolvers private + override :prepare_finder_params + def prepare_finder_params(args) + super.tap do |prepared| + prepared[:non_archived] = !prepared.delete(:include_archived) + end + end + def filter_provided?(args) args.except(*NON_FILTER_ARGUMENTS).values.any?(&:present?) end diff --git a/app/graphql/resolvers/kas/agent_configurations_resolver.rb b/app/graphql/resolvers/kas/agent_configurations_resolver.rb index 9db104287a6..74c5cbe55f1 100644 --- a/app/graphql/resolvers/kas/agent_configurations_resolver.rb +++ b/app/graphql/resolvers/kas/agent_configurations_resolver.rb @@ -21,7 +21,7 @@ module Resolvers private def can_read_agent_configuration? - current_user.can?(:read_cluster, project) + current_user.can?(:read_cluster_agent, project) end def kas_client diff --git a/app/graphql/resolvers/labels_resolver.rb b/app/graphql/resolvers/labels_resolver.rb index f0e099e8fb2..10f5917d84b 100644 --- a/app/graphql/resolvers/labels_resolver.rb +++ b/app/graphql/resolvers/labels_resolver.rb @@ -17,6 +17,10 @@ module Resolvers description: 'Include labels from ancestor groups.', default_value: false + before_connection_authorization do |nodes, current_user| + Preloaders::LabelsPreloader.new(nodes, current_user).preload_all + end + def resolve(**args) return Label.none if parent.nil? @@ -24,6 +28,11 @@ module Resolvers # LabelsFinder uses `search` param, so we transform `search_term` into `search` args[:search] = args.delete(:search_term) + + # Optimization: + # Rely on the LabelsPreloader rather than the default parent record preloading in the + # finder because LabelsPreloader preloads more associations which are required for the + # permission check. LabelsFinder.new(current_user, parent_param.merge(args)).execute end diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index 72372ae6b42..0cdff272ee5 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -55,6 +55,13 @@ module Resolvers required: false, description: 'Limit result to draft merge requests.' + argument :approved, GraphQL::Types::Boolean, + required: false, + description: <<~DESC + Limit results to approved merge requests. + Available only when the feature flag `mr_approved_filter` is enabled. + DESC + argument :created_after, Types::TimeType, required: false, description: 'Merge requests created after this timestamp.' diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb index d2be9fcdd89..5abad0de539 100644 --- a/app/graphql/resolvers/metrics/dashboard_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb @@ -15,6 +15,7 @@ module Resolvers alias_method :environment, :object def resolve(path:) + return if Feature.enabled?(:remove_monitor_metrics) return unless environment ::PerformanceMonitoring::PrometheusDashboard.find_for(path: path, **service_params) diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb index 9d6b0486c04..aad9bbebafb 100644 --- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb @@ -17,6 +17,7 @@ module Resolvers alias_method :dashboard, :object def resolve(**args) + return if Feature.enabled?(:remove_monitor_metrics) return [] unless dashboard ::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 25ff783b408..563c6594665 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -40,8 +40,6 @@ module Resolvers NON_STABLE_CURSOR_SORTS = %i[expired_last_due_date_asc expired_last_due_date_desc].freeze def resolve_with_lookahead(**args) - validate_timeframe_params!(args) - milestones = apply_lookahead(MilestonesFinder.new(milestones_finder_params(args)).execute) if non_stable_cursor_sort?(args[:sort]) diff --git a/app/graphql/resolvers/notes/synthetic_note_resolver.rb b/app/graphql/resolvers/notes/synthetic_note_resolver.rb index d4eafcd2c49..619f54d80b4 100644 --- a/app/graphql/resolvers/notes/synthetic_note_resolver.rb +++ b/app/graphql/resolvers/notes/synthetic_note_resolver.rb @@ -26,10 +26,6 @@ module Resolvers synthetic_notes.find { |note| note.discussion_id == sha } end - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end end diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb index 6c4e978125e..8fd80b1a9b9 100644 --- a/app/graphql/resolvers/paginated_tree_resolver.rb +++ b/app/graphql/resolvers/paginated_tree_resolver.rb @@ -22,7 +22,7 @@ module Resolvers alias_method :repository, :object def resolve(**args) - return unless repository.exists? + return if repository.empty? cursor = args.delete(:after) args[:ref] ||= :head diff --git a/app/graphql/resolvers/project_merge_requests_resolver.rb b/app/graphql/resolvers/project_merge_requests_resolver.rb index 66c020a0c14..6a240541341 100644 --- a/app/graphql/resolvers/project_merge_requests_resolver.rb +++ b/app/graphql/resolvers/project_merge_requests_resolver.rb @@ -22,7 +22,13 @@ module Resolvers def only_count_is_selected_with_merged_at_filter?(args) return unless lookahead - argument_names = args.compact.except(:lookahead, :sort, :merged_before, :merged_after).keys + # Filter out all elements with blank values. If any of the values are not + # scalars, e.g. hashes or array, filter blank values from them and remove + # them if the resulting collection is empty. + argument_names = args.except(:lookahead, :sort, :merged_before, :merged_after).filter_map do |key, value| + value = value.to_hash.compact if value.respond_to?(:to_hash) + key if value.present? + end # no extra filtering arguments are provided return unless argument_names.empty? diff --git a/app/graphql/resolvers/projects/commit_references_resolver.rb b/app/graphql/resolvers/projects/commit_references_resolver.rb new file mode 100644 index 00000000000..ca25bad468c --- /dev/null +++ b/app/graphql/resolvers/projects/commit_references_resolver.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class CommitReferencesResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + argument :commit_sha, GraphQL::Types::String, + required: true, + description: 'Project commit SHA identifier. For example, `287774414568010855642518513f085491644061`.' + + authorize :read_commit + + alias_method :project, :object + + calls_gitaly! + + type ::Types::CommitReferencesType, null: true + + def resolve(commit_sha:) + authorized_find!(oid: commit_sha) + end + + def find_object(oid:) + project.repository.commit(oid) + end + end + end +end diff --git a/app/graphql/resolvers/projects/fork_details_resolver.rb b/app/graphql/resolvers/projects/fork_details_resolver.rb index fcc13a1bc1e..620ce395915 100644 --- a/app/graphql/resolvers/projects/fork_details_resolver.rb +++ b/app/graphql/resolvers/projects/fork_details_resolver.rb @@ -13,8 +13,15 @@ module Resolvers def resolve(**args) return unless project.forked? + return unless authorized_fork_source? - ::Projects::Forks::DivergenceCounts.new(project, args[:ref]).counts + ::Projects::Forks::Details.new(project, args[:ref]) + end + + private + + def authorized_fork_source? + Ability.allowed?(current_user, :read_code, project.fork_source) end end end diff --git a/app/graphql/resolvers/timelog_resolver.rb b/app/graphql/resolvers/timelog_resolver.rb index dc42a5f38c9..d2b67451698 100644 --- a/app/graphql/resolvers/timelog_resolver.rb +++ b/app/graphql/resolvers/timelog_resolver.rb @@ -121,7 +121,7 @@ module Resolvers def apply_user_filter(timelogs, args) return timelogs unless args[:username] - user = UserFinder.new(args[:username]).find_by_username! + user = UserFinder.new(args[:username]).find_by_username timelogs.for_user(user) end diff --git a/app/graphql/resolvers/up_votes_count_resolver.rb b/app/graphql/resolvers/up_votes_count_resolver.rb index 1c78facb694..8b2d705c07a 100644 --- a/app/graphql/resolvers/up_votes_count_resolver.rb +++ b/app/graphql/resolvers/up_votes_count_resolver.rb @@ -1,15 +1,12 @@ # frozen_string_literal: true module Resolvers - class UpVotesCountResolver < BaseResolver - include Gitlab::Graphql::Authorize::AuthorizeResource - include BatchLoaders::AwardEmojiVotesBatchLoader - + class UpVotesCountResolver < Resolvers::AwardEmoji::BaseVotesCountResolver type GraphQL::Types::Int, null: true def resolve authorize!(object) - load_votes(object, AwardEmoji::UPVOTE_NAME) + votes_batch_loader.load_upvotes(object) end end end diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb index f0fd60e9cbb..ddced5ee859 100644 --- a/app/graphql/resolvers/user_resolver.rb +++ b/app/graphql/resolvers/user_resolver.rb @@ -39,7 +39,7 @@ module Resolvers def batch_load(username) BatchLoader::GraphQL.for(username).batch do |usernames, loader| User.by_username(usernames).each do |user| - loader.call(user.username, user) + loader.call(username, user) end end end diff --git a/app/graphql/resolvers/work_item_resolver.rb b/app/graphql/resolvers/work_item_resolver.rb index b174a0d2693..34e2f329efd 100644 --- a/app/graphql/resolvers/work_item_resolver.rb +++ b/app/graphql/resolvers/work_item_resolver.rb @@ -13,11 +13,5 @@ module Resolvers def resolve(id:) authorized_find!(id: id) end - - private - - def find_object(id:) - GitlabSchema.find_by_gid(id) - end end end diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb index 0c9aac80274..14eec4f696a 100644 --- a/app/graphql/resolvers/work_items_resolver.rb +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -4,30 +4,19 @@ module Resolvers class WorkItemsResolver < BaseResolver include SearchArguments include LooksAhead + include ::WorkItems::SharedFilterArguments - type Types::WorkItemType.connection_type, null: true + argument :iid, + GraphQL::Types::String, + required: false, + description: 'IID of the work item. For example, "1".' + argument :sort, + Types::WorkItemSortEnum, + description: 'Sort work items by criteria.', + required: false, + default_value: :created_desc - argument :author_username, GraphQL::Types::String, - required: false, - description: 'Filter work items by author username.', - alpha: { milestone: '15.9' } - argument :iid, GraphQL::Types::String, - required: false, - description: 'IID of the issue. For example, "1".' - argument :iids, [GraphQL::Types::String], - required: false, - description: 'List of IIDs of work items. For example, `["1", "2"]`.' - argument :sort, Types::WorkItemSortEnum, - description: 'Sort work items by this criteria.', - required: false, - default_value: :created_desc - argument :state, Types::IssuableStateEnum, - required: false, - description: 'Current state of this work item.' - argument :types, [Types::IssueTypeEnum], - as: :issue_types, - description: 'Filter work items by the given work item types.', - required: false + type Types::WorkItemType.connection_type, null: true def resolve_with_lookahead(**args) return WorkItem.none if resource_parent.nil? @@ -42,7 +31,7 @@ module Resolvers def preloads { work_item_type: :work_item_type, - web_url: { project: { namespace: :route } }, + web_url: { namespace: :route, project: [:project_namespace, { namespace: :route }] }, widgets: { work_item_type: :enabled_widget_definitions } } end @@ -66,7 +55,9 @@ module Resolvers parent: :work_item_parent, children: { work_item_children_by_relative_position: [:author, { project: :project_feature }] }, labels: :labels, - milestone: { milestone: [:project, :group] } + milestone: { milestone: [:project, :group] }, + subscribed: [:assignees, :award_emoji, { notes: [:author, :award_emoji] }], + award_emoji: { award_emoji: :awardable } } end diff --git a/app/graphql/subscriptions/base_subscription.rb b/app/graphql/subscriptions/base_subscription.rb index 5f7931787df..dcc9fe708d6 100644 --- a/app/graphql/subscriptions/base_subscription.rb +++ b/app/graphql/subscriptions/base_subscription.rb @@ -12,6 +12,18 @@ module Subscriptions current_user.reset if current_user end + # We override graphql-ruby's default `subscribe` since it returns + # :no_response instead, which leads to empty hashes rendered out + # to the caller which has caused problems in the client. + # + # Eventually, we should move to an approach where the caller receives + # a response here upon subscribing, but we don't need this currently + # because Vue components also perform an initial fetch query. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/402614 + def subscribe(*) + nil + end + def authorized?(*) raise NotImplementedError end diff --git a/app/graphql/subscriptions/issuable_updated.rb b/app/graphql/subscriptions/issuable_updated.rb index ad78fd4b4a1..63fe81bbc32 100644 --- a/app/graphql/subscriptions/issuable_updated.rb +++ b/app/graphql/subscriptions/issuable_updated.rb @@ -10,10 +10,6 @@ module Subscriptions required: true, description: 'ID of the issuable.' - def subscribe(issuable_id:) - nil - end - def authorized?(issuable_id:) issuable = force(GitlabSchema.find_by_gid(issuable_id)) diff --git a/app/graphql/subscriptions/notes/base.rb b/app/graphql/subscriptions/notes/base.rb index 3653c01e0e2..c117dc295f2 100644 --- a/app/graphql/subscriptions/notes/base.rb +++ b/app/graphql/subscriptions/notes/base.rb @@ -9,10 +9,6 @@ module Subscriptions required: false, description: 'ID of the noteable.' - def subscribe(*args) - nil - end - def authorized?(noteable_id:) noteable = force(GitlabSchema.find_by_gid(noteable_id)) diff --git a/app/graphql/types/achievements/achievement_type.rb b/app/graphql/types/achievements/achievement_type.rb index 67cc9778797..ec558981465 100644 --- a/app/graphql/types/achievements/achievement_type.rb +++ b/app/graphql/types/achievements/achievement_type.rb @@ -14,7 +14,6 @@ module Types field :namespace, ::Types::NamespaceType, - null: false, description: 'Namespace of the achievement.' field :name, @@ -42,6 +41,14 @@ module Types null: false, description: 'Timestamp the achievement was last updated.' + field :user_achievements, + Types::Achievements::UserAchievementType.connection_type, + null: true, + alpha: { milestone: '15.10' }, + description: "Recipients for the achievement.", + extras: [:lookahead], + resolver: ::Resolvers::Achievements::UserAchievementsResolver + def avatar_url object.avatar_url(only_path: false) end diff --git a/app/graphql/types/achievements/user_achievement_type.rb b/app/graphql/types/achievements/user_achievement_type.rb new file mode 100644 index 00000000000..bf161d2f1e5 --- /dev/null +++ b/app/graphql/types/achievements/user_achievement_type.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Types + module Achievements + class UserAchievementType < BaseObject + graphql_name 'UserAchievement' + + authorize :read_user_achievement + + field :id, + ::Types::GlobalIDType[::Achievements::UserAchievement], + null: false, + description: 'ID of the user achievement.' + + field :achievement, + ::Types::Achievements::AchievementType, + null: false, + description: 'Achievement awarded.' + + field :user, + ::Types::UserType, + null: false, + description: 'Achievement recipient.' + + field :awarded_by_user, + ::Types::UserType, + null: false, + description: 'Awarded by.' + + field :revoked_by_user, + ::Types::UserType, + null: true, + description: 'Revoked by.' + + field :created_at, + Types::TimeType, + null: false, + description: 'Timestamp the achievement was created.' + + field :updated_at, + Types::TimeType, + null: false, + description: 'Timestamp the achievement was last updated.' + + field :revoked_at, + Types::TimeType, + null: true, + description: 'Timestamp the achievement was revoked.' + end + end +end diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index a13453f9194..5784c7a4872 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -114,7 +114,9 @@ module Types field :metrics_dashboard_url, GraphQL::Types::String, null: true, - description: 'URL for metrics embed for the alert.' + description: 'URL for metrics embed for the alert.', + deprecated: { reason: 'Returns no data. Underlying feature was removed in 16.0', + milestone: '16.0' } field :runbook, GraphQL::Types::String, @@ -145,6 +147,12 @@ module Types def notes object.ordered_notes end + + def metrics_dashboard_url + return if Feature.enabled?(:remove_monitor_metrics) + + object.metrics_dashboard_url + end end end end diff --git a/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb new file mode 100644 index 00000000000..c9a28767e11 --- /dev/null +++ b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + module Analytics + module CycleAnalytics + module FlowMetrics + def self.[](context = :project) + Class.new(BaseObject) do + graphql_name "#{context.capitalize}ValueStreamAnalyticsFlowMetrics" + description 'Exposes aggregated value stream flow metrics' + + field :issue_count, + Types::Analytics::CycleAnalytics::MetricType, + null: true, + description: 'Number of issues opened in the given period.', + resolver: Resolvers::Analytics::CycleAnalytics::IssueCountResolver[context] + field :deployment_count, + Types::Analytics::CycleAnalytics::MetricType, + null: true, + description: 'Number of production deployments in the given period.', + resolver: Resolvers::Analytics::CycleAnalytics::DeploymentCountResolver[context] + end + end + end + end + end +end + +mod = Types::Analytics::CycleAnalytics::FlowMetrics +mod.prepend_mod_with('Types::Analytics::CycleAnalytics::FlowMetrics') diff --git a/app/graphql/types/analytics/cycle_analytics/link_type.rb b/app/graphql/types/analytics/cycle_analytics/link_type.rb new file mode 100644 index 00000000000..3db6b58ac55 --- /dev/null +++ b/app/graphql/types/analytics/cycle_analytics/link_type.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Types + module Analytics + module CycleAnalytics + # rubocop: disable Graphql/AuthorizeTypes + class LinkType < BaseObject + graphql_name 'ValueStreamMetricLinkType' + + field :name, + GraphQL::Types::String, + null: false, + description: 'Name of the link group.' + + field :label, + GraphQL::Types::String, + null: false, + description: 'Label for the link.' + + field :url, + GraphQL::Types::String, + null: false, + description: 'Drill-down URL.' + + field :docs_link, + GraphQL::Types::Boolean, + null: true, + description: 'Link to the metric documentation.' + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/analytics/cycle_analytics/metric_type.rb b/app/graphql/types/analytics/cycle_analytics/metric_type.rb new file mode 100644 index 00000000000..3f1a239019f --- /dev/null +++ b/app/graphql/types/analytics/cycle_analytics/metric_type.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Types + module Analytics + module CycleAnalytics + # rubocop: disable Graphql/AuthorizeTypes + class MetricType < BaseObject + graphql_name 'ValueStreamAnalyticsMetric' + description '' + + field :value, + GraphQL::Types::Float, + null: true, + description: 'Value for the metric.' + + field :identifier, + GraphQL::Types::String, + null: false, + description: 'Identifier for the metric.' + + field :unit, + GraphQL::Types::String, + null: true, + description: 'Unit of measurement.' + + field :title, + GraphQL::Types::String, + null: false, + description: 'Title for the metric.' + + field :links, + [LinkType], + null: false, + description: 'Optional links for drilling down.' + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb index 2352a21bd87..20661da8d94 100644 --- a/app/graphql/types/board_list_type.rb +++ b/app/graphql/types/board_list_type.rb @@ -55,7 +55,7 @@ module Types # board lists have a data dependency on label - so we batch load them here def title BatchLoader::GraphQL.for(object).batch do |lists, callback| - ActiveRecord::Associations::Preloader.new.preload(lists, :label) # rubocop: disable CodeReuse/ActiveRecord + ActiveRecord::Associations::Preloader.new(records: lists, associations: :label).call # rubocop: disable CodeReuse/ActiveRecord # all list titles are preloaded at this point lists.each { |list| callback.call(list, list.title) } diff --git a/app/graphql/types/branch_protections/base_access_level_type.rb b/app/graphql/types/branch_protections/base_access_level_type.rb index 472733a6bc5..e6514ba8d7d 100644 --- a/app/graphql/types/branch_protections/base_access_level_type.rb +++ b/app/graphql/types/branch_protections/base_access_level_type.rb @@ -14,7 +14,7 @@ module Types type: GraphQL::Types::String, null: false, description: 'Human readable representation for this access level.', - hash_key: 'humanize' + method: 'humanize' end end end diff --git a/app/graphql/types/ci/catalog/resource_type.rb b/app/graphql/types/ci/catalog/resource_type.rb new file mode 100644 index 00000000000..b5947826fa1 --- /dev/null +++ b/app/graphql/types/ci/catalog/resource_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Ci + module Catalog + # rubocop: disable Graphql/AuthorizeTypes + class ResourceType < BaseObject + graphql_name 'CiCatalogResource' + + connection_type_class(Types::CountableConnectionType) + + field :id, GraphQL::Types::ID, null: false, description: 'ID of the catalog resource.', + alpha: { milestone: '15.11' } + + field :name, GraphQL::Types::String, null: true, description: 'Name of the catalog resource.', + alpha: { milestone: '15.11' } + + field :description, GraphQL::Types::String, null: true, description: 'Description of the catalog resource.', + alpha: { milestone: '15.11' } + + field :icon, GraphQL::Types::String, null: true, description: 'Icon for the catalog resource.', + method: :avatar_path, alpha: { milestone: '15.11' } + end + # rubocop: enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb index dd6647b749d..f7ef94f58c0 100644 --- a/app/graphql/types/ci/ci_cd_setting_type.rb +++ b/app/graphql/types/ci/ci_cd_setting_type.rb @@ -30,11 +30,7 @@ module Types field :merge_trains_enabled, GraphQL::Types::Boolean, null: true, description: 'Whether merge trains are enabled.', method: :merge_trains_enabled? - field :opt_in_jwt, - GraphQL::Types::Boolean, - null: true, - description: 'When disabled, the JSON Web Token is always available in all jobs in the pipeline.', - method: :opt_in_jwt? + field :project, Types::ProjectType, null: true, description: 'Project the CI/CD settings belong to.' end diff --git a/app/graphql/types/ci/config/include_type_enum.rb b/app/graphql/types/ci/config/include_type_enum.rb index 328824ae996..7ebcf786dd8 100644 --- a/app/graphql/types/ci/config/include_type_enum.rb +++ b/app/graphql/types/ci/config/include_type_enum.rb @@ -11,6 +11,7 @@ module Types value 'local', description: 'Local include.', value: :local value 'file', description: 'Project file include.', value: :file value 'template', description: 'Template include.', value: :template + value 'component', description: 'Component include.', value: :component end end end diff --git a/app/graphql/types/ci/inherited_ci_variable_type.rb b/app/graphql/types/ci/inherited_ci_variable_type.rb new file mode 100644 index 00000000000..2d8dcdaeefe --- /dev/null +++ b/app/graphql/types/ci/inherited_ci_variable_type.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class InheritedCiVariableType < BaseObject + graphql_name 'InheritedCiVariable' + description 'CI/CD variables a project inherites from its parent group and ancestors.' + + field :id, GraphQL::Types::ID, + null: false, + description: 'ID of the variable.' + + field :key, GraphQL::Types::String, + null: true, + description: 'Name of the variable.' + + field :raw, GraphQL::Types::Boolean, + null: true, + description: 'Indicates whether the variable is raw.' + + field :variable_type, ::Types::Ci::VariableTypeEnum, + null: true, + description: 'Type of the variable.' + + field :environment_scope, GraphQL::Types::String, + null: true, + description: 'Scope defining the environments that can use the variable.' + + field :protected, GraphQL::Types::Boolean, + null: true, + description: 'Indicates whether the variable is protected.' + + field :masked, GraphQL::Types::Boolean, + null: true, + description: 'Indicates whether the variable is masked.' + + field :group_name, GraphQL::Types::String, + null: true, + description: 'Indicates group the variable belongs to.' + + field :group_ci_cd_settings_path, GraphQL::Types::String, + null: true, + description: 'Indicates the path to the CI/CD settings of the group the variable belongs to.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/job_trace_type.rb b/app/graphql/types/ci/job_trace_type.rb new file mode 100644 index 00000000000..a68e26106b8 --- /dev/null +++ b/app/graphql/types/ci/job_trace_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# rubocop: disable Graphql/AuthorizeTypes +module Types + module Ci + class JobTraceType < BaseObject + graphql_name 'CiJobTrace' + + field :html_summary, GraphQL::Types::String, null: false, + alpha: { milestone: '15.11' }, # As we want the option to change from 10 if needed + description: "HTML summary containing the last 10 lines of the trace." + + def html_summary + object.html(last_lines: 10).html_safe + end + end + end +end +# rubocop: enable Graphql/AuthorizeTypes diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index a97e9cee4b1..e77c2a38608 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -7,6 +7,8 @@ module Types class JobType < BaseObject graphql_name 'CiJob' + present_using ::Ci::BuildPresenter + connection_type_class(Types::LimitedCountableConnectionType) expose_permissions Types::PermissionTypes::Ci::Job @@ -25,6 +27,11 @@ module Types description: 'References to builds that must complete before the jobs run.' field :pipeline, Types::Ci::PipelineType, null: true, description: 'Pipeline the job belongs to.' + + field :runner, Types::Ci::RunnerType, null: true, description: 'Runner assigned to execute the job.' + field :runner_manager, ::Types::Ci::RunnerManagerType, null: true, + description: 'Runner manager assigned to the job.', + alpha: { milestone: '15.11' } field :stage, Types::Ci::StageType, null: true, description: 'Stage of the job.' field :status, @@ -76,6 +83,8 @@ module Types description: 'Whether the job has a manual action.' field :manual_variables, ManualVariableType.connection_type, null: true, description: 'Variables added to a manual job when the job is triggered.' + field :play_path, GraphQL::Types::String, null: true, + description: 'Play path of the job.' field :playable, GraphQL::Types::Boolean, null: false, method: :playable?, description: 'Indicates the job can be played.' field :previous_stage_jobs_or_needs, Types::Ci::JobNeedUnion.connection_type, null: true, @@ -86,14 +95,18 @@ module Types description: 'Path to the ref.' field :retried, GraphQL::Types::Boolean, null: true, description: 'Indicates that the job has been retried.' - field :retryable, GraphQL::Types::Boolean, null: false, method: :retryable?, + field :retryable, GraphQL::Types::Boolean, null: false, description: 'Indicates the job can be retried.' + field :scheduled, GraphQL::Types::Boolean, null: false, method: :scheduled?, + description: 'Indicates the job is scheduled.' field :scheduling_type, GraphQL::Types::String, null: true, description: 'Type of job scheduling. Value is `dag` if the job uses the `needs` keyword, and `stage` otherwise.' field :short_sha, type: GraphQL::Types::String, null: false, description: 'Short SHA1 ID of the commit.' field :stuck, GraphQL::Types::Boolean, null: false, method: :stuck?, description: 'Indicates the job is stuck.' + field :trace, Types::Ci::JobTraceType, null: true, + description: 'Trace generated by the job.' field :triggered, GraphQL::Types::Boolean, null: true, description: 'Whether the job was triggered.' field :web_path, GraphQL::Types::String, null: true, @@ -101,10 +114,25 @@ module Types field :project, Types::ProjectType, null: true, description: 'Project that the job belongs to.' + field :can_play_job, GraphQL::Types::Boolean, + null: false, resolver_method: :can_play_job?, + description: 'Indicates whether the current user can play the job.' + + field :failure_message, GraphQL::Types::String, null: true, + description: 'Message on why the job failed.' + + def can_play_job? + object.playable? && Ability.allowed?(current_user, :play_job, object) + end + def kind - return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class) + return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.build.class) + + object.build.class + end - object.class + def retryable + object.build.retryable? end def pipeline @@ -129,6 +157,10 @@ module Types end end + def trace + object.trace if object.has_trace? + end + def previous_stage_jobs_or_needs if object.scheduling_type == 'stage' Gitlab::Graphql::Lazy.with_value(previous_stage_jobs) do |jobs| @@ -157,6 +189,24 @@ module Types ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Stage, object.stage_id).find end + def runner + Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Runner, object.runner_id).find + end + + def runner_manager + BatchLoader::GraphQL.for(object.id).batch(key: :runner_managers) do |build_ids, loader| + plucked_build_to_runner_manager_ids = + ::Ci::RunnerManagerBuild.for_build(build_ids).pluck_build_id_and_runner_manager_id + runner_managers = ::Ci::RunnerManager.id_in(plucked_build_to_runner_manager_ids.values.uniq) + Preloaders::RunnerManagerPolicyPreloader.new(runner_managers, current_user).execute + runner_managers_by_id = runner_managers.index_by(&:id) + + build_ids.each do |build_id| + loader.call(build_id, runner_managers_by_id[plucked_build_to_runner_manager_ids[build_id]]) + end + end + end + # This class is a secret union! # TODO: turn this into an actual union, so that fields can be referenced safely! def id @@ -183,6 +233,10 @@ module Types ::Gitlab::Routing.url_helpers.project_job_path(object.project, object) end + def play_path + ::Gitlab::Routing.url_helpers.play_project_job_path(object.project, object) + end + def browse_artifacts_path ::Gitlab::Routing.url_helpers.browse_project_job_artifacts_path(object.project, object) end diff --git a/app/graphql/types/ci/runner_manager_type.rb b/app/graphql/types/ci/runner_manager_type.rb new file mode 100644 index 00000000000..2a5053f8f07 --- /dev/null +++ b/app/graphql/types/ci/runner_manager_type.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Types + module Ci + class RunnerManagerType < BaseObject + graphql_name 'CiRunnerManager' + + connection_type_class(::Types::CountableConnectionType) + + authorize :read_runner_manager + + alias_method :runner_manager, :object + + field :architecture_name, GraphQL::Types::String, null: true, + description: 'Architecture provided by the runner manager.', + method: :architecture + field :contacted_at, Types::TimeType, null: true, + description: 'Timestamp of last contact from the runner manager.', + method: :contacted_at + field :created_at, Types::TimeType, null: true, + description: 'Timestamp of creation of the runner manager.' + field :executor_name, GraphQL::Types::String, null: true, + description: 'Executor last advertised by the runner.', + method: :executor_name + field :id, ::Types::GlobalIDType[::Ci::RunnerManager], null: false, + description: 'ID of the runner manager.' + field :ip_address, GraphQL::Types::String, null: true, + description: 'IP address of the runner manager.' + field :platform_name, GraphQL::Types::String, null: true, + description: 'Platform provided by the runner manager.', + method: :platform + field :revision, GraphQL::Types::String, null: true, description: 'Revision of the runner.' + field :runner, RunnerType, null: true, description: 'Runner configuration for the runner manager.' + field :status, + Types::Ci::RunnerStatusEnum, + null: false, + description: 'Status of the runner manager.' + field :system_id, GraphQL::Types::String, + null: false, + description: 'System ID associated with the runner manager.', + method: :system_xid + field :version, GraphQL::Types::String, null: true, description: 'Version of the runner.' + + def executor_name + ::Ci::Runner::EXECUTOR_TYPE_TO_NAMES[runner_manager.executor_type&.to_sym] + end + end + end +end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index 10d18f9ad2a..8e509cc8493 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -14,9 +14,6 @@ module Types JOB_COUNT_LIMIT = 1000 - # Only allow ephemeral_authentication_token to be visible for a short while - RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME = 3.hours - alias_method :runner, :object field :access_level, ::Types::Ci::RunnerAccessLevelEnum, null: false, @@ -34,14 +31,20 @@ module Types method: :contacted_at field :created_at, Types::TimeType, null: true, description: 'Timestamp of creation of this runner.' + field :created_by, Types::UserType, null: true, + description: 'User that created this runner.', + method: :creator field :description, GraphQL::Types::String, null: true, description: 'Description of the runner.' field :edit_admin_url, GraphQL::Types::String, null: true, description: 'Admin form URL of the runner. Only available for administrators.' field :ephemeral_authentication_token, GraphQL::Types::String, null: true, - description: 'Ephemeral authentication token used for runner machine registration.', + description: 'Ephemeral authentication token used for runner manager registration. Only available for the creator of the runner for a limited time during registration.', authorize: :read_ephemeral_token, alpha: { milestone: '15.9' } + field :ephemeral_register_url, GraphQL::Types::String, null: true, + description: 'URL of the registration page of the runner manager. Only available for the creator of the runner for a limited time during registration.', + alpha: { milestone: '15.11' } field :executor_name, GraphQL::Types::String, null: true, description: 'Executor last advertised by the runner.', method: :executor_name @@ -58,7 +61,7 @@ module Types Types::Ci::RunnerJobExecutionStatusEnum, null: true, description: 'Job execution status of the runner.', - deprecated: { milestone: '15.7', reason: :alpha } + alpha: { milestone: '15.7' } field :jobs, ::Types::Ci::JobType.connection_type, null: true, description: 'Jobs assigned to the runner. This field can only be resolved for one runner in any single request.', authorize: :read_builds, @@ -67,6 +70,10 @@ module Types description: 'Indicates the runner is locked.' field :maintenance_note, GraphQL::Types::String, null: true, description: 'Runner\'s maintenance notes.' + field :managers, ::Types::Ci::RunnerManagerType.connection_type, null: true, + description: 'Machines associated with the runner configuration.', + method: :runner_managers, + alpha: { milestone: '15.10' } field :maximum_timeout, GraphQL::Types::Int, null: true, description: 'Maximum timeout (in seconds) for jobs processed by the runner.' field :owner_project, ::Types::ProjectType, null: true, @@ -84,6 +91,8 @@ module Types null: true, resolver: ::Resolvers::Ci::RunnerProjectsResolver, description: 'Find projects the runner is associated with. For project runners only.' + field :register_admin_url, GraphQL::Types::String, null: true, + description: 'URL of the temporary registration page of the runner. Only available before the runner is registered. Only available for administrators.' field :revision, GraphQL::Types::String, null: true, description: 'Revision of the runner.' field :run_untagged, GraphQL::Types::Boolean, null: false, @@ -141,12 +150,27 @@ module Types Gitlab::Routing.url_helpers.edit_admin_runner_url(runner) if can_admin_runners? end - def ephemeral_authentication_token - return unless runner.authenticated_user_registration_type? - return unless runner.created_at > RUNNER_EPHEMERAL_TOKEN_AVAILABILITY_TIME.ago - return if runner.runner_machines.any? + def ephemeral_register_url + return unless context[:current_user]&.can?(:read_ephemeral_token, runner) && runner.registration_available? + + case runner.runner_type + when 'instance_type' + Gitlab::Routing.url_helpers.register_admin_runner_url(runner) + when 'group_type' + Gitlab::Routing.url_helpers.register_group_runner_url(runner.groups[0], runner) + when 'project_type' + Gitlab::Routing.url_helpers.register_project_runner_url(runner.projects[0], runner) + end + end - runner.token + def register_admin_url + return unless can_admin_runners? && runner.registration_available? + + Gitlab::Routing.url_helpers.register_admin_runner_url(runner) + end + + def ephemeral_authentication_token + runner.token if runner.registration_available? end def project_count diff --git a/app/graphql/types/clusters/agent_activity_event_type.rb b/app/graphql/types/clusters/agent_activity_event_type.rb index 3484acfe25e..1d0ec7c4959 100644 --- a/app/graphql/types/clusters/agent_activity_event_type.rb +++ b/app/graphql/types/clusters/agent_activity_event_type.rb @@ -5,7 +5,7 @@ module Types class AgentActivityEventType < BaseObject graphql_name 'ClusterAgentActivityEvent' - authorize :read_cluster + authorize :read_cluster_agent connection_type_class(Types::CountableConnectionType) diff --git a/app/graphql/types/clusters/agent_token_type.rb b/app/graphql/types/clusters/agent_token_type.rb index 24489707698..720ee2f685b 100644 --- a/app/graphql/types/clusters/agent_token_type.rb +++ b/app/graphql/types/clusters/agent_token_type.rb @@ -5,7 +5,7 @@ module Types class AgentTokenType < BaseObject graphql_name 'ClusterAgentToken' - authorize :read_cluster + authorize :read_cluster_agent connection_type_class(Types::CountableConnectionType) diff --git a/app/graphql/types/clusters/agent_type.rb b/app/graphql/types/clusters/agent_type.rb index 5d7b8815cde..317a1aab628 100644 --- a/app/graphql/types/clusters/agent_type.rb +++ b/app/graphql/types/clusters/agent_type.rb @@ -5,7 +5,7 @@ module Types class AgentType < BaseObject graphql_name 'ClusterAgent' - authorize :read_cluster + authorize :read_cluster_agent connection_type_class(Types::CountableConnectionType) diff --git a/app/graphql/types/clusters/agents/authorizations/ci_access_type.rb b/app/graphql/types/clusters/agents/authorizations/ci_access_type.rb new file mode 100644 index 00000000000..a60f32b8b0b --- /dev/null +++ b/app/graphql/types/clusters/agents/authorizations/ci_access_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Clusters + module Agents + module Authorizations + class CiAccessType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'ClusterAgentAuthorizationCiAccess' + + field :agent, Types::Clusters::AgentType, + description: 'Authorized cluster agent.', + null: true + + field :config, GraphQL::Types::JSON, # rubocop:disable Graphql/JSONType + description: 'Configuration for the authorized project.', + null: true + end + end + end + end +end diff --git a/app/graphql/types/clusters/agents/authorizations/user_access_type.rb b/app/graphql/types/clusters/agents/authorizations/user_access_type.rb new file mode 100644 index 00000000000..8c5a466cde2 --- /dev/null +++ b/app/graphql/types/clusters/agents/authorizations/user_access_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Clusters + module Agents + module Authorizations + class UserAccessType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'ClusterAgentAuthorizationUserAccess' + + field :agent, Types::Clusters::AgentType, + description: 'Authorized cluster agent.', + null: true + + field :config, GraphQL::Types::JSON, # rubocop:disable Graphql/JSONType + description: 'Configuration for the authorized project.', + null: true + end + end + end + end +end diff --git a/app/graphql/types/commit_references_type.rb b/app/graphql/types/commit_references_type.rb new file mode 100644 index 00000000000..2844a552f3e --- /dev/null +++ b/app/graphql/types/commit_references_type.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Types + class CommitReferencesType < BaseObject + graphql_name 'CommitReferences' + + authorize :read_commit + + def self.field_for_tipping_refs(field_name, field_description) + field field_name, ::Types::Projects::CommitParentNamesType, + null: true, + calls_gitaly: true, + description: field_description do + argument :limit, GraphQL::Types::Int, + required: true, + default_value: 100, + description: 'Number of ref names to return.', + validates: { numericality: { within: 1..1000 } } + end + end + + def self.field_for_containing_refs(field_name, field_description) + field field_name, ::Types::Projects::CommitParentNamesType, + null: true, + calls_gitaly: true, + description: field_description do + argument :exclude_tipped, GraphQL::Types::Boolean, + required: true, + default_value: false, + description: 'Exclude tipping refs. WARNING: This argument can be confusing, if there is a limit. + for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs, + then the method will only 3 refs, even though there is more.' + # rubocop: disable GraphQL/ArgumentUniqueness + argument :limit, GraphQL::Types::Int, + required: true, + default_value: 100, + description: 'Number of ref names to return.', + validates: { numericality: { within: 1..1000 } } + # rubocop: enable GraphQL/ArgumentUniqueness + end + end + + field_for_tipping_refs :tipping_tags, "Get tag names tipping at a given commit." + + field_for_tipping_refs :tipping_branches, "Get branch names tipping at a given commit." + + field_for_containing_refs :containing_tags, "Get tag names containing a given commit." + + field_for_containing_refs :containing_branches, "Get branch names containing a given commit." + + def tipping_tags(limit:) + { names: object.tipping_tags(limit: limit) } + end + + def tipping_branches(limit:) + { names: object.tipping_branches(limit: limit) } + end + + def containing_tags(limit:, exclude_tipped:) + { names: object.tags_containing(limit: limit, exclude_tipped: exclude_tipped) } + end + + def containing_branches(limit:, exclude_tipped:) + { names: object.branches_containing(limit: limit, exclude_tipped: exclude_tipped) } + end + end +end diff --git a/app/graphql/types/commit_signatures/ssh_signature_type.rb b/app/graphql/types/commit_signatures/ssh_signature_type.rb index 92eb4f7949a..d5db98c39a0 100644 --- a/app/graphql/types/commit_signatures/ssh_signature_type.rb +++ b/app/graphql/types/commit_signatures/ssh_signature_type.rb @@ -10,14 +10,19 @@ module Types authorize :download_code - field :user, Types::UserType, null: true, - method: :signed_by_user, - calls_gitaly: true, - description: 'User associated with the key.' + field :user, Types::UserType, + null: true, + method: :signed_by_user, + calls_gitaly: true, + description: 'User associated with the key.' field :key, Types::KeyType, - null: true, - description: 'SSH key used for the signature.' + null: true, + description: 'SSH key used for the signature.' + + field :key_fingerprint_sha256, String, + null: true, + description: 'Fingerprint of the key.' end end end diff --git a/app/graphql/types/data_transfer/base_type.rb b/app/graphql/types/data_transfer/base_type.rb index e077612bfd5..5031bd5c612 100644 --- a/app/graphql/types/data_transfer/base_type.rb +++ b/app/graphql/types/data_transfer/base_type.rb @@ -7,7 +7,7 @@ module Types field :egress_nodes, type: Types::DataTransfer::EgressNodeType.connection_type, description: 'Data nodes.', - null: true # disallow null once data_transfer_monitoring feature flag is rolled-out! + null: true # disallow null once data_transfer_monitoring feature flag is rolled-out! https://gitlab.com/gitlab-org/gitlab/-/issues/397693 end end end diff --git a/app/graphql/types/data_transfer/egress_node_type.rb b/app/graphql/types/data_transfer/egress_node_type.rb index a050540999f..f0ad3d15b53 100644 --- a/app/graphql/types/data_transfer/egress_node_type.rb +++ b/app/graphql/types/data_transfer/egress_node_type.rb @@ -26,12 +26,8 @@ module Types null: false field :registry_egress, GraphQL::Types::BigInt, - description: 'Registery egress for that project in that period of time.', + description: 'Registry egress for that project in that period of time.', null: false - - def total_egress - object.values.select { |x| x.is_a?(Integer) }.sum - end end end end diff --git a/app/graphql/types/data_transfer/project_data_transfer_type.rb b/app/graphql/types/data_transfer/project_data_transfer_type.rb index f385aa20a7e..36afa20194e 100644 --- a/app/graphql/types/data_transfer/project_data_transfer_type.rb +++ b/app/graphql/types/data_transfer/project_data_transfer_type.rb @@ -8,12 +8,14 @@ module Types field :total_egress, GraphQL::Types::BigInt, description: 'Total egress for that project in that period of time.', - null: true # disallow null once data_transfer_monitoring feature flag is rolled-out! + null: true, # disallow null once data_transfer_monitoring feature flag is rolled-out! https://gitlab.com/gitlab-org/gitlab/-/issues/397693 + extras: [:parent] - def total_egress(**_) - return unless Feature.enabled?(:data_transfer_monitoring) + def total_egress(parent:) + return unless Feature.enabled?(:data_transfer_monitoring, parent.group) + return 40_000_000 if Feature.enabled?(:data_transfer_monitoring_mock_data, parent.group) - 40_000_000 + object[:egress_nodes].sum('repository_egress + artifacts_egress + packages_egress + registry_egress') end end end diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb index cc4c0e19ec7..be5edd17643 100644 --- a/app/graphql/types/design_management/design_type.rb +++ b/app/graphql/types/design_management/design_type.rb @@ -15,6 +15,11 @@ module Types implements(Types::CurrentUserTodos) implements(Types::TodoableInterface) + field :description, + GraphQL::Types::String, + null: true, + description: 'Description of the design.' + field :web_url, GraphQL::Types::String, null: false, @@ -25,6 +30,8 @@ module Types resolver: Resolvers::DesignManagement::VersionsResolver, description: "All versions related to this design ordered newest first." + markdown_field :description_html, null: true + # Returns a `DesignManagement::Version` for this query based on the # `atVersion` argument passed to a parent node if present, or otherwise # the most recent `Version` for the issue. diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 5f58fc38540..a3737cbcd0d 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -53,7 +53,8 @@ module Types field :metrics_dashboard, Types::Metrics::DashboardType, null: true, description: 'Metrics dashboard schema for the environment.', - resolver: Resolvers::Metrics::DashboardResolver + resolver: Resolvers::Metrics::DashboardResolver, + deprecated: { reason: 'Returns no data. Underlying feature was removed in 16.0', milestone: '16.0' } field :latest_opened_most_severe_alert, Types::AlertManagement::AlertType, diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 3543ac29c17..da2c06d04b7 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -167,6 +167,11 @@ module Types null: false, description: 'Total size of the dependency proxy cached images.' + field :dependency_proxy_total_size_in_bytes, + GraphQL::Types::Int, + null: false, + description: 'Total size of the dependency proxy cached images in bytes.' + field :dependency_proxy_image_prefix, GraphQL::Types::String, null: false, @@ -241,7 +246,7 @@ module Types field :data_transfer, Types::DataTransfer::GroupDataTransferType, null: true, - resolver: Resolvers::DataTransferResolver.group, + resolver: Resolvers::DataTransfer::GroupDataTransferResolver, description: 'Data transfer data point for a specific period. This is mocked data under a development feature flag.' def label(title:) @@ -279,10 +284,14 @@ module Types def dependency_proxy_total_size ActiveSupport::NumberHelper.number_to_human_size( - group.dependency_proxy_manifests.sum(:size) + group.dependency_proxy_blobs.sum(:size) + dependency_proxy_total_size_in_bytes ) end + def dependency_proxy_total_size_in_bytes + group.dependency_proxy_manifests.sum(:size) + group.dependency_proxy_blobs.sum(:size) + end + def dependency_proxy_setting group.dependency_proxy_setting || group.create_dependency_proxy_setting end diff --git a/app/graphql/types/issuable_subscription_event_enum.rb b/app/graphql/types/issuable_subscription_event_enum.rb new file mode 100644 index 00000000000..0f56fab8b46 --- /dev/null +++ b/app/graphql/types/issuable_subscription_event_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class IssuableSubscriptionEventEnum < BaseEnum + graphql_name 'IssuableSubscriptionEvent' + description 'Values for subscribing and unsubscribing from issuables' + + value 'SUBSCRIBE', 'Subscribe to an issuable.', value: 'subscribe' + value 'UNSUBSCRIBE', 'Unsubscribe from an issuable.', value: 'unsubscribe' + end +end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 1e5833a5cf0..488e4d10cbc 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -122,6 +122,7 @@ module Types description: 'Collection of design images associated with this issue.' field :type, Types::IssueTypeEnum, null: true, + method: :issue_type, description: 'Type of the issue.' field :alert_management_alert, @@ -209,14 +210,6 @@ module Types def escalation_status object.supports_escalation? ? object.escalation_status&.status_name : nil end - - def type - if Feature.enabled?(:issue_type_uses_work_item_types_table) - object.work_item_type.base_type - else - object.issue_type - end - end end end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 3c288c1d496..f32dfc0dbcf 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -219,6 +219,13 @@ module Types field :timelogs, Types::TimelogType.connection_type, null: false, description: 'Timelogs on the merge request.' + field :award_emoji, Types::AwardEmojis::AwardEmojiType.connection_type, + null: true, + description: 'List of award emojis associated with the merge request.' + + field :prepared_at, Types::TimeType, null: true, + description: 'Timestamp of when the merge request was prepared.' + markdown_field :title_html, null: true markdown_field :description_html, null: true @@ -295,6 +302,13 @@ module Types def detailed_merge_status ::MergeRequests::Mergeability::DetailedMergeStatusService.new(merge_request: object).execute end + + # This is temporary to fix a bug where `committers` is already loaded and memoized + # and calling it again with a certain GraphQL query can cause the Rails to to throw + # a ActiveRecord::ImmutableRelation error + def committers + object.commits.committers + end end end diff --git a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb index 1ba72ae33b5..e7246068a05 100644 --- a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb +++ b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb @@ -45,6 +45,9 @@ module Types value 'EXTERNAL_STATUS_CHECKS', value: :status_checks_must_pass, description: 'Status checks must pass.' + value 'PREPARING', + value: :preparing, + description: 'Merge request diff is being created.' end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index e48e9deae96..7e436d74dcf 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -6,7 +6,11 @@ module Types include Gitlab::Graphql::MountMutation - mount_mutation Mutations::Achievements::Create + mount_mutation Mutations::Achievements::Award, alpha: { milestone: '15.10' } + mount_mutation Mutations::Achievements::Create, alpha: { milestone: '15.8' } + mount_mutation Mutations::Achievements::Delete, alpha: { milestone: '15.11' } + mount_mutation Mutations::Achievements::Revoke, alpha: { milestone: '15.10' } + mount_mutation Mutations::Achievements::Update, alpha: { milestone: '15.11' } mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs mount_mutation Mutations::AlertManagement::CreateAlertIssue mount_mutation Mutations::AlertManagement::UpdateAlertStatus @@ -48,6 +52,7 @@ module Types mount_mutation Mutations::DependencyProxy::ImageTtlGroupPolicy::Update mount_mutation Mutations::DependencyProxy::GroupSettings::Update mount_mutation Mutations::Environments::CanaryIngress::Update + mount_mutation Mutations::Environments::Stop mount_mutation Mutations::IncidentManagement::TimelineEvent::Create, alpha: { milestone: '15.6' } mount_mutation Mutations::IncidentManagement::TimelineEvent::PromoteFromNote mount_mutation Mutations::IncidentManagement::TimelineEvent::Update @@ -69,6 +74,7 @@ module Types mount_mutation Mutations::Issues::BulkUpdate, alpha: { milestone: '15.9' } mount_mutation Mutations::Labels::Create mount_mutation Mutations::Members::Groups::BulkUpdate + mount_mutation Mutations::Members::Projects::BulkUpdate mount_mutation Mutations::MergeRequests::Accept mount_mutation Mutations::MergeRequests::Create mount_mutation Mutations::MergeRequests::Update @@ -80,8 +86,14 @@ module Types mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::SetReviewers mount_mutation Mutations::MergeRequests::ReviewerRereview - mount_mutation Mutations::Metrics::Dashboard::Annotations::Create - mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete + mount_mutation Mutations::Metrics::Dashboard::Annotations::Create, deprecated: { + reason: 'Underlying feature was removed in 16.0', + milestone: '16.0' + } + mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete, deprecated: { + reason: 'Underlying feature was removed in 16.0', + milestone: '16.0' + } mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true @@ -89,6 +101,7 @@ module Types mount_mutation Mutations::Notes::Update::ImageDiffNote mount_mutation Mutations::Notes::RepositionImageDiffNote mount_mutation Mutations::Notes::Destroy + mount_mutation Mutations::Projects::SyncFork, calls_gitaly: true, alpha: { milestone: '15.9' } mount_mutation Mutations::Releases::Create mount_mutation Mutations::Releases::Update mount_mutation Mutations::Releases::Delete @@ -114,6 +127,7 @@ module Types mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true mount_mutation Mutations::DesignManagement::Move + mount_mutation Mutations::DesignManagement::Update mount_mutation Mutations::ContainerExpirationPolicies::Update mount_mutation Mutations::ContainerRepositories::Destroy mount_mutation Mutations::ContainerRepositories::DestroyTags @@ -137,8 +151,10 @@ module Types mount_mutation Mutations::Ci::Job::Cancel mount_mutation Mutations::Ci::Job::Unschedule mount_mutation Mutations::Ci::JobArtifact::Destroy + mount_mutation Mutations::Ci::JobArtifact::BulkDestroy, alpha: { milestone: '15.10' } mount_mutation Mutations::Ci::JobTokenScope::AddProject mount_mutation Mutations::Ci::JobTokenScope::RemoveProject + mount_mutation Mutations::Ci::Runner::Create, alpha: { milestone: '15.10' } mount_mutation Mutations::Ci::Runner::Update mount_mutation Mutations::Ci::Runner::Delete mount_mutation Mutations::Ci::Runner::BulkDelete, alpha: { milestone: '15.3' } @@ -160,6 +176,8 @@ module Types mount_mutation Mutations::WorkItems::DeleteTask, alpha: { milestone: '15.1' } mount_mutation Mutations::WorkItems::Update, alpha: { milestone: '15.1' } mount_mutation Mutations::WorkItems::UpdateTask, alpha: { milestone: '15.1' } + mount_mutation Mutations::WorkItems::Export, alpha: { milestone: '15.10' } + mount_mutation Mutations::WorkItems::Convert, alpha: { milestone: '15.11' } mount_mutation Mutations::SavedReplies::Create mount_mutation Mutations::SavedReplies::Update mount_mutation Mutations::Pages::MarkOnboardingComplete diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb index fc55ff512b6..3420f16213f 100644 --- a/app/graphql/types/namespace_type.rb +++ b/app/graphql/types/namespace_type.rb @@ -68,7 +68,9 @@ module Types null: true, alpha: { milestone: '15.8' }, description: "Achievements for the namespace. " \ - "Returns `null` if the `achievements` feature flag is disabled." + "Returns `null` if the `achievements` feature flag is disabled.", + extras: [:lookahead], + resolver: ::Resolvers::Achievements::AchievementsResolver markdown_field :description_html, null: true @@ -83,10 +85,6 @@ module Types def root_storage_statistics Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find end - - def achievements - object.achievements if Feature.enabled?(:achievements, object) - end end end diff --git a/app/graphql/types/packages/package_base_type.rb b/app/graphql/types/packages/package_base_type.rb index 9ec4bb73c47..8dd2a4467d6 100644 --- a/app/graphql/types/packages/package_base_type.rb +++ b/app/graphql/types/packages/package_base_type.rb @@ -52,8 +52,6 @@ module Types object.nuget_metadatum when 'pypi' object.pypi_metadatum - else - nil end end # rubocop: enable GraphQL/ResolverMethodLength diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb index f63b41b3c92..e00d6eac72f 100644 --- a/app/graphql/types/packages/package_details_type.rb +++ b/app/graphql/types/packages/package_details_type.rb @@ -63,11 +63,11 @@ module Types end def pypi_url - pypi_registry_url(object.project.id) + pypi_registry_url(object.project) end def public_package - object.project.public? || object.project.project_feature.package_registry_access_level == ProjectFeature::PUBLIC + object.project.project_feature.public_packages? end end end diff --git a/app/graphql/types/packages/package_file_type.rb b/app/graphql/types/packages/package_file_type.rb index 3ee0d983745..4f1173a9e0b 100644 --- a/app/graphql/types/packages/package_file_type.rb +++ b/app/graphql/types/packages/package_file_type.rb @@ -29,8 +29,6 @@ module Types object.conan_file_metadatum when 'helm' object.helm_file_metadatum - else - nil end end end diff --git a/app/graphql/types/permission_types/ci/pipeline_schedules.rb b/app/graphql/types/permission_types/ci/pipeline_schedules.rb index 268ac6096d0..dd9d94aa578 100644 --- a/app/graphql/types/permission_types/ci/pipeline_schedules.rb +++ b/app/graphql/types/permission_types/ci/pipeline_schedules.rb @@ -6,11 +6,16 @@ module Types class PipelineSchedules < BasePermissionType graphql_name 'PipelineSchedulePermissions' - abilities :take_ownership_pipeline_schedule, - :update_pipeline_schedule, + abilities :update_pipeline_schedule, :admin_pipeline_schedule ability_field :play_pipeline_schedule, calls_gitaly: true + ability_field :take_ownership_pipeline_schedule, + deprecated: { + reason: 'Use admin_pipeline_schedule permission to determine if the user can take ownership ' \ + 'of a pipeline schedule', + milestone: '15.9' + } end end end diff --git a/app/graphql/types/permission_types/group_enum.rb b/app/graphql/types/permission_types/group_enum.rb index f636d43790f..6d51d94a70d 100644 --- a/app/graphql/types/permission_types/group_enum.rb +++ b/app/graphql/types/permission_types/group_enum.rb @@ -10,6 +10,9 @@ module Types value 'TRANSFER_PROJECTS', value: :transfer_projects, description: 'Groups where the user can transfer projects to.' + value 'IMPORT_PROJECTS', + value: :import_projects, + description: 'Groups where the user can import projects to.' end end end diff --git a/app/graphql/types/permission_types/issue.rb b/app/graphql/types/permission_types/issue.rb index b38971b64cd..a76dc88adfc 100644 --- a/app/graphql/types/permission_types/issue.rb +++ b/app/graphql/types/permission_types/issue.rb @@ -8,7 +8,7 @@ module Types abilities :read_issue, :admin_issue, :update_issue, :reopen_issue, :read_design, :create_design, :destroy_design, - :create_note + :create_note, :update_design end end end diff --git a/app/graphql/types/permission_types/work_item.rb b/app/graphql/types/permission_types/work_item.rb index f35f42001e0..0b6a384ec0e 100644 --- a/app/graphql/types/permission_types/work_item.rb +++ b/app/graphql/types/permission_types/work_item.rb @@ -6,7 +6,8 @@ module Types graphql_name 'WorkItemPermissions' description 'Check permissions for the current user on a work item' - abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item + abilities :read_work_item, :update_work_item, :delete_work_item, + :admin_work_item, :admin_parent_link, :set_work_item_metadata end end end diff --git a/app/graphql/types/project_statistics_redirect_type.rb b/app/graphql/types/project_statistics_redirect_type.rb new file mode 100644 index 00000000000..c8fec0a54c4 --- /dev/null +++ b/app/graphql/types/project_statistics_redirect_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class ProjectStatisticsRedirectType < BaseObject + graphql_name 'ProjectStatisticsRedirect' + + field :repository, GraphQL::Types::String, null: false, + description: 'Redirection Route for repository.' + + field :wiki, GraphQL::Types::String, null: false, + description: 'Redirection Route for wiki.' + + field :build_artifacts, GraphQL::Types::String, null: false, + description: 'Redirection Route for job_artifacts.' + + field :packages, GraphQL::Types::String, null: false, + description: 'Redirection Route for packages.' + + field :snippets, GraphQL::Types::String, null: false, + description: 'Redirection Route for snippets.' + + field :container_registry, GraphQL::Types::String, null: false, + description: 'Redirection Route for container_registry.' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index c105ab9814c..f8a516501c3 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -24,9 +24,9 @@ module Types authorize: :create_pipeline, alpha: { milestone: '15.3' }, description: 'CI/CD config variable.' do - argument :sha, GraphQL::Types::String, + argument :ref, GraphQL::Types::String, required: true, - description: 'Sha.' + description: 'Ref.' end field :full_path, GraphQL::Types::ID, @@ -136,6 +136,11 @@ module Types null: true, description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.' + field :is_catalog_resource, GraphQL::Types::Boolean, + alpha: { milestone: '15.11' }, + null: true, + description: 'Indicates if a project is a catalog resource.' + field :public_jobs, GraphQL::Types::Boolean, null: true, description: 'Indicates if there is public access to pipelines and job details of the project, ' \ @@ -209,6 +214,11 @@ module Types null: true, description: 'Statistics of the project.' + field :statistics_details_paths, Types::ProjectStatisticsRedirectType, + null: true, + description: 'Redirects for Statistics of the project.', + calls_gitaly: true + field :repository, Types::RepositoryType, null: true, description: 'Git repository of the project.' @@ -347,6 +357,12 @@ module Types authorize: :admin_build, resolver: Resolvers::Ci::VariablesResolver + field :inherited_ci_variables, Types::Ci::InheritedCiVariableType.connection_type, + null: true, + description: "List of CI/CD variables the project inherited from its parent group and ancestors.", + authorize: :admin_build, + resolver: Resolvers::Ci::InheritedVariablesResolver + field :ci_cd_settings, Types::Ci::CiCdSettingType, null: true, description: 'CI/CD settings for the project.' @@ -517,6 +533,18 @@ module Types description: 'Cluster agents associated with the project.', resolver: ::Resolvers::Clusters::AgentsResolver + field :ci_access_authorized_agents, ::Types::Clusters::Agents::Authorizations::CiAccessType.connection_type, + null: true, + description: 'Authorized cluster agents for the project through ci_access keyword.', + resolver: ::Resolvers::Clusters::Agents::Authorizations::CiAccessResolver, + authorize: :read_cluster_agent + + field :user_access_authorized_agents, ::Types::Clusters::Agents::Authorizations::UserAccessType.connection_type, + null: true, + description: 'Authorized cluster agents for the project through user_access keyword.', + resolver: ::Resolvers::Clusters::Agents::Authorizations::UserAccessResolver, + authorize: :read_cluster_agent + field :merge_commit_template, GraphQL::Types::String, null: true, description: 'Template used to create merge commit message in merge requests.' @@ -567,8 +595,8 @@ module Types description: "Find runners visible to the current user." field :data_transfer, Types::DataTransfer::ProjectDataTransferType, - null: true, # disallow null once data_transfer_monitoring feature flag is rolled-out! - resolver: Resolvers::DataTransferResolver.project, + null: true, # disallow null once data_transfer_monitoring feature flag is rolled-out! https://gitlab.com/gitlab-org/gitlab/-/issues/391682 + resolver: Resolvers::DataTransfer::ProjectDataTransferResolver, description: 'Data transfer data point for a specific period. This is mocked data under a development feature flag.' field :visible_forks, Types::ProjectType.connection_type, @@ -581,6 +609,20 @@ module Types description: 'Minimum access level.' end + field :flow_metrics, + ::Types::Analytics::CycleAnalytics::FlowMetrics[:project], + null: true, + description: 'Flow metrics for value stream analytics.', + method: :project_namespace, + authorize: :read_cycle_analytics, + alpha: { milestone: '15.10' } + + field :commit_references, ::Types::CommitReferencesType, + null: true, + resolver: Resolvers::Projects::CommitReferencesResolver, + alpha: { milestone: '16.0' }, + description: "Get tag names containing a given commit." + def timelog_categories object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories) end @@ -627,6 +669,16 @@ module Types BatchLoader::GraphQL.wrap(object.forks_count) end + def is_catalog_resource # rubocop:disable Naming/PredicateName + lazy_catalog_resource = BatchLoader::GraphQL.for(object.id).batch do |project_ids, loader| + ::Ci::Catalog::Resource.for_projects(project_ids).each do |catalog_resource| + loader.call(catalog_resource.project_id, catalog_resource) + end + end + + Gitlab::Graphql::Lazy.with_value(lazy_catalog_resource, &:present?) + end + def statistics Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(object.id).find end @@ -635,10 +687,8 @@ module Types project.container_repositories.size end - # Even if the parameter name is `sha`, it is actually a ref name. We always send `ref` to the endpoint. - # See: https://gitlab.com/gitlab-org/gitlab/-/issues/389065 - def ci_config_variables(sha:) - result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(sha) + def ci_config_variables(ref:) + result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(ref) return if result.nil? @@ -657,7 +707,7 @@ module Types if project.repository.empty? raise Gitlab::Graphql::Errors::MutationError, - _(format('You must %s before using Security features.', add_file_docs_link.html_safe)).html_safe + Gitlab::Utils::ErrorMessage.to_user_facing(_(format('You must %s before using Security features.', add_file_docs_link.html_safe)).html_safe) end ::Security::CiConfiguration::SastParserService.new(object).configuration @@ -681,6 +731,19 @@ module Types end end + def statistics_details_paths + root_ref = project.repository.root_ref || project.default_branch_or_main + + { + repository: Gitlab::Routing.url_helpers.project_tree_url(project, root_ref), + wiki: Gitlab::Routing.url_helpers.project_wikis_pages_url(project), + build_artifacts: Gitlab::Routing.url_helpers.project_artifacts_url(project), + packages: Gitlab::Routing.url_helpers.project_packages_url(project), + snippets: Gitlab::Routing.url_helpers.project_snippets_url(project), + container_registry: Gitlab::Routing.url_helpers.project_container_registry_index_url(project) + } + end + private def project diff --git a/app/graphql/types/projects/commit_parent_names_type.rb b/app/graphql/types/projects/commit_parent_names_type.rb new file mode 100644 index 00000000000..39f8f1cdd07 --- /dev/null +++ b/app/graphql/types/projects/commit_parent_names_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Projects + # rubocop: disable Graphql/AuthorizeTypes + class CommitParentNamesType < BaseObject + graphql_name 'CommitParentNames' + + field :names, [GraphQL::Types::String], null: true, description: 'Names of the commit parent (branch or tag).' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/projects/fork_details_type.rb b/app/graphql/types/projects/fork_details_type.rb index 88c17d89620..6157dc47255 100644 --- a/app/graphql/types/projects/fork_details_type.rb +++ b/app/graphql/types/projects/fork_details_type.rb @@ -9,11 +9,37 @@ module Types field :ahead, GraphQL::Types::Int, null: true, + calls_gitaly: true, + method: :ahead, description: 'Number of commits ahead of upstream.' field :behind, GraphQL::Types::Int, null: true, + calls_gitaly: true, + method: :behind, description: 'Number of commits behind upstream.' + + field :is_syncing, GraphQL::Types::Boolean, + null: true, + method: :syncing?, + description: 'Indicates if there is a synchronization in progress.' + + field :has_conflicts, GraphQL::Types::Boolean, + null: true, + method: :has_conflicts?, + description: 'Indicates if the fork conflicts with its upstream project.' + + def ahead + counts[:ahead] + end + + def behind + counts[:behind] + end + + def counts + @counts ||= object.counts + end end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/projects/namespace_project_sort_enum.rb b/app/graphql/types/projects/namespace_project_sort_enum.rb index 7c7b54226d3..14a315781ef 100644 --- a/app/graphql/types/projects/namespace_project_sort_enum.rb +++ b/app/graphql/types/projects/namespace_project_sort_enum.rb @@ -7,8 +7,9 @@ module Types description 'Values for sorting projects' value 'SIMILARITY', 'Most similar to the search query.', value: :similarity - value 'STORAGE', 'Sort by storage size.', value: :storage - value 'ACTIVITY_DESC', 'Sort by latest activity, in descending order.', value: :latest_activity_desc + value 'ACTIVITY_DESC', 'Sort by latest activity, descending order.', value: :latest_activity_desc end end end + +Types::Projects::NamespaceProjectSortEnum.prepend_mod diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index fb906759ba4..20dce54d740 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -14,6 +14,13 @@ module Types null: true, description: 'CI related settings that apply to the entire instance.' field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_MAX_COMPLEXITY / 2 + 1 + + field :ci_pipeline_stage, ::Types::Ci::StageType, + null: true, description: 'Stage belonging to a CI pipeline.' do + argument :id, type: ::Types::GlobalIDType[::Ci::Stage], + required: true, description: 'Global ID of the CI stage.' + end + field :ci_variables, Types::Ci::InstanceVariableType.connection_type, null: true, @@ -202,6 +209,15 @@ module Types def query_complexity context.query end + + def ci_pipeline_stage(id:) + stage = ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(id)) + authorized = Ability.allowed?(current_user, :read_build, stage.project) + + return unless authorized + + stage + end end end diff --git a/app/graphql/types/relative_position_type_enum.rb b/app/graphql/types/relative_position_type_enum.rb new file mode 100644 index 00000000000..e0d28bea648 --- /dev/null +++ b/app/graphql/types/relative_position_type_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class RelativePositionTypeEnum < BaseEnum + graphql_name 'RelativePositionType' + description 'The position to which the object should be moved' + + value 'BEFORE', 'Object is moved before an adjacent object.' + value 'AFTER', 'Object is moved after an adjacent object.' + end +end diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb index b2bc52c7745..c1669f67666 100644 --- a/app/graphql/types/release_asset_link_type.rb +++ b/app/graphql/types/release_asset_link_type.rb @@ -9,12 +9,6 @@ module Types present_using Releases::LinkPresenter - field :external, GraphQL::Types::Boolean, - null: true, - method: :external?, - description: 'Indicates the link points to an external resource.', - deprecated: { reason: 'No longer used', milestone: '15.9' } - field :id, GraphQL::Types::ID, null: false, description: 'ID of the link.' field :link_type, diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index ab5d1bd8c9e..40eade3a4d1 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -28,3 +28,5 @@ module Types description: 'Tree of the repository.' end end + +Types::RepositoryType.prepend_mod diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb index 64aaf3e73a0..67ee0589882 100644 --- a/app/graphql/types/root_storage_statistics_type.rb +++ b/app/graphql/types/root_storage_statistics_type.rb @@ -12,6 +12,7 @@ module Types field :lfs_objects_size, GraphQL::Types::Float, null: false, description: 'LFS objects size in bytes.' field :packages_size, GraphQL::Types::Float, null: false, description: 'Packages size in bytes.' field :pipeline_artifacts_size, GraphQL::Types::Float, null: false, description: 'CI pipeline artifacts size in bytes.' + field :registry_size_estimated, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the deduplicated Container Registry size for the namespace is an estimated value or not.' field :repository_size, GraphQL::Types::Float, null: false, description: 'Git repository size in bytes.' field :snippets_size, GraphQL::Types::Float, null: false, description: 'Snippets size in bytes.' field :storage_size, GraphQL::Types::Float, null: false, description: 'Total storage in bytes.' diff --git a/app/graphql/types/time_tracking/timelog_connection_type.rb b/app/graphql/types/time_tracking/timelog_connection_type.rb index 43e6955c2a3..6ea7a3478c7 100644 --- a/app/graphql/types/time_tracking/timelog_connection_type.rb +++ b/app/graphql/types/time_tracking/timelog_connection_type.rb @@ -5,7 +5,7 @@ module Types # rubocop: disable Graphql/AuthorizeTypes class TimelogConnectionType < CountableConnectionType field :total_spent_time, - GraphQL::Types::Int, + GraphQL::Types::BigInt, null: false, description: 'Total time spent in seconds.' diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb index 3a060518cd9..88baca028ef 100644 --- a/app/graphql/types/timelog_type.rb +++ b/app/graphql/types/timelog_type.rb @@ -49,6 +49,10 @@ module Types null: true, description: 'Summary of how the time was spent.' + field :project, Types::ProjectType, + null: false, + description: 'Target project of the timelog merge request or issue.' + def user Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.user_id).find end diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb index 9115b5a4760..b4950cc60e3 100644 --- a/app/graphql/types/user_interface.rb +++ b/app/graphql/types/user_interface.rb @@ -153,9 +153,18 @@ module Types field :profile_enable_gitpod_path, GraphQL::Types::String, null: true, description: 'Web path to enable Gitpod for the user.' + field :user_achievements, + Types::Achievements::UserAchievementType.connection_type, + null: true, + alpha: { milestone: '15.10' }, + description: "Achievements for the user. " \ + "Only returns for namespaces where the `achievements` feature flag is enabled.", + extras: [:lookahead], + resolver: ::Resolvers::Achievements::UserAchievementsResolver + definition_methods do def resolve_type(object, context) - # in the absense of other information, we cannot tell - just default to + # in the absence of other information, we cannot tell - just default to # the core user type. ::Types::UserType end @@ -166,3 +175,5 @@ module Types end end end + +Types::UserInterface.prepend_mod diff --git a/app/graphql/types/user_preferences_type.rb b/app/graphql/types/user_preferences_type.rb index 9a1ea4a2e4f..094c7352c96 100644 --- a/app/graphql/types/user_preferences_type.rb +++ b/app/graphql/types/user_preferences_type.rb @@ -10,6 +10,10 @@ module Types description: 'Sort order for issue lists.', null: true + field :visibility_pipeline_id_type, Types::VisibilityPipelineIdTypeEnum, + description: 'Determines whether the pipeline list shows ID or IID.', + null: true + def issues_sort object.issues_sort.to_sym end diff --git a/app/graphql/types/visibility_pipeline_id_type_enum.rb b/app/graphql/types/visibility_pipeline_id_type_enum.rb new file mode 100644 index 00000000000..8f0ae7d0c2f --- /dev/null +++ b/app/graphql/types/visibility_pipeline_id_type_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class VisibilityPipelineIdTypeEnum < BaseEnum + graphql_name 'VisibilityPipelineIdType' + description 'Determines whether the pipeline list shows ID or IID' + + UserPreference.visibility_pipeline_id_types.each_key do |field| + value field.upcase, value: field, description: "Display pipeline #{field.upcase}." + end + end +end diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index b46362f66b8..1e58781dbb9 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -27,7 +27,10 @@ module Types GraphQL::Types::Int, null: false, description: 'Lock version of the work item. Incremented each time the work item is updated.' - field :project, Types::ProjectType, null: false, + field :namespace, Types::NamespaceType, null: true, + description: 'Namespace the work item belongs to.', + alpha: { milestone: '15.10' } + field :project, Types::ProjectType, null: true, description: 'Project the work item belongs to.', alpha: { milestone: '15.3' } field :state, WorkItemStateEnum, null: false, @@ -36,6 +39,18 @@ module Types description: 'Title of the work item.' field :updated_at, Types::TimeType, null: false, description: 'Timestamp of when the work item was last updated.' + + field :create_note_email, GraphQL::Types::String, + null: true, + description: 'User specific email address for the work item.' + + field :reference, GraphQL::Types::String, null: false, + description: 'Internal reference of the work item. Returned in shortened format by default.', + method: :to_reference do + argument :full, GraphQL::Types::Boolean, required: false, default_value: false, + description: 'Boolean option specifying whether the reference should be returned in full.' + end + field :widgets, [Types::WorkItems::WidgetInterface], null: true, @@ -51,5 +66,9 @@ module Types def web_url Gitlab::UrlBuilder.build(object) end + + def create_note_email + object.creatable_note_email_address(context[:current_user]) + end end end diff --git a/app/graphql/types/work_items/available_export_fields_enum.rb b/app/graphql/types/work_items/available_export_fields_enum.rb new file mode 100644 index 00000000000..f5b26d9818d --- /dev/null +++ b/app/graphql/types/work_items/available_export_fields_enum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module WorkItems + class AvailableExportFieldsEnum < BaseEnum + graphql_name 'AvailableExportFields' + description 'Available fields to be exported as CSV' + + value 'ID', value: 'id', description: 'Unique identifier.' + value 'TITLE', value: 'title', description: 'Title.' + value 'DESCRIPTION', value: 'description', description: 'Description.' + value 'TYPE', value: 'type', description: 'Type of the work item.' + value 'AUTHOR', value: 'author', description: 'Author name.' + value 'AUTHOR_USERNAME', value: 'author username', description: 'Author username.' + value 'CREATED_AT', value: 'created_at', description: 'Date of creation.' + end + end +end diff --git a/app/graphql/types/work_items/award_emoji_update_action_enum.rb b/app/graphql/types/work_items/award_emoji_update_action_enum.rb new file mode 100644 index 00000000000..5b2512a215f --- /dev/null +++ b/app/graphql/types/work_items/award_emoji_update_action_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module WorkItems + class AwardEmojiUpdateActionEnum < BaseEnum + graphql_name 'WorkItemAwardEmojiUpdateAction' + description 'Values for work item award emoji update enum' + + value 'ADD', 'Adds the emoji.', value: :add + value 'REMOVE', 'Removes the emoji.', value: :remove + end + end +end diff --git a/app/graphql/types/work_items/todo_update_action_enum.rb b/app/graphql/types/work_items/todo_update_action_enum.rb new file mode 100644 index 00000000000..d9ce8f65396 --- /dev/null +++ b/app/graphql/types/work_items/todo_update_action_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module WorkItems + class TodoUpdateActionEnum < BaseEnum + graphql_name 'WorkItemTodoUpdateAction' + description 'Values for work item to-do update enum' + + value 'MARK_AS_DONE', 'Marks the to-do as done.', value: 'mark_as_done' + value 'ADD', 'Adds the to-do.', value: 'add' + end + end +end diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb index 672a78f12e1..53ea901ea10 100644 --- a/app/graphql/types/work_items/widget_interface.rb +++ b/app/graphql/types/work_items/widget_interface.rb @@ -18,7 +18,10 @@ module Types ::Types::WorkItems::Widgets::AssigneesType, ::Types::WorkItems::Widgets::StartAndDueDateType, ::Types::WorkItems::Widgets::MilestoneType, - ::Types::WorkItems::Widgets::NotesType + ::Types::WorkItems::Widgets::NotesType, + ::Types::WorkItems::Widgets::NotificationsType, + ::Types::WorkItems::Widgets::CurrentUserTodosType, + ::Types::WorkItems::Widgets::AwardEmojiType ].freeze def self.ce_orphan_types @@ -44,6 +47,12 @@ module Types ::Types::WorkItems::Widgets::MilestoneType when ::WorkItems::Widgets::Notes ::Types::WorkItems::Widgets::NotesType + when ::WorkItems::Widgets::Notifications + ::Types::WorkItems::Widgets::NotificationsType + when ::WorkItems::Widgets::CurrentUserTodos + ::Types::WorkItems::Widgets::CurrentUserTodosType + when ::WorkItems::Widgets::AwardEmoji + ::Types::WorkItems::Widgets::AwardEmojiType else raise "Unknown GraphQL type for widget #{object}" end diff --git a/app/graphql/types/work_items/widgets/award_emoji_type.rb b/app/graphql/types/work_items/widgets/award_emoji_type.rb new file mode 100644 index 00000000000..421bb8f0e98 --- /dev/null +++ b/app/graphql/types/work_items/widgets/award_emoji_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + # Disabling widget level authorization as it might be too granular + # and we already authorize the parent work item + # rubocop:disable Graphql/AuthorizeTypes + class AwardEmojiType < BaseObject + graphql_name 'WorkItemWidgetAwardEmoji' + description 'Represents the award emoji widget' + + implements Types::WorkItems::WidgetInterface + + field :award_emoji, + ::Types::AwardEmojis::AwardEmojiType.connection_type, + null: true, + description: 'Award emoji on the work item.' + field :downvotes, + GraphQL::Types::Int, + null: false, + description: 'Number of downvotes the work item has received.' + field :upvotes, + GraphQL::Types::Int, + null: false, + description: 'Number of upvotes the work item has received.' + + def downvotes + BatchLoaders::AwardEmojiVotesBatchLoader + .load_downvotes(object.work_item, awardable_class: 'Issue') + end + + def upvotes + BatchLoaders::AwardEmojiVotesBatchLoader + .load_upvotes(object.work_item, awardable_class: 'Issue') + end + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/graphql/types/work_items/widgets/award_emoji_update_input_type.rb b/app/graphql/types/work_items/widgets/award_emoji_update_input_type.rb new file mode 100644 index 00000000000..1d43d4913d2 --- /dev/null +++ b/app/graphql/types/work_items/widgets/award_emoji_update_input_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + class AwardEmojiUpdateInputType < BaseInputObject + graphql_name 'WorkItemWidgetAwardEmojiUpdateInput' + + argument :action, ::Types::WorkItems::AwardEmojiUpdateActionEnum, + required: true, + description: 'Action for the update.' + + argument :name, + GraphQL::Types::String, + required: true, + description: copy_field_description(Types::AwardEmojis::AwardEmojiType, :name) + end + end + end +end diff --git a/app/graphql/types/work_items/widgets/current_user_todos_input_type.rb b/app/graphql/types/work_items/widgets/current_user_todos_input_type.rb new file mode 100644 index 00000000000..630958def53 --- /dev/null +++ b/app/graphql/types/work_items/widgets/current_user_todos_input_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + class CurrentUserTodosInputType < BaseInputObject + graphql_name 'WorkItemWidgetCurrentUserTodosInput' + + argument :action, ::Types::WorkItems::TodoUpdateActionEnum, + required: true, + description: 'Action for the update.' + + argument :todo_id, + ::Types::GlobalIDType[::Todo], + required: false, + description: "Global ID of the to-do. If not present, all to-dos of the work item will be updated.", + prepare: ->(id, _) { id.model_id } + end + end + end +end diff --git a/app/graphql/types/work_items/widgets/current_user_todos_type.rb b/app/graphql/types/work_items/widgets/current_user_todos_type.rb new file mode 100644 index 00000000000..1c7cdd631e2 --- /dev/null +++ b/app/graphql/types/work_items/widgets/current_user_todos_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + # Disabling widget level authorization as it might be too granular + # and we already authorize the parent work item + # rubocop:disable Graphql/AuthorizeTypes + class CurrentUserTodosType < BaseObject + graphql_name 'WorkItemWidgetCurrentUserTodos' + description 'Represents a todos widget' + + implements Types::WorkItems::WidgetInterface + implements Types::CurrentUserTodos + + private + + # Overriden as `Types::CurrentUserTodos` relies on `unpresented` being the Issuable record. + def unpresented + object.work_item + end + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb b/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb index e1a9ebb76e9..297b06a8fab 100644 --- a/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb +++ b/app/graphql/types/work_items/widgets/hierarchy_update_input_type.rb @@ -6,16 +6,27 @@ module Types class HierarchyUpdateInputType < BaseInputObject graphql_name 'WorkItemWidgetHierarchyUpdateInput' - argument :parent_id, ::Types::GlobalIDType[::WorkItem], + argument :adjacent_work_item_id, + ::Types::GlobalIDType[::WorkItem], required: false, loads: ::Types::WorkItemType, - description: 'Global ID of the parent work item. Use `null` to remove the association.' + description: 'ID of the work item to be switched with.' argument :children_ids, [::Types::GlobalIDType[::WorkItem]], required: false, description: 'Global IDs of children work items.', loads: ::Types::WorkItemType, as: :children + + argument :parent_id, ::Types::GlobalIDType[::WorkItem], + required: false, + loads: ::Types::WorkItemType, + description: 'Global ID of the parent work item. Use `null` to remove the association.' + + argument :relative_position, + Types::RelativePositionTypeEnum, + required: false, + description: 'Type of switch. Valid values are `BEFORE` or `AFTER`.' end end end diff --git a/app/graphql/types/work_items/widgets/notifications_type.rb b/app/graphql/types/work_items/widgets/notifications_type.rb new file mode 100644 index 00000000000..85928817d07 --- /dev/null +++ b/app/graphql/types/work_items/widgets/notifications_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + # Disabling widget level authorization as it might be too granular + # and we already authorize the parent work item + # rubocop:disable Graphql/AuthorizeTypes + class NotificationsType < BaseObject + graphql_name 'WorkItemWidgetNotifications' + description 'Represents the notifications widget' + + implements Types::WorkItems::WidgetInterface + + field :subscribed, GraphQL::Types::Boolean, + null: false, + description: 'Whether the current user is subscribed to notifications on the work item.' + + def subscribed + object.work_item.subscribed?(current_user, object.work_item.project) + end + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/graphql/types/work_items/widgets/notifications_update_input_type.rb b/app/graphql/types/work_items/widgets/notifications_update_input_type.rb new file mode 100644 index 00000000000..2f3b46c3f45 --- /dev/null +++ b/app/graphql/types/work_items/widgets/notifications_update_input_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + class NotificationsUpdateInputType < BaseInputObject + graphql_name 'WorkItemWidgetNotificationsUpdateInput' + + argument :subscribed, + GraphQL::Types::Boolean, + required: true, + description: 'Desired state of the subscription.' + end + end + end +end |