diff options
Diffstat (limited to 'app/graphql')
159 files changed, 2401 insertions, 273 deletions
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 2f5043f9ffa..d66a2333d11 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -30,6 +30,8 @@ class GitlabSchema < GraphQL::Schema default_max_page_size 100 + lazy_resolve ::Gitlab::Graphql::Lazy, :force + class << self def multiplex(queries, **kwargs) kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) @@ -76,6 +78,13 @@ class GitlabSchema < GraphQL::Schema find_by_gid(gid) end + def resolve_type(type, object, ctx = :__undefined__) + tc = type.metadata[:type_class] + return if tc.respond_to?(:assignable?) && !tc.assignable?(object) + + super + end + # Find an object by looking it up from its 'GlobalID'. # # * For `ApplicationRecord`s, this is equivalent to diff --git a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb index a3a421f8938..17f9b5b5637 100644 --- a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb +++ b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb @@ -33,9 +33,9 @@ module Mutations super end - def resolve(args) + def resolve(queue_name:, **args) { - result: Gitlab::SidekiqQueue.new(args[:queue_name]).drop_jobs!(args, timeout: 30), + result: Gitlab::SidekiqQueue.new(queue_name).drop_jobs!(args, timeout: 30), errors: [] } rescue Gitlab::SidekiqQueue::NoMetadataError @@ -44,7 +44,7 @@ module Mutations errors: ['No metadata provided'] } rescue Gitlab::SidekiqQueue::InvalidQueueError - raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Queue #{args[:queue_name]} not found" + raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Queue #{queue_name} not found" end end end diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index 0ccfcf34180..81d5ee95f06 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -4,7 +4,6 @@ module Mutations module AlertManagement class Base < BaseMutation include Gitlab::Utils::UsageData - include ResolvesProject argument :project_path, GraphQL::ID_TYPE, required: true, @@ -33,13 +32,12 @@ module Mutations private - def find_object(project_path:, iid:) - project = resolve_project(full_path: project_path) + def find_object(project_path:, **args) + project = Project.find_by_full_path(project_path) return unless project - resolver = Resolvers::AlertManagement::AlertResolver.single.new(object: project, context: context, field: nil) - resolver.resolve(iid: iid) + ::AlertManagement::AlertsFinder.new(current_user, project, args).execute.first end end end diff --git a/app/graphql/mutations/alert_management/http_integration/create.rb b/app/graphql/mutations/alert_management/http_integration/create.rb new file mode 100644 index 00000000000..ddb75e66bb4 --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/create.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class Create < HttpIntegrationBase + include ResolvesProject + + graphql_name 'HttpIntegrationCreate' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'The project to create the integration in' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'The name of the integration' + + argument :active, GraphQL::BOOLEAN_TYPE, + required: true, + description: 'Whether the integration is receiving alerts' + + def resolve(args) + project = authorized_find!(full_path: args[:project_path]) + + response ::AlertManagement::HttpIntegrations::CreateService.new( + project, + current_user, + args.slice(:name, :active) + ).execute + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/destroy.rb b/app/graphql/mutations/alert_management/http_integration/destroy.rb new file mode 100644 index 00000000000..0f478760aab --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/destroy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class Destroy < HttpIntegrationBase + graphql_name 'HttpIntegrationDestroy' + + argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], + required: true, + description: "The id of the integration to remove" + + def resolve(id:) + integration = authorized_find!(id: id) + + response ::AlertManagement::HttpIntegrations::DestroyService.new( + integration, + current_user + ).execute + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb new file mode 100644 index 00000000000..d328eabf244 --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class HttpIntegrationBase < BaseMutation + field :integration, + Types::AlertManagement::HttpIntegrationType, + null: true, + description: "The HTTP integration" + + authorize :admin_operations + + private + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_class: ::AlertManagement::HttpIntegration) + end + + def response(result) + { + integration: result.payload[:integration], + errors: result.errors + } + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/reset_token.rb b/app/graphql/mutations/alert_management/http_integration/reset_token.rb new file mode 100644 index 00000000000..eefab156825 --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/reset_token.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class ResetToken < HttpIntegrationBase + graphql_name 'HttpIntegrationResetToken' + + argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], + required: true, + description: "The id of the integration to mutate" + + def resolve(id:) + integration = authorized_find!(id: id) + + response ::AlertManagement::HttpIntegrations::UpdateService.new( + integration, + current_user, + regenerate_token: true + ).execute + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/http_integration/update.rb b/app/graphql/mutations/alert_management/http_integration/update.rb new file mode 100644 index 00000000000..309c45b04ac --- /dev/null +++ b/app/graphql/mutations/alert_management/http_integration/update.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module HttpIntegration + class Update < HttpIntegrationBase + graphql_name 'HttpIntegrationUpdate' + + argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], + required: true, + description: "The id of the integration to mutate" + + argument :name, GraphQL::STRING_TYPE, + required: false, + description: "The name of the integration" + + argument :active, GraphQL::BOOLEAN_TYPE, + required: false, + description: "Whether the integration is receiving alerts" + + def resolve(args) + integration = authorized_find!(id: args[:id]) + + response ::AlertManagement::HttpIntegrations::UpdateService.new( + integration, + current_user, + args.slice(:name, :active) + ).execute + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/create.rb b/app/graphql/mutations/alert_management/prometheus_integration/create.rb new file mode 100644 index 00000000000..935ec53795c --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/create.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class Create < PrometheusIntegrationBase + include ResolvesProject + + graphql_name 'PrometheusIntegrationCreate' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'The project to create the integration in' + + argument :active, GraphQL::BOOLEAN_TYPE, + required: true, + description: 'Whether the integration is receiving alerts' + + argument :api_url, GraphQL::STRING_TYPE, + required: true, + description: 'Endpoint at which prometheus can be queried' + + def resolve(args) + project = authorized_find!(full_path: args[:project_path]) + + return integration_exists if project.prometheus_service + + result = ::Projects::Operations::UpdateService.new( + project, + current_user, + **integration_attributes(args), + **token_attributes + ).execute + + response(project.prometheus_service, result) + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + + def integration_exists + response(nil, message: _('Multiple Prometheus integrations are not supported')) + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb new file mode 100644 index 00000000000..6b690ac239a --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class PrometheusIntegrationBase < BaseMutation + field :integration, + Types::AlertManagement::PrometheusIntegrationType, + null: true, + description: "The newly created integration" + + authorize :admin_project + + private + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_class: ::PrometheusService) + end + + def response(integration, result) + { + integration: integration, + errors: Array(result[:message]) + } + end + + def integration_attributes(args) + { + prometheus_integration_attributes: { + manual_configuration: args[:active], + api_url: args[:api_url] + }.compact + } + end + + def token_attributes + { alerting_setting_attributes: { regenerate_token: true } } + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb new file mode 100644 index 00000000000..745ac51f6e3 --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class ResetToken < PrometheusIntegrationBase + graphql_name 'PrometheusIntegrationResetToken' + + argument :id, Types::GlobalIDType[::PrometheusService], + required: true, + description: "The id of the integration to mutate" + + def resolve(id:) + integration = authorized_find!(id: id) + + result = ::Projects::Operations::UpdateService.new( + integration.project, + current_user, + token_attributes + ).execute + + response integration, result + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/prometheus_integration/update.rb b/app/graphql/mutations/alert_management/prometheus_integration/update.rb new file mode 100644 index 00000000000..1f0dea119c5 --- /dev/null +++ b/app/graphql/mutations/alert_management/prometheus_integration/update.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + module PrometheusIntegration + class Update < PrometheusIntegrationBase + graphql_name 'PrometheusIntegrationUpdate' + + argument :id, Types::GlobalIDType[::PrometheusService], + required: true, + description: "The id of the integration to mutate" + + argument :active, GraphQL::BOOLEAN_TYPE, + required: false, + description: "Whether the integration is receiving alerts" + + argument :api_url, GraphQL::STRING_TYPE, + required: false, + description: "Endpoint at which prometheus can be queried" + + def resolve(args) + integration = authorized_find!(id: args[:id]) + + result = ::Projects::Operations::UpdateService.new( + integration.project, + current_user, + integration_attributes(args) + ).execute + + response integration.reset, result + end + end + end + end +end diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb index 1e14bae048a..74185dca529 100644 --- a/app/graphql/mutations/alert_management/update_alert_status.rb +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -9,9 +9,9 @@ module Mutations required: true, description: 'The status to set the alert' - def resolve(args) - alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) - result = update_status(alert, args[:status]) + def resolve(project_path:, iid:, status:) + 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) diff --git a/app/graphql/mutations/boards/create.rb b/app/graphql/mutations/boards/create.rb index e381205242e..ebbd19930ec 100644 --- a/app/graphql/mutations/boards/create.rb +++ b/app/graphql/mutations/boards/create.rb @@ -3,8 +3,7 @@ module Mutations module Boards class Create < ::Mutations::BaseMutation - include Mutations::ResolvesGroup - include ResolvesProject + include Mutations::ResolvesResourceParent graphql_name 'CreateBoard' @@ -13,12 +12,6 @@ module Mutations null: true, description: 'The board after mutation.' - argument :project_path, GraphQL::ID_TYPE, - required: false, - description: 'The project full path the board is associated with.' - argument :group_path, GraphQL::ID_TYPE, - required: false, - description: 'The group full path the board is associated with.' argument :name, GraphQL::STRING_TYPE, required: false, @@ -28,7 +21,7 @@ module Mutations required: false, description: 'The ID of the user to be assigned to the board.' argument :milestone_id, - GraphQL::ID_TYPE, + Types::GlobalIDType[Milestone], required: false, description: 'The ID of the milestone to be assigned to the board.' argument :weight, @@ -36,17 +29,14 @@ module Mutations required: false, description: 'The weight of the board.' argument :label_ids, - [GraphQL::ID_TYPE], + [Types::GlobalIDType[Label]], required: false, description: 'The IDs of labels to be added to the board.' authorize :admin_board def resolve(args) - group_path = args.delete(:group_path) - project_path = args.delete(:project_path) - - board_parent = authorized_find!(group_path: group_path, project_path: project_path) + board_parent = authorized_resource_parent_find!(args) response = ::Boards::CreateService.new(board_parent, current_user, args).execute { @@ -54,25 +44,6 @@ module Mutations errors: response.errors } end - - def ready?(**args) - if args.values_at(:project_path, :group_path).compact.blank? - raise Gitlab::Graphql::Errors::ArgumentError, - 'group_path or project_path arguments are required' - end - - super - end - - private - - def find_object(group_path: nil, project_path: nil) - if group_path - resolve_group(full_path: group_path) - else - resolve_project(full_path: project_path) - end - end end end end diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb index 7efed3058b3..14502b5174f 100644 --- a/app/graphql/mutations/boards/lists/update.rb +++ b/app/graphql/mutations/boards/lists/update.rb @@ -6,7 +6,7 @@ module Mutations class Update < BaseMutation graphql_name 'UpdateBoardList' - argument :list_id, GraphQL::ID_TYPE, + argument :list_id, Types::GlobalIDType[List], required: true, loads: Types::BoardListType, description: 'Global ID of the list.' diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb index 9ed1bb819c8..2b9107350fd 100644 --- a/app/graphql/mutations/commits/create.rb +++ b/app/graphql/mutations/commits/create.rb @@ -13,7 +13,11 @@ module Mutations argument :branch, GraphQL::STRING_TYPE, required: true, - description: 'Name of the branch' + description: 'Name of the branch to commit into, it can be a new branch' + + argument :start_branch, GraphQL::STRING_TYPE, + required: false, + description: 'If on a new branch, name of the original branch' argument :message, GraphQL::STRING_TYPE, @@ -32,13 +36,13 @@ module Mutations authorize :push_code - def resolve(project_path:, branch:, message:, actions:) + def resolve(project_path:, branch:, message:, actions:, **args) project = authorized_find!(full_path: project_path) attributes = { commit_message: message, branch_name: branch, - start_branch: branch, + start_branch: args[:start_branch] || branch, actions: actions.map { |action| action.to_h } } diff --git a/app/graphql/mutations/concerns/mutations/package_eventable.rb b/app/graphql/mutations/concerns/mutations/package_eventable.rb new file mode 100644 index 00000000000..86fd7b9a88a --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/package_eventable.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Mutations + module PackageEventable + extend ActiveSupport::Concern + + private + + def track_event(event, scope) + ::Packages::CreateEventService.new(nil, current_user, event_name: event, scope: scope).execute + ::Gitlab::Tracking.event(event.to_s, scope.to_s) + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb new file mode 100644 index 00000000000..04a9abf9529 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Mutations + module ResolvesResourceParent + extend ActiveSupport::Concern + include Mutations::ResolvesGroup + include ResolvesProject + + included do + argument :project_path, GraphQL::ID_TYPE, + required: false, + description: 'The project full path the resource is associated with' + + argument :group_path, GraphQL::ID_TYPE, + required: false, + description: 'The group full path the resource is associated with' + end + + def ready?(**args) + unless args[:project_path].present? ^ args[:group_path].present? + raise Gitlab::Graphql::Errors::ArgumentError, + 'Exactly one of group_path or project_path arguments is required' + end + + super + end + + private + + def authorized_resource_parent_find!(args) + authorized_find!(project_path: args.delete(:project_path), + group_path: args.delete(:group_path)) + end + + def find_object(project_path: nil, group_path: nil) + if group_path.present? + resolve_group(full_path: group_path) + else + resolve_project(full_path: project_path) + end + end + end +end diff --git a/app/graphql/mutations/container_repositories/destroy.rb b/app/graphql/mutations/container_repositories/destroy.rb new file mode 100644 index 00000000000..8312193147f --- /dev/null +++ b/app/graphql/mutations/container_repositories/destroy.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Mutations + module ContainerRepositories + class Destroy < Mutations::BaseMutation + include ::Mutations::PackageEventable + + graphql_name 'DestroyContainerRepository' + + authorize :destroy_container_image + + argument :id, + ::Types::GlobalIDType[::ContainerRepository], + required: true, + description: 'ID of the container repository.' + + field :container_repository, + Types::ContainerRepositoryType, + null: false, + description: 'The container repository policy after scheduling the deletion.' + + def resolve(id:) + container_repository = authorized_find!(id: id) + + container_repository.delete_scheduled! + DeleteContainerRepositoryWorker.perform_async(current_user.id, container_repository.id) + track_event(:delete_repository, :container) + + { + container_repository: container_repository, + errors: [] + } + end + + private + + def find_object(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::ContainerRepository].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/mutations/custom_emoji/create.rb b/app/graphql/mutations/custom_emoji/create.rb new file mode 100644 index 00000000000..d912a29d12e --- /dev/null +++ b/app/graphql/mutations/custom_emoji/create.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module CustomEmoji + class Create < BaseMutation + include Mutations::ResolvesGroup + + graphql_name 'CreateCustomEmoji' + + authorize :create_custom_emoji + + field :custom_emoji, + Types::CustomEmojiType, + null: true, + description: 'The new custom emoji' + + argument :group_path, GraphQL::ID_TYPE, + required: true, + description: 'Namespace full path the emoji is associated with' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the emoji' + + argument :url, GraphQL::STRING_TYPE, + required: true, + as: :file, + description: 'Location of the emoji file' + + def resolve(group_path:, **args) + group = authorized_find!(group_path: group_path) + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911#note_444682238 + args[:external] = true + + custom_emoji = group.custom_emoji.create(args) + + { + custom_emoji: custom_emoji.valid? ? custom_emoji : nil, + errors: errors_on_object(custom_emoji) + } + end + + private + + def find_object(group_path:) + resolve_group(full_path: group_path) + end + end + end +end diff --git a/app/graphql/mutations/labels/create.rb b/app/graphql/mutations/labels/create.rb new file mode 100644 index 00000000000..cb03651618e --- /dev/null +++ b/app/graphql/mutations/labels/create.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Mutations + module Labels + class Create < BaseMutation + include Mutations::ResolvesResourceParent + + graphql_name 'LabelCreate' + + field :label, + Types::LabelType, + null: true, + description: 'The label after mutation' + + argument :title, GraphQL::STRING_TYPE, + required: true, + description: 'Title of the label' + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: 'Description of the label' + + argument :color, GraphQL::STRING_TYPE, + required: false, + default_value: Label::DEFAULT_COLOR, + description: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords" + + authorize :admin_label + + def resolve(args) + parent = authorized_resource_parent_find!(args) + parent_key = parent.is_a?(Project) ? :project : :group + + label = ::Labels::CreateService.new(args).execute(parent_key => parent) + + { + label: label.persisted? ? label : nil, + errors: errors_on_object(label) + } + end + end + end +end diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb index c1e45808593..712c68c9425 100644 --- a/app/graphql/mutations/merge_requests/set_labels.rb +++ b/app/graphql/mutations/merge_requests/set_labels.rb @@ -6,7 +6,7 @@ module Mutations graphql_name 'MergeRequestSetLabels' argument :label_ids, - [GraphQL::ID_TYPE], + [::Types::GlobalIDType[Label]], required: true, description: <<~DESC The Label IDs to set. Replaces existing labels by default. @@ -23,10 +23,11 @@ module Mutations merge_request = authorized_find!(project_path: project_path, iid: iid) project = merge_request.project - label_ids = label_ids - .map { |gid| GlobalID.parse(gid) } - .select(&method(:label_descendant?)) - .map(&:model_id) # MergeRequests::UpdateService expects integers + # TODO: remove this line when the compatibility layer is removed: + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + label_ids = label_ids.map { |id| ::Types::GlobalIDType[::Label].coerce_isolated_input(id) } + # MergeRequests::UpdateService expects integers + label_ids = label_ids.compact.map(&:model_id) attribute_name = case operation_mode when Types::MutationOperationModeEnum.enum[:append] @@ -45,10 +46,6 @@ module Mutations errors: errors_on_object(merge_request) } end - - def label_descendant?(gid) - gid&.model_class&.ancestors&.include?(Label) - end end end end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb index 6e183e78d9b..d6731dfcafd 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb @@ -9,8 +9,7 @@ module Mutations authorize :delete_metrics_dashboard_annotation - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Metrics::Dashboard::Annotation], required: true, description: 'The global ID of the annotation to delete' diff --git a/app/graphql/mutations/notes/reposition_image_diff_note.rb b/app/graphql/mutations/notes/reposition_image_diff_note.rb new file mode 100644 index 00000000000..0d88bcd9a30 --- /dev/null +++ b/app/graphql/mutations/notes/reposition_image_diff_note.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Mutations + module Notes + # This mutation differs from the update note mutations as it checks the + # `reposition_note` permission, and doesn't allow updating a note's `body`. + class RepositionImageDiffNote < Mutations::Notes::Base + graphql_name 'RepositionImageDiffNote' + + description 'Repositions a DiffNote on an image (a `Note` where the `position.positionType` is `"image"`)' + + authorize :reposition_note + + argument :id, + Types::GlobalIDType[DiffNote], + loads: Types::Notes::NoteType, + as: :note, + required: true, + description: 'The global id of the DiffNote to update' + + argument :position, + Types::Notes::UpdateDiffImagePositionInputType, + required: true, + description: copy_field_description(Types::Notes::NoteType, :position) + + def resolve(note:, position:) + authorize!(note) + + pre_update_checks!(note, position) + + updated_note = ::Notes::UpdateService.new( + note.project, + current_user, + note_params(note.position, position) + ).execute(note) + + { + note: updated_note.reset, + errors: errors_on_object(updated_note) + } + end + + private + + # An ImageDiffNote does not exist as a class itself, but is instead + # just a `DiffNote` with a particular kind of `Gitlab::Diff::Position`. + # In addition to accepting a `DiffNote` Global ID we also need to + # perform this check. + def pre_update_checks!(note, position) + unless note.position&.on_image? + raise Gitlab::Graphql::Errors::ResourceNotAvailable, + 'Resource is not an ImageDiffNote' + end + end + + def note_params(old_position, new_position) + position = old_position.to_h.merge(new_position) + + { + position: Gitlab::Diff::Position.new(position) + } + end + end + end +end diff --git a/app/graphql/mutations/notes/update/image_diff_note.rb b/app/graphql/mutations/notes/update/image_diff_note.rb index ef70a8d2bf4..f4533cd9edb 100644 --- a/app/graphql/mutations/notes/update/image_diff_note.rb +++ b/app/graphql/mutations/notes/update/image_diff_note.rb @@ -47,12 +47,11 @@ module Mutations end def position_params(note, args) - new_position = args[:position]&.to_h&.compact - return unless new_position + return unless args[:position] original_position = note.position.to_h - Gitlab::Diff::Position.new(original_position.merge(new_position)) + Gitlab::Diff::Position.new(original_position.merge(args[:position])) end end end diff --git a/app/graphql/mutations/releases/base.rb b/app/graphql/mutations/releases/base.rb new file mode 100644 index 00000000000..d53cfbe6a11 --- /dev/null +++ b/app/graphql/mutations/releases/base.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Mutations + module Releases + class Base < BaseMutation + include ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Full path of the project the release is associated with' + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb new file mode 100644 index 00000000000..57c1541c368 --- /dev/null +++ b/app/graphql/mutations/releases/create.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Mutations + module Releases + class Create < Base + graphql_name 'ReleaseCreate' + + field :release, + Types::ReleaseType, + null: true, + description: 'The release after mutation' + + argument :tag_name, GraphQL::STRING_TYPE, + required: true, as: :tag, + description: 'Name of the tag to associate with the release' + + argument :ref, GraphQL::STRING_TYPE, + required: false, + description: 'The commit SHA or branch name to use if creating a new tag' + + argument :name, GraphQL::STRING_TYPE, + required: false, + description: 'Name of the release' + + argument :description, GraphQL::STRING_TYPE, + required: false, + description: 'Description (also known as "release notes") of the release' + + argument :released_at, Types::TimeType, + required: false, + description: 'The date when the release will be/was ready. Defaults to the current time.' + + argument :milestones, [GraphQL::STRING_TYPE], + required: false, + description: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.' + + argument :assets, Types::ReleaseAssetsInputType, + required: false, + description: 'Assets associated to the release' + + authorize :create_release + + def resolve(project_path:, milestones: nil, assets: nil, **scalars) + project = authorized_find!(full_path: project_path) + + params = { + **scalars, + milestones: milestones.presence || [], + assets: assets.to_h + }.with_indifferent_access + + result = ::Releases::CreateService.new(project, current_user, params).execute + + if result[:status] == :success + { + release: result[:release], + errors: [] + } + else + { + release: nil, + errors: [result[:message]] + } + end + end + end + end +end diff --git a/app/graphql/mutations/snippets/destroy.rb b/app/graphql/mutations/snippets/destroy.rb index dc9a1e82575..4915d7dd77a 100644 --- a/app/graphql/mutations/snippets/destroy.rb +++ b/app/graphql/mutations/snippets/destroy.rb @@ -7,8 +7,7 @@ module Mutations ERROR_MSG = 'Error deleting the snippet' - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Snippet], required: true, description: 'The global id of the snippet to destroy' diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb index 8cfbbae7c08..d6b96c699c0 100644 --- a/app/graphql/mutations/snippets/mark_as_spam.rb +++ b/app/graphql/mutations/snippets/mark_as_spam.rb @@ -5,8 +5,7 @@ module Mutations class MarkAsSpam < Base graphql_name 'MarkAsSpamSnippet' - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Snippet], required: true, description: 'The global id of the snippet to update' diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb index 74266880806..bcaa807e4c1 100644 --- a/app/graphql/mutations/snippets/update.rb +++ b/app/graphql/mutations/snippets/update.rb @@ -7,8 +7,7 @@ module Mutations graphql_name 'UpdateSnippet' - argument :id, - GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Snippet], required: true, description: 'The global id of the snippet to update' diff --git a/app/graphql/mutations/terraform/state/base.rb b/app/graphql/mutations/terraform/state/base.rb new file mode 100644 index 00000000000..b1721c784b1 --- /dev/null +++ b/app/graphql/mutations/terraform/state/base.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Base < BaseMutation + authorize :admin_terraform_state + + argument :id, + Types::GlobalIDType[::Terraform::State], + required: true, + description: 'Global ID of the Terraform state' + + private + + def find_object(id:) + GitlabSchema.find_by_gid(id) + end + end + end + end +end diff --git a/app/graphql/mutations/terraform/state/delete.rb b/app/graphql/mutations/terraform/state/delete.rb new file mode 100644 index 00000000000..f08219cb395 --- /dev/null +++ b/app/graphql/mutations/terraform/state/delete.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Delete < Base + graphql_name 'TerraformStateDelete' + + def resolve(id:) + state = authorized_find!(id: id) + state.destroy + + { errors: errors_on_object(state) } + end + end + end + end +end diff --git a/app/graphql/mutations/terraform/state/lock.rb b/app/graphql/mutations/terraform/state/lock.rb new file mode 100644 index 00000000000..d22c8de2560 --- /dev/null +++ b/app/graphql/mutations/terraform/state/lock.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Lock < Base + graphql_name 'TerraformStateLock' + + def resolve(id:) + state = authorized_find!(id: id) + + if state.locked? + state.errors.add(:base, 'state is already locked') + else + state.update(lock_xid: lock_xid, locked_by_user: current_user, locked_at: Time.current) + end + + { errors: errors_on_object(state) } + end + + private + + def lock_xid + SecureRandom.uuid + end + end + end + end +end diff --git a/app/graphql/mutations/terraform/state/unlock.rb b/app/graphql/mutations/terraform/state/unlock.rb new file mode 100644 index 00000000000..0818dbd7fb3 --- /dev/null +++ b/app/graphql/mutations/terraform/state/unlock.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + module Terraform + module State + class Unlock < Base + graphql_name 'TerraformStateUnlock' + + def resolve(id:) + state = authorized_find!(id: id) + state.update(lock_xid: nil, locked_by_user: nil, locked_at: nil) + + { errors: errors_on_object(state) } + end + end + end + end +end diff --git a/app/graphql/mutations/todos/base.rb b/app/graphql/mutations/todos/base.rb index 6db863796bc..4dab3bbc3f4 100644 --- a/app/graphql/mutations/todos/base.rb +++ b/app/graphql/mutations/todos/base.rb @@ -11,16 +11,6 @@ module Mutations id = ::Types::GlobalIDType[::Todo].coerce_isolated_input(id) GitlabSchema.find_by_gid(id) end - - def map_to_global_ids(ids) - return [] if ids.blank? - - ids.map { |id| to_global_id(id) } - end - - def to_global_id(id) - Gitlab::GlobalId.as_global_id(id, model_name: Todo.name).to_s - end end end end diff --git a/app/graphql/mutations/todos/create.rb b/app/graphql/mutations/todos/create.rb new file mode 100644 index 00000000000..53c88696fdd --- /dev/null +++ b/app/graphql/mutations/todos/create.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mutations + module Todos + class Create < ::Mutations::Todos::Base + graphql_name 'TodoCreate' + + authorize :create_todo + + argument :target_id, + Types::GlobalIDType[Todoable], + required: true, + description: "The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported" + + field :todo, Types::TodoType, + null: true, + description: 'The to-do created' + + def resolve(target_id:) + id = ::Types::GlobalIDType[Todoable].coerce_isolated_input(target_id) + target = authorized_find!(id) + + todo = TodoService.new.mark_todo(target, current_user)&.first + errors = errors_on_object(todo) if todo + + { + todo: todo, + errors: errors + } + end + + private + + def find_object(id) + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb index 8b53658ddd5..97bbbeeaa2f 100644 --- a/app/graphql/mutations/todos/mark_all_done.rb +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -8,7 +8,7 @@ module Mutations authorize :update_user field :updated_ids, - [GraphQL::ID_TYPE], + [::Types::GlobalIDType[::Todo]], null: false, deprecated: { reason: 'Use todos', milestone: '13.2' }, description: 'Ids of the updated todos' @@ -23,7 +23,7 @@ module Mutations updated_ids = mark_all_todos_done { - updated_ids: map_to_global_ids(updated_ids), + updated_ids: updated_ids, todos: Todo.id_in(updated_ids), errors: [] } diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index ea5f5414134..9e0a95c48ec 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -12,7 +12,7 @@ module Mutations required: true, description: 'The global ids of the todos to restore (a maximum of 50 is supported at once)' - field :updated_ids, [GraphQL::ID_TYPE], + field :updated_ids, [::Types::GlobalIDType[Todo]], null: false, description: 'The ids of the updated todo items', deprecated: { reason: 'Use todos', milestone: '13.2' } @@ -28,7 +28,7 @@ module Mutations updated_ids = restore(todos) { - updated_ids: gids_of(updated_ids), + updated_ids: updated_ids, todos: Todo.id_in(updated_ids), errors: errors_on_objects(todos) } @@ -36,10 +36,6 @@ module Mutations private - def gids_of(ids) - ids.map { |id| Gitlab::GlobalId.as_global_id(id, model_name: Todo.name).to_s } - end - def model_ids_of(ids) ids.map do |gid| # TODO: remove this line when the compatibility layer is removed diff --git a/app/graphql/queries/design_management/design_permissions.query.graphql b/app/graphql/queries/design_management/design_permissions.query.graphql new file mode 100644 index 00000000000..55dfa35129c --- /dev/null +++ b/app/graphql/queries/design_management/design_permissions.query.graphql @@ -0,0 +1,13 @@ +query permissions($fullPath: ID!, $iid: String!) { + project(fullPath: $fullPath) { + __typename + id + issue(iid: $iid) { + __typename + userPermissions { + __typename + createDesign + } + } + } +} diff --git a/app/graphql/queries/design_management/get_design_list.query.graphql b/app/graphql/queries/design_management/get_design_list.query.graphql new file mode 100644 index 00000000000..ade03d99797 --- /dev/null +++ b/app/graphql/queries/design_management/get_design_list.query.graphql @@ -0,0 +1,40 @@ +query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) { + project(fullPath: $fullPath) { + __typename + id + issue(iid: $iid) { + __typename + designCollection { + __typename + copyState + designs(atVersion: $atVersion) { + __typename + nodes { + __typename + id + event + filename + notesCount + image + imageV432x230 + currentUserTodos(state: pending) { + __typename + nodes { + __typename + id + } + } + } + } + versions { + __typename + nodes { + __typename + id + sha + } + } + } + } + } +} diff --git a/app/graphql/queries/repository/files.query.graphql b/app/graphql/queries/repository/files.query.graphql new file mode 100644 index 00000000000..232d98a932c --- /dev/null +++ b/app/graphql/queries/repository/files.query.graphql @@ -0,0 +1,76 @@ +fragment PageInfo on PageInfo { + __typename + hasNextPage + hasPreviousPage + startCursor + endCursor +} + +fragment TreeEntry on Entry { + __typename + id + sha + name + flatPath + type +} + +query getFiles( + $projectPath: ID! + $path: String + $ref: String! + $pageSize: Int! + $nextPageCursor: String +) { + project(fullPath: $projectPath) { + __typename + repository { + __typename + tree(path: $path, ref: $ref) { + __typename + trees(first: $pageSize, after: $nextPageCursor) { + __typename + edges { + __typename + node { + ...TreeEntry + webPath + } + } + pageInfo { + ...PageInfo + } + } + submodules(first: $pageSize, after: $nextPageCursor) { + __typename + edges { + __typename + node { + ...TreeEntry + webUrl + treeUrl + } + } + pageInfo { + ...PageInfo + } + } + blobs(first: $pageSize, after: $nextPageCursor) { + __typename + edges { + __typename + node { + ...TreeEntry + mode + webPath + lfsOid + } + } + pageInfo { + ...PageInfo + } + } + } + } + } +} diff --git a/app/graphql/queries/repository/permissions.query.graphql b/app/graphql/queries/repository/permissions.query.graphql new file mode 100644 index 00000000000..c0262a882cd --- /dev/null +++ b/app/graphql/queries/repository/permissions.query.graphql @@ -0,0 +1,11 @@ +query getPermissions($projectPath: ID!) { + project(fullPath: $projectPath) { + __typename + userPermissions { + __typename + pushCode + forkProject + createMergeRequestIn + } + } +} diff --git a/app/graphql/queries/snippet/project_permissions.query.graphql b/app/graphql/queries/snippet/project_permissions.query.graphql new file mode 100644 index 00000000000..0c38e4f8a07 --- /dev/null +++ b/app/graphql/queries/snippet/project_permissions.query.graphql @@ -0,0 +1,9 @@ +query CanCreateProjectSnippet($fullPath: ID!) { + project(fullPath: $fullPath) { + __typename + userPermissions { + __typename + createSnippet + } + } +} diff --git a/app/graphql/queries/snippet/snippet.query.graphql b/app/graphql/queries/snippet/snippet.query.graphql new file mode 100644 index 00000000000..2205dc26642 --- /dev/null +++ b/app/graphql/queries/snippet/snippet.query.graphql @@ -0,0 +1,65 @@ +query GetSnippetQuery($ids: [SnippetID!]) { + snippets(ids: $ids) { + __typename + nodes { + __typename + id + title + description + descriptionHtml + createdAt + updatedAt + visibilityLevel + webUrl + httpUrlToRepo + sshUrlToRepo + blobs { + __typename + nodes { + __typename + binary + name + path + rawPath + size + externalStorage + renderedAsText + simpleViewer { + __typename + collapsed + renderError + tooLarge + type + fileType + } + richViewer { + __typename + collapsed + renderError + tooLarge + type + fileType + } + } + } + userPermissions { + __typename + adminSnippet + updateSnippet + } + project { + __typename + fullPath + webUrl + } + author { + __typename + id + avatarUrl + name + username + webUrl + } + } + } +} diff --git a/app/graphql/queries/snippet/snippet_blob_content.query.graphql b/app/graphql/queries/snippet/snippet_blob_content.query.graphql new file mode 100644 index 00000000000..005f42ff726 --- /dev/null +++ b/app/graphql/queries/snippet/snippet_blob_content.query.graphql @@ -0,0 +1,18 @@ +query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) { + snippets(ids: $ids) { + __typename + nodes { + __typename + id + blobs(paths: $paths) { + __typename + nodes { + __typename + path + richData @include(if: $rich) + plainData @skip(if: $rich) + } + } + } + } +} diff --git a/app/graphql/queries/snippet/user_permissions.query.graphql b/app/graphql/queries/snippet/user_permissions.query.graphql new file mode 100644 index 00000000000..a4914189807 --- /dev/null +++ b/app/graphql/queries/snippet/user_permissions.query.graphql @@ -0,0 +1,9 @@ +query CanCreatePersonalSnippet { + currentUser { + __typename + userPermissions { + __typename + createSnippet + } + } +} diff --git a/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb index aea3afa8ec5..9bac9f222ab 100644 --- a/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb +++ b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb @@ -13,10 +13,20 @@ module Resolvers required: true, description: 'The type of measurement/statistics to retrieve' - def resolve(identifier:) + argument :recorded_after, Types::TimeType, + required: false, + description: 'Measurement recorded after this date' + + argument :recorded_before, Types::TimeType, + required: false, + description: 'Measurement recorded before this date' + + def resolve(identifier:, recorded_before: nil, recorded_after: nil) authorize! ::Analytics::InstanceStatistics::Measurement + .recorded_after(recorded_after) + .recorded_before(recorded_before) .with_identifier(identifier) .order_by_latest end diff --git a/app/graphql/resolvers/alert_management/alert_resolver.rb b/app/graphql/resolvers/alert_management/alert_resolver.rb index dc9b1dbb5f4..c3219d9cdc3 100644 --- a/app/graphql/resolvers/alert_management/alert_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_resolver.rb @@ -19,7 +19,7 @@ module Resolvers required: false argument :search, GraphQL::STRING_TYPE, - description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', + description: 'Search query for title, description, service, or monitoring_tool.', required: false argument :assignee_username, GraphQL::STRING_TYPE, diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb index 96ea4610aff..8fc0f9fd1ff 100644 --- a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb @@ -6,7 +6,7 @@ module Resolvers type Types::AlertManagement::AlertStatusCountsType, null: true argument :search, GraphQL::STRING_TYPE, - description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', + description: 'Search query for title, description, service, or monitoring_tool.', required: false argument :assignee_username, GraphQL::STRING_TYPE, diff --git a/app/graphql/resolvers/alert_management/integrations_resolver.rb b/app/graphql/resolvers/alert_management/integrations_resolver.rb new file mode 100644 index 00000000000..4d1fe367277 --- /dev/null +++ b/app/graphql/resolvers/alert_management/integrations_resolver.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Resolvers + module AlertManagement + class IntegrationsResolver < BaseResolver + alias_method :project, :synchronized_object + + type Types::AlertManagement::IntegrationType.connection_type, null: true + + def resolve(**args) + http_integrations + prometheus_integrations + end + + private + + def prometheus_integrations + return [] unless Ability.allowed?(current_user, :admin_project, project) + + Array(project.prometheus_service) + end + + def http_integrations + return [] unless Ability.allowed?(current_user, :admin_operations, project) + + ::AlertManagement::HttpIntegrationsFinder.new(project, {}).execute + end + end + end +end diff --git a/app/graphql/resolvers/assigned_merge_requests_resolver.rb b/app/graphql/resolvers/assigned_merge_requests_resolver.rb index 172a8e298ad..30415ef5d2d 100644 --- a/app/graphql/resolvers/assigned_merge_requests_resolver.rb +++ b/app/graphql/resolvers/assigned_merge_requests_resolver.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module Resolvers - class AssignedMergeRequestsResolver < UserMergeRequestsResolver + class AssignedMergeRequestsResolver < UserMergeRequestsResolverBase + type ::Types::MergeRequestType.connection_type, null: true accept_author def user_role diff --git a/app/graphql/resolvers/authored_merge_requests_resolver.rb b/app/graphql/resolvers/authored_merge_requests_resolver.rb index bc796f8685a..1426ca83c06 100644 --- a/app/graphql/resolvers/authored_merge_requests_resolver.rb +++ b/app/graphql/resolvers/authored_merge_requests_resolver.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module Resolvers - class AuthoredMergeRequestsResolver < UserMergeRequestsResolver + class AuthoredMergeRequestsResolver < UserMergeRequestsResolverBase + type ::Types::MergeRequestType.connection_type, null: true accept_assignee def user_role diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 2b8854fb4d0..87a63231b22 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -8,32 +8,81 @@ module Resolvers argument_class ::Types::BaseArgument - def self.single - @single ||= Class.new(self) do - def ready?(**args) - ready, early_return = super - [ready, select_result(early_return)] - end + def self.singular_type + return unless type - def resolve(**args) - select_result(super) - end + unwrapped = type.unwrap + + %i[node_type relay_node_type of_type itself].reduce(nil) do |t, m| + t || unwrapped.try(m) + end + end - def single? - true + def self.when_single(&block) + as_single << block + + # Have we been called after defining the single version of this resolver? + if @single.present? + @single.instance_exec(&block) + end + end + + def self.as_single + @as_single ||= [] + end + + def self.single_definition_blocks + ancestors.flat_map { |klass| klass.try(:as_single) || [] } + end + + def self.single + @single ||= begin + parent = self + klass = Class.new(self) do + type parent.singular_type, null: true + + def ready?(**args) + ready, early_return = super + [ready, select_result(early_return)] + end + + def resolve(**args) + select_result(super) + end + + def single? + true + end + + def select_result(results) + results&.first + end + + define_singleton_method :to_s do + "#{parent}.single" + end end - def select_result(results) - results&.first + single_definition_blocks.each do |definition| + klass.instance_exec(&definition) end + + klass end end def self.last + parent = self @last ||= Class.new(self.single) do + type parent.singular_type, null: true + def select_result(results) results&.last end + + define_singleton_method :to_s do + "#{parent}.last" + end end end @@ -68,14 +117,13 @@ module Resolvers end end + # TODO: remove! This should never be necessary + # Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/13984, + # since once we use that authorization approach, the object is guaranteed to + # be synchronized before any field. def synchronized_object strong_memoize(:synchronized_object) do - case object - when BatchLoader::GraphQL - object.sync - else - object - end + ::Gitlab::Graphql::Lazy.force(object) end end diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb index 3384b37e2ce..ef12dfa19ff 100644 --- a/app/graphql/resolvers/board_lists_resolver.rb +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -7,7 +7,7 @@ module Resolvers type Types::BoardListType, null: true - argument :id, GraphQL::ID_TYPE, + argument :id, Types::GlobalIDType[List], required: false, description: 'Find a list by its global ID' diff --git a/app/graphql/resolvers/boards_resolver.rb b/app/graphql/resolvers/boards_resolver.rb index 82efd92d33f..42b6ce03118 100644 --- a/app/graphql/resolvers/boards_resolver.rb +++ b/app/graphql/resolvers/boards_resolver.rb @@ -4,7 +4,7 @@ module Resolvers class BoardsResolver < BaseResolver type Types::BoardType, null: true - argument :id, GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Board], required: false, description: 'Find a board by its ID' @@ -23,10 +23,13 @@ module Resolvers private - def extract_board_id(gid) - return unless gid.present? + def extract_board_id(id) + return unless id.present? - GitlabSchema.parse_gid(gid, expected_type: ::Board).model_id + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = Types::GlobalIDType[Board].coerce_isolated_input(id) + id.model_id end end end diff --git a/app/graphql/resolvers/ci/jobs_resolver.rb b/app/graphql/resolvers/ci/jobs_resolver.rb new file mode 100644 index 00000000000..8a9ae42b375 --- /dev/null +++ b/app/graphql/resolvers/ci/jobs_resolver.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class JobsResolver < BaseResolver + alias_method :pipeline, :object + + argument :security_report_types, [Types::Security::ReportTypeEnum], + required: false, + description: 'Filter jobs by the type of security report they produce' + + def resolve(security_report_types: []) + if security_report_types.present? + ::Security::SecurityJobsFinder.new( + pipeline: pipeline, + job_types: security_report_types + ).execute + else + pipeline.statuses + end + end + end + end +end diff --git a/app/graphql/resolvers/ci/runner_setup_resolver.rb b/app/graphql/resolvers/ci/runner_setup_resolver.rb new file mode 100644 index 00000000000..241cd57f74b --- /dev/null +++ b/app/graphql/resolvers/ci/runner_setup_resolver.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerSetupResolver < BaseResolver + type Types::Ci::RunnerSetupType, null: true + + argument :platform, GraphQL::STRING_TYPE, + required: true, + description: 'Platform to generate the instructions for' + + argument :architecture, GraphQL::STRING_TYPE, + required: true, + description: 'Architecture to generate the instructions for' + + argument :project_id, ::Types::GlobalIDType[::Project], + required: false, + description: 'Project to register the runner for' + + argument :group_id, ::Types::GlobalIDType[::Group], + required: false, + description: 'Group to register the runner for' + + def resolve(platform:, architecture:, **args) + instructions = Gitlab::Ci::RunnerInstructions.new( + { current_user: current_user, os: platform, arch: architecture }.merge(target_param(args)) + ) + + { + install_instructions: instructions.install_script || other_install_instructions(platform), + register_instructions: instructions.register_command + } + ensure + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'User is not authorized to register a runner for the specified resource!' if instructions.errors.include?('Gitlab::Access::AccessDeniedError') + end + + private + + def other_install_instructions(platform) + Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS[platform.to_sym][:installation_instructions_url] + end + + def target_param(args) + project_param(args[:project_id]) || group_param(args[:group_id]) || {} + end + + def project_param(project_id) + return unless project_id + + { project: find_object(project_id) } + end + + def group_param(group_id) + return unless group_id + + { group: find_object(group_id) } + end + + def find_object(gid) + GlobalID::Locator.locate(gid) + end + end + end +end diff --git a/app/graphql/resolvers/commit_pipelines_resolver.rb b/app/graphql/resolvers/commit_pipelines_resolver.rb index 92a83523593..40af392200c 100644 --- a/app/graphql/resolvers/commit_pipelines_resolver.rb +++ b/app/graphql/resolvers/commit_pipelines_resolver.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true +# rubocop: disable Graphql/ResolverType module Resolvers class CommitPipelinesResolver < BaseResolver + # The GraphQL type here gets defined in this include include ::ResolvesPipelines alias_method :commit, :object @@ -11,3 +13,4 @@ module Resolvers end end end +# rubocop: enable Graphql/ResolverType diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb new file mode 100644 index 00000000000..4f2c8b98928 --- /dev/null +++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Concern that will eliminate N+1 queries for size-constrained +# collections of items. +# +# **note**: The resolver will never load more items than +# `@field.max_page_size` if defined, falling back to +# `context.schema.default_max_page_size`. +# +# provided that: +# +# - the query can be uniquely determined by the object and the arguments +# - the model class includes FromUnion +# - the model class defines a scalar primary key +# +# This comes at the cost of returning arrays, not relations, so we don't get +# any keyset pagination goodness. Consequently, this is only suitable for small-ish +# result sets, as the full result set will be loaded into memory. +# +# To enforce this, the resolver limits the size of result sets to +# `@field.max_page_size || context.schema.default_max_page_size`. +# +# **important**: If the cardinality of your collection is likely to be greater than 100, +# then you will want to pass `max_page_size:` as part of the field definition +# or (ideally) as part of the resolver `field_options`. +# +# How to implement: +# -------------------- +# +# Each including class operates on two generic parameters, A and R: +# - A is any Object that can be used as a Hash key. Instances of A +# are returned by `query_input` and then passed to `query_for`. +# - R is any subclass of ApplicationRecord that includes FromUnion. +# R must have a single scalar primary_key +# +# Classes must implement: +# - #model_class -> Class[R]. (Must respond to :primary_key, and :from_union) +# - #query_input(**kwargs) -> A (Must be hashable) +# - #query_for(A) -> ActiveRecord::Relation[R] +# +# Note the relationship between query_input and query_for, one of which +# consumes the input of the other +# (i.e. `resolve(**args).sync == query_for(query_input(**args)).to_a`). +# +# Classes may implement: +# - #item_found(A, R) (return value is ignored) +# - max_union_size Integer (the maximum number of queries to run in any one union) +module CachingArrayResolver + MAX_UNION_SIZE = 50 + + def resolve(**args) + key = query_input(**args) + + BatchLoader::GraphQL.for(key).batch(**batch) do |keys, loader| + if keys.size == 1 + # We can avoid the union entirely. + k = keys.first + limit(query_for(k)).each { |item| found(loader, k, item) } + else + queries = keys.map { |key| query_for(key) } + + queries.in_groups_of(max_union_size, false).each do |group| + by_id = model_class + .from_union(tag(group), remove_duplicates: false) + .group_by { |r| r[primary_key] } + + by_id.values.each do |item_group| + item = item_group.first + item_group.map(&:union_member_idx).each do |i| + found(loader, keys[i], item) + end + end + end + end + end + end + + # Override this to intercept the items once they are found + def item_found(query_input, item) + end + + def max_union_size + MAX_UNION_SIZE + end + + private + + def primary_key + @primary_key ||= (model_class.primary_key || raise("No primary key for #{model_class}")) + end + + def batch + { key: self.class, default_value: [] } + end + + def found(loader, key, value) + loader.call(key) do |vs| + item_found(key, value) + vs << value + end + end + + # Tag each row returned from each query with a the index of which query in + # the union it comes from. This lets us map the results back to the cache key. + def tag(queries) + queries.each_with_index.map do |q, i| + limit(q.select(all_fields, member_idx(i))) + end + end + + def limit(query) + query.limit(query_limit) # rubocop: disable CodeReuse/ActiveRecord + end + + def all_fields + model_class.arel_table[Arel.star] + end + + # rubocop: disable Graphql/Descriptions (false positive!) + def query_limit + field&.max_page_size.presence || context.schema.default_max_page_size + end + # rubocop: enable Graphql/Descriptions + + def member_idx(idx) + ::Arel::Nodes::SqlLiteral.new(idx.to_s).as('union_member_idx') + end +end diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index fe6fa0bb262..4715b867ecb 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -29,7 +29,7 @@ module IssueResolverArguments description: 'Usernames of users assigned to the issue' argument :assignee_id, GraphQL::STRING_TYPE, required: false, - description: 'ID of a user assigned to the issues, "none" and "any" values supported' + description: 'ID of a user assigned to the issues, "none" and "any" values are supported' argument :created_before, Types::TimeType, required: false, description: 'Issues created before this date' diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb index 61f23920ebb..d468047b539 100644 --- a/app/graphql/resolvers/concerns/looks_ahead.rb +++ b/app/graphql/resolvers/concerns/looks_ahead.rb @@ -4,6 +4,7 @@ module LooksAhead extend ActiveSupport::Concern included do + extras [:lookahead] attr_accessor :lookahead end diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb index 46d9e174deb..f061f5f1606 100644 --- a/app/graphql/resolvers/concerns/resolves_pipelines.rb +++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb @@ -4,7 +4,7 @@ module ResolvesPipelines extend ActiveSupport::Concern included do - type [Types::Ci::PipelineType], null: false + type Types::Ci::PipelineType.connection_type, null: false argument :status, Types::Ci::PipelineStatusEnum, required: false, diff --git a/app/graphql/resolvers/concerns/resolves_project.rb b/app/graphql/resolvers/concerns/resolves_project.rb index 3c5ce3dab01..b2ee7d7e850 100644 --- a/app/graphql/resolvers/concerns/resolves_project.rb +++ b/app/graphql/resolvers/concerns/resolves_project.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true module ResolvesProject + # Accepts EITHER one of + # - full_path: String (see Project#full_path) + # - project_id: GlobalID. Arguments should be typed as: `::Types::GlobalIDType[Project]` def resolve_project(full_path: nil, project_id: nil) unless full_path.present? ^ project_id.present? raise ::Gitlab::Graphql::Errors::ArgumentError, 'Incompatible arguments: projectId, projectPath.' diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb index 483372bbf63..790ff4f774f 100644 --- a/app/graphql/resolvers/concerns/resolves_snippets.rb +++ b/app/graphql/resolvers/concerns/resolves_snippets.rb @@ -4,9 +4,9 @@ module ResolvesSnippets extend ActiveSupport::Concern included do - type Types::SnippetType, null: false + type Types::SnippetType.connection_type, null: false - argument :ids, [GraphQL::ID_TYPE], + argument :ids, [::Types::GlobalIDType[::Snippet]], required: false, description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"' @@ -32,16 +32,15 @@ module ResolvesSnippets }.merge(options_by_type(args[:type])) end - def resolve_ids(ids) - Array.wrap(ids).map { |id| resolve_gid(id, :id) } - end - - def resolve_gid(gid, argument) - return unless gid.present? + def resolve_ids(ids, type = ::Types::GlobalIDType[::Snippet]) + Array.wrap(ids).map do |id| + next unless id.present? - GlobalID.parse(gid)&.model_id.tap do |id| - raise Gitlab::Graphql::Errors::ArgumentError, "Invalid global id format for param #{argument}" if id.nil? - end + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = type.coerce_isolated_input(id) + id.model_id + end.compact end def options_by_type(type) diff --git a/app/graphql/resolvers/container_repositories_resolver.rb b/app/graphql/resolvers/container_repositories_resolver.rb new file mode 100644 index 00000000000..b4b2893a3b8 --- /dev/null +++ b/app/graphql/resolvers/container_repositories_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + class ContainerRepositoriesResolver < BaseResolver + include ::Mutations::PackageEventable + + type Types::ContainerRepositoryType, null: true + + argument :name, GraphQL::STRING_TYPE, + required: false, + description: 'Filter the container repositories by their name' + + def resolve(name: nil) + ContainerRepositoriesFinder.new(user: current_user, subject: object, params: { name: name }) + .execute + .tap { track_event(:list_repositories, :container) } + end + end +end diff --git a/app/graphql/resolvers/design_management/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/design_at_version_resolver.rb index fd9b349f974..1b69efebe4e 100644 --- a/app/graphql/resolvers/design_management/design_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/design_at_version_resolver.rb @@ -9,7 +9,7 @@ module Resolvers authorize :read_design - argument :id, GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::DesignManagement::DesignAtVersion], required: true, description: 'The Global ID of the design at this version' @@ -18,7 +18,10 @@ module Resolvers end def find_object(id:) - dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::DesignManagement::DesignAtVersion].coerce_isolated_input(id) + dav = GitlabSchema.find_by_gid(id) return unless consistent?(dav) dav @@ -35,7 +38,7 @@ module Resolvers # that the DesignAtVersion as found by its ID does in fact belong # to this issue. def consistent?(dav) - issue.nil? || (dav&.design&.issue_id == issue.id) + issue.nil? || (dav.present? && dav.design&.issue_id == issue.id) end def issue diff --git a/app/graphql/resolvers/design_management/design_resolver.rb b/app/graphql/resolvers/design_management/design_resolver.rb index 05bdbbbe407..e0a68bae397 100644 --- a/app/graphql/resolvers/design_management/design_resolver.rb +++ b/app/graphql/resolvers/design_management/design_resolver.rb @@ -3,7 +3,9 @@ module Resolvers module DesignManagement class DesignResolver < BaseResolver - argument :id, GraphQL::ID_TYPE, + type ::Types::DesignManagement::DesignType, null: true + + argument :id, ::Types::GlobalIDType[::DesignManagement::Design], required: false, description: 'Find a design by its ID' @@ -50,7 +52,11 @@ module Resolvers end def parse_gid(gid) - GitlabSchema.parse_gid(gid, expected_type: ::DesignManagement::Design).model_id + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + gid = ::Types::GlobalIDType[::DesignManagement::Design].coerce_isolated_input(gid) + + gid.model_id end end end diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb index 955ea6304e0..c588142ea6b 100644 --- a/app/graphql/resolvers/design_management/designs_resolver.rb +++ b/app/graphql/resolvers/design_management/designs_resolver.rb @@ -3,16 +3,18 @@ module Resolvers module DesignManagement class DesignsResolver < BaseResolver - argument :ids, - [GraphQL::ID_TYPE], + DesignID = ::Types::GlobalIDType[::DesignManagement::Design] + VersionID = ::Types::GlobalIDType[::DesignManagement::Version] + + type ::Types::DesignManagement::DesignType.connection_type, null: true + + argument :ids, [DesignID], required: false, description: 'Filters designs by their ID' - argument :filenames, - [GraphQL::STRING_TYPE], + argument :filenames, [GraphQL::STRING_TYPE], required: false, description: 'Filters designs by their filename' - argument :at_version, - GraphQL::ID_TYPE, + argument :at_version, VersionID, required: false, description: 'Filters designs to only those that existed at the version. ' \ 'If argument is omitted or nil then all designs will reflect the latest version' @@ -36,11 +38,20 @@ module Resolvers def version(at_version) return unless at_version - GitlabSchema.object_from_id(at_version, expected_type: ::DesignManagement::Version)&.sync + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + at_version = VersionID.coerce_isolated_input(at_version) + # TODO: when we get promises use this to make resolve lazy + Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(at_version)) end - def design_ids(ids) - ids&.map { |id| GlobalID.parse(id, expected_type: ::DesignManagement::Design).model_id } + def design_ids(gids) + return if gids.nil? + + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + gids = gids.map { |id| DesignID.coerce_isolated_input(id) } + gids.map(&:model_id) end def issue diff --git a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb index 03f7908780c..70021057f71 100644 --- a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb @@ -5,17 +5,20 @@ module Resolvers module Version # Resolver for a DesignAtVersion object given an implicit version context class DesignAtVersionResolver < BaseResolver + DesignAtVersionID = ::Types::GlobalIDType[::DesignManagement::DesignAtVersion] + DesignID = ::Types::GlobalIDType[::DesignManagement::Design] + include Gitlab::Graphql::Authorize::AuthorizeResource type Types::DesignManagement::DesignAtVersionType, null: true authorize :read_design - argument :id, GraphQL::ID_TYPE, + argument :id, DesignAtVersionID, required: false, as: :design_at_version_id, description: 'The ID of the DesignAtVersion' - argument :design_id, GraphQL::ID_TYPE, + argument :design_id, DesignID, required: false, description: 'The ID of a specific design' argument :filename, GraphQL::STRING_TYPE, @@ -29,6 +32,11 @@ module Resolvers def resolve(design_id: nil, filename: nil, design_at_version_id: nil) validate_arguments(design_id, filename, design_at_version_id) + # TODO: remove this when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + design_id &&= DesignID.coerce_isolated_input(design_id) + design_at_version_id &&= DesignAtVersionID.coerce_isolated_input(design_at_version_id) + return unless Ability.allowed?(current_user, :read_design, issue) return specific_design_at_version(design_at_version_id) if design_at_version_id @@ -49,7 +57,7 @@ module Resolvers end def specific_design_at_version(id) - dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion) + dav = GitlabSchema.find_by_gid(id) return unless consistent?(dav) dav @@ -65,8 +73,8 @@ module Resolvers dav.design.visible_in?(version) end - def find(id, filename) - ids = [parse_design_id(id).model_id] if id + def find(gid, filename) + ids = [gid.model_id] if gid filenames = [filename] if filename ::DesignManagement::DesignsFinder @@ -74,10 +82,6 @@ module Resolvers .execute end - def parse_design_id(id) - GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design) - end - def issue version.issue end diff --git a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb index 5ccb2f3e311..a129d8620d4 100644 --- a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb @@ -11,8 +11,9 @@ module Resolvers authorize :read_design - argument :ids, - [GraphQL::ID_TYPE], + DesignID = ::Types::GlobalIDType[::DesignManagement::Design] + + argument :ids, [DesignID], required: false, description: 'Filters designs by their ID' argument :filenames, @@ -31,16 +32,19 @@ module Resolvers private def find(ids, filenames) - ids = ids&.map { |id| parse_design_id(id).model_id } - ::DesignManagement::DesignsFinder.new(issue, current_user, - ids: ids, + ids: design_ids(ids), filenames: filenames, visible_at_version: version) end - def parse_design_id(id) - GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design) + def design_ids(gids) + return if gids.nil? + + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + gids = gids.map { |id| DesignID.coerce_isolated_input(id) } + gids.map(&:model_id) end def issue diff --git a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb index 9e729172881..ecd7ab3ee45 100644 --- a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb +++ b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb @@ -11,20 +11,25 @@ module Resolvers alias_method :collection, :object + VersionID = ::Types::GlobalIDType[::DesignManagement::Version] + argument :sha, GraphQL::STRING_TYPE, required: false, description: "The SHA256 of a specific version" - argument :id, GraphQL::ID_TYPE, + argument :id, VersionID, + as: :version_id, required: false, description: 'The Global ID of the version' - def resolve(id: nil, sha: nil) - check_args(id, sha) + def resolve(version_id: nil, sha: nil) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + version_id &&= VersionID.coerce_isolated_input(version_id) - gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id + check_args(version_id, sha) ::DesignManagement::VersionsFinder - .new(collection, current_user, sha: sha, version_id: gid&.model_id) + .new(collection, current_user, sha: sha, version_id: version_id&.model_id) .execute .first end diff --git a/app/graphql/resolvers/design_management/version_resolver.rb b/app/graphql/resolvers/design_management/version_resolver.rb index b0e0843e6c8..1bc9c1a7cd6 100644 --- a/app/graphql/resolvers/design_management/version_resolver.rb +++ b/app/graphql/resolvers/design_management/version_resolver.rb @@ -9,7 +9,7 @@ module Resolvers authorize :read_design - argument :id, GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::DesignManagement::Version], required: true, description: 'The Global ID of the version' @@ -18,7 +18,11 @@ module Resolvers end def find_object(id:) - GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::DesignManagement::Version].coerce_isolated_input(id) + + GitlabSchema.find_by_gid(id) end end end diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb index a62258dad5c..23858c8e991 100644 --- a/app/graphql/resolvers/design_management/versions_resolver.rb +++ b/app/graphql/resolvers/design_management/versions_resolver.rb @@ -7,12 +7,14 @@ module Resolvers alias_method :design_or_collection, :object + VersionID = ::Types::GlobalIDType[::DesignManagement::Version] + argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE, as: :sha, required: false, description: 'The SHA256 of the most recent acceptable version' - argument :earlier_or_equal_to_id, GraphQL::ID_TYPE, + argument :earlier_or_equal_to_id, VersionID, as: :id, required: false, description: 'The Global ID of the most recent acceptable version' @@ -23,6 +25,9 @@ module Resolvers end def resolve(parent: nil, id: nil, sha: nil) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id &&= VersionID.coerce_isolated_input(id) version = cutoff(parent, id, sha) raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present? @@ -47,8 +52,7 @@ module Resolvers end end - def specific_version(id, sha) - gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id + def specific_version(gid, sha) find(sha: sha, version_id: gid&.model_id).first end @@ -58,8 +62,8 @@ module Resolvers .execute end - def by_id(id) - GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version).sync + def by_id(gid) + ::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(gid)) end # Find an `at_version` argument passed to a parent node. @@ -69,7 +73,11 @@ module Resolvers # for consistency we should only present versions up to the given # version here. def at_version_arg(parent) - ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4) + # TODO: remove coercion when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + version_id = ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4) + version_id &&= VersionID.coerce_isolated_input(version_id) + version_id end end end diff --git a/app/graphql/resolvers/echo_resolver.rb b/app/graphql/resolvers/echo_resolver.rb index fe0b1893a23..6b85b700712 100644 --- a/app/graphql/resolvers/echo_resolver.rb +++ b/app/graphql/resolvers/echo_resolver.rb @@ -2,15 +2,16 @@ module Resolvers class EchoResolver < BaseResolver + type ::GraphQL::STRING_TYPE, null: false description 'Testing endpoint to validate the API with' argument :text, GraphQL::STRING_TYPE, required: true, description: 'Text to echo back' - def resolve(**args) - username = context[:current_user]&.username + def resolve(text:) + username = current_user&.username - "#{username.inspect} says: #{args[:text]}" + "#{username.inspect} says: #{text}" end end end diff --git a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb index 5027403e95c..09e76dba645 100644 --- a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb @@ -3,19 +3,22 @@ module Resolvers module ErrorTracking class SentryDetailedErrorResolver < BaseResolver - argument :id, GraphQL::ID_TYPE, + type Types::ErrorTracking::SentryDetailedErrorType, null: true + + argument :id, ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError], required: true, description: 'ID of the Sentry issue' - def resolve(**args) - current_user = context[:current_user] - issue_id = GlobalID.parse(args[:id])&.model_id + def resolve(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError].coerce_isolated_input(id) # Get data from Sentry response = ::ErrorTracking::IssueDetailsService.new( project, current_user, - { issue_id: issue_id } + { issue_id: id.model_id } ).execute issue = response[:issue] issue.gitlab_project = project if issue diff --git a/app/graphql/resolvers/error_tracking/sentry_error_collection_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_error_collection_resolver.rb index e4b4854c273..d47cc2bae56 100644 --- a/app/graphql/resolvers/error_tracking/sentry_error_collection_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_error_collection_resolver.rb @@ -3,6 +3,8 @@ module Resolvers module ErrorTracking class SentryErrorCollectionResolver < BaseResolver + type Types::ErrorTracking::SentryErrorCollectionType, null: true + def resolve(**args) project = object diff --git a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb index c365baaf475..669b487db10 100644 --- a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb @@ -3,18 +3,20 @@ module Resolvers module ErrorTracking class SentryErrorStackTraceResolver < BaseResolver - argument :id, GraphQL::ID_TYPE, + argument :id, ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError], required: true, description: 'ID of the Sentry issue' - def resolve(**args) - issue_id = GlobalID.parse(args[:id])&.model_id + def resolve(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError].coerce_isolated_input(id) # Get data from Sentry response = ::ErrorTracking::IssueLatestEventService.new( project, current_user, - { issue_id: issue_id } + { issue_id: id.model_id } ).execute event = response[:latest_event] diff --git a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb index 79f99709505..c5cf924ce7f 100644 --- a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb @@ -3,6 +3,8 @@ module Resolvers module ErrorTracking class SentryErrorsResolver < BaseResolver + type Types::ErrorTracking::SentryErrorType.connection_type, null: true + def resolve(**args) args[:cursor] = args.delete(:after) project = object.project diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb index 1fa6c78e730..1db0ab08e31 100644 --- a/app/graphql/resolvers/group_issues_resolver.rb +++ b/app/graphql/resolvers/group_issues_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from IssuesResolver) module Resolvers class GroupIssuesResolver < IssuesResolver diff --git a/app/graphql/resolvers/group_members_resolver.rb b/app/graphql/resolvers/group_members_resolver.rb index f34c873a8a9..d3aa376c29c 100644 --- a/app/graphql/resolvers/group_members_resolver.rb +++ b/app/graphql/resolvers/group_members_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class GroupMembersResolver < MembersResolver + type Types::GroupMemberType.connection_type, null: true + authorize :read_group_member private diff --git a/app/graphql/resolvers/group_merge_requests_resolver.rb b/app/graphql/resolvers/group_merge_requests_resolver.rb index 5ee72e3f781..2bad974daf7 100644 --- a/app/graphql/resolvers/group_merge_requests_resolver.rb +++ b/app/graphql/resolvers/group_merge_requests_resolver.rb @@ -6,6 +6,8 @@ module Resolvers alias_method :group, :synchronized_object + type Types::MergeRequestType.connection_type, null: true + include_subgroups 'merge requests' accept_assignee accept_author diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb index 8d34cea4fa1..83b82e2720b 100644 --- a/app/graphql/resolvers/group_milestones_resolver.rb +++ b/app/graphql/resolvers/group_milestones_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from MilestonesResolver) module Resolvers class GroupMilestonesResolver < MilestonesResolver @@ -6,6 +7,8 @@ module Resolvers required: false, description: 'Also return milestones in all subgroups and subprojects' + type Types::MilestoneType.connection_type, null: true + private def parent_id_parameters(args) diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 396ae02ae13..dd35219454f 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -12,7 +12,7 @@ module Resolvers required: false, default_value: 'created_desc' - type Types::IssueType, null: true + type Types::IssueType.connection_type, null: true NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc label_priority_asc label_priority_desc diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb index 88a1ab71c45..523642e912f 100644 --- a/app/graphql/resolvers/members_resolver.rb +++ b/app/graphql/resolvers/members_resolver.rb @@ -5,6 +5,8 @@ module Resolvers include Gitlab::Graphql::Authorize::AuthorizeResource include LooksAhead + type Types::MemberInterface.connection_type, null: true + argument :search, GraphQL::STRING_TYPE, required: false, description: 'Search query' diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb index b95e46d9cff..6590dfdc78c 100644 --- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb +++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true +# rubocop: disable Graphql/ResolverType module Resolvers class MergeRequestPipelinesResolver < BaseResolver + # The GraphQL type here gets defined in this include include ::ResolvesPipelines alias_method :merge_request, :object @@ -18,3 +20,4 @@ module Resolvers end end end +# rubocop: enable Graphql/ResolverType diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb index a47a128ea32..4cad65fa697 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -6,6 +6,8 @@ module Resolvers alias_method :project, :synchronized_object + type ::Types::MergeRequestType, null: true + argument :iid, GraphQL::STRING_TYPE, required: true, as: :iids, diff --git a/app/graphql/resolvers/metadata_resolver.rb b/app/graphql/resolvers/metadata_resolver.rb index 3a79e6434fb..26bfa81038c 100644 --- a/app/graphql/resolvers/metadata_resolver.rb +++ b/app/graphql/resolvers/metadata_resolver.rb @@ -5,7 +5,7 @@ module Resolvers type Types::MetadataType, null: false def resolve(**args) - { version: Gitlab::VERSION, revision: Gitlab.revision } + ::InstanceMetadata.new end end end diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 84712b674db..564e388d571 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -25,7 +25,7 @@ module Resolvers required: false, description: 'A date that the milestone contains' - type Types::MilestoneType, null: true + type Types::MilestoneType.connection_type, null: true def resolve(**args) validate_timeframe_params!(args) diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb index c221cb9aed6..9f57c8f3405 100644 --- a/app/graphql/resolvers/namespace_projects_resolver.rb +++ b/app/graphql/resolvers/namespace_projects_resolver.rb @@ -23,7 +23,6 @@ module Resolvers # The namespace could have been loaded in batch by `BatchLoader`. # At this point we need the `id` or the `full_path` of the namespace # to query for projects, so make sure it's loaded and not `nil` before continuing. - namespace = object.respond_to?(:sync) ? object.sync : object return Project.none if namespace.nil? query = include_subgroups ? namespace.all_projects.with_route : namespace.projects.with_route @@ -41,6 +40,14 @@ module Resolvers complexity = super complexity + 10 end + + private + + def namespace + strong_memoize(:namespace) do + object.respond_to?(:sync) ? object.sync : object + end + end end end diff --git a/app/graphql/resolvers/project_members_resolver.rb b/app/graphql/resolvers/project_members_resolver.rb index 1ca4e81f397..e64e8b845a5 100644 --- a/app/graphql/resolvers/project_members_resolver.rb +++ b/app/graphql/resolvers/project_members_resolver.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from MembersResolver) module Resolvers class ProjectMembersResolver < MembersResolver - type Types::MemberInterface, null: true - authorize :read_project_member private diff --git a/app/graphql/resolvers/project_merge_requests_resolver.rb b/app/graphql/resolvers/project_merge_requests_resolver.rb index ba13cb6e52c..bf082c0b182 100644 --- a/app/graphql/resolvers/project_merge_requests_resolver.rb +++ b/app/graphql/resolvers/project_merge_requests_resolver.rb @@ -2,6 +2,7 @@ module Resolvers class ProjectMergeRequestsResolver < MergeRequestsResolver + type ::Types::MergeRequestType.connection_type, null: true accept_assignee accept_author end diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb index 976fc300b87..c88c9ce7219 100644 --- a/app/graphql/resolvers/project_milestones_resolver.rb +++ b/app/graphql/resolvers/project_milestones_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from MilestonesResolver) module Resolvers class ProjectMilestonesResolver < MilestonesResolver @@ -6,6 +7,8 @@ module Resolvers required: false, description: "Also return milestones in the project's parent group and its ancestors" + type Types::MilestoneType.connection_type, null: true + private def parent_id_parameters(args) diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb index 181c1e77109..4cf47dbdc60 100644 --- a/app/graphql/resolvers/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class ProjectPipelineResolver < BaseResolver + type ::Types::Ci::PipelineType, null: true + alias_method :project, :object argument :iid, GraphQL::ID_TYPE, diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb index 86094c46c2a..0171473a77f 100644 --- a/app/graphql/resolvers/project_pipelines_resolver.rb +++ b/app/graphql/resolvers/project_pipelines_resolver.rb @@ -1,13 +1,28 @@ # frozen_string_literal: true +# The GraphQL type here gets defined in +# https://gitlab.com/gitlab-org/gitlab/blob/master/app/graphql/resolvers/concerns/resolves_pipelines.rb#L7 +# rubocop: disable Graphql/ResolverType module Resolvers class ProjectPipelinesResolver < BaseResolver + include LooksAhead include ResolvesPipelines alias_method :project, :object - def resolve(**args) - resolve_pipelines(project, args) + def resolve_with_lookahead(**args) + apply_lookahead(resolve_pipelines(project, args)) + end + + private + + def preloads + { + jobs: [:statuses], + upstream: [:triggered_by_pipeline], + downstream: [:triggered_pipelines] + } end end end +# rubocop: enable Graphql/ResolverType diff --git a/app/graphql/resolvers/projects/jira_imports_resolver.rb b/app/graphql/resolvers/projects/jira_imports_resolver.rb index aa9b7139f38..efd45c2c465 100644 --- a/app/graphql/resolvers/projects/jira_imports_resolver.rb +++ b/app/graphql/resolvers/projects/jira_imports_resolver.rb @@ -3,6 +3,8 @@ module Resolvers module Projects class JiraImportsResolver < BaseResolver + type Types::JiraImportType.connection_type, null: true + include Gitlab::Graphql::Authorize::AuthorizeResource alias_method :project, :object diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb index d017f973e17..31f42d305b0 100644 --- a/app/graphql/resolvers/projects/jira_projects_resolver.rb +++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb @@ -5,6 +5,8 @@ module Resolvers class JiraProjectsResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource + type Types::Projects::Services::JiraProjectType.connection_type, null: true + argument :name, GraphQL::STRING_TYPE, required: false, diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb index 40c64c24513..17d81e21c28 100644 --- a/app/graphql/resolvers/projects/services_resolver.rb +++ b/app/graphql/resolvers/projects/services_resolver.rb @@ -5,6 +5,8 @@ module Resolvers class ServicesResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource + type Types::Projects::ServiceType.connection_type, null: true + argument :active, GraphQL::BOOLEAN_TYPE, required: false, diff --git a/app/graphql/resolvers/projects/snippets_resolver.rb b/app/graphql/resolvers/projects/snippets_resolver.rb index 22895a24054..448918be2f5 100644 --- a/app/graphql/resolvers/projects/snippets_resolver.rb +++ b/app/graphql/resolvers/projects/snippets_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets) module Resolvers module Projects diff --git a/app/graphql/resolvers/releases_resolver.rb b/app/graphql/resolvers/releases_resolver.rb index 85892c2abeb..8e8127cf279 100644 --- a/app/graphql/resolvers/releases_resolver.rb +++ b/app/graphql/resolvers/releases_resolver.rb @@ -4,6 +4,10 @@ module Resolvers class ReleasesResolver < BaseResolver type Types::ReleaseType.connection_type, null: true + argument :sort, Types::ReleaseSortEnum, + required: false, default_value: :released_at_desc, + description: 'Sort releases by this criteria' + alias_method :project, :object # This resolver has a custom singular resolver @@ -11,12 +15,20 @@ module Resolvers Resolvers::ReleaseResolver end - def resolve(**args) + SORT_TO_PARAMS_MAP = { + released_at_desc: { order_by: 'released_at', sort: 'desc' }, + released_at_asc: { order_by: 'released_at', sort: 'asc' }, + created_desc: { order_by: 'created_at', sort: 'desc' }, + created_asc: { order_by: 'created_at', sort: 'asc' } + }.freeze + + def resolve(sort:) return unless Feature.enabled?(:graphql_release_data, project, default_enabled: true) ReleasesFinder.new( project, - current_user + current_user, + SORT_TO_PARAMS_MAP[sort] ).execute end end diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb index dc28358cab6..3a0dcb50faf 100644 --- a/app/graphql/resolvers/snippets/blobs_resolver.rb +++ b/app/graphql/resolvers/snippets/blobs_resolver.rb @@ -5,6 +5,8 @@ module Resolvers class BlobsResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource + type Types::Snippets::BlobType.connection_type, null: true + alias_method :snippet, :object argument :paths, [GraphQL::STRING_TYPE], diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb index 530a288a25b..77099565df0 100644 --- a/app/graphql/resolvers/snippets_resolver.rb +++ b/app/graphql/resolvers/snippets_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets) module Resolvers class SnippetsResolver < BaseResolver @@ -8,11 +9,11 @@ module Resolvers alias_method :user, :object - argument :author_id, GraphQL::ID_TYPE, + argument :author_id, ::Types::GlobalIDType[::User], required: false, description: 'The ID of an author' - argument :project_id, GraphQL::ID_TYPE, + argument :project_id, ::Types::GlobalIDType[::Project], required: false, description: 'The ID of a project' @@ -36,9 +37,11 @@ module Resolvers private def snippet_finder_params(args) + # TODO: remove the type arguments when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 super - .merge(author: resolve_gid(args[:author_id], :author), - project: resolve_gid(args[:project_id], :project), + .merge(author: resolve_ids(args[:author_id], ::Types::GlobalIDType[::User]), + project: resolve_ids(args[:project_id], ::Types::GlobalIDType[::Project]), explore: args[:explore]) end end diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb index bd5f8f274cd..9a8f7a71154 100644 --- a/app/graphql/resolvers/todo_resolver.rb +++ b/app/graphql/resolvers/todo_resolver.rb @@ -2,7 +2,7 @@ module Resolvers class TodoResolver < BaseResolver - type Types::TodoType, null: true + type Types::TodoType.connection_type, null: true alias_method :target, :object diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb index 5aad1c71b40..075a1929c47 100644 --- a/app/graphql/resolvers/tree_resolver.rb +++ b/app/graphql/resolvers/tree_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class TreeResolver < BaseResolver + type Types::Tree::TreeType, null: true + argument :path, GraphQL::STRING_TYPE, required: false, default_value: '', diff --git a/app/graphql/resolvers/user_merge_requests_resolver.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb index b0d6e159f73..47967fe69f9 100644 --- a/app/graphql/resolvers/user_merge_requests_resolver.rb +++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true module Resolvers - class UserMergeRequestsResolver < MergeRequestsResolver + class UserMergeRequestsResolverBase < MergeRequestsResolver include ResolvesProject argument :project_path, GraphQL::STRING_TYPE, required: false, description: 'The full-path of the project the authored merge requests should be in. Incompatible with projectId.' - argument :project_id, GraphQL::ID_TYPE, + argument :project_id, ::Types::GlobalIDType[::Project], required: false, description: 'The global ID of the project the authored merge requests should be in. Incompatible with projectPath.' @@ -50,8 +50,10 @@ module Resolvers end def load_project(project_path, project_id) - @project = resolve_project(full_path: project_path, project_id: project_id) - @project = @project.sync if @project.respond_to?(:sync) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + project_id &&= ::Types::GlobalIDType[::Project].coerce_isolated_input(project_id) + @project = ::Gitlab::Graphql::Lazy.force(resolve_project(full_path: project_path, project_id: project_id)) end def no_results_possible?(args) diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb index a34cecba491..06c1f0cb42d 100644 --- a/app/graphql/resolvers/user_resolver.rb +++ b/app/graphql/resolvers/user_resolver.rb @@ -6,7 +6,7 @@ module Resolvers type Types::UserType, null: true - argument :id, GraphQL::ID_TYPE, + argument :id, Types::GlobalIDType[User], required: false, description: 'ID of the User' diff --git a/app/graphql/resolvers/users/group_count_resolver.rb b/app/graphql/resolvers/users/group_count_resolver.rb new file mode 100644 index 00000000000..5033c26554a --- /dev/null +++ b/app/graphql/resolvers/users/group_count_resolver.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resolvers + module Users + class GroupCountResolver < BaseResolver + alias_method :user, :object + + def resolve(**args) + return unless can_read_group_count? + + BatchLoader::GraphQL.for(user.id).batch do |user_ids, loader| + results = UserGroupsCounter.new(user_ids).execute + + results.each do |user_id, count| + loader.call(user_id, count) + end + end + end + + def can_read_group_count? + current_user&.can?(:read_group_count, user) + end + end + end +end diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb index d757640b5ff..c2d42437ffd 100644 --- a/app/graphql/resolvers/users/snippets_resolver.rb +++ b/app/graphql/resolvers/users/snippets_resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from ResolvesSnippets) module Resolvers module Users diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb index 110a283b42e..f5838642141 100644 --- a/app/graphql/resolvers/users_resolver.rb +++ b/app/graphql/resolvers/users_resolver.rb @@ -4,6 +4,7 @@ module Resolvers class UsersResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource + type Types::UserType.connection_type, null: true description 'Find Users' argument :ids, [GraphQL::ID_TYPE], @@ -18,10 +19,14 @@ module Resolvers required: false, default_value: 'created_desc' - def resolve(ids: nil, usernames: nil, sort: nil) + argument :search, GraphQL::STRING_TYPE, + required: false, + description: "Query to search users by name, username, or primary email." + + def resolve(ids: nil, usernames: nil, sort: nil, search: nil) authorize! - ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort)).execute + ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search)).execute end def ready?(**args) @@ -42,11 +47,12 @@ module Resolvers private - def finder_params(ids, usernames, sort) + def finder_params(ids, usernames, sort, search) params = {} params[:sort] = sort if sort params[:username] = usernames if usernames params[:id] = parse_gids(ids) if ids + params[:search] = search if search params end diff --git a/app/graphql/types/alert_management/http_integration_type.rb b/app/graphql/types/alert_management/http_integration_type.rb new file mode 100644 index 00000000000..88782050b94 --- /dev/null +++ b/app/graphql/types/alert_management/http_integration_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + class HttpIntegrationType < BaseObject + graphql_name 'AlertManagementHttpIntegration' + description 'An endpoint and credentials used to accept alerts for a project' + + implements(Types::AlertManagement::IntegrationType) + + authorize :admin_operations + + def type + :http + end + + def api_url + nil + end + end + end +end diff --git a/app/graphql/types/alert_management/integration_type.rb b/app/graphql/types/alert_management/integration_type.rb new file mode 100644 index 00000000000..bf599885584 --- /dev/null +++ b/app/graphql/types/alert_management/integration_type.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + module IntegrationType + include Types::BaseInterface + graphql_name 'AlertManagementIntegration' + + field :id, + GraphQL::ID_TYPE, + null: false, + description: 'ID of the integration' + + field :type, + AlertManagement::IntegrationTypeEnum, + null: false, + description: 'Type of integration' + + field :name, + GraphQL::STRING_TYPE, + null: true, + description: 'Name of the integration' + + field :active, + GraphQL::BOOLEAN_TYPE, + null: true, + description: 'Whether the endpoint is currently accepting alerts' + + field :token, + GraphQL::STRING_TYPE, + null: true, + description: 'Token used to authenticate alert notification requests' + + field :url, + GraphQL::STRING_TYPE, + null: true, + description: 'Endpoint which accepts alert notifications' + + field :api_url, + GraphQL::STRING_TYPE, + null: true, + description: 'URL at which Prometheus metrics can be queried to populate the metrics dashboard' + + definition_methods do + def resolve_type(object, context) + if object.is_a?(::PrometheusService) + Types::AlertManagement::PrometheusIntegrationType + else + Types::AlertManagement::HttpIntegrationType + end + end + end + + orphan_types Types::AlertManagement::PrometheusIntegrationType, + Types::AlertManagement::HttpIntegrationType + end + end +end diff --git a/app/graphql/types/alert_management/integration_type_enum.rb b/app/graphql/types/alert_management/integration_type_enum.rb new file mode 100644 index 00000000000..2f9be549e58 --- /dev/null +++ b/app/graphql/types/alert_management/integration_type_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + class IntegrationTypeEnum < BaseEnum + graphql_name 'AlertManagementIntegrationType' + description 'Values of types of integrations' + + value 'PROMETHEUS', 'Prometheus integration', value: :prometheus + value 'HTTP', 'Integration with any monitoring tool', value: :http + end + end +end diff --git a/app/graphql/types/alert_management/prometheus_integration_type.rb b/app/graphql/types/alert_management/prometheus_integration_type.rb new file mode 100644 index 00000000000..f605e325b8b --- /dev/null +++ b/app/graphql/types/alert_management/prometheus_integration_type.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + class PrometheusIntegrationType < BaseObject + include ::Gitlab::Routing + + graphql_name 'AlertManagementPrometheusIntegration' + description 'An endpoint and credentials used to accept Prometheus alerts for a project' + + implements(Types::AlertManagement::IntegrationType) + + authorize :admin_project + + alias_method :prometheus_service, :object + + def name + prometheus_service.title + end + + def type + :prometheus + end + + def token + prometheus_service.project&.alerting_setting&.token + end + + def url + prometheus_service.project && notify_project_prometheus_alerts_url(prometheus_service.project, format: :json) + end + + def active + prometheus_service.manual_configuration? + end + end + end +end diff --git a/app/graphql/types/availability_enum.rb b/app/graphql/types/availability_enum.rb new file mode 100644 index 00000000000..61686b9359f --- /dev/null +++ b/app/graphql/types/availability_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class AvailabilityEnum < BaseEnum + graphql_name 'AvailabilityEnum' + description 'User availability status' + + ::UserStatus.availabilities.keys.each do |availability_value| + value availability_value.upcase, value: availability_value, description: availability_value.titleize + end + end +end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 70e665f8fc3..9c36c83d4a3 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -8,6 +8,12 @@ module Types field_class Types::BaseField + def self.accepts(*types) + @accepts ||= [] + @accepts += types + @accepts + end + # All graphql fields exposing an id, should expose a global id. def id GitlabSchema.id_from_object(object) @@ -16,5 +22,13 @@ module Types def current_user context[:current_user] end + + def self.assignable?(object) + assignable = accepts + + return true if assignable.blank? + + assignable.any? { |cls| object.is_a?(cls) } + end end end diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb index f5dc9e08427..2a7b318e283 100644 --- a/app/graphql/types/board_type.rb +++ b/app/graphql/types/board_type.rb @@ -4,7 +4,7 @@ module Types class BoardType < BaseObject graphql_name 'Board' description 'Represents a project or group board' - + accepts ::Board authorize :read_board field :id, type: GraphQL::ID_TYPE, null: false, diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb index f4a50115ee6..6d8af400ac4 100644 --- a/app/graphql/types/ci/detailed_status_type.rb +++ b/app/graphql/types/ci/detailed_status_type.rb @@ -30,7 +30,7 @@ module Types if obj.has_action? { button_title: obj.action_button_title, - icon: obj.icon, + icon: obj.action_icon, method: obj.action_method, path: obj.action_path, title: obj.action_title diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 0ee1ad47b62..feaff4e81d8 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -6,6 +6,9 @@ module Types class JobType < BaseObject graphql_name 'CiJob' + field :pipeline, Types::Ci::PipelineType, null: false, + description: 'Pipeline the job belongs to', + resolve: -> (build, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, build.pipeline_id).find } field :name, GraphQL::STRING_TYPE, null: true, description: 'Name of the job' field :needs, JobType.connection_type, null: true, diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index c508b746317..c25db39f600 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -13,49 +13,89 @@ module Types field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the pipeline' + field :iid, GraphQL::STRING_TYPE, null: false, description: 'Internal ID of the pipeline' field :sha, GraphQL::STRING_TYPE, null: false, description: "SHA of the pipeline's commit" + field :before_sha, GraphQL::STRING_TYPE, null: true, description: 'Base SHA of the source branch' + field :status, PipelineStatusEnum, null: false, description: "Status of the pipeline (#{::Ci::Pipeline.all_state_names.compact.join(', ').upcase})" + field :detailed_status, Types::Ci::DetailedStatusType, null: false, description: 'Detailed status of the pipeline', resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } + field :config_source, PipelineConfigSourceEnum, null: true, description: "Config source of the pipeline (#{::Enums::Ci::Pipeline.config_sources.keys.join(', ').upcase})" + field :duration, GraphQL::INT_TYPE, null: true, description: 'Duration of the pipeline in seconds' + field :coverage, GraphQL::FLOAT_TYPE, null: true, description: 'Coverage percentage' + field :created_at, Types::TimeType, null: false, description: "Timestamp of the pipeline's creation" + field :updated_at, Types::TimeType, null: false, description: "Timestamp of the pipeline's last activity" + field :started_at, Types::TimeType, null: true, description: 'Timestamp when the pipeline was started' + field :finished_at, Types::TimeType, null: true, description: "Timestamp of the pipeline's completion" + field :committed_at, Types::TimeType, null: true, description: "Timestamp of the pipeline's commit" + field :stages, Types::Ci::StageType.connection_type, null: true, description: 'Stages of the pipeline', extras: [:lookahead], resolver: Resolvers::Ci::PipelineStagesResolver + field :user, Types::UserType, null: true, description: 'Pipeline user', resolve: -> (pipeline, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, pipeline.user_id).find } + field :retryable, GraphQL::BOOLEAN_TYPE, description: 'Specifies if a pipeline can be retried', method: :retryable?, null: false + field :cancelable, GraphQL::BOOLEAN_TYPE, description: 'Specifies if a pipeline can be canceled', method: :cancelable?, null: false + + field :jobs, + ::Types::Ci::JobType.connection_type, + null: true, + description: 'Jobs belonging to the pipeline', + resolver: ::Resolvers::Ci::JobsResolver + + field :source_job, Types::Ci::JobType, null: true, + description: 'Job where pipeline was triggered from' + + field :downstream, Types::Ci::PipelineType.connection_type, null: true, + description: 'Pipelines this pipeline will trigger', + method: :triggered_pipelines_with_preloads + + field :upstream, Types::Ci::PipelineType, null: true, + description: 'Pipeline that triggered the pipeline', + method: :triggered_by_pipeline + + field :path, GraphQL::STRING_TYPE, null: true, + description: "Relative path to the pipeline's page", + resolve: -> (obj, _args, _ctx) { ::Gitlab::Routing.url_helpers.project_pipeline_path(obj.project, obj) } + + field :project, Types::ProjectType, null: true, + description: 'Project the pipeline belongs to' end end end diff --git a/app/graphql/types/ci/runner_setup_type.rb b/app/graphql/types/ci/runner_setup_type.rb new file mode 100644 index 00000000000..66abcf65adf --- /dev/null +++ b/app/graphql/types/ci/runner_setup_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RunnerSetupType < BaseObject + graphql_name 'RunnerSetup' + + field :install_instructions, GraphQL::STRING_TYPE, null: false, + description: 'Instructions for installing the runner on the specified architecture' + field :register_instructions, GraphQL::STRING_TYPE, null: true, + description: 'Instructions for registering the runner' + end + end +end diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb index dd4b4c3b114..c24b47f08ef 100644 --- a/app/graphql/types/commit_type.rb +++ b/app/graphql/types/commit_type.rb @@ -40,16 +40,9 @@ module Types field :author, type: Types::UserType, null: true, description: 'Author of the commit' - field :pipelines, Types::Ci::PipelineType.connection_type, + field :pipelines, null: true, description: 'Pipelines of the commit ordered latest first', resolver: Resolvers::CommitPipelinesResolver - - field :latest_pipeline, - type: Types::Ci::PipelineType, - null: true, - deprecated: { reason: 'Use `pipelines`', milestone: '12.5' }, - description: 'Latest pipeline of the commit', - resolver: Resolvers::CommitPipelinesResolver.last end end diff --git a/app/graphql/types/container_repository_cleanup_status_enum.rb b/app/graphql/types/container_repository_cleanup_status_enum.rb new file mode 100644 index 00000000000..6e654e65360 --- /dev/null +++ b/app/graphql/types/container_repository_cleanup_status_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + class ContainerRepositoryCleanupStatusEnum < BaseEnum + graphql_name 'ContainerRepositoryCleanupStatus' + description 'Status of the tags cleanup of a container repository' + + value 'UNSCHEDULED', value: 'cleanup_unscheduled', description: 'The tags cleanup is not scheduled. This is the default state.' + value 'SCHEDULED', value: 'cleanup_scheduled', description: 'The tags cleanup is scheduled and is going to be executed shortly.' + value 'UNFINISHED', value: 'cleanup_unfinished', description: 'The tags cleanup has been partially executed. There are still remaining tags to delete.' + value 'ONGOING', value: 'cleanup_ongoing', description: 'The tags cleanup is ongoing.' + end +end diff --git a/app/graphql/types/container_repository_details_type.rb b/app/graphql/types/container_repository_details_type.rb new file mode 100644 index 00000000000..34523f3ea4a --- /dev/null +++ b/app/graphql/types/container_repository_details_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + class ContainerRepositoryDetailsType < Types::ContainerRepositoryType + graphql_name 'ContainerRepositoryDetails' + + description 'Details of a container repository' + + authorize :read_container_image + + field :tags, + Types::ContainerRepositoryTagType.connection_type, + null: true, + description: 'Tags of the container repository', + max_page_size: 20 + + def can_delete + Ability.allowed?(current_user, :destroy_container_image, object) + end + end +end diff --git a/app/graphql/types/container_repository_status_enum.rb b/app/graphql/types/container_repository_status_enum.rb new file mode 100644 index 00000000000..8f3ba8f1083 --- /dev/null +++ b/app/graphql/types/container_repository_status_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + class ContainerRepositoryStatusEnum < BaseEnum + graphql_name 'ContainerRepositoryStatus' + description 'Status of a container repository' + + ::ContainerRepository.statuses.keys.each do |status| + value status.upcase, value: status, description: "#{status.titleize} status." + end + end +end diff --git a/app/graphql/types/container_repository_tag_type.rb b/app/graphql/types/container_repository_tag_type.rb new file mode 100644 index 00000000000..25e605b689d --- /dev/null +++ b/app/graphql/types/container_repository_tag_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + class ContainerRepositoryTagType < BaseObject + graphql_name 'ContainerRepositoryTag' + + description 'A tag from a container repository' + + authorize :read_container_image + + field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the tag.' + field :path, GraphQL::STRING_TYPE, null: false, description: 'Path of the tag.' + field :location, GraphQL::STRING_TYPE, null: false, description: 'URL of the tag.' + field :digest, GraphQL::STRING_TYPE, null: false, description: 'Digest of the tag.' + field :revision, GraphQL::STRING_TYPE, null: false, description: 'Revision of the tag.' + field :short_revision, GraphQL::STRING_TYPE, null: false, description: 'Short revision of the tag.' + field :total_size, GraphQL::INT_TYPE, null: false, description: 'The size of the tag.' + field :created_at, Types::TimeType, null: false, description: 'Timestamp when the tag was created.' + field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete this tag.' + + def can_delete + Ability.allowed?(current_user, :destroy_container_image, object) + end + end +end diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb new file mode 100644 index 00000000000..45d19fdbc50 --- /dev/null +++ b/app/graphql/types/container_repository_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + class ContainerRepositoryType < BaseObject + graphql_name 'ContainerRepository' + + description 'A container repository' + + authorize :read_container_image + + field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the container repository.' + field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the container repository.' + field :path, GraphQL::STRING_TYPE, null: false, description: 'Path of the container repository.' + field :location, GraphQL::STRING_TYPE, null: false, description: 'URL of the container repository.' + field :created_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was created.' + field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.' + 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 :expiration_policy_cleanup_status, Types::ContainerRepositoryCleanupStatusEnum, null: true, description: 'The tags cleanup status for the container repository.' + field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.' + field :tags_count, GraphQL::INT_TYPE, null: false, description: 'Number of tags associated with this image.' + field :can_delete, GraphQL::BOOLEAN_TYPE, null: false, description: 'Can the current user delete the container repository.' + + def can_delete + Ability.allowed?(current_user, :update_container_image, object) + end + end +end diff --git a/app/graphql/types/countable_connection_type.rb b/app/graphql/types/countable_connection_type.rb index 2538366b786..f67194d99b3 100644 --- a/app/graphql/types/countable_connection_type.rb +++ b/app/graphql/types/countable_connection_type.rb @@ -3,7 +3,7 @@ module Types # rubocop: disable Graphql/AuthorizeTypes class CountableConnectionType < GraphQL::Types::Relay::BaseConnection - field :count, Integer, null: false, + field :count, GraphQL::INT_TYPE, null: false, description: 'Total count of collection' def count diff --git a/app/graphql/types/custom_emoji_type.rb b/app/graphql/types/custom_emoji_type.rb new file mode 100644 index 00000000000..f7d1a7800bc --- /dev/null +++ b/app/graphql/types/custom_emoji_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + class CustomEmojiType < BaseObject + graphql_name 'CustomEmoji' + description 'A custom emoji uploaded by user' + + authorize :read_custom_emoji + + field :id, ::Types::GlobalIDType[::CustomEmoji], + null: false, + description: 'The ID of the emoji' + + field :name, GraphQL::STRING_TYPE, + null: false, + description: 'The name of the emoji' + + field :url, GraphQL::STRING_TYPE, + null: false, + method: :file, + description: 'The link to file of the emoji' + + field :external, GraphQL::BOOLEAN_TYPE, + null: false, + description: 'Whether the emoji is an external link' + end +end diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index e4631a4a903..e3885668643 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -18,9 +18,8 @@ module Types field :state, GraphQL::STRING_TYPE, null: false, description: 'State of the environment, for example: available/stopped' - field :path, GraphQL::STRING_TYPE, null: true, - description: 'The path to the environment. Will always return null ' \ - 'if `expose_environment_path_in_alert_details` feature flag is disabled' + field :path, GraphQL::STRING_TYPE, null: false, + description: 'The path to the environment.' field :metrics_dashboard, Types::Metrics::DashboardType, null: true, description: 'Metrics dashboard schema for the environment', diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb index 9ae9ba32c13..4c51d4248dd 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -30,6 +30,8 @@ module Types # @param value [String] # @return [GID] def self.coerce_input(value, _ctx) + return if value.nil? + gid = GlobalID.parse(value) raise GraphQL::CoercionError, "#{value.inspect} is not a valid Global ID" if gid.nil? raise GraphQL::CoercionError, "#{value.inspect} is not a Gitlab Global ID" unless gid.app == GlobalID.app diff --git a/app/graphql/types/grafana_integration_type.rb b/app/graphql/types/grafana_integration_type.rb index 7db733fc62a..6625af36f82 100644 --- a/app/graphql/types/grafana_integration_type.rb +++ b/app/graphql/types/grafana_integration_type.rb @@ -16,13 +16,5 @@ module Types description: 'Timestamp of the issue\'s creation' field :updated_at, Types::TimeType, null: false, description: 'Timestamp of the issue\'s last activity' - - field :token, GraphQL::STRING_TYPE, null: false, - deprecated: { reason: 'Plain text token has been masked for security reasons', milestone: '12.7' }, - description: 'API token for the Grafana integration' - - def token - object.masked_token - end end end diff --git a/app/graphql/types/group_invitation_type.rb b/app/graphql/types/group_invitation_type.rb new file mode 100644 index 00000000000..0372ce178ff --- /dev/null +++ b/app/graphql/types/group_invitation_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + class GroupInvitationType < BaseObject + expose_permissions Types::PermissionTypes::Group + authorize :read_group + + implements InvitationInterface + + graphql_name 'GroupInvitation' + description 'Represents a Group Invitation' + + field :group, Types::GroupType, null: true, + description: 'Group that a User is invited to', + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find } + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 199cc0308c5..fb028184488 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -17,6 +17,10 @@ module Types group.avatar_url(only_path: false) end + field :custom_emoji, Types::CustomEmojiType.connection_type, null: true, + description: 'Custom emoji within this namespace', + feature_flag: :custom_emoji + field :share_with_group_lock, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if sharing a project with another group within this group is prevented' @@ -82,11 +86,16 @@ module Types end field :group_members, - Types::GroupMemberType.connection_type, description: 'A membership of a user within this group', - extras: [:lookahead], resolver: Resolvers::GroupMembersResolver + field :container_repositories, + Types::ContainerRepositoryType.connection_type, + null: true, + description: 'Container repositories of the project', + resolver: Resolvers::ContainerRepositoriesResolver, + authorize: :read_container_image + def label(title:) BatchLoader::GraphQL.for(title).batch(key: group) do |titles, loader, args| LabelsFinder diff --git a/app/graphql/types/invitation_interface.rb b/app/graphql/types/invitation_interface.rb new file mode 100644 index 00000000000..a29716c292e --- /dev/null +++ b/app/graphql/types/invitation_interface.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Types + module InvitationInterface + include BaseInterface + + field :email, GraphQL::STRING_TYPE, null: false, + description: 'Email of the member to invite' + + field :access_level, Types::AccessLevelType, null: true, + description: 'GitLab::Access level' + + field :created_by, Types::UserType, null: true, + description: 'User that authorized membership' + + field :created_at, Types::TimeType, null: true, + description: 'Date and time the membership was created' + + field :updated_at, Types::TimeType, null: true, + description: 'Date and time the membership was last updated' + + field :expires_at, Types::TimeType, null: true, + description: 'Date and time the membership expires' + + field :user, Types::UserType, null: true, + description: 'User that is associated with the member object' + + definition_methods do + def resolve_type(object, context) + case object + when GroupMember + Types::GroupInvitationType + when ProjectMember + Types::ProjectInvitationType + else + raise ::Gitlab::Graphql::Errors::BaseError, "Unknown member type #{object.class.name}" + end + end + end + end +end diff --git a/app/graphql/types/issue_connection_type.rb b/app/graphql/types/issue_connection_type.rb new file mode 100644 index 00000000000..2e0f05f741e --- /dev/null +++ b/app/graphql/types/issue_connection_type.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class IssueConnectionType < CountableConnectionType + end +end + +Types::IssueConnectionType.prepend_if_ee('::EE::Types::IssueConnectionType') diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 487508f448f..49c84f75e1a 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -4,7 +4,7 @@ module Types class IssueType < BaseObject graphql_name 'Issue' - connection_type_class(Types::CountableConnectionType) + connection_type_class(Types::IssueConnectionType) implements(Types::Notes::NoteableType) implements(Types::CurrentUserTodos) @@ -41,6 +41,9 @@ module Types field :assignees, Types::UserType.connection_type, null: true, description: 'Assignees of the issue' + field :updated_by, Types::UserType, null: true, + description: 'User that last updated the issue' + field :labels, Types::LabelType.connection_type, null: true, description: 'Labels of the issue' field :milestone, Types::MilestoneType, null: true, @@ -59,6 +62,8 @@ module Types description: 'Number of downvotes the issue has received' field :user_notes_count, GraphQL::INT_TYPE, null: false, description: 'Number of user notes of the issue' + field :user_discussions_count, GraphQL::INT_TYPE, null: false, + description: 'Number of user discussions in the issue' field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path, description: 'Web path of the issue' field :web_url, GraphQL::STRING_TYPE, null: false, @@ -68,12 +73,19 @@ module Types field :participants, Types::UserType.connection_type, null: true, complexity: 5, description: 'List of participants in the issue' + field :emails_disabled, GraphQL::BOOLEAN_TYPE, null: false, + method: :project_emails_disabled?, + description: 'Indicates if a project has email notifications disabled: `true` if email notifications are disabled' field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5, description: 'Indicates the currently logged in user is subscribed to the issue' field :time_estimate, GraphQL::INT_TYPE, null: false, description: 'Time estimate of the issue' field :total_time_spent, GraphQL::INT_TYPE, null: false, description: 'Total time reported as spent on the issue' + field :human_time_estimate, GraphQL::STRING_TYPE, null: true, + description: 'Human-readable time estimate of the issue' + field :human_total_time_spent, GraphQL::STRING_TYPE, null: true, + description: 'Human-readable total time reported as spent on the issue' field :closed_at, Types::TimeType, null: true, description: 'Timestamp of when the issue was closed' @@ -86,11 +98,6 @@ module Types field :task_completion_status, Types::TaskCompletionStatus, null: false, description: 'Task completion status of the issue' - field :designs, Types::DesignManagement::DesignCollectionType, null: true, - method: :design_collection, - deprecated: { reason: 'Use `designCollection`', milestone: '12.2' }, - description: 'The designs associated with this issue' - field :design_collection, Types::DesignManagement::DesignCollectionType, null: true, description: 'Collection of design images associated with this issue' @@ -106,14 +113,48 @@ module Types field :severity, Types::IssuableSeverityEnum, null: true, description: 'Severity level of the incident' + field :moved, GraphQL::BOOLEAN_TYPE, method: :moved?, null: true, + description: 'Indicates if issue got moved from other project' + + field :moved_to, Types::IssueType, null: true, + description: 'Updated Issue after it got moved to another project' + + def user_notes_count + BatchLoader::GraphQL.for(object.id).batch(key: :issue_user_notes_count) do |ids, loader, args| + counts = Note.count_for_collection(ids, 'Issue').index_by(&:noteable_id) + + ids.each do |id| + loader.call(id, counts[id]&.count || 0) + end + end + end + + def user_discussions_count + BatchLoader::GraphQL.for(object.id).batch(key: :issue_user_discussions_count) do |ids, loader, args| + counts = Note.count_for_collection(ids, 'Issue', 'COUNT(DISTINCT discussion_id) as count').index_by(&:noteable_id) + + ids.each do |id| + loader.call(id, counts[id]&.count || 0) + end + end + end + def author Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find end + def updated_by + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.updated_by_id).find + end + def milestone Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, object.milestone_id).find end + def moved_to + Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.moved_to_id).find + end + def discussion_locked !!object.discussion_locked end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 372aeac055b..e68d6706c43 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -68,6 +68,8 @@ module Types description: 'SHA of the merge request commit (set once merged)' field :user_notes_count, GraphQL::INT_TYPE, null: true, description: 'User notes count of the merge request' + field :user_discussions_count, GraphQL::INT_TYPE, null: true, + description: 'Number of user discussions in the merge request' field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true, description: 'Indicates if the source branch of the merge request will be deleted after merge' field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true, @@ -86,9 +88,6 @@ module Types description: 'Rebase commit SHA of the merge request' field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true, description: 'Indicates if there is a rebase currently in progress for the merge request' - field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, - deprecated: { reason: 'Use `defaultMergeCommitMessage`', milestone: '11.8' }, - description: 'Default merge commit message of the merge request' field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true, description: 'Default merge commit message of the merge request' field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false, @@ -112,14 +111,13 @@ module Types field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline, description: 'The pipeline running on the branch HEAD of the merge request' - field :pipelines, Types::Ci::PipelineType.connection_type, + field :pipelines, null: true, description: 'Pipelines for the merge request', resolver: Resolvers::MergeRequestPipelinesResolver field :milestone, Types::MilestoneType, null: true, - description: 'The milestone of the merge request', - resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find } + description: 'The milestone of the merge request' field :assignees, Types::UserType.connection_type, null: true, complexity: 5, description: 'Assignees of the merge request' field :author, Types::UserType, null: true, @@ -159,17 +157,25 @@ module Types object.approved_by_users end - # rubocop: disable CodeReuse/ActiveRecord def user_notes_count BatchLoader::GraphQL.for(object.id).batch(key: :merge_request_user_notes_count) do |ids, loader, args| - counts = Note.where(noteable_type: 'MergeRequest', noteable_id: ids).user.group(:noteable_id).count + counts = Note.count_for_collection(ids, 'MergeRequest').index_by(&:noteable_id) + + ids.each do |id| + loader.call(id, counts[id]&.count || 0) + end + end + end + + def user_discussions_count + BatchLoader::GraphQL.for(object.id).batch(key: :merge_request_user_discussions_count) do |ids, loader, args| + counts = Note.count_for_collection(ids, 'MergeRequest', 'COUNT(DISTINCT discussion_id) as count').index_by(&:noteable_id) ids.each do |id| - loader.call(id, counts[id] || 0) + loader.call(id, counts[id]&.count || 0) end end end - # rubocop: enable CodeReuse/ActiveRecord def diff_stats(path: nil) stats = Array.wrap(object.diff_stats&.to_a) diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 3f48e7b4a16..75ccac6d590 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -11,6 +11,13 @@ module Types mount_mutation Mutations::AlertManagement::UpdateAlertStatus mount_mutation Mutations::AlertManagement::Alerts::SetAssignees mount_mutation Mutations::AlertManagement::Alerts::Todo::Create + mount_mutation Mutations::AlertManagement::HttpIntegration::Create + mount_mutation Mutations::AlertManagement::HttpIntegration::Update + mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken + mount_mutation Mutations::AlertManagement::HttpIntegration::Destroy + mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create + mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update + mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle @@ -22,6 +29,7 @@ module Types mount_mutation Mutations::Boards::Lists::Destroy mount_mutation Mutations::Branches::Create, calls_gitaly: true mount_mutation Mutations::Commits::Create, calls_gitaly: true + mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji mount_mutation Mutations::Discussions::ToggleResolve mount_mutation Mutations::Issues::Create mount_mutation Mutations::Issues::SetAssignees @@ -32,6 +40,7 @@ module Types mount_mutation Mutations::Issues::SetSubscription mount_mutation Mutations::Issues::Update mount_mutation Mutations::Issues::Move + mount_mutation Mutations::Labels::Create mount_mutation Mutations::MergeRequests::Create mount_mutation Mutations::MergeRequests::Update mount_mutation Mutations::MergeRequests::SetLabels @@ -53,7 +62,13 @@ module Types description: 'Updates a DiffNote on an image (a `Note` where the `position.positionType` is `"image"`). ' \ 'If the body of the Note contains only quick actions, the Note will be ' \ 'destroyed during the update, and no Note will be returned' + mount_mutation Mutations::Notes::RepositionImageDiffNote mount_mutation Mutations::Notes::Destroy + mount_mutation Mutations::Releases::Create + mount_mutation Mutations::Terraform::State::Delete + mount_mutation Mutations::Terraform::State::Lock + mount_mutation Mutations::Terraform::State::Unlock + mount_mutation Mutations::Todos::Create mount_mutation Mutations::Todos::MarkDone mount_mutation Mutations::Todos::Restore mount_mutation Mutations::Todos::MarkAllDone @@ -68,6 +83,7 @@ module Types mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true mount_mutation Mutations::DesignManagement::Move mount_mutation Mutations::ContainerExpirationPolicies::Update + mount_mutation Mutations::ContainerRepositories::Destroy mount_mutation Mutations::Ci::PipelineCancel mount_mutation Mutations::Ci::PipelineDestroy mount_mutation Mutations::Ci::PipelineRetry diff --git a/app/graphql/types/notes/update_diff_image_position_input_type.rb b/app/graphql/types/notes/update_diff_image_position_input_type.rb index af99764f9f2..1b915b65ae9 100644 --- a/app/graphql/types/notes/update_diff_image_position_input_type.rb +++ b/app/graphql/types/notes/update_diff_image_position_input_type.rb @@ -23,6 +23,14 @@ module Types argument :height, GraphQL::INT_TYPE, required: false, description: copy_field_description(Types::Notes::DiffPositionType, :height) + + def prepare + to_h.compact.tap do |properties| + if properties.empty? + raise GraphQL::ExecutionError, "At least one property of `#{self.class.graphql_name}` must be set" + end + end + end end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/permission_types/custom_emoji.rb b/app/graphql/types/permission_types/custom_emoji.rb new file mode 100644 index 00000000000..0b2e7da44f5 --- /dev/null +++ b/app/graphql/types/permission_types/custom_emoji.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class CustomEmoji < BasePermissionType + graphql_name 'CustomEmojiPermissions' + + abilities :create_custom_emoji, :read_custom_emoji + end + end +end diff --git a/app/graphql/types/permission_types/note.rb b/app/graphql/types/permission_types/note.rb index a585d3daaa8..923f2b660fe 100644 --- a/app/graphql/types/permission_types/note.rb +++ b/app/graphql/types/permission_types/note.rb @@ -5,7 +5,7 @@ module Types class Note < BasePermissionType graphql_name 'NotePermissions' - abilities :read_note, :create_note, :admin_note, :resolve_note, :award_emoji + abilities :read_note, :create_note, :admin_note, :resolve_note, :reposition_note, :award_emoji end end end diff --git a/app/graphql/types/project_invitation_type.rb b/app/graphql/types/project_invitation_type.rb new file mode 100644 index 00000000000..a5367a4f204 --- /dev/null +++ b/app/graphql/types/project_invitation_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + class ProjectInvitationType < BaseObject + graphql_name 'ProjectInvitation' + description 'Represents a Project Membership Invitation' + + expose_permissions Types::PermissionTypes::Project + + implements InvitationInterface + + authorize :read_project + + field :project, Types::ProjectType, null: true, + description: 'Project ID for the project of the invitation' + + def project + Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.source_id).find + end + end +end diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb index b3916e42e92..26cb5ab59b5 100644 --- a/app/graphql/types/project_statistics_type.rb +++ b/app/graphql/types/project_statistics_type.rb @@ -10,18 +10,20 @@ module Types description: 'Commit count of the project' field :storage_size, GraphQL::FLOAT_TYPE, null: false, - description: 'Storage size of the project' + description: 'Storage size of the project in bytes' field :repository_size, GraphQL::FLOAT_TYPE, null: false, - description: 'Repository size of the project' + description: 'Repository size of the project in bytes' field :lfs_objects_size, GraphQL::FLOAT_TYPE, null: false, - description: 'Large File Storage (LFS) object size of the project' + description: 'Large File Storage (LFS) object size of the project in bytes' field :build_artifacts_size, GraphQL::FLOAT_TYPE, null: false, - description: 'Build artifacts size of the project' + description: 'Build artifacts size of the project in bytes' field :packages_size, GraphQL::FLOAT_TYPE, null: false, - description: 'Packages size of the project' + description: 'Packages size of the project in bytes' field :wiki_size, GraphQL::FLOAT_TYPE, null: true, - description: 'Wiki size of the project' + description: 'Wiki size of the project in bytes' field :snippets_size, GraphQL::FLOAT_TYPE, null: true, - description: 'Snippets size of the project' + description: 'Snippets size of the project in bytes' + field :uploads_size, GraphQL::FLOAT_TYPE, null: true, + description: 'Uploads 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 c7fc193abe8..5a436886117 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -161,7 +161,6 @@ module Types resolver: Resolvers::ProjectMilestonesResolver field :project_members, - Types::MemberInterface.connection_type, description: 'Members of the project', resolver: Resolvers::ProjectMembersResolver @@ -188,9 +187,9 @@ module Types resolver: Resolvers::PackagesResolver field :pipelines, - Types::Ci::PipelineType.connection_type, null: true, description: 'Build pipelines of the project', + extras: [:lookahead], resolver: Resolvers::ProjectPipelinesResolver field :pipeline, @@ -267,6 +266,12 @@ module Types description: 'Counts of alerts by status for the project', resolver: Resolvers::AlertManagement::AlertStatusCountsResolver + field :alert_management_integrations, + Types::AlertManagement::IntegrationType.connection_type, + null: true, + description: 'Integrations which can receive alerts for the project', + resolver: Resolvers::AlertManagement::IntegrationsResolver + field :releases, Types::ReleaseType.connection_type, null: true, @@ -285,6 +290,12 @@ module Types null: true, description: 'The container expiration policy of the project' + field :container_repositories, + Types::ContainerRepositoryType.connection_type, + null: true, + description: 'Container repositories of the project', + resolver: Resolvers::ContainerRepositoriesResolver + field :label, Types::LabelType, null: true, diff --git a/app/graphql/types/projects/namespace_project_sort_enum.rb b/app/graphql/types/projects/namespace_project_sort_enum.rb index 1e13deb6508..ede29748beb 100644 --- a/app/graphql/types/projects/namespace_project_sort_enum.rb +++ b/app/graphql/types/projects/namespace_project_sort_enum.rb @@ -7,6 +7,7 @@ module Types description 'Values for sorting projects' value 'SIMILARITY', 'Most similar to the search query', value: :similarity + value 'STORAGE', 'Sort by storage size', value: :storage end end end diff --git a/app/graphql/types/projects/service_type_enum.rb b/app/graphql/types/projects/service_type_enum.rb index 340fdff6b86..34e06c67be6 100644 --- a/app/graphql/types/projects/service_type_enum.rb +++ b/app/graphql/types/projects/service_type_enum.rb @@ -5,7 +5,7 @@ module Types class ServiceTypeEnum < BaseEnum graphql_name 'ServiceType' - ::Service.services_types.each do |service_type| + ::Service.available_services_types(include_dev: false).each do |service_type| value service_type.underscore.upcase, value: service_type end end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index bd4b53bdaa7..d194b0979b3 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -50,10 +50,14 @@ module Types field :milestone, ::Types::MilestoneType, null: true, description: 'Find a milestone' do - argument :id, ::Types::GlobalIDType[Milestone], - required: true, - description: 'Find a milestone by its ID' - end + argument :id, ::Types::GlobalIDType[Milestone], required: true, description: 'Find a milestone by its ID' + end + + field :container_repository, Types::ContainerRepositoryDetailsType, + null: true, + description: 'Find a container repository' do + argument :id, ::Types::GlobalIDType[::ContainerRepository], required: true, description: 'The global ID of the container repository' + end field :user, Types::UserType, null: true, @@ -84,6 +88,10 @@ module Types null: true, description: 'Supported runner platforms', resolver: Resolvers::Ci::RunnerPlatformsResolver + field :runner_setup, Types::Ci::RunnerSetupType, null: true, + description: 'Get runner setup instructions', + resolver: Resolvers::Ci::RunnerSetupResolver + def design_management DesignManagementObject.new(nil) end @@ -101,6 +109,13 @@ module Types id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id) GitlabSchema.find_by_gid(id) end + + def container_repository(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::ContainerRepository].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end end end diff --git a/app/graphql/types/release_asset_link_input_type.rb b/app/graphql/types/release_asset_link_input_type.rb new file mode 100644 index 00000000000..d13861fad8f --- /dev/null +++ b/app/graphql/types/release_asset_link_input_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class ReleaseAssetLinkInputType < BaseInputObject + graphql_name 'ReleaseAssetLinkInput' + description 'Fields that are available when modifying a release asset link' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the asset link' + + argument :url, GraphQL::STRING_TYPE, + required: true, + description: 'URL of the asset link' + + argument :direct_asset_path, GraphQL::STRING_TYPE, + required: false, as: :filepath, + description: 'Relative path for a direct asset link' + + argument :link_type, Types::ReleaseAssetLinkTypeEnum, + required: false, default_value: 'other', + description: 'The type of the asset link' + end +end diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb index 0e519ece791..8fb051f5627 100644 --- a/app/graphql/types/release_asset_link_type.rb +++ b/app/graphql/types/release_asset_link_type.rb @@ -24,10 +24,8 @@ module Types def direct_asset_url return object.url unless object.filepath - release = object.release - project = release.project - - Gitlab::Routing.url_helpers.project_release_url(project, release) << object.filepath + release = object.release.present + release.download_url(object.filepath) end end end diff --git a/app/graphql/types/release_asset_link_type_enum.rb b/app/graphql/types/release_asset_link_type_enum.rb index 01862ada56d..70601b9f8da 100644 --- a/app/graphql/types/release_asset_link_type_enum.rb +++ b/app/graphql/types/release_asset_link_type_enum.rb @@ -3,7 +3,7 @@ module Types class ReleaseAssetLinkTypeEnum < BaseEnum graphql_name 'ReleaseAssetLinkType' - description 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`' + description 'Type of the link: `other`, `runbook`, `image`, `package`' ::Releases::Link.link_types.keys.each do |link_type| value link_type.upcase, value: link_type, description: "#{link_type.titleize} link type" diff --git a/app/graphql/types/release_assets_input_type.rb b/app/graphql/types/release_assets_input_type.rb new file mode 100644 index 00000000000..3fcb517e044 --- /dev/null +++ b/app/graphql/types/release_assets_input_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class ReleaseAssetsInputType < BaseInputObject + graphql_name 'ReleaseAssetsInput' + description 'Fields that are available when modifying release assets' + + argument :links, [Types::ReleaseAssetLinkInputType], + required: false, + description: 'A list of asset links to associate to the release' + end +end diff --git a/app/graphql/types/release_links_type.rb b/app/graphql/types/release_links_type.rb index f61a16f5b67..619bb1e6c3a 100644 --- a/app/graphql/types/release_links_type.rb +++ b/app/graphql/types/release_links_type.rb @@ -12,12 +12,18 @@ module Types field :self_url, GraphQL::STRING_TYPE, null: true, description: 'HTTP URL of the release' - field :merge_requests_url, GraphQL::STRING_TYPE, null: true, - description: 'HTTP URL of the merge request page filtered by this release' - field :issues_url, GraphQL::STRING_TYPE, null: true, - description: 'HTTP URL of the issues page filtered by this release' field :edit_url, GraphQL::STRING_TYPE, null: true, description: "HTTP URL of the release's edit page", authorize: :update_release + field :opened_merge_requests_url, GraphQL::STRING_TYPE, null: true, + description: 'HTTP URL of the merge request page, filtered by this release and `state=open`' + field :merged_merge_requests_url, GraphQL::STRING_TYPE, null: true, + description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`' + field :closed_merge_requests_url, GraphQL::STRING_TYPE, null: true, + description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`' + field :opened_issues_url, GraphQL::STRING_TYPE, null: true, + description: 'HTTP URL of the issues page, filtered by this release and `state=open`' + field :closed_issues_url, GraphQL::STRING_TYPE, null: true, + description: 'HTTP URL of the issues page, filtered by this release and `state=closed`' end end diff --git a/app/graphql/types/release_sort_enum.rb b/app/graphql/types/release_sort_enum.rb new file mode 100644 index 00000000000..2f9af1bced9 --- /dev/null +++ b/app/graphql/types/release_sort_enum.rb @@ -0,0 +1,18 @@ +# 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 ReleaseSortEnum < BaseEnum + graphql_name 'ReleaseSort' + description 'Values for sorting releases' + + # Borrowed from Types::SortEnum + # These values/descriptions should stay in-sync as much as possible. + value 'CREATED_DESC', 'Created at descending order', value: :created_desc + value 'CREATED_ASC', 'Created at ascending order', value: :created_asc + + 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/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb index 224e8c7ee03..21448b33bb5 100644 --- a/app/graphql/types/root_storage_statistics_type.rb +++ b/app/graphql/types/root_storage_statistics_type.rb @@ -14,5 +14,6 @@ module Types field :wiki_size, GraphQL::FLOAT_TYPE, null: false, description: 'The wiki size in bytes' field :snippets_size, GraphQL::FLOAT_TYPE, null: false, description: 'The snippets size in bytes' field :pipeline_artifacts_size, GraphQL::FLOAT_TYPE, null: false, description: 'The CI pipeline artifacts size in bytes' + field :uploads_size, GraphQL::FLOAT_TYPE, null: false, description: 'The uploads size in bytes' end end diff --git a/app/graphql/types/security/report_type_enum.rb b/app/graphql/types/security/report_type_enum.rb new file mode 100644 index 00000000000..ee67f68b27b --- /dev/null +++ b/app/graphql/types/security/report_type_enum.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Security + class ReportTypeEnum < BaseEnum + graphql_name 'SecurityReportTypeEnum' + + ::Security::SecurityJobsFinder.allowed_job_types.each do |report_type| + value report_type.upcase, + value: report_type, + description: "#{report_type.upcase.to_s.tr('_', ' ')} scan report" + end + end + end +end diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb index 495c25c1776..f587adf207f 100644 --- a/app/graphql/types/snippet_type.rb +++ b/app/graphql/types/snippet_type.rb @@ -13,7 +13,7 @@ module Types expose_permissions Types::PermissionTypes::Snippet - field :id, GraphQL::ID_TYPE, + field :id, Types::GlobalIDType[::Snippet], description: 'ID of the snippet', null: false diff --git a/app/graphql/types/terraform/state_type.rb b/app/graphql/types/terraform/state_type.rb index f25f3a7789b..05b6d130f19 100644 --- a/app/graphql/types/terraform/state_type.rb +++ b/app/graphql/types/terraform/state_type.rb @@ -7,6 +7,8 @@ module Types authorize :read_terraform_state + connection_type_class(Types::CountableConnectionType) + field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the Terraform state' @@ -25,6 +27,11 @@ module Types null: true, description: 'Timestamp the Terraform state was locked' + field :latest_version, Types::Terraform::StateVersionType, + complexity: 3, + null: true, + description: 'The latest version of the Terraform state' + field :created_at, Types::TimeType, null: false, description: 'Timestamp the Terraform state was created' diff --git a/app/graphql/types/terraform/state_version_type.rb b/app/graphql/types/terraform/state_version_type.rb new file mode 100644 index 00000000000..b1fbe42ecaf --- /dev/null +++ b/app/graphql/types/terraform/state_version_type.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Types + module Terraform + class StateVersionType < BaseObject + graphql_name 'TerraformStateVersion' + + authorize :read_terraform_state + + field :id, GraphQL::ID_TYPE, + null: false, + description: 'ID of the Terraform state version' + + field :created_by_user, Types::UserType, + null: true, + authorize: :read_user, + description: 'The user that created this version', + resolve: -> (version, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, version.created_by_user_id).find } + + field :job, Types::Ci::JobType, + null: true, + authorize: :read_build, + description: 'The job that created this version', + resolve: -> (version, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Build, version.ci_build_id).find } + + field :created_at, Types::TimeType, + null: false, + description: 'Timestamp the version was created' + + field :updated_at, Types::TimeType, + null: false, + description: 'Timestamp the version was updated' + end + end +end diff --git a/app/graphql/types/user_status_type.rb b/app/graphql/types/user_status_type.rb index ff277c1f8e8..9cf6c862d3d 100644 --- a/app/graphql/types/user_status_type.rb +++ b/app/graphql/types/user_status_type.rb @@ -11,5 +11,7 @@ module Types description: 'User status message' field :emoji, GraphQL::STRING_TYPE, null: true, description: 'String representation of emoji' + field :availability, Types::AvailabilityEnum, null: false, + description: 'User availability status' end end diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index 8047708776d..11c5369f726 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -32,6 +32,10 @@ module Types field :group_memberships, Types::GroupMemberType.connection_type, null: true, description: 'Group memberships of the user', method: :group_members + field :group_count, GraphQL::INT_TYPE, null: true, + resolver: Resolvers::Users::GroupCountResolver, + description: 'Group count for the user', + feature_flag: :user_group_counts field :status, Types::UserStatusType, null: true, description: 'User status' field :project_memberships, Types::ProjectMemberType.connection_type, null: true, @@ -42,10 +46,10 @@ module Types resolver: Resolvers::UserStarredProjectsResolver # Merge request field: MRs can be either authored or assigned: - field :authored_merge_requests, Types::MergeRequestType.connection_type, null: true, + field :authored_merge_requests, resolver: Resolvers::AuthoredMergeRequestsResolver, description: 'Merge Requests authored by the user' - field :assigned_merge_requests, Types::MergeRequestType.connection_type, null: true, + field :assigned_merge_requests, resolver: Resolvers::AssignedMergeRequestsResolver, description: 'Merge Requests assigned to the user' |