diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb index a51057571f1f937737a79587c94778a331142957..860c4068b31ae9bbd655d1d4b13aa54900130604 100644 --- a/app/finders/packages/group_packages_finder.rb +++ b/app/finders/packages/group_packages_finder.rb @@ -27,9 +27,9 @@ module Packages .including_tags .for_projects(group_projects_visible_to_current_user.select(:id)) .processed - .has_version .sort_by_attribute("#{params[:order_by]}_#{params[:sort]}") + packages = filter_with_version(packages) packages = filter_by_package_type(packages) packages = filter_by_package_name(packages) packages @@ -72,5 +72,11 @@ module Packages packages.search_by_name(params[:package_name]) end + + def filter_with_version(packages) + return packages if params[:include_versionless].present? + + packages.has_version + end end end diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb index 519e8bf9c34f2866208603d6dc908368f409d777..72a63224d2f6bf34b2cab929655089c378bf8ba7 100644 --- a/app/finders/packages/packages_finder.rb +++ b/app/finders/packages/packages_finder.rb @@ -18,7 +18,7 @@ module Packages .including_project_route .including_tags .processed - .has_version + packages = filter_with_version(packages) packages = filter_by_package_type(packages) packages = filter_by_package_name(packages) packages = order_packages(packages) @@ -27,6 +27,12 @@ module Packages private + def filter_with_version(packages) + return packages if params[:include_versionless].present? + + packages.has_version + end + def filter_by_package_type(packages) return packages unless params[:package_type] diff --git a/changelogs/unreleased/versionless-package-api-option.yml b/changelogs/unreleased/versionless-package-api-option.yml new file mode 100644 index 0000000000000000000000000000000000000000..c929fe8c33797149e92be12ebe1c3a708e96e63e --- /dev/null +++ b/changelogs/unreleased/versionless-package-api-option.yml @@ -0,0 +1,5 @@ +--- +title: Add include_versionless param to the Package API +merge_request: 50669 +author: +type: added diff --git a/doc/api/packages.md b/doc/api/packages.md index a52487e35a3cca7f12bda7723b0d8e8ffa0db133..2f588954ae11db28d77752f1f8ff7a8ec269c932 100644 --- a/doc/api/packages.md +++ b/doc/api/packages.md @@ -28,6 +28,7 @@ GET /projects/:id/packages | `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. | | `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, or `golang`. (_Introduced in GitLab 12.9_) | `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_Introduced in GitLab 12.9_) +| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_) ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/packages" @@ -88,6 +89,7 @@ GET /groups/:id/packages | `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. | | `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi`, `composer`, `nuget`, or `golang`. (_Introduced in GitLab 12.9_) | | `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30980) in GitLab 13.0_) +| `include_versionless` | boolean | no | When set to true, versionless packages are included in the response. (_Introduced in GitLab 13.8_) ```shell curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/packages?exclude_subgroups=true" diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb index 31b28c3990f8c4f548878f4a4ee4b9bcb84a20e3..d482f4d05851fda229414f374bcbab2416c3c5be 100644 --- a/lib/api/group_packages.rb +++ b/lib/api/group_packages.rb @@ -31,12 +31,14 @@ module API desc: 'Return packages of a certain type' optional :package_name, type: String, desc: 'Return packages with this name' + optional :include_versionless, type: Boolean, + desc: 'Returns packages without a version' end get ':id/packages' do packages = Packages::GroupPackagesFinder.new( current_user, user_group, - declared(params).slice(:exclude_subgroups, :order_by, :sort, :package_type, :package_name) + declared(params).slice(:exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless) ).execute present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index 56e94333433ee71bdd532f7f840a26330b37ff25..32636662987b7d927e9b76d3fb847f6ff47a2958 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -30,11 +30,13 @@ module API desc: 'Return packages of a certain type' optional :package_name, type: String, desc: 'Return packages with this name' + optional :include_versionless, type: Boolean, + desc: 'Returns packages without a version' end get ':id/packages' do packages = ::Packages::PackagesFinder.new( user_project, - declared_params.slice(:order_by, :sort, :package_type, :package_name) + declared_params.slice(:order_by, :sort, :package_type, :package_name, :include_versionless) ).execute present paginate(packages), with: ::API::Entities::Package, user: current_user diff --git a/spec/finders/packages/group_packages_finder_spec.rb b/spec/finders/packages/group_packages_finder_spec.rb index 0db69de65a5c845f46d538cdcffef28e4c0dd331..8dd53b9c3f9813d5a7c9adc6caaa17452d927f2e 100644 --- a/spec/finders/packages/group_packages_finder_spec.rb +++ b/spec/finders/packages/group_packages_finder_spec.rb @@ -127,12 +127,6 @@ RSpec.describe Packages::GroupPackagesFinder do it { is_expected.to match_array([package1, package2]) } end - context 'does not include packages without version number' do - let_it_be(:package_without_version) { create(:maven_package, project: project, version: nil) } - - it { is_expected.not_to include(package_without_version) } - end - context 'with package_name' do let_it_be(:named_package) { create(:maven_package, project: project, name: 'maven') } let(:params) { { package_name: package_name } } @@ -151,6 +145,8 @@ RSpec.describe Packages::GroupPackagesFinder do end end end + + it_behaves_like 'concerning versionless param' end context 'group has package of all types' do diff --git a/spec/finders/packages/packages_finder_spec.rb b/spec/finders/packages/packages_finder_spec.rb index 925b003bb8eefdb12063821efee7d626d6cecc2e..77a171db144c5acd866fe743309a73d81480412d 100644 --- a/spec/finders/packages/packages_finder_spec.rb +++ b/spec/finders/packages/packages_finder_spec.rb @@ -81,10 +81,6 @@ RSpec.describe ::Packages::PackagesFinder do it { is_expected.to match_array([conan_package, maven_package]) } end - context 'does not include packages without version number' do - let_it_be(:package_without_version) { create(:maven_package, project: project, version: nil) } - - it { is_expected.not_to include(package_without_version) } - end + it_behaves_like 'concerning versionless param' end end diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb index 72ba25c59afed8b5eba1547f2c4e8b2f6cb43a38..26895e473ded2d8f7a9dc6e466c7a4d18840f30e 100644 --- a/spec/requests/api/group_packages_spec.rb +++ b/spec/requests/api/group_packages_spec.rb @@ -6,8 +6,9 @@ RSpec.describe API::GroupPackages do let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, :public, namespace: group, name: 'project A') } let_it_be(:user) { create(:user) } + let(:params) { {} } - subject { get api(url) } + subject { get api(url), params: params } describe 'GET /groups/:id/packages' do let(:url) { "/groups/#{group.id}/packages" } @@ -142,6 +143,7 @@ RSpec.describe API::GroupPackages do it_behaves_like 'returning response status', :bad_request end + it_behaves_like 'with versionless packages' it_behaves_like 'does not cause n^2 queries' end end diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb index 4c8599d1a20d36fca3cf65ece1d643826f581fb3..eb86df36dbbc8288f1c46b395b9e2cbfee4f53d0 100644 --- a/spec/requests/api/project_packages_spec.rb +++ b/spec/requests/api/project_packages_spec.rb @@ -11,12 +11,13 @@ RSpec.describe API::ProjectPackages do let!(:another_package) { create(:npm_package) } let(:no_package_url) { "/projects/#{project.id}/packages/0" } let(:wrong_package_url) { "/projects/#{project.id}/packages/#{another_package.id}" } + let(:params) { {} } describe 'GET /projects/:id/packages' do let(:url) { "/projects/#{project.id}/packages" } let(:package_schema) { 'public_api/v4/packages/packages' } - subject { get api(url) } + subject { get api(url), params: params } context 'without the need for a license' do context 'project is public' do @@ -118,6 +119,7 @@ RSpec.describe API::ProjectPackages do end end + it_behaves_like 'with versionless packages' it_behaves_like 'does not cause n^2 queries' end end diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..52976565b21cce638c8862608aa6dde060c6f0f9 --- /dev/null +++ b/spec/support/shared_examples/finders/packages_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'concerning versionless param' do + let_it_be(:versionless_package) { create(:maven_package, project: project, version: nil) } + + it { is_expected.not_to include(versionless_package) } + + context 'with valid include_versionless param' do + let(:params) { { include_versionless: true } } + + it { is_expected.to include(versionless_package) } + end + + context 'with empty include_versionless param' do + let(:params) { { include_versionless: '' } } + + it { is_expected.not_to include(versionless_package) } + end +end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 70d29b4bc99d9d6fe81aa82d5e55bd71693aa275..fa307d2a9a62ffcac0e3364d11ad2566f611ca8f 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -220,3 +220,45 @@ RSpec.shared_examples 'package workhorse uploads' do end end end + +RSpec.shared_examples 'with versionless packages' do + context 'with versionless package' do + let!(:versionless_package) { create(:maven_package, project: project, version: nil) } + + shared_examples 'not including the package' do + it 'does not return the package' do + subject + + expect(json_response.map { |package| package['id'] }).not_to include(versionless_package.id) + end + end + + it_behaves_like 'not including the package' + + context 'with include_versionless param' do + context 'with true include_versionless param' do + [true, 'true', 1, '1'].each do |param| + context "for param #{param}" do + let(:params) { super().merge(include_versionless: param) } + + it 'returns the package' do + subject + + expect(json_response.map { |package| package['id'] }).to include(versionless_package.id) + end + end + end + end + + context 'with falsy include_versionless param' do + [false, '', nil, 'false', 0, '0'].each do |param| + context "for param #{param}" do + let(:params) { super().merge(include_versionless: param) } + + it_behaves_like 'not including the package' + end + end + end + end + end +end