diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 17:36:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 17:36:54 +0300 |
commit | f61bb2a16a514b71bf33aabbbb999d6732016a24 (patch) | |
tree | 9548caa89e60b4f40b99bbd1dac030420b812aa8 /lib/gitlab/graphql | |
parent | 35fc54e5d261f8898e390aea7c2f5ec5fdf0539d (diff) |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc42
Diffstat (limited to 'lib/gitlab/graphql')
20 files changed, 432 insertions, 259 deletions
diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb deleted file mode 100644 index e83b567308b..00000000000 --- a/lib/gitlab/graphql/authorize.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - # Allow fields to declare permissions their objects must have. The field - # will be set to nil unless all required permissions are present. - module Authorize - extend ActiveSupport::Concern - - def self.use(schema_definition) - schema_definition.instrument(:field, Gitlab::Graphql::Authorize::Instrumentation.new, after_built_ins: true) - end - end - end -end diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb deleted file mode 100644 index e8db619f88a..00000000000 --- a/lib/gitlab/graphql/authorize/authorize_field_service.rb +++ /dev/null @@ -1,147 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - module Authorize - class AuthorizeFieldService - def initialize(field) - @field = field - @old_resolve_proc = @field.resolve_proc - end - - def authorizations? - authorizations.present? - end - - def authorized_resolve - proc do |parent_typed_object, args, ctx| - resolved_type = @old_resolve_proc.call(parent_typed_object, args, ctx) - authorizing_object = authorize_against(parent_typed_object, resolved_type) - - filter_allowed(ctx[:current_user], resolved_type, authorizing_object) - end - end - - private - - def authorizations - @authorizations ||= (type_authorizations + field_authorizations).uniq - end - - # Returns any authorize metadata from the return type of @field - def type_authorizations - type = @field.type - - # When the return type of @field is a collection, find the singular type - if @field.connection? - type = node_type_for_relay_connection(type) - elsif type.list? - type = node_type_for_basic_connection(type) - end - - type = type.unwrap if type.kind.non_null? - - Array.wrap(type.metadata[:authorize]) - end - - # Returns any authorize metadata from @field - def field_authorizations - return [] if @field.metadata[:authorize] == true - - Array.wrap(@field.metadata[:authorize]) - end - - def authorize_against(parent_typed_object, resolved_type) - if scalar_type? - # The field is a built-in/scalar type, or a list of scalars - # authorize using the parent's object - parent_typed_object.object - elsif @field.connection? || @field.type.list? || resolved_type.is_a?(Array) - # The field is a connection or a list of non-built-in types, we'll - # authorize each element when rendering - nil - elsif resolved_type.respond_to?(:object) - # The field is a type representing a single object, we'll authorize - # against the object directly - resolved_type.object - else - # Resolved type is a single object that might not be loaded yet by - # the batchloader, we'll authorize that - resolved_type - end - end - - def filter_allowed(current_user, resolved_type, authorizing_object) - if resolved_type.nil? - # We're not rendering anything, for example when a record was not found - # no need to do anything - elsif authorizing_object - # Authorizing fields representing scalars, or a simple field with an object - ::Gitlab::Graphql::Lazy.with_value(authorizing_object) do |object| - resolved_type if allowed_access?(current_user, object) - end - elsif @field.connection? - ::Gitlab::Graphql::Lazy.with_value(resolved_type) do |type| - # A connection with pagination, modify the visible nodes on the - # connection type in place - nodes = to_nodes(type) - nodes.keep_if { |node| allowed_access?(current_user, node) } if nodes - type - end - elsif @field.type.list? || resolved_type.is_a?(Array) - # A simple list of rendered types each object being an object to authorize - ::Gitlab::Graphql::Lazy.with_value(resolved_type) do |items| - items.select do |single_object_type| - object_type = realized(single_object_type) - object = object_type.try(:object) || object_type - allowed_access?(current_user, object) - end - end - else - raise "Can't authorize #{@field}" - end - end - - # Ensure that we are dealing with realized objects, not delayed promises - def realized(thing) - ::Gitlab::Graphql::Lazy.force(thing) - end - - # Try to get the connection - # can be at type.object or at type - def to_nodes(type) - if type.respond_to?(:nodes) - type.nodes - elsif type.respond_to?(:object) - to_nodes(type.object) - else - nil - end - end - - def allowed_access?(current_user, object) - object = realized(object) - - authorizations.all? do |ability| - Ability.allowed?(current_user, ability, object) - end - end - - # Returns the singular type for relay connections. - # This will be the type class of edges.node - def node_type_for_relay_connection(type) - type.unwrap.get_field('edges').type.unwrap.get_field('node').type - end - - # Returns the singular type for basic connections, for example `[Types::ProjectType]` - def node_type_for_basic_connection(type) - type.unwrap - end - - def scalar_type? - node_type_for_basic_connection(@field.type).kind.scalar? - end - end - end - end -end diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb index 6ee446011d4..4d575b964e5 100644 --- a/lib/gitlab/graphql/authorize/authorize_resource.rb +++ b/lib/gitlab/graphql/authorize/authorize_resource.rb @@ -5,15 +5,17 @@ module Gitlab module Authorize module AuthorizeResource extend ActiveSupport::Concern + ConfigurationError = Class.new(StandardError) - RESOURCE_ACCESS_ERROR = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + RESOURCE_ACCESS_ERROR = "The resource that you are attempting to access does " \ + "not exist or you don't have permission to perform this action" class_methods do def required_permissions # If the `#authorize` call is used on multiple classes, we add the # permissions specified on a subclass, to the ones that were specified - # on it's superclass. - @required_permissions ||= if self.respond_to?(:superclass) && superclass.respond_to?(:required_permissions) + # on its superclass. + @required_permissions ||= if respond_to?(:superclass) && superclass.respond_to?(:required_permissions) superclass.required_permissions.dup else [] @@ -23,6 +25,18 @@ module Gitlab def authorize(*permissions) required_permissions.concat(permissions) end + + def authorizes_object? + defined?(@authorizes_object) ? @authorizes_object : false + end + + def authorizes_object! + @authorizes_object = true + end + + def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR) + raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, msg + end end def find_object(*args) @@ -37,33 +51,21 @@ module Gitlab object end + # authorizes the object using the current class authorization. def authorize!(object) - unless authorized_resource?(object) - raise_resource_not_available_error! - end + raise_resource_not_available_error! unless authorized_resource?(object) end - # this was named `#authorized?`, however it conflicts with the native - # graphql gem version - # TODO consider adopting the gem's built in authorization system - # https://gitlab.com/gitlab-org/gitlab/issues/13984 def authorized_resource?(object) # Sanity check. We don't want to accidentally allow a developer to authorize # without first adding permissions to authorize against - if self.class.required_permissions.empty? - raise Gitlab::Graphql::Errors::ArgumentError, "#{self.class.name} has no authorizations" - end + raise ConfigurationError, "#{self.class.name} has no authorizations" if self.class.authorization.none? - self.class.required_permissions.all? do |ability| - # The actions could be performed across multiple objects. In which - # case the current user is common, and we could benefit from the - # caching in `DeclarativePolicy`. - Ability.allowed?(current_user, ability, object, scope: :user) - end + self.class.authorization.ok?(object, current_user) end - def raise_resource_not_available_error!(msg = RESOURCE_ACCESS_ERROR) - raise Gitlab::Graphql::Errors::ResourceNotAvailable, msg + def raise_resource_not_available_error!(*args) + self.class.raise_resource_not_available_error!(*args) end end end diff --git a/lib/gitlab/graphql/authorize/connection_filter_extension.rb b/lib/gitlab/graphql/authorize/connection_filter_extension.rb new file mode 100644 index 00000000000..c75510df3e3 --- /dev/null +++ b/lib/gitlab/graphql/authorize/connection_filter_extension.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Authorize + class ConnectionFilterExtension < GraphQL::Schema::FieldExtension + class Redactor + include ::Gitlab::Graphql::Laziness + + def initialize(type, context) + @type = type + @context = context + end + + def redact(nodes) + remove_unauthorized(nodes) + + nodes + end + + def active? + # some scalar types (such as integers) do not respond to :authorized? + return false unless @type.respond_to?(:authorized?) + + auth = @type.try(:authorization) + + auth.nil? || auth.any? + end + + private + + def remove_unauthorized(nodes) + nodes + .map! { |lazy| force(lazy) } + .keep_if { |forced| @type.authorized?(forced, @context) } + end + end + + def after_resolve(value:, context:, **rest) + return value if value.is_a?(GraphQL::Execution::Execute::Skip) + + if @field.connection? + redact_connection(value, context) + elsif @field.type.list? + redact_list(value.to_a, context) unless value.nil? + end + + value + end + + def redact_connection(conn, context) + redactor = Redactor.new(@field.type.unwrap.node_type, context) + return unless redactor.active? + + conn.redactor = redactor if conn.respond_to?(:redactor=) + end + + def redact_list(list, context) + redactor = Redactor.new(@field.type.unwrap, context) + redactor.redact(list) if redactor.active? + end + end + end + end +end diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb deleted file mode 100644 index 15ecc3b04f0..00000000000 --- a/lib/gitlab/graphql/authorize/instrumentation.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - module Authorize - class Instrumentation - # Replace the resolver for the field with one that will only return the - # resolved object if the permissions check is successful. - def instrument(_type, field) - service = AuthorizeFieldService.new(field) - - if service.authorizations? - field.redefine { resolve(service.authorized_resolve) } - else - field - end - end - end - end - end -end diff --git a/lib/gitlab/graphql/authorize/object_authorization.rb b/lib/gitlab/graphql/authorize/object_authorization.rb new file mode 100644 index 00000000000..0bc87108871 --- /dev/null +++ b/lib/gitlab/graphql/authorize/object_authorization.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Authorize + class ObjectAuthorization + attr_reader :abilities + + def initialize(abilities) + @abilities = Array.wrap(abilities).flatten + end + + def none? + abilities.empty? + end + + def any? + abilities.present? + end + + def ok?(object, current_user) + return true if none? + + subject = object.try(:declarative_policy_subject) || object + abilities.all? do |ability| + Ability.allowed?(current_user, ability, subject) + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/deprecation.rb b/lib/gitlab/graphql/deprecation.rb new file mode 100644 index 00000000000..e0176e2d6e0 --- /dev/null +++ b/lib/gitlab/graphql/deprecation.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + class Deprecation + REASONS = { + renamed: 'This was renamed.', + discouraged: 'Use of this is not recommended.' + }.freeze + + include ActiveModel::Validations + + validates :milestone, presence: true, format: { with: /\A\d+\.\d+\z/, message: 'must be milestone-ish' } + validates :reason, presence: true + validates :reason, + format: { with: /.*[^.]\z/, message: 'must not end with a period' }, + if: :reason_is_string? + validate :milestone_is_string + validate :reason_known_or_string + + def self.parse(options) + new(**options) if options + end + + def initialize(reason: nil, milestone: nil, replacement: nil) + @reason = reason.presence + @milestone = milestone.presence + @replacement = replacement.presence + end + + def ==(other) + return false unless other.is_a?(self.class) + + [reason_text, milestone, replacement] == [:reason_text, :milestone, :replacement].map do |attr| + other.send(attr) # rubocop: disable GitlabSecurity/PublicSend + end + end + alias_method :eql, :== + + def markdown(context: :inline) + parts = [ + "#{deprecated_in(format: :markdown)}.", + reason_text, + replacement.then { |r| "Use: `#{r}`." if r } + ].compact + + case context + when :block + ['WARNING:', *parts].join("\n") + when :inline + parts.join(' ') + end + end + + def edit_description(original_description) + @original_description = original_description + return unless original_description + + original_description + description_suffix + end + + def original_description + return unless @original_description + return @original_description if @original_description.ends_with?('.') + + "#{@original_description}." + end + + def deprecation_reason + [ + reason_text, + replacement && "Please use `#{replacement}`.", + "#{deprecated_in}." + ].compact.join(' ') + end + + private + + attr_reader :reason, :milestone, :replacement + + def milestone_is_string + return if milestone.is_a?(String) + + errors.add(:milestone, 'must be a string') + end + + def reason_known_or_string + return if REASONS.key?(reason) + return if reason_is_string? + + errors.add(:reason, 'must be a known reason or a string') + end + + def reason_is_string? + reason.is_a?(String) + end + + def reason_text + @reason_text ||= REASONS[reason] || "#{reason.to_s.strip}." + end + + def description_suffix + " #{deprecated_in}: #{reason_text}" + end + + def deprecated_in(format: :plain) + case format + when :plain + "Deprecated in #{milestone}" + when :markdown + "**Deprecated** in #{milestone}" + end + end + end + end +end diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb index e9ff85d9ca9..f4173e26224 100644 --- a/lib/gitlab/graphql/docs/helper.rb +++ b/lib/gitlab/graphql/docs/helper.rb @@ -27,7 +27,10 @@ module Gitlab MD end - def render_name_and_description(object, level = 3) + # Template methods: + # Methods that return chunks of Markdown for insertion into the document + + def render_name_and_description(object, owner: nil, level: 3) content = [] content << "#{'#' * level} `#{object[:name]}`" @@ -35,10 +38,22 @@ module Gitlab if object[:description].present? desc = object[:description].strip desc += '.' unless desc.ends_with?('.') + end + + if object[:is_deprecated] + owner = Array.wrap(owner) + deprecation = schema_deprecation(owner, object[:name]) + content << (deprecation&.original_description || desc) + content << render_deprecation(object, owner, :block) + else content << desc end - content.join("\n\n") + content.compact.join("\n\n") + end + + def render_return_type(query) + "Returns #{render_field_type(query[:type])}.\n" end def sorted_by_name(objects) @@ -47,39 +62,25 @@ module Gitlab objects.sort_by { |o| o[:name] } end - def render_field(field) - row(render_name(field), render_field_type(field[:type]), render_description(field)) + def render_field(field, owner) + render_row( + render_name(field, owner), + render_field_type(field[:type]), + render_description(field, owner, :inline) + ) end - def render_enum_value(value) - row(render_name(value), render_description(value)) + def render_enum_value(enum, value) + render_row(render_name(value, enum[:name]), render_description(value, enum[:name], :inline)) end - def row(*values) - "| #{values.join(' | ')} |" + def render_union_member(member) + "- [`#{member}`](##{member.downcase})" end - def render_name(object) - rendered_name = "`#{object[:name]}`" - rendered_name += ' **{warning-solid}**' if object[:is_deprecated] - rendered_name - end + # QUERIES: - # Returns the object description. If the object has been deprecated, - # the deprecation reason will be returned in place of the description. - def render_description(object) - return object[:description] unless object[:is_deprecated] - - "**Deprecated:** #{object[:deprecation_reason]}" - end - - def render_field_type(type) - "[`#{type[:info]}`](##{type[:name].downcase})" - end - - def render_return_type(query) - "Returns #{render_field_type(query[:type])}.\n" - end + # Methods that return parts of the schema, or related information: # We are ignoring connections and built in types for now, # they should be added when queries are generated. @@ -103,6 +104,83 @@ module Gitlab !enum_type[:name].in?(%w[__DirectiveLocation __TypeKind]) end end + + private # DO NOT CALL THESE METHODS IN TEMPLATES + + # Template methods + + def render_row(*values) + "| #{values.map { |val| val.to_s.squish }.join(' | ')} |" + end + + def render_name(object, owner = nil) + rendered_name = "`#{object[:name]}`" + rendered_name += ' **{warning-solid}**' if object[:is_deprecated] + rendered_name + end + + # Returns the object description. If the object has been deprecated, + # the deprecation reason will be returned in place of the description. + def render_description(object, owner = nil, context = :block) + owner = Array.wrap(owner) + return render_deprecation(object, owner, context) if object[:is_deprecated] + return if object[:description].blank? + + desc = object[:description].strip + desc += '.' unless desc.ends_with?('.') + desc + end + + def render_deprecation(object, owner, context) + deprecation = schema_deprecation(owner, object[:name]) + return deprecation.markdown(context: context) if deprecation + + reason = object[:deprecation_reason] || 'Use of this is deprecated.' + "**Deprecated:** #{reason}" + end + + def render_field_type(type) + "[`#{type[:info]}`](##{type[:name].downcase})" + end + + # Queries + + # returns the deprecation information for a field or argument + # See: Gitlab::Graphql::Deprecation + def schema_deprecation(type_name, field_name) + schema_member(type_name, field_name)&.deprecation + end + + # Return a part of the schema. + # + # This queries the Schema by owner and name to find: + # + # - fields (e.g. `schema_member('Query', 'currentUser')`) + # - arguments (e.g. `schema_member(['Query', 'project], 'fullPath')`) + def schema_member(type_name, field_name) + type_name = Array.wrap(type_name) + if type_name.size == 2 + arg_name = field_name + type_name, field_name = type_name + else + type_name = type_name.first + arg_name = nil + end + + return if type_name.nil? || field_name.nil? + + type = schema.types[type_name] + return unless type && type.kind.fields? + + field = type.fields[field_name] + return field if arg_name.nil? + + args = field.arguments + is_mutation = field.mutation && field.mutation <= ::Mutations::BaseMutation + args = args['input'].type.unwrap.arguments if is_mutation + + args[arg_name] + end end end end diff --git a/lib/gitlab/graphql/docs/renderer.rb b/lib/gitlab/graphql/docs/renderer.rb index 6abd56c89c6..497567f9389 100644 --- a/lib/gitlab/graphql/docs/renderer.rb +++ b/lib/gitlab/graphql/docs/renderer.rb @@ -10,17 +10,20 @@ module Gitlab # It uses graphql-docs helpers and schema parser, more information in https://github.com/gjtorikian/graphql-docs. # # Arguments: - # schema - the GraphQL schema definition. For GitLab should be: GitlabSchema.graphql_definition + # schema - the GraphQL schema definition. For GitLab should be: GitlabSchema # output_dir: The folder where the markdown files will be saved # template: The path of the haml template to be parsed class Renderer include Gitlab::Graphql::Docs::Helper + attr_reader :schema + def initialize(schema, output_dir:, template:) @output_dir = output_dir @template = template @layout = Haml::Engine.new(File.read(template)) - @parsed_schema = GraphQLDocs::Parser.new(schema, {}).parse + @parsed_schema = GraphQLDocs::Parser.new(schema.graphql_definition, {}).parse + @schema = schema end def contents diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml index 847f1777b08..fe73297d0d9 100644 --- a/lib/gitlab/graphql/docs/templates/default.md.haml +++ b/lib/gitlab/graphql/docs/templates/default.md.haml @@ -27,7 +27,7 @@ \ - sorted_by_name(queries).each do |query| - = render_name_and_description(query) + = render_name_and_description(query, owner: 'Query') \ = render_return_type(query) - unless query[:arguments].empty? @@ -35,7 +35,7 @@ ~ "| Name | Type | Description |" ~ "| ---- | ---- | ----------- |" - sorted_by_name(query[:arguments]).each do |argument| - = render_field(argument) + = render_field(argument, query[:type][:name]) \ :plain @@ -58,7 +58,7 @@ ~ "| Field | Type | Description |" ~ "| ----- | ---- | ----------- |" - sorted_by_name(type[:fields]).each do |field| - = render_field(field) + = render_field(field, type[:name]) \ :plain @@ -79,7 +79,7 @@ ~ "| Value | Description |" ~ "| ----- | ----------- |" - sorted_by_name(enum[:values]).each do |value| - = render_enum_value(value) + = render_enum_value(enum, value) \ :plain @@ -121,12 +121,12 @@ \ - graphql_union_types.each do |type| - = render_name_and_description(type, 4) + = render_name_and_description(type, level: 4) \ One of: \ - - type[:possible_types].each do |type_name| - ~ "- [`#{type_name}`](##{type_name.downcase})" + - type[:possible_types].each do |member| + = render_union_member(member) \ :plain @@ -134,7 +134,7 @@ \ - graphql_interface_types.each do |type| - = render_name_and_description(type, 4) + = render_name_and_description(type, level: 4) \ Implementations: \ @@ -144,5 +144,5 @@ ~ "| Field | Type | Description |" ~ "| ----- | ---- | ----------- |" - sorted_by_name(type[:fields] + type[:connections]).each do |field| - = render_field(field) + = render_field(field, type[:name]) \ diff --git a/lib/gitlab/graphql/loaders/batch_lfs_oid_loader.rb b/lib/gitlab/graphql/loaders/batch_lfs_oid_loader.rb index 67511c124e4..1945388cdd4 100644 --- a/lib/gitlab/graphql/loaders/batch_lfs_oid_loader.rb +++ b/lib/gitlab/graphql/loaders/batch_lfs_oid_loader.rb @@ -5,7 +5,8 @@ module Gitlab module Loaders class BatchLfsOidLoader def initialize(repository, blob_id) - @repository, @blob_id = repository, blob_id + @repository = repository + @blob_id = blob_id end def find diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb index 9b85ba164d4..805864cdd4c 100644 --- a/lib/gitlab/graphql/loaders/batch_model_loader.rb +++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb @@ -7,7 +7,8 @@ module Gitlab attr_reader :model_class, :model_id def initialize(model_class, model_id) - @model_class, @model_id = model_class, model_id + @model_class = model_class + @model_id = model_id end # rubocop: disable CodeReuse/ActiveRecord diff --git a/lib/gitlab/graphql/loaders/full_path_model_loader.rb b/lib/gitlab/graphql/loaders/full_path_model_loader.rb index 0aa237c78de..26c1ce64a83 100644 --- a/lib/gitlab/graphql/loaders/full_path_model_loader.rb +++ b/lib/gitlab/graphql/loaders/full_path_model_loader.rb @@ -9,7 +9,8 @@ module Gitlab attr_reader :model_class, :full_path def initialize(model_class, full_path) - @model_class, @full_path = model_class, full_path + @model_class = model_class + @full_path = full_path end def find diff --git a/lib/gitlab/graphql/negatable_arguments.rb b/lib/gitlab/graphql/negatable_arguments.rb new file mode 100644 index 00000000000..b4ab31ed51a --- /dev/null +++ b/lib/gitlab/graphql/negatable_arguments.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module NegatableArguments + class TypeDefiner + def initialize(resolver_class, type_definition) + @resolver_class = resolver_class + @type_definition = type_definition + end + + def define! + negated_params_type.instance_eval(&@type_definition) + end + + def negated_params_type + @negated_params_type ||= existing_type || build_type + end + + private + + def existing_type + ::Types.const_get(type_class_name, false) if ::Types.const_defined?(type_class_name) + end + + def build_type + klass = Class.new(::Types::BaseInputObject) + ::Types.const_set(type_class_name, klass) + klass + end + + def type_class_name + @type_class_name ||= begin + base_name = @resolver_class.name.sub('Resolvers::', '') + base_name + 'NegatedParamsType' + end + end + end + + def negated(param_key: :not, &block) + definer = ::Gitlab::Graphql::NegatableArguments::TypeDefiner.new(self, block) + definer.define! + + argument param_key, definer.negated_params_type, + required: false, + description: <<~MD + List of negated arguments. + Warning: this argument is experimental and a subject to change in future. + MD + end + end + end +end diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb index bd785880b57..6645dac36fa 100644 --- a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb +++ b/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb @@ -13,7 +13,11 @@ module Gitlab # @param [Symbol] before_or_after indicates whether we want # items :before the cursor or :after the cursor def initialize(arel_table, order_list, values, operators, before_or_after) - @arel_table, @order_list, @values, @operators, @before_or_after = arel_table, order_list, values, operators, before_or_after + @arel_table = arel_table + @order_list = order_list + @values = values + @operators = operators + @before_or_after = before_or_after @before_or_after = :after unless [:after, :before].include?(@before_or_after) end diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb index 3164598b7b9..ec70f5c5a24 100644 --- a/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb +++ b/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb @@ -30,15 +30,13 @@ module Gitlab # ex: " OR (relative_position = 23 AND id > 500)" def second_attribute_condition - condition = <<~SQL + <<~SQL OR ( #{table_condition(order_list.first, values.first, '=').to_sql} AND #{table_condition(order_list[1], values[1], operators[1]).to_sql} ) SQL - - condition end # ex: " OR (relative_position IS NULL)" diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb index fa25181d663..1aae1020e79 100644 --- a/lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb +++ b/lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb @@ -14,15 +14,13 @@ module Gitlab # ex: "(relative_position IS NULL AND id > 500)" def first_attribute_condition - condition = <<~SQL + <<~SQL ( #{table_condition(order_list.first, nil, 'is_null').to_sql} AND #{table_condition(order_list[1], values[1], operators[1]).to_sql} ) SQL - - condition end # ex: " OR (relative_position IS NOT NULL)" diff --git a/lib/gitlab/graphql/pagination/keyset/query_builder.rb b/lib/gitlab/graphql/pagination/keyset/query_builder.rb index 29169449843..ee9c902c735 100644 --- a/lib/gitlab/graphql/pagination/keyset/query_builder.rb +++ b/lib/gitlab/graphql/pagination/keyset/query_builder.rb @@ -6,7 +6,10 @@ module Gitlab module Keyset class QueryBuilder def initialize(arel_table, order_list, decoded_cursor, before_or_after) - @arel_table, @order_list, @decoded_cursor, @before_or_after = arel_table, order_list, decoded_cursor, before_or_after + @arel_table = arel_table + @order_list = order_list + @decoded_cursor = decoded_cursor + @before_or_after = before_or_after if order_list.empty? raise ArgumentError.new('No ordering scopes have been supplied') diff --git a/lib/gitlab/graphql/queries.rb b/lib/gitlab/graphql/queries.rb index fcf293fb13e..74f55abccbc 100644 --- a/lib/gitlab/graphql/queries.rb +++ b/lib/gitlab/graphql/queries.rb @@ -224,11 +224,9 @@ module Gitlab frag_path = frag_path.gsub(DOTS_RE) do |dots| rel_dir(dots.split('/').count) end - frag_path = frag_path.gsub(IMPLICIT_ROOT) do + frag_path.gsub(IMPLICIT_ROOT) do (Rails.root / 'app').to_s + '/' end - - frag_path end def rel_dir(n_steps_up) diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb index 8acd27869a9..c6f22e0bd4f 100644 --- a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb +++ b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb @@ -12,6 +12,7 @@ module Gitlab def initial_value(query) variables = process_variables(query.provided_variables) default_initial_values(query).merge({ + operation_name: query.operation_name, query_string: query.query_string, variables: variables }) @@ -20,8 +21,8 @@ module Gitlab default_initial_values(query) end - def call(memo, visit_type, irep_node) - RequestStore.store[:graphql_logs] = memo + def call(memo, *) + memo end def final_value(memo) @@ -37,6 +38,8 @@ module Gitlab memo[:used_fields] = field_usages.first memo[:used_deprecated_fields] = field_usages.second + RequestStore.store[:graphql_logs] ||= [] + RequestStore.store[:graphql_logs] << memo GraphqlLogger.info(memo.except!(:time_started, :query)) rescue => e Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) |