Commit 3ff12715 authored by Maxime Orefice's avatar Maxime Orefice Committed by Tiger Watson

Add namespace projects finder

This commits introduces a new finder which will allow us
to fetch projects at the namespace level with our graphql api.
parent f85dc2a0
# frozen_string_literal: true
# Namespaces::ProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user
# namespace
# params:
# sort: string
# search: string
# include_subgroups: boolean
# ids: int[]
#
module Namespaces
class ProjectsFinder
def initialize(namespace: nil, current_user: nil, params: {})
@namespace = namespace
@current_user = current_user
@params = params
end
def execute
return Project.none if namespace.nil?
collection = if params[:include_subgroups].present?
namespace.all_projects.with_route
else
namespace.projects.with_route
end
filter_projects(collection)
end
private
attr_reader :namespace, :params, :current_user
def filter_projects(collection)
collection = by_ids(collection)
collection = by_similarity(collection)
collection
end
def by_ids(items)
return items unless params[:ids].present?
items.id_in(params[:ids])
end
def by_similarity(items)
return items unless params[:search].present?
if params[:sort] == :similarity
items = items.sorted_by_similarity_desc(params[:search], include_in_select: true)
end
items.merge(Project.search(params[:search]))
end
end
end
Namespaces::ProjectsFinder.prepend_if_ee('::EE::Namespaces::ProjectsFinder')
...@@ -24,22 +24,16 @@ module Resolvers ...@@ -24,22 +24,16 @@ module Resolvers
type Types::ProjectType, null: true type Types::ProjectType, null: true
def resolve(include_subgroups:, sort:, search:, ids:) def resolve(args)
# The namespace could have been loaded in batch by `BatchLoader`. # The namespace could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` or the `full_path` of the namespace # At this point we need the `id` or the `full_path` of the namespace
# to query for projects, so make sure it's loaded and not `nil` before continuing. # to query for projects, so make sure it's loaded and not `nil` before continuing.
return Project.none if namespace.nil?
query = include_subgroups ? namespace.all_projects.with_route : namespace.projects.with_route ::Namespaces::ProjectsFinder.new(
query = ids ? query.merge(Project.where(id: parse_gids(ids))) : query # rubocop: disable CodeReuse/ActiveRecord namespace: namespace,
current_user: current_user,
return query unless search.present? params: finder_params(args)
).execute
if sort == :similarity
query.sorted_by_similarity_desc(search, include_in_select: true).merge(Project.search(search))
else
query.merge(Project.search(search))
end
end end
def self.resolver_complexity(args, child_complexity:) def self.resolver_complexity(args, child_complexity:)
...@@ -55,6 +49,15 @@ module Resolvers ...@@ -55,6 +49,15 @@ module Resolvers
end end
end end
def finder_params(args)
{
include_subgroups: args.dig(:include_subgroups),
sort: args.dig(:sort),
search: args.dig(:search),
ids: parse_gids(args.dig(:ids))
}
end
def parse_gids(gids) def parse_gids(gids)
gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id } gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Project).model_id }
end end
......
# frozen_string_literal: true
module EE
# Namespaces::ProjectsFinder
#
# Extends Namespaces::ProjectsFinder
#
# Added arguments:
# params:
# has_vulnerabilities: boolean
# has_code_coverage: boolean
#
module Namespaces
module ProjectsFinder
extend ::Gitlab::Utils::Override
private
override :filter_projects
def filter_projects(collection)
collection = super(collection)
collection = with_vulnerabilities(collection)
collection = with_code_coverage(collection)
collection = by_storage(collection)
collection
end
def by_storage(items)
return items if params[:sort] != :storage
items.order_by_total_repository_size_excess_desc(namespace.actual_size_limit)
end
def with_vulnerabilities(items)
return items unless params[:has_vulnerabilities].present?
items.has_vulnerabilities
end
def with_code_coverage(items)
return items unless params[:has_code_coverage].present?
items.with_code_coverage
end
end
end
end
...@@ -4,6 +4,7 @@ module EE ...@@ -4,6 +4,7 @@ module EE
module Resolvers module Resolvers
module NamespaceProjectsResolver module NamespaceProjectsResolver
extend ActiveSupport::Concern extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
prepended do prepended do
argument :has_code_coverage, GraphQL::BOOLEAN_TYPE, argument :has_code_coverage, GraphQL::BOOLEAN_TYPE,
...@@ -17,12 +18,14 @@ module EE ...@@ -17,12 +18,14 @@ module EE
description: 'Returns only the projects which have vulnerabilities.' description: 'Returns only the projects which have vulnerabilities.'
end end
def resolve(include_subgroups:, search:, sort:, ids:, has_vulnerabilities: false, has_code_coverage: false) private
projects = super(include_subgroups: include_subgroups, search: search, sort: sort, ids: ids)
projects = projects.has_vulnerabilities if has_vulnerabilities override :finder_params
projects = projects.with_code_coverage if has_code_coverage def finder_params(args)
projects = projects.order_by_total_repository_size_excess_desc(namespace.actual_size_limit) if sort == :storage super(args).merge(
projects has_vulnerabilities: args.dig(:has_vulnerabilities),
has_code_coverage: args.dig(:has_code_coverage)
)
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::ProjectsFinder do
let_it_be(:current_user) { create(:user) }
let_it_be(:namespace) { create(:group, :public) }
let_it_be(:subgroup) { create(:group, parent: namespace) }
let_it_be(:project_1) { create(:project, :public, group: namespace, path: 'project', name: 'Project') }
let_it_be(:project_2) { create(:project, :public, group: namespace, path: 'test-project', name: 'Test Project') }
let_it_be(:project_3) { create(:project, :public, group: subgroup, path: 'test-subgroup', name: 'Subgroup Project') }
let(:params) { {} }
let(:finder) { described_class.new(namespace: namespace, params: params, current_user: current_user) }
subject(:projects) { finder.execute }
describe '#execute' do
context 'has_vulnerabilities' do
before do
project_1.project_setting.update!(has_vulnerabilities: true)
end
context 'when has_vulnerabilities is provided' do
let(:params) { { has_vulnerabilities: true } }
it 'returns projects with vulnerabilities' do
expect(projects).to contain_exactly(project_1)
end
end
context 'when has_vulnerabilities is not provided' do
it 'returns all projects' do
expect(projects).to contain_exactly(project_1, project_2)
end
end
end
context 'sorting' do
before do
project_1.statistics.update!(lfs_objects_size: 11, repository_size: 10)
project_2.statistics.update!(lfs_objects_size: 10, repository_size: 12)
end
context 'when sort equals :storage' do
let(:params) { { sort: :storage } }
it 'returns projects sorted by storage' do
expect(projects).to eq [project_2, project_1]
end
end
context 'when sort does not equal :storage' do
it 'returns all projects' do
expect(projects).to eq [project_1, project_2]
end
end
end
context 'has_code_coverage' do
let_it_be(:coverage_1) { create(:ci_daily_build_group_report_result, project: project_1) }
context 'when has_code_coverage is provided' do
let(:params) { { has_code_coverage: true } }
it 'returns projects with code coverage' do
expect(projects).to contain_exactly(project_1)
end
end
context 'when has_code_coverage is not provided' do
it 'returns all projects' do
expect(projects).to contain_exactly(project_1, project_2)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespaces::ProjectsFinder do
let_it_be(:current_user) { create(:user) }
let_it_be(:namespace) { create(:group, :public) }
let_it_be(:subgroup) { create(:group, parent: namespace) }
let_it_be(:project_1) { create(:project, :public, group: namespace, path: 'project', name: 'Project') }
let_it_be(:project_2) { create(:project, :public, group: namespace, path: 'test-project', name: 'Test Project') }
let_it_be(:project_3) { create(:project, :public, path: 'sub-test-project', group: subgroup, name: 'Sub Test Project') }
let_it_be(:project_4) { create(:project, :public, path: 'test-project-2', group: namespace, name: 'Test Project 2') }
let(:params) { {} }
let(:finder) { described_class.new(namespace: namespace, params: params, current_user: current_user) }
subject(:projects) { finder.execute }
describe '#execute' do
context 'without a namespace' do
let(:namespace) { nil }
it 'returns an empty array' do
expect(projects).to be_empty
end
end
context 'with a namespace' do
it 'returns the project for the namespace' do
expect(projects).to contain_exactly(project_1, project_2, project_4)
end
context 'when include_subgroups is provided' do
let(:params) { { include_subgroups: true } }
it 'returns all projects for the namespace' do
expect(projects).to contain_exactly(project_1, project_2, project_3, project_4)
end
context 'when ids are provided' do
let(:params) { { include_subgroups: true, ids: [project_3.id] } }
it 'returns all projects for the ids' do
expect(projects).to contain_exactly(project_3)
end
end
end
context 'when ids are provided' do
let(:params) { { ids: [project_1.id] } }
it 'returns all projects for the ids' do
expect(projects).to contain_exactly(project_1)
end
end
context 'when sort is similarity' do
let(:params) { { sort: :similarity, search: 'test' } }
it 'returns projects by similarity' do
expect(projects).to eq([project_2, project_4])
end
end
context 'when search parameter is missing' do
let(:params) { { sort: :similarity } }
it 'returns all projects' do
expect(projects).to eq([project_1, project_2, project_4])
end
end
context 'when sort parameter is missing' do
let(:params) { { search: 'test' } }
it 'returns matching projects' do
expect(projects).to eq([project_2, project_4])
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment