1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
# frozen_string_literal: true
module Packages
module Nuget
class SearchService < BaseService
include ::Packages::FinderHelper
include Gitlab::Utils::StrongMemoize
include ActiveRecord::ConnectionAdapters::Quoting
MAX_PER_PAGE = 30
MAX_VERSIONS_PER_PACKAGE = 10
PRE_RELEASE_VERSION_MATCHING_TERM = '%-%'
DEFAULT_OPTIONS = {
include_prerelease_versions: true,
per_page: Kaminari.config.default_per_page,
padding: 0
}.freeze
def initialize(current_user, project_or_group, search_term, options = {})
@current_user = current_user
@project_or_group = project_or_group
@search_term = search_term
@options = DEFAULT_OPTIONS.merge(options)
raise ArgumentError, 'negative per_page' if per_page < 0
raise ArgumentError, 'negative padding' if padding < 0
end
def execute
Result.new(
total_count: non_paginated_matching_package_names.count,
results: search_packages
)
end
private
def search_packages
# custom query to get package names and versions as expected from the nuget search api
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24182#technical-notes
# and https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
subquery_name = :partition_subquery
arel_table = Arel::Table.new(subquery_name)
column_names = Packages::Package.column_names.map do |cn|
"#{subquery_name}.#{quote_column_name(cn)}"
end
# rubocop: disable CodeReuse/ActiveRecord
pkgs = Packages::Package
pkgs = pkgs.with(project_ids_cte.to_arel) if use_project_ids_cte?
pkgs = pkgs.select(column_names.join(','))
.from(package_names_partition, subquery_name)
.where(arel_table[:row_number].lteq(MAX_VERSIONS_PER_PACKAGE))
return pkgs if include_prerelease_versions?
# we can't use pkgs.without_version_like since we have a custom from
pkgs.where.not(arel_table[:version].matches(PRE_RELEASE_VERSION_MATCHING_TERM))
# rubocop: enable CodeReuse/ActiveRecord
end
def package_names_partition
# rubocop: disable CodeReuse/ActiveRecord
table_name = quote_table_name(Packages::Package.table_name)
name_column = "#{table_name}.#{quote_column_name('name')}"
created_at_column = "#{table_name}.#{quote_column_name('created_at')}"
select_sql = "ROW_NUMBER() OVER (PARTITION BY #{name_column} ORDER BY #{created_at_column} DESC) AS row_number, #{table_name}.*"
nuget_packages.select(select_sql)
.with_name(paginated_matching_package_names)
.where(project_id: project_ids)
# rubocop: enable CodeReuse/ActiveRecord
end
def paginated_matching_package_names
pkgs = base_matching_package_names
pkgs.page(0) # we're using a padding
.per(per_page)
.padding(padding)
end
def non_paginated_matching_package_names
# rubocop: disable CodeReuse/ActiveRecord
pkgs = base_matching_package_names
pkgs = pkgs.with(project_ids_cte.to_arel) if use_project_ids_cte?
pkgs
# rubocop: enable CodeReuse/ActiveRecord
end
def base_matching_package_names
strong_memoize(:base_matching_package_names) do
# rubocop: disable CodeReuse/ActiveRecord
pkgs = nuget_packages.order_name
.select_distinct_name
.where(project_id: project_ids)
pkgs = pkgs.without_version_like(PRE_RELEASE_VERSION_MATCHING_TERM) unless include_prerelease_versions?
pkgs = pkgs.search_by_name(@search_term) if @search_term.present?
pkgs
# rubocop: enable CodeReuse/ActiveRecord
end
end
def nuget_packages
Packages::Package.nuget
.has_version
.without_nuget_temporary_name
end
def project_ids_cte
return unless use_project_ids_cte?
strong_memoize(:project_ids_cte) do
query = projects_visible_to_user(@current_user, within_group: @project_or_group)
Gitlab::SQL::CTE.new(:project_ids, query.select(:id))
end
end
def project_ids
return @project_or_group.id if project?
if use_project_ids_cte?
# rubocop: disable CodeReuse/ActiveRecord
Project.select(:id)
.from(project_ids_cte.table)
# rubocop: enable CodeReuse/ActiveRecord
end
end
def use_project_ids_cte?
group?
end
def project?
@project_or_group.is_a?(::Project)
end
def group?
@project_or_group.is_a?(::Group)
end
def include_prerelease_versions?
@options[:include_prerelease_versions]
end
def padding
@options[:padding]
end
def per_page
[@options[:per_page], MAX_PER_PAGE].min
end
class Result
include ActiveModel::Model
attr_accessor :results, :total_count
end
end
end
end
|