2018-09-11 19:08:34 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2018-05-23 07:55:14 +00:00
|
|
|
class GitlabSchema < GraphQL::Schema
|
2019-04-05 17:30:10 +00:00
|
|
|
# Currently an IntrospectionQuery has a complexity of 179.
|
2019-03-27 20:02:25 +00:00
|
|
|
# These values will evolve over time.
|
2019-04-05 17:30:10 +00:00
|
|
|
DEFAULT_MAX_COMPLEXITY = 200
|
|
|
|
AUTHENTICATED_COMPLEXITY = 250
|
|
|
|
ADMIN_COMPLEXITY = 300
|
2019-03-27 20:02:25 +00:00
|
|
|
|
2019-06-07 17:13:26 +00:00
|
|
|
DEFAULT_MAX_DEPTH = 15
|
|
|
|
AUTHENTICATED_MAX_DEPTH = 20
|
2019-05-06 14:00:03 +00:00
|
|
|
|
2020-04-08 03:09:31 +00:00
|
|
|
use GraphQL::Pagination::Connections
|
2018-02-23 15:36:40 +00:00
|
|
|
use BatchLoader::GraphQL
|
2020-04-08 03:09:31 +00:00
|
|
|
use Gitlab::Graphql::Pagination::Connections
|
2019-05-02 07:01:14 +00:00
|
|
|
use Gitlab::Graphql::GenericTracing
|
2020-03-11 00:09:09 +00:00
|
|
|
use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout
|
2017-08-16 13:04:41 +00:00
|
|
|
|
2019-05-02 00:16:49 +00:00
|
|
|
query_analyzer Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer.new
|
2019-08-22 14:17:38 +00:00
|
|
|
query_analyzer Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer.new
|
2019-03-27 20:02:25 +00:00
|
|
|
|
|
|
|
max_complexity DEFAULT_MAX_COMPLEXITY
|
2019-05-09 09:27:07 +00:00
|
|
|
max_depth DEFAULT_MAX_DEPTH
|
2019-03-27 20:02:25 +00:00
|
|
|
|
2019-08-22 14:17:38 +00:00
|
|
|
query Types::QueryType
|
|
|
|
mutation Types::MutationType
|
|
|
|
|
|
|
|
default_max_page_size 100
|
2019-03-27 20:02:25 +00:00
|
|
|
|
2020-10-30 15:08:59 +00:00
|
|
|
lazy_resolve ::Gitlab::Graphql::Lazy, :force
|
|
|
|
|
2019-05-06 14:00:03 +00:00
|
|
|
class << self
|
2019-05-09 09:27:07 +00:00
|
|
|
def multiplex(queries, **kwargs)
|
|
|
|
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
|
|
|
|
|
|
|
|
queries.each do |query|
|
|
|
|
query[:max_depth] = max_query_depth(kwargs[:context])
|
|
|
|
end
|
|
|
|
|
|
|
|
super(queries, **kwargs)
|
|
|
|
end
|
|
|
|
|
2019-05-06 14:00:03 +00:00
|
|
|
def execute(query_str = nil, **kwargs)
|
|
|
|
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
|
|
|
|
kwargs[:max_depth] ||= max_query_depth(kwargs[:context])
|
2019-03-27 20:02:25 +00:00
|
|
|
|
2019-05-06 14:00:03 +00:00
|
|
|
super(query_str, **kwargs)
|
|
|
|
end
|
|
|
|
|
2020-08-14 03:10:00 +00:00
|
|
|
def get_type(type_name)
|
|
|
|
# This is a backwards compatibility hack to work around an accidentally
|
|
|
|
# released argument typed as EEIterationID
|
|
|
|
type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID')
|
|
|
|
super(type_name)
|
|
|
|
end
|
|
|
|
|
2019-11-04 09:06:21 +00:00
|
|
|
def id_from_object(object, _type = nil, _ctx = nil)
|
2019-06-03 17:38:16 +00:00
|
|
|
unless object.respond_to?(:to_global_id)
|
|
|
|
# This is an error in our schema and needs to be solved. So raise a
|
2019-08-26 08:48:06 +00:00
|
|
|
# more meaningful error message
|
2019-06-03 17:38:16 +00:00
|
|
|
raise "#{object} does not implement `to_global_id`. "\
|
|
|
|
"Include `GlobalID::Identification` into `#{object.class}"
|
|
|
|
end
|
|
|
|
|
|
|
|
object.to_global_id
|
|
|
|
end
|
|
|
|
|
2020-01-08 21:08:08 +00:00
|
|
|
# Find an object by looking it up from its global ID, passed as a string.
|
|
|
|
#
|
|
|
|
# This is the composition of 'parse_gid' and 'find_by_gid', see these
|
|
|
|
# methods for further documentation.
|
2019-12-23 12:08:18 +00:00
|
|
|
def object_from_id(global_id, ctx = {})
|
2020-01-08 21:08:08 +00:00
|
|
|
gid = parse_gid(global_id, ctx)
|
|
|
|
|
|
|
|
find_by_gid(gid)
|
|
|
|
end
|
|
|
|
|
2020-11-07 00:08:58 +00:00
|
|
|
def resolve_type(type, object, ctx = :__undefined__)
|
|
|
|
tc = type.metadata[:type_class]
|
|
|
|
return if tc.respond_to?(:assignable?) && !tc.assignable?(object)
|
|
|
|
|
|
|
|
super
|
|
|
|
end
|
|
|
|
|
2020-01-08 21:08:08 +00:00
|
|
|
# Find an object by looking it up from its 'GlobalID'.
|
|
|
|
#
|
|
|
|
# * For `ApplicationRecord`s, this is equivalent to
|
|
|
|
# `global_id.model_class.find(gid.model_id)`, but more efficient.
|
|
|
|
# * For classes that implement `.lazy_find(global_id)`, this class method
|
|
|
|
# will be called.
|
|
|
|
# * All other classes will use `GlobalID#find`
|
|
|
|
def find_by_gid(gid)
|
2020-08-07 21:10:07 +00:00
|
|
|
return unless gid
|
|
|
|
|
2020-01-08 21:08:08 +00:00
|
|
|
if gid.model_class < ApplicationRecord
|
|
|
|
Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
|
|
|
|
elsif gid.model_class.respond_to?(:lazy_find)
|
|
|
|
gid.model_class.lazy_find(gid.model_id)
|
|
|
|
else
|
|
|
|
gid.find
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Parse a string to a GlobalID, raising ArgumentError if there are problems
|
|
|
|
# with it.
|
|
|
|
#
|
|
|
|
# Problems that may occur:
|
|
|
|
# * it may not be syntactically valid
|
|
|
|
# * it may not match the expected type (see below)
|
|
|
|
#
|
|
|
|
# Options:
|
|
|
|
# * :expected_type [Class] - the type of object this GlobalID should refer to.
|
|
|
|
#
|
|
|
|
# e.g.
|
|
|
|
#
|
|
|
|
# ```
|
|
|
|
# gid = GitlabSchema.parse_gid(my_string, expected_type: ::Project)
|
|
|
|
# project_id = gid.model_id
|
|
|
|
# gid.model_class == ::Project
|
|
|
|
# ```
|
|
|
|
def parse_gid(global_id, ctx = {})
|
2019-12-23 12:08:18 +00:00
|
|
|
expected_type = ctx[:expected_type]
|
2019-06-03 17:38:16 +00:00
|
|
|
gid = GlobalID.parse(global_id)
|
|
|
|
|
2020-08-21 00:10:44 +00:00
|
|
|
raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID." unless gid
|
2019-06-03 17:38:16 +00:00
|
|
|
|
2021-03-24 03:09:04 +00:00
|
|
|
if expected_type && gid.model_class.ancestors.exclude?(expected_type)
|
2019-12-23 12:08:18 +00:00
|
|
|
vars = { global_id: global_id, expected_type: expected_type }
|
2020-08-21 00:10:44 +00:00
|
|
|
msg = _('%{global_id} is not a valid ID for %{expected_type}.') % vars
|
2019-12-23 12:08:18 +00:00
|
|
|
raise Gitlab::Graphql::Errors::ArgumentError, msg
|
|
|
|
end
|
|
|
|
|
2020-01-08 21:08:08 +00:00
|
|
|
gid
|
2019-06-03 17:38:16 +00:00
|
|
|
end
|
|
|
|
|
2019-05-06 14:00:03 +00:00
|
|
|
private
|
|
|
|
|
|
|
|
def max_query_complexity(ctx)
|
|
|
|
current_user = ctx&.fetch(:current_user, nil)
|
|
|
|
|
|
|
|
if current_user&.admin
|
|
|
|
ADMIN_COMPLEXITY
|
|
|
|
elsif current_user
|
|
|
|
AUTHENTICATED_COMPLEXITY
|
|
|
|
else
|
|
|
|
DEFAULT_MAX_COMPLEXITY
|
|
|
|
end
|
|
|
|
end
|
2019-03-27 20:02:25 +00:00
|
|
|
|
2019-05-06 14:00:03 +00:00
|
|
|
def max_query_depth(ctx)
|
|
|
|
current_user = ctx&.fetch(:current_user, nil)
|
2019-03-27 20:02:25 +00:00
|
|
|
|
2019-05-06 14:00:03 +00:00
|
|
|
if current_user
|
|
|
|
AUTHENTICATED_MAX_DEPTH
|
|
|
|
else
|
2019-05-09 09:27:07 +00:00
|
|
|
DEFAULT_MAX_DEPTH
|
2019-05-06 14:00:03 +00:00
|
|
|
end
|
2019-03-27 20:02:25 +00:00
|
|
|
end
|
|
|
|
end
|
2020-08-14 03:10:00 +00:00
|
|
|
|
|
|
|
# This is a backwards compatibility hack to work around an accidentally
|
|
|
|
# released argument typed as EE{Type}ID
|
|
|
|
def get_type(type_name)
|
|
|
|
type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID')
|
|
|
|
super(type_name)
|
|
|
|
end
|
2017-08-16 13:04:41 +00:00
|
|
|
end
|
2020-03-10 15:08:08 +00:00
|
|
|
|
2020-04-09 09:10:17 +00:00
|
|
|
GitlabSchema.prepend_if_ee('EE::GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule
|
|
|
|
|
|
|
|
# Force the schema to load as a workaround for intermittent errors we
|
|
|
|
# see due to a lack of thread safety.
|
|
|
|
#
|
|
|
|
# TODO: We can remove this workaround when we convert the schema to use
|
|
|
|
# the new query interpreter runtime.
|
|
|
|
#
|
|
|
|
# See:
|
|
|
|
# - https://gitlab.com/gitlab-org/gitlab/-/issues/211478
|
|
|
|
# - https://gitlab.com/gitlab-org/gitlab/-/issues/210556
|
|
|
|
GitlabSchema.graphql_definition
|