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
|
# frozen_string_literal: true
module Packages
module Npm
class GenerateMetadataService
include API::Helpers::RelatedResourcesHelpers
include Gitlab::Utils::StrongMemoize
# Allowed fields are those defined in the abbreviated form
# defined here: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
# except: name, version, dist, dependencies and xDependencies. Those are generated by this service.
PACKAGE_JSON_ALLOWED_FIELDS = %w[deprecated bin directories dist engines _hasShrinkwrap].freeze
def initialize(name, packages)
@name = name
@packages = packages
@dependencies = {}
@dependency_ids = Hash.new { |h, key| h[key] = {} }
end
def execute(only_dist_tags: false)
ServiceResponse.success(payload: metadata(only_dist_tags))
end
private
attr_reader :name, :packages, :dependencies, :dependency_ids
def metadata(only_dist_tags)
result = { dist_tags: dist_tags }
unless only_dist_tags
result[:name] = name
result[:versions] = versions
end
result
end
def versions
package_versions = {}
packages.each_batch do |relation|
load_dependencies(relation)
load_dependency_ids(relation)
batched_packages = relation.preload_files
.preload_npm_metadatum
batched_packages.each do |package|
package_file = package.installable_package_files.last
next unless package_file
package_versions[package.version] = build_package_version(package, package_file)
end
end
package_versions
end
def dist_tags
build_package_tags.tap { |t| t['latest'] ||= sorted_versions.last }
end
def build_package_tags
package_tags.to_h { |tag| [tag.name, tag.package.version] }
end
def build_package_version(package, package_file)
abbreviated_package_json(package).merge(
name: package.name,
version: package.version,
dist: {
shasum: package_file.file_sha1,
tarball: tarball_url(package, package_file)
}
).tap do |package_version|
package_version.merge!(build_package_dependencies(package))
end
end
def tarball_url(package, package_file)
expose_url api_v4_projects_packages_npm_package_name___file_name_path(
{ id: package.project_id, package_name: package.name, file_name: package_file.file_name }, true
)
end
def build_package_dependencies(package)
dependency_ids[package.id].each_with_object(Hash.new { |h, key| h[key] = {} }) do |(type, ids), memo|
ids.each do |id|
memo[inverted_dependency_types[type]].merge!(dependencies[id])
end
end
end
def inverted_dependency_types
Packages::DependencyLink.dependency_types.invert.stringify_keys
end
strong_memoize_attr :inverted_dependency_types
def sorted_versions
versions = packages.pluck_versions.compact
VersionSorter.sort(versions)
end
def package_tags
Packages::Tag.for_package_ids(packages)
.preload_package
end
def abbreviated_package_json(package)
json = package.npm_metadatum&.package_json || {}
json.slice(*PACKAGE_JSON_ALLOWED_FIELDS)
end
def load_dependencies(packages)
Packages::Dependency
.id_in(
Packages::DependencyLink
.for_packages(packages)
.select_dependency_id
)
.id_not_in(dependencies.keys)
.each_batch do |relation|
relation.each do |dependency|
dependencies[dependency.id] = { dependency.name => dependency.version_pattern }
end
end
end
def load_dependency_ids(packages)
Packages::DependencyLink
.dependency_ids_grouped_by_type(packages)
.each_batch(column: :package_id) do |relation|
relation.each do |dependency_link|
dependency_ids[dependency_link.package_id] = dependency_link.dependency_ids_by_type
end
end
end
end
end
end
|