diff options
Diffstat (limited to 'app/graphql')
93 files changed, 1074 insertions, 201 deletions
diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb index 7f83b62a2ff..89656f1e018 100644 --- a/app/graphql/graphql_triggers.rb +++ b/app/graphql/graphql_triggers.rb @@ -29,27 +29,33 @@ module GraphqlTriggers GitlabSchema.subscriptions.trigger('issuableMilestoneUpdated', { 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) + end + + def self.work_item_note_deleted(work_item_gid, note_data) + GitlabSchema.subscriptions.trigger('workItemNoteDeleted', { 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) + end + def self.merge_request_reviewers_updated(merge_request) GitlabSchema.subscriptions.trigger( - 'mergeRequestReviewersUpdated', - { issuable_id: merge_request.to_gid }, - merge_request + 'mergeRequestReviewersUpdated', { issuable_id: merge_request.to_gid }, merge_request ) end def self.merge_request_merge_status_updated(merge_request) GitlabSchema.subscriptions.trigger( - 'mergeRequestMergeStatusUpdated', - { issuable_id: merge_request.to_gid }, - merge_request + 'mergeRequestMergeStatusUpdated', { 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 + 'mergeRequestApprovalStateUpdated', { issuable_id: merge_request.to_gid }, merge_request ) end end diff --git a/app/graphql/mutations/achievements/create.rb b/app/graphql/mutations/achievements/create.rb index 6cfe6c0e643..310a653c705 100644 --- a/app/graphql/mutations/achievements/create.rb +++ b/app/graphql/mutations/achievements/create.rb @@ -28,10 +28,6 @@ module Mutations required: false, description: 'Description of or notes for the achievement.' - argument :revokeable, GraphQL::Types::Boolean, - required: true, - description: 'Revokeability for the achievement.' - authorize :admin_achievement def resolve(args) 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 e16c08cb116..6f0f87b47a1 100644 --- a/app/graphql/mutations/ci/job_token_scope/add_project.rb +++ b/app/graphql/mutations/ci/job_token_scope/add_project.rb @@ -18,18 +18,23 @@ module Mutations required: true, description: 'Project to be added to the CI job token scope.' + argument :direction, + ::Types::Ci::JobTokenScope::DirectionEnum, + required: false, + description: 'Direction of access, which defaults to outbound.' + field :ci_job_token_scope, - Types::Ci::JobTokenScopeType, - null: true, - description: "CI job token's scope of access." + Types::Ci::JobTokenScopeType, + null: true, + description: "CI job token's access scope." - def resolve(project_path:, target_project_path:) + def resolve(project_path:, target_project_path:, direction: :outbound) project = authorized_find!(project_path) target_project = Project.find_by_full_path(target_project_path) result = ::Ci::JobTokenScope::AddProjectService .new(project, current_user) - .execute(target_project) + .execute(target_project, direction: direction) if result.success? { diff --git a/app/graphql/mutations/ci/job_token_scope/remove_project.rb b/app/graphql/mutations/ci/job_token_scope/remove_project.rb index f503b4f2f7a..20e991f5388 100644 --- a/app/graphql/mutations/ci/job_token_scope/remove_project.rb +++ b/app/graphql/mutations/ci/job_token_scope/remove_project.rb @@ -18,18 +18,23 @@ module Mutations required: true, description: 'Project to be removed from the CI job token scope.' + argument :direction, + ::Types::Ci::JobTokenScope::DirectionEnum, + required: false, + description: 'Direction of access, which defaults to outbound.' + field :ci_job_token_scope, - Types::Ci::JobTokenScopeType, - null: true, - description: "CI job token's scope of access." + Types::Ci::JobTokenScopeType, + null: true, + description: "CI job token's scope of access." - def resolve(project_path:, target_project_path:) + def resolve(project_path:, target_project_path:, direction: :outbound) project = authorized_find!(project_path) target_project = Project.find_by_full_path(target_project_path) result = ::Ci::JobTokenScope::RemoveProjectService .new(project, current_user) - .execute(target_project) + .execute(target_project, direction) if result.success? { diff --git a/app/graphql/mutations/ci/pipeline_schedule/update.rb b/app/graphql/mutations/ci/pipeline_schedule/update.rb new file mode 100644 index 00000000000..a0b5e793ecb --- /dev/null +++ b/app/graphql/mutations/ci/pipeline_schedule/update.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module PipelineSchedule + class Update < Base + graphql_name 'PipelineScheduleUpdate' + + authorize :update_pipeline_schedule + + argument :description, GraphQL::Types::String, + required: false, + description: 'Description of the pipeline schedule.' + + argument :cron, GraphQL::Types::String, + required: false, + description: 'Cron expression of the pipeline schedule.' + + argument :cron_timezone, GraphQL::Types::String, + required: false, + description: + <<-STR + Cron time zone supported by ActiveSupport::TimeZone. + For example: "Pacific Time (US & Canada)" (default: "UTC"). + STR + + argument :ref, GraphQL::Types::String, + required: false, + description: 'Ref of the pipeline schedule.' + + argument :active, GraphQL::Types::Boolean, + required: false, + description: 'Indicates if the pipeline schedule should be active or not.' + + argument :variables, [Mutations::Ci::PipelineSchedule::VariableInputType], + required: false, + description: 'Variables for the pipeline schedule.' + + field :pipeline_schedule, + Types::Ci::PipelineScheduleType, + description: 'Updated pipeline schedule.' + + def resolve(id:, variables: [], **pipeline_schedule_attrs) + schedule = authorized_find!(id: id) + + params = pipeline_schedule_attrs.merge(variables_attributes: variables.map(&:to_h)) + + service_response = ::Ci::PipelineSchedules::UpdateService + .new(schedule, current_user, params) + .execute + + { + pipeline_schedule: schedule, + errors: service_response.errors + } + end + end + end + end +end 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 934d62e92cf..d214aa46cfc 100644 --- a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb +++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb @@ -27,6 +27,10 @@ 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, diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb index 86f37207a2d..189c926fcc4 100644 --- a/app/graphql/mutations/concerns/mutations/assignable.rb +++ b/app/graphql/mutations/concerns/mutations/assignable.rb @@ -33,7 +33,7 @@ module Mutations def assign!(resource, users, operation_mode) update_service_class.new( - project: resource.project, + **update_service_class.constructor_container_arg(resource.project), current_user: current_user, params: { assignee_ids: assignee_ids(resource, users, operation_mode) } ).execute(resource) diff --git a/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb index 508e1627032..3f32cd51ae7 100644 --- a/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb +++ b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb @@ -7,7 +7,7 @@ module Mutations def extract_widget_params!(work_item_type, attributes) # Get the list of widgets for the work item's type to extract only the supported attributes - widget_keys = ::WorkItems::Type.available_widgets.map(&:api_symbol) + widget_keys = ::WorkItems::WidgetDefinition.available_widgets.map(&:api_symbol) widget_params = attributes.extract!(*widget_keys) not_supported_keys = widget_params.keys - work_item_type.widgets.map(&:api_symbol) diff --git a/app/graphql/mutations/issues/bulk_update.rb b/app/graphql/mutations/issues/bulk_update.rb new file mode 100644 index 00000000000..7f3d5f6ffb2 --- /dev/null +++ b/app/graphql/mutations/issues/bulk_update.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class BulkUpdate < BaseMutation + graphql_name 'IssuesBulkUpdate' + + include Gitlab::Graphql::Authorize::AuthorizeResource + + MAX_ISSUES = 100 + + description 'Allows updating several properties for a set of issues. ' \ + 'Does nothing if the `bulk_update_issues_mutation` feature flag is disabled.' + + argument :parent_id, ::Types::GlobalIDType[::IssueParent], + required: true, + description: 'Global ID of the parent that the bulk update will be scoped to . ' \ + 'Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`.' + + argument :ids, [::Types::GlobalIDType[::Issue]], + required: true, + description: 'Global ID array of the issues that will be updated. ' \ + "IDs that the user can\'t update will be ignored. A max of #{MAX_ISSUES} can be provided." + + argument :assignee_ids, [::Types::GlobalIDType[::User]], + required: false, + description: 'Global ID array of the users that will be assigned to the given issues. ' \ + 'Existing assignees will be replaced with the ones on this list.' + + argument :milestone_id, ::Types::GlobalIDType[::Milestone], + required: false, + description: 'Global ID of the milestone that will be assigned to the issues.' + + field :updated_issue_count, GraphQL::Types::Int, + null: true, + description: 'Number of issues that were successfully updated.' + + def ready?(**args) + if Feature.disabled?(:bulk_update_issues_mutation) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, '`bulk_update_issues_mutation` feature flag is disabled.' + end + + if args[:ids].size > MAX_ISSUES + raise Gitlab::Graphql::Errors::ArgumentError, + format(_('No more than %{max_issues} issues can be updated at the same time'), max_issues: MAX_ISSUES) + end + + super + end + + def resolve(ids:, parent_id:, **attributes) + parent = find_parent!(parent_id) + + result = Issuable::BulkUpdateService.new( + parent, + current_user, + prepared_params(attributes, ids) + ).execute('issue') + + if result.success? + { updated_issue_count: result.payload[:count], errors: [] } + else + { errors: result.errors } + end + end + + private + + def find_parent!(parent_id) + parent = GitlabSchema.find_by_gid(parent_id).sync + raise_resource_not_available_error! unless current_user.can?("read_#{parent.to_ability_name}", parent) + + parent + end + + def prepared_params(attributes, ids) + prepared = { issuable_ids: model_ids_from(ids).uniq } + + global_id_arguments.each do |argument| + next unless attributes.key?(argument) + + prepared[argument] = model_ids_from(attributes[argument]) + end + + prepared.transform_keys(param_mappings) + end + + def param_mappings + {} + end + + def global_id_arguments + %i[assignee_ids milestone_id] + end + + def model_ids_from(attributes) + return if attributes.nil? + return attributes.map(&:model_id) if attributes.is_a?(Array) + + attributes.model_id + end + end + end +end + +Mutations::Issues::BulkUpdate.prepend_mod diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb index 0389a482822..0c1acdf316e 100644 --- a/app/graphql/mutations/issues/create.rb +++ b/app/graphql/mutations/issues/create.rb @@ -83,7 +83,7 @@ module Mutations params = build_create_issue_params(attributes.merge(author_id: current_user.id), project) spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) - result = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute + result = ::Issues::CreateService.new(container: project, current_user: current_user, params: params, spam_params: spam_params).execute check_spam_action_response!(result[:issue]) if result[:issue] diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb index 63bc9dabbf9..ef3f70c78b9 100644 --- a/app/graphql/mutations/issues/move.rb +++ b/app/graphql/mutations/issues/move.rb @@ -18,7 +18,7 @@ module Mutations target_project = resolve_project(full_path: target_project_path).sync begin - moved_issue = ::Issues::MoveService.new(project: source_project, current_user: current_user).execute(issue, target_project) + moved_issue = ::Issues::MoveService.new(container: source_project, current_user: current_user).execute(issue, target_project) rescue ::Issues::MoveService::MoveError => e errors = e.message end diff --git a/app/graphql/mutations/issues/set_confidential.rb b/app/graphql/mutations/issues/set_confidential.rb index b795d66c16f..08578881a13 100644 --- a/app/graphql/mutations/issues/set_confidential.rb +++ b/app/graphql/mutations/issues/set_confidential.rb @@ -19,7 +19,7 @@ module Mutations # spam_params so a check can be performed. spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) - ::Issues::UpdateService.new(project: project, current_user: current_user, params: { confidential: confidential }, spam_params: spam_params) + ::Issues::UpdateService.new(container: project, current_user: current_user, params: { confidential: confidential }, spam_params: spam_params) .execute(issue) check_spam_action_response!(issue) diff --git a/app/graphql/mutations/issues/set_due_date.rb b/app/graphql/mutations/issues/set_due_date.rb index 70b76da4fcb..e361d241083 100644 --- a/app/graphql/mutations/issues/set_due_date.rb +++ b/app/graphql/mutations/issues/set_due_date.rb @@ -14,7 +14,7 @@ module Mutations issue = authorized_find!(project_path: project_path, iid: iid) project = issue.project - ::Issues::UpdateService.new(project: project, current_user: current_user, params: { due_date: due_date }) + ::Issues::UpdateService.new(container: project, current_user: current_user, params: { due_date: due_date }) .execute(issue) { diff --git a/app/graphql/mutations/issues/set_escalation_status.rb b/app/graphql/mutations/issues/set_escalation_status.rb index 4f3fcb4886d..13286034ada 100644 --- a/app/graphql/mutations/issues/set_escalation_status.rb +++ b/app/graphql/mutations/issues/set_escalation_status.rb @@ -17,7 +17,7 @@ module Mutations check_feature_availability!(issue) ::Issues::UpdateService.new( - project: project, + container: project, current_user: current_user, params: { escalation_status: { status: status } } ).execute(issue) diff --git a/app/graphql/mutations/issues/set_locked.rb b/app/graphql/mutations/issues/set_locked.rb index 93b31350bbf..86ad129f4cb 100644 --- a/app/graphql/mutations/issues/set_locked.rb +++ b/app/graphql/mutations/issues/set_locked.rb @@ -13,7 +13,7 @@ module Mutations def resolve(project_path:, iid:, locked:) issue = authorized_find!(project_path: project_path, iid: iid) - ::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: { discussion_locked: locked }) + ::Issues::UpdateService.new(container: issue.project, current_user: current_user, params: { discussion_locked: locked }) .execute(issue) { diff --git a/app/graphql/mutations/issues/set_severity.rb b/app/graphql/mutations/issues/set_severity.rb index 4a24bfd18ef..68d7fb7d0c0 100644 --- a/app/graphql/mutations/issues/set_severity.rb +++ b/app/graphql/mutations/issues/set_severity.rb @@ -15,7 +15,7 @@ module Mutations issue = authorized_find!(project_path: project_path, iid: iid) project = issue.project - ::Issues::UpdateService.new(project: project, current_user: current_user, params: { severity: severity }) + ::Issues::UpdateService.new(container: project, current_user: current_user, params: { severity: severity }) .execute(issue) { diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb index 6cab1214d24..b5af048dc07 100644 --- a/app/graphql/mutations/issues/update.rb +++ b/app/graphql/mutations/issues/update.rb @@ -31,6 +31,10 @@ module Mutations description: 'Close or reopen an issue.', required: false + argument :time_estimate, GraphQL::Types::String, + required: false, + description: 'Estimated time to complete the issue, or `0` to remove the current estimate.' + def resolve(project_path:, iid:, **args) issue = authorized_find!(project_path: project_path, iid: iid) project = issue.project @@ -38,7 +42,7 @@ module Mutations args = parse_arguments(args) spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) - ::Issues::UpdateService.new(project: project, current_user: current_user, params: args, spam_params: spam_params).execute(issue) + ::Issues::UpdateService.new(container: project, current_user: current_user, params: args, spam_params: spam_params).execute(issue) { issue: issue, @@ -46,11 +50,15 @@ module Mutations } end - def ready?(label_ids: [], add_label_ids: [], remove_label_ids: [], **args) + def ready?(label_ids: [], add_label_ids: [], remove_label_ids: [], time_estimate: nil, **args) if label_ids.any? && (add_label_ids.any? || remove_label_ids.any?) raise Gitlab::Graphql::Errors::ArgumentError, 'labelIds is mutually exclusive with any of addLabelIds or removeLabelIds' end + if !time_estimate.nil? && Gitlab::TimeTrackingFormatter.parse(time_estimate, keep_zero: true).nil? + raise Gitlab::Graphql::Errors::ArgumentError, 'timeEstimate must be formatted correctly, for example `1h 30m`' + end + super end @@ -61,6 +69,10 @@ module Mutations args[:remove_label_ids] = parse_label_ids(args[:remove_label_ids]) args[:label_ids] = parse_label_ids(args[:label_ids]) + unless args[:time_estimate].nil? + args[:time_estimate] = Gitlab::TimeTrackingFormatter.parse(args[:time_estimate], keep_zero: true) + end + args end diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb index bf40c12aec5..320aa423ce3 100644 --- a/app/graphql/mutations/merge_requests/set_milestone.rb +++ b/app/graphql/mutations/merge_requests/set_milestone.rb @@ -17,7 +17,7 @@ module Mutations merge_request = authorized_find!(project_path: project_path, iid: iid) project = merge_request.project - ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { milestone: milestone }) + ::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { milestone_id: milestone&.id }) .execute(merge_request) { diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb index 0f4923e15a1..da4db7342a3 100644 --- a/app/graphql/mutations/merge_requests/update.rb +++ b/app/graphql/mutations/merge_requests/update.rb @@ -24,12 +24,16 @@ module Mutations as: :state_event, description: 'Action to perform to change the state.' + argument :time_estimate, GraphQL::Types::String, + required: false, + description: 'Estimated time to complete the merge request, or `0` to remove the current estimate.' + def resolve(project_path:, iid:, **args) merge_request = authorized_find!(project_path: project_path, iid: iid) - attributes = args.compact + args = parse_arguments(args) ::MergeRequests::UpdateService - .new(project: merge_request.project, current_user: current_user, params: attributes) + .new(project: merge_request.project, current_user: current_user, params: args) .execute(merge_request) errors = errors_on_object(merge_request) @@ -39,6 +43,25 @@ module Mutations errors: errors } end + + def ready?(time_estimate: nil, **args) + if !time_estimate.nil? && Gitlab::TimeTrackingFormatter.parse(time_estimate, keep_zero: true).nil? + raise Gitlab::Graphql::Errors::ArgumentError, + 'timeEstimate must be formatted correctly, for example `1h 30m`' + end + + super + end + + private + + def parse_arguments(args) + unless args[:time_estimate].nil? + args[:time_estimate] = Gitlab::TimeTrackingFormatter.parse(args[:time_estimate], keep_zero: true) + end + + args.compact + end end end end diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb index a4efffb69c1..9f124de7ab2 100644 --- a/app/graphql/mutations/work_items/create.rb +++ b/app/graphql/mutations/work_items/create.rb @@ -48,7 +48,7 @@ module Mutations widget_params = extract_widget_params!(type, params) create_result = ::WorkItems::CreateService.new( - project: project, + container: project, current_user: current_user, params: params, spam_params: spam_params, diff --git a/app/graphql/mutations/work_items/delete.rb b/app/graphql/mutations/work_items/delete.rb index 4b0067d40d4..ec0244fa65e 100644 --- a/app/graphql/mutations/work_items/delete.rb +++ b/app/graphql/mutations/work_items/delete.rb @@ -20,7 +20,7 @@ module Mutations work_item = authorized_find!(id: id) result = ::WorkItems::DeleteService.new( - project: work_item.project, + container: work_item.project, current_user: current_user ).execute(work_item) diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb index 04c63d8e876..db6af38d82e 100644 --- a/app/graphql/mutations/work_items/update.rb +++ b/app/graphql/mutations/work_items/update.rb @@ -22,8 +22,10 @@ module Mutations spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) widget_params = extract_widget_params!(work_item.work_item_type, attributes) + interpret_quick_actions!(work_item, current_user, widget_params, attributes) + update_result = ::WorkItems::UpdateService.new( - project: work_item.project, + container: work_item.project, current_user: current_user, params: attributes, widget_params: widget_params, @@ -43,6 +45,37 @@ module Mutations 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) + + description_param = widget_params[::WorkItems::Widgets::Description.api_symbol] + return unless description_param + + original_description = description_param.fetch(:description, work_item.description) + + description, command_params = QuickActions::InterpretService + .new(work_item.project, current_user, {}) + .execute(original_description, work_item) + + 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 || {}) + end end end end diff --git a/app/graphql/mutations/work_items/update_task.rb b/app/graphql/mutations/work_items/update_task.rb index aeb4f1d0f06..8dcc4c325ea 100644 --- a/app/graphql/mutations/work_items/update_task.rb +++ b/app/graphql/mutations/work_items/update_task.rb @@ -32,7 +32,7 @@ module Mutations spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) ::WorkItems::UpdateService.new( - project: task.project, + container: task.project, current_user: current_user, params: task_data_hash.except(:id), spam_params: spam_params diff --git a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql index c8353c738a5..01c312c567f 100644 --- a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql +++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql @@ -34,6 +34,7 @@ fragment LinkedPipelineData on Pipeline { __typename id name + retried } project { __typename diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb index d70acdf7ca0..03b12dbb12e 100644 --- a/app/graphql/resolvers/board_list_issues_resolver.rb +++ b/app/graphql/resolvers/board_list_issues_resolver.rb @@ -13,7 +13,7 @@ module Resolvers alias_method :list, :object def resolve(**args) - filters = item_filters(args[:filters]) + filters = item_filters(args[:filters], list.board.resource_parent) mutually_exclusive_milestone_args!(filters) filter_params = filters.merge(board_id: list.board.id, id: list.id) diff --git a/app/graphql/resolvers/board_list_resolver.rb b/app/graphql/resolvers/board_list_resolver.rb index d853846b674..f9d3541cd5f 100644 --- a/app/graphql/resolvers/board_list_resolver.rb +++ b/app/graphql/resolvers/board_list_resolver.rb @@ -19,9 +19,8 @@ module Resolvers description: 'Filters applied when getting issue metadata in the board list.' def resolve(id: nil, issue_filters: {}) - context.scoped_set!(:issue_filters, item_filters(issue_filters)) - Gitlab::Graphql::Lazy.with_value(find_list(id: id)) do |list| + context.scoped_set!(:issue_filters, item_filters(issue_filters, list&.board&.resource_parent)) list if authorized_resource?(list) end end diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb index 4dae3b4a9d1..5fc813f4acb 100644 --- a/app/graphql/resolvers/board_lists_resolver.rb +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -22,7 +22,7 @@ module Resolvers def resolve_with_lookahead(id: nil, issue_filters: {}) lists = board_lists(id) - context.scoped_set!(:issue_filters, item_filters(issue_filters)) + context.scoped_set!(:issue_filters, item_filters(issue_filters, board.resource_parent)) List.preload_preferences_for_user(lists, current_user) if load_preferences? diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb index b818be3f018..467a3525867 100644 --- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb +++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb @@ -30,6 +30,7 @@ module Resolvers previous_stage_jobs_or_needs: [:needs, :pipeline], artifacts: [:job_artifacts], pipeline: [:user], + project: [{ project: [:route, { namespace: [:route] }] }], detailed_status: [ :metadata, { pipeline: [:merge_request] }, diff --git a/app/graphql/resolvers/ci/variables_resolver.rb b/app/graphql/resolvers/ci/variables_resolver.rb new file mode 100644 index 00000000000..71d420120b3 --- /dev/null +++ b/app/graphql/resolvers/ci/variables_resolver.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class VariablesResolver < BaseResolver + type Types::Ci::InstanceVariableType.connection_type, null: true + + argument :sort, ::Types::Ci::VariableSortEnum, + required: false, + description: 'Sort order of results.' + + def resolve(**args) + if parent.is_a?(Group) || parent.is_a?(Project) + parent.variables.order_by(args[:sort]) + elsif current_user&.can_admin_all_resources? + ::Ci::InstanceVariable.order_by(args[:sort]) + end + end + + private + + def parent + object.respond_to?(:sync) ? object.sync : object + end + end + end +end diff --git a/app/graphql/resolvers/concerns/board_item_filterable.rb b/app/graphql/resolvers/concerns/board_item_filterable.rb index 035cdbbd282..dcd2f265573 100644 --- a/app/graphql/resolvers/concerns/board_item_filterable.rb +++ b/app/graphql/resolvers/concerns/board_item_filterable.rb @@ -5,7 +5,7 @@ module BoardItemFilterable private - def item_filters(args) + def item_filters(args, resource_parent) filters = args.to_h set_filter_values(filters) @@ -45,10 +45,6 @@ module BoardItemFilterable def rewrite_param_name(filters, old_name, new_name) filters[new_name] = filters.delete(old_name) if filters[old_name].present? end - - def resource_parent - respond_to?(:board) ? board.resource_parent : list.board.resource_parent - end end ::BoardItemFilterable.prepend_mod_with('Resolvers::BoardItemFilterable') diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb index 1268e74fd58..86dda5cb1cb 100644 --- a/app/graphql/resolvers/concerns/resolves_groups.rb +++ b/app/graphql/resolvers/concerns/resolves_groups.rb @@ -5,8 +5,8 @@ module ResolvesGroups extend ActiveSupport::Concern include LooksAhead - def resolve_with_lookahead(**args) - apply_lookahead(resolve_groups(**args)) + def resolve_with_lookahead(...) + apply_lookahead(resolve_groups(...)) end private diff --git a/app/graphql/resolvers/concerns/search_arguments.rb b/app/graphql/resolvers/concerns/search_arguments.rb index cc1a13fdf29..ac5b7beb5ef 100644 --- a/app/graphql/resolvers/concerns/search_arguments.rb +++ b/app/graphql/resolvers/concerns/search_arguments.rb @@ -17,7 +17,6 @@ module SearchArguments def ready?(**args) validate_search_in_params!(args) - validate_anonymous_search_access!(args) validate_search_rate_limit!(args) super @@ -25,14 +24,6 @@ module SearchArguments private - def validate_anonymous_search_access!(args) - return unless args[:search].present? - return if current_user.present? || Feature.disabled?(:disable_anonymous_search, type: :ops) - - raise ::Gitlab::Graphql::Errors::ArgumentError, - "User must be authenticated to include the `search` argument." - end - def validate_search_in_params!(args) return unless args[:in].present? && args[:search].blank? @@ -41,7 +32,7 @@ module SearchArguments end def validate_search_rate_limit!(args) - return if args[:search].blank? || context[:request].nil? || Feature.disabled?(:rate_limit_issuable_searches) + return if args[:search].blank? || context[:request].nil? if current_user.present? rate_limiter_key = :search_rate_limit diff --git a/app/graphql/resolvers/data_transfer_resolver.rb b/app/graphql/resolvers/data_transfer_resolver.rb new file mode 100644 index 00000000000..1a240d2811f --- /dev/null +++ b/app/graphql/resolvers/data_transfer_resolver.rb @@ -0,0 +1,57 @@ +# 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/group_releases_resolver.rb b/app/graphql/resolvers/group_releases_resolver.rb new file mode 100644 index 00000000000..115289e1fca --- /dev/null +++ b/app/graphql/resolvers/group_releases_resolver.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Resolvers + class GroupReleasesResolver < BaseResolver + type Types::ReleaseType.connection_type, null: true + + argument :sort, Types::GroupReleaseSortEnum, + required: false, default_value: :released_at_desc, + description: 'Sort group releases by given criteria.' + + alias_method :group, :object + + # GroupReleasesFinder only supports sorting by `released_at` + SORT_TO_PARAMS_MAP = { + released_at_desc: { sort: 'desc' }, + released_at_asc: { sort: 'asc' } + }.freeze + + def resolve(sort:) + releases = Releases::GroupReleasesFinder.new( + group, + current_user, + SORT_TO_PARAMS_MAP[sort] + ).execute + # fix ordering problem with GroupReleasesFinder and keyset pagination + # See more on https://gitlab.com/gitlab-org/gitlab/-/issues/378160 + offset_pagination(releases) + end + end +end diff --git a/app/graphql/resolvers/groups_resolver.rb b/app/graphql/resolvers/groups_resolver.rb index 6cfdba240f0..902b5279364 100644 --- a/app/graphql/resolvers/groups_resolver.rb +++ b/app/graphql/resolvers/groups_resolver.rb @@ -4,31 +4,18 @@ module Resolvers class GroupsResolver < BaseResolver include ResolvesGroups - type Types::GroupType, null: true - - argument :include_parent_descendants, GraphQL::Types::Boolean, - required: false, - description: 'List of descendant groups of the parent group.', - default_value: true - - argument :owned, GraphQL::Types::Boolean, - required: false, - description: 'Limit result to groups owned by authenticated user.' + type "Types::GroupConnection", null: true argument :search, GraphQL::Types::String, required: false, description: 'Search query for group name or group full path.' - alias_method :parent, :object - private # rubocop: disable CodeReuse/ActiveRecord - def resolve_groups(args) - return Group.none unless parent.present? - + def resolve_groups(**args) GroupsFinder - .new(context[:current_user], args.merge(parent: parent)) + .new(context[:current_user], args) .execute .reorder(name: :asc) end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 24009bf7e18..bbf45efa33e 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -17,6 +17,7 @@ module Resolvers before_connection_authorization do |nodes, current_user| projects = nodes.map(&:project) ::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute + ::Preloaders::GroupPolicyPreloader.new(projects.filter_map(&:group), current_user).execute end def ready?(**args) @@ -28,8 +29,6 @@ module Resolvers end def resolve_with_lookahead(**args) - return unless Feature.enabled?(:root_level_issues_query) - issues = apply_lookahead( IssuesFinder.new(current_user, prepare_finder_params(args)).execute ) diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb index c3c61d31e8d..726e78f9971 100644 --- a/app/graphql/resolvers/namespace_projects_resolver.rb +++ b/app/graphql/resolvers/namespace_projects_resolver.rb @@ -8,9 +8,9 @@ module Resolvers description: 'Include also subgroup projects.' argument :search, GraphQL::Types::String, - required: false, - default_value: nil, - description: 'Search project with most similar names or paths.' + required: false, + default_value: nil, + description: 'Search project with most similar names or paths.' argument :sort, Types::Projects::NamespaceProjectSortEnum, required: false, @@ -22,6 +22,14 @@ module Resolvers default_value: nil, description: 'Filter projects by IDs.' + argument :with_issues_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with issues enabled." + + argument :with_merge_requests_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with merge requests enabled." + type Types::ProjectType, null: true def resolve(args) @@ -54,7 +62,9 @@ module Resolvers include_subgroups: args.dig(:include_subgroups), sort: args.dig(:sort), search: args.dig(:search), - ids: parse_gids(args.dig(:ids)) + ids: parse_gids(args.dig(:ids)), + with_issues_enabled: args[:with_issues_enabled], + with_merge_requests_enabled: args[:with_merge_requests_enabled] } end diff --git a/app/graphql/resolvers/nested_groups_resolver.rb b/app/graphql/resolvers/nested_groups_resolver.rb new file mode 100644 index 00000000000..a2869b50cbb --- /dev/null +++ b/app/graphql/resolvers/nested_groups_resolver.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Resolvers + class NestedGroupsResolver < BaseResolver + include ResolvesGroups + + type Types::GroupType, null: true + + argument :include_parent_descendants, GraphQL::Types::Boolean, + required: false, + description: 'List of descendant groups of the parent group.', + default_value: true + + argument :owned, GraphQL::Types::Boolean, + required: false, + description: 'Limit result to groups owned by authenticated user.' + + argument :search, GraphQL::Types::String, + required: false, + description: 'Search query for group name or group full path.' + + alias_method :parent, :object + + private + + # rubocop: disable CodeReuse/ActiveRecord + def resolve_groups(args) + return Group.none unless parent.present? + + GroupsFinder + .new(context[:current_user], args.merge(parent: parent)) + .execute + .reorder(name: :asc) + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/graphql/resolvers/notes/synthetic_note_resolver.rb b/app/graphql/resolvers/notes/synthetic_note_resolver.rb new file mode 100644 index 00000000000..d4eafcd2c49 --- /dev/null +++ b/app/graphql/resolvers/notes/synthetic_note_resolver.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Resolvers + module Notes + class SyntheticNoteResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorize :read_note + + type Types::Notes::NoteType, null: true + + argument :sha, GraphQL::Types::String, + required: true, + description: 'Global ID of the note.' + + argument :noteable_id, ::Types::GlobalIDType[::Noteable], + required: true, + description: 'Global ID of the resource to search synthetic note on.' + + def resolve(noteable_id:, sha:) + noteable = authorized_find!(id: noteable_id) + + synthetic_notes = ResourceEvents::MergeIntoNotesService.new( + noteable, current_user, paginated_notes: nil + ).execute + + 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/project_pipeline_schedules_resolver.rb b/app/graphql/resolvers/project_pipeline_schedules_resolver.rb index eb980f72717..32887385d26 100644 --- a/app/graphql/resolvers/project_pipeline_schedules_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_schedules_resolver.rb @@ -10,8 +10,13 @@ module Resolvers required: false, description: 'Filter pipeline schedules by active status.' - def resolve(status: nil) - ::Ci::PipelineSchedulesFinder.new(project).execute(scope: status) + argument :ids, [GraphQL::Types::ID], + required: false, + default_value: nil, + description: 'Filter pipeline schedules by IDs.' + + def resolve(status: nil, ids: nil) + ::Ci::PipelineSchedulesFinder.new(project).execute(scope: status, ids: ids) end end end diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb index 99de4df945c..3eca0dfd83f 100644 --- a/app/graphql/resolvers/projects/services_resolver.rb +++ b/app/graphql/resolvers/projects/services_resolver.rb @@ -2,6 +2,7 @@ module Resolvers module Projects + # TODO: Remove in 17.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108418 class ServicesResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb index 0bdba53c7af..08981f2c441 100644 --- a/app/graphql/resolvers/projects_resolver.rb +++ b/app/graphql/resolvers/projects_resolver.rb @@ -15,14 +15,30 @@ module Resolvers description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \ "for example: `id_desc` or `name_asc`" + argument :with_issues_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with issues enabled." + + argument :with_merge_requests_enabled, GraphQL::Types::Boolean, + required: false, + description: "Return only projects with merge requests enabled." + def resolve(**args) ProjectsFinder - .new(current_user: current_user, params: project_finder_params(args), project_ids_relation: parse_gids(args[:ids])) + .new(current_user: current_user, params: finder_params(args), project_ids_relation: parse_gids(args[:ids])) .execute end private + def finder_params(args) + { + **project_finder_params(args), + with_issues_enabled: args[:with_issues_enabled], + with_merge_requests_enabled: args[:with_merge_requests_enabled] + } + end + def parse_gids(gids) gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id } end diff --git a/app/graphql/resolvers/releases_resolver.rb b/app/graphql/resolvers/releases_resolver.rb index 358f3c33836..06f4ca2065c 100644 --- a/app/graphql/resolvers/releases_resolver.rb +++ b/app/graphql/resolvers/releases_resolver.rb @@ -6,7 +6,7 @@ module Resolvers argument :sort, Types::ReleaseSortEnum, required: false, default_value: :released_at_desc, - description: 'Sort releases by this criteria.' + description: 'Sort releases by given criteria.' alias_method :project, :object diff --git a/app/graphql/resolvers/saved_reply_resolver.rb b/app/graphql/resolvers/saved_reply_resolver.rb new file mode 100644 index 00000000000..96bbc139c96 --- /dev/null +++ b/app/graphql/resolvers/saved_reply_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + class SavedReplyResolver < BaseResolver + type Types::SavedReplyType, null: true + + alias_method :target, :object + + argument :id, Types::GlobalIDType[::Users::SavedReply], + required: true, + description: 'ID of a saved reply.' + + def resolve(id:) + return unless Feature.enabled?(:saved_replies, current_user) + + saved_reply = ::Users::SavedReply.find_saved_reply(user_id: current_user.id, id: id.model_id) + + return unless saved_reply + + saved_reply + end + end +end diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb index 83ed8c37250..0c9aac80274 100644 --- a/app/graphql/resolvers/work_items_resolver.rb +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -7,6 +7,10 @@ module Resolvers type Types::WorkItemType.connection_type, null: true + 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".' @@ -39,14 +43,19 @@ module Resolvers { work_item_type: :work_item_type, web_url: { project: { namespace: :route } }, - widgets: :work_item_type + widgets: { work_item_type: :enabled_widget_definitions } } end def nested_preloads { widgets: widget_preloads, - user_permissions: { update_work_item: :assignees } + user_permissions: { update_work_item: :assignees }, + project: { jira_import_status: { project: :jira_imports } }, + author: { + location: { author: :user_detail }, + gitpod_enabled: { author: :user_preference } + } } end @@ -55,9 +64,9 @@ module Resolvers last_edited_by: :last_edited_by, assignees: :assignees, parent: :work_item_parent, - children: { work_item_children_by_created_at: [:author, { project: :project_feature }] }, + children: { work_item_children_by_relative_position: [:author, { project: :project_feature }] }, labels: :labels, - milestone: :milestone + milestone: { milestone: [:project, :group] } } end diff --git a/app/graphql/subscriptions/notes/base.rb b/app/graphql/subscriptions/notes/base.rb new file mode 100644 index 00000000000..3653c01e0e2 --- /dev/null +++ b/app/graphql/subscriptions/notes/base.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Subscriptions + module Notes + class Base < ::Subscriptions::BaseSubscription + include Gitlab::Graphql::Laziness + + argument :noteable_id, ::Types::GlobalIDType[::Noteable], + required: false, + description: 'ID of the noteable.' + + def subscribe(*args) + nil + end + + def authorized?(noteable_id:) + noteable = force(GitlabSchema.find_by_gid(noteable_id)) + + # unsubscribe if user cannot read the noteable anymore for any reason, e.g. issue was set confidential, + # in the meantime the read note permissions is checked within its corresponding returned type, i.e. NoteType + unauthorized! unless noteable && Ability.allowed?(current_user, :"read_#{noteable.to_ability_name}", noteable) + + true + end + end + end +end diff --git a/app/graphql/subscriptions/notes/created.rb b/app/graphql/subscriptions/notes/created.rb new file mode 100644 index 00000000000..07b7b308163 --- /dev/null +++ b/app/graphql/subscriptions/notes/created.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Subscriptions + module Notes + class Created < Base + payload_type ::Types::Notes::NoteType + + def update(*args) + case object + when ResourceEvent + object.work_item_synthetic_system_note + when Array + object.first.work_item_synthetic_system_note(events: object) + else + object + end + end + end + end +end diff --git a/app/graphql/subscriptions/notes/deleted.rb b/app/graphql/subscriptions/notes/deleted.rb new file mode 100644 index 00000000000..d931ef00d0d --- /dev/null +++ b/app/graphql/subscriptions/notes/deleted.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Subscriptions + module Notes + class Deleted < Base + payload_type ::Types::Notes::DeletedNoteType + + DeletedNote = Struct.new(:model_id, :model_name, :discussion_model_id, :last_discussion_note) do + def to_global_id + ::Gitlab::GlobalId.as_global_id(model_id, model_name: model_name) + end + + def discussion_id + ::Gitlab::GlobalId.as_global_id(discussion_model_id, model_name: Discussion.name) + end + end + + def update(*args) + DeletedNote.new(object[:id], object[:model_name], object[:discussion_id], object[:last_discussion_note]) + end + end + end +end diff --git a/app/graphql/subscriptions/notes/updated.rb b/app/graphql/subscriptions/notes/updated.rb new file mode 100644 index 00000000000..a4748a3361e --- /dev/null +++ b/app/graphql/subscriptions/notes/updated.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Subscriptions + module Notes + class Updated < Base + payload_type Types::Notes::NoteType + end + end +end diff --git a/app/graphql/types/achievements/achievement_type.rb b/app/graphql/types/achievements/achievement_type.rb index e2b9495c83d..67cc9778797 100644 --- a/app/graphql/types/achievements/achievement_type.rb +++ b/app/graphql/types/achievements/achievement_type.rb @@ -32,11 +32,6 @@ module Types null: true, description: 'Description or notes for the achievement.' - field :revokeable, - GraphQL::Types::Boolean, - null: false, - description: 'Revokeability of the achievement.' - field :created_at, Types::TimeType, null: false, diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb index 4086015dad6..d2bc1d55408 100644 --- a/app/graphql/types/base_argument.rb +++ b/app/graphql/types/base_argument.rb @@ -2,7 +2,7 @@ module Types class BaseArgument < GraphQL::Schema::Argument - include GitlabStyleDeprecations + include Gitlab::Graphql::Deprecations attr_reader :doc_reference diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb index 11877b79e59..45e78b330fb 100644 --- a/app/graphql/types/base_enum.rb +++ b/app/graphql/types/base_enum.rb @@ -4,7 +4,7 @@ module Types class BaseEnum < GraphQL::Schema::Enum class CustomValue < GraphQL::Schema::EnumValue - include ::GitlabStyleDeprecations + include Gitlab::Graphql::Deprecations def initialize(name, desc = nil, **kwargs) init_gitlab_deprecation(kwargs) diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 615c143a0b9..caeb81c95cb 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -2,7 +2,7 @@ module Types class BaseField < GraphQL::Schema::Field - include GitlabStyleDeprecations + include Gitlab::Graphql::Deprecations argument_class ::Types::BaseArgument diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb index 574791b79e6..dd6647b749d 100644 --- a/app/graphql/types/ci/ci_cd_setting_type.rb +++ b/app/graphql/types/ci/ci_cd_setting_type.rb @@ -30,6 +30,11 @@ 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/code_quality_report_summary_type.rb b/app/graphql/types/ci/code_quality_report_summary_type.rb new file mode 100644 index 00000000000..0d560d9e9e8 --- /dev/null +++ b/app/graphql/types/ci/code_quality_report_summary_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + # This is presented through `PipelineType` that has its own authorization + class CodeQualityReportSummaryType < BaseObject + graphql_name 'CodeQualityReportSummary' + description 'Code Quality report for a pipeline' + + field :count, GraphQL::Types::Int, null: true, + description: 'Total number of Code Quality reports.' + ::Gitlab::Ci::Reports::CodequalityReports::SEVERITY_PRIORITIES.each_key do |status| + field status, GraphQL::Types::Int, null: true, + description: "Total number of #{status} status." + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/job_token_scope/direction_enum.rb b/app/graphql/types/ci/job_token_scope/direction_enum.rb new file mode 100644 index 00000000000..f52cf891af8 --- /dev/null +++ b/app/graphql/types/ci/job_token_scope/direction_enum.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Ci + module JobTokenScope + class DirectionEnum < BaseEnum + graphql_name 'CiJobTokenScopeDirection' + description 'Direction of access.' + + value 'OUTBOUND', + value: :outbound, + description: 'Job token scope project can access target project in the outbound allowlist.' + + value 'INBOUND', + value: :inbound, + description: 'Target projects in the inbound allowlist can access the scope project ' \ + 'through their job tokens.' + end + end + end +end diff --git a/app/graphql/types/ci/job_token_scope_type.rb b/app/graphql/types/ci/job_token_scope_type.rb index 37c0af944a7..639bbaa22af 100644 --- a/app/graphql/types/ci/job_token_scope_type.rb +++ b/app/graphql/types/ci/job_token_scope_type.rb @@ -11,7 +11,23 @@ module Types Types::ProjectType.connection_type, null: false, description: 'Allow list of projects that can be accessed by CI Job tokens created by this project.', - method: :all_projects + method: :outbound_projects, + deprecated: { + reason: 'The `projects` attribute is being deprecated. Use `outbound_allowlist`', + milestone: '15.9' + } + + field :outbound_allowlist, + Types::ProjectType.connection_type, + null: false, + description: "Allow list of projects that are accessible using the current project's CI Job tokens.", + method: :outbound_projects + + field :inbound_allowlist, + Types::ProjectType.connection_type, + null: false, + description: "Allow list of projects that can access the current project through its CI Job tokens.", + method: :inbound_projects 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 4447a10a74e..a97e9cee4b1 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -37,6 +37,8 @@ module Types # Life-cycle timestamps: field :created_at, Types::TimeType, null: false, description: "When the job was created." + field :erased_at, Types::TimeType, null: true, + description: "When the job was erased." field :finished_at, Types::TimeType, null: true, description: 'When a job has finished running.' field :queued_at, Types::TimeType, null: true, @@ -97,6 +99,8 @@ module Types field :web_path, GraphQL::Types::String, null: true, description: 'Web path of the job.' + field :project, Types::ProjectType, null: true, description: 'Project that the job belongs to.' + def kind return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class) diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index cb561f48b3b..19d261853a7 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -178,6 +178,10 @@ module Types field :merge_request_event_type, Types::Ci::PipelineMergeRequestEventTypeEnum, null: true, description: "Event type of the pipeline associated with a merge request." + def commit + BatchLoader::GraphQL.wrap(object.commit) + end + def detailed_status object.detailed_status(current_user) end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index 35339624e37..10d18f9ad2a 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -14,6 +14,9 @@ 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, @@ -35,6 +38,10 @@ module Types 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.', + authorize: :read_ephemeral_token, + alpha: { milestone: '15.9' } field :executor_name, GraphQL::Types::String, null: true, description: 'Executor last advertised by the runner.', method: :executor_name @@ -134,6 +141,14 @@ 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? + + runner.token + end + def project_count BatchLoader::GraphQL.for(runner.id).batch(key: :runner_project_count) do |ids, loader, args| counts = ::Ci::Runner.project_type diff --git a/app/graphql/types/ci/runner_upgrade_status_enum.rb b/app/graphql/types/ci/runner_upgrade_status_enum.rb index 34a931c8f79..668970aaff2 100644 --- a/app/graphql/types/ci/runner_upgrade_status_enum.rb +++ b/app/graphql/types/ci/runner_upgrade_status_enum.rb @@ -5,13 +5,13 @@ module Types class RunnerUpgradeStatusEnum < BaseEnum graphql_name 'CiRunnerUpgradeStatus' + MODEL_STATUS_TO_GRAPHQL_TRANSLATIONS = { + invalid_version: :invalid, + unavailable: :not_available + }.freeze + ::Ci::RunnerVersion::STATUS_DESCRIPTIONS.each do |status, description| - status_name_src = - if status == :invalid_version - :invalid - else - status - end + status_name_src = MODEL_STATUS_TO_GRAPHQL_TRANSLATIONS.fetch(status, status) value status_name_src.to_s.upcase, description: description, value: status end diff --git a/app/graphql/types/ci/variable_sort_enum.rb b/app/graphql/types/ci/variable_sort_enum.rb new file mode 100644 index 00000000000..3a60899ab5d --- /dev/null +++ b/app/graphql/types/ci/variable_sort_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Ci + class VariableSortEnum < BaseEnum + graphql_name 'CiVariableSort' + description 'Values for sorting variables' + + value 'KEY_ASC', 'Sorted by key in ascending order.', value: :key_asc + value 'KEY_DESC', 'Sorted by key in descending order.', value: :key_desc + end + end +end diff --git a/app/graphql/types/concerns/gitlab_style_deprecations.rb b/app/graphql/types/concerns/gitlab_style_deprecations.rb deleted file mode 100644 index 859a27cac4c..00000000000 --- a/app/graphql/types/concerns/gitlab_style_deprecations.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -# Concern for handling GraphQL deprecations. -# https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-schema-items -module GitlabStyleDeprecations - extend ActiveSupport::Concern - - included do - attr_accessor :deprecation - end - - def visible?(ctx) - super && ctx[:remove_deprecated] == true ? deprecation.nil? : true - end - - private - - # Set deprecation, mutate the arguments - def init_gitlab_deprecation(kwargs) - if kwargs[:deprecation_reason].present? - raise ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \ - 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-schema-items' - end - - # GitLab allows items to be marked as "alpha", which leverages GraphQL deprecations. - deprecation_args = kwargs.extract!(:alpha, :deprecated) - - self.deprecation = ::Gitlab::Graphql::Deprecation.parse(**deprecation_args) - return unless deprecation - - raise ArgumentError, "Bad deprecation. #{deprecation.errors.full_messages.to_sentence}" unless deprecation.valid? - - kwargs[:deprecation_reason] = deprecation.deprecation_reason - kwargs[:description] = deprecation.edit_description(kwargs[:description]) - end -end diff --git a/app/graphql/types/data_transfer/base_type.rb b/app/graphql/types/data_transfer/base_type.rb new file mode 100644 index 00000000000..e077612bfd5 --- /dev/null +++ b/app/graphql/types/data_transfer/base_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module DataTransfer + class BaseType < BaseObject + authorize + + 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! + 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 new file mode 100644 index 00000000000..a050540999f --- /dev/null +++ b/app/graphql/types/data_transfer/egress_node_type.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Types + module DataTransfer + class EgressNodeType < BaseObject + authorize + + field :date, GraphQL::Types::String, + description: 'First day of the node range. There is one node per month.', + null: false + + field :total_egress, GraphQL::Types::BigInt, + description: 'Total egress for that project in that period of time.', + null: false + + field :repository_egress, GraphQL::Types::BigInt, + description: 'Repository egress for that project in that period of time.', + null: false + + field :artifacts_egress, GraphQL::Types::BigInt, + description: 'Artifacts egress for that project in that period of time.', + null: false + + field :packages_egress, GraphQL::Types::BigInt, + description: 'Packages egress for that project in that period of time.', + null: false + + field :registry_egress, GraphQL::Types::BigInt, + description: 'Registery 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/group_data_transfer_type.rb b/app/graphql/types/data_transfer/group_data_transfer_type.rb new file mode 100644 index 00000000000..a9a353e10e8 --- /dev/null +++ b/app/graphql/types/data_transfer/group_data_transfer_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + module DataTransfer + class GroupDataTransferType < BaseType + graphql_name 'GroupDataTransfer' + authorize + 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 new file mode 100644 index 00000000000..f385aa20a7e --- /dev/null +++ b/app/graphql/types/data_transfer/project_data_transfer_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module DataTransfer + class ProjectDataTransferType < BaseType + graphql_name 'ProjectDataTransfer' + authorize + + 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! + + def total_egress(**_) + return unless Feature.enabled?(:data_transfer_monitoring) + + 40_000_000 + end + end + end +end diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb index 1c23fd44ea1..6d895cc81cf 100644 --- a/app/graphql/types/deployment_type.rb +++ b/app/graphql/types/deployment_type.rb @@ -65,9 +65,9 @@ module Types field :tags, [Types::DeploymentTagType], description: 'Git tags that contain this deployment. ' \ - 'This field can only be resolved for one deployment in any single request.', + 'This field can only be resolved for two deployments in any single request.', calls_gitaly: true do - extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 2 end end end diff --git a/app/graphql/types/group_release_sort_enum.rb b/app/graphql/types/group_release_sort_enum.rb new file mode 100644 index 00000000000..7420e7a31ad --- /dev/null +++ b/app/graphql/types/group_release_sort_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + # Not inheriting from Types::SortEnum since we only want + # to implement a subset of the sort values it defines. + class GroupReleaseSortEnum < BaseEnum + graphql_name 'GroupReleaseSort' + description 'Values for sorting releases belonging to a group' + + # Borrowed from Types::ReleaseSortEnum and Types::SortEnum + # These values/descriptions should stay in-sync as much as possible. + value 'RELEASED_AT_DESC', 'Released at by descending order.', value: :released_at_desc + value 'RELEASED_AT_ASC', 'Released at by ascending order.', value: :released_at_asc + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 4e5ddbac8a2..3543ac29c17 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -193,14 +193,14 @@ module Types null: true, description: 'List of descendant groups of this group.', complexity: 5, - resolver: Resolvers::GroupsResolver + resolver: Resolvers::NestedGroupsResolver field :ci_variables, Types::Ci::GroupVariableType.connection_type, null: true, description: "List of the group's CI/CD variables.", authorize: :admin_group, - method: :variables + resolver: Resolvers::Ci::VariablesResolver field :runners, Types::Ci::RunnerType.connection_type, null: true, @@ -233,6 +233,17 @@ module Types resolver: Resolvers::WorkItems::TypesResolver, description: 'Work item types available to the group.' + field :releases, + Types::ReleaseType.connection_type, + null: true, + description: 'Releases belonging to projects in the group.', + resolver: Resolvers::GroupReleasesResolver + + field :data_transfer, Types::DataTransfer::GroupDataTransferType, + null: true, + resolver: Resolvers::DataTransferResolver.group, + description: 'Data transfer data point for a specific period. This is mocked data under a development feature flag.' + def label(title:) BatchLoader::GraphQL.for(title).batch(key: group) do |titles, loader, args| LabelsFinder diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 4948063610a..1e5833a5cf0 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -54,19 +54,24 @@ module Types description: 'Indicates the issue is confidential.' field :discussion_locked, GraphQL::Types::Boolean, null: false, description: 'Indicates discussion is locked on the issue.' - field :due_date, Types::TimeType, null: true, - description: 'Due date of the issue.' - field :hidden, GraphQL::Types::Boolean, null: true, resolver_method: :hidden?, - description: 'Indicates the issue is hidden because the author has been banned. ' \ - 'Will always return `null` if `ban_user_feature_flag` feature flag is disabled.' - field :downvotes, GraphQL::Types::Int, null: false, description: 'Number of downvotes the issue has received.', resolver: Resolvers::DownVotesCountResolver + field :due_date, Types::TimeType, null: true, + description: 'Due date of the issue.' + field :hidden, GraphQL::Types::Boolean, null: true, + description: 'Indicates the issue is hidden because the author has been banned.', method: :hidden? field :merge_requests_count, GraphQL::Types::Int, null: false, description: 'Number of merge requests that close the issue on merge.', resolver: Resolvers::MergeRequestsCountResolver + + field :related_merge_requests, Types::MergeRequestType.connection_type, + null: true, + description: 'Merge requests related to the issue. This field can only be resolved for one issue in any single request.' do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + field :relative_position, GraphQL::Types::Int, null: true, description: 'Relative position of the issue (used for positioning in epic tree and issue boards).' field :upvotes, GraphQL::Types::Int, @@ -182,6 +187,17 @@ module Types Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.duplicated_to_id).find end + def related_merge_requests + # rubocop: disable CodeReuse/ActiveRecord + MergeRequest.where( + id: ::Issues::ReferencedMergeRequestsService.new(container: object.project, current_user: current_user) + .execute(object) + .first + .map(&:id) + ) + # rubocop: enable CodeReuse/ActiveRecord + end + def discussion_locked !!object.discussion_locked end @@ -190,10 +206,6 @@ module Types object.creatable_note_email_address(context[:current_user]) end - def hidden? - object.hidden? if Feature.enabled?(:ban_user_feature_flag) - end - def escalation_status object.supports_escalation? ? object.escalation_status&.status_name : nil end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index abf7b3ad530..3c288c1d496 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -213,9 +213,9 @@ module Types field :security_auto_fix, GraphQL::Types::Boolean, null: true, description: 'Indicates if the merge request is created by @GitLab-Security-Bot.' field :squash, GraphQL::Types::Boolean, null: false, - description: 'Indicates if squash on merge is enabled.' + description: 'Indicates if the merge request is set to be squashed when merged. [Project settings](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html#configure-squash-options-for-a-project) may override this value. Use `squash_on_merge` instead to take project squash options into account.' field :squash_on_merge, GraphQL::Types::Boolean, null: false, method: :squash_on_merge?, - description: 'Indicates if squash on merge is enabled.' + description: 'Indicates if the merge request will be squashed when merged.' field :timelogs, Types::TimelogType.connection_type, null: false, description: 'Timelogs on the merge request.' diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 5a92ba754aa..e48e9deae96 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -66,6 +66,7 @@ module Types mount_mutation Mutations::Issues::Move mount_mutation Mutations::Issues::LinkAlerts mount_mutation Mutations::Issues::UnlinkAlert + mount_mutation Mutations::Issues::BulkUpdate, alpha: { milestone: '15.9' } mount_mutation Mutations::Labels::Create mount_mutation Mutations::Members::Groups::BulkUpdate mount_mutation Mutations::MergeRequests::Accept @@ -123,6 +124,7 @@ module Types mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership mount_mutation Mutations::Ci::PipelineSchedule::Play mount_mutation Mutations::Ci::PipelineSchedule::Create + mount_mutation Mutations::Ci::PipelineSchedule::Update mount_mutation Mutations::Ci::CiCdSettingsUpdate, deprecated: { reason: :renamed, replacement: 'ProjectCiCdSettingsUpdate', diff --git a/app/graphql/types/notes/deleted_note_type.rb b/app/graphql/types/notes/deleted_note_type.rb new file mode 100644 index 00000000000..f799fc01f6e --- /dev/null +++ b/app/graphql/types/notes/deleted_note_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Types + module Notes + # rubocop: disable Graphql/AuthorizeTypes + class DeletedNoteType < BaseObject + graphql_name 'DeletedNote' + + field :id, ::Types::GlobalIDType[::Note], + null: false, + description: 'ID of the deleted note.' + + field :discussion_id, ::Types::GlobalIDType[::Discussion], + null: true, + description: 'ID of the discussion for the deleted note.' + + field :last_discussion_note, GraphQL::Types::Boolean, + null: true, + description: 'Whether deleted note is the last note in the discussion.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb index 6c0d955ed77..f63b41b3c92 100644 --- a/app/graphql/types/packages/package_details_type.rb +++ b/app/graphql/types/packages/package_details_type.rb @@ -28,6 +28,8 @@ module Types field :last_downloaded_at, Types::TimeType, null: true, description: 'Last time that a file of this package was downloaded.' + field :public_package, GraphQL::Types::Boolean, null: true, description: 'Indicates if there is public access to the package.' + def versions object.versions end @@ -63,6 +65,10 @@ module Types def pypi_url pypi_registry_url(object.project.id) end + + def public_package + object.project.public? || object.project.project_feature.package_registry_access_level == ProjectFeature::PUBLIC + end end end end diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb index 0192af25d0f..d45c61f489b 100644 --- a/app/graphql/types/permission_types/base_permission_type.rb +++ b/app/graphql/types/permission_types/base_permission_type.rb @@ -28,7 +28,7 @@ module Types end def self.define_field_resolver_method(ability) - unless self.respond_to?(ability) + unless respond_to?(ability) define_method ability.to_sym do |*args| Ability.allowed?(context[:current_user], ability, object, args.to_h) end diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb index 73a2f820f79..88d8c38361a 100644 --- a/app/graphql/types/permission_types/merge_request.rb +++ b/app/graphql/types/permission_types/merge_request.rb @@ -21,10 +21,15 @@ module Types end permission_field :can_merge, calls_gitaly: true + permission_field :can_approve def can_merge object.can_be_merged_by?(context[:current_user]) end + + def can_approve + object.eligible_for_approval_by?(context[:current_user]) + end end end end diff --git a/app/graphql/types/permission_types/work_item.rb b/app/graphql/types/permission_types/work_item.rb index bae1dae4834..f35f42001e0 100644 --- a/app/graphql/types/permission_types/work_item.rb +++ b/app/graphql/types/permission_types/work_item.rb @@ -6,7 +6,7 @@ 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 + abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item end end end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index fe13ee7ef3c..c105ab9814c 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -345,7 +345,7 @@ module Types null: true, description: "List of the project's CI/CD variables.", authorize: :admin_build, - method: :variables + resolver: Resolvers::Ci::VariablesResolver field :ci_cd_settings, Types::Ci::CiCdSettingType, null: true, @@ -393,6 +393,10 @@ module Types field :services, Types::Projects::ServiceType.connection_type, null: true, + deprecated: { + reason: 'This will be renamed to `Project.integrations`', + milestone: '15.9' + }, description: 'Project services.', resolver: Resolvers::Projects::ServicesResolver @@ -562,6 +566,21 @@ module Types resolver: ::Resolvers::Ci::ProjectRunnersResolver, 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, + 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, + null: true, + alpha: { milestone: '15.10' }, + description: "Visible forks of the project." do + argument :minimum_access_level, + type: ::Types::AccessLevelEnum, + required: false, + description: 'Minimum access level.' + end + def timelog_categories object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories) end @@ -601,7 +620,11 @@ module Types end def open_issues_count - object.open_issues_count if object.feature_available?(:issues, context[:current_user]) + BatchLoader::GraphQL.wrap(object.open_issues_count) if object.feature_available?(:issues, context[:current_user]) + end + + def forks_count + BatchLoader::GraphQL.wrap(object.forks_count) end def statistics @@ -612,6 +635,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) @@ -630,6 +655,11 @@ module Types def sast_ci_configuration return unless Ability.allowed?(current_user, :read_code, object) + 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 + end + ::Security::CiConfiguration::SastParserService.new(object).configuration end @@ -643,11 +673,28 @@ module Types ::Projects::RepositoryLanguagesService.new(project, current_user).execute end + def visible_forks(minimum_access_level: nil) + if minimum_access_level.nil? + object.forks.public_or_visible_to_user(current_user) + else + object.forks.visible_to_user_and_access_level(current_user, minimum_access_level) + end + end + private def project @project ||= object.respond_to?(:sync) ? object.sync : object end + + def add_file_docs_link + ActionController::Base.helpers.link_to _('add at least one file to the repository'), + Rails.application.routes.url_helpers.help_page_url( + 'user/project/repository/index.md', + anchor: 'add-files-to-a-repository'), + target: '_blank', + rel: 'noopener noreferrer' + end end end diff --git a/app/graphql/types/projects/namespace_project_sort_enum.rb b/app/graphql/types/projects/namespace_project_sort_enum.rb index bd7058196dd..7c7b54226d3 100644 --- a/app/graphql/types/projects/namespace_project_sort_enum.rb +++ b/app/graphql/types/projects/namespace_project_sort_enum.rb @@ -8,6 +8,7 @@ module Types 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 end end end diff --git a/app/graphql/types/projects/service_type.rb b/app/graphql/types/projects/service_type.rb index 1416d93d3b4..ec58e3254ae 100644 --- a/app/graphql/types/projects/service_type.rb +++ b/app/graphql/types/projects/service_type.rb @@ -2,6 +2,7 @@ module Types module Projects + # TODO: Remove in 17.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108418 module ServiceType include Types::BaseInterface graphql_name 'Service' diff --git a/app/graphql/types/projects/service_type_enum.rb b/app/graphql/types/projects/service_type_enum.rb index d0cecbfea49..fd88fa957e7 100644 --- a/app/graphql/types/projects/service_type_enum.rb +++ b/app/graphql/types/projects/service_type_enum.rb @@ -2,6 +2,7 @@ module Types module Projects + # TODO: Remove in 17.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108418 class ServiceTypeEnum < BaseEnum graphql_name 'ServiceType' diff --git a/app/graphql/types/projects/services/base_service_type.rb b/app/graphql/types/projects/services/base_service_type.rb index 5341ae2a864..9a48aafa5a8 100644 --- a/app/graphql/types/projects/services/base_service_type.rb +++ b/app/graphql/types/projects/services/base_service_type.rb @@ -3,6 +3,7 @@ module Types module Projects module Services + # TODO: Remove in 17.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108418 class BaseServiceType < BaseObject graphql_name 'BaseService' diff --git a/app/graphql/types/projects/services/jira_project_type.rb b/app/graphql/types/projects/services/jira_project_type.rb index 1c5b97802e3..eb721d02b36 100644 --- a/app/graphql/types/projects/services/jira_project_type.rb +++ b/app/graphql/types/projects/services/jira_project_type.rb @@ -4,6 +4,7 @@ module Types module Projects module Services # rubocop:disable Graphql/AuthorizeTypes + # TODO: Remove in 17.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108418 class JiraProjectType < BaseObject graphql_name 'JiraProject' diff --git a/app/graphql/types/projects/services/jira_service_type.rb b/app/graphql/types/projects/services/jira_service_type.rb index 425a283c674..ac274d7f890 100644 --- a/app/graphql/types/projects/services/jira_service_type.rb +++ b/app/graphql/types/projects/services/jira_service_type.rb @@ -3,6 +3,7 @@ module Types module Projects module Services + # TODO: Remove in 17.0, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108418 class JiraServiceType < BaseObject graphql_name 'JiraService' diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 990ba1fb7fc..fb906759ba4 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -17,7 +17,8 @@ module Types field :ci_variables, Types::Ci::InstanceVariableType.connection_type, null: true, - description: "List of the instance's CI/CD variables." + description: "List of the instance's CI/CD variables.", + resolver: Resolvers::Ci::VariablesResolver field :container_repository, Types::ContainerRepositoryDetailsType, null: true, description: 'Find a container repository.' do @@ -40,6 +41,10 @@ module Types null: true, resolver: Resolvers::GroupResolver, description: "Find a group." + field :groups, Types::GroupType.connection_type, + null: true, + resolver: Resolvers::GroupsResolver, + description: "Find groups." field :issue, Types::IssueType, null: true, description: 'Find an issue.' do @@ -50,8 +55,7 @@ module Types alpha: { milestone: '15.6' }, resolver: Resolvers::IssuesResolver, description: 'Find issues visible to the current user.' \ - ' At least one filter must be provided.' \ - ' Returns `null` if the `root_level_issues_query` feature flag is disabled.' + ' At least one filter must be provided.' field :jobs, ::Types::Ci::JobType.connection_type, null: true, @@ -76,6 +80,15 @@ module Types null: true, resolver: Resolvers::NamespaceResolver, description: "Find a namespace." + field :note, + ::Types::Notes::NoteType, + null: true, + description: 'Find a note.', + alpha: { milestone: '15.9' } do + argument :id, ::Types::GlobalIDType[::Note], + required: true, + description: 'Global ID of the note.' + end field :package, description: 'Find a package. This field can only be resolved for one query in any single request. Returns `null` if a package has no `default` status.', resolver: Resolvers::PackageDetailsResolver @@ -95,8 +108,10 @@ module Types resolver: Resolvers::Ci::RunnerResolver, extras: [:lookahead], description: "Find a runner." - field :runner_platforms, resolver: Resolvers::Ci::RunnerPlatformsResolver - field :runner_setup, resolver: Resolvers::Ci::RunnerSetupResolver + field :runner_platforms, resolver: Resolvers::Ci::RunnerPlatformsResolver, + deprecated: { reason: 'No longer used, use gitlab-runner documentation to learn about supported platforms', milestone: '15.9' } + field :runner_setup, resolver: Resolvers::Ci::RunnerSetupResolver, + deprecated: { reason: 'No longer used, use gitlab-runner documentation to learn about runner registration commands', milestone: '15.9' } field :runners, Types::Ci::RunnerType.connection_type, null: true, resolver: Resolvers::Ci::RunnersResolver, @@ -106,6 +121,12 @@ module Types null: true, resolver: Resolvers::SnippetsResolver, description: 'Find Snippets visible to the current user.' + field :synthetic_note, + Types::Notes::NoteType, + null: true, + description: 'Find a synthetic note', + resolver: ::Resolvers::Notes::SyntheticNoteResolver, + alpha: { milestone: '15.9' } field :timelogs, Types::TimelogType.connection_type, null: true, description: 'Find timelogs visible to the current user.', @@ -145,6 +166,10 @@ module Types GitlabSchema.find_by_gid(id) end + def note(id:) + GitlabSchema.find_by_gid(id) + end + def merge_request(id:) GitlabSchema.find_by_gid(id) end @@ -166,12 +191,6 @@ module Types application_settings end - def ci_variables - return unless current_user&.can_admin_all_resources? - - ::Ci::InstanceVariable.all - end - def application_settings Gitlab::CurrentSettings.current_application_settings end diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb index e171c683e7d..b2bc52c7745 100644 --- a/app/graphql/types/release_asset_link_type.rb +++ b/app/graphql/types/release_asset_link_type.rb @@ -9,8 +9,12 @@ module Types present_using Releases::LinkPresenter - field :external, GraphQL::Types::Boolean, null: true, method: :external?, - description: 'Indicates the link points to an external resource.' + 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/saved_reply_type.rb b/app/graphql/types/saved_reply_type.rb index 329f431b10e..8c9f3d19810 100644 --- a/app/graphql/types/saved_reply_type.rb +++ b/app/graphql/types/saved_reply_type.rb @@ -4,6 +4,8 @@ module Types class SavedReplyType < BaseObject graphql_name 'SavedReply' + connection_type_class(Types::CountableConnectionType) + authorize :read_saved_replies field :id, Types::GlobalIDType[::Users::SavedReply], diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb index f7f26ba4c5a..33fc0cbe20e 100644 --- a/app/graphql/types/subscription_type.rb +++ b/app/graphql/types/subscription_type.rb @@ -4,41 +4,60 @@ module Types class SubscriptionType < ::Types::BaseObject graphql_name 'Subscription' - field :issuable_assignees_updated, subscription: Subscriptions::IssuableUpdated, null: true, - description: 'Triggered when the assignees of an issuable are updated.' + field :issuable_assignees_updated, + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the assignees of an issuable are updated.' - field :issue_crm_contacts_updated, subscription: Subscriptions::IssuableUpdated, null: true, - description: 'Triggered when the crm contacts of an issuable are updated.' + field :issue_crm_contacts_updated, + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the crm contacts of an issuable are updated.' - field :issuable_title_updated, subscription: Subscriptions::IssuableUpdated, null: true, - description: 'Triggered when the title of an issuable is updated.' + field :issuable_title_updated, + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the title of an issuable is updated.' - field :issuable_description_updated, subscription: Subscriptions::IssuableUpdated, null: true, - description: 'Triggered when the description of an issuable is updated.' + field :issuable_description_updated, + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the description of an issuable is updated.' - field :issuable_labels_updated, subscription: Subscriptions::IssuableUpdated, null: true, - description: 'Triggered when the labels of an issuable are updated.' + field :issuable_labels_updated, + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the labels of an issuable are updated.' - field :issuable_dates_updated, subscription: Subscriptions::IssuableUpdated, null: true, - description: 'Triggered when the due date or start date of an issuable is updated.' + field :issuable_dates_updated, + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the due date or start date of an issuable is updated.' - field :issuable_milestone_updated, subscription: Subscriptions::IssuableUpdated, null: true, - description: 'Triggered when the milestone of an issuable is updated.' + field :issuable_milestone_updated, + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the milestone of an issuable is updated.' + + field :work_item_note_created, + subscription: ::Subscriptions::Notes::Created, null: true, + description: 'Triggered when a note is created.', + alpha: { milestone: '15.9' } + + field :work_item_note_deleted, + subscription: ::Subscriptions::Notes::Deleted, null: true, + description: 'Triggered when a note is deleted.', + alpha: { milestone: '15.9' } + + field :work_item_note_updated, + subscription: ::Subscriptions::Notes::Updated, null: true, + description: 'Triggered when a note is updated.', + alpha: { milestone: '15.9' } field :merge_request_reviewers_updated, - subscription: Subscriptions::IssuableUpdated, - null: true, - description: 'Triggered when the reviewers of a merge request are updated.' + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the reviewers of a merge request are updated.' field :merge_request_merge_status_updated, - subscription: Subscriptions::IssuableUpdated, - null: true, - description: 'Triggered when the merge status of a merge request is updated.' + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when the merge status of a merge request is updated.' field :merge_request_approval_state_updated, - subscription: Subscriptions::IssuableUpdated, - null: true, - description: 'Triggered when approval state of a merge request is updated.' + subscription: Subscriptions::IssuableUpdated, null: true, + description: 'Triggered when approval state of a merge request is updated.' end end diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb index a5bed3b9e19..9115b5a4760 100644 --- a/app/graphql/types/user_interface.rb +++ b/app/graphql/types/user_interface.rb @@ -137,6 +137,11 @@ module Types description: 'Saved replies authored by the user. ' \ 'Will not return saved replies if `saved_replies` feature flag is disabled.' + field :saved_reply, + resolver: Resolvers::SavedReplyResolver, + description: 'Saved reply authored by the user. ' \ + 'Will not return saved reply if `saved_replies` feature flag is disabled.' + field :gitpod_enabled, GraphQL::Types::Boolean, null: true, description: 'Whether Gitpod is enabled at the user level.' diff --git a/app/graphql/types/work_item_id_type.rb b/app/graphql/types/work_item_id_type.rb index bb01f865414..777edfad529 100644 --- a/app/graphql/types/work_item_id_type.rb +++ b/app/graphql/types/work_item_id_type.rb @@ -37,7 +37,11 @@ module Types def suitable?(gid) return false if gid&.model_name&.safe_constantize.blank? + # Using === operation doesn't work for model classes. + # See https://github.com/rails/rails/blob/v6.1.6.1/activerecord/lib/active_record/core.rb#L452 + # rubocop:disable Performance/RedundantEqualityComparisonBlock [::WorkItem, ::Issue].any? { |model_class| gid.model_class == model_class } + # rubocop:enable Performance/RedundantEqualityComparisonBlock end private diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index 6a1a4f158be..b46362f66b8 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -8,6 +8,9 @@ module Types authorize :read_work_item + field :author, Types::UserType, null: true, + description: 'User that created the work item.', + alpha: { milestone: '15.9' } field :closed_at, Types::TimeType, null: true, description: 'Timestamp of when the work item was closed.' field :confidential, GraphQL::Types::Boolean, null: false, diff --git a/app/graphql/types/work_items/widget_type_enum.rb b/app/graphql/types/work_items/widget_type_enum.rb index 4e5933bff86..2ad951d421b 100644 --- a/app/graphql/types/work_items/widget_type_enum.rb +++ b/app/graphql/types/work_items/widget_type_enum.rb @@ -6,8 +6,8 @@ module Types graphql_name 'WorkItemWidgetType' description 'Type of a work item widget' - ::WorkItems::Type.available_widgets.each do |widget| - value widget.type.to_s.upcase, value: widget.type, description: "#{widget.type.to_s.titleize} widget." + ::WorkItems::WidgetDefinition.widget_classes.each do |cls| + value cls.type.to_s.upcase, value: cls.type, description: "#{cls.type.to_s.titleize} widget." end end end |