diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-19 23:18:09 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-19 23:18:09 +0000 |
commit | 6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch) | |
tree | dc4d20fe6064752c0bd323187252c77e0a89144b /app/graphql | |
parent | 9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff) | |
download | gitlab-ce-6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde.tar.gz |
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'app/graphql')
76 files changed, 1454 insertions, 433 deletions
diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb index b39875b83a9..8086d8c02a4 100644 --- a/app/graphql/graphql_triggers.rb +++ b/app/graphql/graphql_triggers.rb @@ -21,3 +21,5 @@ module GraphqlTriggers GitlabSchema.subscriptions.trigger('issuableDatesUpdated', { issuable_id: issuable.to_gid }, issuable) end end + +GraphqlTriggers.prepend_mod diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb index 14fe9714f99..e9cae80e5f9 100644 --- a/app/graphql/mutations/boards/issues/issue_move_list.rb +++ b/app/graphql/mutations/boards/issues/issue_move_list.rb @@ -38,10 +38,16 @@ module Mutations required: false, description: 'ID of issue that should be placed after the current issue.' + argument :position_in_list, GraphQL::Types::Int, + required: false, + description: "Position of issue within the board list. Positions start at 0. "\ + "Use #{::Boards::Issues::MoveService::LIST_END_POSITION} to move to the end of the list." + def ready?(**args) if move_arguments(args).blank? raise Gitlab::Graphql::Errors::ArgumentError, - 'At least one of the arguments fromListId, toListId, afterId or beforeId is required' + 'At least one of the arguments ' \ + 'fromListId, toListId, positionInList, moveAfterId, or moveBeforeId is required' end if move_list_arguments(args).one? @@ -49,6 +55,24 @@ module Mutations 'Both fromListId and toListId must be present' end + if args[:position_in_list].present? + if move_list_arguments(args).empty? + raise Gitlab::Graphql::Errors::ArgumentError, + 'Both fromListId and toListId are required when positionInList is given' + end + + if args[:move_before_id].present? || args[:move_after_id].present? + raise Gitlab::Graphql::Errors::ArgumentError, + 'positionInList is mutually exclusive with any of moveBeforeId or moveAfterId' + end + + if args[:position_in_list] != ::Boards::Issues::MoveService::LIST_END_POSITION && + args[:position_in_list] < 0 + raise Gitlab::Graphql::Errors::ArgumentError, + "positionInList must be >= 0 or #{::Boards::Issues::MoveService::LIST_END_POSITION}" + end + end + super end @@ -77,7 +101,7 @@ module Mutations end def move_arguments(args) - args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id) + args.slice(:from_list_id, :to_list_id, :position_in_list, :move_after_id, :move_before_id) end def error_for(result) diff --git a/app/graphql/mutations/ci/job/artifacts_destroy.rb b/app/graphql/mutations/ci/job/artifacts_destroy.rb new file mode 100644 index 00000000000..c27ab9c4d89 --- /dev/null +++ b/app/graphql/mutations/ci/job/artifacts_destroy.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Job + class ArtifactsDestroy < Base + graphql_name 'JobArtifactsDestroy' + + authorize :destroy_artifacts + + field :job, + Types::Ci::JobType, + null: true, + description: 'Job with artifacts to be deleted.' + + field :destroyed_artifacts_count, + GraphQL::Types::Int, + null: false, + description: 'Number of artifacts deleted.' + + def find_object(id: ) + GlobalID::Locator.locate(id) + end + + def resolve(id:) + job = authorized_find!(id: id) + + result = ::Ci::JobArtifacts::DestroyBatchService.new(job.job_artifacts, pick_up_at: Time.current).execute + { + job: job, + destroyed_artifacts_count: result[:destroyed_artifacts_count], + errors: Array(result[:errors]) + } + end + end + end + end +end diff --git a/app/graphql/mutations/ci/job_artifact/destroy.rb b/app/graphql/mutations/ci/job_artifact/destroy.rb new file mode 100644 index 00000000000..47b3535d631 --- /dev/null +++ b/app/graphql/mutations/ci/job_artifact/destroy.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module JobArtifact + class Destroy < BaseMutation + graphql_name 'ArtifactDestroy' + + authorize :destroy_artifacts + + ArtifactID = ::Types::GlobalIDType[::Ci::JobArtifact] + + argument :id, + ArtifactID, + required: true, + description: 'ID of the artifact to delete.' + + field :artifact, + Types::Ci::JobArtifactType, + null: true, + description: 'Deleted artifact.' + + def find_object(id: ) + GlobalID::Locator.locate(id) + end + + def resolve(id:) + artifact = authorized_find!(id: id) + + if artifact.destroy + { errors: [] } + else + { errors: artifact.errors.full_messages } + end + end + end + end + end +end diff --git a/app/graphql/mutations/ci/runner/bulk_delete.rb b/app/graphql/mutations/ci/runner/bulk_delete.rb index 4c1c2967799..4265099d28e 100644 --- a/app/graphql/mutations/ci/runner/bulk_delete.rb +++ b/app/graphql/mutations/ci/runner/bulk_delete.rb @@ -40,9 +40,7 @@ module Mutations private def model_ids_of(ids) - ids.map do |gid| - gid.model_id.to_i - end.compact + ids.filter_map { |gid| gid.model_id.to_i } end def find_all_runners_by_ids(ids) diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb index 1c6cf6989bf..f98138646be 100644 --- a/app/graphql/mutations/ci/runner/update.rb +++ b/app/graphql/mutations/ci/runner/update.rb @@ -48,8 +48,13 @@ module Mutations description: 'Indicates the runner is able to run untagged jobs.' argument :tag_list, [GraphQL::Types::String], - required: false, - description: 'Tags associated with the runner.' + required: false, + description: 'Tags associated with the runner.' + + argument :associated_projects, [::Types::GlobalIDType[::Project]], + required: false, + description: 'Projects associated with the runner. Available only for project runners.', + prepare: -> (global_ids, ctx) { global_ids&.filter_map { |gid| gid.model_id.to_i } } field :runner, Types::Ci::RunnerType, @@ -59,16 +64,47 @@ module Mutations def resolve(id:, **runner_attrs) runner = authorized_find!(id) - unless ::Ci::Runners::UpdateRunnerService.new(runner).update(runner_attrs) - return { runner: nil, errors: runner.errors.full_messages } + associated_projects_ids = runner_attrs.delete(:associated_projects) + + response = { runner: runner, errors: [] } + ::Ci::Runner.transaction do + associate_runner_projects(response, runner, associated_projects_ids) if associated_projects_ids.present? + update_runner(response, runner, runner_attrs) end - { runner: runner, errors: [] } + response end def find_object(id) GitlabSchema.find_by_gid(id) end + + private + + def associate_runner_projects(response, runner, associated_project_ids) + unless runner.project_type? + raise Gitlab::Graphql::Errors::ArgumentError, + "associatedProjects must not be specified for '#{runner.runner_type}' scope" + end + + result = ::Ci::Runners::SetRunnerAssociatedProjectsService.new( + runner: runner, + current_user: current_user, + project_ids: associated_project_ids + ).execute + return if result.success? + + response[:errors] = result.errors + raise ActiveRecord::Rollback + end + + def update_runner(response, runner, attrs) + result = ::Ci::Runners::UpdateRunnerService.new(runner).execute(attrs) + return if result.success? + + response[:errors] = result.errors + raise ActiveRecord::Rollback + end end end end diff --git a/app/graphql/mutations/custom_emoji/create.rb b/app/graphql/mutations/custom_emoji/create.rb index 269ea6c9999..535ff44a7fd 100644 --- a/app/graphql/mutations/custom_emoji/create.rb +++ b/app/graphql/mutations/custom_emoji/create.rb @@ -28,6 +28,10 @@ module Mutations description: 'Location of the emoji file.' def resolve(group_path:, **args) + if Feature.disabled?(:custom_emoji) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Custom emoji feature is disabled' + end + group = authorized_find!(group_path: group_path) # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911#note_444682238 args[:external] = true diff --git a/app/graphql/mutations/custom_emoji/destroy.rb b/app/graphql/mutations/custom_emoji/destroy.rb index 863b8152cc7..64e3f2ed7d3 100644 --- a/app/graphql/mutations/custom_emoji/destroy.rb +++ b/app/graphql/mutations/custom_emoji/destroy.rb @@ -17,6 +17,10 @@ module Mutations description: 'Global ID of the custom emoji to destroy.' def resolve(id:) + if Feature.disabled?(:custom_emoji) + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Custom emoji feature is disabled' + end + custom_emoji = authorized_find!(id: id) custom_emoji.destroy! diff --git a/app/graphql/mutations/dependency_proxy/group_settings/update.rb b/app/graphql/mutations/dependency_proxy/group_settings/update.rb index 65c919db3c3..6be07edd883 100644 --- a/app/graphql/mutations/dependency_proxy/group_settings/update.rb +++ b/app/graphql/mutations/dependency_proxy/group_settings/update.rb @@ -8,6 +8,11 @@ module Mutations include Mutations::ResolvesGroup + description 'These settings can be adjusted by the group Owner or Maintainer. However, in GitLab 16.0, we ' \ + 'will be limiting this to the Owner role. ' \ + '[GitLab-#364441](https://gitlab.com/gitlab-org/gitlab/-/issues/364441) proposes making ' \ + 'this change to match the permissions level in the user interface.' + authorize :admin_dependency_proxy argument :group_path, diff --git a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb index 31ae29d896b..bb1da9278ff 100644 --- a/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb +++ b/app/graphql/mutations/incident_management/timeline_event/promote_from_note.rb @@ -6,6 +6,8 @@ module Mutations class PromoteFromNote < Base graphql_name 'TimelineEventPromoteFromNote' + include NotesHelper + argument :note_id, Types::GlobalIDType[::Note], required: true, description: 'Note ID from which the timeline event promoted.' @@ -20,7 +22,7 @@ module Mutations incident, current_user, promoted_from_note: note, - note: note.note, + note: build_note_string(note), occurred_at: note.created_at, editable: true ).execute @@ -38,6 +40,11 @@ module Mutations super end + def build_note_string(note) + commented = _('commented') + "@#{note.author.username} [#{commented}](#{noteable_note_url(note)}): '#{note.note}'" + end + def raise_noteable_not_incident! raise_resource_not_available_error! 'Note does not belong to an incident' end diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb index 70a0e71c869..ba1fa8d446c 100644 --- a/app/graphql/mutations/releases/create.rb +++ b/app/graphql/mutations/releases/create.rb @@ -32,7 +32,7 @@ module Mutations argument :released_at, Types::TimeType, required: false, - description: 'Date and time for the release. Defaults to the current date and time.' + description: 'Date and time for the release. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Only provide this field if creating an upcoming or historical release.' argument :milestones, [GraphQL::Types::String], required: false, diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index fe0ad6df65b..20913a9e7da 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -32,9 +32,7 @@ module Mutations private def model_ids_of(ids) - ids.map do |gid| - gid.model_id.to_i - end.compact + ids.filter_map { |gid| gid.model_id.to_i } end def raise_too_many_todos_requested_error diff --git a/app/graphql/queries/repository/blob_info.query.graphql b/app/graphql/queries/repository/blob_info.query.graphql new file mode 100644 index 00000000000..fd463436ed4 --- /dev/null +++ b/app/graphql/queries/repository/blob_info.query.graphql @@ -0,0 +1,62 @@ +query getBlobInfo( + $projectPath: ID! + $filePath: String! + $ref: String! + $shouldFetchRawText: Boolean! +) { + project(fullPath: $projectPath) { + __typename + id + repository { + __typename + empty + blobs(paths: [$filePath], ref: $ref) { + __typename + nodes { + __typename + id + webPath + name + size + rawSize + rawTextBlob @include(if: $shouldFetchRawText) + fileType + language + path + blamePath + editBlobPath + gitpodBlobUrl + ideEditPath + forkAndEditPath + ideForkAndEditPath + codeNavigationPath + projectBlobPathRoot + forkAndViewPath + environmentFormattedExternalUrl + environmentExternalUrlForRouteMap + canModifyBlob + canCurrentUserPushToBranch + archived + storedExternally + externalStorage + externalStorageUrl + rawPath + replacePath + pipelineEditorPath + simpleViewer { + fileType + tooLarge + type + renderError + } + richViewer { + fileType + tooLarge + type + renderError + } + } + } + } + } +} diff --git a/app/graphql/resolvers/ci/job_token_scope_resolver.rb b/app/graphql/resolvers/ci/job_token_scope_resolver.rb index ca76a7b94fc..7c6aedad1d6 100644 --- a/app/graphql/resolvers/ci/job_token_scope_resolver.rb +++ b/app/graphql/resolvers/ci/job_token_scope_resolver.rb @@ -6,14 +6,12 @@ module Resolvers include Gitlab::Graphql::Authorize::AuthorizeResource authorize :admin_project - description 'Container for resources that can be accessed by a CI job token from the current project. Null if job token scope setting is disabled.' + description 'Container for resources that can be accessed by a CI job token from the current project.' type ::Types::Ci::JobTokenScopeType, null: true def resolve authorize!(object) - return unless object.ci_job_token_scope_enabled? - ::Ci::JobToken::Scope.new(object) end end diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb index 2f6ca09d031..de00aadaea8 100644 --- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb +++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb @@ -9,6 +9,7 @@ module Resolvers type ::Types::Ci::JobType.connection_type, null: true authorize :read_builds authorizes_object! + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 argument :statuses, [::Types::Ci::JobStatusEnum], required: false, @@ -16,15 +17,6 @@ module Resolvers alias_method :runner, :object - def ready?(**args) - context[self.class] ||= { executions: 0 } - context[self.class][:executions] += 1 - - raise GraphQL::ExecutionError, "Jobs can be requested for only one runner 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, runner: runner, params: { scope: statuses }).execute diff --git a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb index 14b5f8f90eb..da8fab93619 100644 --- a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb +++ b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb @@ -9,7 +9,7 @@ module Resolvers alias_method :runner, :object - def resolve_with_lookahead(**args) + def resolve_with_lookahead(**_args) resolve_owner end @@ -19,6 +19,8 @@ module Resolvers } end + private + def filtered_preloads selection = lookahead @@ -27,8 +29,6 @@ module Resolvers end end - private - def resolve_owner return unless runner.project_type? @@ -48,14 +48,13 @@ module Resolvers .transform_values { |runner_projects| runner_projects.first.project_id } project_ids = owner_project_id_by_runner_id.values.uniq - all_preloads = unconditional_includes + filtered_preloads - owner_relation = Project.all - owner_relation = owner_relation.preload(*all_preloads) if all_preloads.any? - projects = owner_relation.where(id: project_ids).index_by(&:id) + projects = Project.where(id: project_ids) + Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute + projects_by_id = projects.index_by(&:id) runner_ids.each do |runner_id| owner_project_id = owner_project_id_by_runner_id[runner_id] - loader.call(runner_id, projects[owner_project_id]) + loader.call(runner_id, projects_by_id[owner_project_id]) end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/graphql/resolvers/ci/runner_projects_resolver.rb b/app/graphql/resolvers/ci/runner_projects_resolver.rb new file mode 100644 index 00000000000..ca3b4ebb797 --- /dev/null +++ b/app/graphql/resolvers/ci/runner_projects_resolver.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerProjectsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include LooksAhead + include ProjectSearchArguments + + type Types::ProjectType.connection_type, null: true + authorize :read_runner + authorizes_object! + + alias_method :runner, :object + + argument :sort, GraphQL::Types::String, + required: false, + default_value: 'id_asc', # TODO: Remove in %16.0 and move :sort to ProjectSearchArguments, see https://gitlab.com/gitlab-org/gitlab/-/issues/372117 + deprecated: { + reason: 'Default sort order will change in 16.0. ' \ + 'Specify `"id_asc"` if query results\' order is important', + milestone: '15.4' + }, + description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \ + "for example: 'id_desc' or 'name_asc'" + + def resolve_with_lookahead(**args) + return unless runner.project_type? + + # rubocop:disable CodeReuse/ActiveRecord + BatchLoader::GraphQL.for(runner.id).batch(key: :runner_projects) do |runner_ids, loader| + plucked_runner_and_project_ids = ::Ci::RunnerProject + .select(:runner_id, :project_id) + .where(runner_id: runner_ids) + .pluck(:runner_id, :project_id) + + project_ids = plucked_runner_and_project_ids.collect { |_runner_id, project_id| project_id }.uniq + projects = ProjectsFinder + .new(current_user: current_user, + params: project_finder_params(args), + project_ids_relation: project_ids) + .execute + Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute + projects_by_id = projects.index_by(&:id) + + # In plucked_runner_and_project_ids, first() represents the runner ID, and second() the project ID, + # so let's group the project IDs by runner ID + runner_project_ids_by_runner_id = + plucked_runner_and_project_ids + .group_by(&:first) + .transform_values { |values| values.map(&:second).filter_map { |project_id| projects_by_id[project_id] } } + + runner_ids.each do |runner_id| + runner_projects = runner_project_ids_by_runner_id[runner_id] || [] + + loader.call(runner_id, runner_projects) + end + end + # rubocop:enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb index f758e217b47..a2d3af9c664 100644 --- a/app/graphql/resolvers/ci/test_suite_resolver.rb +++ b/app/graphql/resolvers/ci/test_suite_resolver.rb @@ -28,7 +28,8 @@ module Resolvers def load_test_suite_data(builds) suite = builds.sum do |build| - build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new) + test_report = build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new) + test_report.get_suite(build.test_suite_name) end Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load! diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index fe213936f55..8295bd58388 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -76,24 +76,11 @@ module IssueResolverArguments end def resolve_with_lookahead(**args) - # The project could have been loaded in batch by `BatchLoader`. - # At this point we need the `id` of the project to query for issues, so - # make sure it's loaded and not `nil` before continuing. - parent = object.respond_to?(:sync) ? object.sync : object - return Issue.none if parent.nil? - - # Will need to be made group & namespace aware with - # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 - args[:not] = args[:not].to_h if args[:not].present? - args[:iids] ||= [args.delete(:iid)].compact if args[:iid] - args[:attempt_project_search_optimizations] = true if args[:search].present? + return Issue.none if resource_parent.nil? - prepare_assignee_username_params(args) - prepare_release_tag_params(args) + finder = IssuesFinder.new(current_user, prepare_finder_params(args)) - finder = IssuesFinder.new(current_user, args) - - continue_issue_resolve(parent, finder, **args) + continue_issue_resolve(resource_parent, finder, **args) end def ready?(**args) @@ -103,7 +90,6 @@ module IssueResolverArguments params_not_mutually_exclusive(args, mutually_exclusive_milestone_args) params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args) params_not_mutually_exclusive(args, mutually_exclusive_release_tag_args) - validate_anonymous_search_access! if args[:search].present? super end @@ -128,6 +114,17 @@ module IssueResolverArguments private + def prepare_finder_params(args) + params = super(args) + params[:iids] ||= [params.delete(:iid)].compact if params[:iid] + params[:attempt_project_search_optimizations] = true if params[:search].present? + + prepare_assignee_username_params(params) + prepare_release_tag_params(params) + + params + end + def prepare_release_tag_params(args) release_tag_wildcard = args.delete(:release_tag_wildcard_id) return if release_tag_wildcard.blank? @@ -135,20 +132,13 @@ module IssueResolverArguments args[:release_tag] ||= release_tag_wildcard end - def mutually_exclusive_release_tag_args - [:release_tag, :release_tag_wildcard_id] - end - 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 params_not_mutually_exclusive(args, mutually_exclusive_args) - if args.slice(*mutually_exclusive_args).compact.size > 1 - arg_str = mutually_exclusive_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 + def mutually_exclusive_release_tag_args + [:release_tag, :release_tag_wildcard_id] end def mutually_exclusive_milestone_args @@ -158,4 +148,20 @@ module IssueResolverArguments def mutually_exclusive_assignee_username_args [:assignee_usernames, :assignee_username] end + + def params_not_mutually_exclusive(args, mutually_exclusive_args) + if args.slice(*mutually_exclusive_args).compact.size > 1 + arg_str = mutually_exclusive_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 + end + + def resource_parent + # The project could have been loaded in batch by `BatchLoader`. + # At this point we need the `id` of the project to query for issues, so + # make sure it's loaded and not `nil` before continuing. + strong_memoize(:resource_parent) do + object.respond_to?(:sync) ? object.sync : object + end + end end diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb index 644b2a11460..b548dc1e175 100644 --- a/app/graphql/resolvers/concerns/looks_ahead.rb +++ b/app/graphql/resolvers/concerns/looks_ahead.rb @@ -33,10 +33,14 @@ module LooksAhead end def filtered_preloads - selection = node_selection + nodes = node_selection + + return [] unless nodes + + selected_fields = nodes.selections.map(&:name) preloads.each.flat_map do |name, requirements| - selection&.selects?(name) ? requirements : [] + selected_fields.include?(name) ? requirements : [] end end diff --git a/app/graphql/resolvers/concerns/project_search_arguments.rb b/app/graphql/resolvers/concerns/project_search_arguments.rb new file mode 100644 index 00000000000..7e03963f412 --- /dev/null +++ b/app/graphql/resolvers/concerns/project_search_arguments.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ProjectSearchArguments + extend ActiveSupport::Concern + + included do + argument :membership, GraphQL::Types::Boolean, + required: false, + description: 'Return only projects that the current user is a member of.' + + argument :search, GraphQL::Types::String, + required: false, + description: 'Search query, which can be for the project name, a path, or a description.' + + argument :search_namespaces, GraphQL::Types::Boolean, + required: false, + description: 'Include namespace in project search.' + + argument :topics, type: [GraphQL::Types::String], + required: false, + description: 'Filter projects by topics.' + end + + private + + def project_finder_params(params) + { + without_deleted: true, + non_public: params[:membership], + search: params[:search], + search_namespaces: params[:search_namespaces], + sort: params[:sort], + topic: params[:topics] + }.compact + end +end diff --git a/app/graphql/resolvers/concerns/search_arguments.rb b/app/graphql/resolvers/concerns/search_arguments.rb index 7f480f9d0b6..95c6dbf7497 100644 --- a/app/graphql/resolvers/concerns/search_arguments.rb +++ b/app/graphql/resolvers/concerns/search_arguments.rb @@ -7,12 +7,49 @@ module SearchArguments argument :search, GraphQL::Types::String, required: false, description: 'Search query for title or description.' + argument :in, [Types::IssuableSearchableFieldEnum], + required: false, + description: <<~DESC + Specify the fields to perform the search in. + Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.' + DESC + end + + def ready?(**args) + validate_search_in_params!(args) + validate_anonymous_search_access!(args) + + super end - def validate_anonymous_search_access! + private + + def validate_anonymous_search_access!(args) + return unless args[:search].present? return if current_user.present? || Feature.disabled?(:disable_anonymous_search, type: :ops) raise ::Gitlab::Graphql::Errors::ArgumentError, "User must be authenticated to include the `search` argument." end + + def validate_search_in_params!(args) + return unless args[:in].present? && args[:search].blank? + + raise Gitlab::Graphql::Errors::ArgumentError, + '`search` should be present when including the `in` argument' + end + + def prepare_finder_params(args) + prepare_search_params(args) + end + + def prepare_search_params(args) + return args unless args[:search].present? + + parent_type = resource_parent.is_a?(Project) ? :project : :group + args[:"attempt_#{parent_type}_search_optimizations"] = true + args[:in] = args[:in].join(',') if args[:in].present? + + args + end end diff --git a/app/graphql/resolvers/crm/organization_state_counts_resolver.rb b/app/graphql/resolvers/crm/organization_state_counts_resolver.rb new file mode 100644 index 00000000000..c16a4bd24ea --- /dev/null +++ b/app/graphql/resolvers/crm/organization_state_counts_resolver.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Resolvers + module Crm + class OrganizationStateCountsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorize :read_crm_organization + authorizes_object! + + type Types::CustomerRelations::OrganizationStateCountsType, null: true + + argument :search, GraphQL::Types::String, + required: false, + description: 'Search term to find organizations with.' + + argument :state, Types::CustomerRelations::OrganizationStateEnum, + required: false, + description: 'State of the organizations to search for.' + + def resolve(**args) + ::Crm::OrganizationsFinder.counts_by_state(context[:current_user], args.merge({ group: object })) + end + end + end +end diff --git a/app/graphql/resolvers/crm/organizations_resolver.rb b/app/graphql/resolvers/crm/organizations_resolver.rb index ca0a908ee22..719834f406d 100644 --- a/app/graphql/resolvers/crm/organizations_resolver.rb +++ b/app/graphql/resolvers/crm/organizations_resolver.rb @@ -10,6 +10,11 @@ module Resolvers type Types::CustomerRelations::OrganizationType, null: true + argument :sort, Types::CustomerRelations::OrganizationSortEnum, + description: 'Criteria to sort organizations by.', + required: false, + default_value: { field: 'name', direction: :asc } + argument :search, GraphQL::Types::String, required: false, description: 'Search term used to find organizations with.' @@ -24,6 +29,7 @@ module Resolvers def resolve(**args) args[:ids] = resolve_ids(args.delete(:ids)) + args.delete(:state) if args[:state] == :all ::Crm::OrganizationsFinder.new(current_user, { group: group }.merge(args)).execute end diff --git a/app/graphql/resolvers/deployment_resolver.rb b/app/graphql/resolvers/deployment_resolver.rb new file mode 100644 index 00000000000..7d9ce0f023c --- /dev/null +++ b/app/graphql/resolvers/deployment_resolver.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Resolvers + class DeploymentResolver < BaseResolver + argument :iid, + GraphQL::Types::ID, + required: true, + description: 'Project-level internal ID of the Deployment.' + + type Types::DeploymentType, null: true + + alias_method :project, :object + + def resolve(iid:) + return unless project.present? && project.is_a?(::Project) + + Deployment.for_iid(project, iid) + end + end +end diff --git a/app/graphql/resolvers/deployments_resolver.rb b/app/graphql/resolvers/deployments_resolver.rb new file mode 100644 index 00000000000..341d23c2ccb --- /dev/null +++ b/app/graphql/resolvers/deployments_resolver.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Resolvers + class DeploymentsResolver < BaseResolver + argument :statuses, [Types::DeploymentStatusEnum], + description: 'Statuses of the deployments.', + required: false, + as: :status + + argument :order_by, Types::DeploymentsOrderByInputType, + description: 'Order by a specified field.', + required: false + + type Types::DeploymentType, null: true + + alias_method :environment, :object + + def resolve(**args) + return unless environment.present? && environment.is_a?(::Environment) + + args = transform_args_for_finder(**args) + + # GraphQL BatchLoader shouldn't be used here because pagination query will be inefficient + # that fetches thousands of rows before limiting and offsetting. + DeploymentsFinder.new(environment: environment.id, **args).execute + end + + private + + def transform_args_for_finder(**args) + if (order_by = args.delete(:order_by)) + order_by = order_by.to_h.map { |k, v| { order_by: k.to_s, sort: v } }.first + args.merge!(order_by) + end + + args + end + end +end diff --git a/app/graphql/resolvers/environments/last_deployment_resolver.rb b/app/graphql/resolvers/environments/last_deployment_resolver.rb new file mode 100644 index 00000000000..76f80112673 --- /dev/null +++ b/app/graphql/resolvers/environments/last_deployment_resolver.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Resolvers + module Environments + class LastDeploymentResolver < BaseResolver + argument :status, + Types::DeploymentStatusEnum, + required: true, + description: 'Status of the Deployment.' + + type Types::DeploymentType, null: true + + def resolve(status:) + return unless object.present? && object.is_a?(::Environment) + + validate!(status) + + find_last_deployment(status) + end + + private + + def find_last_deployment(status) + BatchLoader::GraphQL.for(object).batch(key: status) do |environments, loader, args| + association_name = "last_#{args[:key]}_deployment".to_sym + + Preloaders::Environments::DeploymentPreloader.new(environments) + .execute_with_union(association_name, {}) + + environments.each do |environment| + loader.call(environment, environment.public_send(association_name)) # rubocop:disable GitlabSecurity/PublicSend + end + end + end + + def validate!(status) + unless Deployment::FINISHED_STATUSES.include?(status.to_sym) || + Deployment::UPCOMING_STATUSES.include?(status.to_sym) + raise Gitlab::Graphql::Errors::ArgumentError, "\"#{status}\" status is not supported." + end + end + end + end +end diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb index 934c1ba2738..f265e2183d0 100644 --- a/app/graphql/resolvers/environments_resolver.rb +++ b/app/graphql/resolvers/environments_resolver.rb @@ -21,8 +21,8 @@ module Resolvers def resolve(**args) return unless project.present? - Environments::EnvironmentsFinder.new(project, context[:current_user], args).execute - rescue Environments::EnvironmentsFinder::InvalidStatesError => e + ::Environments::EnvironmentsFinder.new(project, context[:current_user], args).execute + rescue ::Environments::EnvironmentsFinder::InvalidStatesError => e raise Gitlab::Graphql::Errors::ArgumentError, e.message end end diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb index b48e0b75190..e6a6abb39dd 100644 --- a/app/graphql/resolvers/group_packages_resolver.rb +++ b/app/graphql/resolvers/group_packages_resolver.rb @@ -5,6 +5,8 @@ module Resolvers class GroupPackagesResolver < PackagesBaseResolver # The GraphQL type is defined in the extended class + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + argument :sort, Types::Packages::PackageGroupSortEnum, description: 'Sort packages by this criteria.', required: false, @@ -15,14 +17,6 @@ module Resolvers project_path_asc: { order_by: 'project_path', sort: 'asc' } }).freeze - def ready?(**args) - context[self.class] ||= { executions: 0 } - context[self.class][:executions] += 1 - raise GraphQL::ExecutionError, "Packages can be requested only for one group at a time" if context[self.class][:executions] > 1 - - super - end - def resolve(sort:, **filters) return unless packages_available? diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb index 827db54134a..3d7894fdd6a 100644 --- a/app/graphql/resolvers/members_resolver.rb +++ b/app/graphql/resolvers/members_resolver.rb @@ -11,6 +11,10 @@ module Resolvers required: false, description: 'Search query.' + argument :sort, ::Types::MemberSortEnum, + required: false, + description: 'sort query.' + def resolve_with_lookahead(**args) authorize!(object) diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb index 705d3900cd2..b77c6b1112b 100644 --- a/app/graphql/resolvers/package_details_resolver.rb +++ b/app/graphql/resolvers/package_details_resolver.rb @@ -2,20 +2,14 @@ module Resolvers class PackageDetailsResolver < BaseResolver + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + type ::Types::Packages::PackageDetailsType, null: true argument :id, ::Types::GlobalIDType[::Packages::Package], required: true, description: '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:) GitlabSchema.find_by_gid(id) end diff --git a/app/graphql/resolvers/project_jobs_resolver.rb b/app/graphql/resolvers/project_jobs_resolver.rb index b09158d475d..4d13a4a3fae 100644 --- a/app/graphql/resolvers/project_jobs_resolver.rb +++ b/app/graphql/resolvers/project_jobs_resolver.rb @@ -8,6 +8,7 @@ module Resolvers type ::Types::Ci::JobType.connection_type, null: true authorize :read_build authorizes_object! + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 argument :statuses, [::Types::Ci::JobStatusEnum], required: false, @@ -15,15 +16,6 @@ module Resolvers alias_method :project, :object - def ready?(**args) - context[self.class] ||= { executions: 0 } - context[self.class][:executions] += 1 - - raise GraphQL::ExecutionError, "Jobs can be requested for only 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 diff --git a/app/graphql/resolvers/projects/branch_rules_resolver.rb b/app/graphql/resolvers/projects/branch_rules_resolver.rb new file mode 100644 index 00000000000..6c8b416bcea --- /dev/null +++ b/app/graphql/resolvers/projects/branch_rules_resolver.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Resolvers + module Projects + class BranchRulesResolver < BaseResolver + type Types::Projects::BranchRuleType.connection_type, null: false + + alias_method :project, :object + + def resolve(**args) + project.protected_branches + end + end + end +end diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb index facf8ffe36f..4d1e1b867da 100644 --- a/app/graphql/resolvers/projects_resolver.rb +++ b/app/graphql/resolvers/projects_resolver.rb @@ -2,31 +2,18 @@ module Resolvers class ProjectsResolver < BaseResolver - type Types::ProjectType, null: true - - argument :membership, GraphQL::Types::Boolean, - required: false, - description: 'Limit projects that the current user is a member of.' + include ProjectSearchArguments - argument :search, GraphQL::Types::String, - required: false, - description: 'Search query for project name, path, or description.' + type Types::ProjectType, null: true argument :ids, [GraphQL::Types::ID], required: false, description: 'Filter projects by IDs.' - argument :search_namespaces, GraphQL::Types::Boolean, - required: false, - description: 'Include namespace in project search.' - argument :sort, GraphQL::Types::String, required: false, - description: 'Sort order of results.' - - argument :topics, type: [GraphQL::Types::String], - required: false, - description: 'Filters projects by topics.' + description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \ + "for example: 'id_desc' or 'name_asc'" def resolve(**args) ProjectsFinder @@ -36,17 +23,6 @@ module Resolvers private - def project_finder_params(params) - { - without_deleted: true, - non_public: params[:membership], - search: params[:search], - search_namespaces: params[:search_namespaces], - sort: params[:sort], - topic: params[:topics] - }.compact - end - def parse_gids(gids) gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id } end diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb index 055984db3cb..9c7931a4edb 100644 --- a/app/graphql/resolvers/work_items_resolver.rb +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -26,27 +26,31 @@ module Resolvers required: false def resolve_with_lookahead(**args) - # The project could have been loaded in batch by `BatchLoader`. - # At this point we need the `id` of the project to query for issues, so - # make sure it's loaded and not `nil` before continuing. - parent = object.respond_to?(:sync) ? object.sync : object - return WorkItem.none if parent.nil? || !parent.work_items_feature_flag_enabled? + return WorkItem.none if resource_parent.nil? || !resource_parent.work_items_feature_flag_enabled? - args[:iids] ||= [args.delete(:iid)].compact if args[:iid] - args[:attempt_project_search_optimizations] = true if args[:search].present? + finder = ::WorkItems::WorkItemsFinder.new(current_user, prepare_finder_params(args)) - finder = ::WorkItems::WorkItemsFinder.new(current_user, args) - - Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) } + Gitlab::Graphql::Loaders::IssuableLoader.new(resource_parent, finder).batching_find_all { |q| apply_lookahead(q) } end - def ready?(**args) - validate_anonymous_search_access! if args[:search].present? + private - super + def preloads + { + last_edited_by: :last_edited_by + } end - private + # Allows to apply lookahead for fields + # selected from WidgetInterface + override :node_selection + def node_selection + selected_fields = super + + return unless selected_fields + + selected_fields.selection(:widgets) + end def unconditional_includes [ @@ -56,6 +60,22 @@ module Resolvers :author ] end + + def prepare_finder_params(args) + params = super(args) + params[:iids] ||= [params.delete(:iid)].compact if params[:iid] + + params + end + + def resource_parent + # The project could have been loaded in batch by `BatchLoader`. + # At this point we need the `id` of the project to query for work items, so + # make sure it's loaded and not `nil` before continuing. + strong_memoize(:resource_parent) do + object.respond_to?(:sync) ? object.sync : object + end + end end end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index 1c43432594a..6f64e5b5053 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -17,8 +17,6 @@ module Types @requires_argument = !!kwargs.delete(:requires_argument) @authorize = Array.wrap(kwargs.delete(:authorize)) kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity]) - @feature_flag = kwargs[:_deprecated_feature_flag] - kwargs = check_feature_flag(kwargs) @deprecation = gitlab_deprecation(kwargs) after_connection_extensions = kwargs.delete(:late_extensions) || [] @@ -91,16 +89,8 @@ module Types @constant_complexity end - def visible?(context) - return false if feature_flag.present? && !Feature.enabled?(feature_flag) - - super - end - private - attr_reader :feature_flag - def field_authorized?(object, ctx) object = object.node if object.is_a?(GraphQL::Pagination::Connection::Edge) @@ -123,27 +113,6 @@ module Types @authorization ||= ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(@authorize) end - def feature_documentation_message(key, description) - message_parts = ["#{description} Available only when feature flag `#{key}` is enabled."] - - message_parts << if Feature::Definition.has_definition?(key) && Feature::Definition.default_enabled?(key) - "This flag is enabled by default." - else - "This flag is disabled by default, because the feature is experimental and is subject to change without notice." - end - - message_parts.join(' ') - end - - def check_feature_flag(args) - ff = args.delete(:_deprecated_feature_flag) - return args unless ff.present? - - args[:description] = feature_documentation_message(ff, args[:description]) - - args - end - def field_complexity(resolver_class, current) return current if current.present? && current > 0 diff --git a/app/graphql/types/branch_protections/base_access_level_type.rb b/app/graphql/types/branch_protections/base_access_level_type.rb new file mode 100644 index 00000000000..472733a6bc5 --- /dev/null +++ b/app/graphql/types/branch_protections/base_access_level_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module BranchProtections + class BaseAccessLevelType < Types::BaseObject + authorize :read_protected_branch + + field :access_level, + type: GraphQL::Types::Int, + null: false, + description: 'GitLab::Access level.' + + field :access_level_description, + type: GraphQL::Types::String, + null: false, + description: 'Human readable representation for this access level.', + hash_key: 'humanize' + end + end +end + +Types::BranchProtections::BaseAccessLevelType.prepend_mod diff --git a/app/graphql/types/branch_protections/merge_access_level_type.rb b/app/graphql/types/branch_protections/merge_access_level_type.rb new file mode 100644 index 00000000000..85295e1ba25 --- /dev/null +++ b/app/graphql/types/branch_protections/merge_access_level_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module BranchProtections + class MergeAccessLevelType < BaseAccessLevelType # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'MergeAccessLevel' + description 'Represents the merge access level of a branch protection.' + accepts ::ProtectedBranch::MergeAccessLevel + end + end +end diff --git a/app/graphql/types/branch_protections/push_access_level_type.rb b/app/graphql/types/branch_protections/push_access_level_type.rb new file mode 100644 index 00000000000..bfbdc4edbea --- /dev/null +++ b/app/graphql/types/branch_protections/push_access_level_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module BranchProtections + class PushAccessLevelType < BaseAccessLevelType # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'PushAccessLevel' + description 'Represents the push access level of a branch protection.' + accepts ::ProtectedBranch::PushAccessLevel + end + end +end diff --git a/app/graphql/types/branch_rules/branch_protection_type.rb b/app/graphql/types/branch_rules/branch_protection_type.rb new file mode 100644 index 00000000000..4177a6f92a1 --- /dev/null +++ b/app/graphql/types/branch_rules/branch_protection_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + module BranchRules + class BranchProtectionType < BaseObject + graphql_name 'BranchProtection' + description 'Branch protection details for a branch rule.' + accepts ::ProtectedBranch + authorize :read_protected_branch + + field :merge_access_levels, + type: Types::BranchProtections::MergeAccessLevelType.connection_type, + null: true, + description: 'Details about who can merge when this branch is the source branch.' + + field :push_access_levels, + type: Types::BranchProtections::PushAccessLevelType.connection_type, + null: true, + description: 'Details about who can push when this branch is the source branch.' + + field :allow_force_push, + type: GraphQL::Types::Boolean, + null: false, + description: 'Toggle force push to the branch for users with write access.' + end + end +end + +Types::BranchRules::BranchProtectionType.prepend_mod_with('Types::BranchRules::BranchProtectionType') diff --git a/app/graphql/types/ci/config_variable_type.rb b/app/graphql/types/ci/config_variable_type.rb new file mode 100644 index 00000000000..87ae026c2c1 --- /dev/null +++ b/app/graphql/types/ci/config_variable_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Types + module Ci + class ConfigVariableType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'CiConfigVariable' + description 'CI/CD config variables.' + + field :key, GraphQL::Types::String, + null: true, + description: 'Name of the variable.' + + field :description, GraphQL::Types::String, + null: true, + description: 'Description for the CI/CD config variable.' + + field :value, GraphQL::Types::String, + null: true, + description: 'Value of the variable.' + end + end +end diff --git a/app/graphql/types/ci/group_variable_connection_type.rb b/app/graphql/types/ci/group_variable_connection_type.rb new file mode 100644 index 00000000000..1f55dde6697 --- /dev/null +++ b/app/graphql/types/ci/group_variable_connection_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class GroupVariableConnectionType < GraphQL::Types::Relay::BaseConnection + field :limit, GraphQL::Types::Int, + null: false, + description: 'Maximum amount of group CI/CD variables.' + + def limit + ::Plan.default.actual_limits.group_ci_variables + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/group_variable_type.rb b/app/graphql/types/ci/group_variable_type.rb index 3322f741342..f9ed54f0d10 100644 --- a/app/graphql/types/ci/group_variable_type.rb +++ b/app/graphql/types/ci/group_variable_type.rb @@ -7,19 +7,20 @@ module Types graphql_name 'CiGroupVariable' description 'CI/CD variables for a group.' + connection_type_class(Types::Ci::GroupVariableConnectionType) implements(VariableInterface) field :environment_scope, GraphQL::Types::String, - null: true, - description: 'Scope defining the environments that can use the variable.' - - field :protected, GraphQL::Types::Boolean, - null: true, - description: 'Indicates whether the variable is protected.' + null: true, + description: 'Scope defining the environments that can use the variable.' field :masked, GraphQL::Types::Boolean, - null: true, - description: 'Indicates whether the variable is masked.' + null: true, + description: 'Indicates whether the variable is masked.' + + field :protected, GraphQL::Types::Boolean, + null: true, + description: 'Indicates whether the variable is protected.' end end end diff --git a/app/graphql/types/ci/instance_variable_type.rb b/app/graphql/types/ci/instance_variable_type.rb index f564a2f59a0..7ffc52deb73 100644 --- a/app/graphql/types/ci/instance_variable_type.rb +++ b/app/graphql/types/ci/instance_variable_type.rb @@ -9,21 +9,29 @@ module Types implements(VariableInterface) + field :id, GraphQL::Types::ID, + null: false, + description: 'ID of the variable.' + field :environment_scope, GraphQL::Types::String, - null: true, - deprecated: { - reason: 'No longer used, only available for GroupVariableType and ProjectVariableType', - milestone: '15.3' - }, - description: 'Scope defining the environments that can use the variable.' + null: true, + deprecated: { + reason: 'No longer used, only available for GroupVariableType and ProjectVariableType', + milestone: '15.3' + }, + description: 'Scope defining the environments that can use the variable.' field :protected, GraphQL::Types::Boolean, - null: true, - description: 'Indicates whether the variable is protected.' + null: true, + description: 'Indicates whether the variable is protected.' field :masked, GraphQL::Types::Boolean, - null: true, - description: 'Indicates whether the variable is masked.' + null: true, + description: 'Indicates whether the variable is masked.' + + field :raw, GraphQL::Types::Boolean, + null: true, + description: 'Indicates whether the variable is raw.' def environment_scope nil diff --git a/app/graphql/types/ci/job_artifact_type.rb b/app/graphql/types/ci/job_artifact_type.rb index a6ab445702c..6346d50de3a 100644 --- a/app/graphql/types/ci/job_artifact_type.rb +++ b/app/graphql/types/ci/job_artifact_type.rb @@ -6,6 +6,9 @@ module Types class JobArtifactType < BaseObject graphql_name 'CiJobArtifact' + field :id, Types::GlobalIDType[::Ci::JobArtifact], null: false, + description: 'ID of the artifact.' + field :download_path, GraphQL::Types::String, null: true, description: "URL for downloading the artifact's file." @@ -16,6 +19,12 @@ module Types description: 'File name of the artifact.', method: :filename + field :size, GraphQL::Types::Int, null: false, + description: 'Size of the artifact in bytes.' + + field :expire_at, Types::TimeType, null: true, + description: 'Expiry date of the artifact.' + def download_path ::Gitlab::Routing.url_helpers.download_project_job_artifacts_path( object.project, diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 4ea9a016e74..ab6103d9469 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -92,6 +92,8 @@ module Types description: 'Indicates the job is stuck.' field :triggered, GraphQL::Types::Boolean, null: true, description: 'Whether the job was triggered.' + field :web_path, GraphQL::Types::String, null: true, + description: 'Web path of the job.' def kind return ::Ci::Build unless [::Ci::Build, ::Ci::Bridge].include?(object.class) @@ -181,6 +183,10 @@ module Types ::Gitlab::Routing.url_helpers.project_commits_path(object.project, ref_name) end + def web_path + ::Gitlab::Routing.url_helpers.project_job_path(object.project, object) + end + def coverage object&.coverage end diff --git a/app/graphql/types/ci/manual_variable_type.rb b/app/graphql/types/ci/manual_variable_type.rb index d6f59c1d249..ed92a6645b4 100644 --- a/app/graphql/types/ci/manual_variable_type.rb +++ b/app/graphql/types/ci/manual_variable_type.rb @@ -10,12 +10,12 @@ module Types implements(VariableInterface) field :environment_scope, GraphQL::Types::String, - null: true, - deprecated: { - reason: 'No longer used, only available for GroupVariableType and ProjectVariableType', - milestone: '15.3' - }, - description: 'Scope defining the environments that can use the variable.' + null: true, + deprecated: { + reason: 'No longer used, only available for GroupVariableType and ProjectVariableType', + milestone: '15.3' + }, + description: 'Scope defining the environments that can use the variable.' def environment_scope nil diff --git a/app/graphql/types/ci/project_variable_connection_type.rb b/app/graphql/types/ci/project_variable_connection_type.rb new file mode 100644 index 00000000000..c3cdc425f10 --- /dev/null +++ b/app/graphql/types/ci/project_variable_connection_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class ProjectVariableConnectionType < GraphQL::Types::Relay::BaseConnection + field :limit, GraphQL::Types::Int, + null: false, + description: 'Maximum amount of project CI/CD variables.' + + def limit + ::Plan.default.actual_limits.project_ci_variables + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/ci/project_variable_type.rb b/app/graphql/types/ci/project_variable_type.rb index 625bb7fd4b1..2a5375045e5 100644 --- a/app/graphql/types/ci/project_variable_type.rb +++ b/app/graphql/types/ci/project_variable_type.rb @@ -7,19 +7,20 @@ module Types graphql_name 'CiProjectVariable' description 'CI/CD variables for a project.' + connection_type_class(Types::Ci::ProjectVariableConnectionType) implements(VariableInterface) field :environment_scope, GraphQL::Types::String, - null: true, - description: 'Scope defining the environments that can use the variable.' + null: true, + description: 'Scope defining the environments that can use the variable.' field :protected, GraphQL::Types::Boolean, - null: true, - description: 'Indicates whether the variable is protected.' + null: true, + description: 'Indicates whether the variable is protected.' field :masked, GraphQL::Types::Boolean, - null: true, - description: 'Indicates whether the variable is masked.' + null: true, + description: 'Indicates whether the variable is masked.' end end end diff --git a/app/graphql/types/ci/runner_membership_filter_enum.rb b/app/graphql/types/ci/runner_membership_filter_enum.rb index 2e1051b2151..4fd7e0749b0 100644 --- a/app/graphql/types/ci/runner_membership_filter_enum.rb +++ b/app/graphql/types/ci/runner_membership_filter_enum.rb @@ -3,15 +3,17 @@ module Types module Ci class RunnerMembershipFilterEnum < BaseEnum - graphql_name 'RunnerMembershipFilter' - description 'Values for filtering runners in namespaces.' + graphql_name 'CiRunnerMembershipFilter' + description 'Values for filtering runners in namespaces. ' \ + 'The previous type name `RunnerMembershipFilter` was deprecated in 15.4.' value 'DIRECT', description: "Include runners that have a direct relationship.", value: :direct value 'DESCENDANTS', - description: "Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried).", + description: "Include runners that have either a direct or inherited relationship. " \ + "These runners can be specific to a project or a group.", value: :descendants end end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index 0afb61d2b64..a9c76974850 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -52,7 +52,7 @@ module Types field :job_count, GraphQL::Types::Int, null: true, description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)." field :jobs, ::Types::Ci::JobType.connection_type, null: true, - description: 'Jobs assigned to the runner.', + description: 'Jobs assigned to the runner. This field can only be resolved for one runner in any single request.', authorize: :read_builds, resolver: ::Resolvers::Ci::RunnerJobsResolver field :locked, GraphQL::Types::Boolean, null: true, @@ -63,8 +63,11 @@ module Types description: 'Indicates the runner is paused and not available to run jobs.' field :project_count, GraphQL::Types::Int, null: true, description: 'Number of projects that the runner is associated with.' - field :projects, ::Types::ProjectType.connection_type, null: true, - description: 'Projects the runner is associated with. For project runners only.' + field :projects, + ::Types::ProjectType.connection_type, + null: true, + resolver: ::Resolvers::Ci::RunnerProjectsResolver, + description: 'Find projects the runner is associated with. For project runners only.' field :revision, GraphQL::Types::String, null: true, description: 'Revision of the runner.' field :run_untagged, GraphQL::Types::Boolean, null: false, @@ -131,12 +134,6 @@ module Types batched_owners(::Ci::RunnerNamespace, Group, :runner_groups, :namespace_id) end - def projects - return unless runner.project_type? - - batched_owners(::Ci::RunnerProject, Project, :runner_projects, :project_id) - end - private def can_admin_runners? @@ -159,19 +156,12 @@ module Types owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq owners = assoc_type.where(id: owner_ids).index_by(&:id) - # Preload projects namespaces to avoid N+1 queries when checking the `read_project` policy for each - preload_projects_namespaces(owners.values) if assoc_type == Project - runner_ids.each do |runner_id| loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || []) end end end # rubocop: enable CodeReuse/ActiveRecord - - def preload_projects_namespaces(_projects) - # overridden in EE - end end end end diff --git a/app/graphql/types/ci/variable_interface.rb b/app/graphql/types/ci/variable_interface.rb index 82c9ba7121c..ec68d3c987c 100644 --- a/app/graphql/types/ci/variable_interface.rb +++ b/app/graphql/types/ci/variable_interface.rb @@ -8,24 +8,24 @@ module Types graphql_name 'CiVariable' field :id, GraphQL::Types::ID, - null: false, - description: 'ID of the variable.' + null: false, + description: 'ID of the variable.' field :key, GraphQL::Types::String, - null: true, - description: 'Name of the variable.' + null: true, + description: 'Name of the variable.' + + field :raw, GraphQL::Types::Boolean, + null: true, + description: 'Indicates whether the variable is raw.' field :value, GraphQL::Types::String, - null: true, - description: 'Value of the variable.' + null: true, + description: 'Value of the variable.' field :variable_type, ::Types::Ci::VariableTypeEnum, - null: true, - description: 'Type of the variable.' - - field :raw, GraphQL::Types::Boolean, - null: true, - description: 'Indicates whether the variable is raw.' + null: true, + description: 'Type of the variable.' end end end diff --git a/app/graphql/types/clusters/agent_type.rb b/app/graphql/types/clusters/agent_type.rb index 546252b2285..5d7b8815cde 100644 --- a/app/graphql/types/clusters/agent_type.rb +++ b/app/graphql/types/clusters/agent_type.rb @@ -71,3 +71,5 @@ module Types end end end + +Types::Clusters::AgentType.prepend_mod diff --git a/app/graphql/types/customer_relations/contact_sort_enum.rb b/app/graphql/types/customer_relations/contact_sort_enum.rb index 221dedacb6a..bb11d741368 100644 --- a/app/graphql/types/customer_relations/contact_sort_enum.rb +++ b/app/graphql/types/customer_relations/contact_sort_enum.rb @@ -11,10 +11,10 @@ module Types sortable_fields.each do |field| value "#{field.upcase.tr(' ', '_')}_ASC", value: { field: field.downcase.tr(' ', '_'), direction: :asc }, - description: "#{field} by ascending order." + description: "#{field} in ascending order." value "#{field.upcase.tr(' ', '_')}_DESC", value: { field: field.downcase.tr(' ', '_'), direction: :desc }, - description: "#{field} by descending order." + description: "#{field} in descending order." end end end diff --git a/app/graphql/types/customer_relations/organization_sort_enum.rb b/app/graphql/types/customer_relations/organization_sort_enum.rb new file mode 100644 index 00000000000..742a5a4fa99 --- /dev/null +++ b/app/graphql/types/customer_relations/organization_sort_enum.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module CustomerRelations + class OrganizationSortEnum < SortEnum + graphql_name 'OrganizationSort' + description 'Values for sorting organizations' + + sortable_fields = ['Name', 'Description', 'Default Rate'] + + sortable_fields.each do |field| + value "#{field.upcase.tr(' ', '_')}_ASC", + value: { field: field.downcase.tr(' ', '_'), direction: :asc }, + description: "#{field} in ascending order." + value "#{field.upcase.tr(' ', '_')}_DESC", + value: { field: field.downcase.tr(' ', '_'), direction: :desc }, + description: "#{field} in descending order." + end + end + end +end diff --git a/app/graphql/types/customer_relations/organization_state_counts_type.rb b/app/graphql/types/customer_relations/organization_state_counts_type.rb new file mode 100644 index 00000000000..7d813209a8e --- /dev/null +++ b/app/graphql/types/customer_relations/organization_state_counts_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + module CustomerRelations + # `object` is a hash. Authorization is performed by OrganizationStateCountsResolver + class OrganizationStateCountsType < Types::BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'OrganizationStateCounts' + description 'Represents the total number of organizations for the represented states.' + + AVAILABLE_STATES = ::CustomerRelations::Organization.states.keys.push('all').freeze + + AVAILABLE_STATES.each do |state| + field state, + GraphQL::Types::Int, + null: true, + description: "Number of organizations with state `#{state.upcase}`" + end + + def all + object.values.sum + end + end + end +end diff --git a/app/graphql/types/customer_relations/organization_state_enum.rb b/app/graphql/types/customer_relations/organization_state_enum.rb index ecdd7d092ad..84bbbbc90fc 100644 --- a/app/graphql/types/customer_relations/organization_state_enum.rb +++ b/app/graphql/types/customer_relations/organization_state_enum.rb @@ -5,12 +5,16 @@ module Types class OrganizationStateEnum < BaseEnum graphql_name 'CustomerRelationsOrganizationState' + value 'all', + description: "All available organizations.", + value: :all + value 'active', - description: "Active organization.", + description: "Active organizations.", value: :active value 'inactive', - description: "Inactive organization.", + description: "Inactive organizations.", value: :inactive end end diff --git a/app/graphql/types/deployment_details_type.rb b/app/graphql/types/deployment_details_type.rb new file mode 100644 index 00000000000..f8ba0cb1b24 --- /dev/null +++ b/app/graphql/types/deployment_details_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + class DeploymentDetailsType < DeploymentType + graphql_name 'DeploymentDetails' + description 'The details of the deployment' + authorize :read_deployment + present_using Deployments::DeploymentPresenter + + field :tags, + [Types::DeploymentTagType], + description: 'Git tags that contain this deployment.', + calls_gitaly: true + end +end diff --git a/app/graphql/types/deployment_status_enum.rb b/app/graphql/types/deployment_status_enum.rb new file mode 100644 index 00000000000..7ef69d3f1c1 --- /dev/null +++ b/app/graphql/types/deployment_status_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class DeploymentStatusEnum < BaseEnum + graphql_name 'DeploymentStatus' + description 'All deployment statuses.' + + ::Deployment.statuses.each_key do |status| + value status.upcase, + description: "A deployment that is #{status.tr('_', ' ')}.", + value: status + end + end +end diff --git a/app/graphql/types/deployment_tag_type.rb b/app/graphql/types/deployment_tag_type.rb new file mode 100644 index 00000000000..bc3597404a2 --- /dev/null +++ b/app/graphql/types/deployment_tag_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + # DeploymentTagType is a hash, authorized by the deployment + # rubocop:disable Graphql/AuthorizeTypes + class DeploymentTagType < BaseObject + graphql_name 'DeploymentTag' + description 'Tags for a given deployment' + + field :name, + GraphQL::Types::String, + description: 'Name of this git tag.' + + field :path, + GraphQL::Types::String, + description: 'Path for this tag.', + hash_key: :path + end + # rubocop:enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb new file mode 100644 index 00000000000..70a3a4cb574 --- /dev/null +++ b/app/graphql/types/deployment_type.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Types + # If you're considering to add a new field in DeploymentType, please follow this guideline: + # - If the field is preloadable in batch, define it in DeploymentType. + # In this case, you should extend DeploymentsResolver logic to preload the field. Also, add a new test that + # fetching the specific field for multiple deployments doesn't cause N+1 query problem. + # - If the field is NOT preloadable in batch, define it in DeploymentDetailsType. + # This type can be only fetched for a single deployment, so you don't need to take care of the preloading. + class DeploymentType < BaseObject + graphql_name 'Deployment' + description 'The deployment of an environment' + + present_using Deployments::DeploymentPresenter + + authorize :read_deployment + + field :id, + GraphQL::Types::ID, + description: 'Global ID of the deployment.' + + field :iid, + GraphQL::Types::ID, + description: 'Project-level internal ID of the deployment.' + + field :ref, + GraphQL::Types::String, + description: 'Git-Ref that the deployment ran on.' + + field :tag, + GraphQL::Types::Boolean, + description: 'True or false if the deployment ran on a Git-tag.' + + field :sha, + GraphQL::Types::String, + description: 'Git-SHA that the deployment ran on.' + + field :created_at, + Types::TimeType, + description: 'When the deployment record was created.' + + field :updated_at, + Types::TimeType, + description: 'When the deployment record was updated.' + + field :finished_at, + Types::TimeType, + description: 'When the deployment finished.' + + field :status, + Types::DeploymentStatusEnum, + description: 'Status of the deployment.' + + field :commit, + Types::CommitType, + description: 'Commit details of the deployment.', + calls_gitaly: true + + field :job, + Types::Ci::JobType, + description: 'Pipeline job of the deployment.', + method: :build + + field :triggerer, + Types::UserType, + description: 'User who executed the deployment.', + method: :deployed_by + end +end diff --git a/app/graphql/types/deployments_order_by_input_type.rb b/app/graphql/types/deployments_order_by_input_type.rb new file mode 100644 index 00000000000..a87fef9fe8a --- /dev/null +++ b/app/graphql/types/deployments_order_by_input_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Types + class DeploymentsOrderByInputType < BaseInputObject + graphql_name 'DeploymentsOrderByInput' + description 'Values for ordering deployments by a specific field' + + argument :created_at, + Types::SortDirectionEnum, + required: false, + description: 'Order by Created time.' + + argument :finished_at, + Types::SortDirectionEnum, + required: false, + description: 'Order by Finished time.' + + def prepare + raise GraphQL::ExecutionError, 'orderBy parameter must contain one key-value pair.' unless to_h.size == 1 + + super + end + end +end diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb index 2a7076cc3c9..eb4e7b1dabf 100644 --- a/app/graphql/types/environment_type.rb +++ b/app/graphql/types/environment_type.rb @@ -21,6 +21,30 @@ module Types field :path, GraphQL::Types::String, null: false, description: 'Path to the environment.' + field :slug, GraphQL::Types::String, + description: 'Slug of the environment.' + + field :external_url, GraphQL::Types::String, null: true, + description: 'External URL of the environment.' + + field :created_at, Types::TimeType, + description: 'When the environment was created.' + + field :updated_at, Types::TimeType, + description: 'When the environment was updated.' + + field :auto_stop_at, Types::TimeType, + description: 'When the environment is going to be stopped automatically.' + + field :auto_delete_at, Types::TimeType, + description: 'When the environment is going to be deleted automatically.' + + field :tier, Types::DeploymentTierEnum, + description: 'Deployment tier of the environment.' + + field :environment_type, GraphQL::Types::String, + description: 'Folder name of the environment.' + field :metrics_dashboard, Types::Metrics::DashboardType, null: true, description: 'Metrics dashboard schema for the environment.', resolver: Resolvers::Metrics::DashboardResolver @@ -29,5 +53,22 @@ module Types Types::AlertManagement::AlertType, null: true, description: 'Most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.' + + field :deployments, + Types::DeploymentType.connection_type, + null: true, + description: 'Deployments of the environment. This field can only be resolved for one project in any single request.', + resolver: Resolvers::DeploymentsResolver do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + + field :last_deployment, + Types::DeploymentType, + description: 'Last deployment of the environment.', + resolver: Resolvers::Environments::LastDeploymentResolver + + def tier + object.tier.to_sym + end end end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 235a2bc2a34..45357de5502 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -22,7 +22,7 @@ module Types type: Types::CustomEmojiType.connection_type, null: true, description: 'Custom emoji within this namespace.', - _deprecated_feature_flag: :custom_emoji + alpha: { milestone: '13.6' } field :share_with_group_lock, type: GraphQL::Types::Boolean, @@ -134,7 +134,7 @@ module Types description: 'Number of container repositories in the group.' field :packages, - description: 'Packages of the group.', + description: 'Packages of the group. This field can only be resolved for one group in any single request.', resolver: Resolvers::GroupPackagesResolver field :dependency_proxy_setting, @@ -212,6 +212,12 @@ module Types description: "Find organizations of this group.", resolver: Resolvers::Crm::OrganizationsResolver + field :organization_state_counts, + Types::CustomerRelations::OrganizationStateCountsType, + null: true, + description: 'Counts of organizations by status for the group.', + resolver: Resolvers::Crm::OrganizationStateCountsResolver + field :contacts, Types::CustomerRelations::ContactType.connection_type, null: true, description: "Find contacts of this group.", @@ -272,6 +278,10 @@ module Types group.dependency_proxy_setting || group.create_dependency_proxy_setting end + def custom_emoji + object.custom_emoji if Feature.enabled?(:custom_emoji) + end + private def group diff --git a/app/graphql/types/member_sort_enum.rb b/app/graphql/types/member_sort_enum.rb new file mode 100644 index 00000000000..f3291dda13b --- /dev/null +++ b/app/graphql/types/member_sort_enum.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + class MemberSortEnum < SortEnum + graphql_name 'MemberSort' + description 'Values for sorting members' + + value 'ACCESS_LEVEL_ASC', 'Access level ascending order.', value: :access_level_asc + value 'ACCESS_LEVEL_DESC', 'Access level descending order.', value: :access_level_desc + value 'USER_FULL_NAME_ASC', "User's full name ascending order.", value: :name_asc + value 'USER_FULL_NAME_DESC', "User's full name descending order.", value: :name_desc + end +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index d88653f2f8c..399dcc8e03d 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -94,9 +94,10 @@ module Types method: :public_merge_status, null: true, description: 'Merge status of the merge request.' - field :detailed_merge_status, ::Types::MergeRequests::DetailedMergeStatusEnum, method: :detailed_merge_status, null: true, + field :detailed_merge_status, ::Types::MergeRequests::DetailedMergeStatusEnum, null: true, calls_gitaly: true, - description: 'Detailed merge status of the merge request.', alpha: { milestone: '15.3' } + description: 'Detailed merge status of the merge request.', + alpha: { milestone: '15.3' } field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true, calls_gitaly: true, @@ -280,6 +281,10 @@ module Types def merge_user object.metrics&.merged_by || object.merge_user end + + def detailed_merge_status + ::MergeRequests::Mergeability::DetailedMergeStatusService.new(merge_request: object).execute + end end end diff --git a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb index 58104159303..3de6296154d 100644 --- a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb +++ b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb @@ -21,6 +21,9 @@ module Types value 'CI_MUST_PASS', value: :ci_must_pass, description: 'Pipeline must succeed before merging.' + value 'CI_STILL_RUNNING', + value: :ci_still_running, + description: 'Pipeline is still running.' value 'DISCUSSIONS_NOT_RESOLVED', value: :discussions_not_resolved, description: 'Discussions must be resolved before merging.' diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 499c2e786bf..ea833b35085 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -37,8 +37,8 @@ module Types mount_mutation Mutations::Clusters::AgentTokens::Create mount_mutation Mutations::Clusters::AgentTokens::Revoke mount_mutation Mutations::Commits::Create, calls_gitaly: true - mount_mutation Mutations::CustomEmoji::Create, _deprecated_feature_flag: :custom_emoji - mount_mutation Mutations::CustomEmoji::Destroy, _deprecated_feature_flag: :custom_emoji + mount_mutation Mutations::CustomEmoji::Create, alpha: { milestone: '13.6' } + mount_mutation Mutations::CustomEmoji::Destroy, alpha: { milestone: '13.6' } mount_mutation Mutations::CustomerRelations::Contacts::Create mount_mutation Mutations::CustomerRelations::Contacts::Update mount_mutation Mutations::CustomerRelations::Organizations::Create @@ -120,10 +120,12 @@ module Types milestone: '15.0' } mount_mutation Mutations::Ci::ProjectCiCdSettingsUpdate + mount_mutation Mutations::Ci::Job::ArtifactsDestroy mount_mutation Mutations::Ci::Job::Play mount_mutation Mutations::Ci::Job::Retry mount_mutation Mutations::Ci::Job::Cancel mount_mutation Mutations::Ci::Job::Unschedule + mount_mutation Mutations::Ci::JobArtifact::Destroy mount_mutation Mutations::Ci::JobTokenScope::AddProject mount_mutation Mutations::Ci::JobTokenScope::RemoveProject mount_mutation Mutations::Ci::Runner::Update diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb index 0413177ef14..6c0d955ed77 100644 --- a/app/graphql/types/packages/package_details_type.rb +++ b/app/graphql/types/packages/package_details_type.rb @@ -26,6 +26,8 @@ module Types field :pypi_setup_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project setup endpoint.' field :pypi_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project endpoint.' + field :last_downloaded_at, Types::TimeType, null: true, description: 'Last time that a file of this package was downloaded.' + def versions object.versions end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index ecc6c9d7811..f43f5c27dac 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -10,119 +10,204 @@ module Types expose_permissions Types::PermissionTypes::Project - field :id, GraphQL::Types::ID, null: false, - description: 'ID of the project.' - - field :ci_config_path_or_default, GraphQL::Types::String, null: false, - description: 'Path of the CI configuration file.' - field :full_path, GraphQL::Types::ID, null: false, - description: 'Full path of the project.' - field :path, GraphQL::Types::String, null: false, - description: 'Path of the project.' - - field :sast_ci_configuration, Types::CiConfiguration::Sast::Type, null: true, - calls_gitaly: true, - description: 'SAST CI configuration for the project.' - - field :name, GraphQL::Types::String, null: false, - description: 'Name of the project (without namespace).' - field :name_with_namespace, GraphQL::Types::String, null: false, - description: 'Full name of the project with its namespace.' - - field :description, GraphQL::Types::String, null: true, - description: 'Short description of the project.' - - field :tag_list, GraphQL::Types::String, null: true, - deprecated: { reason: 'Use `topics`', milestone: '13.12' }, - description: 'List of project topics (not Git tags).', method: :topic_list - - field :topics, [GraphQL::Types::String], null: true, - description: 'List of project topics.', method: :topic_list - - field :http_url_to_repo, GraphQL::Types::String, null: true, - description: 'URL to connect to the project via HTTPS.' - field :ssh_url_to_repo, GraphQL::Types::String, null: true, - description: 'URL to connect to the project via SSH.' - field :web_url, GraphQL::Types::String, null: true, - description: 'Web URL of the project.' - - field :forks_count, GraphQL::Types::Int, null: false, calls_gitaly: true, # 4 times - description: 'Number of times the project has been forked.' - field :star_count, GraphQL::Types::Int, null: false, - description: 'Number of times the project has been starred.' - - field :created_at, Types::TimeType, null: true, - description: 'Timestamp of the project creation.' - field :last_activity_at, Types::TimeType, null: true, - description: 'Timestamp of the project last activity.' - - field :archived, GraphQL::Types::Boolean, null: true, - description: 'Indicates the archived status of the project.' - - field :visibility, GraphQL::Types::String, null: true, - description: 'Visibility of the project.' - - field :lfs_enabled, GraphQL::Types::Boolean, null: true, - description: 'Indicates if the project has Large File Storage (LFS) enabled.' - field :merge_requests_ff_only_enabled, GraphQL::Types::Boolean, null: true, - description: 'Indicates if no merge commits should be created and all merges should instead be fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.' - field :shared_runners_enabled, GraphQL::Types::Boolean, null: true, - description: 'Indicates if shared runners are enabled for the project.' - - field :service_desk_enabled, GraphQL::Types::Boolean, null: true, - description: 'Indicates if the project has Service Desk enabled.' - - field :service_desk_address, GraphQL::Types::String, null: true, - description: 'E-mail address of the Service Desk.' - - field :avatar_url, GraphQL::Types::String, null: true, calls_gitaly: true, - description: 'URL to avatar image file of the project.' - - field :jobs_enabled, GraphQL::Types::Boolean, null: true, - description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.' - - field :public_jobs, GraphQL::Types::Boolean, method: :public_builds, null: true, - description: 'Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts.' - - field :open_issues_count, GraphQL::Types::Int, null: true, - description: 'Number of open issues for the project.' - - field :allow_merge_on_skipped_pipeline, GraphQL::Types::Boolean, null: true, - description: 'If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs.' - field :autoclose_referenced_issues, GraphQL::Types::Boolean, null: true, - description: 'Indicates if issues referenced by merge requests and commits within the default branch are closed automatically.' - field :import_status, GraphQL::Types::String, null: true, - description: 'Status of import background job of the project.' - field :jira_import_status, GraphQL::Types::String, null: true, - description: 'Status of Jira import background job of the project.' - field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::Types::Boolean, null: true, - description: 'Indicates if merge requests of the project can only be merged when all the discussions are resolved.' - field :only_allow_merge_if_pipeline_succeeds, GraphQL::Types::Boolean, null: true, - description: 'Indicates if merge requests of the project can only be merged with successful jobs.' - field :printing_merge_request_link_enabled, GraphQL::Types::Boolean, null: true, - description: 'Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line.' - field :remove_source_branch_after_merge, GraphQL::Types::Boolean, null: true, - description: 'Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project.' - field :request_access_enabled, GraphQL::Types::Boolean, null: true, - description: 'Indicates if users can request member access to the project.' - field :squash_read_only, GraphQL::Types::Boolean, null: false, method: :squash_readonly?, - description: 'Indicates if `squashReadOnly` is enabled.' - field :suggestion_commit_message, GraphQL::Types::String, null: true, - description: 'Commit message used to apply merge request suggestions.' + field :id, GraphQL::Types::ID, + null: false, + description: 'ID of the project.' + + field :ci_config_path_or_default, GraphQL::Types::String, + null: false, + description: 'Path of the CI configuration file.' + + field :ci_config_variables, [Types::Ci::ConfigVariableType], + null: true, + calls_gitaly: true, + authorize: :create_pipeline, + alpha: { milestone: '15.3' }, + description: 'CI/CD config variable.' do + argument :sha, GraphQL::Types::String, + required: true, + description: 'Sha.' + end + + field :full_path, GraphQL::Types::ID, + null: false, + description: 'Full path of the project.' + + field :path, GraphQL::Types::String, + null: false, + description: 'Path of the project.' + + field :sast_ci_configuration, Types::CiConfiguration::Sast::Type, + null: true, + calls_gitaly: true, + description: 'SAST CI configuration for the project.' + + field :name, GraphQL::Types::String, + null: false, + description: 'Name of the project (without namespace).' + + field :name_with_namespace, GraphQL::Types::String, + null: false, + description: 'Full name of the project with its namespace.' + + field :description, GraphQL::Types::String, + null: true, + description: 'Short description of the project.' + + field :tag_list, GraphQL::Types::String, + null: true, + deprecated: { reason: 'Use `topics`', milestone: '13.12' }, + description: 'List of project topics (not Git tags).', + method: :topic_list + + field :topics, [GraphQL::Types::String], + null: true, + description: 'List of project topics.', + method: :topic_list + + field :http_url_to_repo, GraphQL::Types::String, + null: true, + description: 'URL to connect to the project via HTTPS.' + + field :ssh_url_to_repo, GraphQL::Types::String, + null: true, + description: 'URL to connect to the project via SSH.' + + field :web_url, GraphQL::Types::String, + null: true, + description: 'Web URL of the project.' + + field :forks_count, GraphQL::Types::Int, + null: false, + calls_gitaly: true, # 4 times + description: 'Number of times the project has been forked.' + + field :star_count, GraphQL::Types::Int, + null: false, + description: 'Number of times the project has been starred.' + + field :created_at, Types::TimeType, + null: true, + description: 'Timestamp of the project creation.' + + field :last_activity_at, Types::TimeType, + null: true, + description: 'Timestamp of the project last activity.' + + field :archived, GraphQL::Types::Boolean, + null: true, + description: 'Indicates the archived status of the project.' + + field :visibility, GraphQL::Types::String, + null: true, + description: 'Visibility of the project.' + + field :lfs_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if the project has Large File Storage (LFS) enabled.' + + field :merge_requests_ff_only_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if no merge commits should be created and all merges should instead be ' \ + 'fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.' + + field :shared_runners_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if shared runners are enabled for the project.' + + field :service_desk_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if the project has Service Desk enabled.' + + field :service_desk_address, GraphQL::Types::String, + null: true, + description: 'E-mail address of the Service Desk.' + + field :avatar_url, GraphQL::Types::String, + null: true, + calls_gitaly: true, + description: 'URL to avatar image file of the project.' + + field :jobs_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.' + + field :public_jobs, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if there is public access to pipelines and job details of the project, ' \ + 'including output logs and artifacts.', + method: :public_builds + + field :open_issues_count, GraphQL::Types::Int, + null: true, + description: 'Number of open issues for the project.' + + field :allow_merge_on_skipped_pipeline, GraphQL::Types::Boolean, + null: true, + description: 'If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of ' \ + 'the project can also be merged with skipped jobs.' + + field :autoclose_referenced_issues, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if issues referenced by merge requests and commits within the default branch ' \ + 'are closed automatically.' + + field :import_status, GraphQL::Types::String, + null: true, + description: 'Status of import background job of the project.' + + field :jira_import_status, GraphQL::Types::String, + null: true, + description: 'Status of Jira import background job of the project.' + + field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if merge requests of the project can only be merged when all the discussions are resolved.' + + field :only_allow_merge_if_pipeline_succeeds, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if merge requests of the project can only be merged with successful jobs.' + + field :printing_merge_request_link_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if a link to create or view a merge request should display after a push to Git ' \ + 'repositories of the project from the command line.' + + field :remove_source_branch_after_merge, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if `Delete source branch` option should be enabled by default for all ' \ + 'new merge requests of the project.' + + field :request_access_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Indicates if users can request member access to the project.' + + field :squash_read_only, GraphQL::Types::Boolean, + null: false, + description: 'Indicates if `squashReadOnly` is enabled.', + method: :squash_readonly? + + field :suggestion_commit_message, GraphQL::Types::String, + null: true, + description: 'Commit message used to apply merge request suggestions.' # No, the quotes are not a typo. Used to get around circular dependencies. # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536#note_871009675 - field :group, 'Types::GroupType', null: true, - description: 'Group of the project.' - field :namespace, Types::NamespaceType, null: true, - description: 'Namespace of the project.' + field :group, 'Types::GroupType', + null: true, + description: 'Group of the project.' + + field :namespace, Types::NamespaceType, + null: true, + description: 'Namespace of the project.' field :statistics, Types::ProjectStatisticsType, null: true, description: 'Statistics of the project.' - field :repository, Types::RepositoryType, null: true, - description: 'Git repository of the project.' + field :repository, Types::RepositoryType, + null: true, + description: 'Git repository of the project.' field :merge_requests, Types::MergeRequestType.connection_type, @@ -159,9 +244,10 @@ module Types extras: [:lookahead], resolver: Resolvers::IssueStatusCountsResolver - field :milestones, Types::MilestoneType.connection_type, null: true, - description: 'Milestones of the project.', - resolver: Resolvers::ProjectMilestonesResolver + field :milestones, Types::MilestoneType.connection_type, + null: true, + description: 'Milestones of the project.', + resolver: Resolvers::ProjectMilestonesResolver field :project_members, description: 'Members of the project.', @@ -179,6 +265,12 @@ module Types description: 'A single environment of the project.', resolver: Resolvers::EnvironmentsResolver.single + field :deployment, + Types::DeploymentDetailsType, + null: true, + description: 'Details of the deployment of the project.', + resolver: Resolvers::DeploymentResolver.single + field :issue, Types::IssueType, null: true, @@ -201,164 +293,150 @@ module Types description: 'Jobs of a project. This field can only be resolved for one project in any single request.', resolver: Resolvers::ProjectJobsResolver + field :job, + type: Types::Ci::JobType, + null: true, + authorize: :read_build, + description: 'One job belonging to the project, selected by ID.' do + argument :id, Types::GlobalIDType[::CommitStatus], + required: true, + description: 'ID of the job.' + end + field :pipelines, null: true, description: 'Build pipelines of the project.', extras: [:lookahead], resolver: Resolvers::ProjectPipelinesResolver - field :pipeline, - Types::Ci::PipelineType, + field :pipeline, Types::Ci::PipelineType, null: true, description: 'Build pipeline of the project.', extras: [:lookahead], resolver: Resolvers::ProjectPipelineResolver - field :pipeline_counts, - Types::Ci::PipelineCountsType, + field :pipeline_counts, Types::Ci::PipelineCountsType, null: true, description: 'Build pipeline counts of the project.', resolver: Resolvers::Ci::ProjectPipelineCountsResolver - field :ci_variables, - Types::Ci::ProjectVariableType.connection_type, + field :ci_variables, Types::Ci::ProjectVariableType.connection_type, null: true, description: "List of the project's CI/CD variables.", authorize: :admin_build, method: :variables - field :ci_cd_settings, - Types::Ci::CiCdSettingType, + field :ci_cd_settings, Types::Ci::CiCdSettingType, null: true, description: 'CI/CD settings for the project.' - field :sentry_detailed_error, - Types::ErrorTracking::SentryDetailedErrorType, + field :sentry_detailed_error, Types::ErrorTracking::SentryDetailedErrorType, null: true, description: 'Detailed version of a Sentry error on the project.', resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver - field :grafana_integration, - Types::GrafanaIntegrationType, + field :grafana_integration, Types::GrafanaIntegrationType, null: true, description: 'Grafana integration details for the project.', resolver: Resolvers::Projects::GrafanaIntegrationResolver - field :snippets, - Types::SnippetType.connection_type, + field :snippets, Types::SnippetType.connection_type, null: true, description: 'Snippets of the project.', resolver: Resolvers::Projects::SnippetsResolver - field :sentry_errors, - Types::ErrorTracking::SentryErrorCollectionType, + field :sentry_errors, Types::ErrorTracking::SentryErrorCollectionType, null: true, description: 'Paginated collection of Sentry errors on the project.', resolver: Resolvers::ErrorTracking::SentryErrorCollectionResolver - field :boards, - Types::BoardType.connection_type, + field :boards, Types::BoardType.connection_type, null: true, description: 'Boards of the project.', max_page_size: 2000, resolver: Resolvers::BoardsResolver - field :recent_issue_boards, - Types::BoardType.connection_type, + field :recent_issue_boards, Types::BoardType.connection_type, null: true, description: 'List of recently visited boards of the project. Maximum size is 4.', resolver: Resolvers::RecentBoardsResolver - field :board, - Types::BoardType, + field :board, Types::BoardType, null: true, description: 'A single board of the project.', resolver: Resolvers::BoardResolver - field :jira_imports, - Types::JiraImportType.connection_type, + field :jira_imports, Types::JiraImportType.connection_type, null: true, description: 'Jira imports into the project.' - field :services, - Types::Projects::ServiceType.connection_type, + field :services, Types::Projects::ServiceType.connection_type, null: true, description: 'Project services.', resolver: Resolvers::Projects::ServicesResolver - field :alert_management_alerts, - Types::AlertManagement::AlertType.connection_type, + field :alert_management_alerts, Types::AlertManagement::AlertType.connection_type, null: true, description: 'Alert Management alerts of the project.', extras: [:lookahead], resolver: Resolvers::AlertManagement::AlertResolver - field :alert_management_alert, - Types::AlertManagement::AlertType, + field :alert_management_alert, Types::AlertManagement::AlertType, null: true, description: 'A single Alert Management alert of the project.', resolver: Resolvers::AlertManagement::AlertResolver.single - field :alert_management_alert_status_counts, - Types::AlertManagement::AlertStatusCountsType, + field :alert_management_alert_status_counts, Types::AlertManagement::AlertStatusCountsType, null: true, description: 'Counts of alerts by status for the project.', resolver: Resolvers::AlertManagement::AlertStatusCountsResolver - field :alert_management_integrations, - Types::AlertManagement::IntegrationType.connection_type, + field :alert_management_integrations, Types::AlertManagement::IntegrationType.connection_type, null: true, description: 'Integrations which can receive alerts for the project.', resolver: Resolvers::AlertManagement::IntegrationsResolver - field :alert_management_http_integrations, - Types::AlertManagement::HttpIntegrationType.connection_type, + field :alert_management_http_integrations, Types::AlertManagement::HttpIntegrationType.connection_type, null: true, description: 'HTTP Integrations which can receive alerts for the project.', resolver: Resolvers::AlertManagement::HttpIntegrationsResolver - field :incident_management_timeline_events, - Types::IncidentManagement::TimelineEventType.connection_type, + field :incident_management_timeline_events, Types::IncidentManagement::TimelineEventType.connection_type, null: true, description: 'Incident Management Timeline events associated with the incident.', extras: [:lookahead], resolver: Resolvers::IncidentManagement::TimelineEventsResolver - field :incident_management_timeline_event, - Types::IncidentManagement::TimelineEventType, + field :incident_management_timeline_event, Types::IncidentManagement::TimelineEventType, null: true, description: 'Incident Management Timeline event associated with the incident.', resolver: Resolvers::IncidentManagement::TimelineEventsResolver.single - field :releases, - Types::ReleaseType.connection_type, + field :releases, Types::ReleaseType.connection_type, null: true, description: 'Releases of the project.', resolver: Resolvers::ReleasesResolver - field :release, - Types::ReleaseType, + field :release, Types::ReleaseType, null: true, description: 'A single release of the project.', resolver: Resolvers::ReleasesResolver.single, authorize: :read_release - field :container_expiration_policy, - Types::ContainerExpirationPolicyType, + field :container_expiration_policy, Types::ContainerExpirationPolicyType, null: true, description: 'Container expiration policy of the project.' - field :container_repositories, - Types::ContainerRepositoryType.connection_type, + field :container_repositories, Types::ContainerRepositoryType.connection_type, null: true, description: 'Container repositories of the project.', resolver: Resolvers::ContainerRepositoriesResolver - field :container_repositories_count, GraphQL::Types::Int, null: false, - description: 'Number of container repositories in the project.' + field :container_repositories_count, GraphQL::Types::Int, + null: false, + description: 'Number of container repositories in the project.' - field :label, - Types::LabelType, + field :label, Types::LabelType, null: true, description: 'Label available on this project.' do argument :title, GraphQL::Types::String, @@ -366,68 +444,63 @@ module Types description: 'Title of the label.' end - field :terraform_state, - Types::Terraform::StateType, + field :terraform_state, Types::Terraform::StateType, null: true, description: 'Find a single Terraform state by name.', resolver: Resolvers::Terraform::StatesResolver.single - field :terraform_states, - Types::Terraform::StateType.connection_type, + field :terraform_states, Types::Terraform::StateType.connection_type, null: true, description: 'Terraform states associated with the project.', resolver: Resolvers::Terraform::StatesResolver - field :pipeline_analytics, Types::Ci::AnalyticsType, null: true, - description: 'Pipeline analytics.', - resolver: Resolvers::ProjectPipelineStatisticsResolver + field :pipeline_analytics, Types::Ci::AnalyticsType, + null: true, + description: 'Pipeline analytics.', + resolver: Resolvers::ProjectPipelineStatisticsResolver - field :ci_template, Types::Ci::TemplateType, null: true, - description: 'Find a single CI/CD template by name.', - resolver: Resolvers::Ci::TemplateResolver + field :ci_template, Types::Ci::TemplateType, + null: true, + description: 'Find a single CI/CD template by name.', + resolver: Resolvers::Ci::TemplateResolver - field :ci_job_token_scope, Types::Ci::JobTokenScopeType, null: true, - description: 'The CI Job Tokens scope of access.', - resolver: Resolvers::Ci::JobTokenScopeResolver + field :ci_job_token_scope, Types::Ci::JobTokenScopeType, + null: true, + description: 'The CI Job Tokens scope of access.', + resolver: Resolvers::Ci::JobTokenScopeResolver - field :timelogs, - Types::TimelogType.connection_type, null: true, - description: 'Time logged on issues and merge requests in the project.', - extras: [:lookahead], - complexity: 5, - resolver: ::Resolvers::TimelogResolver + field :timelogs, Types::TimelogType.connection_type, + null: true, + description: 'Time logged on issues and merge requests in the project.', + extras: [:lookahead], + complexity: 5, + resolver: ::Resolvers::TimelogResolver - field :agent_configurations, - ::Types::Kas::AgentConfigurationType.connection_type, + field :agent_configurations, ::Types::Kas::AgentConfigurationType.connection_type, null: true, description: 'Agent configurations defined by the project', resolver: ::Resolvers::Kas::AgentConfigurationsResolver - field :cluster_agent, - ::Types::Clusters::AgentType, + field :cluster_agent, ::Types::Clusters::AgentType, null: true, description: 'Find a single cluster agent by name.', resolver: ::Resolvers::Clusters::AgentsResolver.single - field :cluster_agents, - ::Types::Clusters::AgentType.connection_type, + field :cluster_agents, ::Types::Clusters::AgentType.connection_type, extras: [:lookahead], null: true, description: 'Cluster agents associated with the project.', resolver: ::Resolvers::Clusters::AgentsResolver - field :merge_commit_template, - GraphQL::Types::String, + field :merge_commit_template, GraphQL::Types::String, null: true, description: 'Template used to create merge commit message in merge requests.' - field :squash_commit_template, - GraphQL::Types::String, + field :squash_commit_template, GraphQL::Types::String, null: true, description: 'Template used to create squash commit message in merge requests.' - field :labels, - Types::LabelType.connection_type, + field :labels, Types::LabelType.connection_type, null: true, description: 'Labels available on this project.', resolver: Resolvers::LabelsResolver @@ -438,8 +511,7 @@ module Types ' Returns `null` if `work_items` feature flag is disabled.' \ ' This flag is disabled by default, because the feature is experimental and is subject to change without notice.' - field :timelog_categories, - Types::TimeTracking::TimelogCategoryType.connection_type, + field :timelog_categories, Types::TimeTracking::TimelogCategoryType.connection_type, null: true, description: "Timelog categories for the project.", alpha: { milestone: '15.3' } @@ -448,6 +520,12 @@ module Types resolver: Resolvers::Projects::ForkTargetsResolver, description: 'Namespaces in which the current user can fork the project into.' + field :branch_rules, + Types::Projects::BranchRuleType.connection_type, + null: true, + description: "Branch rules configured for the project.", + resolver: Resolvers::Projects::BranchRulesResolver + def timelog_categories object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories) end @@ -498,6 +576,21 @@ module Types project.container_repositories.size end + def ci_config_variables(sha:) + result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(sha) + + return if result.nil? + + result.map do |var_key, var_config| + { key: var_key, **var_config } + end + end + + def job(id:) + object.commit_statuses.find(id.model_id) + rescue ActiveRecord::RecordNotFound + end + def sast_ci_configuration return unless Ability.allowed?(current_user, :download_code, object) diff --git a/app/graphql/types/projects/branch_rule_type.rb b/app/graphql/types/projects/branch_rule_type.rb new file mode 100644 index 00000000000..866cff0f439 --- /dev/null +++ b/app/graphql/types/projects/branch_rule_type.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Types + module Projects + class BranchRuleType < BaseObject + graphql_name 'BranchRule' + description 'List of branch rules for a project, grouped by branch name.' + accepts ::ProtectedBranch + authorize :read_protected_branch + + field :name, + type: GraphQL::Types::String, + null: false, + description: 'Branch name, with wildcards, for the branch rules.' + + field :branch_protection, + type: Types::BranchRules::BranchProtectionType, + null: false, + description: 'Branch protections configured for this branch rule.', + method: :itself + + field :created_at, + Types::TimeType, + null: false, + description: 'Timestamp of when the branch rule was created.' + + field :updated_at, + Types::TimeType, + null: false, + description: 'Timestamp of when the branch rule was last updated.' + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 84355390ea0..78463a1804a 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -67,7 +67,7 @@ module Types end field :package, - description: 'Find a package.', + description: 'Find a package. This field can only be resolved for one query in any single request.', resolver: Resolvers::PackageDetailsResolver field :user, Types::UserType, diff --git a/app/graphql/types/sort_direction_enum.rb b/app/graphql/types/sort_direction_enum.rb new file mode 100644 index 00000000000..28dba1abfb6 --- /dev/null +++ b/app/graphql/types/sort_direction_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class SortDirectionEnum < BaseEnum + graphql_name 'SortDirectionEnum' + description 'Values for sort direction' + + value 'ASC', 'Ascending order.', value: 'asc' + value 'DESC', 'Descending order.', value: 'desc' + end +end diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb index 9b5f028a857..ef701bbfc10 100644 --- a/app/graphql/types/subscription_type.rb +++ b/app/graphql/types/subscription_type.rb @@ -18,5 +18,12 @@ module Types field :issuable_dates_updated, subscription: Subscriptions::IssuableUpdated, null: true, description: 'Triggered when the due date or start date of an issuable is updated.' + + field :merge_request_reviewers_updated, + subscription: Subscriptions::IssuableUpdated, + null: true, + description: 'Triggered when the reviewers of a merge request are updated.' end end + +Types::SubscriptionType.prepend_mod diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb index c3fb9b77927..3856e1aa3b3 100644 --- a/app/graphql/types/timelog_type.rb +++ b/app/graphql/types/timelog_type.rb @@ -4,7 +4,7 @@ module Types class TimelogType < BaseObject graphql_name 'Timelog' - authorize :read_issue + authorize :read_issuable expose_permissions Types::PermissionTypes::Timelog diff --git a/app/graphql/types/work_items/widgets/description_type.rb b/app/graphql/types/work_items/widgets/description_type.rb index 4c365a67bfd..4861f7f46d8 100644 --- a/app/graphql/types/work_items/widgets/description_type.rb +++ b/app/graphql/types/work_items/widgets/description_type.rb @@ -13,8 +13,18 @@ module Types implements Types::WorkItems::WidgetInterface field :description, GraphQL::Types::String, - null: true, - description: 'Description of the work item.' + null: true, + description: 'Description of the work item.' + field :edited, GraphQL::Types::Boolean, + null: false, + description: 'Whether the description has been edited since the work item was created.', + method: :edited? + field :last_edited_at, Types::TimeType, + null: true, + description: 'Timestamp of when the work item\'s description was last edited.' + field :last_edited_by, Types::UserType, + null: true, + description: 'User that made the last edit to the work item\'s description.' markdown_field :description_html, null: true do |resolved_object| resolved_object.work_item |