Commit 9de2f841 authored by Steve Abrams's avatar Steve Abrams Committed by Vitali Tatarintev

Sorting for repository tags in GraphQL

parent c7162942
# frozen_string_literal: true
module Resolvers
class ContainerRepositoryTagsResolver < BaseResolver
type Types::ContainerRepositoryTagType.connection_type, null: true
argument :sort, Types::ContainerRepositoryTagsSortEnum,
description: 'Sort tags by these criteria.',
required: false,
default_value: nil
argument :name, GraphQL::Types::String,
description: 'Search by tag name.',
required: false,
default_value: nil
def resolve(sort:, **filters)
result = tags
if filters[:name]
result = tags.filter do |tag|
tag.name.include?(filters[:name])
end
end
result = sort_tags(result, sort) if sort
result
end
private
def sort_tags(to_be_sorted, sort)
raise StandardError unless Types::ContainerRepositoryTagsSortEnum.enum.include?(sort)
sort_value, _, direction = sort.to_s.rpartition('_')
sorted = to_be_sorted.sort_by(&sort_value.to_sym)
return sorted.reverse if direction == 'desc'
sorted
end
def tags
object.tags
rescue Faraday::Error
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, "Can't connect to the Container Registry. If this error persists, please review the troubleshooting documentation."
end
end
end
......@@ -12,16 +12,11 @@ module Types
Types::ContainerRepositoryTagType.connection_type,
null: true,
description: 'Tags of the container repository.',
max_page_size: 20
max_page_size: 20,
resolver: Resolvers::ContainerRepositoryTagsResolver
def can_delete
Ability.allowed?(current_user, :destroy_container_image, object)
end
def tags
object.tags
rescue Faraday::Error
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'We are having trouble connecting to the Container Registry. If this error persists, please review the troubleshooting documentation.'
end
end
end
# frozen_string_literal: true
module Types
class ContainerRepositoryTagsSortEnum < BaseEnum
graphql_name 'ContainerRepositoryTagSort'
description 'Values for sorting tags'
value 'NAME_ASC', 'Ordered by name in ascending order.', value: :name_asc
value 'NAME_DESC', 'Ordered by name in descending order.', value: :name_desc
end
end
......@@ -9011,10 +9011,28 @@ Details of a container repository.
| <a id="containerrepositorydetailspath"></a>`path` | [`String!`](#string) | Path of the container repository. |
| <a id="containerrepositorydetailsproject"></a>`project` | [`Project!`](#project) | Project of the container registry. |
| <a id="containerrepositorydetailsstatus"></a>`status` | [`ContainerRepositoryStatus`](#containerrepositorystatus) | Status of the container repository. |
| <a id="containerrepositorydetailstags"></a>`tags` | [`ContainerRepositoryTagConnection`](#containerrepositorytagconnection) | Tags of the container repository. (see [Connections](#connections)) |
| <a id="containerrepositorydetailstagscount"></a>`tagsCount` | [`Int!`](#int) | Number of tags associated with this image. |
| <a id="containerrepositorydetailsupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp when the container repository was updated. |
#### Fields with arguments
##### `ContainerRepositoryDetails.tags`
Tags of the container repository.
Returns [`ContainerRepositoryTagConnection`](#containerrepositorytagconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="containerrepositorydetailstagsname"></a>`name` | [`String`](#string) | Search by tag name. |
| <a id="containerrepositorydetailstagssort"></a>`sort` | [`ContainerRepositoryTagSort`](#containerrepositorytagsort) | Sort tags by these criteria. |
### `ContainerRepositoryTag`
A tag from a container repository.
......@@ -16021,6 +16039,15 @@ Status of a container repository.
| <a id="containerrepositorystatusdelete_failed"></a>`DELETE_FAILED` | Delete Failed status. |
| <a id="containerrepositorystatusdelete_scheduled"></a>`DELETE_SCHEDULED` | Delete Scheduled status. |
### `ContainerRepositoryTagSort`
Values for sorting tags.
| Value | Description |
| ----- | ----------- |
| <a id="containerrepositorytagsortname_asc"></a>`NAME_ASC` | Ordered by name in ascending order. |
| <a id="containerrepositorytagsortname_desc"></a>`NAME_DESC` | Ordered by name in descending order. |
### `DastProfileCadenceUnit`
Unit for the duration of Dast Profile Cadence.
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::ContainerRepositoryTagsResolver do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be_with_reload(:repository) { create(:container_repository, project: project) }
let(:args) { { sort: nil } }
describe '#resolve' do
let(:resolver) { resolve(described_class, ctx: { current_user: user }, obj: repository, args: args) }
before do
stub_container_registry_config(enabled: true)
end
context 'by name' do
subject { resolver.map(&:name) }
before do
stub_container_registry_tags(repository: repository.path, tags: %w(aaa bab bbb ccc 123), with_manifest: false)
end
context 'without sort' do
# order is not guaranteed
it { is_expected.to contain_exactly('aaa', 'bab', 'bbb', 'ccc', '123') }
end
context 'with sorting and filtering' do
context "name_asc" do
let(:args) { { sort: :name_asc } }
it { is_expected.to eq(%w(123 aaa bab bbb ccc)) }
end
context "name_desc" do
let(:args) { { sort: :name_desc } }
it { is_expected.to eq(%w(ccc bbb bab aaa 123)) }
end
context 'filter by name' do
let(:args) { { sort: :name_desc, name: 'b' } }
it { is_expected.to eq(%w(bbb bab)) }
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryTagSort'] do
specify { expect(described_class.graphql_name).to eq('ContainerRepositoryTagSort') }
it 'exposes all the existing issue sort values' do
expect(described_class.values.keys).to include(
*%w[NAME_ASC NAME_DESC]
)
end
end
......@@ -30,6 +30,14 @@ RSpec.describe 'container repository details' do
subject { post_graphql(query, current_user: user, variables: variables) }
shared_examples 'returning an invalid value error' do
it 'returns an error' do
subject
expect(graphql_errors.first.dig('message')).to match(/invalid value/)
end
end
it_behaves_like 'a working graphql query' do
before do
subject
......@@ -138,6 +146,80 @@ RSpec.describe 'container repository details' do
end
end
context 'sorting the tags' do
let(:sort) { 'NAME_DESC' }
let(:tags_response) { container_repository_details_response.dig('tags', 'edges') }
let(:variables) do
{ id: container_repository_global_id, n: sort }
end
let(:query) do
<<~GQL
query($id: ID!, $n: ContainerRepositoryTagSort) {
containerRepository(id: $id) {
tags(sort: $n) {
edges {
node {
#{all_graphql_fields_for('ContainerRepositoryTag')}
}
}
}
}
}
GQL
end
it 'sorts the tags', :aggregate_failures do
subject
expect(tags_response.first.dig('node', 'name')).to eq('tag5')
expect(tags_response.last.dig('node', 'name')).to eq('latest')
end
context 'invalid sort' do
let(:sort) { 'FOO_ASC' }
it_behaves_like 'returning an invalid value error'
end
end
context 'filtering by name' do
let(:name) { 'l' }
let(:tags_response) { container_repository_details_response.dig('tags', 'edges') }
let(:variables) do
{ id: container_repository_global_id, n: name }
end
let(:query) do
<<~GQL
query($id: ID!, $n: String) {
containerRepository(id: $id) {
tags(name: $n) {
edges {
node {
#{all_graphql_fields_for('ContainerRepositoryTag')}
}
}
}
}
}
GQL
end
it 'sorts the tags', :aggregate_failures do
subject
expect(tags_response.size).to eq(1)
expect(tags_response.first.dig('node', 'name')).to eq('latest')
end
context 'invalid filter' do
let(:name) { 1 }
it_behaves_like 'returning an invalid value error'
end
end
context 'with tags with a manifest containing nil fields' do
let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') }
let(:errors) { container_repository_details_response.dig('errors') }
......
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