Commit c97bac9f authored by Vasilii Iakliushin's avatar Vasilii Iakliushin Committed by Nikola Milojevic

Extend graphql repository with paginated tree field

Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/334140

Changelog: added
parent 4441b7c9
# frozen_string_literal: true
module Resolvers
class PaginatedTreeResolver < BaseResolver
type Types::Tree::TreeType.connection_type, null: true
extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
calls_gitaly!
argument :path, GraphQL::Types::String,
required: false,
default_value: '', # root of the repository
description: 'The path to get the tree for. Default value is the root of the repository.'
argument :ref, GraphQL::Types::String,
required: false,
default_value: :head,
description: 'The commit ref to get the tree for. Default value is HEAD.'
argument :recursive, GraphQL::Types::Boolean,
required: false,
default_value: false,
description: 'Used to get a recursive tree. Default is false.'
alias_method :repository, :object
def resolve(**args)
return unless repository.exists?
cursor = args.delete(:after)
pagination_params = {
limit: @field.max_page_size || 100,
page_token: cursor
}
tree = repository.tree(args[:ref], args[:path], recursive: args[:recursive], pagination_params: pagination_params)
next_cursor = tree.cursor&.next_cursor
Gitlab::Graphql::ExternallyPaginatedArray.new(cursor, next_cursor, *tree)
rescue Gitlab::Git::CommandError => e
raise Gitlab::Graphql::Errors::ArgumentError, e
end
def self.field_options
super.merge(connection: false) # we manage the pagination manually, so opt out of the connection field extension
end
end
end
...@@ -14,6 +14,10 @@ module Types ...@@ -14,6 +14,10 @@ module Types
description: 'Indicates a corresponding Git repository exists on disk.' description: 'Indicates a corresponding Git repository exists on disk.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true, field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.' description: 'Tree of the repository.'
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
max_page_size: 100,
description: 'Paginated tree of the repository.',
feature_flag: :paginated_tree_graphql_query
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true, field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository' description: 'Blobs contained within the repository'
field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true, field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true,
......
---
name: paginated_tree_graphql_query
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66751
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337214
milestone: '14.2'
type: development
group: group::source code
default_enabled: false
...@@ -7039,6 +7039,29 @@ The edge type for [`Todo`](#todo). ...@@ -7039,6 +7039,29 @@ The edge type for [`Todo`](#todo).
| <a id="todoedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="todoedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="todoedgenode"></a>`node` | [`Todo`](#todo) | The item at the end of the edge. | | <a id="todoedgenode"></a>`node` | [`Todo`](#todo) | The item at the end of the edge. |
#### `TreeConnection`
The connection type for [`Tree`](#tree).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="treeconnectionedges"></a>`edges` | [`[TreeEdge]`](#treeedge) | A list of edges. |
| <a id="treeconnectionnodes"></a>`nodes` | [`[Tree]`](#tree) | A list of nodes. |
| <a id="treeconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `TreeEdge`
The edge type for [`Tree`](#tree).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="treeedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="treeedgenode"></a>`node` | [`Tree`](#tree) | The item at the end of the edge. |
#### `TreeEntryConnection` #### `TreeEntryConnection`
The connection type for [`TreeEntry`](#treeentry). The connection type for [`TreeEntry`](#treeentry).
...@@ -12714,6 +12737,24 @@ Returns [`[String!]`](#string). ...@@ -12714,6 +12737,24 @@ Returns [`[String!]`](#string).
| <a id="repositorybranchnamesoffset"></a>`offset` | [`Int!`](#int) | The number of branch names to skip. | | <a id="repositorybranchnamesoffset"></a>`offset` | [`Int!`](#int) | The number of branch names to skip. |
| <a id="repositorybranchnamessearchpattern"></a>`searchPattern` | [`String!`](#string) | The pattern to search for branch names by. | | <a id="repositorybranchnamessearchpattern"></a>`searchPattern` | [`String!`](#string) | The pattern to search for branch names by. |
##### `Repository.paginatedTree`
Paginated tree of the repository. Available only when feature flag `paginated_tree_graphql_query` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
Returns [`TreeConnection`](#treeconnection).
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="repositorypaginatedtreepath"></a>`path` | [`String`](#string) | The path to get the tree for. Default value is the root of the repository. |
| <a id="repositorypaginatedtreerecursive"></a>`recursive` | [`Boolean`](#boolean) | Used to get a recursive tree. Default is false. |
| <a id="repositorypaginatedtreeref"></a>`ref` | [`String`](#string) | The commit ref to get the tree for. Default value is HEAD. |
##### `Repository.tree` ##### `Repository.tree`
Tree of the repository. Tree of the repository.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::PaginatedTreeResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:repository) { project.repository }
specify do
expect(described_class).to have_nullable_graphql_type(Types::Tree::TreeType.connection_type)
end
describe '#resolve', :aggregate_failures do
subject { resolve_repository(args, opts) }
let(:args) { { ref: 'master' } }
let(:opts) { {} }
let(:start_cursor) { subject.start_cursor }
let(:end_cursor) { subject.end_cursor }
let(:items) { subject.items }
let(:entries) { items.first.entries }
it 'resolves to a collection with a tree object' do
expect(items.first).to be_an_instance_of(Tree)
expect(start_cursor).to be_nil
expect(end_cursor).to be_blank
expect(entries.count).to eq(repository.tree.entries.count)
end
context 'with recursive option' do
let(:args) { super().merge(recursive: true) }
it 'resolve to a recursive tree' do
expect(entries[4].path).to eq('files/html')
end
end
context 'with limited max_page_size' do
let(:opts) { { max_page_size: 5 } }
it 'resolves to a pagination collection with a tree object' do
expect(items.first).to be_an_instance_of(Tree)
expect(start_cursor).to be_nil
expect(end_cursor).to be_present
expect(entries.count).to eq(5)
end
end
context 'when repository does not exist' do
before do
allow(repository).to receive(:exists?).and_return(false)
end
it 'returns nil' do
is_expected.to be(nil)
end
end
describe 'Cursor pagination' do
context 'when cursor is invalid' do
let(:args) { super().merge(after: 'invalid') }
it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) }
end
it 'returns all tree entries during cursor pagination' do
cursor = nil
expected_entries = repository.tree.entries.map(&:path)
collected_entries = []
loop do
result = resolve_repository(args.merge(after: cursor), max_page_size: 10)
collected_entries += result.items.first.entries.map(&:path)
expect(result.start_cursor).to eq(cursor)
cursor = result.end_cursor
break if cursor.blank?
end
expect(collected_entries).to match_array(expected_entries)
end
end
end
def resolve_repository(args, opts = {})
field_options = described_class.field_options.merge(
owner: resolver_parent,
name: 'field_value'
).merge(opts)
field = ::Types::BaseField.new(**field_options)
resolve_field(field, repository, args: args, object_type: resolver_parent)
end
end
...@@ -11,6 +11,8 @@ RSpec.describe GitlabSchema.types['Repository'] do ...@@ -11,6 +11,8 @@ RSpec.describe GitlabSchema.types['Repository'] do
specify { expect(described_class).to have_graphql_field(:tree) } specify { expect(described_class).to have_graphql_field(:tree) }
specify { expect(described_class).to have_graphql_field(:paginated_tree, calls_gitaly?: true, max_page_size: 100) }
specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) } specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) }
specify { expect(described_class).to have_graphql_field(:blobs) } specify { expect(described_class).to have_graphql_field(:blobs) }
......
...@@ -83,4 +83,26 @@ RSpec.describe 'getting a repository in a project' do ...@@ -83,4 +83,26 @@ RSpec.describe 'getting a repository in a project' do
expect(graphql_data['project']['repository']).to be_nil expect(graphql_data['project']['repository']).to be_nil
end end
end end
context 'when paginated tree requested' do
let(:fields) do
%(
paginatedTree {
nodes {
trees {
nodes {
path
}
}
}
}
)
end
it 'returns paginated tree' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']['repository']['paginatedTree']).to be_present
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