diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 11:10:13 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 11:10:13 +0000 |
commit | 0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch) | |
tree | 7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /app/graphql | |
parent | 72123183a20411a36d607d70b12d57c484394c8e (diff) | |
download | gitlab-ce-0ea3fcec397b69815975647f5e2aa5fe944a8486.tar.gz |
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'app/graphql')
78 files changed, 951 insertions, 174 deletions
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 9b23aa60eab..b399f0490ee 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -14,17 +14,19 @@ class GitlabSchema < GraphQL::Schema use Gitlab::Graphql::Tracers::ApplicationContextTracer use Gitlab::Graphql::Tracers::MetricsTracer use Gitlab::Graphql::Tracers::LoggerTracer - use Gitlab::Graphql::GenericTracing # Old tracer which will be removed eventually + + # TODO: Old tracer which will be removed eventually + # See https://gitlab.com/gitlab-org/gitlab/-/issues/345396 + use Gitlab::Graphql::GenericTracing use Gitlab::Graphql::Tracers::TimerTracer use GraphQL::Subscriptions::ActionCableSubscriptions - use GraphQL::Pagination::Connections use BatchLoader::GraphQL use Gitlab::Graphql::Pagination::Connections use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout - query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new - query_analyzer Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer.new + query_analyzer Gitlab::Graphql::QueryAnalyzers::AST::LoggerAnalyzer + query_analyzer Gitlab::Graphql::QueryAnalyzers::AST::RecursionAnalyzer query Types::QueryType mutation Types::MutationType @@ -49,10 +51,10 @@ class GitlabSchema < GraphQL::Schema super(queries, **kwargs) end - def get_type(type_name) + def get_type(type_name, context = GraphQL::Query::NullContext) type_name = Gitlab::GlobalId::Deprecations.apply_to_graphql_name(type_name) - super(type_name) + super(type_name, context) end def id_from_object(object, _type = nil, _ctx = nil) @@ -77,8 +79,7 @@ class GitlabSchema < GraphQL::Schema end def resolve_type(type, object, ctx = :__undefined__) - tc = type.metadata[:type_class] - return if tc.respond_to?(:assignable?) && !tc.assignable?(object) + return if type.respond_to?(:assignable?) && !type.assignable?(object) super end @@ -168,14 +169,3 @@ class GitlabSchema < GraphQL::Schema end GitlabSchema.prepend_mod_with('GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule - -# Force the schema to load as a workaround for intermittent errors we -# see due to a lack of thread safety. -# -# TODO: We can remove this workaround when we convert the schema to use -# the new query interpreter runtime. -# -# See: -# - https://gitlab.com/gitlab-org/gitlab/-/issues/211478 -# - https://gitlab.com/gitlab-org/gitlab/-/issues/210556 -GitlabSchema.graphql_definition diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index d57a097a9e2..5f98b222099 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -39,14 +39,16 @@ module Mutations true end - def load_application_object(argument, lookup_as_type, id, context) - ::Gitlab::Graphql::Lazy.new { super }.catch(::GraphQL::UnauthorizedError) do |e| - Gitlab::ErrorTracking.track_exception(e) - # The default behaviour is to abort processing and return nil for the - # entire mutation field, but not set any top-level errors. We prefer to - # at least say that something went wrong. - raise_resource_not_available_error! - end + def load_application_object(argument, id, context) + ::Gitlab::Graphql::Lazy.new { super } + end + + def unauthorized_object(error) + # The default behavior is to abort processing and return nil for the + # entire mutation field, but not set any top-level errors. We prefer to + # at least say that something went wrong. + Gitlab::ErrorTracking.track_exception(error) + raise_resource_not_available_error! end def self.authorizes_object? diff --git a/app/graphql/mutations/ci/pipeline/destroy.rb b/app/graphql/mutations/ci/pipeline/destroy.rb index 3f933818ce1..935cf45c4ab 100644 --- a/app/graphql/mutations/ci/pipeline/destroy.rb +++ b/app/graphql/mutations/ci/pipeline/destroy.rb @@ -12,12 +12,25 @@ module Mutations pipeline = authorized_find!(id: id) project = pipeline.project + return undergoing_refresh_error(project) if project.refreshing_build_artifacts_size? + result = ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline) { success: result.success?, errors: result.errors } end + + private + + def undergoing_refresh_error(project) + Gitlab::ProjectStatsRefreshConflictsLogger.warn_request_rejected_during_stats_refresh(project.id) + + { + success: false, + errors: ['Action temporarily disabled. The project this pipeline belongs to is undergoing stats refresh.'] + } + end end end end diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb index faccd1273e5..b6d8c20c40b 100644 --- a/app/graphql/mutations/ci/runner/update.rb +++ b/app/graphql/mutations/ci/runner/update.rb @@ -18,6 +18,10 @@ module Mutations required: false, description: 'Description of the runner.' + argument :maintenance_note, GraphQL::Types::String, + required: false, + description: 'Runner\'s maintenance notes.' + argument :maximum_timeout, GraphQL::Types::Int, required: false, description: 'Maximum timeout (in seconds) for jobs processed by the runner.' diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb new file mode 100644 index 00000000000..6a91a097a17 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Mutations + module WorkItems + module UpdateArguments + extend ActiveSupport::Concern + + included do + argument :id, ::Types::GlobalIDType[::WorkItem], + required: true, + description: 'Global ID of the work item.' + argument :state_event, Types::WorkItems::StateEventEnum, + description: 'Close or reopen a work item.', + required: false + argument :title, GraphQL::Types::String, + required: false, + description: copy_field_description(Types::WorkItemType, :title) + end + end + end +end diff --git a/app/graphql/mutations/incident_management/timeline_event/create.rb b/app/graphql/mutations/incident_management/timeline_event/create.rb index cbc708a2530..1907954cada 100644 --- a/app/graphql/mutations/incident_management/timeline_event/create.rb +++ b/app/graphql/mutations/incident_management/timeline_event/create.rb @@ -23,7 +23,9 @@ module Mutations authorize!(incident) - response ::IncidentManagement::TimelineEvents::CreateService.new(incident, current_user, args).execute + response ::IncidentManagement::TimelineEvents::CreateService.new( + incident, current_user, args.merge(editable: true) + ).execute end private 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 73a20b8a380..31ae29d896b 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 @@ -21,7 +21,8 @@ module Mutations current_user, promoted_from_note: note, note: note.note, - occurred_at: note.created_at + occurred_at: note.created_at, + editable: true ).execute end diff --git a/app/graphql/mutations/issues/set_crm_contacts.rb b/app/graphql/mutations/issues/set_crm_contacts.rb index 4df65e4769c..cc718b4ae33 100644 --- a/app/graphql/mutations/issues/set_crm_contacts.rb +++ b/app/graphql/mutations/issues/set_crm_contacts.rb @@ -48,7 +48,7 @@ module Mutations private def feature_enabled?(project) - Feature.enabled?(:customer_relations, project.group) && project.group&.crm_enabled? + project.group&.crm_enabled? end end end diff --git a/app/graphql/mutations/merge_requests/set_draft.rb b/app/graphql/mutations/merge_requests/set_draft.rb index ab4ca73e5dc..f83c1a0caf4 100644 --- a/app/graphql/mutations/merge_requests/set_draft.rb +++ b/app/graphql/mutations/merge_requests/set_draft.rb @@ -27,8 +27,8 @@ module Mutations private - def wip_event(wip) - wip ? 'wip' : 'unwip' + def wip_event(draft) + draft ? 'draft' : 'ready' end end end diff --git a/app/graphql/mutations/packages/cleanup/policy/update.rb b/app/graphql/mutations/packages/cleanup/policy/update.rb new file mode 100644 index 00000000000..e7ab7439949 --- /dev/null +++ b/app/graphql/mutations/packages/cleanup/policy/update.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Mutations + module Packages + module Cleanup + module Policy + class Update < Mutations::BaseMutation + graphql_name 'UpdatePackagesCleanupPolicy' + + include FindsProject + + authorize :admin_package + + argument :project_path, + GraphQL::Types::ID, + required: true, + description: 'Project path where the packages cleanup policy is located.' + + argument :keep_n_duplicated_package_files, + Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum, + required: false, + description: copy_field_description( + Types::Packages::Cleanup::PolicyType, + :keep_n_duplicated_package_files + ) + + field :packages_cleanup_policy, + Types::Packages::Cleanup::PolicyType, + null: true, + description: 'Packages cleanup policy after mutation.' + + def resolve(project_path:, **args) + project = authorized_find!(project_path) + + result = ::Packages::Cleanup::UpdatePolicyService + .new(project: project, current_user: current_user, params: args) + .execute + + { + packages_cleanup_policy: result.payload[:packages_cleanup_policy], + errors: result.errors + } + end + end + end + end + end +end diff --git a/app/graphql/mutations/packages/destroy_files.rb b/app/graphql/mutations/packages/destroy_files.rb new file mode 100644 index 00000000000..3900a2c46ae --- /dev/null +++ b/app/graphql/mutations/packages/destroy_files.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Mutations + module Packages + class DestroyFiles < ::Mutations::BaseMutation + graphql_name 'DestroyPackageFiles' + + include FindsProject + + MAXIMUM_FILES = 100 + + authorize :destroy_package + + argument :project_path, + GraphQL::Types::ID, + required: true, + description: 'Project path where the packages cleanup policy is located.' + + argument :ids, + [::Types::GlobalIDType[::Packages::PackageFile]], + required: true, + description: 'IDs of the Package file.' + + def resolve(project_path:, ids:) + project = authorized_find!(project_path) + raise_resource_not_available_error! "Cannot delete more than #{MAXIMUM_FILES} files" if ids.size > MAXIMUM_FILES + + package_files = ::Packages::PackageFile.where(id: parse_gids(ids)) # rubocop:disable CodeReuse/ActiveRecord + + ensure_file_access!(project, package_files) + + result = ::Packages::MarkPackageFilesForDestructionService.new(package_files).execute + + errors = result.error? ? Array.wrap(result[:message]) : [] + + { errors: errors } + end + + private + + def ensure_file_access!(project, package_files) + project_ids = package_files.map(&:project_id).uniq + + unless project_ids.size == 1 && project_ids.include?(project.id) + raise_resource_not_available_error! 'All files must be in the requested project' + end + end + + def parse_gids(gids) + gids.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Packages::PackageFile).model_id } + end + end + end +end diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb index 037ade2589c..70a0e71c869 100644 --- a/app/graphql/mutations/releases/create.rb +++ b/app/graphql/mutations/releases/create.rb @@ -14,6 +14,10 @@ module Mutations required: true, as: :tag, description: 'Name of the tag to associate with the release.' + argument :tag_message, GraphQL::Types::String, + required: false, + description: 'Message to use if creating a new annotated tag.' + argument :ref, GraphQL::Types::String, required: false, description: 'Commit SHA or branch name to use if creating a new tag.' diff --git a/app/graphql/mutations/security/ci_configuration/configure_sast.rb b/app/graphql/mutations/security/ci_configuration/configure_sast.rb index 7ce0bf83a4b..cc3c1d6033b 100644 --- a/app/graphql/mutations/security/ci_configuration/configure_sast.rb +++ b/app/graphql/mutations/security/ci_configuration/configure_sast.rb @@ -16,7 +16,7 @@ module Mutations description: 'SAST CI configuration for the project.' def configure_analyzer(project, **args) - ::Security::CiConfiguration::SastCreateService.new(project, current_user, args[:configuration]).execute + ::Security::CiConfiguration::SastCreateService.new(project, current_user, args[:configuration].to_h).execute end end end diff --git a/app/graphql/mutations/terraform/state/delete.rb b/app/graphql/mutations/terraform/state/delete.rb index f08219cb395..f52ace07393 100644 --- a/app/graphql/mutations/terraform/state/delete.rb +++ b/app/graphql/mutations/terraform/state/delete.rb @@ -8,9 +8,9 @@ module Mutations def resolve(id:) state = authorized_find!(id: id) - state.destroy + response = ::Terraform::States::TriggerDestroyService.new(state, current_user: current_user).execute - { errors: errors_on_object(state) } + { errors: response.errors } end end end diff --git a/app/graphql/mutations/user_preferences/update.rb b/app/graphql/mutations/user_preferences/update.rb index b71c952b0f2..c92c6d725b7 100644 --- a/app/graphql/mutations/user_preferences/update.rb +++ b/app/graphql/mutations/user_preferences/update.rb @@ -14,15 +14,6 @@ module Mutations null: true, description: 'User preferences after mutation.' - def ready?(**args) - if disabled_sort_value?(args) - raise Gitlab::Graphql::Errors::ArgumentError, - 'Feature flag `incident_escalations` must be enabled to use this sort order.' - end - - super - end - def resolve(**attributes) user_preferences = current_user.user_preference user_preferences.update(attributes) @@ -32,14 +23,6 @@ module Mutations errors: errors_on_object(user_preferences) } end - - private - - def disabled_sort_value?(args) - return false unless [:escalation_status_asc, :escalation_status_desc].include?(args[:issues_sort]) - - Feature.disabled?(:incident_escalations) - end end end end diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb index 2e136d409ab..2ae26ed0e1a 100644 --- a/app/graphql/mutations/work_items/create.rb +++ b/app/graphql/mutations/work_items/create.rb @@ -8,8 +8,7 @@ module Mutations include Mutations::SpamProtection include FindsProject - description "Creates a work item." \ - " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice." + description "Creates a work item. Available only when feature flag `work_items` is enabled." authorize :create_work_item diff --git a/app/graphql/mutations/work_items/create_from_task.rb b/app/graphql/mutations/work_items/create_from_task.rb index 4da709401a6..5ebe8b2c6d7 100644 --- a/app/graphql/mutations/work_items/create_from_task.rb +++ b/app/graphql/mutations/work_items/create_from_task.rb @@ -8,7 +8,7 @@ module Mutations include Mutations::SpamProtection description "Creates a work item from a task in another work item's description." \ - " Available only when feature flag `work_items` is enabled. This feature is experimental and is subject to change without notice." + " Available only when feature flag `work_items` is enabled." authorize :update_work_item diff --git a/app/graphql/mutations/work_items/delete.rb b/app/graphql/mutations/work_items/delete.rb index 1830ab5443c..240a8b4c11e 100644 --- a/app/graphql/mutations/work_items/delete.rb +++ b/app/graphql/mutations/work_items/delete.rb @@ -5,7 +5,7 @@ module Mutations class Delete < BaseMutation graphql_name 'WorkItemDelete' description "Deletes a work item." \ - " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice." + " Available only when feature flag `work_items` is enabled." authorize :delete_work_item diff --git a/app/graphql/mutations/work_items/delete_task.rb b/app/graphql/mutations/work_items/delete_task.rb index 87620a28fa1..b1bfed0cbf1 100644 --- a/app/graphql/mutations/work_items/delete_task.rb +++ b/app/graphql/mutations/work_items/delete_task.rb @@ -6,8 +6,7 @@ module Mutations graphql_name 'WorkItemDeleteTask' description "Deletes a task in a work item's description." \ - ' Available only when feature flag `work_items` is enabled. This feature is experimental and' \ - ' is subject to change without notice.' + ' Available only when feature flag `work_items` is enabled.' authorize :update_work_item diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb index 20319301482..c495da00f41 100644 --- a/app/graphql/mutations/work_items/update.rb +++ b/app/graphql/mutations/work_items/update.rb @@ -5,22 +5,13 @@ module Mutations class Update < BaseMutation graphql_name 'WorkItemUpdate' description "Updates a work item by Global ID." \ - " Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice." + " Available only when feature flag `work_items` is enabled." include Mutations::SpamProtection + include Mutations::WorkItems::UpdateArguments authorize :update_work_item - argument :id, ::Types::GlobalIDType[::WorkItem], - required: true, - description: 'Global ID of the work item.' - argument :state_event, Types::WorkItems::StateEventEnum, - description: 'Close or reopen a work item.', - required: false - argument :title, GraphQL::Types::String, - required: false, - description: copy_field_description(Types::WorkItemType, :title) - field :work_item, Types::WorkItemType, null: true, description: 'Updated work item.' diff --git a/app/graphql/mutations/work_items/update_task.rb b/app/graphql/mutations/work_items/update_task.rb new file mode 100644 index 00000000000..35fbe672b66 --- /dev/null +++ b/app/graphql/mutations/work_items/update_task.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Mutations + module WorkItems + class UpdateTask < BaseMutation + graphql_name 'WorkItemUpdateTask' + description "Updates a work item's task by Global ID." \ + " Available only when feature flag `work_items` is enabled." + + include Mutations::SpamProtection + + authorize :read_work_item + + argument :id, ::Types::GlobalIDType[::WorkItem], + required: true, + description: 'Global ID of the work item.' + argument :task_data, ::Types::WorkItems::UpdatedTaskInputType, + required: true, + description: 'Arguments necessary to update a task.' + + field :task, Types::WorkItemType, + null: true, + description: 'Updated task.' + field :work_item, Types::WorkItemType, + null: true, + description: 'Updated work item.' + + def resolve(id:, task_data:) + task_data_hash = task_data.to_h + work_item = authorized_find!(id: id) + task = authorized_find_task!(task_data_hash[:id]) + + unless work_item.project.work_items_feature_flag_enabled? + return { errors: ['`work_items` feature flag disabled for this project'] } + end + + spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) + + ::WorkItems::UpdateService.new( + project: task.project, + current_user: current_user, + params: task_data_hash.except(:id), + spam_params: spam_params + ).execute(task) + + check_spam_action_response!(task) + + response = { errors: errors_on_object(task) } + + if task.valid? + work_item.expire_etag_cache + + response.merge(work_item: work_item, task: task) + else + response + end + end + + private + + def authorized_find_task!(task_id) + task = task_id.find + + if current_user.can?(:update_work_item, task) + task + else + # Fail early if user cannot update task + raise_resource_not_available_error! + end + end + + def find_object(id:) + id.find + end + end + end +end diff --git a/app/graphql/mutations/work_items/update_widgets.rb b/app/graphql/mutations/work_items/update_widgets.rb new file mode 100644 index 00000000000..d19da0abaac --- /dev/null +++ b/app/graphql/mutations/work_items/update_widgets.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Mutations + module WorkItems + class UpdateWidgets < BaseMutation + graphql_name 'WorkItemUpdateWidgets' + description "Updates the attributes of a work item's widgets by global ID." \ + " Available only when feature flag `work_items` is enabled." + + include Mutations::SpamProtection + + authorize :update_work_item + + argument :id, ::Types::GlobalIDType[::WorkItem], + required: true, + description: 'Global ID of the work item.' + + argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType, + required: false, + description: 'Input for description widget.' + + field :work_item, Types::WorkItemType, + null: true, + description: 'Updated work item.' + + def resolve(id:, **widget_attributes) + work_item = authorized_find!(id: id) + + unless work_item.project.work_items_feature_flag_enabled? + return { errors: ['`work_items` feature flag disabled for this project'] } + end + + spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) + + ::WorkItems::UpdateService.new( + project: work_item.project, + current_user: current_user, + # Cannot use prepare to use `.to_h` on each input due to + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87472#note_945199865 + widget_params: widget_attributes.transform_values { |values| values.to_h }, + spam_params: spam_params + ).execute(work_item) + + check_spam_action_response!(work_item) + + { + work_item: work_item.valid? ? work_item : nil, + errors: errors_on_object(work_item) + } + end + + private + + def find_object(id:) + GitlabSchema.find_by_gid(id) + end + end + end +end diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb index a1fda976876..ec47a8996eb 100644 --- a/app/graphql/resolvers/base_issues_resolver.rb +++ b/app/graphql/resolvers/base_issues_resolver.rb @@ -33,13 +33,6 @@ module Resolvers end end - def prepare_params(args, parent) - return unless [:escalation_status_asc, :escalation_status_desc].include?(args[:sort]) - return if Feature.enabled?(:incident_escalations, parent) - - args[:sort] = :created_desc # default for sort argument - end - private def unconditional_includes diff --git a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb new file mode 100644 index 00000000000..14b5f8f90eb --- /dev/null +++ b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerOwnerProjectResolver < BaseResolver + include LooksAhead + + type Types::ProjectType, null: true + + alias_method :runner, :object + + def resolve_with_lookahead(**args) + resolve_owner + end + + def preloads + { + full_path: [:route] + } + end + + def filtered_preloads + selection = lookahead + + preloads.each.flat_map do |name, requirements| + selection&.selects?(name) ? requirements : [] + end + end + + private + + def resolve_owner + return unless runner.project_type? + + BatchLoader::GraphQL.for(runner.id).batch(key: :runner_owner_projects) do |runner_ids, loader| + # rubocop: disable CodeReuse/ActiveRecord + runner_and_projects_with_row_number = + ::Ci::RunnerProject + .where(runner_id: runner_ids) + .select('id, runner_id, project_id, ROW_NUMBER() OVER (PARTITION BY runner_id ORDER BY id ASC)') + runner_and_owner_projects = + ::Ci::RunnerProject + .select(:id, :runner_id, :project_id) + .from("(#{runner_and_projects_with_row_number.to_sql}) temp WHERE row_number = 1") + owner_project_id_by_runner_id = + runner_and_owner_projects + .group_by(&:runner_id) + .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) + + 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]) + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end + end +end diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb index 722fbab3bb7..9740bc6bb6a 100644 --- a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb +++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb @@ -16,7 +16,7 @@ module Resolvers def resolve(**args) return ::Clusters::AgentToken.none unless can_read_agent_tokens? - tokens = agent.last_used_agent_tokens + tokens = agent.agent_tokens tokens = tokens.with_status(args[:status]) if args[:status].present? tokens diff --git a/app/graphql/resolvers/clusters/agents_resolver.rb b/app/graphql/resolvers/clusters/agents_resolver.rb index 5ad66ed7cdd..28618bef807 100644 --- a/app/graphql/resolvers/clusters/agents_resolver.rb +++ b/app/graphql/resolvers/clusters/agents_resolver.rb @@ -30,7 +30,7 @@ module Resolvers def preloads { activity_events: { activity_events: [:user, agent_token: :agent] }, - tokens: :last_used_agent_tokens + tokens: :agent_tokens } end end diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index de44dbb26d7..fe213936f55 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -66,7 +66,6 @@ module IssueResolverArguments description: 'Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues.' argument :not, Types::Issues::NegatedIssueFilterInputType, description: 'Negated arguments.', - prepare: ->(negated_args, ctx) { negated_args.to_h }, required: false argument :crm_contact_id, GraphQL::Types::String, required: false, @@ -85,12 +84,12 @@ module IssueResolverArguments # 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? prepare_assignee_username_params(args) prepare_release_tag_params(args) - prepare_params(args, parent) if defined?(prepare_params) finder = IssuesFinder.new(current_user, args) @@ -98,6 +97,8 @@ module IssueResolverArguments end def ready?(**args) + args[:not] = args[:not].to_h if args[:not].present? + params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args) params_not_mutually_exclusive(args, mutually_exclusive_milestone_args) params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args) diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb index c451d4e7936..2a3dce80057 100644 --- a/app/graphql/resolvers/concerns/resolves_groups.rb +++ b/app/graphql/resolvers/concerns/resolves_groups.rb @@ -18,11 +18,9 @@ module ResolvesGroups def preloads { - contacts: [:contacts], container_repositories_count: [:container_repositories], custom_emoji: [:custom_emoji], full_path: [:route], - organizations: [:organizations], path: [:route], dependency_proxy_blob_count: [:dependency_proxy_blobs], dependency_proxy_blobs: [:dependency_proxy_blobs], diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index a72b9a09118..697cc6f5b03 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -52,6 +52,7 @@ module ResolvesMergeRequests security_auto_fix: [:author], head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }], timelogs: [:timelogs], + pipelines: [:merge_request_diffs], # used by `recent_diff_head_shas` to load pipelines committers: [merge_request_diff: [:merge_request_diff_commits]] } end diff --git a/app/graphql/resolvers/crm/contacts_resolver.rb b/app/graphql/resolvers/crm/contacts_resolver.rb new file mode 100644 index 00000000000..58d0e2ce13d --- /dev/null +++ b/app/graphql/resolvers/crm/contacts_resolver.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Resolvers + module Crm + class ContactsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include ResolvesIds + + authorize :read_crm_contact + + type Types::CustomerRelations::ContactType, null: true + + argument :search, GraphQL::Types::String, + required: false, + description: 'Search term to find contacts with.' + + argument :state, Types::CustomerRelations::ContactStateEnum, + required: false, + description: 'State of the contacts to search for.' + + argument :ids, [::Types::GlobalIDType[CustomerRelations::Contact]], + required: false, + description: 'Filter contacts by IDs.' + + def resolve(**args) + args[:ids] = resolve_ids(args.delete(:ids)) + + ::Crm::ContactsFinder.new(current_user, { group: group }.merge(args)).execute + end + + def group + object.respond_to?(:sync) ? object.sync : object + end + end + end +end diff --git a/app/graphql/resolvers/crm/organizations_resolver.rb b/app/graphql/resolvers/crm/organizations_resolver.rb new file mode 100644 index 00000000000..ca0a908ee22 --- /dev/null +++ b/app/graphql/resolvers/crm/organizations_resolver.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Resolvers + module Crm + class OrganizationsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include ResolvesIds + + authorize :read_crm_organization + + type Types::CustomerRelations::OrganizationType, null: true + + argument :search, GraphQL::Types::String, + required: false, + description: 'Search term used to find organizations with.' + + argument :state, Types::CustomerRelations::OrganizationStateEnum, + required: false, + description: 'State of the organization to search for.' + + argument :ids, [Types::GlobalIDType[CustomerRelations::Organization]], + required: false, + description: 'Filter organizations by IDs.' + + def resolve(**args) + args[:ids] = resolve_ids(args.delete(:ids)) + + ::Crm::OrganizationsFinder.new(current_user, { group: group }.merge(args)).execute + end + + def group + object.respond_to?(:sync) ? object.sync : object + end + end + end +end diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb index de636655087..bd9b82283c3 100644 --- a/app/graphql/resolvers/design_management/versions_resolver.rb +++ b/app/graphql/resolvers/design_management/versions_resolver.rb @@ -42,8 +42,6 @@ module Resolvers def cutoff(id, sha) if sha.present? || id.present? specific_version(id, sha) - elsif at_version = context[:at_version_argument] - by_id(at_version) # See: DesignsResolver else :unconstrained end diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb index f84eedb4c3b..deb698c63e1 100644 --- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb +++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb @@ -21,8 +21,9 @@ module Resolvers super end - def query_for(args) - resolve_pipelines(project, args).merge(merge_request.all_pipelines) + def query_for(input) + mr, args = input + resolve_pipelines(mr.source_project, args).merge(mr.all_pipelines) end def model_class @@ -30,7 +31,7 @@ module Resolvers end def query_input(**args) - args + [merge_request, args] end def project diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index dc6d781f584..25ff783b408 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -4,6 +4,11 @@ module Resolvers class MilestonesResolver < BaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource include TimeFrameArguments + include LooksAhead + + # authorize before resolution + authorize :read_milestone + authorizes_object! argument :ids, [GraphQL::Types::ID], required: false, @@ -34,12 +39,10 @@ module Resolvers NON_STABLE_CURSOR_SORTS = %i[expired_last_due_date_asc expired_last_due_date_desc].freeze - def resolve(**args) + def resolve_with_lookahead(**args) validate_timeframe_params!(args) - authorize! - - milestones = MilestonesFinder.new(milestones_finder_params(args)).execute + milestones = apply_lookahead(MilestonesFinder.new(milestones_finder_params(args)).execute) if non_stable_cursor_sort?(args[:sort]) offset_pagination(milestones) @@ -50,6 +53,12 @@ module Resolvers private + def preloads + { + releases: :releases + } + end + def milestones_finder_params(args) { ids: parse_gids(args[:ids]), @@ -69,12 +78,6 @@ module Resolvers raise NotImplementedError end - # MilestonesFinder does not check for current_user permissions, - # so for now we need to keep it here. - def authorize! - Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error! - end - def parse_gids(gids) gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: Milestone).model_id } end diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb index d29d87ca204..1b4211366e0 100644 --- a/app/graphql/resolvers/paginated_tree_resolver.rb +++ b/app/graphql/resolvers/paginated_tree_resolver.rb @@ -17,7 +17,6 @@ module Resolvers description: 'Used to get a recursive tree. Default is false.' argument :ref, GraphQL::Types::String, required: false, - default_value: :head, description: 'Commit ref to get the tree for. Default value is HEAD.' alias_method :repository, :object @@ -26,6 +25,7 @@ module Resolvers return unless repository.exists? cursor = args.delete(:after) + args[:ref] ||= :head pagination_params = { limit: @field.max_page_size || 100, diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb index f02eb226810..553f9aa6cd9 100644 --- a/app/graphql/resolvers/tree_resolver.rb +++ b/app/graphql/resolvers/tree_resolver.rb @@ -16,7 +16,6 @@ module Resolvers description: 'Used to get a recursive tree. Default is false.' argument :ref, GraphQL::Types::String, required: false, - default_value: :head, description: 'Commit ref to get the tree for. Default value is HEAD.' alias_method :repository, :object @@ -24,6 +23,7 @@ module Resolvers def resolve(**args) return unless repository.exists? + args[:ref] ||= :head repository.tree(args[:ref], args[:path], recursive: args[:recursive]) end end diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb index 99fd0d4927d..f0fd60e9cbb 100644 --- a/app/graphql/resolvers/user_resolver.rb +++ b/app/graphql/resolvers/user_resolver.rb @@ -2,6 +2,8 @@ module Resolvers class UserResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + description 'Retrieve a single user' type Types::UserType, null: true @@ -23,6 +25,8 @@ module Resolvers end def resolve(id: nil, username: nil) + authorize! + if id GitlabSchema.object_from_id(id, expected_type: User) else @@ -39,5 +43,9 @@ module Resolvers end end end + + def authorize! + raise_resource_not_available_error! unless context[:current_user].present? + end end end diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb index 1424c14083d..b0d704d09fc 100644 --- a/app/graphql/resolvers/users_resolver.rb +++ b/app/graphql/resolvers/users_resolver.rb @@ -47,10 +47,7 @@ module Resolvers end def authorize!(usernames) - authorized = Ability.allowed?(context[:current_user], :read_users_list) - authorized &&= usernames.present? if context[:current_user].blank? - - raise_resource_not_available_error! unless authorized + raise_resource_not_available_error! unless context[:current_user].present? end private diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb new file mode 100644 index 00000000000..1bc74131b9e --- /dev/null +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Resolvers + class WorkItemsResolver < BaseResolver + include SearchArguments + include LooksAhead + + type Types::WorkItemType.connection_type, null: true + + argument :iid, GraphQL::Types::String, + required: false, + description: 'IID of the issue. For example, "1".' + argument :iids, [GraphQL::Types::String], + required: false, + description: 'List of IIDs of work items. For example, `["1", "2"]`.' + argument :sort, Types::WorkItemSortEnum, + description: 'Sort work items by this criteria.', + required: false, + default_value: :created_desc + argument :state, Types::IssuableStateEnum, + required: false, + description: 'Current state of this work item.' + argument :types, [Types::IssueTypeEnum], + as: :issue_types, + description: 'Filter work items by the given work item types.', + 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? + + 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, args) + + Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) } + end + + def ready?(**args) + validate_anonymous_search_access! if args[:search].present? + + super + end + + private + + def unconditional_includes + [ + { + project: [:project_feature, :group] + }, + :author + ] + end + end +end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index b4cd54b1332..6aee9a5c052 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -53,6 +53,30 @@ module Types field_authorized?(object, ctx) && resolver_authorized?(object, ctx) end + # This gets called from the gem's `calculate_complexity` method, allowing us + # to ensure our complexity calculation is used even for connections. + # This code is actually a copy of the default case in `calculate_complexity` + # in `lib/graphql/schema/field.rb` + # (https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/schema/field.rb) + def complexity_for(child_complexity:, query:, lookahead:) + defined_complexity = complexity + + case defined_complexity + when Proc + arguments = query.arguments_for(lookahead.ast_nodes.first, self) + + if arguments.respond_to?(:keyword_arguments) + defined_complexity.call(query.context, arguments.keyword_arguments, child_complexity) + else + child_complexity + end + when Numeric + defined_complexity + child_complexity + else + raise("Invalid complexity: #{defined_complexity.inspect} on #{path} (#{inspect})") + end + end + def base_complexity complexity = DEFAULT_COMPLEXITY complexity += 1 if calls_gitaly? @@ -150,10 +174,9 @@ module Types def connection_complexity_multiplier(ctx, args) # Resolvers may add extra complexity depending on number of items being loaded. - field_defn = to_graphql - return 0 unless field_defn.connection? + return 0 unless connection? - page_size = field_defn.connection_max_page_size || ctx.schema.default_max_page_size + page_size = max_page_size || ctx.schema.default_max_page_size limit_value = [args[:first], args[:last], page_size].compact.min multiplier = resolver&.try(:complexity_multiplier, args).to_f limit_value * multiplier diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb index e3413551a3f..3fab040cc0b 100644 --- a/app/graphql/types/ci/detailed_status_type.rb +++ b/app/graphql/types/ci/detailed_status_type.rb @@ -33,7 +33,7 @@ module Types method: :status_tooltip def id(parent:) - "#{object.id}-#{parent.object.object.id}" + "#{object.id}-#{parent.id}" end def action diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index f25fc56a588..b20a671179b 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -7,7 +7,7 @@ module Types class JobType < BaseObject graphql_name 'CiJob' - connection_type_class(Types::CountableConnectionType) + connection_type_class(Types::LimitedCountableConnectionType) expose_permissions Types::PermissionTypes::Ci::Job diff --git a/app/graphql/types/ci/pipeline_merge_request_event_type_enum.rb b/app/graphql/types/ci/pipeline_merge_request_event_type_enum.rb new file mode 100644 index 00000000000..a1236b8f2c1 --- /dev/null +++ b/app/graphql/types/ci/pipeline_merge_request_event_type_enum.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + module Ci + class PipelineMergeRequestEventTypeEnum < BaseEnum + graphql_name 'PipelineMergeRequestEventType' + description 'Event type of the pipeline associated with a merge request' + + value 'MERGED_RESULT', + 'Pipeline run on the changes from the source branch combined with the target branch.', + value: :merged_result + value 'DETACHED', + 'Pipeline run on the changes in the merge request source branch.', + value: :detached + end + end +end + +Types::Ci::PipelineMergeRequestEventTypeEnum.prepend_mod diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 81afc7f0f42..60418fec6c5 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -175,6 +175,9 @@ module Types field :warning_messages, [Types::Ci::PipelineMessageType], null: true, description: 'Pipeline warning messages.' + field :merge_request_event_type, Types::Ci::PipelineMergeRequestEventTypeEnum, null: true, + description: "Event type of the pipeline associated with a merge request." + def detailed_status object.detailed_status(current_user) end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index 6f957d2511f..949e216a982 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -85,6 +85,15 @@ module Types method: :token_expires_at field :version, GraphQL::Types::String, null: true, description: 'Version of the runner.' + field :owner_project, ::Types::ProjectType, null: true, + description: 'Project that owns the runner. For project runners only.', + resolver: ::Resolvers::Ci::RunnerOwnerProjectResolver + + markdown_field :maintenance_note_html, null: true + + def maintenance_note_html_resolver + ::MarkupHelper.markdown(object.maintenance_note, context.to_h.dup) + end def job_count # We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT @@ -136,16 +145,22 @@ module Types # rubocop: disable CodeReuse/ActiveRecord def batched_owners(runner_assoc_type, assoc_type, key, column_name) - BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader, args| - runner_and_owner_ids = runner_assoc_type.where(runner_id: runner_ids).pluck(:runner_id, column_name) - - owner_ids_by_runner_id = runner_and_owner_ids.group_by(&:first).transform_values { |v| v.pluck(1) } - owner_ids = runner_and_owner_ids.pluck(1).uniq - + BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader| + plucked_runner_and_owner_ids = runner_assoc_type + .select(:runner_id, column_name) + .where(runner_id: runner_ids) + .pluck(:runner_id, column_name) + # In plucked_runner_and_owner_ids, first() represents the runner ID, and second() the owner ID, + # so let's group the owner IDs by runner ID + runner_owner_ids_by_runner_id = plucked_runner_and_owner_ids + .group_by(&:first) + .transform_values { |runner_and_owner_id| runner_and_owner_id.map(&:second) } + + owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq owners = assoc_type.where(id: owner_ids).index_by(&:id) runner_ids.each do |runner_id| - loader.call(runner_id, owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || []) + loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || []) end end end diff --git a/app/graphql/types/ci/runner_web_url_edge.rb b/app/graphql/types/ci/runner_web_url_edge.rb index 035d75c22c6..7dfcd1f3510 100644 --- a/app/graphql/types/ci/runner_web_url_edge.rb +++ b/app/graphql/types/ci/runner_web_url_edge.rb @@ -4,8 +4,6 @@ module Types module Ci # rubocop: disable Graphql/AuthorizeTypes class RunnerWebUrlEdge < ::Types::BaseEdge - include FindClosest - field :edit_url, GraphQL::Types::String, null: true, description: 'Web URL of the runner edit page. The value depends on where you put this field in the query. You can use it for projects or groups.', extras: [:parent] @@ -19,19 +17,18 @@ module Types @runner = node.node end + # here parent is a Keyset::Connection def edit_url(parent:) - runner_url(parent: parent, url_type: :edit_url) + runner_url(owner: parent.parent, url_type: :edit_url) end def web_url(parent:) - runner_url(parent: parent, url_type: :default) + runner_url(owner: parent.parent, url_type: :default) end private - def runner_url(parent:, url_type: :default) - owner = closest_parent([::Types::ProjectType, ::Types::GroupType], parent) - + def runner_url(owner:, url_type: :default) # Only ::Group is supported at the moment, future iterations will include ::Project. # See https://gitlab.com/gitlab-org/gitlab/-/issues/16338 case owner diff --git a/app/graphql/types/ci/status_action_type.rb b/app/graphql/types/ci/status_action_type.rb index 26ca3c1438a..c0f61cf49f2 100644 --- a/app/graphql/types/ci/status_action_type.rb +++ b/app/graphql/types/ci/status_action_type.rb @@ -21,7 +21,8 @@ module Types description: 'Title for the action, for example: Retry.' def id(parent:) - "#{parent.parent.object.object.class.name}-#{parent.object.object.id}" + # parent is a SimpleDelegator + "#{parent.subject.class.name}-#{parent.id}" end def action_method diff --git a/app/graphql/types/concerns/find_closest.rb b/app/graphql/types/concerns/find_closest.rb deleted file mode 100644 index 3064db19ea0..00000000000 --- a/app/graphql/types/concerns/find_closest.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module FindClosest - # Find the closest node which has any of the given types above this node, and return the domain object - def closest_parent(types, parent) - while parent - - if types.any? {|type| parent.object.instance_of? type} - return parent.object.object - else - parent = parent.try(:parent) - end - end - end -end diff --git a/app/graphql/types/customer_relations/contact_state_enum.rb b/app/graphql/types/customer_relations/contact_state_enum.rb new file mode 100644 index 00000000000..445d2a41401 --- /dev/null +++ b/app/graphql/types/customer_relations/contact_state_enum.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module CustomerRelations + class ContactStateEnum < BaseEnum + graphql_name 'CustomerRelationsContactState' + + value 'active', + description: "Active contact.", + value: :active + + value 'inactive', + description: "Inactive contact.", + value: :inactive + 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 new file mode 100644 index 00000000000..ecdd7d092ad --- /dev/null +++ b/app/graphql/types/customer_relations/organization_state_enum.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module CustomerRelations + class OrganizationStateEnum < BaseEnum + graphql_name 'CustomerRelationsOrganizationState' + + value 'active', + description: "Active organization.", + value: :active + + value 'inactive', + description: "Inactive organization.", + value: :inactive + end + end +end diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb index 6a924c13a3c..145a5a22460 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -84,7 +84,8 @@ module Types end define_singleton_method(:suitable?) do |gid| - next false if gid.nil? + # an argument can be nil, so allow it here + next true if gid.nil? gid.model_name.safe_constantize.present? && gid.model_class.ancestors.include?(model_class) diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb index 18242f7b8b1..c4582f31bec 100644 --- a/app/graphql/types/group_member_type.rb +++ b/app/graphql/types/group_member_type.rb @@ -15,7 +15,7 @@ module Types field :notification_email, resolver: Resolvers::GroupMembers::NotificationEmailResolver, - description: "Group notification email for User. Only availble for admins." + description: "Group notification email for User. Only available for admins." def group Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.source_id).find diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index a94cd6fad20..49971d52a30 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -201,11 +201,13 @@ module Types field :organizations, Types::CustomerRelations::OrganizationType.connection_type, null: true, - description: "Find organizations of this group." + description: "Find organizations of this group.", + resolver: Resolvers::Crm::OrganizationsResolver field :contacts, Types::CustomerRelations::ContactType.connection_type, null: true, - description: "Find contacts of this group." + description: "Find contacts of this group.", + resolver: Resolvers::Crm::ContactsResolver field :work_item_types, Types::WorkItems::TypeType.connection_type, resolver: Resolvers::WorkItems::TypesResolver, diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index db51e491d4e..7dced3c8e00 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -14,8 +14,10 @@ module Types value 'TITLE_DESC', 'Title by descending order.', value: :title_desc value 'POPULARITY_ASC', 'Number of upvotes (awarded "thumbs up" emoji) by ascending order.', value: :popularity_asc value 'POPULARITY_DESC', 'Number of upvotes (awarded "thumbs up" emoji) by descending order.', value: :popularity_desc - value 'ESCALATION_STATUS_ASC', 'Status from triggered to resolved. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled.', value: :escalation_status_asc - value 'ESCALATION_STATUS_DESC', 'Status from resolved to triggered. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled.', value: :escalation_status_desc + value 'ESCALATION_STATUS_ASC', 'Status from triggered to resolved.', value: :escalation_status_asc + value 'ESCALATION_STATUS_DESC', 'Status from resolved to triggered.', value: :escalation_status_desc + value 'CLOSED_AT_ASC', 'Closed time by ascending order.', value: :closed_at_asc + value 'CLOSED_AT_DESC', 'Closed time by descending order.', value: :closed_at_desc end end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index c83200bd614..58729b34fc7 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -127,6 +127,9 @@ module Types field :moved_to, Types::IssueType, null: true, description: 'Updated Issue after it got moved to another project.' + field :closed_as_duplicate_of, Types::IssueType, null: true, + description: 'Issue this issue was closed as a duplicate of.' + field :create_note_email, GraphQL::Types::String, null: true, description: 'User specific email address for the issue.' @@ -161,6 +164,10 @@ module Types Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.moved_to_id).find end + def closed_as_duplicate_of + Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.duplicated_to_id).find + end + def discussion_locked !!object.discussion_locked end diff --git a/app/graphql/types/limited_countable_connection_type.rb b/app/graphql/types/limited_countable_connection_type.rb new file mode 100644 index 00000000000..f0698222ea3 --- /dev/null +++ b/app/graphql/types/limited_countable_connection_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class LimitedCountableConnectionType < GraphQL::Types::Relay::BaseConnection + COUNT_LIMIT = 1000 + COUNT_DESCRIPTION = "Limited count of collection. Returns limit + 1 for counts greater than the limit." + + field :count, GraphQL::Types::Int, null: false, description: COUNT_DESCRIPTION do + argument :limit, GraphQL::Types::Int, + required: false, default_value: COUNT_LIMIT, + validates: { numericality: { greater_than: 0, less_than_or_equal_to: COUNT_LIMIT } }, + description: "Limit value to be applied to the count query. Default is 1000." + end + + def count(limit:) + relation = object.items + + if relation.respond_to?(:page) + relation.page.total_count_with_limit(:all, limit: limit) + else + [relation.size, limit.next].min + end + end + end +end diff --git a/app/graphql/types/merge_requests/interacts_with_merge_request.rb b/app/graphql/types/merge_requests/interacts_with_merge_request.rb index 15621ef1472..bef2d39dc5c 100644 --- a/app/graphql/types/merge_requests/interacts_with_merge_request.rb +++ b/app/graphql/types/merge_requests/interacts_with_merge_request.rb @@ -5,8 +5,6 @@ module Types module InteractsWithMergeRequest extend ActiveSupport::Concern - include FindClosest - included do field :merge_request_interaction, type: ::Types::UserMergeRequestInteractionType, @@ -16,11 +14,9 @@ module Types end def merge_request_interaction(parent:, id: nil) - merge_request = closest_parent([::Types::MergeRequestType], parent) - - return unless merge_request - - Users::MergeRequestInteraction.new(user: object, merge_request: merge_request) + # need the connection parent if called from a connection node: + parent = parent.parent if parent.try(:field)&.connection? + Users::MergeRequestInteraction.new(user: object, merge_request: parent) end end end diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb index 18e4a5d33e3..7741fd723f0 100644 --- a/app/graphql/types/milestone_type.rb +++ b/app/graphql/types/milestone_type.rb @@ -59,6 +59,10 @@ module Types field :stats, Types::MilestoneStatsType, null: true, description: 'Milestone statistics.' + field :releases, ::Types::ReleaseType.connection_type, + null: true, + description: 'Releases associated with this milestone.' + def stats milestone end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 7d8ada82d40..8642957af02 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -136,12 +136,16 @@ module Types mount_mutation Mutations::UserPreferences::Update mount_mutation Mutations::Packages::Destroy mount_mutation Mutations::Packages::DestroyFile + mount_mutation Mutations::Packages::DestroyFiles + mount_mutation Mutations::Packages::Cleanup::Policy::Update mount_mutation Mutations::Echo - mount_mutation Mutations::WorkItems::Create - mount_mutation Mutations::WorkItems::CreateFromTask - mount_mutation Mutations::WorkItems::Delete - mount_mutation Mutations::WorkItems::DeleteTask - mount_mutation Mutations::WorkItems::Update + mount_mutation Mutations::WorkItems::Create, deprecated: { milestone: '15.1', reason: :alpha } + mount_mutation Mutations::WorkItems::CreateFromTask, deprecated: { milestone: '15.1', reason: :alpha } + mount_mutation Mutations::WorkItems::Delete, deprecated: { milestone: '15.1', reason: :alpha } + mount_mutation Mutations::WorkItems::DeleteTask, deprecated: { milestone: '15.1', reason: :alpha } + mount_mutation Mutations::WorkItems::Update, deprecated: { milestone: '15.1', reason: :alpha } + mount_mutation Mutations::WorkItems::UpdateWidgets, deprecated: { milestone: '15.1', reason: :alpha } + mount_mutation Mutations::WorkItems::UpdateTask, deprecated: { milestone: '15.1', reason: :alpha } mount_mutation Mutations::SavedReplies::Create mount_mutation Mutations::SavedReplies::Update mount_mutation Mutations::SavedReplies::Destroy diff --git a/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb b/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb new file mode 100644 index 00000000000..bf8d625a334 --- /dev/null +++ b/app/graphql/types/packages/cleanup/keep_duplicated_package_files_enum.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module Packages + module Cleanup + class KeepDuplicatedPackageFilesEnum < BaseEnum + graphql_name 'PackagesCleanupKeepDuplicatedPackageFilesEnum' + + OPTIONS_MAPPING = { + 'all' => 'ALL_PACKAGE_FILES', + '1' => 'ONE_PACKAGE_FILE', + '10' => 'TEN_PACKAGE_FILES', + '20' => 'TWENTY_PACKAGE_FILES', + '30' => 'THIRTY_PACKAGE_FILES', + '40' => 'FORTY_PACKAGE_FILES', + '50' => 'FIFTY_PACKAGE_FILES' + }.freeze + + ::Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES.each do |keep_value| + value OPTIONS_MAPPING[keep_value], value: keep_value, description: "Value to keep #{keep_value} package files" + end + end + end + end +end diff --git a/app/graphql/types/packages/cleanup/policy_type.rb b/app/graphql/types/packages/cleanup/policy_type.rb new file mode 100644 index 00000000000..f08aace7df9 --- /dev/null +++ b/app/graphql/types/packages/cleanup/policy_type.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Types + module Packages + module Cleanup + class PolicyType < ::Types::BaseObject + graphql_name 'PackagesCleanupPolicy' + description 'A packages cleanup policy designed to keep only packages and packages assets that matter most' + + authorize :admin_package + + field :keep_n_duplicated_package_files, + Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum, + null: false, + description: 'Number of duplicated package files to retain.' + field :next_run_at, + Types::TimeType, + null: true, + description: 'Next time that this packages cleanup policy will be executed.' + end + end + end +end diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb index a2cefb872c9..07e6e7a55d6 100644 --- a/app/graphql/types/permission_types/base_permission_type.rb +++ b/app/graphql/types/permission_types/base_permission_type.rb @@ -12,11 +12,7 @@ module Types end def self.ability_field(ability, **kword_args) - unless resolving_keywords?(kword_args) - kword_args[:resolve] ||= -> (object, args, context) do - can?(context[:current_user], ability, object, args.to_h) - end - end + define_field_resolver_method(ability) unless resolving_keywords?(kword_args) permission_field(ability, **kword_args) end @@ -31,6 +27,14 @@ module Types field(**kword_args) # rubocop:disable Graphql/Descriptions end + def self.define_field_resolver_method(ability) + unless self.respond_to?(ability) + define_method ability.to_sym do |*args| + Ability.allowed?(context[:current_user], ability, object, args.to_h) + end + end + end + def self.resolving_keywords?(arguments) RESOLVING_KEYWORDS.intersect?(arguments.keys.to_set) end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index f1de8e985b3..603d5ead540 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -4,6 +4,8 @@ module Types class ProjectType < BaseObject graphql_name 'Project' + connection_type_class(Types::CountableConnectionType) + authorize :read_project expose_permissions Types::PermissionTypes::Project @@ -142,6 +144,14 @@ module Types extras: [:lookahead], resolver: Resolvers::IssuesResolver + field :work_items, + Types::WorkItemType.connection_type, + null: true, + deprecated: { milestone: '15.1', reason: :alpha }, + description: 'Work items of the project.', + extras: [:lookahead], + resolver: Resolvers::WorkItemsResolver + field :issue_status_counts, Types::IssueStatusCountsType, null: true, @@ -179,6 +189,11 @@ module Types description: 'Packages of the project.', resolver: Resolvers::ProjectPackagesResolver + field :packages_cleanup_policy, + Types::Packages::Cleanup::PolicyType, + null: true, + description: 'Packages cleanup policy for the project.' + field :jobs, type: Types::Ci::JobType.connection_type, null: true, diff --git a/app/graphql/types/query_complexity_type.rb b/app/graphql/types/query_complexity_type.rb index 13b618cf5ce..ddcf448c64a 100644 --- a/app/graphql/types/query_complexity_type.rb +++ b/app/graphql/types/query_complexity_type.rb @@ -5,7 +5,7 @@ module Types class QueryComplexityType < ::Types::BaseObject graphql_name 'QueryComplexity' - ANALYZER = GraphQL::Analysis::QueryComplexity.new { |_query, complexity| complexity } + ANALYZER = GraphQL::Analysis::AST::QueryComplexity alias_method :query, :object @@ -23,7 +23,7 @@ module Types description: 'GraphQL query complexity score.' def score - ::GraphQL::Analysis.analyze_query(query, [ANALYZER]).first + ::GraphQL::Analysis::AST.analyze_query(query, [ANALYZER]).first end end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 01b1a71896a..46d121f6552 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -52,6 +52,7 @@ module Types field :milestone, ::Types::MilestoneType, null: true, + extras: [:lookahead], description: 'Find a milestone.' do argument :id, ::Types::GlobalIDType[Milestone], required: true, description: 'Find a milestone by its ID.' end @@ -90,8 +91,8 @@ module Types field :work_item, Types::WorkItemType, null: true, resolver: Resolvers::WorkItemResolver, - description: 'Find a work item. Returns `null` if `work_items` feature flag is disabled.' \ - ' The feature is experimental and is subject to change without notice.' + deprecated: { milestone: '15.1', reason: :alpha }, + description: 'Find a work item. Returns `null` if `work_items` feature flag is disabled.' field :merge_request, Types::MergeRequestType, null: true, @@ -156,8 +157,9 @@ module Types GitlabSchema.find_by_gid(id) end - def milestone(id:) - GitlabSchema.find_by_gid(id) + def milestone(id:, lookahead:) + preloads = [:releases] if lookahead.selects?(:releases) + Gitlab::Graphql::Loaders::BatchModelLoader.new(id.model_class, id.model_id, preloads).find end def container_repository(id:) diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb index 33dcb5125e3..29738de27e5 100644 --- a/app/graphql/types/release_asset_link_type.rb +++ b/app/graphql/types/release_asset_link_type.rb @@ -7,6 +7,8 @@ module Types authorize :read_release + present_using Releases::LinkPresenter + field :external, GraphQL::Types::Boolean, null: true, method: :external?, description: 'Indicates the link points to an external resource.' field :id, GraphQL::Types::ID, null: false, @@ -22,12 +24,5 @@ module Types description: 'Relative path for the direct asset link.' field :direct_asset_url, GraphQL::Types::String, null: true, description: 'Direct asset URL of the link.' - - def direct_asset_url - return object.url unless object.filepath - - release = object.release.present - release.download_url(object.filepath) - end end end diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb index 95b6b43bb46..43dc0c4ce85 100644 --- a/app/graphql/types/release_type.rb +++ b/app/graphql/types/release_type.rb @@ -13,6 +13,9 @@ module Types present_using ReleasePresenter + field :id, ::Types::GlobalIDType[Release], + null: false, + description: 'Global ID of the release.' field :assets, Types::ReleaseAssetsType, null: true, method: :itself, description: 'Assets of the release.' field :created_at, Types::TimeType, null: true, diff --git a/app/graphql/types/terraform/state_type.rb b/app/graphql/types/terraform/state_type.rb index bce34a85f85..be17fc41c2c 100644 --- a/app/graphql/types/terraform/state_type.rb +++ b/app/graphql/types/terraform/state_type.rb @@ -38,6 +38,10 @@ module Types null: false, description: 'Timestamp the Terraform state was updated.' + field :deleted_at, Types::TimeType, + null: true, + description: 'Timestamp the Terraform state was deleted.' + def locked_by_user Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.locked_by_user_id).find end diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb index 2db14953308..121515c04db 100644 --- a/app/graphql/types/time_type.rb +++ b/app/graphql/types/time_type.rb @@ -12,6 +12,9 @@ module Types DESC def self.coerce_input(value, ctx) + # arguments can be nil, so don't raise an error + return if value.nil? + Time.parse(value) rescue ArgumentError, TypeError => e raise GraphQL::CoercionError, e.message diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb index f21b2b261a3..0de6b1d6f8a 100644 --- a/app/graphql/types/todo_type.rb +++ b/app/graphql/types/todo_type.rb @@ -53,6 +53,10 @@ module Types description: 'Timestamp this to-do item was created.', null: false + field :note, Types::Notes::NoteType, + description: 'Note which created this to-do item.', + null: true + def project Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find end diff --git a/app/graphql/types/work_item_sort_enum.rb b/app/graphql/types/work_item_sort_enum.rb new file mode 100644 index 00000000000..e644313d409 --- /dev/null +++ b/app/graphql/types/work_item_sort_enum.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + class WorkItemSortEnum < SortEnum + graphql_name 'WorkItemSort' + description 'Values for sorting work items' + + value 'TITLE_ASC', 'Title by ascending order.', value: :title_asc + value 'TITLE_DESC', 'Title by descending order.', value: :title_desc + end +end diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index cd784d54959..18b9bfd1c9a 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -18,6 +18,8 @@ module Types description: 'State of the work item.' field :title, GraphQL::Types::String, null: false, description: 'Title of the work item.' + field :widgets, [Types::WorkItems::WidgetInterface], null: true, + description: 'Collection of widgets that belong to the work item.' field :work_item_type, Types::WorkItems::TypeType, null: false, description: 'Type assigned to the work item.' diff --git a/app/graphql/types/work_items/updated_task_input_type.rb b/app/graphql/types/work_items/updated_task_input_type.rb new file mode 100644 index 00000000000..9f8afa2ff1b --- /dev/null +++ b/app/graphql/types/work_items/updated_task_input_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module WorkItems + class UpdatedTaskInputType < BaseInputObject + graphql_name 'WorkItemUpdatedTaskInput' + + include Mutations::WorkItems::UpdateArguments + end + end +end diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb new file mode 100644 index 00000000000..f3cf1d74829 --- /dev/null +++ b/app/graphql/types/work_items/widget_interface.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module WidgetInterface + include Types::BaseInterface + + graphql_name 'WorkItemWidget' + + field :type, ::Types::WorkItems::WidgetTypeEnum, null: true, + description: 'Widget type.' + + def self.resolve_type(object, context) + case object + when ::WorkItems::Widgets::Description + ::Types::WorkItems::Widgets::DescriptionType + when ::WorkItems::Widgets::Hierarchy + ::Types::WorkItems::Widgets::HierarchyType + else + raise "Unknown GraphQL type for widget #{object}" + end + end + + orphan_types ::Types::WorkItems::Widgets::DescriptionType, + ::Types::WorkItems::Widgets::HierarchyType + end + end +end diff --git a/app/graphql/types/work_items/widget_type_enum.rb b/app/graphql/types/work_items/widget_type_enum.rb new file mode 100644 index 00000000000..4e5933bff86 --- /dev/null +++ b/app/graphql/types/work_items/widget_type_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module WorkItems + class WidgetTypeEnum < BaseEnum + graphql_name 'WorkItemWidgetType' + description 'Type of a work item widget' + + ::WorkItems::Type.available_widgets.each do |widget| + value widget.type.to_s.upcase, value: widget.type, description: "#{widget.type.to_s.titleize} widget." + end + end + end +end diff --git a/app/graphql/types/work_items/widgets/description_input_type.rb b/app/graphql/types/work_items/widgets/description_input_type.rb new file mode 100644 index 00000000000..382cfdf659f --- /dev/null +++ b/app/graphql/types/work_items/widgets/description_input_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + class DescriptionInputType < BaseInputObject + graphql_name 'WorkItemWidgetDescriptionInput' + + argument :description, GraphQL::Types::String, + required: true, + description: copy_field_description(Types::WorkItemType, :description) + end + end + end +end diff --git a/app/graphql/types/work_items/widgets/description_type.rb b/app/graphql/types/work_items/widgets/description_type.rb new file mode 100644 index 00000000000..79192d7c3d4 --- /dev/null +++ b/app/graphql/types/work_items/widgets/description_type.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + # Disabling widget level authorization as it might be too granular + # and we already authorize the parent work item + # rubocop:disable Graphql/AuthorizeTypes + class DescriptionType < BaseObject + graphql_name 'WorkItemWidgetDescription' + description 'Represents a description widget' + + implements Types::WorkItems::WidgetInterface + + field :description, GraphQL::Types::String, null: true, + description: 'Description of the work item.' + + markdown_field :description_html, null: true do |resolved_object| + resolved_object.work_item + end + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb new file mode 100644 index 00000000000..057d5fbf056 --- /dev/null +++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + # Disabling widget level authorization as it might be too granular + # and we already authorize the parent work item + # rubocop:disable Graphql/AuthorizeTypes + class HierarchyType < BaseObject + graphql_name 'WorkItemWidgetHierarchy' + description 'Represents a hierarchy widget' + + implements Types::WorkItems::WidgetInterface + + field :parent, ::Types::WorkItemType, null: true, + description: 'Parent work item.', + complexity: 5 + + field :children, ::Types::WorkItemType.connection_type, null: true, + description: 'Child work items.', + complexity: 5 + + def children + object.children.inc_relations_for_permission_check + end + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end |