diff options
Diffstat (limited to 'lib/gitlab/graphql')
-rw-r--r-- | lib/gitlab/graphql/authorize/authorize_field_service.rb | 14 | ||||
-rw-r--r-- | lib/gitlab/graphql/authorize/instrumentation.rb | 6 | ||||
-rw-r--r-- | lib/gitlab/graphql/filterable_array.rb | 14 | ||||
-rw-r--r-- | lib/gitlab/graphql/loaders/full_path_model_loader.rb | 26 | ||||
-rw-r--r-- | lib/gitlab/graphql/pagination/connections.rb | 4 | ||||
-rw-r--r-- | lib/gitlab/graphql/pagination/filterable_array_connection.rb | 17 | ||||
-rw-r--r-- | lib/gitlab/graphql/pagination/keyset/connection.rb | 100 |
7 files changed, 129 insertions, 52 deletions
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb index 61668b634fd..cbf3e7b8429 100644 --- a/lib/gitlab/graphql/authorize/authorize_field_service.rb +++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb @@ -84,13 +84,25 @@ module Gitlab elsif resolved_type.is_a? Array # A simple list of rendered types each object being an object to authorize resolved_type.select do |single_object_type| - allowed_access?(current_user, single_object_type.object) + allowed_access?(current_user, realized(single_object_type).object) end else raise "Can't authorize #{@field}" end end + # Ensure that we are dealing with realized objects, not delayed promises + def realized(thing) + case thing + when BatchLoader::GraphQL + thing.sync + when GraphQL::Execution::Lazy + thing.value # part of the private api, but we need to unwrap it here. + else + thing + end + end + def allowed_access?(current_user, object) object = object.sync if object.respond_to?(:sync) diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb index f9ff2b30eae..15ecc3b04f0 100644 --- a/lib/gitlab/graphql/authorize/instrumentation.rb +++ b/lib/gitlab/graphql/authorize/instrumentation.rb @@ -9,16 +9,12 @@ module Gitlab def instrument(_type, field) service = AuthorizeFieldService.new(field) - if service.authorizations? && !resolver_skips_authorizations?(field) + if service.authorizations? field.redefine { resolve(service.authorized_resolve) } else field end end - - def resolver_skips_authorizations?(field) - field.metadata[:resolver].try(:skip_authorizations?) - end end end end diff --git a/lib/gitlab/graphql/filterable_array.rb b/lib/gitlab/graphql/filterable_array.rb deleted file mode 100644 index 4909d291fd6..00000000000 --- a/lib/gitlab/graphql/filterable_array.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - class FilterableArray < Array - attr_reader :filter_callback - - def initialize(filter_callback, *args) - super(args) - @filter_callback = filter_callback - end - end - end -end diff --git a/lib/gitlab/graphql/loaders/full_path_model_loader.rb b/lib/gitlab/graphql/loaders/full_path_model_loader.rb new file mode 100644 index 00000000000..0aa237c78de --- /dev/null +++ b/lib/gitlab/graphql/loaders/full_path_model_loader.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Loaders + # Suitable for use to find resources that expose `where_full_path_in`, + # such as Project, Group, Namespace + class FullPathModelLoader + attr_reader :model_class, :full_path + + def initialize(model_class, full_path) + @model_class, @full_path = model_class, full_path + end + + def find + BatchLoader::GraphQL.for(full_path).batch(key: model_class) do |full_paths, loader, args| + # `with_route` avoids an N+1 calculating full_path + args[:key].where_full_path_in(full_paths).with_route.each do |model_instance| + loader.call(model_instance.full_path, model_instance) + end + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/pagination/connections.rb b/lib/gitlab/graphql/pagination/connections.rb index febdc938317..8f37fa3f474 100644 --- a/lib/gitlab/graphql/pagination/connections.rb +++ b/lib/gitlab/graphql/pagination/connections.rb @@ -10,10 +10,6 @@ module Gitlab Gitlab::Graphql::Pagination::Keyset::Connection) schema.connections.add( - Gitlab::Graphql::FilterableArray, - Gitlab::Graphql::Pagination::FilterableArrayConnection) - - schema.connections.add( Gitlab::Graphql::ExternallyPaginatedArray, Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection) end diff --git a/lib/gitlab/graphql/pagination/filterable_array_connection.rb b/lib/gitlab/graphql/pagination/filterable_array_connection.rb deleted file mode 100644 index 4a76cd5fb00..00000000000 --- a/lib/gitlab/graphql/pagination/filterable_array_connection.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Graphql - module Pagination - # FilterableArrayConnection is useful especially for lazy-loaded values. - # It allows us to call a callback only on the slice of array being - # rendered in the "after loaded" phase. For example we can check - # permissions only on a small subset of items. - class FilterableArrayConnection < GraphQL::Pagination::ArrayConnection - def nodes - @nodes ||= items.filter_callback.call(super) - end - end - end - end -end diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb index 1a32ab468b1..17cd22d38ad 100644 --- a/lib/gitlab/graphql/pagination/keyset/connection.rb +++ b/lib/gitlab/graphql/pagination/keyset/connection.rb @@ -32,6 +32,49 @@ module Gitlab class Connection < GraphQL::Pagination::ActiveRecordRelationConnection include Gitlab::Utils::StrongMemoize + # rubocop: disable Naming/PredicateName + # https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields + def has_previous_page + strong_memoize(:has_previous_page) do + if after + # If `after` is specified, that points to a specific record, + # even if it's the first one. Since we're asking for `after`, + # then the specific record we're pointing to is in the + # previous page + true + elsif last + limited_nodes + !!@has_previous_page + else + # Key thing to remember. When `before` is specified (and no `last`), + # the spec says return _all_ edges minus anything after the `before`. + # Which means the returned list starts at the very first record. + # Then the max_page kicks in, and returns the first max_page items. + # Because of this, `has_previous_page` will be false + false + end + end + end + + def has_next_page + strong_memoize(:has_next_page) do + if before + # If `before` is specified, that points to a specific record, + # even if it's the last one. Since we're asking for `before`, + # then the specific record we're pointing to is in the + # next page + true + elsif first + # If we count the number of requested items plus one (`limit_value + 1`), + # then if we get `limit_value + 1` then we know there is a next page + relation_count(set_limit(sliced_nodes, limit_value + 1)) == limit_value + 1 + else + false + end + end + end + # rubocop: enable Naming/PredicateName + def cursor_for(node) encoded_json_from_ordering(node) end @@ -39,7 +82,7 @@ module Gitlab def sliced_nodes @sliced_nodes ||= begin - OrderInfo.validate_ordering(ordered_items, order_list) + OrderInfo.validate_ordering(ordered_items, order_list) unless loaded?(ordered_items) sliced = ordered_items sliced = slice_nodes(sliced, before, :before) if before.present? @@ -54,20 +97,30 @@ module Gitlab # So we're ok loading them into memory here as that's bound to happen # anyway. Having them ready means we can modify the result while # rendering the fields. - @nodes ||= load_paged_nodes.to_a + @nodes ||= limited_nodes.to_a end private - def load_paged_nodes - if first && last - raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both") - end + # Apply `first` and `last` to `sliced_nodes` + def limited_nodes + strong_memoize(:limited_nodes) do + if first && last + raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both") + end - if last - sliced_nodes.last(limit_value) - else - sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord + if last + # grab one more than we need + paginated_nodes = sliced_nodes.last(limit_value + 1) + + # there is an extra node, so there is a previous page + @has_previous_page = paginated_nodes.count > limit_value + @has_previous_page ? paginated_nodes.last(limit_value) : paginated_nodes + elsif loaded?(sliced_nodes) + sliced_nodes.take(limit_value) # rubocop: disable CodeReuse/ActiveRecord + else + sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord + end end end @@ -82,9 +135,19 @@ module Gitlab # rubocop: enable CodeReuse/ActiveRecord def limit_value + # note: only first _or_ last can be specified, not both @limit_value ||= [first, last, max_page_size].compact.min end + def loaded?(items) + case items + when Array + true + else + items.loaded? + end + end + def ordered_items strong_memoize(:ordered_items) do unless items.primary_key.present? @@ -93,6 +156,16 @@ module Gitlab list = OrderInfo.build_order_list(items) + if loaded?(items) + @order_list = list.presence || [items.primary_key] + + # already sorted, or trivially sorted + next items if list.present? || items.size <= 1 + + pkey = items.primary_key.to_sym + next items.sort_by { |item| item[pkey] }.reverse + end + # ensure there is a primary key ordering if list&.last&.attribute_name != items.primary_key items.order(arel_table[items.primary_key].desc) # rubocop: disable CodeReuse/ActiveRecord @@ -121,7 +194,12 @@ module Gitlab order_list.each do |field| field_name = field.attribute_name - ordering[field_name] = node[field_name].to_s + field_value = node[field_name] + ordering[field_name] = if field_value.is_a?(Time) + field_value.strftime('%Y-%m-%d %H:%M:%S.%N %Z') + else + field_value.to_s + end end encode(ordering.to_json) |