summaryrefslogtreecommitdiff
path: root/app/graphql
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
commit9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch)
tree70467ae3692a0e35e5ea56bcb803eb512a10bedb /app/graphql
parent4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff)
downloadgitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'app/graphql')
-rw-r--r--app/graphql/gitlab_schema.rb13
-rw-r--r--app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb2
-rw-r--r--app/graphql/mutations/alert_management/prometheus_integration/create.rb2
-rw-r--r--app/graphql/mutations/alert_management/prometheus_integration/update.rb2
-rw-r--r--app/graphql/mutations/base_mutation.rb29
-rw-r--r--app/graphql/mutations/boards/issues/issue_move_list.rb15
-rw-r--r--app/graphql/mutations/ci/ci_cd_settings_update.rb12
-rw-r--r--app/graphql/mutations/concerns/mutations/assignable.rb46
-rw-r--r--app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb41
-rw-r--r--app/graphql/mutations/concerns/mutations/spam_protection.rb67
-rw-r--r--app/graphql/mutations/container_repositories/destroy_tags.rb2
-rw-r--r--app/graphql/mutations/issues/move.rb2
-rw-r--r--app/graphql/mutations/issues/set_assignees.rb13
-rw-r--r--app/graphql/mutations/merge_requests/accept.rb3
-rw-r--r--app/graphql/mutations/merge_requests/set_assignees.rb2
-rw-r--r--app/graphql/mutations/release_asset_links/delete.rb40
-rw-r--r--app/graphql/mutations/snippets/create.rb13
-rw-r--r--app/graphql/mutations/snippets/update.rb13
-rw-r--r--app/graphql/queries/pipelines/get_pipeline_details.query.graphql1
-rw-r--r--app/graphql/resolvers/alert_management/http_integrations_resolver.rb30
-rw-r--r--app/graphql/resolvers/alert_management/integrations_resolver.rb43
-rw-r--r--app/graphql/resolvers/base_resolver.rb24
-rw-r--r--app/graphql/resolvers/blobs_resolver.rb37
-rw-r--r--app/graphql/resolvers/board_lists_resolver.rb18
-rw-r--r--app/graphql/resolvers/board_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/config_resolver.rb6
-rw-r--r--app/graphql/resolvers/ci/jobs_resolver.rb13
-rw-r--r--app/graphql/resolvers/ci/pipeline_stages_resolver.rb2
-rw-r--r--app/graphql/resolvers/ci/runner_platforms_resolver.rb3
-rw-r--r--app/graphql/resolvers/ci/runner_setup_resolver.rb43
-rw-r--r--app/graphql/resolvers/ci/test_report_summary_resolver.rb17
-rw-r--r--app/graphql/resolvers/ci/test_suite_resolver.rb40
-rw-r--r--app/graphql/resolvers/concerns/board_issue_filterable.rb13
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb33
-rw-r--r--app/graphql/resolvers/concerns/looks_ahead.rb15
-rw-r--r--app/graphql/resolvers/concerns/manual_authorization.rb11
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb3
-rw-r--r--app/graphql/resolvers/concerns/resolves_snippets.rb2
-rw-r--r--app/graphql/resolvers/echo_resolver.rb6
-rw-r--r--app/graphql/resolvers/environments_resolver.rb2
-rw-r--r--app/graphql/resolvers/group_members_resolver.rb6
-rw-r--r--app/graphql/resolvers/group_merge_requests_resolver.rb2
-rw-r--r--app/graphql/resolvers/group_milestones_resolver.rb28
-rw-r--r--app/graphql/resolvers/issues_resolver.rb3
-rw-r--r--app/graphql/resolvers/members_resolver.rb6
-rw-r--r--app/graphql/resolvers/merge_request_resolver.rb8
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb39
-rw-r--r--app/graphql/resolvers/metrics/dashboard_resolver.rb9
-rw-r--r--app/graphql/resolvers/milestones_resolver.rb4
-rw-r--r--app/graphql/resolvers/package_details_resolver.rb10
-rw-r--r--app/graphql/resolvers/project_jobs_resolver.rb41
-rw-r--r--app/graphql/resolvers/project_pipeline_resolver.rb2
-rw-r--r--app/graphql/resolvers/projects/services_resolver.rb12
-rw-r--r--app/graphql/resolvers/repository_branch_names_resolver.rb17
-rw-r--r--app/graphql/resolvers/snippets/blobs_resolver.rb3
-rw-r--r--app/graphql/resolvers/timelog_resolver.rb112
-rw-r--r--app/graphql/resolvers/user_merge_requests_resolver_base.rb25
-rw-r--r--app/graphql/resolvers/user_starred_projects_resolver.rb6
-rw-r--r--app/graphql/resolvers/users/snippets_resolver.rb7
-rw-r--r--app/graphql/types/base_argument.rb4
-rw-r--r--app/graphql/types/base_enum.rb27
-rw-r--r--app/graphql/types/base_field.rb41
-rw-r--r--app/graphql/types/base_interface.rb6
-rw-r--r--app/graphql/types/base_object.rb8
-rw-r--r--app/graphql/types/base_union.rb3
-rw-r--r--app/graphql/types/board_type.rb6
-rw-r--r--app/graphql/types/boards/assignee_wildcard_id_enum.rb13
-rw-r--r--app/graphql/types/boards/board_issuable_input_base_type.rb20
-rw-r--r--app/graphql/types/boards/board_issue_input_base_type.rb17
-rw-r--r--app/graphql/types/boards/board_issue_input_type.rb13
-rw-r--r--app/graphql/types/boards/negated_board_issue_input_type.rb10
-rw-r--r--app/graphql/types/ci/job_status_enum.rb15
-rw-r--r--app/graphql/types/ci/job_type.rb99
-rw-r--r--app/graphql/types/ci/pipeline_config_source_enum.rb3
-rw-r--r--app/graphql/types/ci/pipeline_status_enum.rb4
-rw-r--r--app/graphql/types/ci/pipeline_type.rb45
-rw-r--r--app/graphql/types/ci/recent_failures_type.rb20
-rw-r--r--app/graphql/types/ci/stage_type.rb35
-rw-r--r--app/graphql/types/ci/test_case_status_enum.rb15
-rw-r--r--app/graphql/types/ci/test_case_type.rb41
-rw-r--r--app/graphql/types/ci/test_report_summary_type.rb19
-rw-r--r--app/graphql/types/ci/test_report_total_type.rb33
-rw-r--r--app/graphql/types/ci/test_suite_summary_type.rb41
-rw-r--r--app/graphql/types/ci/test_suite_type.rb41
-rw-r--r--app/graphql/types/concerns/find_closest.rb11
-rw-r--r--app/graphql/types/concerns/gitlab_style_deprecations.rb18
-rw-r--r--app/graphql/types/global_id_type.rb13
-rw-r--r--app/graphql/types/group_type.rb69
-rw-r--r--app/graphql/types/issue_type.rb3
-rw-r--r--app/graphql/types/issues/negated_issue_filter_input_type.rb27
-rw-r--r--app/graphql/types/jira_users_mapping_input_type.rb12
-rw-r--r--app/graphql/types/merge_request_review_state_enum.rb11
-rw-r--r--app/graphql/types/merge_request_state_enum.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb7
-rw-r--r--app/graphql/types/merge_requests/reviewer_type.rb26
-rw-r--r--app/graphql/types/milestone_type.rb3
-rw-r--r--app/graphql/types/mutation_operation_mode_enum.rb8
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/packages/conan/file_metadatum_type.rb22
-rw-r--r--app/graphql/types/packages/conan/metadatum_file_type_enum.rb16
-rw-r--r--app/graphql/types/packages/conan/metadatum_type.rb22
-rw-r--r--app/graphql/types/packages/file_metadata_type.rb27
-rw-r--r--app/graphql/types/packages/metadata_type.rb4
-rw-r--r--app/graphql/types/packages/package_details_type.rb20
-rw-r--r--app/graphql/types/packages/package_file_type.rb36
-rw-r--r--app/graphql/types/packages/package_type.rb47
-rw-r--r--app/graphql/types/packages/package_without_versions_type.rb44
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/graphql/types/query_type.rb34
-rw-r--r--app/graphql/types/repository/blob_type.rb40
-rw-r--r--app/graphql/types/repository_type.rb5
-rw-r--r--app/graphql/types/sort_enum.rb32
-rw-r--r--app/graphql/types/timelog_type.rb42
-rw-r--r--app/graphql/types/user_merge_request_interaction_type.rb47
-rw-r--r--app/graphql/types/user_type.rb75
115 files changed, 1932 insertions, 398 deletions
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 7ab5dc36e4a..8369d0e120f 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -12,7 +12,6 @@ class GitlabSchema < GraphQL::Schema
use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
- use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Pagination::Connections
use Gitlab::Graphql::GenericTracing
use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout
@@ -32,9 +31,10 @@ class GitlabSchema < GraphQL::Schema
class << self
def multiplex(queries, **kwargs)
- kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
+ kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context]) unless kwargs.key?(:max_complexity)
queries.each do |query|
+ query[:max_complexity] ||= max_query_complexity(kwargs[:context]) unless query.key?(:max_complexity)
query[:max_depth] = max_query_depth(kwargs[:context])
end
@@ -111,6 +111,7 @@ class GitlabSchema < GraphQL::Schema
#
# Options:
# * :expected_type [Class] - the type of object this GlobalID should refer to.
+ # * :expected_type [[Class]] - array of the types of object this GlobalID should refer to.
#
# e.g.
#
@@ -120,14 +121,14 @@ class GitlabSchema < GraphQL::Schema
# gid.model_class == ::Project
# ```
def parse_gid(global_id, ctx = {})
- expected_type = ctx[:expected_type]
+ expected_types = Array(ctx[:expected_type])
gid = GlobalID.parse(global_id)
raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID." unless gid
- if expected_type && !gid.model_class.ancestors.include?(expected_type)
- vars = { global_id: global_id, expected_type: expected_type }
- msg = _('%{global_id} is not a valid ID for %{expected_type}.') % vars
+ if expected_types.any? && expected_types.none? { |type| gid.model_class.ancestors.include?(type) }
+ vars = { global_id: global_id, expected_types: expected_types.join(', ') }
+ msg = _('%{global_id} is not a valid ID for %{expected_types}.') % vars
raise Gitlab::Graphql::Errors::ArgumentError, msg
end
diff --git a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb
index 32ca6de9b96..ea1502d4b62 100644
--- a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb
+++ b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb
@@ -8,7 +8,7 @@ module Mutations
ADMIN_MESSAGE = 'You must be an admin to use this mutation'
- Labkit::Context::KNOWN_KEYS.each do |key|
+ Gitlab::ApplicationContext::KNOWN_KEYS.each do |key|
argument key,
GraphQL::STRING_TYPE,
required: false,
diff --git a/app/graphql/mutations/alert_management/prometheus_integration/create.rb b/app/graphql/mutations/alert_management/prometheus_integration/create.rb
index 87e6bc46937..c6dc85dc07c 100644
--- a/app/graphql/mutations/alert_management/prometheus_integration/create.rb
+++ b/app/graphql/mutations/alert_management/prometheus_integration/create.rb
@@ -18,7 +18,7 @@ module Mutations
argument :api_url, GraphQL::STRING_TYPE,
required: true,
- description: 'Endpoint at which prometheus can be queried.'
+ description: 'Endpoint at which Prometheus can be queried.'
def resolve(args)
project = authorized_find!(args[:project_path])
diff --git a/app/graphql/mutations/alert_management/prometheus_integration/update.rb b/app/graphql/mutations/alert_management/prometheus_integration/update.rb
index 62fb81bca5a..7594766176f 100644
--- a/app/graphql/mutations/alert_management/prometheus_integration/update.rb
+++ b/app/graphql/mutations/alert_management/prometheus_integration/update.rb
@@ -16,7 +16,7 @@ module Mutations
argument :api_url, GraphQL::STRING_TYPE,
required: false,
- description: "Endpoint at which prometheus can be queried."
+ description: "Endpoint at which Prometheus can be queried."
def resolve(args)
integration = authorized_find!(id: args[:id])
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index ac5ddc5bd4c..1f18a37fcb9 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -2,13 +2,14 @@
module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation
- prepend Gitlab::Graphql::Authorize::AuthorizeResource
+ include Gitlab::Graphql::Authorize::AuthorizeResource
prepend Gitlab::Graphql::CopyFieldDescription
prepend ::Gitlab::Graphql::GlobalIDCompatibility
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
field_class ::Types::BaseField
+ argument_class ::Types::BaseArgument
field :errors, [GraphQL::STRING_TYPE],
null: false,
@@ -28,11 +29,29 @@ module Mutations
end
def ready?(**args)
- if Gitlab::Database.read_only?
- raise Gitlab::Graphql::Errors::ResourceNotAvailable, ERROR_MESSAGE
- else
- true
+ raise_resource_not_available_error! ERROR_MESSAGE if Gitlab::Database.read_only?
+
+ true
+ end
+
+ def load_application_object(argument, lookup_as_type, id, context)
+ ::Gitlab::Graphql::Lazy.new { super }.catch(::GraphQL::UnauthorizedError) do |e|
+ Gitlab::ErrorTracking.track_exception(e)
+ # The default behaviour is to abort processing and return nil for the
+ # entire mutation field, but not set any top-level errors. We prefer to
+ # at least say that something went wrong.
+ raise_resource_not_available_error!
end
end
+
+ def self.authorized?(object, context)
+ # we never provide an object to mutations, but we do need to have a user.
+ context[:current_user].present? && !context[:current_user].blocked?
+ end
+
+ # See: AuthorizeResource#authorized_resource?
+ def self.authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize)
+ end
end
end
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb
index 096ac89db1c..f32205643da 100644
--- a/app/graphql/mutations/boards/issues/issue_move_list.rb
+++ b/app/graphql/mutations/boards/issues/issue_move_list.rb
@@ -52,13 +52,10 @@ module Mutations
super
end
- def resolve(board:, **args)
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/247861')
+ def resolve(board:, project_path:, iid:, **args)
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/247861')
- raise_resource_not_available_error! unless board
- authorize_board!(board)
-
- issue = authorized_find!(project_path: args[:project_path], iid: args[:iid])
+ issue = authorized_find!(project_path: project_path, iid: iid)
move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args))
move_issue(board, issue, move_params)
@@ -84,12 +81,6 @@ module Mutations
def move_arguments(args)
args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id)
end
-
- def authorize_board!(board)
- return if Ability.allowed?(current_user, :read_issue_board, board.resource_parent)
-
- raise_resource_not_available_error!
- end
end
end
end
diff --git a/app/graphql/mutations/ci/ci_cd_settings_update.rb b/app/graphql/mutations/ci/ci_cd_settings_update.rb
index 6b7750ee860..d7451babaea 100644
--- a/app/graphql/mutations/ci/ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/ci_cd_settings_update.rb
@@ -17,13 +17,23 @@ module Mutations
required: false,
description: 'Indicates if the latest artifact should be kept for this project.'
+ field :ci_cd_settings,
+ Types::Ci::CiCdSettingType,
+ null: false,
+ description: 'The CI/CD settings after mutation.'
+
def resolve(full_path:, **args)
project = authorized_find!(full_path)
settings = project.ci_cd_settings
settings.update(args)
- { errors: errors_on_object(settings) }
+ {
+ ci_cd_settings: settings,
+ errors: errors_on_object(settings)
+ }
end
end
end
end
+
+Mutations::Ci::CiCdSettingsUpdate.prepend_if_ee('::EE::Mutations::Ci::CiCdSettingsUpdate')
diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb
index f6f4b744f4e..d3ab0a1779a 100644
--- a/app/graphql/mutations/concerns/mutations/assignable.rb
+++ b/app/graphql/mutations/concerns/mutations/assignable.rb
@@ -13,19 +13,15 @@ module Mutations
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
+ default_value: Types::MutationOperationModeEnum.default_mode,
description: 'The operation to perform. Defaults to REPLACE.'
end
- def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
+ def resolve(project_path:, iid:, assignee_usernames:, operation_mode:)
resource = authorized_find!(project_path: project_path, iid: iid)
+ users = new_assignees(resource, assignee_usernames)
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098') if resource.is_a?(MergeRequest)
-
- update_service_class.new(
- resource.project,
- current_user,
- assignee_ids: assignee_ids(resource, assignee_usernames, operation_mode)
- ).execute(resource)
+ assign!(resource, users, operation_mode)
{
resource.class.name.underscore.to_sym => resource,
@@ -35,18 +31,32 @@ module Mutations
private
- def assignee_ids(resource, usernames, operation_mode)
- assignee_ids = []
- assignee_ids += resource.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode)
- user_ids = UsersFinder.new(current_user, username: usernames).execute.map(&:id)
+ def assign!(resource, users, operation_mode)
+ update_service_class.new(
+ resource.project,
+ current_user,
+ assignee_ids: assignee_ids(resource, users, operation_mode)
+ ).execute(resource)
+ end
- if operation_mode == Types::MutationOperationModeEnum.enum[:remove]
- assignee_ids -= user_ids
- else
- assignee_ids |= user_ids
- end
+ def new_assignees(resource, usernames)
+ UsersFinder.new(current_user, username: usernames).execute.to_a
+ end
+
+ def assignee_ids(resource, users, mode)
+ transform_list(mode, resource, users.map(&:id))
+ end
+
+ def current_assignee_ids(resource)
+ resource.assignees.map(&:id)
+ end
- assignee_ids
+ def transform_list(mode, resource, new_values)
+ case mode
+ when 'REPLACE' then new_values
+ when 'APPEND' then current_assignee_ids(resource) | new_values
+ when 'REMOVE' then current_assignee_ids(resource) - new_values
+ end
end
end
end
diff --git a/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb b/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb
index ba644eff36c..3c5f077110c 100644
--- a/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb
+++ b/app/graphql/mutations/concerns/mutations/can_mutate_spammable.rb
@@ -1,64 +1,51 @@
# frozen_string_literal: true
module Mutations
- # This concern can be mixed into a mutation to provide support for spam checking,
- # and optionally support the workflow to allow clients to display and solve CAPTCHAs.
+ # This concern is deprecated and will be deleted in 14.6
+ #
+ # Use the SpamProtection concern instead.
module CanMutateSpammable
extend ActiveSupport::Concern
- include Spam::Concerns::HasSpamActionResponseFields
- # NOTE: The arguments and fields are intentionally named with 'captcha' instead of 'recaptcha',
- # so that they can be applied to future alternative CAPTCHA implementations other than
- # reCAPTCHA (e.g. FriendlyCaptcha) without having to change the names and descriptions in the API.
+ DEPRECATION_NOTICE = {
+ reason: 'Use spam protection with HTTP headers instead',
+ milestone: '13.11'
+ }.freeze
+
included do
argument :captcha_response, GraphQL::STRING_TYPE,
required: false,
+ deprecated: DEPRECATION_NOTICE,
description: 'A valid CAPTCHA response value obtained by using the provided captchaSiteKey with a CAPTCHA API to present a challenge to be solved on the client. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".'
argument :spam_log_id, GraphQL::INT_TYPE,
required: false,
+ deprecated: DEPRECATION_NOTICE,
description: 'The spam log ID which must be passed along with a valid CAPTCHA response for the operation to be completed. Required to resubmit if the previous operation returned "NeedsCaptchaResponse: true".'
field :spam,
GraphQL::BOOLEAN_TYPE,
null: true,
+ deprecated: DEPRECATION_NOTICE,
description: 'Indicates whether the operation was detected as definite spam. There is no option to resubmit the request with a CAPTCHA response.'
field :needs_captcha_response,
GraphQL::BOOLEAN_TYPE,
null: true,
+ deprecated: DEPRECATION_NOTICE,
description: 'Indicates whether the operation was detected as possible spam and not completed. If CAPTCHA is enabled, the request must be resubmitted with a valid CAPTCHA response and spam_log_id included for the operation to be completed. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
field :spam_log_id,
GraphQL::INT_TYPE,
null: true,
+ deprecated: DEPRECATION_NOTICE,
description: 'The spam log ID which must be passed along with a valid CAPTCHA response for an operation to be completed. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
field :captcha_site_key,
GraphQL::STRING_TYPE,
null: true,
+ deprecated: DEPRECATION_NOTICE,
description: 'The CAPTCHA site key which must be used to render a challenge for the user to solve to obtain a valid captchaResponse value. Included only when an operation was not completed because "NeedsCaptchaResponse" is true.'
end
-
- private
-
- # additional_spam_params -> hash
- #
- # Used from a spammable mutation's #resolve method to generate
- # the required additional spam/recaptcha params which must be merged into the params
- # passed to the constructor of a service, where they can then be used in the service
- # to perform spam checking via SpamActionService.
- #
- # Also accesses the #context of the mutation's Resolver superclass to obtain the request.
- #
- # Example:
- #
- # existing_args.merge!(additional_spam_params)
- def additional_spam_params
- {
- api: true,
- request: context[:request]
- }
- end
end
end
diff --git a/app/graphql/mutations/concerns/mutations/spam_protection.rb b/app/graphql/mutations/concerns/mutations/spam_protection.rb
new file mode 100644
index 00000000000..d765da23a4b
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/spam_protection.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Mutations
+ # This concern can be mixed into a mutation to provide support for spam checking,
+ # and optionally support the workflow to allow clients to display and solve CAPTCHAs.
+ module SpamProtection
+ extend ActiveSupport::Concern
+ include Spam::Concerns::HasSpamActionResponseFields
+
+ SpamActionError = Class.new(GraphQL::ExecutionError)
+ NeedsCaptchaResponseError = Class.new(SpamActionError)
+ SpamDisallowedError = Class.new(SpamActionError)
+
+ NEEDS_CAPTCHA_RESPONSE_MESSAGE = "Request denied. Solve CAPTCHA challenge and retry"
+ SPAM_DISALLOWED_MESSAGE = "Request denied. Spam detected"
+
+ private
+
+ # additional_spam_params -> hash
+ #
+ # Used from a spammable mutation's #resolve method to generate
+ # the required additional spam/CAPTCHA params which must be merged into the params
+ # passed to the constructor of a service, where they can then be used in the service
+ # to perform spam checking via SpamActionService.
+ #
+ # Also accesses the #context of the mutation's Resolver superclass to obtain the request.
+ #
+ # Example:
+ #
+ # existing_args.merge!(additional_spam_params)
+ def additional_spam_params
+ {
+ api: true,
+ request: context[:request]
+ }
+ end
+
+ def spam_action_response(object)
+ fields = spam_action_response_fields(object)
+
+ # If the SpamActionService detected something as spam,
+ # this is non-recoverable and the needs_captcha_response
+ # should not be considered
+ kind = if fields[:spam]
+ :spam
+ elsif fields[:needs_captcha_response]
+ :needs_captcha_response
+ end
+
+ [kind, fields]
+ end
+
+ def check_spam_action_response!(object)
+ kind, fields = spam_action_response(object)
+
+ case kind
+ when :needs_captcha_response
+ fields.delete :spam
+ raise NeedsCaptchaResponseError.new(NEEDS_CAPTCHA_RESPONSE_MESSAGE, extensions: fields)
+ when :spam
+ raise SpamDisallowedError.new(SPAM_DISALLOWED_MESSAGE, extensions: { spam: true })
+ else
+ nil
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/container_repositories/destroy_tags.rb b/app/graphql/mutations/container_repositories/destroy_tags.rb
index 636ceccee04..12d65f604b8 100644
--- a/app/graphql/mutations/container_repositories/destroy_tags.rb
+++ b/app/graphql/mutations/container_repositories/destroy_tags.rb
@@ -3,7 +3,7 @@
module Mutations
module ContainerRepositories
class DestroyTags < ::Mutations::ContainerRepositories::DestroyBase
- LIMIT = 20.freeze
+ LIMIT = 20
TOO_MANY_TAGS_ERROR_MESSAGE = "Number of tags is greater than #{LIMIT}"
diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb
index 3f97325c921..0f2af99bf61 100644
--- a/app/graphql/mutations/issues/move.rb
+++ b/app/graphql/mutations/issues/move.rb
@@ -11,7 +11,7 @@ module Mutations
description: 'The project to move the issue to.'
def resolve(project_path:, iid:, target_project_path:)
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/267762')
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20816')
issue = authorized_find!(project_path: project_path, iid: iid)
source_project = issue.project
diff --git a/app/graphql/mutations/issues/set_assignees.rb b/app/graphql/mutations/issues/set_assignees.rb
index a4d1c755b53..8413c89b010 100644
--- a/app/graphql/mutations/issues/set_assignees.rb
+++ b/app/graphql/mutations/issues/set_assignees.rb
@@ -7,6 +7,19 @@ module Mutations
include Assignable
+ def assign!(issue, users, mode)
+ permitted, forbidden = users.partition { |u| u.can?(:read_issue, issue) }
+
+ super(issue, permitted, mode)
+
+ forbidden.each do |user|
+ issue.errors.add(
+ :assignees,
+ "Cannot assign #{user.to_reference} to #{issue.to_reference}"
+ )
+ end
+ end
+
def update_service_class
::Issues::UpdateService
end
diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb
index 540be7098ac..da94dcd8890 100644
--- a/app/graphql/mutations/merge_requests/accept.rb
+++ b/app/graphql/mutations/merge_requests/accept.rb
@@ -42,7 +42,8 @@ module Mutations
description: 'Squash commits on the source branch before merge.'
def resolve(project_path:, iid:, **args)
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42317')
+ Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/4796')
+
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.target_project
merge_params = args.compact.with_indifferent_access
diff --git a/app/graphql/mutations/merge_requests/set_assignees.rb b/app/graphql/mutations/merge_requests/set_assignees.rb
index 548c6b55a85..dc96523685e 100644
--- a/app/graphql/mutations/merge_requests/set_assignees.rb
+++ b/app/graphql/mutations/merge_requests/set_assignees.rb
@@ -8,7 +8,7 @@ module Mutations
include Assignable
def update_service_class
- ::MergeRequests::UpdateService
+ ::MergeRequests::UpdateAssigneesService
end
end
end
diff --git a/app/graphql/mutations/release_asset_links/delete.rb b/app/graphql/mutations/release_asset_links/delete.rb
new file mode 100644
index 00000000000..dd450f36cdd
--- /dev/null
+++ b/app/graphql/mutations/release_asset_links/delete.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ReleaseAssetLinks
+ class Delete < BaseMutation
+ graphql_name 'ReleaseAssetLinkDelete'
+
+ authorize :destroy_release
+
+ ReleaseAssetLinkID = ::Types::GlobalIDType[::Releases::Link]
+
+ argument :id, ReleaseAssetLinkID,
+ required: true,
+ description: 'ID of the release asset link to delete.'
+
+ field :link,
+ Types::ReleaseAssetLinkType,
+ null: true,
+ description: 'The deleted release asset link.'
+
+ def resolve(id:)
+ link = authorized_find!(id)
+
+ unless link.destroy
+ return { link: nil, errors: link.errors.full_messages }
+ end
+
+ { link: link, errors: [] }
+ end
+
+ def find_object(id)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ReleaseAssetLinkID.coerce_isolated_input(id)
+
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb
index 7f2dd448b8b..e9b45294659 100644
--- a/app/graphql/mutations/snippets/create.rb
+++ b/app/graphql/mutations/snippets/create.rb
@@ -5,6 +5,7 @@ module Mutations
class Create < BaseMutation
include ServiceCompatibility
include CanMutateSpammable
+ include Mutations::SpamProtection
authorize :create_snippet
@@ -56,12 +57,12 @@ module Mutations
end
snippet = service_response.payload[:snippet]
- with_spam_action_response_fields(snippet) do
- {
- snippet: service_response.success? ? snippet : nil,
- errors: errors_on_object(snippet)
- }
- end
+ check_spam_action_response!(snippet)
+
+ {
+ snippet: service_response.success? ? snippet : nil,
+ errors: errors_on_object(snippet)
+ }
end
private
diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb
index 9f9f8bca848..b9b9b13eebb 100644
--- a/app/graphql/mutations/snippets/update.rb
+++ b/app/graphql/mutations/snippets/update.rb
@@ -5,6 +5,7 @@ module Mutations
class Update < Base
include ServiceCompatibility
include CanMutateSpammable
+ include Mutations::SpamProtection
graphql_name 'UpdateSnippet'
@@ -45,12 +46,12 @@ module Mutations
end
snippet = service_response.payload[:snippet]
- with_spam_action_response_fields(snippet) do
- {
- snippet: service_response.success? ? snippet : snippet.reset,
- errors: errors_on_object(snippet)
- }
- end
+ check_spam_action_response!(snippet)
+
+ {
+ snippet: service_response.success? ? snippet : snippet.reset,
+ errors: errors_on_object(snippet)
+ }
end
private
diff --git a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
index 92323923266..959bf7dc91d 100644
--- a/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
+++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql
@@ -27,6 +27,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
__typename
id
iid
+ usesNeeds
downstream {
__typename
nodes {
diff --git a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
index 94a72bca7c7..abc54614a59 100644
--- a/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
+++ b/app/graphql/resolvers/alert_management/http_integrations_resolver.rb
@@ -3,19 +3,39 @@
module Resolvers
module AlertManagement
class HttpIntegrationsResolver < BaseResolver
- alias_method :project, :synchronized_object
+ include ::Gitlab::Graphql::Laziness
+
+ alias_method :project, :object
+
+ argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
+ required: false,
+ description: 'ID of the integration.'
type Types::AlertManagement::HttpIntegrationType.connection_type, null: true
- def resolve(**args)
- http_integrations
+ def resolve(id: nil)
+ return [] unless Ability.allowed?(current_user, :admin_operations, project)
+
+ if id
+ integrations_by(gid: id)
+ else
+ http_integrations
+ end
end
private
- def http_integrations
- return [] unless Ability.allowed?(current_user, :admin_operations, project)
+ def integrations_by(gid:)
+ id = Types::GlobalIDType[::AlertManagement::HttpIntegration].coerce_isolated_input(gid)
+ object = GitlabSchema.find_by_gid(id)
+
+ defer { object }.then do |integration|
+ ret = integration if project == integration&.project
+ Array.wrap(ret)
+ end
+ end
+ def http_integrations
::AlertManagement::HttpIntegrationsFinder.new(project, {}).execute
end
end
diff --git a/app/graphql/resolvers/alert_management/integrations_resolver.rb b/app/graphql/resolvers/alert_management/integrations_resolver.rb
index 4d1fe367277..cb7e73c2d1a 100644
--- a/app/graphql/resolvers/alert_management/integrations_resolver.rb
+++ b/app/graphql/resolvers/alert_management/integrations_resolver.rb
@@ -3,27 +3,60 @@
module Resolvers
module AlertManagement
class IntegrationsResolver < BaseResolver
- alias_method :project, :synchronized_object
+ include ::Gitlab::Graphql::Laziness
+
+ alias_method :project, :object
+
+ argument :id, ::Types::GlobalIDType,
+ required: false,
+ description: 'ID of the integration.'
type Types::AlertManagement::IntegrationType.connection_type, null: true
- def resolve(**args)
- http_integrations + prometheus_integrations
+ def resolve(id: nil)
+ if id
+ integrations_by(gid: id)
+ else
+ http_integrations + prometheus_integrations
+ end
end
private
+ def integrations_by(gid:)
+ object = GitlabSchema.object_from_id(gid, expected_type: expected_integration_types)
+ defer { object }.then do |integration|
+ ret = integration if project == integration&.project
+ Array.wrap(ret)
+ end
+ end
+
def prometheus_integrations
- return [] unless Ability.allowed?(current_user, :admin_project, project)
+ return [] unless prometheus_integrations_allowed?
Array(project.prometheus_service)
end
def http_integrations
- return [] unless Ability.allowed?(current_user, :admin_operations, project)
+ return [] unless http_integrations_allowed?
::AlertManagement::HttpIntegrationsFinder.new(project, {}).execute
end
+
+ def prometheus_integrations_allowed?
+ Ability.allowed?(current_user, :admin_project, project)
+ end
+
+ def http_integrations_allowed?
+ Ability.allowed?(current_user, :admin_operations, project)
+ end
+
+ def expected_integration_types
+ [].tap do |types|
+ types << ::AlertManagement::HttpIntegration if http_integrations_allowed?
+ types << ::PrometheusService if prometheus_integrations_allowed?
+ end
+ end
end
end
end
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 67bba079512..48563633d11 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -39,9 +39,7 @@ module Resolvers
as_single << block
# Have we been called after defining the single version of this resolver?
- if @single.present?
- @single.instance_exec(&block)
- end
+ @single.instance_exec(&block) if @single.present?
end
def self.as_single
@@ -90,7 +88,7 @@ module Resolvers
def self.last
parent = self
- @last ||= Class.new(self.single) do
+ @last ||= Class.new(single) do
type parent.singular_type, null: true
def select_result(results)
@@ -138,16 +136,6 @@ 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
- ::Gitlab::Graphql::Lazy.force(object)
- end
- end
-
def single?
false
end
@@ -160,5 +148,13 @@ module Resolvers
def select_result(results)
results
end
+
+ def self.authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(try(:required_permissions))
+ end
+
+ def self.authorized?(object, context)
+ authorization.ok?(object, context[:current_user])
+ end
end
end
diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb
new file mode 100644
index 00000000000..d006769bd4b
--- /dev/null
+++ b/app/graphql/resolvers/blobs_resolver.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BlobsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Tree::BlobType.connection_type, null: true
+ authorize :download_code
+ calls_gitaly!
+
+ alias_method :repository, :object
+
+ argument :paths, [GraphQL::STRING_TYPE],
+ required: true,
+ description: 'Array of desired blob paths.'
+ argument :ref, GraphQL::STRING_TYPE,
+ required: false,
+ default_value: nil,
+ description: 'The commit ref to get the blobs from. Default value is HEAD.'
+
+ # We fetch blobs from Gitaly efficiently but it still scales O(N) with the
+ # number of paths being fetched, so apply a scaling limit to that.
+ def self.resolver_complexity(args, child_complexity:)
+ super + (args[:paths] || []).size
+ end
+
+ def resolve(paths:, ref:)
+ authorize!(repository.container)
+
+ return [] if repository.empty?
+
+ ref ||= repository.root_ref
+
+ repository.blobs_at(paths.map { |path| [ref, path] })
+ end
+ end
+end
diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb
index e66f7b97b40..0b699006626 100644
--- a/app/graphql/resolvers/board_lists_resolver.rb
+++ b/app/graphql/resolvers/board_lists_resolver.rb
@@ -3,13 +3,12 @@
module Resolvers
class BoardListsResolver < BaseResolver
include BoardIssueFilterable
- prepend ManualAuthorization
include Gitlab::Graphql::Authorize::AuthorizeResource
+ include LooksAhead
type Types::BoardListType, null: true
- extras [:lookahead]
-
authorize :read_issue_board_list
+ authorizes_object!
argument :id, Types::GlobalIDType[List],
required: false,
@@ -21,15 +20,11 @@ module Resolvers
alias_method :board, :object
- def resolve(lookahead: nil, id: nil, issue_filters: {})
- authorize!(board)
-
+ def resolve_with_lookahead(id: nil, issue_filters: {})
lists = board_lists(id)
context.scoped_set!(:issue_filters, issue_filters(issue_filters))
- if load_preferences?(lookahead)
- List.preload_preferences_for_user(lists, current_user)
- end
+ List.preload_preferences_for_user(lists, current_user) if load_preferences?
offset_pagination(lists)
end
@@ -46,9 +41,8 @@ module Resolvers
service.execute(board, create_default_lists: false)
end
- def load_preferences?(lookahead)
- lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed) ||
- lookahead&.selection(:nodes)&.selects?(:collapsed)
+ def load_preferences?
+ node_selection&.selects?(:collapsed)
end
def extract_list_id(gid)
diff --git a/app/graphql/resolvers/board_resolver.rb b/app/graphql/resolvers/board_resolver.rb
index 637d690e4cd..85362ab1422 100644
--- a/app/graphql/resolvers/board_resolver.rb
+++ b/app/graphql/resolvers/board_resolver.rb
@@ -2,7 +2,7 @@
module Resolvers
class BoardResolver < BaseResolver.single
- alias_method :parent, :synchronized_object
+ alias_method :parent, :object
type Types::BoardType, null: true
diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb
index f8670649e48..252c9d3acf0 100644
--- a/app/graphql/resolvers/ci/config_resolver.rb
+++ b/app/graphql/resolvers/ci/config_resolver.rb
@@ -7,6 +7,10 @@ module Resolvers
include ResolvesProject
type Types::Ci::Config::ConfigType, null: true
+ description <<~MD
+ Linted and processed contents of a CI config.
+ Should not be requested more than once per request.
+ MD
authorize :read_pipeline
@@ -55,7 +59,7 @@ module Resolvers
name: job[:name],
stage: job[:stage],
group_name: CommitStatus.new(name: job[:name]).group_name,
- needs: job.dig(:needs) || [],
+ needs: job[:needs] || [],
allow_failure: job[:allow_failure],
before_script: job[:before_script],
script: job[:script],
diff --git a/app/graphql/resolvers/ci/jobs_resolver.rb b/app/graphql/resolvers/ci/jobs_resolver.rb
index dd565094017..5ae9e721cc8 100644
--- a/app/graphql/resolvers/ci/jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/jobs_resolver.rb
@@ -11,7 +11,18 @@ module Resolvers
required: false,
description: 'Filter jobs by the type of security report they produce.'
- def resolve(security_report_types: [])
+ argument :statuses, [::Types::Ci::JobStatusEnum],
+ required: false,
+ description: 'Filter jobs by status.'
+
+ def resolve(statuses: nil, security_report_types: [])
+ jobs = init_collection(security_report_types)
+ jobs = jobs.with_status(statuses) if statuses.present?
+
+ jobs
+ end
+
+ def init_collection(security_report_types)
if security_report_types.present?
::Security::SecurityJobsFinder.new(
pipeline: pipeline,
diff --git a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
index 98170e0cd2e..a458e873935 100644
--- a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
+++ b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
@@ -16,7 +16,7 @@ module Resolvers
def preloads
{
- statuses: [:needs]
+ jobs: { latest_statuses: [:needs] }
}
end
end
diff --git a/app/graphql/resolvers/ci/runner_platforms_resolver.rb b/app/graphql/resolvers/ci/runner_platforms_resolver.rb
index 9677c5139b4..f120e94b67b 100644
--- a/app/graphql/resolvers/ci/runner_platforms_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_platforms_resolver.rb
@@ -3,7 +3,8 @@
module Resolvers
module Ci
class RunnerPlatformsResolver < BaseResolver
- type Types::Ci::RunnerPlatformType, null: false
+ type Types::Ci::RunnerPlatformType.connection_type, null: true
+ description 'Supported runner platforms.'
def resolve(**args)
runner_instructions.map do |platform, data|
diff --git a/app/graphql/resolvers/ci/runner_setup_resolver.rb b/app/graphql/resolvers/ci/runner_setup_resolver.rb
index ac2a56b89a7..9166999b400 100644
--- a/app/graphql/resolvers/ci/runner_setup_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_setup_resolver.rb
@@ -3,30 +3,37 @@
module Resolvers
module Ci
class RunnerSetupResolver < BaseResolver
+ ACCESS_DENIED = 'User is not authorized to register a runner for the specified resource!'
+
type Types::Ci::RunnerSetupType, null: true
+ description 'Runner setup instructions.'
- argument :platform, GraphQL::STRING_TYPE,
- required: true,
- description: 'Platform to generate the instructions for.'
+ argument :platform,
+ type: 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 :architecture,
+ type: 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 :project_id,
+ type: ::Types::GlobalIDType[::Project],
+ required: false,
+ deprecated: { reason: 'No longer used', milestone: '13.11' },
+ description: 'Project to register the runner for.'
- argument :group_id, ::Types::GlobalIDType[::Group],
- required: false,
- description: 'Group to register the runner for.'
+ argument :group_id,
+ type: ::Types::GlobalIDType[::Group],
+ required: false,
+ deprecated: { reason: 'No longer used', milestone: '13.11' },
+ 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,
- **target_param(args)
+ arch: architecture
)
{
@@ -34,11 +41,15 @@ module Resolvers
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')
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, ACCESS_DENIED if access_denied?(instructions)
end
private
+ def access_denied?(instructions)
+ instructions.errors.include?('Gitlab::Access::AccessDeniedError')
+ end
+
def other_install_instructions(platform)
Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS[platform.to_sym][:installation_instructions_url]
end
diff --git a/app/graphql/resolvers/ci/test_report_summary_resolver.rb b/app/graphql/resolvers/ci/test_report_summary_resolver.rb
new file mode 100644
index 00000000000..22db70f032a
--- /dev/null
+++ b/app/graphql/resolvers/ci/test_report_summary_resolver.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class TestReportSummaryResolver < BaseResolver
+ type ::Types::Ci::TestReportSummaryType, null: true
+
+ alias_method :pipeline, :object
+
+ def resolve(**args)
+ TestReportSummarySerializer
+ .new(project: pipeline.project, current_user: @current_user)
+ .represent(pipeline.test_report_summary)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb
new file mode 100644
index 00000000000..90cc30b1281
--- /dev/null
+++ b/app/graphql/resolvers/ci/test_suite_resolver.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class TestSuiteResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type ::Types::Ci::TestSuiteType, null: true
+ authorize :read_build
+ authorizes_object!
+
+ alias_method :pipeline, :object
+
+ argument :build_ids, [GraphQL::ID_TYPE],
+ required: true,
+ description: 'IDs of the builds used to run the test suite.'
+
+ def resolve(build_ids:)
+ builds = pipeline.latest_builds.id_in(build_ids).presence
+ return unless builds
+
+ TestSuiteSerializer
+ .new(project: pipeline.project, current_user: @current_user)
+ .represent(load_test_suite_data(builds), details: true)
+ end
+
+ private
+
+ def load_test_suite_data(builds)
+ suite = builds.sum do |build|
+ build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
+ end
+
+ Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load!
+
+ suite
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/concerns/board_issue_filterable.rb b/app/graphql/resolvers/concerns/board_issue_filterable.rb
index 1541738f46c..3484a1cc4ba 100644
--- a/app/graphql/resolvers/concerns/board_issue_filterable.rb
+++ b/app/graphql/resolvers/concerns/board_issue_filterable.rb
@@ -7,10 +7,10 @@ module BoardIssueFilterable
def issue_filters(args)
filters = args.to_h
+
set_filter_values(filters)
if filters[:not]
- filters[:not] = filters[:not].to_h
set_filter_values(filters[:not])
end
@@ -18,6 +18,17 @@ module BoardIssueFilterable
end
def set_filter_values(filters)
+ filter_by_assignee(filters)
+ end
+
+ def filter_by_assignee(filters)
+ if filters[:assignee_username] && filters[:assignee_wildcard_id]
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'Incompatible arguments: assigneeUsername, assigneeWildcardId.'
+ end
+
+ if filters[:assignee_wildcard_id]
+ filters[:assignee_id] = filters.delete(:assignee_wildcard_id)
+ end
end
end
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index 84b0dafe213..0ff3997f3bc 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -12,10 +12,10 @@ module IssueResolverArguments
argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'List of IIDs of issues. For example, [1, 2].'
- argument :label_name, GraphQL::STRING_TYPE.to_list_type,
+ argument :label_name, [GraphQL::STRING_TYPE, null: true],
required: false,
description: 'Labels applied to this issue.'
- argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
+ argument :milestone_title, [GraphQL::STRING_TYPE, null: true],
required: false,
description: 'Milestone applied to this issue.'
argument :author_username, GraphQL::STRING_TYPE,
@@ -23,7 +23,8 @@ module IssueResolverArguments
description: 'Username of the author of the issue.'
argument :assignee_username, GraphQL::STRING_TYPE,
required: false,
- description: 'Username of a user assigned to the issue.'
+ description: 'Username of a user assigned to the issue.',
+ deprecated: { reason: 'Use `assigneeUsernames`', milestone: '13.11' }
argument :assignee_usernames, [GraphQL::STRING_TYPE],
required: false,
description: 'Usernames of users assigned to the issue.'
@@ -55,6 +56,10 @@ module IssueResolverArguments
as: :issue_types,
description: 'Filter issues by the given issue types.',
required: false
+ argument :not, Types::Issues::NegatedIssueFilterInputType,
+ description: 'List of negated params.',
+ prepare: ->(negated_args, ctx) { negated_args.to_h },
+ required: false
end
def resolve_with_lookahead(**args)
@@ -69,11 +74,22 @@ module IssueResolverArguments
args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
args[:attempt_project_search_optimizations] = true if args[:search].present?
+ prepare_assignee_username_params(args)
+
finder = IssuesFinder.new(current_user, args)
continue_issue_resolve(parent, finder, **args)
end
+ def ready?(**args)
+ if args.slice(*mutually_exclusive_assignee_username_args).compact.size > 1
+ arg_str = mutually_exclusive_assignee_username_args.map { |x| x.to_s.camelize(:lower) }.join(', ')
+ raise Gitlab::Graphql::Errors::ArgumentError, "only one of [#{arg_str}] arguments is allowed at the same time."
+ end
+
+ super
+ end
+
class_methods do
def resolver_complexity(args, child_complexity:)
complexity = super
@@ -82,4 +98,15 @@ module IssueResolverArguments
complexity
end
end
+
+ private
+
+ def prepare_assignee_username_params(args)
+ args[:assignee_username] = args.delete(:assignee_usernames) if args[:assignee_usernames].present?
+ args[:not][:assignee_username] = args[:not].delete(:assignee_usernames) if args.dig(:not, :assignee_usernames).present?
+ end
+
+ def mutually_exclusive_assignee_username_args
+ [:assignee_usernames, :assignee_username]
+ end
end
diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb
index 77a85edfba6..644b2a11460 100644
--- a/app/graphql/resolvers/concerns/looks_ahead.rb
+++ b/app/graphql/resolvers/concerns/looks_ahead.rb
@@ -15,12 +15,7 @@ module LooksAhead
end
def apply_lookahead(query)
- selection = node_selection
-
- includes = preloads.each.flat_map do |name, requirements|
- selection&.selects?(name) ? requirements : []
- end
- all_preloads = (unconditional_includes + includes).uniq
+ all_preloads = (unconditional_includes + filtered_preloads).uniq
return query if all_preloads.empty?
@@ -37,6 +32,14 @@ module LooksAhead
{}
end
+ def filtered_preloads
+ selection = node_selection
+
+ preloads.each.flat_map do |name, requirements|
+ selection&.selects?(name) ? requirements : []
+ end
+ end
+
def node_selection
return unless lookahead
diff --git a/app/graphql/resolvers/concerns/manual_authorization.rb b/app/graphql/resolvers/concerns/manual_authorization.rb
deleted file mode 100644
index 182110b9594..00000000000
--- a/app/graphql/resolvers/concerns/manual_authorization.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: remove this entirely when framework authorization is released
-# See: https://gitlab.com/gitlab-org/gitlab/-/issues/290216
-module ManualAuthorization
- def resolve(**args)
- super
- rescue ::Gitlab::Graphql::Errors::ResourceNotAvailable
- nil
- end
-end
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index 31444b0c592..75f1ee478a8 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -50,7 +50,8 @@ module ResolvesMergeRequests
approved_by: [:approved_by_users],
milestone: [:milestone],
security_auto_fix: [:author],
- head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
+ head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }],
+ timelogs: [:timelogs]
}
end
end
diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb
index 445f3567b1d..8de85c074ec 100644
--- a/app/graphql/resolvers/concerns/resolves_snippets.rb
+++ b/app/graphql/resolvers/concerns/resolves_snippets.rb
@@ -4,7 +4,7 @@ module ResolvesSnippets
extend ActiveSupport::Concern
included do
- type Types::SnippetType.connection_type, null: false
+ type Types::SnippetType.connection_type, null: true
argument :ids, [::Types::GlobalIDType[::Snippet]],
required: false,
diff --git a/app/graphql/resolvers/echo_resolver.rb b/app/graphql/resolvers/echo_resolver.rb
index 0c7dad622cf..a09b0a1fd87 100644
--- a/app/graphql/resolvers/echo_resolver.rb
+++ b/app/graphql/resolvers/echo_resolver.rb
@@ -5,8 +5,10 @@ module Resolvers
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.'
+ argument :text,
+ type: GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Text to echo back.'
def resolve(text:)
username = current_user&.username
diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb
index ed3395d05aa..df04e70e250 100644
--- a/app/graphql/resolvers/environments_resolver.rb
+++ b/app/graphql/resolvers/environments_resolver.rb
@@ -21,7 +21,7 @@ module Resolvers
def resolve(**args)
return unless project.present?
- EnvironmentsFinder.new(project, context[:current_user], args).find
+ EnvironmentsFinder.new(project, context[:current_user], args).execute
rescue EnvironmentsFinder::InvalidStatesError => exception
raise Gitlab::Graphql::Errors::ArgumentError, exception.message
end
diff --git a/app/graphql/resolvers/group_members_resolver.rb b/app/graphql/resolvers/group_members_resolver.rb
index 36e1977b756..d3662b08cdf 100644
--- a/app/graphql/resolvers/group_members_resolver.rb
+++ b/app/graphql/resolvers/group_members_resolver.rb
@@ -13,12 +13,6 @@ module Resolvers
private
- def preloads
- {
- user: [:user, :source]
- }
- end
-
def finder_class
GroupMembersFinder
end
diff --git a/app/graphql/resolvers/group_merge_requests_resolver.rb b/app/graphql/resolvers/group_merge_requests_resolver.rb
index 2bad974daf7..34a4c67bc56 100644
--- a/app/graphql/resolvers/group_merge_requests_resolver.rb
+++ b/app/graphql/resolvers/group_merge_requests_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class GroupMergeRequestsResolver < MergeRequestsResolver
include GroupIssuableResolver
- alias_method :group, :synchronized_object
+ alias_method :group, :object
type Types::MergeRequestType.connection_type, null: true
diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb
index 179283fd7b7..31280b36278 100644
--- a/app/graphql/resolvers/group_milestones_resolver.rb
+++ b/app/graphql/resolvers/group_milestones_resolver.rb
@@ -1,22 +1,40 @@
# frozen_string_literal: true
-# rubocop:disable Graphql/ResolverType (inherited from MilestonesResolver)
module Resolvers
class GroupMilestonesResolver < MilestonesResolver
argument :include_descendants, GraphQL::BOOLEAN_TYPE,
required: false,
- description: 'Also return milestones in all subgroups and subprojects.'
+ description: 'Include milestones from all subgroups and subprojects.'
+ argument :include_ancestors, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Include milestones from all parent groups.'
type Types::MilestoneType.connection_type, null: true
private
def parent_id_parameters(args)
- return { group_ids: parent.id } unless args[:include_descendants].present?
+ include_ancestors = args[:include_ancestors].present?
+ include_descendants = args[:include_descendants].present?
+ return { group_ids: parent.id } unless include_ancestors || include_descendants
+
+ group_ids = if include_ancestors && include_descendants
+ parent.self_and_hierarchy
+ elsif include_ancestors
+ parent.self_and_ancestors
+ else
+ parent.self_and_descendants
+ end
+
+ project_ids = if include_descendants
+ group_projects.with_issues_or_mrs_available_for_user(current_user)
+ else
+ nil
+ end
{
- group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id),
- project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user)
+ group_ids: group_ids.public_or_visible_to_user(current_user).select(:id),
+ project_ids: project_ids
}
end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index ac3bdda0f12..7a67f115abf 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -44,7 +44,8 @@ module Resolvers
{
alert_management_alert: [:alert_management_alert],
labels: [:labels],
- assignees: [:assignees]
+ assignees: [:assignees],
+ timelogs: [:timelogs]
}
end
diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb
index 76c3ae936ee..2b731d54cdd 100644
--- a/app/graphql/resolvers/members_resolver.rb
+++ b/app/graphql/resolvers/members_resolver.rb
@@ -21,6 +21,12 @@ module Resolvers
private
+ def preloads
+ {
+ user: [:user, :source]
+ }
+ end
+
def finder_class
# override in subclass
end
diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb
index 8fd33c6626e..c431d079beb 100644
--- a/app/graphql/resolvers/merge_request_resolver.rb
+++ b/app/graphql/resolvers/merge_request_resolver.rb
@@ -4,14 +4,14 @@ module Resolvers
class MergeRequestResolver < BaseResolver.single
include ResolvesMergeRequests
- alias_method :project, :synchronized_object
+ alias_method :project, :object
type ::Types::MergeRequestType, null: true
argument :iid, GraphQL::STRING_TYPE,
- required: true,
- as: :iids,
- description: 'IID of the merge request, for example `1`.'
+ required: true,
+ as: :iids,
+ description: 'IID of the merge request, for example `1`.'
def no_results_possible?(args)
project.nil?
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index ecbdaaa3f55..a9eea4ae4b8 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -3,42 +3,49 @@
module Resolvers
class MergeRequestsResolver < BaseResolver
include ResolvesMergeRequests
+ extend ::Gitlab::Graphql::NegatableArguments
type ::Types::MergeRequestType.connection_type, null: true
- alias_method :project, :synchronized_object
+ alias_method :project, :object
def self.accept_assignee
argument :assignee_username, GraphQL::STRING_TYPE,
- required: false,
- description: 'Username of the assignee.'
+ required: false,
+ description: 'Username of the assignee.'
end
def self.accept_author
argument :author_username, GraphQL::STRING_TYPE,
- required: false,
- description: 'Username of the author.'
+ required: false,
+ description: 'Username of the author.'
end
def self.accept_reviewer
argument :reviewer_username, GraphQL::STRING_TYPE,
- required: false,
- description: 'Username of the reviewer.'
+ required: false,
+ description: 'Username of the reviewer.'
end
argument :iids, [GraphQL::STRING_TYPE],
- required: false,
- description: 'Array of IIDs of merge requests, for example `[1, 2]`.'
+ required: false,
+ description: 'Array of IIDs of merge requests, for example `[1, 2]`.'
argument :source_branches, [GraphQL::STRING_TYPE],
required: false,
as: :source_branch,
- description: 'Array of source branch names. All resolved merge requests will have one of these branches as their source.'
+ description: <<~DESC
+ Array of source branch names.
+ All resolved merge requests will have one of these branches as their source.
+ DESC
argument :target_branches, [GraphQL::STRING_TYPE],
required: false,
as: :target_branch,
- description: 'Array of target branch names. All resolved merge requests will have one of these branches as their target.'
+ description: <<~DESC
+ Array of target branch names.
+ All resolved merge requests will have one of these branches as their target.
+ DESC
argument :state, ::Types::MergeRequestStateEnum,
required: false,
@@ -62,6 +69,16 @@ module Resolvers
required: false,
default_value: :created_desc
+ negated do
+ argument :labels, [GraphQL::STRING_TYPE],
+ required: false,
+ as: :label_name,
+ description: 'Array of label names. All resolved merge requests will not have these labels.'
+ argument :milestone_title, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Title of the milestone.'
+ end
+
def self.single
::Resolvers::MergeRequestResolver
end
diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb
index a82a4a95254..0669fececd5 100644
--- a/app/graphql/resolvers/metrics/dashboard_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb
@@ -8,15 +8,16 @@ module Resolvers
argument :path, GraphQL::STRING_TYPE,
required: true,
- description: "Path to a file which defines metrics dashboard " \
- "eg: 'config/prometheus/common_metrics.yml'."
+ description: <<~MD
+ Path to a file which defines a metrics dashboard eg: `"config/prometheus/common_metrics.yml"`.
+ MD
alias_method :environment, :object
- def resolve(**args)
+ def resolve(path:)
return unless environment
- ::PerformanceMonitoring::PrometheusDashboard.find_for(**args, **service_params)
+ ::PerformanceMonitoring::PrometheusDashboard.find_for(path: path, **service_params)
end
private
diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb
index 9a715e4d08b..c94e3d9e1d8 100644
--- a/app/graphql/resolvers/milestones_resolver.rb
+++ b/app/graphql/resolvers/milestones_resolver.rb
@@ -7,7 +7,7 @@ module Resolvers
argument :ids, [GraphQL::ID_TYPE],
required: false,
- description: 'Array of global milestone IDs, e.g., "gid://gitlab/Milestone/1".'
+ description: 'Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`.'
argument :state, Types::MilestoneStateEnum,
required: false,
@@ -56,7 +56,7 @@ module Resolvers
end
def parent
- synchronized_object
+ object
end
def parent_id_parameters(args)
diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb
index e688e34599a..89d79747732 100644
--- a/app/graphql/resolvers/package_details_resolver.rb
+++ b/app/graphql/resolvers/package_details_resolver.rb
@@ -2,12 +2,20 @@
module Resolvers
class PackageDetailsResolver < BaseResolver
- type ::Types::Packages::PackageType, null: true
+ type ::Types::Packages::PackageDetailsType, null: true
argument :id, ::Types::GlobalIDType[::Packages::Package],
required: true,
description: 'The global ID of the package.'
+ def ready?(**args)
+ context[self.class] ||= { executions: 0 }
+ context[self.class][:executions] += 1
+ raise GraphQL::ExecutionError, "Package details can be requested only for one package at a time" if context[self.class][:executions] > 1
+
+ super
+ end
+
def resolve(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
diff --git a/app/graphql/resolvers/project_jobs_resolver.rb b/app/graphql/resolvers/project_jobs_resolver.rb
new file mode 100644
index 00000000000..75068014242
--- /dev/null
+++ b/app/graphql/resolvers/project_jobs_resolver.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ProjectJobsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include LooksAhead
+
+ type ::Types::Ci::JobType.connection_type, null: true
+ authorize :read_build
+ authorizes_object!
+
+ argument :statuses, [::Types::Ci::JobStatusEnum],
+ required: false,
+ description: 'Filter jobs by status.'
+
+ alias_method :project, :object
+
+ def ready?(**args)
+ context[self.class] ||= { executions: 0 }
+ context[self.class][:executions] += 1
+ raise GraphQL::ExecutionError, "Jobs can only be requested for one project at a time" if context[self.class][:executions] > 1
+
+ super
+ end
+
+ def resolve_with_lookahead(statuses: nil)
+ jobs = ::Ci::JobsFinder.new(current_user: current_user, project: project, params: { scope: statuses }).execute
+
+ apply_lookahead(jobs)
+ end
+
+ private
+
+ def preloads
+ {
+ artifacts: [:job_artifacts],
+ pipeline: [:user]
+ }
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb
index 8fca6b829c0..aa8808b15ac 100644
--- a/app/graphql/resolvers/project_pipeline_resolver.rb
+++ b/app/graphql/resolvers/project_pipeline_resolver.rb
@@ -31,7 +31,7 @@ module Resolvers
end
else
BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
- finder = ::Ci::PipelinesFinder.new(project, current_user, shas: shas)
+ finder = ::Ci::PipelinesFinder.new(project, current_user, sha: shas)
finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) }
end
diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb
index f618bf2df77..ec31a7dbe6d 100644
--- a/app/graphql/resolvers/projects/services_resolver.rb
+++ b/app/graphql/resolvers/projects/services_resolver.rb
@@ -3,11 +3,11 @@
module Resolvers
module Projects
class ServicesResolver < BaseResolver
- prepend ManualAuthorization
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Projects::ServiceType.connection_type, null: true
authorize :admin_project
+ authorizes_object!
argument :active,
GraphQL::BOOLEAN_TYPE,
@@ -20,15 +20,7 @@ module Resolvers
alias_method :project, :object
- def resolve(**args)
- authorize!(project)
-
- services(args[:active], args[:type])
- end
-
- private
-
- def services(active, type)
+ def resolve(active: nil, type: nil)
servs = project.services
servs = servs.by_active_flag(active) unless active.nil?
servs = servs.by_type(type) unless type.blank?
diff --git a/app/graphql/resolvers/repository_branch_names_resolver.rb b/app/graphql/resolvers/repository_branch_names_resolver.rb
new file mode 100644
index 00000000000..45cfe229b2f
--- /dev/null
+++ b/app/graphql/resolvers/repository_branch_names_resolver.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class RepositoryBranchNamesResolver < BaseResolver
+ type ::GraphQL::STRING_TYPE, null: false
+
+ calls_gitaly!
+
+ argument :search_pattern, GraphQL::STRING_TYPE,
+ required: true,
+ description: 'The pattern to search for branch names by.'
+
+ def resolve(search_pattern:)
+ Repositories::BranchNamesFinder.new(object, search: search_pattern).execute
+ end
+ end
+end
diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb
index 569b82149d3..4328d38d485 100644
--- a/app/graphql/resolvers/snippets/blobs_resolver.rb
+++ b/app/graphql/resolvers/snippets/blobs_resolver.rb
@@ -3,12 +3,12 @@
module Resolvers
module Snippets
class BlobsResolver < BaseResolver
- prepend ManualAuthorization
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Snippets::BlobType.connection_type, null: true
authorize :read_snippet
calls_gitaly!
+ authorizes_object!
alias_method :snippet, :object
@@ -17,7 +17,6 @@ module Resolvers
description: 'Paths of the blobs.'
def resolve(paths: [])
- authorize!(snippet)
return [snippet.blob] if snippet.empty_repo?
if paths.empty?
diff --git a/app/graphql/resolvers/timelog_resolver.rb b/app/graphql/resolvers/timelog_resolver.rb
new file mode 100644
index 00000000000..aebd04259ee
--- /dev/null
+++ b/app/graphql/resolvers/timelog_resolver.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class TimelogResolver < BaseResolver
+ include LooksAhead
+
+ type ::Types::TimelogType.connection_type, null: false
+
+ argument :start_date, Types::TimeType,
+ required: false,
+ description: 'List time logs within a date range where the logged date is equal to or after startDate.'
+
+ argument :end_date, Types::TimeType,
+ required: false,
+ description: 'List time logs within a date range where the logged date is equal to or before endDate.'
+
+ argument :start_time, Types::TimeType,
+ required: false,
+ description: 'List time-logs within a time range where the logged time is equal to or after startTime.'
+
+ argument :end_time, Types::TimeType,
+ required: false,
+ description: 'List time-logs within a time range where the logged time is equal to or before endTime.'
+
+ def resolve_with_lookahead(**args)
+ return Timelog.none unless timelogs_available_for_user?
+
+ validate_params_presence!(args)
+ transformed_args = transform_args(args)
+ validate_time_difference!(transformed_args)
+
+ find_timelogs(transformed_args)
+ end
+
+ private
+
+ def preloads
+ {
+ note: [:note]
+ }
+ end
+
+ def find_timelogs(args)
+ apply_lookahead(group.timelogs(args[:start_time], args[:end_time]))
+ end
+
+ def timelogs_available_for_user?
+ group&.user_can_access_group_timelogs?(context[:current_user])
+ end
+
+ def validate_params_presence!(args)
+ message = case time_params_count(args)
+ when 0
+ 'Start and End arguments must be present'
+ when 1
+ 'Both Start and End arguments must be present'
+ when 2
+ validate_duplicated_args(args)
+ when 3 || 4
+ 'Only Time or Date arguments must be present'
+ end
+
+ raise_argument_error(message) if message
+ end
+
+ def validate_time_difference!(args)
+ message = if args[:end_time] < args[:start_time]
+ 'Start argument must be before End argument'
+ elsif args[:end_time] - args[:start_time] > 60.days
+ 'The time range period cannot contain more than 60 days'
+ end
+
+ raise_argument_error(message) if message
+ end
+
+ def transform_args(args)
+ return args if args.keys == [:start_time, :end_time]
+
+ time_args = args.except(:start_date, :end_date)
+
+ if time_args.empty?
+ time_args[:start_time] = args[:start_date].beginning_of_day
+ time_args[:end_time] = args[:end_date].end_of_day
+ elsif time_args.key?(:start_time)
+ time_args[:end_time] = args[:end_date].end_of_day
+ elsif time_args.key?(:end_time)
+ time_args[:start_time] = args[:start_date].beginning_of_day
+ end
+
+ time_args
+ end
+
+ def time_params_count(args)
+ [:start_time, :end_time, :start_date, :end_date].count { |param| args.key?(param) }
+ end
+
+ def validate_duplicated_args(args)
+ if args.key?(:start_time) && args.key?(:start_date) ||
+ args.key?(:end_time) && args.key?(:end_date)
+ 'Both Start and End arguments must be present'
+ end
+ end
+
+ def raise_argument_error(message)
+ raise Gitlab::Graphql::Errors::ArgumentError, message
+ end
+
+ def group
+ @group ||= object.respond_to?(:sync) ? object.sync : object
+ end
+ end
+end
diff --git a/app/graphql/resolvers/user_merge_requests_resolver_base.rb b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
index 47967fe69f9..221a43f691d 100644
--- a/app/graphql/resolvers/user_merge_requests_resolver_base.rb
+++ b/app/graphql/resolvers/user_merge_requests_resolver_base.rb
@@ -4,16 +4,24 @@ module Resolvers
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_path,
+ type: GraphQL::STRING_TYPE,
+ required: false,
+ description: <<~DESC
+ The full-path of the project the authored merge requests should be in.
+ Incompatible with projectId.
+ DESC
- 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.'
+ argument :project_id,
+ type: ::Types::GlobalIDType[::Project],
+ required: false,
+ description: <<~DESC
+ The global ID of the project the authored merge requests should be in.
+ Incompatible with projectPath.
+ DESC
attr_reader :project
- alias_method :user, :synchronized_object
+ alias_method :user, :object
def ready?(project_id: nil, project_path: nil, **args)
return early_return unless can_read_profile?
@@ -22,8 +30,7 @@ module Resolvers
load_project(project_path, project_id)
return early_return unless can_read_project?
elsif args[:iids].present?
- raise ::Gitlab::Graphql::Errors::ArgumentError,
- 'iids requires projectPath or projectId'
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'iids requires projectPath or projectId'
end
super(**args)
diff --git a/app/graphql/resolvers/user_starred_projects_resolver.rb b/app/graphql/resolvers/user_starred_projects_resolver.rb
index db420b3d116..a8abe759f27 100644
--- a/app/graphql/resolvers/user_starred_projects_resolver.rb
+++ b/app/graphql/resolvers/user_starred_projects_resolver.rb
@@ -2,11 +2,11 @@
module Resolvers
class UserStarredProjectsResolver < BaseResolver
- type Types::ProjectType, null: true
+ type Types::ProjectType.connection_type, null: true
argument :search, GraphQL::STRING_TYPE,
- required: false,
- description: 'Search query.'
+ required: false,
+ description: 'Search query.'
alias_method :user, :object
diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb
index e8048b9deb9..ee1727aadbe 100644
--- a/app/graphql/resolvers/users/snippets_resolver.rb
+++ b/app/graphql/resolvers/users/snippets_resolver.rb
@@ -5,6 +5,7 @@ module Resolvers
module Users
class SnippetsResolver < BaseResolver
include ResolvesSnippets
+ include Gitlab::Allowable
alias_method :user, :object
@@ -14,6 +15,12 @@ module Resolvers
private
+ def resolve_snippets(_args)
+ return Snippet.none unless Ability.allowed?(current_user, :read_user_profile, user)
+
+ super
+ end
+
def snippet_finder_params(args)
super.merge(author: user)
end
diff --git a/app/graphql/types/base_argument.rb b/app/graphql/types/base_argument.rb
index 4ad9e8c0e40..ff9a5a0611d 100644
--- a/app/graphql/types/base_argument.rb
+++ b/app/graphql/types/base_argument.rb
@@ -4,8 +4,10 @@ module Types
class BaseArgument < GraphQL::Schema::Argument
include GitlabStyleDeprecations
+ attr_reader :deprecation
+
def initialize(*args, **kwargs, &block)
- kwargs = gitlab_deprecation(kwargs)
+ @deprecation = gitlab_deprecation(kwargs)
super(*args, **kwargs, &block)
end
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
index 4d470aceca4..518a902a5d7 100644
--- a/app/graphql/types/base_enum.rb
+++ b/app/graphql/types/base_enum.rb
@@ -21,12 +21,23 @@ module Types
graphql_name(enum_mod.name) if use_name
description(enum_mod.description) if use_description
- enum_mod.definition.each { |key, content| value(key.to_s.upcase, **content) }
+ enum_mod.definition.each do |key, content|
+ value(key.to_s.upcase, **content)
+ end
+ end
+
+ # Helper to define an enum member for each element of a Rails AR enum
+ def from_rails_enum(enum, description:)
+ enum.each_key do |name|
+ value name.to_s.upcase,
+ value: name,
+ description: format(description, name: name)
+ end
end
def value(*args, **kwargs, &block)
enum[args[0].downcase] = kwargs[:value] || args[0]
- kwargs = gitlab_deprecation(kwargs)
+ gitlab_deprecation(kwargs)
super(*args, **kwargs, &block)
end
@@ -36,6 +47,18 @@ module Types
def enum
@enum_values ||= {}.with_indifferent_access
end
+
+ def authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize)
+ end
+
+ def authorize(*abilities)
+ @abilities = abilities
+ end
+
+ def authorized?(object, context)
+ authorization.ok?(object, context[:current_user])
+ end
end
end
end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 78ab6890923..7c939f94dde 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -2,28 +2,30 @@
module Types
class BaseField < GraphQL::Schema::Field
- prepend Gitlab::Graphql::Authorize
include GitlabStyleDeprecations
argument_class ::Types::BaseArgument
DEFAULT_COMPLEXITY = 1
+ attr_reader :deprecation
+
def initialize(**kwargs, &block)
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
@constant_complexity = kwargs[:complexity].is_a?(Integer) && kwargs[:complexity] > 0
@requires_argument = !!kwargs.delete(:requires_argument)
+ @authorize = Array.wrap(kwargs.delete(:authorize))
kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity])
@feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs)
- kwargs = gitlab_deprecation(kwargs)
+ @deprecation = gitlab_deprecation(kwargs)
super(**kwargs, &block)
# We want to avoid the overhead of this in prod
extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env?
-
extension ::Gitlab::Graphql::Present::FieldExtension
+ extension ::Gitlab::Graphql::Authorize::ConnectionFilterExtension
end
def may_call_gitaly?
@@ -34,6 +36,19 @@ module Types
@requires_argument || arguments.values.any? { |argument| argument.type.non_null? }
end
+ # By default fields authorize against the current object, but that is not how our
+ # resolvers work - they use declarative permissions to authorize fields
+ # manually (so we make them opt in).
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/300922
+ # (separate out authorize into permissions on the object, and on the
+ # resolved values)
+ # We do not support argument authorization in our schema. If/when we do,
+ # we should call `super` here, to apply argument authorization checks.
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/324647
+ def authorized?(object, args, ctx)
+ field_authorized?(object, ctx) && resolver_authorized?(object, ctx)
+ end
+
def base_complexity
complexity = DEFAULT_COMPLEXITY
complexity += 1 if calls_gitaly?
@@ -58,6 +73,26 @@ module Types
attr_reader :feature_flag
+ def field_authorized?(object, ctx)
+ authorization.ok?(object, ctx[:current_user])
+ end
+
+ # Historically our resolvers have used declarative permission checks only
+ # for _what they resolved_, not the _object they resolved these things from_
+ # We preserve these semantics here, and only apply resolver authorization
+ # if the resolver has opted in.
+ def resolver_authorized?(object, ctx)
+ if @resolver_class && @resolver_class.try(:authorizes_object?)
+ @resolver_class.authorized?(object, ctx)
+ else
+ true
+ end
+ end
+
+ def authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(@authorize)
+ end
+
def feature_documentation_message(key, description)
"#{description} Available only when feature flag `#{key}` is enabled."
end
diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb
index 4b1f3193136..c21c95876be 100644
--- a/app/graphql/types/base_interface.rb
+++ b/app/graphql/types/base_interface.rb
@@ -5,5 +5,11 @@ module Types
include GraphQL::Schema::Interface
field_class ::Types::BaseField
+
+ definition_methods do
+ def authorized?(object, context)
+ resolve_type(object, context).authorized?(object, context)
+ end
+ end
end
end
diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb
index 9c36c83d4a3..cd677e50d28 100644
--- a/app/graphql/types/base_object.rb
+++ b/app/graphql/types/base_object.rb
@@ -19,6 +19,14 @@ module Types
GitlabSchema.id_from_object(object)
end
+ def self.authorization
+ @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(authorize)
+ end
+
+ def self.authorized?(object, context)
+ authorization.ok?(object, context[:current_user])
+ end
+
def current_user
context[:current_user]
end
diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb
index 30a5668c0bb..aeafbf85020 100644
--- a/app/graphql/types/base_union.rb
+++ b/app/graphql/types/base_union.rb
@@ -2,5 +2,8 @@
module Types
class BaseUnion < GraphQL::Schema::Union
+ def self.authorized?(object, context)
+ resolve_type(object, context).authorized?(object, context)
+ end
end
end
diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb
index f33f3f5e537..42d8eecc366 100644
--- a/app/graphql/types/board_type.rb
+++ b/app/graphql/types/board_type.rb
@@ -20,6 +20,12 @@ module Types
field :hide_closed_list, type: GraphQL::BOOLEAN_TYPE, null: true,
description: 'Whether or not closed list is hidden.'
+ field :created_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the board was created.'
+
+ field :updated_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the board was last updated.'
+
field :lists,
Types::BoardListType.connection_type,
null: true,
diff --git a/app/graphql/types/boards/assignee_wildcard_id_enum.rb b/app/graphql/types/boards/assignee_wildcard_id_enum.rb
new file mode 100644
index 00000000000..ba9058a78d9
--- /dev/null
+++ b/app/graphql/types/boards/assignee_wildcard_id_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Boards
+ class AssigneeWildcardIdEnum < BaseEnum
+ graphql_name 'AssigneeWildcardId'
+ description 'Assignee ID wildcard values'
+
+ value 'NONE', 'No assignee is assigned.'
+ value 'ANY', 'An assignee is assigned.'
+ end
+ end
+end
diff --git a/app/graphql/types/boards/board_issuable_input_base_type.rb b/app/graphql/types/boards/board_issuable_input_base_type.rb
new file mode 100644
index 00000000000..2cd057347d6
--- /dev/null
+++ b/app/graphql/types/boards/board_issuable_input_base_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Boards
+ # Common arguments that we can be used to filter boards epics and issues
+ class BoardIssuableInputBaseType < BaseInputObject
+ argument :label_name, [GraphQL::STRING_TYPE, null: true],
+ required: false,
+ description: 'Filter by label name.'
+
+ argument :author_username, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Filter by author username.'
+
+ argument :my_reaction_emoji, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Filter by reaction emoji applied by the current user.'
+ end
+ end
+end
diff --git a/app/graphql/types/boards/board_issue_input_base_type.rb b/app/graphql/types/boards/board_issue_input_base_type.rb
index b762cef6e58..7cf2dcb9c82 100644
--- a/app/graphql/types/boards/board_issue_input_base_type.rb
+++ b/app/graphql/types/boards/board_issue_input_base_type.rb
@@ -2,30 +2,19 @@
module Types
module Boards
- class BoardIssueInputBaseType < BaseInputObject
- argument :label_name, GraphQL::STRING_TYPE.to_list_type,
- required: false,
- description: 'Filter by label name.'
-
+ # rubocop: disable Graphql/AuthorizeTypes
+ class BoardIssueInputBaseType < BoardIssuableInputBaseType
argument :milestone_title, GraphQL::STRING_TYPE,
required: false,
description: 'Filter by milestone title.'
- argument :assignee_username, GraphQL::STRING_TYPE.to_list_type,
+ argument :assignee_username, [GraphQL::STRING_TYPE, null: true],
required: false,
description: 'Filter by assignee username.'
- argument :author_username, GraphQL::STRING_TYPE,
- required: false,
- description: 'Filter by author username.'
-
argument :release_tag, GraphQL::STRING_TYPE,
required: false,
description: 'Filter by release tag.'
-
- argument :my_reaction_emoji, GraphQL::STRING_TYPE,
- required: false,
- description: 'Filter by reaction emoji.'
end
end
end
diff --git a/app/graphql/types/boards/board_issue_input_type.rb b/app/graphql/types/boards/board_issue_input_type.rb
index 9cc0f484a16..8c0e37e5cb7 100644
--- a/app/graphql/types/boards/board_issue_input_type.rb
+++ b/app/graphql/types/boards/board_issue_input_type.rb
@@ -2,19 +2,24 @@
module Types
module Boards
- class NegatedBoardIssueInputType < BoardIssueInputBaseType
- end
-
class BoardIssueInputType < BoardIssueInputBaseType
graphql_name 'BoardIssueInput'
argument :not, NegatedBoardIssueInputType,
required: false,
- description: 'List of negated params. Warning: this argument is experimental and a subject to change in future.'
+ prepare: ->(negated_args, ctx) { negated_args.to_h },
+ description: <<~MD
+ List of negated arguments.
+ Warning: this argument is experimental and a subject to change in future.
+ MD
argument :search, GraphQL::STRING_TYPE,
required: false,
description: 'Search query for issue title or description.'
+
+ argument :assignee_wildcard_id, ::Types::Boards::AssigneeWildcardIdEnum,
+ required: false,
+ description: 'Filter by assignee wildcard. Incompatible with assigneeUsername.'
end
end
end
diff --git a/app/graphql/types/boards/negated_board_issue_input_type.rb b/app/graphql/types/boards/negated_board_issue_input_type.rb
new file mode 100644
index 00000000000..a0fab2ae969
--- /dev/null
+++ b/app/graphql/types/boards/negated_board_issue_input_type.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ module Boards
+ class NegatedBoardIssueInputType < BoardIssueInputBaseType
+ end
+ end
+end
+
+Types::Boards::NegatedBoardIssueInputType.prepend_if_ee('::EE::Types::Boards::NegatedBoardIssueInputType')
diff --git a/app/graphql/types/ci/job_status_enum.rb b/app/graphql/types/ci/job_status_enum.rb
new file mode 100644
index 00000000000..ec80b1f4776
--- /dev/null
+++ b/app/graphql/types/ci/job_status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class JobStatusEnum < BaseEnum
+ graphql_name 'CiJobStatus'
+
+ ::Ci::HasStatus::AVAILABLE_STATUSES.each do |status|
+ value status.upcase,
+ description: "A job that is #{status.tr('_', ' ')}.",
+ value: status
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index c86337eea89..94a256fed3d 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -6,27 +6,74 @@ module Types
graphql_name 'CiJob'
authorize :read_commit_status
+ connection_type_class(Types::CountableConnectionType)
+
+ field :id, ::Types::GlobalIDType[::CommitStatus].as('JobID'), null: true,
+ description: 'ID of the job.'
field :pipeline, Types::Ci::PipelineType, null: true,
description: 'Pipeline the job belongs to.'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the job.'
field :needs, BuildNeedType.connection_type, null: true,
description: 'References to builds that must complete before the jobs run.'
- field :detailed_status, Types::Ci::DetailedStatusType, null: true,
- description: 'Detailed status of the job.'
+ field :status,
+ type: ::Types::Ci::JobStatusEnum,
+ null: true,
+ description: "Status of the job."
+ field :stage, Types::Ci::StageType, null: true,
+ description: 'Stage of the job.'
+ field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Whether this job is allowed to fail.'
+ field :duration, GraphQL::INT_TYPE, null: true,
+ description: 'Duration of the job in seconds.'
+ field :tags, [GraphQL::STRING_TYPE], null: true,
+ description: 'Tags for the current job.'
+
+ # Life-cycle timestamps:
+ field :created_at, Types::TimeType, null: false,
+ description: "When the job was created."
+ field :queued_at, Types::TimeType, null: true,
+ description: 'When the job was enqueued and marked as pending.'
+ field :started_at, Types::TimeType, null: true,
+ description: 'When the job was started.'
+ field :finished_at, Types::TimeType, null: true,
+ description: 'When a job has finished running.'
field :scheduled_at, Types::TimeType, null: true,
description: 'Schedule for the build.'
+
+ field :detailed_status, Types::Ci::DetailedStatusType, null: true,
+ description: 'Detailed status of the job.'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
description: 'Artifacts generated by the job.'
- field :finished_at, Types::TimeType, null: true,
- description: 'When a job has finished running.'
- field :duration, GraphQL::INT_TYPE, null: true,
- description: 'Duration of the job in seconds.'
+ field :short_sha, type: GraphQL::STRING_TYPE, null: false,
+ description: 'Short SHA1 ID of the commit.'
+ field :scheduling_type, GraphQL::STRING_TYPE, null: true,
+ description: 'Type of pipeline scheduling. Value is `dag` if the pipeline uses the `needs` keyword, and `stage` otherwise.'
+ field :commit_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Path to the commit that triggered the job.'
+ field :ref_name, GraphQL::STRING_TYPE, null: true,
+ description: 'Ref name of the job.'
+ field :ref_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Path to the ref.'
+ field :playable, GraphQL::BOOLEAN_TYPE, null: false, method: :playable?,
+ description: 'Indicates the job can be played.'
+ field :retryable, GraphQL::BOOLEAN_TYPE, null: false, method: :retryable?,
+ description: 'Indicates the job can be retried.'
+ field :cancelable, GraphQL::BOOLEAN_TYPE, null: false, method: :cancelable?,
+ description: 'Indicates the job can be canceled.'
+ field :active, GraphQL::BOOLEAN_TYPE, null: false, method: :active?,
+ description: 'Indicates the job is active.'
+ field :coverage, GraphQL::FLOAT_TYPE, null: true,
+ description: 'Coverage level of the job.'
def pipeline
Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, object.pipeline_id).find
end
+ def tags
+ object.tags.map(&:name) if object.is_a?(::Ci::Build)
+ end
+
def detailed_status
object.detailed_status(context[:current_user])
end
@@ -36,6 +83,46 @@ module Types
object.job_artifacts
end
end
+
+ def stage
+ ::Gitlab::Graphql::Lazy.with_value(pipeline) do |pl|
+ BatchLoader::GraphQL.for([pl, object.stage]).batch do |ids, loader|
+ by_pipeline = ids
+ .group_by(&:first)
+ .transform_values { |grp| grp.map(&:second) }
+
+ by_pipeline.each do |p, names|
+ p.stages.by_name(names).each { |s| loader.call([p, s.name], s) }
+ end
+ end
+ end
+ end
+
+ # This class is a secret union!
+ # TODO: turn this into an actual union, so that fields can be referenced safely!
+ def id
+ return unless object.id.present?
+
+ model_name = object.type || ::CommitStatus.name
+ id = object.id
+ Gitlab::GlobalId.build(model_name: model_name, id: id)
+ end
+
+ def commit_path
+ ::Gitlab::Routing.url_helpers.project_commit_path(object.project, object.sha)
+ end
+
+ def ref_name
+ object&.ref
+ end
+
+ def ref_path
+ ::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_name)
+ end
+
+ def coverage
+ object&.coverage
+ end
end
end
end
diff --git a/app/graphql/types/ci/pipeline_config_source_enum.rb b/app/graphql/types/ci/pipeline_config_source_enum.rb
index e1575cb2f99..96c8a5f2941 100644
--- a/app/graphql/types/ci/pipeline_config_source_enum.rb
+++ b/app/graphql/types/ci/pipeline_config_source_enum.rb
@@ -4,7 +4,8 @@ module Types
module Ci
class PipelineConfigSourceEnum < BaseEnum
::Enums::Ci::Pipeline.config_sources.keys.each do |state_symbol|
- value state_symbol.to_s.upcase, value: state_symbol.to_s
+ description = state_symbol == :auto_devops_source ? "Auto DevOps source." : "#{state_symbol.to_s.titleize.capitalize}." # This is needed to avoid failure in doc lint
+ value state_symbol.to_s.upcase, value: state_symbol.to_s, description: description
end
end
end
diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb
index c19ddf5bb25..e0b2020dcc1 100644
--- a/app/graphql/types/ci/pipeline_status_enum.rb
+++ b/app/graphql/types/ci/pipeline_status_enum.rb
@@ -4,7 +4,9 @@ module Types
module Ci
class PipelineStatusEnum < BaseEnum
::Ci::Pipeline.all_state_names.each do |state_symbol|
- value state_symbol.to_s.upcase, value: state_symbol.to_s
+ value state_symbol.to_s.upcase,
+ description: ::Ci::Pipeline::STATUSES_DESCRIPTION[state_symbol],
+ value: state_symbol.to_s
end
end
end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 49be200a788..2e83f6c1f5a 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -81,6 +81,20 @@ module Types
description: 'Jobs belonging to the pipeline.',
resolver: ::Resolvers::Ci::JobsResolver
+ field :job,
+ type: ::Types::Ci::JobType,
+ null: true,
+ description: 'A specific job in this pipeline, either by name or ID.' do
+ argument :id,
+ type: ::Types::GlobalIDType[::CommitStatus],
+ required: false,
+ description: 'ID of the job.'
+ argument :name,
+ type: ::GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Name of the job.'
+ end
+
field :source_job, Types::Ci::JobType, null: true,
description: 'Job where pipeline was triggered from.'
@@ -104,8 +118,24 @@ module Types
field :active, GraphQL::BOOLEAN_TYPE, null: false, method: :active?,
description: 'Indicates if the pipeline is active.'
+ field :uses_needs, GraphQL::BOOLEAN_TYPE, null: true,
+ method: :uses_needs?,
+ description: 'Indicates if the pipeline has jobs with `needs` dependencies.'
+
+ field :test_report_summary,
+ Types::Ci::TestReportSummaryType,
+ null: false,
+ description: 'Summary of the test report generated by the pipeline.',
+ resolver: Resolvers::Ci::TestReportSummaryResolver
+
+ field :test_suite,
+ Types::Ci::TestSuiteType,
+ null: true,
+ description: 'A specific test suite in a pipeline test report.',
+ resolver: Resolvers::Ci::TestSuiteResolver
+
def detailed_status
- object.detailed_status(context[:current_user])
+ object.detailed_status(current_user)
end
def user
@@ -119,6 +149,19 @@ module Types
def path
::Gitlab::Routing.url_helpers.project_pipeline_path(object.project, object)
end
+
+ def job(id: nil, name: nil)
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'One of id or name is required' unless id || name
+
+ if id
+ id = ::Types::GlobalIDType[::CommitStatus].coerce_isolated_input(id) if id
+ pipeline.statuses.id_in(id.model_id)
+ else
+ pipeline.statuses.by_name(name)
+ end.take # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ alias_method :pipeline, :object
end
end
end
diff --git a/app/graphql/types/ci/recent_failures_type.rb b/app/graphql/types/ci/recent_failures_type.rb
new file mode 100644
index 00000000000..eeff7222762
--- /dev/null
+++ b/app/graphql/types/ci/recent_failures_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class RecentFailuresType < BaseObject
+ graphql_name 'RecentFailures'
+ description 'Recent failure history of a test case.'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :count, GraphQL::INT_TYPE, null: true,
+ description: 'Number of times the test case has failed in the past 14 days.'
+
+ field :base_branch, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the base branch of the project.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb
index 836f2430890..56b4f248697 100644
--- a/app/graphql/types/ci/stage_type.rb
+++ b/app/graphql/types/ci/stage_type.rb
@@ -12,10 +12,13 @@ module Types
extras: [:lookahead],
description: 'Group of jobs for the stage.'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
- description: 'Detailed status of the stage.'
+ description: 'Detailed status of the stage.'
+ field :jobs, Ci::JobType.connection_type, null: true,
+ description: 'Jobs for the stage.',
+ method: 'latest_statuses'
def detailed_status
- object.detailed_status(context[:current_user])
+ object.detailed_status(current_user)
end
# Issues one query per pipeline
@@ -33,6 +36,34 @@ module Types
jobs_for_pipeline(pl, indexed.keys, include_needs).each do |stage_id, statuses|
key = indexed[stage_id]
groups = ::Ci::Group.fabricate(project, key.stage, statuses)
+
+ if Feature.enabled?(:ci_no_empty_groups, project)
+ groups.each do |group|
+ rejected = group.jobs.reject { |job| Ability.allowed?(current_user, :read_commit_status, job) }
+ group.jobs.select! { |job| Ability.allowed?(current_user, :read_commit_status, job) }
+ next unless group.jobs.empty?
+
+ exc = StandardError.new('Empty Ci::Group')
+ traces = rejected.map do |job|
+ trace = []
+ policy = Ability.policy_for(current_user, job)
+ policy.debug(:read_commit_status, trace)
+ trace
+ end
+ extra = {
+ current_user_id: current_user&.id,
+ project_id: project.id,
+ pipeline_id: pl.id,
+ stage_id: stage_id,
+ group_name: group.name,
+ rejected_job_ids: rejected.map(&:id),
+ rejected_traces: traces
+ }
+ Gitlab::ErrorTracking.track_exception(exc, extra)
+ end
+ groups.reject! { |group| group.jobs.empty? }
+ end
+
loader.call(key, groups)
end
end
diff --git a/app/graphql/types/ci/test_case_status_enum.rb b/app/graphql/types/ci/test_case_status_enum.rb
new file mode 100644
index 00000000000..6a5f8bc2a59
--- /dev/null
+++ b/app/graphql/types/ci/test_case_status_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class TestCaseStatusEnum < BaseEnum
+ graphql_name 'TestCaseStatus'
+
+ ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status|
+ value status,
+ description: "Test case that has a status of #{status}.",
+ value: status
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/test_case_type.rb b/app/graphql/types/ci/test_case_type.rb
new file mode 100644
index 00000000000..9cc3f918125
--- /dev/null
+++ b/app/graphql/types/ci/test_case_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TestCaseType < BaseObject
+ graphql_name 'TestCase'
+ description 'Test case in pipeline test report.'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :status, Types::Ci::TestCaseStatusEnum, null: true,
+ description: "Status of the test case (#{::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.join(', ')})."
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the test case.'
+
+ field :classname, GraphQL::STRING_TYPE, null: true,
+ description: 'Classname of the test case.'
+
+ field :execution_time, GraphQL::FLOAT_TYPE, null: true,
+ description: 'Test case execution time in seconds.'
+
+ field :file, GraphQL::STRING_TYPE, null: true,
+ description: 'Path to the file of the test case.'
+
+ field :attachment_url, GraphQL::STRING_TYPE, null: true,
+ description: 'URL of the test case attachment file.'
+
+ field :system_output, GraphQL::STRING_TYPE, null: true,
+ description: 'System output of the test case.'
+
+ field :stack_trace, GraphQL::STRING_TYPE, null: true,
+ description: 'Stack trace of the test case.'
+
+ field :recent_failures, Types::Ci::RecentFailuresType, null: true,
+ description: 'Recent failure history of the test case on the base branch.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/test_report_summary_type.rb b/app/graphql/types/ci/test_report_summary_type.rb
new file mode 100644
index 00000000000..87207c8a765
--- /dev/null
+++ b/app/graphql/types/ci/test_report_summary_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `PipelineType` that has its own authorization
+ class TestReportSummaryType < BaseObject
+ graphql_name 'TestReportSummary'
+ description 'Test report for a pipeline'
+
+ field :total, Types::Ci::TestReportTotalType, null: false,
+ description: 'Total report statistics for a pipeline test report.'
+
+ field :test_suites, Types::Ci::TestSuiteSummaryType.connection_type, null: false,
+ description: 'Test suites belonging to a pipeline test report.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/test_report_total_type.rb b/app/graphql/types/ci/test_report_total_type.rb
new file mode 100644
index 00000000000..1123734adc3
--- /dev/null
+++ b/app/graphql/types/ci/test_report_total_type.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TestReportTotalType < BaseObject
+ graphql_name 'TestReportTotal'
+ description 'Total test report statistics.'
+
+ field :time, GraphQL::FLOAT_TYPE, null: true,
+ description: 'Total duration of the tests.'
+
+ field :count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of the test cases.'
+
+ field :success, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that succeeded.'
+
+ field :failed, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that failed.'
+
+ field :skipped, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that were skipped.'
+
+ field :error, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that had an error.'
+
+ field :suite_error, GraphQL::STRING_TYPE, null: true,
+ description: 'Test suite error message.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/test_suite_summary_type.rb b/app/graphql/types/ci/test_suite_summary_type.rb
new file mode 100644
index 00000000000..a80a9179cb4
--- /dev/null
+++ b/app/graphql/types/ci/test_suite_summary_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TestSuiteSummaryType < BaseObject
+ graphql_name 'TestSuiteSummary'
+ description 'Test suite summary in a pipeline test report.'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the test suite.'
+
+ field :total_time, GraphQL::FLOAT_TYPE, null: true,
+ description: 'Total duration of the tests in the test suite.'
+
+ field :total_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of the test cases in the test suite.'
+
+ field :success_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that succeeded in the test suite.'
+
+ field :failed_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that failed in the test suite.'
+
+ field :skipped_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that were skipped in the test suite.'
+
+ field :error_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that had an error.'
+
+ field :suite_error, GraphQL::STRING_TYPE, null: true,
+ description: 'Test suite error message.'
+
+ field :build_ids, [GraphQL::ID_TYPE], null: true,
+ description: 'IDs of the builds used to run the test suite.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/ci/test_suite_type.rb b/app/graphql/types/ci/test_suite_type.rb
new file mode 100644
index 00000000000..7d4c01da81b
--- /dev/null
+++ b/app/graphql/types/ci/test_suite_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class TestSuiteType < BaseObject
+ graphql_name 'TestSuite'
+ description 'Test suite in a pipeline test report.'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the test suite.'
+
+ field :total_time, GraphQL::FLOAT_TYPE, null: true,
+ description: 'Total duration of the tests in the test suite.'
+
+ field :total_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of the test cases in the test suite.'
+
+ field :success_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that succeeded in the test suite.'
+
+ field :failed_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that failed in the test suite.'
+
+ field :skipped_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that were skipped in the test suite.'
+
+ field :error_count, GraphQL::INT_TYPE, null: true,
+ description: 'Total number of test cases that had an error.'
+
+ field :suite_error, GraphQL::STRING_TYPE, null: true,
+ description: 'Test suite error message.'
+
+ field :test_cases, Types::Ci::TestCaseType.connection_type, null: true,
+ description: 'Test cases in the test suite.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/concerns/find_closest.rb b/app/graphql/types/concerns/find_closest.rb
new file mode 100644
index 00000000000..1d76e872364
--- /dev/null
+++ b/app/graphql/types/concerns/find_closest.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module FindClosest
+ # Find the closest node of a given type above this node, and return the domain object
+ def closest_parent(type, parent)
+ parent = parent.try(:parent) while parent && parent.object.class != type
+ return unless parent
+
+ parent.object.object
+ end
+end
diff --git a/app/graphql/types/concerns/gitlab_style_deprecations.rb b/app/graphql/types/concerns/gitlab_style_deprecations.rb
index ad195354930..802562ed958 100644
--- a/app/graphql/types/concerns/gitlab_style_deprecations.rb
+++ b/app/graphql/types/concerns/gitlab_style_deprecations.rb
@@ -7,25 +7,21 @@ module GitlabStyleDeprecations
private
+ # Mutate the arguments, returns the deprecation
def gitlab_deprecation(kwargs)
if kwargs[:deprecation_reason].present?
raise ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \
'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-arguments-and-enum-values'
end
- deprecation = kwargs.delete(:deprecated)
- return kwargs unless deprecation
+ deprecation = ::Gitlab::Graphql::Deprecation.parse(kwargs.delete(:deprecated))
+ return unless deprecation
- milestone, reason = deprecation.values_at(:milestone, :reason).map(&:presence)
+ raise ArgumentError, "Bad deprecation. #{deprecation.errors.full_messages.to_sentence}" unless deprecation.valid?
- raise ArgumentError, 'Please provide a `milestone` within `deprecated`' unless milestone
- raise ArgumentError, 'Please provide a `reason` within `deprecated`' unless reason
- raise ArgumentError, '`milestone` must be a `String`' unless milestone.is_a?(String)
+ kwargs[:deprecation_reason] = deprecation.deprecation_reason
+ kwargs[:description] = deprecation.edit_description(kwargs[:description])
- deprecated_in = "Deprecated in #{milestone}"
- kwargs[:deprecation_reason] = "#{reason}. #{deprecated_in}."
- kwargs[:description] += " #{deprecated_in}: #{reason}." if kwargs[:description]
-
- kwargs
+ deprecation
end
end
diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb
index 750bd1bfe8d..79061df7282 100644
--- a/app/graphql/types/global_id_type.rb
+++ b/app/graphql/types/global_id_type.rb
@@ -23,7 +23,7 @@ module Types
A global identifier.
A global identifier represents an object uniquely across the application.
- An example of such an identifier is "gid://gitlab/User/1".
+ An example of such an identifier is `"gid://gitlab/User/1"`.
Global identifiers are encoded as strings.
DESC
@@ -67,6 +67,17 @@ module Types
graphql_name
end
+ define_singleton_method(:as) do |new_name|
+ if @renamed && graphql_name != new_name
+ raise "Conflicting names for ID of #{model_class.name}: " \
+ "#{graphql_name} and #{new_name}"
+ end
+
+ @renamed = true
+ graphql_name(new_name)
+ self
+ end
+
define_singleton_method(:coerce_result) do |gid, ctx|
global_id = ::Gitlab::GlobalId.as_global_id(gid, model_name: model_class.name)
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 7a84e76657b..a44281b2bdf 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -8,39 +8,65 @@ module Types
expose_permissions Types::PermissionTypes::Group
- field :web_url, GraphQL::STRING_TYPE, null: false,
+ field :web_url,
+ type: GraphQL::STRING_TYPE,
+ null: false,
description: 'Web URL of the group.'
- field :avatar_url, GraphQL::STRING_TYPE, null: true,
+ field :avatar_url,
+ type: GraphQL::STRING_TYPE,
+ null: true,
description: 'Avatar URL of the group.'
- field :custom_emoji, Types::CustomEmojiType.connection_type, null: true,
+ field :custom_emoji,
+ type: 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,
+ field :share_with_group_lock,
+ type: GraphQL::BOOLEAN_TYPE,
+ null: true,
description: 'Indicates if sharing a project with another group within this group is prevented.'
- field :project_creation_level, GraphQL::STRING_TYPE, null: true, method: :project_creation_level_str,
+ field :project_creation_level,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ method: :project_creation_level_str,
description: 'The permission level required to create projects in the group.'
- field :subgroup_creation_level, GraphQL::STRING_TYPE, null: true, method: :subgroup_creation_level_str,
+ field :subgroup_creation_level,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ method: :subgroup_creation_level_str,
description: 'The permission level required to create subgroups within the group.'
- field :require_two_factor_authentication, GraphQL::BOOLEAN_TYPE, null: true,
+ field :require_two_factor_authentication,
+ type: GraphQL::BOOLEAN_TYPE,
+ null: true,
description: 'Indicates if all users in this group are required to set up two-factor authentication.'
- field :two_factor_grace_period, GraphQL::INT_TYPE, null: true,
+ field :two_factor_grace_period,
+ type: GraphQL::INT_TYPE,
+ null: true,
description: 'Time before two-factor authentication is enforced.'
- field :auto_devops_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :auto_devops_enabled,
+ type: GraphQL::BOOLEAN_TYPE,
+ null: true,
description: 'Indicates whether Auto DevOps is enabled for all projects within this group.'
- field :emails_disabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :emails_disabled,
+ type: GraphQL::BOOLEAN_TYPE,
+ null: true,
description: 'Indicates if a group has email notifications disabled.'
- field :mentions_disabled, GraphQL::BOOLEAN_TYPE, null: true,
+ field :mentions_disabled,
+ type: GraphQL::BOOLEAN_TYPE,
+ null: true,
description: 'Indicates if a group is disabled from getting mentioned.'
- field :parent, GroupType, null: true,
+ field :parent,
+ type: GroupType,
+ null: true,
description: 'Parent group.'
field :issues,
@@ -55,7 +81,7 @@ module Types
description: 'Merge requests for projects in this group.',
resolver: Resolvers::GroupMergeRequestsResolver
- field :milestones, Types::MilestoneType.connection_type, null: true,
+ field :milestones,
description: 'Milestones of the group.',
resolver: Resolvers::GroupMilestonesResolver
@@ -76,9 +102,10 @@ module Types
Types::LabelType,
null: true,
description: 'A label available on this group.' do
- argument :title, GraphQL::STRING_TYPE,
- required: true,
- description: 'Title of the label.'
+ argument :title,
+ type: GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Title of the label.'
end
field :group_members,
@@ -92,7 +119,9 @@ module Types
resolver: Resolvers::ContainerRepositoriesResolver,
authorize: :read_container_image
- field :container_repositories_count, GraphQL::INT_TYPE, null: false,
+ field :container_repositories_count,
+ type: GraphQL::INT_TYPE,
+ null: false,
description: 'Number of container repositories in the group.'
field :packages,
@@ -114,6 +143,12 @@ module Types
description: 'Labels available on this group.',
resolver: Resolvers::GroupLabelsResolver
+ field :timelogs, ::Types::TimelogType.connection_type, null: false,
+ description: 'Time logged on issues in the group and its subgroups.',
+ extras: [:lookahead],
+ complexity: 5,
+ resolver: ::Resolvers::TimelogResolver
+
def avatar_url
object.avatar_url(only_path: false)
end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index f15ab69f2d4..34c824fe9fb 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -124,6 +124,9 @@ module Types
field :create_note_email, GraphQL::STRING_TYPE, null: true,
description: 'User specific email address for the issue.'
+ field :timelogs, Types::TimelogType.connection_type, null: false,
+ description: 'Timelogs on the issue.'
+
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb
new file mode 100644
index 00000000000..10bf6f21792
--- /dev/null
+++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Types
+ module Issues
+ class NegatedIssueFilterInputType < BaseInputObject
+ graphql_name 'NegatedIssueFilterInput'
+
+ argument :iids, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'List of IIDs of issues to exclude. For example, [1, 2].'
+ argument :label_name, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'Labels not applied to this issue.'
+ argument :milestone_title, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'Milestone not applied to this issue.'
+ argument :assignee_usernames, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'Usernames of users not assigned to the issue.'
+ argument :assignee_id, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'ID of a user not assigned to the issues.'
+ end
+ end
+end
+
+Types::Issues::NegatedIssueFilterInputType.prepend_if_ee('::EE::Types::Issues::NegatedIssueFilterInputType')
diff --git a/app/graphql/types/jira_users_mapping_input_type.rb b/app/graphql/types/jira_users_mapping_input_type.rb
index 61e3240ecf3..32640b9cb17 100644
--- a/app/graphql/types/jira_users_mapping_input_type.rb
+++ b/app/graphql/types/jira_users_mapping_input_type.rb
@@ -5,12 +5,12 @@ module Types
graphql_name 'JiraUsersMappingInputType'
argument :jira_account_id,
- GraphQL::STRING_TYPE,
- required: true,
- description: 'Jira account ID of the user.'
+ GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Jira account ID of the user.'
argument :gitlab_id,
- GraphQL::INT_TYPE,
- required: false,
- description: 'Id of the GitLab user.'
+ GraphQL::INT_TYPE,
+ required: false,
+ description: 'ID of the GitLab user.'
end
end
diff --git a/app/graphql/types/merge_request_review_state_enum.rb b/app/graphql/types/merge_request_review_state_enum.rb
new file mode 100644
index 00000000000..45f97758425
--- /dev/null
+++ b/app/graphql/types/merge_request_review_state_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ class MergeRequestReviewStateEnum < BaseEnum
+ graphql_name 'MergeRequestReviewState'
+ description 'State of a review of a GitLab merge request.'
+
+ from_rails_enum(::MergeRequestReviewer.states,
+ description: "The merge request is %{name}.")
+ end
+end
diff --git a/app/graphql/types/merge_request_state_enum.rb b/app/graphql/types/merge_request_state_enum.rb
index a2d7bd0306c..bcf18b836de 100644
--- a/app/graphql/types/merge_request_state_enum.rb
+++ b/app/graphql/types/merge_request_state_enum.rb
@@ -5,6 +5,6 @@ module Types
graphql_name 'MergeRequestState'
description 'State of a GitLab merge request'
- value 'merged', description: "Merge Request has been merged."
+ value 'merged', description: "Merge request has been merged."
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 449286915f2..c8ccf9d8aff 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -132,7 +132,10 @@ module Types
description: 'The milestone of the merge request.'
field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Assignees of the merge request.'
- field :reviewers, Types::UserType.connection_type, null: true, complexity: 5,
+ field :reviewers,
+ type: Types::MergeRequests::ReviewerType.connection_type,
+ null: true,
+ complexity: 5,
description: 'Users from whom a review has been requested.'
field :author, Types::UserType, null: true,
description: 'User who created this merge request.'
@@ -183,6 +186,8 @@ module Types
description: 'Selected auto merge strategy.'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request.'
+ field :timelogs, Types::TimelogType.connection_type, null: false,
+ description: 'Timelogs on the merge request.'
def approved_by
object.approved_by_users
diff --git a/app/graphql/types/merge_requests/reviewer_type.rb b/app/graphql/types/merge_requests/reviewer_type.rb
new file mode 100644
index 00000000000..09ced39844a
--- /dev/null
+++ b/app/graphql/types/merge_requests/reviewer_type.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Types
+ module MergeRequests
+ class ReviewerType < ::Types::UserType
+ include FindClosest
+
+ graphql_name 'MergeRequestReviewer'
+ description 'A user from whom a merge request review has been requested.'
+ authorize :read_user
+
+ field :merge_request_interaction,
+ type: ::Types::UserMergeRequestInteractionType,
+ null: true,
+ extras: [:parent],
+ description: "Details of this user's interactions with the merge request."
+
+ def merge_request_interaction(parent:)
+ merge_request = closest_parent(::Types::MergeRequestType, parent)
+ return unless merge_request
+
+ Users::MergeRequestInteraction.new(user: object, merge_request: merge_request)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb
index c3816116e2b..91a5109c748 100644
--- a/app/graphql/types/milestone_type.rb
+++ b/app/graphql/types/milestone_type.rb
@@ -14,6 +14,9 @@ module Types
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the milestone.'
+ field :iid, GraphQL::ID_TYPE, null: false,
+ description: "Internal ID of the milestone."
+
field :title, GraphQL::STRING_TYPE, null: false,
description: 'Title of the milestone.'
diff --git a/app/graphql/types/mutation_operation_mode_enum.rb b/app/graphql/types/mutation_operation_mode_enum.rb
index 75c1d7cd4a6..08214eebc7e 100644
--- a/app/graphql/types/mutation_operation_mode_enum.rb
+++ b/app/graphql/types/mutation_operation_mode_enum.rb
@@ -10,5 +10,13 @@ module Types
value 'REPLACE', 'Performs a replace operation.'
value 'APPEND', 'Performs an append operation.'
value 'REMOVE', 'Performs a removal operation.'
+
+ def self.default_mode
+ enum[:replace]
+ end
+
+ def self.transform_modes
+ enum.values_at(:remove, :append)
+ end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 76ffddf416f..5a9c7b32deb 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -68,6 +68,7 @@ module Types
mount_mutation Mutations::Releases::Delete
mount_mutation Mutations::ReleaseAssetLinks::Create
mount_mutation Mutations::ReleaseAssetLinks::Update
+ mount_mutation Mutations::ReleaseAssetLinks::Delete
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
diff --git a/app/graphql/types/packages/conan/file_metadatum_type.rb b/app/graphql/types/packages/conan/file_metadatum_type.rb
new file mode 100644
index 00000000000..97d5abe6ba4
--- /dev/null
+++ b/app/graphql/types/packages/conan/file_metadatum_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Conan
+ class FileMetadatumType < BaseObject
+ graphql_name 'ConanFileMetadata'
+ description 'Conan file metadata'
+
+ implements Types::Packages::FileMetadataType
+
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::Conan::FileMetadatum], null: false, description: 'ID of the metadatum.'
+ field :recipe_revision, GraphQL::STRING_TYPE, null: false, description: 'Revision of the Conan recipe.'
+ field :package_revision, GraphQL::STRING_TYPE, null: true, description: 'Revision of the package.'
+ field :conan_package_reference, GraphQL::STRING_TYPE, null: true, description: 'Reference of the Conan package.'
+ field :conan_file_type, ::Types::Packages::Conan::MetadatumFileTypeEnum, null: false, description: 'Type of the Conan file.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/conan/metadatum_file_type_enum.rb b/app/graphql/types/packages/conan/metadatum_file_type_enum.rb
new file mode 100644
index 00000000000..d8ec3a44d4d
--- /dev/null
+++ b/app/graphql/types/packages/conan/metadatum_file_type_enum.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Conan
+ class MetadatumFileTypeEnum < BaseEnum
+ graphql_name 'ConanMetadatumFileTypeEnum'
+ description 'Conan file types'
+
+ ::Packages::Conan::FileMetadatum.conan_file_types.keys.each do |file|
+ value file.upcase, value: file, description: "A #{file.humanize(capitalize: false)} type."
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/conan/metadatum_type.rb b/app/graphql/types/packages/conan/metadatum_type.rb
new file mode 100644
index 00000000000..00b84235d27
--- /dev/null
+++ b/app/graphql/types/packages/conan/metadatum_type.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module Conan
+ class MetadatumType < BaseObject
+ graphql_name 'ConanMetadata'
+ description 'Conan metadata'
+
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::Conan::Metadatum], null: false, description: 'ID of the metadatum.'
+ field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
+ field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
+ field :package_username, GraphQL::STRING_TYPE, null: false, description: 'Username of the Conan package.'
+ field :package_channel, GraphQL::STRING_TYPE, null: false, description: 'Channel of the Conan package.'
+ field :recipe, GraphQL::STRING_TYPE, null: false, description: 'Recipe of the Conan package.'
+ field :recipe_path, GraphQL::STRING_TYPE, null: false, description: 'Recipe path of the Conan package.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/file_metadata_type.rb b/app/graphql/types/packages/file_metadata_type.rb
new file mode 100644
index 00000000000..46ccb424218
--- /dev/null
+++ b/app/graphql/types/packages/file_metadata_type.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ module FileMetadataType
+ include ::Types::BaseInterface
+ graphql_name 'PackageFileMetadata'
+ description 'Represents metadata associated with a Package file'
+
+ field :created_at, ::Types::TimeType, null: false, description: 'Date of creation.'
+ field :updated_at, ::Types::TimeType, null: false, description: 'Date of most recent update.'
+
+ def self.resolve_type(object, context)
+ case object
+ when ::Packages::Conan::FileMetadatum
+ ::Types::Packages::Conan::FileMetadatumType
+ else
+ # NOTE: This method must be kept in sync with `PackageFileType#file_metadata`,
+ # which must never produce data that this discriminator cannot handle.
+ raise 'Unsupported file metadata type'
+ end
+ end
+
+ orphan_types Types::Packages::Conan::FileMetadatumType
+ end
+ end
+end
diff --git a/app/graphql/types/packages/metadata_type.rb b/app/graphql/types/packages/metadata_type.rb
index 26c43b51a69..4ab6707df88 100644
--- a/app/graphql/types/packages/metadata_type.rb
+++ b/app/graphql/types/packages/metadata_type.rb
@@ -6,12 +6,14 @@ module Types
graphql_name 'PackageMetadata'
description 'Represents metadata associated with a Package'
- possible_types ::Types::Packages::Composer::MetadatumType
+ possible_types ::Types::Packages::Composer::MetadatumType, ::Types::Packages::Conan::MetadatumType
def self.resolve_type(object, context)
case object
when ::Packages::Composer::Metadatum
::Types::Packages::Composer::MetadatumType
+ when ::Packages::Conan::Metadatum
+ ::Types::Packages::Conan::MetadatumType
else
# NOTE: This method must be kept in sync with `PackageWithoutVersionsType#metadata`,
# which must never produce data that this discriminator cannot handle.
diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb
new file mode 100644
index 00000000000..510b7e2ba41
--- /dev/null
+++ b/app/graphql/types/packages/package_details_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageDetailsType < PackageType
+ graphql_name 'PackageDetailsType'
+ description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes'
+ authorize :read_package
+
+ field :versions, ::Types::Packages::PackageType.connection_type, null: true,
+ description: 'The other versions of the package.'
+
+ field :package_files, Types::Packages::PackageFileType.connection_type, null: true, description: 'Package files.'
+
+ def versions
+ object.versions
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_file_type.rb b/app/graphql/types/packages/package_file_type.rb
new file mode 100644
index 00000000000..e9e38559626
--- /dev/null
+++ b/app/graphql/types/packages/package_file_type.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Types
+ module Packages
+ class PackageFileType < BaseObject
+ graphql_name 'PackageFile'
+ description 'Represents a package file'
+ authorize :read_package
+
+ field :id, ::Types::GlobalIDType[::Packages::PackageFile], null: false, description: 'ID of the file.'
+ field :created_at, Types::TimeType, null: false, description: 'The created date.'
+ field :updated_at, Types::TimeType, null: false, description: 'The updated date.'
+ field :size, GraphQL::STRING_TYPE, null: false, description: 'Size of the package file.'
+ field :file_name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package file.'
+ field :download_path, GraphQL::STRING_TYPE, null: false, description: 'Download path of the package file.'
+ field :file_md5, GraphQL::STRING_TYPE, null: true, description: 'Md5 of the package file.'
+ field :file_sha1, GraphQL::STRING_TYPE, null: true, description: 'Sha1 of the package file.'
+ field :file_sha256, GraphQL::STRING_TYPE, null: true, description: 'Sha256 of the package file.'
+ field :file_metadata, Types::Packages::FileMetadataType, null: true,
+ description: 'File metadata.'
+
+ # NOTE: This method must be kept in sync with the union
+ # type: `Types::Packages::FileMetadataType`.
+ #
+ # `Types::Packages::FileMetadataType.resolve_type(metadata, ctx)` must never raise.
+ def file_metadata
+ case object.package.package_type
+ when 'conan'
+ object.conan_file_metadatum
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb
index 331898a1e84..a263ca1577a 100644
--- a/app/graphql/types/packages/package_type.rb
+++ b/app/graphql/types/packages/package_type.rb
@@ -2,13 +2,52 @@
module Types
module Packages
- class PackageType < PackageWithoutVersionsType
+ class PackageType < ::Types::BaseObject
graphql_name 'Package'
- description 'Represents a package in the Package Registry'
+ description 'Represents a package in the Package Registry. Note that this type is in beta and susceptible to changes'
+
authorize :read_package
- field :versions, ::Types::Packages::PackageWithoutVersionsType.connection_type, null: true,
- description: 'The other versions of the package.'
+ field :id, ::Types::GlobalIDType[::Packages::Package], null: false,
+ description: 'ID of the package.'
+
+ field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package.'
+ field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
+ field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
+ field :version, GraphQL::STRING_TYPE, null: true, description: 'Version string.'
+ field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
+ field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'
+ field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
+ field :pipelines, Types::Ci::PipelineType.connection_type, null: true,
+ description: 'Pipelines that built the package.'
+ field :metadata, Types::Packages::MetadataType, null: true,
+ description: 'Package metadata.'
+ field :versions, ::Types::Packages::PackageType.connection_type, null: true,
+ description: 'The other versions of the package.',
+ deprecated: { reason: 'This field is now only returned in the PackageDetailsType', milestone: '13.11' }
+
+ def project
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
+ end
+
+ def versions
+ []
+ end
+
+ # NOTE: This method must be kept in sync with the union
+ # type: `Types::Packages::MetadataType`.
+ #
+ # `Types::Packages::MetadataType.resolve_type(metadata, ctx)` must never raise.
+ def metadata
+ case object.package_type
+ when 'composer'
+ object.composer_metadatum
+ when 'conan'
+ object.conan_metadatum
+ else
+ nil
+ end
+ end
end
end
end
diff --git a/app/graphql/types/packages/package_without_versions_type.rb b/app/graphql/types/packages/package_without_versions_type.rb
deleted file mode 100644
index 9c6bb37e6cc..00000000000
--- a/app/graphql/types/packages/package_without_versions_type.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- module Packages
- class PackageWithoutVersionsType < ::Types::BaseObject
- graphql_name 'PackageWithoutVersions'
- description 'Represents a version of a package in the Package Registry'
-
- authorize :read_package
-
- field :id, ::Types::GlobalIDType[::Packages::Package], null: false,
- description: 'ID of the package.'
-
- field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the package.'
- field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
- field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
- field :version, GraphQL::STRING_TYPE, null: true, description: 'Version string.'
- field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
- field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'
- field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
- field :pipelines, Types::Ci::PipelineType.connection_type, null: true,
- description: 'Pipelines that built the package.'
- field :metadata, Types::Packages::MetadataType, null: true,
- description: 'Package metadata.'
-
- def project
- Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
- end
-
- # NOTE: This method must be kept in sync with the union
- # type: `Types::Packages::MetadataType`.
- #
- # `Types::Packages::MetadataType.resolve_type(metadata, ctx)` must never raise.
- def metadata
- case object.package_type
- when 'composer'
- object.composer_metadatum
- else
- nil
- end
- end
- end
- end
-end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 9a3f2e311e6..21534f40499 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -183,6 +183,12 @@ module Types
description: 'Packages of the project.',
resolver: Resolvers::ProjectPackagesResolver
+ field :jobs,
+ Types::Ci::JobType.connection_type,
+ null: true,
+ description: 'Jobs of a project. This field can only be resolved for one project in any single request.',
+ resolver: Resolvers::ProjectJobsResolver
+
field :pipelines,
null: true,
description: 'Build pipelines of the project.',
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 74818bfcd42..8af0db644dd 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -55,7 +55,10 @@ module Types
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.'
+ argument :id,
+ type: ::Types::GlobalIDType[::ContainerRepository],
+ required: true,
+ description: 'The global ID of the container repository.'
end
field :package,
@@ -72,9 +75,7 @@ module Types
description: 'Find users.',
resolver: Resolvers::UsersResolver
- field :echo, GraphQL::STRING_TYPE, null: false,
- description: 'Text to echo back.',
- resolver: Resolvers::EchoResolver
+ field :echo, resolver: Resolvers::EchoResolver
field :issue, Types::IssueType,
null: true,
@@ -82,11 +83,16 @@ module Types
argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue.'
end
- field :instance_statistics_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
+ field :instance_statistics_measurements,
+ type: Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
null: true,
description: 'Get statistics on the instance.',
- deprecated: { reason: 'This field was renamed. Use the `usageTrendsMeasurements` field instead', milestone: '13.10' },
- resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver
+ resolver: Resolvers::Admin::Analytics::UsageTrends::MeasurementsResolver,
+ deprecated: {
+ reason: :renamed,
+ replacement: 'Query.usageTrendsMeasurements',
+ milestone: '13.10'
+ }
field :usage_trends_measurements, Types::Admin::Analytics::UsageTrends::MeasurementType.connection_type,
null: true,
@@ -97,18 +103,10 @@ module Types
null: true,
description: 'CI related settings that apply to the entire instance.'
- field :runner_platforms, Types::Ci::RunnerPlatformType.connection_type,
- 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
+ field :runner_platforms, resolver: Resolvers::Ci::RunnerPlatformsResolver
+ field :runner_setup, resolver: Resolvers::Ci::RunnerSetupResolver
- field :ci_config, Types::Ci::Config::ConfigType, null: true,
- description: 'Get linted and processed contents of a CI config. Should not be requested more than once per request.',
- resolver: Resolvers::Ci::ConfigResolver,
- complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1
+ field :ci_config, resolver: Resolvers::Ci::ConfigResolver, complexity: 126 # AUTHENTICATED_COMPLEXITY / 2 + 1
def design_management
DesignManagementObject.new(nil)
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
new file mode 100644
index 00000000000..912fc5f643a
--- /dev/null
+++ b/app/graphql/types/repository/blob_type.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+module Types
+ module Repository
+ # rubocop: disable Graphql/AuthorizeTypes
+ # This is presented through `Repository` that has its own authorization
+ class BlobType < BaseObject
+ present_using BlobPresenter
+
+ graphql_name 'RepositoryBlob'
+
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the blob.'
+
+ field :oid, GraphQL::STRING_TYPE, null: false, method: :id,
+ description: 'OID of the blob.'
+
+ field :path, GraphQL::STRING_TYPE, null: false,
+ description: 'Path of the blob.'
+
+ field :name, GraphQL::STRING_TYPE,
+ description: 'Blob name.',
+ null: true
+
+ field :mode, type: GraphQL::STRING_TYPE,
+ description: 'Blob mode.',
+ null: true
+
+ field :lfs_oid, GraphQL::STRING_TYPE, null: true,
+ calls_gitaly: true,
+ description: 'LFS OID of the blob.'
+
+ field :web_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path of the blob.'
+
+ def lfs_oid
+ Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index e319a5f3124..963a4296c4f 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -14,5 +14,10 @@ module Types
description: 'Indicates a corresponding Git repository exists on disk.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.'
+ field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
+ description: 'Blobs contained within the repository'
+ field :branch_names, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true,
+ complexity: 170, description: 'Names of branches available in this repository that match the search pattern.',
+ resolver: Resolvers::RepositoryBranchNamesResolver
end
end
diff --git a/app/graphql/types/sort_enum.rb b/app/graphql/types/sort_enum.rb
index ff994039b6d..cc04394004d 100644
--- a/app/graphql/types/sort_enum.rb
+++ b/app/graphql/types/sort_enum.rb
@@ -7,10 +7,34 @@ module Types
# Deprecated, as we prefer uppercase enums
# https://gitlab.com/groups/gitlab-org/-/epics/1838
- value 'updated_desc', 'Updated at descending order.', value: :updated_desc, deprecated: { reason: 'Use UPDATED_DESC', milestone: '13.5' }
- value 'updated_asc', 'Updated at ascending order.', value: :updated_asc, deprecated: { reason: 'Use UPDATED_ASC', milestone: '13.5' }
- value 'created_desc', 'Created at descending order.', value: :created_desc, deprecated: { reason: 'Use CREATED_DESC', milestone: '13.5' }
- value 'created_asc', 'Created at ascending order.', value: :created_asc, deprecated: { reason: 'Use CREATED_ASC', milestone: '13.5' }
+ value 'updated_desc', 'Updated at descending order.',
+ value: :updated_desc,
+ deprecated: {
+ reason: :renamed,
+ replacement: 'UPDATED_DESC',
+ milestone: '13.5'
+ }
+ value 'updated_asc', 'Updated at ascending order.',
+ value: :updated_asc,
+ deprecated: {
+ reason: :renamed,
+ replacement: 'UPDATED_ASC',
+ milestone: '13.5'
+ }
+ value 'created_desc', 'Created at descending order.',
+ value: :created_desc,
+ deprecated: {
+ reason: :renamed,
+ replacement: 'CREATED_DESC',
+ milestone: '13.5'
+ }
+ value 'created_asc', 'Created at ascending order.',
+ value: :created_asc,
+ deprecated: {
+ reason: :renamed,
+ replacement: 'CREATED_ASC',
+ milestone: '13.5'
+ }
value 'UPDATED_DESC', 'Updated at descending order.', value: :updated_desc
value 'UPDATED_ASC', 'Updated at ascending order.', value: :updated_asc
diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb
new file mode 100644
index 00000000000..465e3c492bc
--- /dev/null
+++ b/app/graphql/types/timelog_type.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Types
+ class TimelogType < BaseObject
+ graphql_name 'Timelog'
+
+ authorize :read_group_timelogs
+
+ field :spent_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp of when the time tracked was spent at.'
+
+ field :time_spent,
+ GraphQL::INT_TYPE,
+ null: false,
+ description: 'The time spent displayed in seconds.'
+
+ field :user,
+ Types::UserType,
+ null: false,
+ description: 'The user that logged the time.'
+
+ field :issue,
+ Types::IssueType,
+ null: true,
+ description: 'The issue that logged time was added to.'
+
+ field :note,
+ Types::Notes::NoteType,
+ null: true,
+ description: 'The note where the quick action to add the logged time was executed.'
+
+ def user
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.user_id).find
+ end
+
+ def issue
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.issue_id).find
+ end
+ end
+end
diff --git a/app/graphql/types/user_merge_request_interaction_type.rb b/app/graphql/types/user_merge_request_interaction_type.rb
new file mode 100644
index 00000000000..5ff0d79f13e
--- /dev/null
+++ b/app/graphql/types/user_merge_request_interaction_type.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Types
+ class UserMergeRequestInteractionType < BaseObject
+ graphql_name 'UserMergeRequestInteraction'
+ description <<~MD
+ Information about a merge request given a specific user.
+
+ This object has two parts to its state: a `User` and a `MergeRequest`. All
+ fields relate to interactions between the two entities.
+ MD
+
+ authorize :read_merge_request
+
+ field :can_merge,
+ type: ::GraphQL::BOOLEAN_TYPE,
+ null: false,
+ calls_gitaly: true,
+ method: :can_merge?,
+ description: 'Whether this user can merge this merge request.'
+
+ field :can_update,
+ type: ::GraphQL::BOOLEAN_TYPE,
+ null: false,
+ method: :can_update?,
+ description: 'Whether this user can update this merge request.'
+
+ field :review_state,
+ ::Types::MergeRequestReviewStateEnum,
+ null: true,
+ description: 'The state of the review by this user.'
+
+ field :reviewed,
+ type: ::GraphQL::BOOLEAN_TYPE,
+ null: false,
+ method: :reviewed?,
+ description: 'Whether this user has provided a review for this merge request.'
+
+ field :approved,
+ type: ::GraphQL::BOOLEAN_TYPE,
+ null: false,
+ method: :approved?,
+ description: 'Whether this user has approved this merge request.'
+ end
+end
+
+::Types::UserMergeRequestInteractionType.prepend_if_ee('EE::Types::UserMergeRequestInteractionType')
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 2cc7d379240..3d7db80ae11 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -3,6 +3,7 @@
module Types
class UserType < BaseObject
graphql_name 'User'
+ description 'Representation of a GitLab user.'
authorize :read_user
@@ -10,61 +11,87 @@ module Types
expose_permissions Types::PermissionTypes::User
- field :id, GraphQL::ID_TYPE, null: false,
+ field :id,
+ type: GraphQL::ID_TYPE,
+ null: false,
description: 'ID of the user.'
- field :bot, GraphQL::BOOLEAN_TYPE, null: false,
+ field :bot,
+ type: GraphQL::BOOLEAN_TYPE,
+ null: false,
description: 'Indicates if the user is a bot.',
method: :bot?
- field :username, GraphQL::STRING_TYPE, null: false,
+ field :username,
+ type: GraphQL::STRING_TYPE,
+ null: false,
description: 'Username of the user. Unique within this instance of GitLab.'
- field :name, GraphQL::STRING_TYPE, null: false,
+ field :name,
+ type: GraphQL::STRING_TYPE,
+ null: false,
description: 'Human-readable name of the user.'
- field :state, Types::UserStateEnum, null: false,
+ field :state,
+ type: Types::UserStateEnum,
+ null: false,
description: 'State of the user.'
- field :email, GraphQL::STRING_TYPE, null: true,
+ field :email,
+ type: GraphQL::STRING_TYPE,
+ null: true,
description: 'User email.', method: :public_email,
- deprecated: { reason: 'Use public_email', milestone: '13.7' }
- field :public_email, GraphQL::STRING_TYPE, null: true,
+ deprecated: { reason: :renamed, replacement: 'User.publicEmail', milestone: '13.7' }
+ field :public_email,
+ type: GraphQL::STRING_TYPE,
+ null: true,
description: "User's public email."
- field :avatar_url, GraphQL::STRING_TYPE, null: true,
+ field :avatar_url,
+ type: GraphQL::STRING_TYPE,
+ null: true,
description: "URL of the user's avatar."
- field :web_url, GraphQL::STRING_TYPE, null: false,
+ field :web_url,
+ type: GraphQL::STRING_TYPE,
+ null: false,
description: 'Web URL of the user.'
- field :web_path, GraphQL::STRING_TYPE, null: false,
+ field :web_path,
+ type: GraphQL::STRING_TYPE,
+ null: false,
description: 'Web path of the user.'
- field :todos, Types::TodoType.connection_type, null: false,
+ field :todos,
resolver: Resolvers::TodoResolver,
description: 'To-do items of the user.'
- field :group_memberships, Types::GroupMemberType.connection_type, null: true,
+ field :group_memberships,
+ type: Types::GroupMemberType.connection_type,
+ null: true,
description: 'Group memberships of the user.'
- field :group_count, GraphQL::INT_TYPE, null: true,
+ field :group_count,
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 :location, ::GraphQL::STRING_TYPE, null: true,
+ field :status,
+ type: Types::UserStatusType,
+ null: true,
+ description: 'User status.'
+ field :location,
+ type: ::GraphQL::STRING_TYPE,
+ null: true,
description: 'The location of the user.'
- field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
+ field :project_memberships,
+ type: Types::ProjectMemberType.connection_type,
+ null: true,
description: 'Project memberships of the user.'
- field :starred_projects, Types::ProjectType.connection_type, null: true,
+ field :starred_projects,
description: 'Projects starred by the user.',
resolver: Resolvers::UserStarredProjectsResolver
# Merge request field: MRs can be authored, assigned, or assigned-for-review:
field :authored_merge_requests,
resolver: Resolvers::AuthoredMergeRequestsResolver,
- description: 'Merge Requests authored by the user.'
+ description: 'Merge requests authored by the user.'
field :assigned_merge_requests,
resolver: Resolvers::AssignedMergeRequestsResolver,
- description: 'Merge Requests assigned to the user.'
+ description: 'Merge requests assigned to the user.'
field :review_requested_merge_requests,
resolver: Resolvers::ReviewRequestedMergeRequestsResolver,
- description: 'Merge Requests assigned to the user for review.'
+ description: 'Merge requests assigned to the user for review.'
field :snippets,
- Types::SnippetType.connection_type,
- null: true,
description: 'Snippets authored by the user.',
resolver: Resolvers::Users::SnippetsResolver
field :callouts,