Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/graphql/resolvers/concerns')
-rw-r--r--app/graphql/resolvers/concerns/caching_array_resolver.rb128
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb2
-rw-r--r--app/graphql/resolvers/concerns/looks_ahead.rb1
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb2
-rw-r--r--app/graphql/resolvers/concerns/resolves_project.rb3
-rw-r--r--app/graphql/resolvers/concerns/resolves_snippets.rb21
6 files changed, 144 insertions, 13 deletions
diff --git a/app/graphql/resolvers/concerns/caching_array_resolver.rb b/app/graphql/resolvers/concerns/caching_array_resolver.rb
new file mode 100644
index 00000000000..4f2c8b98928
--- /dev/null
+++ b/app/graphql/resolvers/concerns/caching_array_resolver.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+# Concern that will eliminate N+1 queries for size-constrained
+# collections of items.
+#
+# **note**: The resolver will never load more items than
+# `@field.max_page_size` if defined, falling back to
+# `context.schema.default_max_page_size`.
+#
+# provided that:
+#
+# - the query can be uniquely determined by the object and the arguments
+# - the model class includes FromUnion
+# - the model class defines a scalar primary key
+#
+# This comes at the cost of returning arrays, not relations, so we don't get
+# any keyset pagination goodness. Consequently, this is only suitable for small-ish
+# result sets, as the full result set will be loaded into memory.
+#
+# To enforce this, the resolver limits the size of result sets to
+# `@field.max_page_size || context.schema.default_max_page_size`.
+#
+# **important**: If the cardinality of your collection is likely to be greater than 100,
+# then you will want to pass `max_page_size:` as part of the field definition
+# or (ideally) as part of the resolver `field_options`.
+#
+# How to implement:
+# --------------------
+#
+# Each including class operates on two generic parameters, A and R:
+# - A is any Object that can be used as a Hash key. Instances of A
+# are returned by `query_input` and then passed to `query_for`.
+# - R is any subclass of ApplicationRecord that includes FromUnion.
+# R must have a single scalar primary_key
+#
+# Classes must implement:
+# - #model_class -> Class[R]. (Must respond to :primary_key, and :from_union)
+# - #query_input(**kwargs) -> A (Must be hashable)
+# - #query_for(A) -> ActiveRecord::Relation[R]
+#
+# Note the relationship between query_input and query_for, one of which
+# consumes the input of the other
+# (i.e. `resolve(**args).sync == query_for(query_input(**args)).to_a`).
+#
+# Classes may implement:
+# - #item_found(A, R) (return value is ignored)
+# - max_union_size Integer (the maximum number of queries to run in any one union)
+module CachingArrayResolver
+ MAX_UNION_SIZE = 50
+
+ def resolve(**args)
+ key = query_input(**args)
+
+ BatchLoader::GraphQL.for(key).batch(**batch) do |keys, loader|
+ if keys.size == 1
+ # We can avoid the union entirely.
+ k = keys.first
+ limit(query_for(k)).each { |item| found(loader, k, item) }
+ else
+ queries = keys.map { |key| query_for(key) }
+
+ queries.in_groups_of(max_union_size, false).each do |group|
+ by_id = model_class
+ .from_union(tag(group), remove_duplicates: false)
+ .group_by { |r| r[primary_key] }
+
+ by_id.values.each do |item_group|
+ item = item_group.first
+ item_group.map(&:union_member_idx).each do |i|
+ found(loader, keys[i], item)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ # Override this to intercept the items once they are found
+ def item_found(query_input, item)
+ end
+
+ def max_union_size
+ MAX_UNION_SIZE
+ end
+
+ private
+
+ def primary_key
+ @primary_key ||= (model_class.primary_key || raise("No primary key for #{model_class}"))
+ end
+
+ def batch
+ { key: self.class, default_value: [] }
+ end
+
+ def found(loader, key, value)
+ loader.call(key) do |vs|
+ item_found(key, value)
+ vs << value
+ end
+ end
+
+ # Tag each row returned from each query with a the index of which query in
+ # the union it comes from. This lets us map the results back to the cache key.
+ def tag(queries)
+ queries.each_with_index.map do |q, i|
+ limit(q.select(all_fields, member_idx(i)))
+ end
+ end
+
+ def limit(query)
+ query.limit(query_limit) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def all_fields
+ model_class.arel_table[Arel.star]
+ end
+
+ # rubocop: disable Graphql/Descriptions (false positive!)
+ def query_limit
+ field&.max_page_size.presence || context.schema.default_max_page_size
+ end
+ # rubocop: enable Graphql/Descriptions
+
+ def member_idx(idx)
+ ::Arel::Nodes::SqlLiteral.new(idx.to_s).as('union_member_idx')
+ end
+end
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index fe6fa0bb262..4715b867ecb 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -29,7 +29,7 @@ module IssueResolverArguments
description: 'Usernames of users assigned to the issue'
argument :assignee_id, GraphQL::STRING_TYPE,
required: false,
- description: 'ID of a user assigned to the issues, "none" and "any" values supported'
+ description: 'ID of a user assigned to the issues, "none" and "any" values are supported'
argument :created_before, Types::TimeType,
required: false,
description: 'Issues created before this date'
diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb
index 61f23920ebb..d468047b539 100644
--- a/app/graphql/resolvers/concerns/looks_ahead.rb
+++ b/app/graphql/resolvers/concerns/looks_ahead.rb
@@ -4,6 +4,7 @@ module LooksAhead
extend ActiveSupport::Concern
included do
+ extras [:lookahead]
attr_accessor :lookahead
end
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
index 46d9e174deb..f061f5f1606 100644
--- a/app/graphql/resolvers/concerns/resolves_pipelines.rb
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -4,7 +4,7 @@ module ResolvesPipelines
extend ActiveSupport::Concern
included do
- type [Types::Ci::PipelineType], null: false
+ type Types::Ci::PipelineType.connection_type, null: false
argument :status,
Types::Ci::PipelineStatusEnum,
required: false,
diff --git a/app/graphql/resolvers/concerns/resolves_project.rb b/app/graphql/resolvers/concerns/resolves_project.rb
index 3c5ce3dab01..b2ee7d7e850 100644
--- a/app/graphql/resolvers/concerns/resolves_project.rb
+++ b/app/graphql/resolvers/concerns/resolves_project.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
module ResolvesProject
+ # Accepts EITHER one of
+ # - full_path: String (see Project#full_path)
+ # - project_id: GlobalID. Arguments should be typed as: `::Types::GlobalIDType[Project]`
def resolve_project(full_path: nil, project_id: nil)
unless full_path.present? ^ project_id.present?
raise ::Gitlab::Graphql::Errors::ArgumentError, 'Incompatible arguments: projectId, projectPath.'
diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb
index 483372bbf63..790ff4f774f 100644
--- a/app/graphql/resolvers/concerns/resolves_snippets.rb
+++ b/app/graphql/resolvers/concerns/resolves_snippets.rb
@@ -4,9 +4,9 @@ module ResolvesSnippets
extend ActiveSupport::Concern
included do
- type Types::SnippetType, null: false
+ type Types::SnippetType.connection_type, null: false
- argument :ids, [GraphQL::ID_TYPE],
+ argument :ids, [::Types::GlobalIDType[::Snippet]],
required: false,
description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"'
@@ -32,16 +32,15 @@ module ResolvesSnippets
}.merge(options_by_type(args[:type]))
end
- def resolve_ids(ids)
- Array.wrap(ids).map { |id| resolve_gid(id, :id) }
- end
-
- def resolve_gid(gid, argument)
- return unless gid.present?
+ def resolve_ids(ids, type = ::Types::GlobalIDType[::Snippet])
+ Array.wrap(ids).map do |id|
+ next unless id.present?
- GlobalID.parse(gid)&.model_id.tap do |id|
- raise Gitlab::Graphql::Errors::ArgumentError, "Invalid global id format for param #{argument}" if id.nil?
- end
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = type.coerce_isolated_input(id)
+ id.model_id
+ end.compact
end
def options_by_type(type)