Commit 53060a18 authored by David Fernandez's avatar David Fernandez

Update the npm package finder

To let clients choose if they want to load the last package of each
version or not. Depending on what they do with the result of the finder,
the last_of_each_version scope can trigger queries with duplicated SQL
conditions which run poorly.
parent 37ec7b18
......@@ -5,17 +5,23 @@ module Packages
delegate :find_by_version, to: :execute
delegate :last, to: :execute
def initialize(package_name, project: nil, namespace: nil)
# /!\ CAUTION: don't use last_of_each_version: false with find_by_version. Ordering is not
# guaranteed!
def initialize(package_name, project: nil, namespace: nil, last_of_each_version: true)
@package_name = package_name
@project = project
@namespace = namespace
@last_of_each_version = last_of_each_version
end
def execute
base.npm
.with_name(@package_name)
.installable
.last_of_each_version
result = base.npm
.with_name(@package_name)
.installable
return result unless @last_of_each_version
result.last_of_each_version
end
private
......
---
name: npm_finder_query_avoid_duplicated_conditions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69572
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340099
milestone: '14.3'
type: development
group: group::package
default_enabled: false
......@@ -57,7 +57,11 @@ module API
.by_path(namespace_path)
next unless namespace
finder = ::Packages::Npm::PackageFinder.new(package_name, namespace: namespace)
finder = ::Packages::Npm::PackageFinder.new(
package_name,
namespace: namespace,
last_of_each_version: Feature.disabled?(:npm_finder_query_avoid_duplicated_conditions)
)
finder.last&.project_id
end
......
......@@ -7,6 +7,7 @@ RSpec.describe ::Packages::Npm::PackageFinder do
let(:project) { package.project }
let(:package_name) { package.name }
let(:last_of_each_version) { true }
shared_examples 'accepting a namespace for' do |example_name|
before do
......@@ -38,6 +39,8 @@ RSpec.describe ::Packages::Npm::PackageFinder do
end
describe '#execute' do
subject { finder.execute }
shared_examples 'finding packages by name' do
it { is_expected.to eq([package]) }
......@@ -56,13 +59,27 @@ RSpec.describe ::Packages::Npm::PackageFinder do
end
end
subject { finder.execute }
shared_examples 'handling last_of_each_version' do
include_context 'last_of_each_version setup context'
context 'disabled' do
let(:last_of_each_version) { false }
it { is_expected.to contain_exactly(package1, package2) }
end
context 'enabled' do
it { is_expected.to contain_exactly(package2) }
end
end
context 'with a project' do
let(:finder) { described_class.new(package_name, project: project) }
let(:finder) { described_class.new(package_name, project: project, last_of_each_version: last_of_each_version) }
it_behaves_like 'finding packages by name'
it_behaves_like 'handling last_of_each_version'
context 'set to nil' do
let(:project) { nil }
......@@ -71,10 +88,12 @@ RSpec.describe ::Packages::Npm::PackageFinder do
end
context 'with a namespace' do
let(:finder) { described_class.new(package_name, namespace: namespace) }
let(:finder) { described_class.new(package_name, namespace: namespace, last_of_each_version: last_of_each_version) }
it_behaves_like 'accepting a namespace for', 'finding packages by name'
it_behaves_like 'accepting a namespace for', 'handling last_of_each_version'
context 'set to nil' do
let_it_be(:namespace) { nil }
......@@ -98,16 +117,28 @@ RSpec.describe ::Packages::Npm::PackageFinder do
end
end
shared_examples 'handling last_of_each_version' do
include_context 'last_of_each_version setup context'
context 'enabled' do
it { is_expected.to eq(package2) }
end
end
context 'with a project' do
let(:finder) { described_class.new(package_name, project: project) }
let(:finder) { described_class.new(package_name, project: project, last_of_each_version: last_of_each_version) }
it_behaves_like 'finding packages by version'
it_behaves_like 'handling last_of_each_version'
end
context 'with a namespace' do
let(:finder) { described_class.new(package_name, namespace: namespace) }
let(:finder) { described_class.new(package_name, namespace: namespace, last_of_each_version: last_of_each_version) }
it_behaves_like 'accepting a namespace for', 'finding packages by version'
it_behaves_like 'accepting a namespace for', 'handling last_of_each_version'
end
end
......@@ -118,35 +149,49 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it { is_expected.to eq(package) }
end
shared_examples 'handling project or namespace parameter' do
context 'with a project' do
let(:finder) { described_class.new(package_name, project: project) }
shared_examples 'handling last_of_each_version' do
include_context 'last_of_each_version setup context'
context 'disabled' do
let(:last_of_each_version) { false }
it { is_expected.to eq(package2) }
end
it_behaves_like 'finding package by last'
context 'enabled' do
it { is_expected.to eq(package2) }
end
end
context 'with a project' do
let(:finder) { described_class.new(package_name, project: project, last_of_each_version: last_of_each_version) }
context 'with a namespace' do
let(:finder) { described_class.new(package_name, namespace: namespace) }
it_behaves_like 'finding package by last'
it_behaves_like 'accepting a namespace for', 'finding package by last'
it_behaves_like 'handling last_of_each_version'
end
context 'with a namespace' do
let(:finder) { described_class.new(package_name, namespace: namespace) }
context 'with duplicate packages' do
let_it_be(:namespace) { create(:group) }
let_it_be(:subgroup1) { create(:group, parent: namespace) }
let_it_be(:subgroup2) { create(:group, parent: namespace) }
let_it_be(:project2) { create(:project, namespace: subgroup2) }
let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
it_behaves_like 'accepting a namespace for', 'finding package by last'
before do
project.update!(namespace: subgroup1)
end
it_behaves_like 'accepting a namespace for', 'handling last_of_each_version'
# the most recent one is returned
it { is_expected.to eq(package2) }
context 'with duplicate packages' do
let_it_be(:namespace) { create(:group) }
let_it_be(:subgroup1) { create(:group, parent: namespace) }
let_it_be(:subgroup2) { create(:group, parent: namespace) }
let_it_be(:project2) { create(:project, namespace: subgroup2) }
let_it_be(:package2) { create(:npm_package, name: package.name, project: project2) }
before do
project.update!(namespace: subgroup1)
end
# the most recent one is returned
it { is_expected.to eq(package2) }
end
end
it_behaves_like 'handling project or namespace parameter'
end
end
......@@ -10,27 +10,39 @@ RSpec.describe API::NpmInstancePackages do
include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do
it_behaves_like 'handling get metadata requests', scope: :instance do
let(:url) { api("/packages/npm/#{package_name}") }
shared_examples 'handling all endpoints' do
describe 'GET /api/v4/packages/npm/*package_name' do
it_behaves_like 'handling get metadata requests', scope: :instance do
let(:url) { api("/packages/npm/#{package_name}") }
end
end
end
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
it_behaves_like 'handling get dist tags requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags") }
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
it_behaves_like 'handling get dist tags requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags") }
end
end
describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling create dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
end
describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling create dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling delete dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
end
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling delete dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
it_behaves_like 'handling all endpoints'
context 'with npm_finder_query_avoid_duplicated_conditions disabled' do
before do
stub_feature_flags(npm_finder_query_avoid_duplicated_conditions: false)
end
it_behaves_like 'handling all endpoints'
end
end
# frozen_string_literal: true
RSpec.shared_context 'last_of_each_version setup context' do
let_it_be(:package1) { create(:npm_package, name: 'test', version: '1.2.3', project: project) }
let_it_be(:package2) { create(:npm_package, name: 'test2', version: '1.2.3', project: project) }
let(:package_name) { 'test' }
let(:version) { '1.2.3' }
before do
# create a duplicated package without triggering model validation errors
package2.update_column(:name, 'test')
end
end
......@@ -8,7 +8,8 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:group) { create(:group, name: 'test-group') }
let_it_be(:namespace) { group }
let_it_be(:project, reload: true) { create(:project, :public, namespace: namespace) }
let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:package1, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package", version: '1.2.4') }
let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package", version: '1.2.3') }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) }
......@@ -16,6 +17,11 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:package_name) { package.name }
before do
# create a duplicated package without triggering model validation errors
package1.update_column(:version, '1.2.3')
end
end
RSpec.shared_context 'set package name from package name type' do
......
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