diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
commit | 0c872e02b2c822e3397515ec324051ff540f0cd5 (patch) | |
tree | ce2fb6ce7030e4dad0f4118d21ab6453e5938cdd /app/graphql | |
parent | f7e05a6853b12f02911494c4b3fe53d9540d74fc (diff) | |
download | gitlab-ce-0c872e02b2c822e3397515ec324051ff540f0cd5.tar.gz |
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'app/graphql')
87 files changed, 1007 insertions, 184 deletions
diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb index 710e7fe110c..7f83b62a2ff 100644 --- a/app/graphql/graphql_triggers.rb +++ b/app/graphql/graphql_triggers.rb @@ -44,6 +44,14 @@ module GraphqlTriggers merge_request ) end + + def self.merge_request_approval_state_updated(merge_request) + GitlabSchema.subscriptions.trigger( + 'mergeRequestApprovalStateUpdated', + { issuable_id: merge_request.to_gid }, + merge_request + ) + end end GraphqlTriggers.prepend_mod diff --git a/app/graphql/mutations/alert_management/alerts/set_assignees.rb b/app/graphql/mutations/alert_management/alerts/set_assignees.rb index c986111d290..500e2b868b1 100644 --- a/app/graphql/mutations/alert_management/alerts/set_assignees.rb +++ b/app/graphql/mutations/alert_management/alerts/set_assignees.rb @@ -20,7 +20,7 @@ module Mutations alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) result = set_assignees(alert, args[:assignee_usernames], args[:operation_mode]) - track_usage_event(:incident_management_alert_assigned, current_user.id) + track_alert_events('incident_management_alert_assigned', alert) prepare_response(result) end diff --git a/app/graphql/mutations/alert_management/alerts/todo/create.rb b/app/graphql/mutations/alert_management/alerts/todo/create.rb index 2a1056e8f64..999c0bec5af 100644 --- a/app/graphql/mutations/alert_management/alerts/todo/create.rb +++ b/app/graphql/mutations/alert_management/alerts/todo/create.rb @@ -11,7 +11,7 @@ module Mutations alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) result = ::AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute - track_usage_event(:incident_management_alert_todo, current_user.id) + track_alert_events('incident_management_alert_todo', alert) prepare_response(result) end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index d01f200107c..2eef6bb9db7 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -39,6 +39,24 @@ module Mutations ::AlertManagement::AlertsFinder.new(current_user, project, args).execute.first end + + def track_alert_events(event, alert) + project = alert.project + 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, + project: project, + namespace: namespace, + user: current_user, + label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly', + context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context] + ) + end end end end diff --git a/app/graphql/mutations/alert_management/create_alert_issue.rb b/app/graphql/mutations/alert_management/create_alert_issue.rb index 77a7d7a4147..7c8de6365e7 100644 --- a/app/graphql/mutations/alert_management/create_alert_issue.rb +++ b/app/graphql/mutations/alert_management/create_alert_issue.rb @@ -9,7 +9,7 @@ module Mutations alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) result = create_alert_issue(alert, current_user) - track_usage_event(:incident_management_incident_created, current_user.id) + track_alert_events('incident_management_incident_created', alert) track_usage_event(:incident_management_alert_create_incident, current_user.id) prepare_response(alert, result) diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb index 21566c7d66f..be271a7d795 100644 --- a/app/graphql/mutations/alert_management/update_alert_status.rb +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -13,7 +13,7 @@ module Mutations alert = authorized_find!(project_path: project_path, iid: iid) result = update_status(alert, status) - track_usage_event(:incident_management_alert_status_changed, current_user.id) + track_alert_events('incident_management_alert_status_changed', alert) prepare_response(result) end diff --git a/app/graphql/mutations/ci/pipeline_schedule/create.rb b/app/graphql/mutations/ci/pipeline_schedule/create.rb new file mode 100644 index 00000000000..65b355cd80f --- /dev/null +++ b/app/graphql/mutations/ci/pipeline_schedule/create.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module PipelineSchedule + class Create < BaseMutation + graphql_name 'PipelineScheduleCreate' + + include FindsProject + + authorize :create_pipeline_schedule + + argument :project_path, GraphQL::Types::ID, + required: true, + description: 'Full path of the project the pipeline schedule is associated with.' + + argument :description, GraphQL::Types::String, + required: true, + description: 'Description of the pipeline schedule.' + + argument :cron, GraphQL::Types::String, + required: true, + 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: true, + 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: 'Created pipeline schedule.' + + def resolve(project_path:, variables: [], **pipeline_schedule_attrs) + project = authorized_find!(project_path) + + params = pipeline_schedule_attrs.merge(variables_attributes: variables.map(&:to_h)) + + schedule = ::Ci::CreatePipelineScheduleService + .new(project, current_user, params) + .execute + + unless schedule.persisted? + return { + pipeline_schedule: nil, errors: schedule.errors.full_messages + } + end + + { + pipeline_schedule: schedule, + errors: [] + } + end + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline_schedule/play.rb b/app/graphql/mutations/ci/pipeline_schedule/play.rb new file mode 100644 index 00000000000..056890852c9 --- /dev/null +++ b/app/graphql/mutations/ci/pipeline_schedule/play.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module PipelineSchedule + class Play < Base + graphql_name 'PipelineSchedulePlay' + + authorize :play_pipeline_schedule + + field :pipeline_schedule, + Types::Ci::PipelineScheduleType, + null: true, + description: 'Pipeline schedule after mutation.' + + def resolve(id:) + schedule = authorized_find!(id: id) + + job_id = ::Ci::PipelineScheduleService + .new(schedule.project, current_user) + .execute(schedule) + + if job_id + { pipeline_schedule: schedule, errors: [] } + else + { pipeline_schedule: nil, errors: ['Unable to schedule a pipeline to run immediately.'] } + end + end + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb new file mode 100644 index 00000000000..54a6ad92448 --- /dev/null +++ b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module PipelineSchedule + class VariableInputType < Types::BaseInputObject + graphql_name 'PipelineScheduleVariableInput' + + description 'Attributes for the pipeline schedule variable.' + + argument :key, GraphQL::Types::String, required: true, description: 'Name of the variable.' + + argument :value, GraphQL::Types::String, required: true, description: 'Value of the variable.' + + argument :variable_type, Types::Ci::VariableTypeEnum, required: true, description: 'Type of the variable.' + end + end + end +end diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb index 3c99cde60a4..4f0bf19f09c 100644 --- a/app/graphql/mutations/ci/runner/update.rb +++ b/app/graphql/mutations/ci/runner/update.rb @@ -54,7 +54,7 @@ module Mutations 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, diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb index a99a54fa5ed..c10e1633350 100644 --- a/app/graphql/mutations/clusters/agent_tokens/create.rb +++ b/app/graphql/mutations/clusters/agent_tokens/create.rb @@ -49,9 +49,9 @@ module Mutations payload = result.payload { - secret: payload[:secret], - token: payload[:token], - errors: Array.wrap(result.message) + secret: payload[:secret], + token: payload[:token], + errors: Array.wrap(result.message) } end diff --git a/app/graphql/mutations/container_repositories/destroy.rb b/app/graphql/mutations/container_repositories/destroy.rb index fe1c3fe4e61..c3bd7acf444 100644 --- a/app/graphql/mutations/container_repositories/destroy.rb +++ b/app/graphql/mutations/container_repositories/destroy.rb @@ -22,10 +22,6 @@ module Mutations container_repository.delete_scheduled! - unless Feature.enabled?(:container_registry_delete_repository_with_cron_worker) - DeleteContainerRepositoryWorker.perform_async(current_user.id, container_repository.id) # rubocop:disable CodeReuse/Worker - end - track_event(:delete_repository, :container) { diff --git a/app/graphql/mutations/incident_management/timeline_event/update.rb b/app/graphql/mutations/incident_management/timeline_event/update.rb index 1f53bdc19cb..b35feed3082 100644 --- a/app/graphql/mutations/incident_management/timeline_event/update.rb +++ b/app/graphql/mutations/incident_management/timeline_event/update.rb @@ -18,6 +18,10 @@ module Mutations required: false, description: 'Timestamp when the event occurred.' + argument :timeline_event_tag_names, [GraphQL::Types::String], + required: false, + description: copy_field_description(Types::IncidentManagement::TimelineEventType, :timeline_event_tags) + def resolve(id:, **args) timeline_event = authorized_find!(id: id) diff --git a/app/graphql/mutations/issues/link_alerts.rb b/app/graphql/mutations/issues/link_alerts.rb new file mode 100644 index 00000000000..c45e90c598f --- /dev/null +++ b/app/graphql/mutations/issues/link_alerts.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class LinkAlerts < Base + graphql_name 'IssueLinkAlerts' + + argument :alert_references, [GraphQL::Types::String], + required: true, + description: 'Alerts references to be linked to the incident.' + + authorize :admin_issue + + def resolve(project_path:, iid:, alert_references:) + issue = authorized_find!(project_path: project_path, iid: iid) + + ::IncidentManagement::LinkAlerts::CreateService.new(issue, current_user, alert_references).execute + + { + issue: issue, + errors: errors_on_object(issue) + } + end + end + end +end diff --git a/app/graphql/mutations/issues/unlink_alert.rb b/app/graphql/mutations/issues/unlink_alert.rb new file mode 100644 index 00000000000..a11af4133cf --- /dev/null +++ b/app/graphql/mutations/issues/unlink_alert.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module Issues + class UnlinkAlert < Base + graphql_name 'IssueUnlinkAlert' + + argument :alert_id, ::Types::GlobalIDType[::AlertManagement::Alert], + required: true, + description: 'Global ID of the alert to unlink from the incident.' + + authorize :admin_issue + + def resolve(project_path:, iid:, alert_id:) + issue = authorized_find!(project_path: project_path, iid: iid) + alert = find_alert_by_gid(alert_id) + + result = ::IncidentManagement::LinkAlerts::DestroyService.new(issue, current_user, alert).execute + + { + issue: issue, + errors: result.errors + } + end + + private + + def find_alert_by_gid(alert_id) + ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(alert_id, expected_type: ::AlertManagement::Alert)) + end + end + end +end diff --git a/app/graphql/mutations/notes/create/diff_note.rb b/app/graphql/mutations/notes/create/diff_note.rb index 7b8c06fd104..df2bd55106e 100644 --- a/app/graphql/mutations/notes/create/diff_note.rb +++ b/app/graphql/mutations/notes/create/diff_note.rb @@ -31,10 +31,10 @@ module Mutations def create_note_params(noteable, args) super(noteable, args).merge({ - type: 'DiffNote', - position: position(noteable, args), - merge_request_diff_head_sha: args[:position][:head_sha] - }) + type: 'DiffNote', + position: position(noteable, args), + merge_request_diff_head_sha: args[:position][:head_sha] + }) end def position(noteable, args) diff --git a/app/graphql/mutations/notes/create/image_diff_note.rb b/app/graphql/mutations/notes/create/image_diff_note.rb index d94fd4d6ff8..3de93e4f5c1 100644 --- a/app/graphql/mutations/notes/create/image_diff_note.rb +++ b/app/graphql/mutations/notes/create/image_diff_note.rb @@ -15,9 +15,9 @@ module Mutations def create_note_params(noteable, args) super(noteable, args).merge({ - type: 'DiffNote', - position: position(noteable, args) - }) + type: 'DiffNote', + position: position(noteable, args) + }) end def position(noteable, args) diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb index 4d6f056de09..9b105b7fe1c 100644 --- a/app/graphql/mutations/notes/create/note.rb +++ b/app/graphql/mutations/notes/create/note.rb @@ -31,9 +31,9 @@ module Mutations end super(noteable, args).merge({ - in_reply_to_discussion_id: discussion_id, - merge_request_diff_head_sha: args[:merge_request_diff_head_sha] - }) + in_reply_to_discussion_id: discussion_id, + merge_request_diff_head_sha: args[:merge_request_diff_head_sha] + }) end def authorize_discussion!(discussion) diff --git a/app/graphql/mutations/timelogs/create.rb b/app/graphql/mutations/timelogs/create.rb index bab7508454e..1be023eed8a 100644 --- a/app/graphql/mutations/timelogs/create.rb +++ b/app/graphql/mutations/timelogs/create.rb @@ -11,7 +11,7 @@ module Mutations description: 'Amount of time spent.' argument :spent_at, - Types::DateType, + Types::TimeType, required: true, description: 'When the time was spent.' @@ -28,8 +28,12 @@ module Mutations authorize :create_timelog def resolve(issuable_id:, time_spent:, spent_at:, summary:, **args) - issuable = authorized_find!(id: issuable_id) parsed_time_spent = Gitlab::TimeTrackingFormatter.parse(time_spent) + if parsed_time_spent.nil? + return { timelog: nil, errors: [_('Time spent must be formatted correctly. For example: 1h 30m.')] } + end + + issuable = authorized_find!(id: issuable_id) result = ::Timelogs::CreateService.new( issuable, parsed_time_spent, spent_at, summary, current_user diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index 20913a9e7da..f2f944860c2 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -23,9 +23,9 @@ module Mutations updated_ids = restore(todos) { - updated_ids: updated_ids, - todos: Todo.id_in(updated_ids), - errors: errors_on_objects(todos) + updated_ids: updated_ids, + todos: Todo.id_in(updated_ids), + errors: errors_on_objects(todos) } end diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb index 793e5d3caf8..a4efffb69c1 100644 --- a/app/graphql/mutations/work_items/create.rb +++ b/app/graphql/mutations/work_items/create.rb @@ -73,3 +73,5 @@ module Mutations end end end + +Mutations::WorkItems::Create.prepend_mod diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 2b54a3fdd55..6f847221f1b 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -15,6 +15,13 @@ module Resolvers @calls_gitaly = true end + # This is a flag to allow us to use `complexity_multiplier` to compute complexity for connection + # fields(see BaseField#connection_complexity_multiplier) in resolvers that do external connection pagination, + # thus disabling the default `connection` option(see self.field_options method above). + def self.calculate_ext_conn_complexity + false + end + def self.field_options extra_options = { requires_argument: @requires_argument, @@ -116,7 +123,7 @@ module Resolvers # When fetching many items, additional complexity is added to the field # depending on how many items is fetched. For each item we add 1% of the # original complexity - this means that loading 100 items (our default - # maxp_age_size limit) doubles the original complexity. + # max_page_size limit) doubles the original complexity. # # Complexity is not increased when searching by specific ID(s), because # complexity difference is minimal in this case. diff --git a/app/graphql/resolvers/ci/project_runners_resolver.rb b/app/graphql/resolvers/ci/project_runners_resolver.rb new file mode 100644 index 00000000000..378fa73c065 --- /dev/null +++ b/app/graphql/resolvers/ci/project_runners_resolver.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class ProjectRunnersResolver < RunnersResolver + type Types::Ci::RunnerType.connection_type, null: true + + def parent_param + raise 'Expected project missing' unless parent.is_a?(Project) + + { project: parent } + end + end + end +end diff --git a/app/graphql/resolvers/ci/runner_groups_resolver.rb b/app/graphql/resolvers/ci/runner_groups_resolver.rb new file mode 100644 index 00000000000..3360e820bd2 --- /dev/null +++ b/app/graphql/resolvers/ci/runner_groups_resolver.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerGroupsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include ResolvesGroups + + type Types::GroupConnection, null: true + + authorize :read_runner + authorizes_object! + + alias_method :runner, :object + + def resolve_with_lookahead(**args) + return unless runner.group_type? + + BatchLoader::GraphQL.for(runner.id).batch(key: :runner_namespaces) do |runner_ids, loader| + plucked_runner_and_namespace_ids = + ::Ci::RunnerNamespace + .for_runner(runner_ids) + .select(:runner_id, :namespace_id) + .pluck(:runner_id, :namespace_id) # rubocop: disable CodeReuse/ActiveRecord) + + namespace_ids = plucked_runner_and_namespace_ids.collect(&:last).uniq + groups = apply_lookahead(::Group.id_in(namespace_ids)) + Preloaders::GroupPolicyPreloader.new(groups, current_user).execute + groups_by_id = groups.index_by(&:id) + + runner_group_ids_by_runner_id = + plucked_runner_and_namespace_ids + .group_by { |runner_id, _namespace_id| runner_id } + .transform_values { |values| values.filter_map { |_runner_id, namespace_id| groups_by_id[namespace_id] } } + + runner_ids.each do |runner_id| + runner_namespaces = runner_group_ids_by_runner_id[runner_id] || [] + + loader.call(runner_id, runner_namespaces) + end + end + end + end + end +end diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb index de00aadaea8..b818be3f018 100644 --- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb +++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb @@ -27,9 +27,17 @@ module Resolvers def preloads { - previous_stage_jobs_and_needs: [:needs, :pipeline], + previous_stage_jobs_or_needs: [:needs, :pipeline], artifacts: [:job_artifacts], - pipeline: [:user] + pipeline: [:user], + detailed_status: [ + :metadata, + { pipeline: [:merge_request] }, + { project: [:route, { namespace: :route }] } + ], + commit_path: [:pipeline, { project: [:route, { namespace: [:route] }] }], + short_sha: [:pipeline], + tags: [:tags] } end end diff --git a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb index da8fab93619..f4e044b81c9 100644 --- a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb +++ b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb @@ -13,20 +13,22 @@ module Resolvers resolve_owner end - def preloads - { - full_path: [:route] - } - end - private - def filtered_preloads - selection = lookahead + def node_selection(selection = lookahead) + # There are no nodes or edges selections in RunnerOwnerProjectResolver, but rather a project directly + selection + end + + def unconditional_includes + [:project_feature] + end - preloads.each.flat_map do |name, requirements| - selection&.selects?(name) ? requirements : [] - end + def preloads + { + full_path: [:route, { namespace: [:route] }], + web_url: [:route, { namespace: [:route] }] + } end def resolve_owner @@ -48,7 +50,7 @@ module Resolvers .transform_values { |runner_projects| runner_projects.first.project_id } project_ids = owner_project_id_by_runner_id.values.uniq - projects = Project.where(id: project_ids) + projects = apply_lookahead(Project.id_in(project_ids)) Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute projects_by_id = projects.index_by(&:id) diff --git a/app/graphql/resolvers/ci/runner_projects_resolver.rb b/app/graphql/resolvers/ci/runner_projects_resolver.rb index af9a67acfda..2a2d63f85de 100644 --- a/app/graphql/resolvers/ci/runner_projects_resolver.rb +++ b/app/graphql/resolvers/ci/runner_projects_resolver.rb @@ -40,6 +40,7 @@ module Resolvers params: project_finder_params(args), project_ids_relation: project_ids) .execute + projects = apply_lookahead(projects) Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute projects_by_id = projects.index_by(&:id) @@ -58,6 +59,19 @@ module Resolvers end # rubocop:enable CodeReuse/ActiveRecord end + + private + + def unconditional_includes + [:project_feature] + end + + def preloads + super.merge({ + 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 9740bc6bb6a..b7355a1752e 100644 --- a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb +++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb @@ -14,18 +14,7 @@ module Resolvers description: 'Status of the token.' def resolve(**args) - return ::Clusters::AgentToken.none unless can_read_agent_tokens? - - tokens = agent.agent_tokens - tokens = tokens.with_status(args[:status]) if args[:status].present? - - tokens - end - - private - - def can_read_agent_tokens? - current_user.can?(:read_cluster, project) + ::Clusters::AgentTokensFinder.new(agent, current_user, args).execute end end end diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb index 81099c04e9f..1d532eb2486 100644 --- a/app/graphql/resolvers/concerns/looks_ahead.rb +++ b/app/graphql/resolvers/concerns/looks_ahead.rb @@ -39,7 +39,7 @@ module LooksAhead def filtered_preloads nodes = node_selection - return [] unless nodes + return [] unless nodes&.selected? selected_fields = nodes.selections.map(&:name) root_level_preloads = preloads_from_node_selection(selected_fields, preloads) @@ -65,13 +65,13 @@ module LooksAhead end.flatten end - def node_selection - return unless lookahead + def node_selection(selection = lookahead) + return selection unless selection&.selected? + return selection.selection(:edges).selection(:node) if selection.selects?(:edges) - if lookahead.selects?(:nodes) - lookahead.selection(:nodes) - elsif lookahead.selects?(:edges) - lookahead.selection(:edges).selection(:node) - end + # Will return a NullSelection object if :nodes is not a selection. This + # is better than returning nil as we can continue chaining selections on + # without raising errors. + selection.selection(:nodes) end end diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb index 2a3dce80057..1268e74fd58 100644 --- a/app/graphql/resolvers/concerns/resolves_groups.rb +++ b/app/graphql/resolvers/concerns/resolves_groups.rb @@ -22,6 +22,7 @@ module ResolvesGroups custom_emoji: [:custom_emoji], full_path: [:route], path: [:route], + web_url: [:route], dependency_proxy_blob_count: [:dependency_proxy_blobs], dependency_proxy_blobs: [:dependency_proxy_blobs], dependency_proxy_image_count: [:dependency_proxy_manifests], diff --git a/app/graphql/resolvers/environments/nested_environments_resolver.rb b/app/graphql/resolvers/environments/nested_environments_resolver.rb new file mode 100644 index 00000000000..f043270beca --- /dev/null +++ b/app/graphql/resolvers/environments/nested_environments_resolver.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Resolvers + module Environments + class NestedEnvironmentsResolver < EnvironmentsResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::NestedEnvironmentType, null: true + + authorizes_object! + authorize :read_environment + + def resolve(**args) + offset_pagination(super(**args).nested) + end + end + end +end diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb index f265e2183d0..aca1a36f0f5 100644 --- a/app/graphql/resolvers/environments_resolver.rb +++ b/app/graphql/resolvers/environments_resolver.rb @@ -14,6 +14,10 @@ module Resolvers required: false, description: 'States of environments that should be included in result.' + argument :type, GraphQL::Types::String, + required: false, + description: 'Search query for environment type.' + type Types::EnvironmentType, null: true alias_method :project, :object diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb index e6a6abb39dd..ae578390fd5 100644 --- a/app/graphql/resolvers/group_packages_resolver.rb +++ b/app/graphql/resolvers/group_packages_resolver.rb @@ -13,9 +13,9 @@ module Resolvers default_value: :created_desc GROUP_SORT_TO_PARAMS_MAP = SORT_TO_PARAMS_MAP.merge({ - project_path_desc: { order_by: 'project_path', sort: 'desc' }, - project_path_asc: { order_by: 'project_path', sort: 'asc' } - }).freeze + project_path_desc: { order_by: 'project_path', sort: 'desc' }, + project_path_asc: { order_by: 'project_path', sort: 'asc' } + }).freeze def resolve(sort:, **filters) return unless packages_available? diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index e3102a7d32a..3e61ba755d8 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -12,6 +12,11 @@ module Resolvers # see app/graphql/types/issue_connection.rb type 'Types::IssueConnection', null: true + before_connection_authorization do |nodes, current_user| + projects = nodes.map(&:project) + ::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute + end + def resolve_with_lookahead(**args) return unless Feature.enabled?(:root_level_issues_query) diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb index b77c6b1112b..c565fcb70e3 100644 --- a/app/graphql/resolvers/package_details_resolver.rb +++ b/app/graphql/resolvers/package_details_resolver.rb @@ -11,6 +11,14 @@ module Resolvers description: 'Global ID of the package.' def resolve(id:) + Gitlab::Graphql::Lazy.with_value(find_object(id: id)) do |package| + package if package.default? + end + end + + private + + def find_object(id:) GitlabSchema.find_by_gid(id) end end diff --git a/app/graphql/resolvers/package_pipelines_resolver.rb b/app/graphql/resolvers/package_pipelines_resolver.rb index 9ff77f02547..7f610915489 100644 --- a/app/graphql/resolvers/package_pipelines_resolver.rb +++ b/app/graphql/resolvers/package_pipelines_resolver.rb @@ -18,7 +18,7 @@ module Resolvers # This returns a promise for a connection of promises for pipelines: # Lazy[Connection[Lazy[Pipeline]]] structure - def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:) + def resolve(lookahead:, first: nil, last: nil, after: nil, before: nil) default_value = default_value_for(first: first, last: last, after: after, before: before) BatchLoader::GraphQL.for(package.id) .batch(default_value: default_value) do |package_ids, loader| diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb index c7e9e522c25..6c4e978125e 100644 --- a/app/graphql/resolvers/paginated_tree_resolver.rb +++ b/app/graphql/resolvers/paginated_tree_resolver.rb @@ -41,7 +41,10 @@ module Resolvers next_cursor = tree.cursor&.next_cursor Gitlab::Graphql::ExternallyPaginatedArray.new(cursor, next_cursor, *tree) rescue Gitlab::Git::CommandError => e - raise Gitlab::Graphql::Errors::ArgumentError, e + raise Gitlab::Graphql::Errors::BaseError.new( + e, + extensions: { code: e.code, gitaly_code: e.status, service: e.service } + ) end def self.field_options diff --git a/app/graphql/resolvers/project_jobs_resolver.rb b/app/graphql/resolvers/project_jobs_resolver.rb index 4d13a4a3fae..2bf71679dbf 100644 --- a/app/graphql/resolvers/project_jobs_resolver.rb +++ b/app/graphql/resolvers/project_jobs_resolver.rb @@ -14,10 +14,18 @@ module Resolvers required: false, description: 'Filter jobs by status.' + argument :with_artifacts, ::GraphQL::Types::Boolean, + required: false, + description: 'Filter by artifacts presence.' + alias_method :project, :object - def resolve_with_lookahead(statuses: nil) - jobs = ::Ci::JobsFinder.new(current_user: current_user, project: project, params: { scope: statuses }).execute + def resolve_with_lookahead(statuses: nil, with_artifacts: nil) + jobs = ::Ci::JobsFinder.new( + current_user: current_user, project: project, params: { + scope: statuses, with_artifacts: with_artifacts + } + ).execute apply_lookahead(jobs) end @@ -26,7 +34,7 @@ module Resolvers def preloads { - previous_stage_jobs_and_needs: [:needs, :pipeline], + previous_stage_jobs_or_needs: [:needs, :pipeline], artifacts: [:job_artifacts], pipeline: [:user] } diff --git a/app/graphql/resolvers/projects/fork_details_resolver.rb b/app/graphql/resolvers/projects/fork_details_resolver.rb new file mode 100644 index 00000000000..fcc13a1bc1e --- /dev/null +++ b/app/graphql/resolvers/projects/fork_details_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class ForkDetailsResolver < BaseResolver + type Types::Projects::ForkDetailsType, null: true + + argument :ref, GraphQL::Types::String, + required: false, + description: 'Ref of the fork. Default value is HEAD.' + + alias_method :project, :object + + def resolve(**args) + return unless project.forked? + + ::Projects::Forks::DivergenceCounts.new(project, args[:ref]).counts + end + end + end +end diff --git a/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb b/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb new file mode 100644 index 00000000000..b40d85e8003 --- /dev/null +++ b/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Resolvers + module WorkItems + class WorkItemDiscussionsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + extension Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension + + authorize :read_work_item + authorizes_object! + + # this resolver may be calling gitaly as part of parsing notes that contain commit references + calls_gitaly! + + alias_method :notes_widget, :object + + argument :filter, Types::WorkItems::NotesFilterTypeEnum, + required: false, + default_value: Types::WorkItems::NotesFilterTypeEnum.default_value, + description: 'Type of notes collection: ALL_NOTES, ONLY_COMMENTS, ONLY_ACTIVITY.' + + type Types::Notes::DiscussionType.connection_type, null: true + + def resolve(**args) + finder = Issuable::DiscussionsListService.new(current_user, work_item, params(args)) + + Gitlab::Graphql::ExternallyPaginatedArray.new( + finder.paginator.cursor_for_previous_page, + finder.paginator.cursor_for_next_page, + *finder.execute + ) + end + + def self.field_options + # we manage the pagination manually through external array, so opt out of the connection field extension + super.merge(connection: false) + end + + def self.calculate_ext_conn_complexity + true + end + + def self.complexity_multiplier(args) + 0.05 + end + + private + + def work_item + notes_widget.work_item + end + strong_memoize_attr :work_item + + def params(args) + { + notes_filter: args[:filter], + cursor: args[:after], + per_page: self.class.nodes_limit(args, @field, context: context) + } + end + + def self.nodes_limit(args, field, **kwargs) + page_size = field&.max_page_size || kwargs[:context]&.schema&.default_max_page_size + [args[:first], page_size].compact.min + end + end + end +end diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb index 42f4f99d4a9..a3de875c196 100644 --- a/app/graphql/resolvers/work_items_resolver.rb +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -55,6 +55,7 @@ module Resolvers last_edited_by: :last_edited_by, assignees: :assignees, parent: :work_item_parent, + children: { work_item_children: [:author, { project: :project_feature }] }, labels: :labels, milestone: :milestone } diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb index a0d19229d3d..a13453f9194 100644 --- a/app/graphql/types/alert_management/alert_type.rb +++ b/app/graphql/types/alert_management/alert_type.rb @@ -13,6 +13,11 @@ module Types authorize :read_alert_management_alert + field :id, + GraphQL::Types::ID, + null: false, + description: 'ID of the alert.' + field :iid, GraphQL::Types::ID, null: false, @@ -116,7 +121,10 @@ module Types null: true, description: 'Runbook for the alert as defined in alert details.' - field :todos, description: 'To-do items of the current user for the alert.', resolver: Resolvers::TodosResolver + field :todos, + Types::TodoType.connection_type, + description: 'To-do items of the current user for the alert.', + resolver: Resolvers::TodosResolver field :details_url, GraphQL::Types::String, diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 36ba3399754..615c143a0b9 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -135,15 +135,16 @@ module Types :resolver_complexity, args, child_complexity: child_complexity ).to_i complexity += 1 if calls_gitaly? - complexity += complexity * connection_complexity_multiplier(ctx, args) + ext_conn = resolver&.try(:calculate_ext_conn_complexity) + complexity += complexity * connection_complexity_multiplier(ctx, args, calculate_ext_conn_complexity: ext_conn) complexity.to_i end end - def connection_complexity_multiplier(ctx, args) + def connection_complexity_multiplier(ctx, args, calculate_ext_conn_complexity:) # Resolvers may add extra complexity depending on number of items being loaded. - return 0 unless connection? + return 0 if !connection? && !calculate_ext_conn_complexity page_size = max_page_size || ctx.schema.default_max_page_size limit_value = [args[:first], args[:last], page_size].compact.min diff --git a/app/graphql/types/ci/config_variable_type.rb b/app/graphql/types/ci/config_variable_type.rb index 5b5890fd5a5..020af5b2444 100644 --- a/app/graphql/types/ci/config_variable_type.rb +++ b/app/graphql/types/ci/config_variable_type.rb @@ -19,6 +19,7 @@ module Types description: 'Value of the variable.' field :value_options, [GraphQL::Types::String], + hash_key: :options, null: true, description: 'Value options for the variable.' end diff --git a/app/graphql/types/ci/freeze_period_status_enum.rb b/app/graphql/types/ci/freeze_period_status_enum.rb new file mode 100644 index 00000000000..aebd0f537e9 --- /dev/null +++ b/app/graphql/types/ci/freeze_period_status_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Ci + class FreezePeriodStatusEnum < BaseEnum + graphql_name 'CiFreezePeriodStatus' + description 'Deploy freeze period status' + + value 'ACTIVE', value: :active, description: 'Freeze period is active.' + value 'INACTIVE', value: :inactive, description: 'Freeze period is inactive.' + end + end +end diff --git a/app/graphql/types/ci/freeze_period_type.rb b/app/graphql/types/ci/freeze_period_type.rb new file mode 100644 index 00000000000..6a3f2ed8fa4 --- /dev/null +++ b/app/graphql/types/ci/freeze_period_type.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module Ci + class FreezePeriodType < BaseObject + graphql_name 'CiFreezePeriod' + description 'Represents a deployment freeze window of a project' + + authorize :read_freeze_period + + present_using ::Ci::FreezePeriodPresenter + + field :status, Types::Ci::FreezePeriodStatusEnum, + description: 'Freeze period status.', + null: false + + field :start_cron, GraphQL::Types::String, + description: 'Start of the freeze period in cron format.', + null: false, + method: :freeze_start + + field :end_cron, GraphQL::Types::String, + description: 'End of the freeze period in cron format.', + null: false, + method: :freeze_end + + field :cron_timezone, GraphQL::Types::String, + description: 'Time zone for the cron fields, defaults to UTC if not provided.', + null: true + + field :start_time, Types::TimeType, + description: 'Timestamp (UTC) of when the current/next active period starts.', + null: true + + field :end_time, Types::TimeType, + description: 'Timestamp (UTC) of when the current/next active period ends.', + null: true, + method: :time_end_from_now + end + end +end diff --git a/app/graphql/types/ci/pipeline_schedule_type.rb b/app/graphql/types/ci/pipeline_schedule_type.rb index 04f9fc78a92..904fa3f1c72 100644 --- a/app/graphql/types/ci/pipeline_schedule_type.rb +++ b/app/graphql/types/ci/pipeline_schedule_type.rb @@ -5,6 +5,8 @@ module Types class PipelineScheduleType < BaseObject graphql_name 'PipelineSchedule' + description 'Represents a pipeline schedule' + connection_type_class(Types::CountableConnectionType) expose_permissions Types::PermissionTypes::Ci::PipelineSchedules @@ -17,7 +19,9 @@ module Types field :owner, ::Types::UserType, null: false, description: 'Owner of the pipeline schedule.' - field :active, GraphQL::Types::Boolean, null: false, description: 'Indicates if a pipeline schedule is active.' + field :active, GraphQL::Types::Boolean, null: false, description: 'Indicates if the pipeline schedule is active.' + + field :project, ::Types::ProjectType, null: true, description: 'Project of the pipeline schedule.' field :next_run_at, Types::TimeType, null: false, description: 'Time when the next pipeline will run.' @@ -26,20 +30,50 @@ module Types field :last_pipeline, PipelineType, null: true, description: 'Last pipeline object.' field :ref_for_display, GraphQL::Types::String, - null: true, description: 'Git ref for the pipeline schedule.', method: :ref_for_display - - field :ref_path, GraphQL::Types::String, null: true, description: 'Path to the ref that triggered the pipeline.' + null: true, description: 'Git ref for the pipeline schedule.' field :for_tag, GraphQL::Types::Boolean, null: false, description: 'Indicates if a pipelines schedule belongs to a tag.', method: :for_tag? - field :cron, GraphQL::Types::String, null: false, description: 'Cron notation for the schedule.' + field :edit_path, GraphQL::Types::String, + null: true, + description: 'Edit path of the pipeline schedule.', + authorize: :update_pipeline_schedule + + field :variables, + Types::Ci::PipelineScheduleVariableType.connection_type, + null: true, + description: 'Pipeline schedule variables.', + authorize: :read_pipeline_schedule_variables + + field :ref, GraphQL::Types::String, + null: true, description: 'Ref of the pipeline schedule.', method: :ref_for_display + + field :ref_path, GraphQL::Types::String, + null: true, + description: 'Path to the ref that triggered the pipeline.' - field :cron_timezone, GraphQL::Types::String, null: false, description: 'Timezone for the pipeline schedule.' + field :cron, GraphQL::Types::String, + null: false, + description: 'Cron notation for the schedule.' + + field :cron_timezone, GraphQL::Types::String, + null: false, + description: 'Timezone for the pipeline schedule.' + + field :created_at, Types::TimeType, + null: false, description: 'Timestamp of when the pipeline schedule was created.' + + field :updated_at, Types::TimeType, + null: false, description: 'Timestamp of when the pipeline schedule was last updated.' def ref_path ::Gitlab::Routing.url_helpers.project_commits_path(object.project, object.ref_for_display) end + + def edit_path + ::Gitlab::Routing.url_helpers.edit_project_pipeline_schedule_path(object.project, object) + end end end end diff --git a/app/graphql/types/ci/pipeline_schedule_variable_type.rb b/app/graphql/types/ci/pipeline_schedule_variable_type.rb new file mode 100644 index 00000000000..1cb407bc2e4 --- /dev/null +++ b/app/graphql/types/ci/pipeline_schedule_variable_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Ci + class PipelineScheduleVariableType < BaseObject + graphql_name 'PipelineScheduleVariable' + + authorize :read_pipeline_schedule_variables + + implements(VariableInterface) + end + end +end diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 4a523f2edd9..cb561f48b3b 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -78,7 +78,7 @@ module Types resolver: Resolvers::Ci::PipelineStagesResolver field :user, - type: Types::UserType, + type: 'Types::UserType', null: true, description: 'Pipeline user.' diff --git a/app/graphql/types/ci/runner_job_execution_status_enum.rb b/app/graphql/types/ci/runner_job_execution_status_enum.rb new file mode 100644 index 00000000000..686ea085199 --- /dev/null +++ b/app/graphql/types/ci/runner_job_execution_status_enum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + module Ci + class RunnerJobExecutionStatusEnum < BaseEnum + graphql_name 'CiRunnerJobExecutionStatus' + + value 'IDLE', + description: "Runner is idle.", + value: :idle, + deprecated: { milestone: '15.7', reason: :alpha } + + value 'RUNNING', + description: 'Runner is executing jobs.', + value: :running, + deprecated: { milestone: '15.7', reason: :alpha } + end + end +end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index a9c76974850..5d34906f7b8 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -23,6 +23,9 @@ module Types deprecated: { reason: 'Use paused', milestone: '14.8' } field :admin_url, GraphQL::Types::String, null: true, description: 'Admin URL of the runner. Only available for administrators.' + field :architecture_name, GraphQL::Types::String, null: true, + description: 'Architecture provided by the the runner.', + method: :architecture field :contacted_at, Types::TimeType, null: true, description: 'Timestamp of last contact from this runner.', method: :contacted_at @@ -35,32 +38,39 @@ module Types field :executor_name, GraphQL::Types::String, null: true, description: 'Executor last advertised by the runner.', method: :executor_name - field :platform_name, GraphQL::Types::String, null: true, - description: 'Platform provided by the runner.', - method: :platform - field :architecture_name, GraphQL::Types::String, null: true, - description: 'Architecture provided by the the runner.', - method: :architecture - field :maintenance_note, GraphQL::Types::String, null: true, - description: 'Runner\'s maintenance notes.' - field :groups, ::Types::GroupType.connection_type, null: true, - description: 'Groups the runner is associated with. For group runners only.' + field :groups, 'Types::GroupConnection', + null: true, + resolver: ::Resolvers::Ci::RunnerGroupsResolver, + description: 'Groups the runner is associated with. For group runners only.' field :id, ::Types::GlobalIDType[::Ci::Runner], null: false, description: 'ID of the runner.' field :ip_address, GraphQL::Types::String, null: true, description: 'IP address of the runner.' field :job_count, GraphQL::Types::Int, null: true, description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)." + field :job_execution_status, + Types::Ci::RunnerJobExecutionStatusEnum, + null: true, + description: 'Job execution status of the runner.', + deprecated: { milestone: '15.7', reason: :alpha } 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, resolver: ::Resolvers::Ci::RunnerJobsResolver field :locked, GraphQL::Types::Boolean, null: true, description: 'Indicates the runner is locked.' + field :maintenance_note, GraphQL::Types::String, null: true, + description: 'Runner\'s maintenance notes.' 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, + description: 'Project that owns the runner. For project runners only.', + resolver: ::Resolvers::Ci::RunnerOwnerProjectResolver field :paused, GraphQL::Types::Boolean, null: false, description: 'Indicates the runner is paused and not available to run jobs.' + field :platform_name, GraphQL::Types::String, null: true, + description: 'Platform provided by the runner.', + method: :platform field :project_count, GraphQL::Types::Int, null: true, description: 'Number of projects that the runner is associated with.' field :projects, @@ -88,9 +98,6 @@ module Types method: :token_expires_at field :version, GraphQL::Types::String, null: true, description: 'Version of the runner.' - field :owner_project, ::Types::ProjectType, null: true, - description: 'Project that owns the runner. For project runners only.', - resolver: ::Resolvers::Ci::RunnerOwnerProjectResolver markdown_field :maintenance_note_html, null: true @@ -99,8 +106,25 @@ module Types end def job_count - # We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT - runner.builds.limit(JOB_COUNT_LIMIT + 1).count + BatchLoader::GraphQL.for(runner.id).batch(key: :job_count) do |runner_ids, loader, _args| + # rubocop: disable CodeReuse/ActiveRecord + # We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT + builds_tbl = ::Ci::Build.arel_table + runners_tbl = ::Ci::Runner.arel_table + lateral_query = ::Ci::Build.select(1) + .where(builds_tbl['runner_id'].eq(runners_tbl['id'])) + .limit(JOB_COUNT_LIMIT + 1) + counts = ::Ci::Runner.joins("JOIN LATERAL (#{lateral_query.to_sql}) builds_with_limit ON true") + .id_in(runner_ids) + .select(:id, Arel.star.count.as('count')) + .group(:id) + .index_by(&:id) + # rubocop: enable CodeReuse/ActiveRecord + + runner_ids.each do |runner_id| + loader.call(runner_id, counts[runner_id]&.count || 0) + end + end end def admin_url @@ -111,14 +135,13 @@ module Types Gitlab::Routing.url_helpers.edit_admin_runner_url(runner) if can_admin_runners? end - # rubocop: disable CodeReuse/ActiveRecord def project_count BatchLoader::GraphQL.for(runner.id).batch(key: :runner_project_count) do |ids, loader, args| counts = ::Ci::Runner.project_type .select(:id, 'COUNT(ci_runner_projects.id) as count') .left_outer_joins(:runner_projects) - .where(id: ids) - .group(:id) + .id_in(ids) + .group(:id) # rubocop: disable CodeReuse/ActiveRecord .index_by(&:id) ids.each do |id| @@ -126,12 +149,15 @@ module Types end end end - # rubocop: enable CodeReuse/ActiveRecord - def groups - return unless runner.group_type? + def job_execution_status + BatchLoader::GraphQL.for(runner.id).batch(key: :running_builds_exist) do |runner_ids, loader| + statuses = ::Ci::Runner.id_in(runner_ids).with_running_builds.index_by(&:id) - batched_owners(::Ci::RunnerNamespace, Group, :runner_groups, :namespace_id) + runner_ids.each do |runner_id| + loader.call(runner_id, statuses[runner_id] ? :running : :idle) + end + end end private @@ -139,29 +165,6 @@ module Types def can_admin_runners? context[:current_user]&.can_admin_all_resources? end - - # rubocop: disable CodeReuse/ActiveRecord - def batched_owners(runner_assoc_type, assoc_type, key, column_name) - BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader| - plucked_runner_and_owner_ids = runner_assoc_type - .select(:runner_id, column_name) - .where(runner_id: runner_ids) - .pluck(:runner_id, column_name) - # In plucked_runner_and_owner_ids, first() represents the runner ID, and second() the owner ID, - # so let's group the owner IDs by runner ID - runner_owner_ids_by_runner_id = plucked_runner_and_owner_ids - .group_by(&:first) - .transform_values { |runner_and_owner_id| runner_and_owner_id.map(&:second) } - - owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq - owners = assoc_type.where(id: owner_ids).index_by(&:id) - - runner_ids.each do |runner_id| - loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || []) - end - end - end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/graphql/types/commit_signature_interface.rb b/app/graphql/types/commit_signature_interface.rb index 6b0c16e538a..0449a0634ef 100644 --- a/app/graphql/types/commit_signature_interface.rb +++ b/app/graphql/types/commit_signature_interface.rb @@ -21,7 +21,8 @@ module Types description: 'Project of the associated commit.' orphan_types Types::CommitSignatures::GpgSignatureType, - Types::CommitSignatures::X509SignatureType + Types::CommitSignatures::X509SignatureType, + Types::CommitSignatures::SshSignatureType def self.resolve_type(object, context) case object @@ -29,6 +30,8 @@ module Types Types::CommitSignatures::GpgSignatureType when ::CommitSignatures::X509CommitSignature Types::CommitSignatures::X509SignatureType + when ::CommitSignatures::SshSignature + Types::CommitSignatures::SshSignatureType else raise 'Unsupported commit signature type' end diff --git a/app/graphql/types/commit_signatures/gpg_signature_type.rb b/app/graphql/types/commit_signatures/gpg_signature_type.rb index 2a845fff3e2..3baf2d9d21d 100644 --- a/app/graphql/types/commit_signatures/gpg_signature_type.rb +++ b/app/graphql/types/commit_signatures/gpg_signature_type.rb @@ -11,6 +11,7 @@ module Types authorize :download_code field :user, Types::UserType, null: true, + method: :signed_by_user, description: 'User associated with the key.' field :gpg_key_user_name, GraphQL::Types::String, diff --git a/app/graphql/types/commit_signatures/ssh_signature_type.rb b/app/graphql/types/commit_signatures/ssh_signature_type.rb new file mode 100644 index 00000000000..92eb4f7949a --- /dev/null +++ b/app/graphql/types/commit_signatures/ssh_signature_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Types + module CommitSignatures + class SshSignatureType < Types::BaseObject + graphql_name 'SshSignature' + description 'SSH signature for a signed commit' + + implements Types::CommitSignatureInterface + + authorize :download_code + + 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.' + end + end +end diff --git a/app/graphql/types/commit_signatures/x509_signature_type.rb b/app/graphql/types/commit_signatures/x509_signature_type.rb index 9ac96dbc015..2d58c3d5b5d 100644 --- a/app/graphql/types/commit_signatures/x509_signature_type.rb +++ b/app/graphql/types/commit_signatures/x509_signature_type.rb @@ -11,6 +11,7 @@ module Types authorize :download_code field :user, Types::UserType, null: true, + method: :signed_by_user, calls_gitaly: true, description: 'User associated with the key.' diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb index cb818ac5e92..dfa599e798c 100644 --- a/app/graphql/types/container_repository_type.rb +++ b/app/graphql/types/container_repository_type.rb @@ -13,6 +13,7 @@ module Types field :expiration_policy_cleanup_status, Types::ContainerRepositoryCleanupStatusEnum, null: true, description: 'Tags cleanup status for the container repository.' field :expiration_policy_started_at, Types::TimeType, null: true, description: 'Timestamp when the cleanup done by the expiration policy was started on the container repository.' field :id, GraphQL::Types::ID, null: false, description: 'ID of the container repository.' + field :last_cleanup_deleted_tags_count, GraphQL::Types::Int, null: true, description: 'Number of deleted tags from the last cleanup.' field :location, GraphQL::Types::String, null: false, description: 'URL of the container repository.' field :migration_state, GraphQL::Types::String, null: false, description: 'Migration state of the container repository.' field :name, GraphQL::Types::String, null: false, description: 'Name of the container repository.' @@ -21,7 +22,6 @@ module Types field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.' field :tags_count, GraphQL::Types::Int, null: false, description: 'Number of tags associated with this image.' field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.' - field :last_cleanup_deleted_tags_count, GraphQL::Types::Int, null: true, description: 'Number of deleted tags from the last cleanup.' def can_delete Ability.allowed?(current_user, :update_container_image, object) diff --git a/app/graphql/types/dependency_proxy/manifest_type.rb b/app/graphql/types/dependency_proxy/manifest_type.rb index f7e751e30d3..53b7610e490 100644 --- a/app/graphql/types/dependency_proxy/manifest_type.rb +++ b/app/graphql/types/dependency_proxy/manifest_type.rb @@ -14,11 +14,11 @@ module Types field :id, ::Types::GlobalIDType[::DependencyProxy::Manifest], null: false, description: 'ID of the manifest.' field :image_name, GraphQL::Types::String, null: false, description: 'Name of the image.' field :size, GraphQL::Types::String, null: false, description: 'Size of the manifest file.' - field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.' field :status, Types::DependencyProxy::ManifestTypeEnum, null: false, description: "Status of the manifest (#{::DependencyProxy::Manifest.statuses.keys.join(', ')})" + field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.' def image_name object.file_name.chomp(File.extname(object.file_name)) diff --git a/app/graphql/types/deployment_details_type.rb b/app/graphql/types/deployment_details_type.rb deleted file mode 100644 index bbb5cc8e3f1..00000000000 --- a/app/graphql/types/deployment_details_type.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Types - class DeploymentDetailsType < DeploymentType - graphql_name 'DeploymentDetails' - description 'The details of the deployment' - authorize :read_deployment - present_using ::Deployments::DeploymentPresenter - - field :tags, - [Types::DeploymentTagType], - description: 'Git tags that contain this deployment.', - calls_gitaly: true - end -end - -Types::DeploymentDetailsType.prepend_mod_with('Types::DeploymentDetailsType') diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb index 59b59dc4e1d..1c23fd44ea1 100644 --- a/app/graphql/types/deployment_type.rb +++ b/app/graphql/types/deployment_type.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true module Types - # If you're considering to add a new field in DeploymentType, please follow this guideline: - # - If the field is preloadable in batch, define it in DeploymentType. - # In this case, you should extend DeploymentsResolver logic to preload the field. Also, add a new test that - # fetching the specific field for multiple deployments doesn't cause N+1 query problem. - # - If the field is NOT preloadable in batch, define it in DeploymentDetailsType. - # This type can be only fetched for a single deployment, so you don't need to take care of the preloading. class DeploymentType < BaseObject graphql_name 'Deployment' description 'The deployment of an environment' @@ -15,6 +9,8 @@ module Types authorize :read_deployment + expose_permissions Types::PermissionTypes::Deployment + field :id, GraphQL::Types::ID, description: 'Global ID of the deployment.' @@ -65,5 +61,15 @@ module Types Types::UserType, description: 'User who executed the deployment.', method: :deployed_by + + field :tags, + [Types::DeploymentTagType], + description: 'Git tags that contain this deployment. ' \ + 'This field can only be resolved for one deployment in any single request.', + calls_gitaly: true do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end end end + +Types::DeploymentType.prepend_mod_with('Types::DeploymentType') diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index dd2286d333d..5f58fc38540 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -9,6 +9,12 @@ module Types authorize :read_environment + expose_permissions Types::PermissionTypes::Environment, + description: 'Permissions for the current user on the resource. '\ + 'This field can only be resolved for one environment in any single request.' do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + field :name, GraphQL::Types::String, null: false, description: 'Human-readable name of the environment.' @@ -67,6 +73,11 @@ module Types description: 'Last deployment of the environment.', resolver: Resolvers::Environments::LastDeploymentResolver + field :deploy_freezes, + [Types::Ci::FreezePeriodType], + null: true, + description: 'Deployment freeze periods of the environment.' + def tier object.tier.to_sym end diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb index a71c2fb0e6c..7ebd98ff2e7 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -49,9 +49,7 @@ module Types An example `#{graphql_name}` is: `"#{::Gitlab::GlobalId.build(model_name: model_name, id: 1)}"`. #{ if deprecation = Gitlab::GlobalId::Deprecations.deprecation_by(model_name) - 'The older format `"' + - ::Gitlab::GlobalId.build(model_name: deprecation.old_name, id: 1).to_s + - '"` was deprecated in ' + deprecation.milestone + '.' + "The older format `\"#{::Gitlab::GlobalId.build(model_name: deprecation.old_name, id: 1)}\"` was deprecated in #{deprecation.milestone}." end} MD diff --git a/app/graphql/types/group_connection.rb b/app/graphql/types/group_connection.rb new file mode 100644 index 00000000000..e4332e24302 --- /dev/null +++ b/app/graphql/types/group_connection.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Normally this wouldn't be needed and we could use +# +# type Types::GroupType.connection_type, null: true +# +# in a resolver. However we can end up with cyclic definitions. +# Running the spec locally can result in errors like +# +# NameError: uninitialized constant Types::GroupType +# +# or other errors. To fix this, we created this file and use +# +# type "Types::GroupConnection", null: true +# +# which gives a delayed resolution, and the proper connection type. +# +# See gitlab/app/graphql/types/ci/runner_type.rb +# Reference: https://github.com/rmosolgo/graphql-ruby/issues/3974#issuecomment-1084444214 +# and https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#testing-tips-and-tricks +# +Types::GroupConnection = Types::GroupType.connection_type diff --git a/app/graphql/types/issue_type_enum.rb b/app/graphql/types/issue_type_enum.rb index 78cd27f60c3..d7f587ff03d 100644 --- a/app/graphql/types/issue_type_enum.rb +++ b/app/graphql/types/issue_type_enum.rb @@ -16,5 +16,9 @@ module Types value 'OBJECTIVE', value: 'objective', description: 'Objective issue type. Available only when feature flag `okrs_mvc` is enabled.', alpha: { milestone: '15.6' } + + value 'KEY_RESULT', value: 'key_result', + description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.', + alpha: { milestone: '15.7' } end end diff --git a/app/graphql/types/key_type.rb b/app/graphql/types/key_type.rb new file mode 100644 index 00000000000..30699793045 --- /dev/null +++ b/app/graphql/types/key_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + class KeyType < Types::BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'Key' + description 'Represents an SSH key.' + + field :created_at, Types::TimeType, null: false, + description: 'Timestamp of when the key was created.' + field :expires_at, Types::TimeType, null: false, + description: "Timestamp of when the key expires. It's null if it never expires." + field :id, GraphQL::Types::ID, null: false, description: 'ID of the key.' + field :key, GraphQL::Types::String, null: false, method: :publishable_key, + description: 'Public key of the key pair.' + field :title, GraphQL::Types::String, null: false, description: 'Title of the key.' + end +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 49bf7aa638c..abf7b3ad530 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -200,10 +200,10 @@ module Types description: 'Array of available auto merge strategies.' field :commits, Types::CommitType.connection_type, null: true, calls_gitaly: true, description: 'Merge request commits.' - field :committers, Types::UserType.connection_type, null: true, complexity: 5, - calls_gitaly: true, description: 'Users who have added commits to the merge request.' field :commits_without_merge_commits, Types::CommitType.connection_type, null: true, calls_gitaly: true, description: 'Merge request commits excluding merge commits.' + field :committers, Types::UserType.connection_type, null: true, complexity: 5, + calls_gitaly: true, description: 'Users who have added commits to the merge request.' field :has_ci, GraphQL::Types::Boolean, null: false, method: :has_ci?, description: 'Indicates if the merge request has CI.' field :merge_user, Types::UserType, null: true, diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 1cbb2ede544..b342e57804b 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -63,6 +63,8 @@ module Types mount_mutation Mutations::Issues::SetEscalationStatus mount_mutation Mutations::Issues::Update mount_mutation Mutations::Issues::Move + mount_mutation Mutations::Issues::LinkAlerts + mount_mutation Mutations::Issues::UnlinkAlert mount_mutation Mutations::Labels::Create mount_mutation Mutations::MergeRequests::Accept mount_mutation Mutations::MergeRequests::Create @@ -117,6 +119,8 @@ module Types mount_mutation Mutations::Ci::Pipeline::Retry mount_mutation Mutations::Ci::PipelineSchedule::Delete mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership + mount_mutation Mutations::Ci::PipelineSchedule::Play + mount_mutation Mutations::Ci::PipelineSchedule::Create mount_mutation Mutations::Ci::CiCdSettingsUpdate, deprecated: { reason: :renamed, replacement: 'ProjectCiCdSettingsUpdate', diff --git a/app/graphql/types/nested_environment_type.rb b/app/graphql/types/nested_environment_type.rb new file mode 100644 index 00000000000..b835af2bf45 --- /dev/null +++ b/app/graphql/types/nested_environment_type.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class NestedEnvironmentType < BaseObject + graphql_name 'NestedEnvironment' + description 'Describes where code is deployed for a project organized by folder.' + + field :name, GraphQL::Types::String, + null: false, description: 'Human-readable name of the environment.' + + field :size, GraphQL::Types::Int, + null: false, description: 'Number of environments nested in the folder.' + + field :environment, + Types::EnvironmentType, + null: true, description: 'Latest environment in the folder.' + + def environment + BatchLoader::GraphQL.for(object.last_id).batch do |environment_ids, loader| + Environment.id_in(environment_ids).each do |environment| + loader.call(environment.id, environment) + end + end + end + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb index eef5ce40bde..05629ea9223 100644 --- a/app/graphql/types/notes/note_type.rb +++ b/app/graphql/types/notes/note_type.rb @@ -77,6 +77,14 @@ module Types def author Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find end + + # We now support also SyntheticNote notes as a NoteType, but SyntheticNote does not have a real note ID, + # as SyntheticNote is generated dynamically from a ResourceEvent instance. + def id + return super unless object.is_a?(SyntheticNote) + + ::Gitlab::GlobalId.build(object, model_name: object.class.to_s, id: object.discussion_id) + end end end end diff --git a/app/graphql/types/packages/package_links_type.rb b/app/graphql/types/packages/package_links_type.rb index f16937530b9..eb29fb655bd 100644 --- a/app/graphql/types/packages/package_links_type.rb +++ b/app/graphql/types/packages/package_links_type.rb @@ -12,6 +12,8 @@ module Types field :web_path, GraphQL::Types::String, null: true, description: 'Path to the package details page.' def web_path + return unless object.default? + package_path(object) 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 07e6e7a55d6..0192af25d0f 100644 --- a/app/graphql/types/permission_types/base_permission_type.rb +++ b/app/graphql/types/permission_types/base_permission_type.rb @@ -11,20 +11,20 @@ module Types abilities.each { |ability| ability_field(ability) } end - def self.ability_field(ability, **kword_args) + def self.ability_field(ability, **kword_args, &block) define_field_resolver_method(ability) unless resolving_keywords?(kword_args) - permission_field(ability, **kword_args) + permission_field(ability, **kword_args, &block) end - def self.permission_field(name, **kword_args) + def self.permission_field(name, **kword_args, &block) kword_args = kword_args.reverse_merge( name: name, type: GraphQL::Types::Boolean, description: "Indicates the user can perform `#{name}` on this resource", null: false) - field(**kword_args) # rubocop:disable Graphql/Descriptions + field(**kword_args, &block) # rubocop:disable Graphql/Descriptions end def self.define_field_resolver_method(ability) diff --git a/app/graphql/types/permission_types/deployment.rb b/app/graphql/types/permission_types/deployment.rb new file mode 100644 index 00000000000..fce376552b1 --- /dev/null +++ b/app/graphql/types/permission_types/deployment.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Deployment < BasePermissionType + graphql_name 'DeploymentPermissions' + + abilities :destroy_deployment + ability_field :update_deployment, calls_gitaly: true + end + end +end + +Types::PermissionTypes::Deployment.prepend_mod_with('Types::PermissionTypes::Deployment') diff --git a/app/graphql/types/permission_types/environment.rb b/app/graphql/types/permission_types/environment.rb new file mode 100644 index 00000000000..59c9fce64e5 --- /dev/null +++ b/app/graphql/types/permission_types/environment.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class Environment < BasePermissionType + graphql_name 'EnvironmentPermissions' + + abilities :update_environment, :destroy_environment, :stop_environment + end + end +end diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index f6a5563d367..c833b512222 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -17,7 +17,8 @@ module Types :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content, :admin_operations, - :read_merge_request, :read_design, :create_design, :destroy_design + :read_merge_request, :read_design, :create_design, :destroy_design, + :read_environment permission_field :create_snippet diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb index c43baf1280b..a1d721856a9 100644 --- a/app/graphql/types/project_statistics_type.rb +++ b/app/graphql/types/project_statistics_type.rb @@ -11,6 +11,10 @@ module Types field :build_artifacts_size, GraphQL::Types::Float, null: false, description: 'Build artifacts size of the project in bytes.' + field :container_registry_size, + GraphQL::Types::Float, + null: true, + description: 'Container Registry size of the project in bytes.' field :lfs_objects_size, GraphQL::Types::Float, null: false, @@ -29,9 +33,5 @@ module Types description: 'Uploads size of the project in bytes.' field :wiki_size, GraphQL::Types::Float, null: true, description: 'Wiki size of the project in bytes.' - field :container_registry_size, - GraphQL::Types::Float, - null: true, - description: 'Container Registry size of the project in bytes.' end end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 771dad00fb3..fe13ee7ef3c 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -258,8 +258,11 @@ module Types field :environments, Types::EnvironmentType.connection_type, null: true, - description: 'Environments of the project.', - resolver: Resolvers::EnvironmentsResolver + description: 'Environments of the project. ' \ + 'This field can only be resolved for one project in any single request.', + resolver: Resolvers::EnvironmentsResolver do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end field :environment, Types::EnvironmentType, @@ -267,8 +270,18 @@ module Types description: 'A single environment of the project.', resolver: Resolvers::EnvironmentsResolver.single + field :nested_environments, + Types::NestedEnvironmentType.connection_type, + null: true, + calls_gitaly: true, + description: 'Environments for this project with nested folders, ' \ + 'can only be resolved for one project in any single request', + resolver: Resolvers::Environments::NestedEnvironmentsResolver do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + field :deployment, - Types::DeploymentDetailsType, + Types::DeploymentType, null: true, description: 'Details of the deployment of the project.', resolver: Resolvers::DeploymentResolver.single @@ -526,6 +539,13 @@ module Types resolver: Resolvers::Projects::ForkTargetsResolver, description: 'Namespaces in which the current user can fork the project into.' + field :fork_details, Types::Projects::ForkDetailsType, + calls_gitaly: true, + alpha: { milestone: '15.7' }, + authorize: :read_code, + resolver: Resolvers::Projects::ForkDetailsResolver, + description: 'Details of the fork project compared to its upstream project.' + field :branch_rules, Types::Projects::BranchRuleType.connection_type, null: true, @@ -537,6 +557,11 @@ module Types description: "Programming languages used in the project.", calls_gitaly: true + field :runners, Types::Ci::RunnerType.connection_type, + null: true, + resolver: ::Resolvers::Ci::ProjectRunnersResolver, + description: "Find runners visible to the current user." + def timelog_categories object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories) end diff --git a/app/graphql/types/projects/fork_details_type.rb b/app/graphql/types/projects/fork_details_type.rb new file mode 100644 index 00000000000..88c17d89620 --- /dev/null +++ b/app/graphql/types/projects/fork_details_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Projects + # rubocop: disable Graphql/AuthorizeTypes + class ForkDetailsType < BaseObject + graphql_name 'ForkDetails' + description 'Details of the fork project compared to its upstream project.' + + field :ahead, GraphQL::Types::Int, + null: true, + description: 'Number of commits ahead of upstream.' + + field :behind, GraphQL::Types::Int, + null: true, + description: 'Number of commits behind upstream.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 21cb3f9e06c..7263f792bae 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -67,7 +67,7 @@ module Types end field :package, - description: 'Find a package. This field can only be resolved for one query in any single request.', + 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 field :user, Types::UserType, diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb index a20e53ad1bd..8516256b433 100644 --- a/app/graphql/types/release_type.rb +++ b/app/graphql/types/release_type.rb @@ -13,9 +13,6 @@ module Types present_using ReleasePresenter - field :id, ::Types::GlobalIDType[Release], - null: false, - description: 'Global ID of the release.' field :assets, Types::ReleaseAssetsType, null: true, method: :itself, description: 'Assets of the release.' field :created_at, Types::TimeType, null: true, @@ -26,6 +23,11 @@ module Types description: 'Description (also known as "release notes") of the release.' field :evidences, Types::EvidenceType.connection_type, null: true, description: 'Evidence for the release.' + field :historical_release, GraphQL::Types::Boolean, null: true, method: :historical_release?, + description: 'Indicates the release is an historical release.' + field :id, ::Types::GlobalIDType[Release], + null: false, + description: 'Global ID of the release.' field :links, Types::ReleaseLinksType, null: true, method: :itself, description: 'Links of the release.' field :milestones, Types::MilestoneType.connection_type, null: true, @@ -42,8 +44,6 @@ module Types authorize: :read_code field :upcoming_release, GraphQL::Types::Boolean, null: true, method: :upcoming_release?, description: 'Indicates the release is an upcoming release.' - field :historical_release, GraphQL::Types::Boolean, null: true, method: :historical_release?, - description: 'Indicates the release is an historical release.' field :author, Types::UserType, null: true, description: 'User that created the release.' diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb index b1b712aab38..64aaf3e73a0 100644 --- a/app/graphql/types/root_storage_statistics_type.rb +++ b/app/graphql/types/root_storage_statistics_type.rb @@ -7,6 +7,7 @@ module Types authorize :read_statistics field :build_artifacts_size, GraphQL::Types::Float, null: false, description: 'CI artifacts size in bytes.' + field :container_registry_size, GraphQL::Types::Float, null: false, description: 'Container Registry size in bytes.' field :dependency_proxy_size, GraphQL::Types::Float, null: false, description: 'Dependency Proxy sizes in bytes.' 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.' @@ -16,6 +17,5 @@ module Types field :storage_size, GraphQL::Types::Float, null: false, description: 'Total storage in bytes.' field :uploads_size, GraphQL::Types::Float, null: false, description: 'Uploads size in bytes.' field :wiki_size, GraphQL::Types::Float, null: false, description: 'Wiki size in bytes.' - field :container_registry_size, GraphQL::Types::Float, null: false, description: 'Container Registry size in bytes.' end end diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb index 9d5edec82b2..f7f26ba4c5a 100644 --- a/app/graphql/types/subscription_type.rb +++ b/app/graphql/types/subscription_type.rb @@ -34,6 +34,11 @@ module Types 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.' end end diff --git a/app/graphql/types/todo_action_enum.rb b/app/graphql/types/todo_action_enum.rb index ef43b6eb464..33e1c4e98a4 100644 --- a/app/graphql/types/todo_action_enum.rb +++ b/app/graphql/types/todo_action_enum.rb @@ -5,11 +5,12 @@ module Types value 'assigned', value: 1, description: 'User was assigned.' value 'mentioned', value: 2, description: 'User was mentioned.' value 'build_failed', value: 3, description: 'Build triggered by the user failed.' - value 'marked', value: 4, description: 'User added a TODO.' + value 'marked', value: 4, description: 'User added a to-do item.' value 'approval_required', value: 5, description: 'User was set as an approver.' value 'unmergeable', value: 6, description: 'Merge request authored by the user could not be merged.' value 'directly_addressed', value: 7, description: 'User was directly addressed.' value 'merge_train_removed', value: 8, description: 'Merge request authored by the user was removed from the merge train.' value 'review_requested', value: 9, description: 'Review was requested from the user.' + value 'member_access_requested', value: 10, description: 'Group access requested from the user.' end end diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb index 0de6b1d6f8a..6e5ce35033b 100644 --- a/app/graphql/types/todo_type.rb +++ b/app/graphql/types/todo_type.rb @@ -15,13 +15,11 @@ module Types field :project, Types::ProjectType, description: 'Project this to-do item is associated with.', - null: true, - authorize: :read_project + null: true field :group, 'Types::GroupType', description: 'Group this to-do item is associated with.', - null: true, - authorize: :read_group + null: true field :author, Types::UserType, description: 'Author of this to-do item.', diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb index f49b3eee4f5..51046d09f90 100644 --- a/app/graphql/types/user_interface.rb +++ b/app/graphql/types/user_interface.rb @@ -88,7 +88,10 @@ module Types null: true, description: 'Personal namespace of the user.' - field :todos, resolver: Resolvers::TodosResolver, description: 'To-do items of the user.' + field :todos, + Types::TodoType.connection_type, + description: 'To-do items of the user.', + resolver: Resolvers::TodosResolver # Merge request field: MRs can be authored, assigned, or assigned-for-review: field :authored_merge_requests, diff --git a/app/graphql/types/work_items/notes_filter_type_enum.rb b/app/graphql/types/work_items/notes_filter_type_enum.rb new file mode 100644 index 00000000000..93fb4689f0b --- /dev/null +++ b/app/graphql/types/work_items/notes_filter_type_enum.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module WorkItems + class NotesFilterTypeEnum < BaseEnum + graphql_name 'NotesFilterType' + description 'Work item notes collection type.' + + ::UserPreference::NOTES_FILTERS.each_pair do |key, value| + value key.upcase, + value: value, + description: UserPreference.notes_filters.invert[::UserPreference::NOTES_FILTERS[key]] + end + + def self.default_value + ::UserPreference::NOTES_FILTERS[:all_notes] + end + end + end +end diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb index b85d0a23535..672a78f12e1 100644 --- a/app/graphql/types/work_items/widget_interface.rb +++ b/app/graphql/types/work_items/widget_interface.rb @@ -17,7 +17,8 @@ module Types ::Types::WorkItems::Widgets::LabelsType, ::Types::WorkItems::Widgets::AssigneesType, ::Types::WorkItems::Widgets::StartAndDueDateType, - ::Types::WorkItems::Widgets::MilestoneType + ::Types::WorkItems::Widgets::MilestoneType, + ::Types::WorkItems::Widgets::NotesType ].freeze def self.ce_orphan_types @@ -41,6 +42,8 @@ module Types ::Types::WorkItems::Widgets::StartAndDueDateType when ::WorkItems::Widgets::Milestone ::Types::WorkItems::Widgets::MilestoneType + when ::WorkItems::Widgets::Notes + ::Types::WorkItems::Widgets::NotesType else raise "Unknown GraphQL type for widget #{object}" end diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb index 0ccd8af7dc8..4ec8ec84779 100644 --- a/app/graphql/types/work_items/widgets/hierarchy_type.rb +++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb @@ -20,8 +20,29 @@ module Types null: true, complexity: 5, description: 'Child work items.' + field :has_children, GraphQL::Types::Boolean, + null: false, description: 'Indicates if the work item has children.' + + # rubocop: disable CodeReuse/ActiveRecord + def has_children? + BatchLoader::GraphQL.for(object.work_item.id).batch(default_value: false) do |ids, loader| + links_for_parents = ::WorkItems::ParentLink.for_parents(ids) + .select(:work_item_parent_id) + .group(:work_item_parent_id) + .reorder(nil) + + links_for_parents.each { |link| loader.call(link.work_item_parent_id, true) } + end + end + # rubocop: enable CodeReuse/ActiveRecord + + alias_method :has_children, :has_children? + def children - object.children.inc_relations_for_permission_check + relation = object.children + relation = relation.inc_relations_for_permission_check unless object.children.loaded? + + relation end end # rubocop:enable Graphql/AuthorizeTypes diff --git a/app/graphql/types/work_items/widgets/notes_type.rb b/app/graphql/types/work_items/widgets/notes_type.rb new file mode 100644 index 00000000000..7da2777beee --- /dev/null +++ b/app/graphql/types/work_items/widgets/notes_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 NotesType < BaseObject + graphql_name 'WorkItemWidgetNotes' + description 'Represents a notes widget' + + implements Types::WorkItems::WidgetInterface + + # This field loads user comments, system notes and resource events as a discussion for an work item, + # raising the complexity considerably. In order to discourage fetching this field as part of fetching + # a list of issues we raise the complexity + field :discussions, Types::Notes::DiscussionType.connection_type, + null: true, + description: "Notes on this work item.", + resolver: Resolvers::WorkItems::WorkItemDiscussionsResolver + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end |