Commit d0093862 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Stan Hu

Add package managers api paths to details type

Add all the package managers API paths to package details
type

Changelog: added
parent 90ab79b4
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Types module Types
module Packages module Packages
class PackageDetailsType < PackageType class PackageDetailsType < PackageType
include ::PackagesHelper
graphql_name 'PackageDetailsType' graphql_name 'PackageDetailsType'
description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes' description 'Represents a package details in the Package Registry. Note that this type is in beta and susceptible to changes'
authorize :read_package authorize :read_package
...@@ -21,6 +23,15 @@ module Types ...@@ -21,6 +23,15 @@ module Types
description: 'Pipelines that built the package.', description: 'Pipelines that built the package.',
deprecated: { reason: 'Due to scalability concerns, this field is going to be removed', milestone: '14.6' } deprecated: { reason: 'Due to scalability concerns, this field is going to be removed', milestone: '14.6' }
field :composer_config_repository_url, GraphQL::Types::String, null: true, description: 'Url of the Composer setup endpoint.'
field :composer_url, GraphQL::Types::String, null: true, description: 'Url of the Composer endpoint.'
field :conan_url, GraphQL::Types::String, null: true, description: 'Url of the Conan project endpoint.'
field :maven_url, GraphQL::Types::String, null: true, description: 'Url of the Maven project endpoint.'
field :npm_url, GraphQL::Types::String, null: true, description: 'Url of the NPM project endpoint.'
field :nuget_url, GraphQL::Types::String, null: true, description: 'Url of the Nuget project endpoint.'
field :pypi_setup_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project setup endpoint.'
field :pypi_url, GraphQL::Types::String, null: true, description: 'Url of the PyPi project endpoint.'
def versions def versions
object.versions object.versions
end end
...@@ -32,6 +43,38 @@ module Types ...@@ -32,6 +43,38 @@ module Types
object.package_files object.package_files
end end
end end
def composer_config_repository_url
composer_config_repository_name(object.project.group&.id)
end
def composer_url
composer_registry_url(object.project.group&.id)
end
def conan_url
package_registry_project_url(object.project.id, :conan)
end
def maven_url
package_registry_project_url(object.project.id, :maven)
end
def npm_url
package_registry_project_url(object.project.id, :npm)
end
def nuget_url
nuget_package_registry_url(object.project.id)
end
def pypi_setup_url
package_registry_project_url(object.project.id, :pypi)
end
def pypi_url
pypi_registry_url(object.project.id)
end
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module PackagesHelper module PackagesHelper
include ::API::Helpers::RelatedResourcesHelpers
def package_sort_path(options = {}) def package_sort_path(options = {})
"#{request.path}?#{options.to_param}" "#{request.path}?#{options.to_param}"
end end
......
...@@ -12683,15 +12683,23 @@ Represents a package details in the Package Registry. Note that this type is in ...@@ -12683,15 +12683,23 @@ Represents a package details in the Package Registry. Note that this type is in
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="packagedetailstypecandestroy"></a>`canDestroy` | [`Boolean!`](#boolean) | Whether the user can destroy the package. | | <a id="packagedetailstypecandestroy"></a>`canDestroy` | [`Boolean!`](#boolean) | Whether the user can destroy the package. |
| <a id="packagedetailstypecomposerconfigrepositoryurl"></a>`composerConfigRepositoryUrl` | [`String`](#string) | Url of the Composer setup endpoint. |
| <a id="packagedetailstypecomposerurl"></a>`composerUrl` | [`String`](#string) | Url of the Composer endpoint. |
| <a id="packagedetailstypeconanurl"></a>`conanUrl` | [`String`](#string) | Url of the Conan project endpoint. |
| <a id="packagedetailstypecreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. | | <a id="packagedetailstypecreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
| <a id="packagedetailstypedependencylinks"></a>`dependencyLinks` | [`PackageDependencyLinkConnection`](#packagedependencylinkconnection) | Dependency link. (see [Connections](#connections)) | | <a id="packagedetailstypedependencylinks"></a>`dependencyLinks` | [`PackageDependencyLinkConnection`](#packagedependencylinkconnection) | Dependency link. (see [Connections](#connections)) |
| <a id="packagedetailstypeid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the package. | | <a id="packagedetailstypeid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the package. |
| <a id="packagedetailstypemavenurl"></a>`mavenUrl` | [`String`](#string) | Url of the Maven project endpoint. |
| <a id="packagedetailstypemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. | | <a id="packagedetailstypemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. |
| <a id="packagedetailstypename"></a>`name` | [`String!`](#string) | Name of the package. | | <a id="packagedetailstypename"></a>`name` | [`String!`](#string) | Name of the package. |
| <a id="packagedetailstypenpmurl"></a>`npmUrl` | [`String`](#string) | Url of the NPM project endpoint. |
| <a id="packagedetailstypenugeturl"></a>`nugetUrl` | [`String`](#string) | Url of the Nuget project endpoint. |
| <a id="packagedetailstypepackagefiles"></a>`packageFiles` | [`PackageFileConnection`](#packagefileconnection) | Package files. (see [Connections](#connections)) | | <a id="packagedetailstypepackagefiles"></a>`packageFiles` | [`PackageFileConnection`](#packagefileconnection) | Package files. (see [Connections](#connections)) |
| <a id="packagedetailstypepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. | | <a id="packagedetailstypepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagedetailstypepipelines"></a>`pipelines` **{warning-solid}** | [`PipelineConnection`](#pipelineconnection) | **Deprecated** in 14.6. Due to scalability concerns, this field is going to be removed. | | <a id="packagedetailstypepipelines"></a>`pipelines` **{warning-solid}** | [`PipelineConnection`](#pipelineconnection) | **Deprecated** in 14.6. Due to scalability concerns, this field is going to be removed. |
| <a id="packagedetailstypeproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. | | <a id="packagedetailstypeproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagedetailstypepypisetupurl"></a>`pypiSetupUrl` | [`String`](#string) | Url of the PyPi project setup endpoint. |
| <a id="packagedetailstypepypiurl"></a>`pypiUrl` | [`String`](#string) | Url of the PyPi project endpoint. |
| <a id="packagedetailstypestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. | | <a id="packagedetailstypestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
| <a id="packagedetailstypetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) | | <a id="packagedetailstypetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
| <a id="packagedetailstypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. | | <a id="packagedetailstypeupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
...@@ -149,6 +149,30 @@ ...@@ -149,6 +149,30 @@
} }
} }
} }
},
"npmUrl": {
"type": "string"
},
"mavenUrl": {
"type": "string"
},
"conanUrl": {
"type": "string"
},
"nugetUrl": {
"type": "string"
},
"pypiUrl": {
"type": "string"
},
"pypiSetupUrl": {
"type": "string"
},
"composerUrl": {
"type": "string"
},
"composerConfigRepositoryUrl": {
"type": "string"
} }
} }
} }
...@@ -5,7 +5,10 @@ require 'spec_helper' ...@@ -5,7 +5,10 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['PackageDetailsType'] do RSpec.describe GitlabSchema.types['PackageDetailsType'] do
it 'includes all the package fields' do it 'includes all the package fields' do
expected_fields = %w[ expected_fields = %w[
id name version created_at updated_at package_type tags project pipelines versions package_files dependency_links id name version created_at updated_at package_type tags project
pipelines versions package_files dependency_links
npm_url maven_url conan_url nuget_url pypi_url pypi_setup_url
composer_url composer_config_repository_url
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -4,7 +4,9 @@ require 'spec_helper' ...@@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe 'package details' do RSpec.describe 'package details' do
include GraphqlHelpers include GraphqlHelpers
let_it_be_with_reload(:project) { create(:project) } let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:composer_package) { create(:composer_package, project: project) } let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } } let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do let_it_be(:composer_metadatum) do
...@@ -17,7 +19,6 @@ RSpec.describe 'package details' do ...@@ -17,7 +19,6 @@ RSpec.describe 'package details' do
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] } let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:metadata) { query_graphql_fragment('ComposerMetadata') } let(:metadata) { query_graphql_fragment('ComposerMetadata') }
let(:package_files) {all_graphql_fields_for('PackageFile')} let(:package_files) {all_graphql_fields_for('PackageFile')}
let(:user) { project.owner }
let(:package_global_id) { global_id_of(composer_package) } let(:package_global_id) { global_id_of(composer_package) }
let(:package_details) { graphql_data_at(:package) } let(:package_details) { graphql_data_at(:package) }
...@@ -37,170 +38,198 @@ RSpec.describe 'package details' do ...@@ -37,170 +38,198 @@ RSpec.describe 'package details' do
subject { post_graphql(query, current_user: user) } subject { post_graphql(query, current_user: user) }
it_behaves_like 'a working graphql query' do context 'with unauthorized user' do
before do before do
subject project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end end
it 'matches the JSON schema' do it 'returns no packages' do
expect(package_details).to match_schema('graphql/packages/package_details') subject
expect(graphql_data_at(:package)).to be_nil
end end
end end
context 'there are other versions of this package' do context 'with authorized user' do
let(:depth) { 3 } before do
let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity project.add_developer(user)
end
let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
it 'includes the sibling versions' do it_behaves_like 'a working graphql query' do
subject before do
subject
end
expect(graphql_data_at(:package, :versions, :nodes)).to match_array( it 'matches the JSON schema' do
siblings.map { |p| a_hash_including('id' => global_id_of(p)) } expect(package_details).to match_schema('graphql/packages/package_details')
) end
end end
context 'going deeper' do context 'there are other versions of this package' do
let(:depth) { 6 } let(:depth) { 3 }
let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
it 'does not create a cycle of versions' do it 'includes the sibling versions' do
subject subject
expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present expect(graphql_data_at(:package, :versions, :nodes)).to match_array(
expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to eq [nil, nil] siblings.map { |p| a_hash_including('id' => global_id_of(p)) }
)
end end
end
end
context 'with package files pending destruction' do context 'going deeper' do
let_it_be(:package_file) { create(:package_file, package: composer_package) } let(:depth) { 6 }
let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
let(:package_file_ids) { graphql_data_at(:package, :package_files, :nodes).map { |node| node["id"] } } it 'does not create a cycle of versions' do
subject
it 'does not return them' do expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
subject expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to match_array [nil, nil]
end
expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s) end
end end
context 'with packages_installable_package_files disabled' do context 'with package files pending destruction' do
before do let_it_be(:package_file) { create(:package_file, package: composer_package) }
stub_feature_flags(packages_installable_package_files: false) let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: composer_package) }
end
it 'returns them' do let(:package_file_ids) { graphql_data_at(:package, :package_files, :nodes).map { |node| node["id"] } }
it 'does not return them' do
subject subject
expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s) expect(package_file_ids).to contain_exactly(package_file.to_global_id.to_s)
end end
end
end
context 'with a batched query' do context 'with packages_installable_package_files disabled' do
let_it_be(:conan_package) { create(:conan_package, project: project) } before do
stub_feature_flags(packages_installable_package_files: false)
end
let(:batch_query) do it 'returns them' do
<<~QUERY subject
{
a: package(id: "#{global_id_of(composer_package)}") { name } expect(package_file_ids).to contain_exactly(package_file_pending_destruction.to_global_id.to_s, package_file.to_global_id.to_s)
b: package(id: "#{global_id_of(conan_package)}") { name } end
} end
QUERY
end end
let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) } context 'with a batched query' do
let_it_be(:conan_package) { create(:conan_package, project: project) }
it 'returns an error for the second package and data for the first' do let(:batch_query) do
post_graphql(batch_query, current_user: user) <<~QUERY
{
a: package(id: "#{global_id_of(composer_package)}") { name }
b: package(id: "#{global_id_of(conan_package)}") { name }
}
QUERY
end
expect(graphql_data_at(:a, :name)).to eq(composer_package.name) let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/] it 'returns an error for the second package and data for the first' do
expect(graphql_data_at(:b)).to be(nil) post_graphql(batch_query, current_user: user)
end
end
context 'with unauthorized user' do expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
let_it_be(:user) { create(:user) }
before do expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) expect(graphql_data_at(:b)).to be(nil)
end
end end
it 'returns no packages' do context 'pipelines field', :aggregate_failures do
subject let(:pipelines) { create_list(:ci_pipeline, 6, project: project) }
let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
expect(graphql_data_at(:package)).to be_nil before do
end composer_package.pipelines = pipelines
end composer_package.save!
end
context 'pipelines field', :aggregate_failures do def run_query(args)
let(:pipelines) { create_list(:ci_pipeline, 6, project: project) } pipelines_nodes = <<~QUERY
let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse } nodes {
id
}
pageInfo {
startCursor
endCursor
}
QUERY
query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes))
post_graphql(query, current_user: user)
end
before do it 'loads the second page with pagination first correctly' do
composer_package.pipelines = pipelines run_query(first: 2)
composer_package.save! pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
end
def run_query(args) expect(pipeline_ids).to eq(pipeline_gids[0..1])
pipelines_nodes = <<~QUERY
nodes {
id
}
pageInfo {
startCursor
endCursor
}
QUERY
query = graphql_query_for(:package, { id: package_global_id }, query_graphql_field("pipelines", args, pipelines_nodes)) cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor')
post_graphql(query, current_user: user)
end run_query(first: 2, after: cursor)
it 'loads the second page with pagination first correctly' do pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
run_query(first: 2)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[0..1]) expect(pipeline_ids).to eq(pipeline_gids[2..3])
end
cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'endCursor') it 'loads the second page with pagination last correctly' do
run_query(last: 2)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
run_query(first: 2, after: cursor) expect(pipeline_ids).to eq(pipeline_gids[4..5])
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor')
expect(pipeline_ids).to eq(pipeline_gids[2..3]) run_query(last: 2, before: cursor)
end
it 'loads the second page with pagination last correctly' do pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
run_query(last: 2)
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id')
expect(pipeline_ids).to eq(pipeline_gids[4..5]) expect(pipeline_ids).to eq(pipeline_gids[2..3])
end
end
cursor = graphql_data.dig('package', 'pipelines', 'pageInfo', 'startCursor') context 'package managers paths' do
before do
subject
end
run_query(last: 2, before: cursor) it 'returns npm_url correctly' do
expect(graphql_data_at(:package, :npm_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/npm")
end
pipeline_ids = graphql_data.dig('package', 'pipelines', 'nodes').pluck('id') it 'returns maven_url correctly' do
expect(graphql_data_at(:package, :maven_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/maven")
end
expect(pipeline_ids).to eq(pipeline_gids[2..3]) it 'returns conan_url correctly' do
end expect(graphql_data_at(:package, :conan_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/conan")
end
context 'with unauthorized user' do it 'returns nuget_url correctly' do
let_it_be(:user) { create(:user) } expect(graphql_data_at(:package, :nuget_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/nuget/index.json")
end
before do it 'returns pypi_url correctly' do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
end end
it 'returns no packages' do it 'returns pypi_setup_url correctly' do
run_query(first: 2) expect(graphql_data_at(:package, :pypi_setup_url)).to eq("http://localhost/api/v4/projects/#{project.id}/packages/pypi")
end
it 'returns composer_url correctly' do
expect(graphql_data_at(:package, :composer_url)).to eq("http://localhost/api/v4/group/#{group.id}/-/packages/composer/packages.json")
end
expect(graphql_data_at(:package)).to be_nil it 'returns composer_config_repository_url correctly' do
expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end 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