elasticgraph-graphql 0.19.1.1 → 0.19.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +1 -1
  4. data/lib/elastic_graph/graphql/aggregation/computation.rb +1 -1
  5. data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +1 -1
  6. data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +1 -1
  7. data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +1 -1
  8. data/lib/elastic_graph/graphql/aggregation/key.rb +1 -1
  9. data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +1 -1
  10. data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +2 -2
  11. data/lib/elastic_graph/graphql/aggregation/path_segment.rb +2 -2
  12. data/lib/elastic_graph/graphql/aggregation/query.rb +1 -1
  13. data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +33 -6
  14. data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +1 -1
  15. data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +2 -6
  16. data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +1 -1
  17. data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +26 -6
  18. data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +1 -1
  19. data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +5 -6
  20. data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +10 -8
  21. data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +1 -1
  22. data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +2 -2
  23. data/lib/elastic_graph/graphql/client.rb +1 -1
  24. data/lib/elastic_graph/graphql/config.rb +21 -6
  25. data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +10 -5
  26. data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +2 -3
  27. data/lib/elastic_graph/graphql/datastore_query/paginator.rb +1 -1
  28. data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +2 -3
  29. data/lib/elastic_graph/graphql/datastore_query.rb +66 -74
  30. data/lib/elastic_graph/graphql/datastore_response/document.rb +1 -1
  31. data/lib/elastic_graph/graphql/datastore_response/search_response.rb +83 -9
  32. data/lib/elastic_graph/graphql/datastore_search_router.rb +19 -4
  33. data/lib/elastic_graph/graphql/decoded_cursor.rb +1 -1
  34. data/lib/elastic_graph/graphql/filtering/boolean_query.rb +1 -1
  35. data/lib/elastic_graph/graphql/filtering/field_path.rb +1 -1
  36. data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +2 -2
  37. data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +10 -5
  38. data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +2 -2
  39. data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +17 -2
  40. data/lib/elastic_graph/graphql/filtering/range_query.rb +1 -1
  41. data/lib/elastic_graph/graphql/http_endpoint.rb +2 -2
  42. data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +1 -1
  43. data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +1 -1
  44. data/lib/elastic_graph/graphql/query_adapter/filters.rb +1 -1
  45. data/lib/elastic_graph/graphql/query_adapter/pagination.rb +1 -1
  46. data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +18 -3
  47. data/lib/elastic_graph/graphql/query_adapter/sort.rb +1 -1
  48. data/lib/elastic_graph/graphql/query_details_tracker.rb +13 -4
  49. data/lib/elastic_graph/graphql/query_executor.rb +12 -5
  50. data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +6 -12
  51. data/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb +123 -0
  52. data/lib/elastic_graph/graphql/resolvers/list_records.rb +4 -4
  53. data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +57 -27
  54. data/lib/elastic_graph/graphql/resolvers/nested_relationships_source.rb +324 -0
  55. data/lib/elastic_graph/graphql/resolvers/object.rb +36 -0
  56. data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +2 -2
  57. data/lib/elastic_graph/graphql/resolvers/query_source.rb +6 -3
  58. data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +1 -1
  59. data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +1 -1
  60. data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +1 -1
  61. data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +1 -1
  62. data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +1 -1
  63. data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +2 -7
  64. data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +1 -1
  65. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +1 -1
  66. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +1 -1
  67. data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +1 -1
  68. data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +1 -1
  69. data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +1 -1
  70. data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +1 -1
  71. data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +1 -1
  72. data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +1 -1
  73. data/lib/elastic_graph/graphql/schema/arguments.rb +1 -1
  74. data/lib/elastic_graph/graphql/schema/enum_value.rb +1 -1
  75. data/lib/elastic_graph/graphql/schema/field.rb +12 -27
  76. data/lib/elastic_graph/graphql/schema/relation_join.rb +17 -9
  77. data/lib/elastic_graph/graphql/schema/type.rb +15 -7
  78. data/lib/elastic_graph/graphql/schema.rb +11 -31
  79. data/lib/elastic_graph/graphql.rb +38 -40
  80. data/script/dump_time_zones +1 -1
  81. metadata +25 -27
  82. data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +0 -114
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -18,12 +18,13 @@ module ElasticGraph
18
18
  # @dynamic schema
19
19
  attr_reader :schema
20
20
 
21
- def initialize(schema:, monotonic_clock:, logger:, slow_query_threshold_ms:, datastore_search_router:)
21
+ def initialize(schema:, monotonic_clock:, logger:, slow_query_threshold_ms:, datastore_search_router:, config:)
22
22
  @schema = schema
23
23
  @monotonic_clock = monotonic_clock
24
24
  @logger = logger
25
25
  @slow_query_threshold_ms = slow_query_threshold_ms
26
26
  @datastore_search_router = datastore_search_router
27
+ @config = config
27
28
  end
28
29
 
29
30
  # Executes the given `query_string` using the provided `variables`.
@@ -60,11 +61,13 @@ module ElasticGraph
60
61
  operation_name: operation_name,
61
62
  client: client,
62
63
  context: context.merge({
64
+ logger: @logger,
63
65
  monotonic_clock_deadline: timeout_in_ms&.+(start_time_in_ms),
64
66
  elastic_graph_schema: @schema,
65
67
  schema_element_names: @schema.element_names,
66
68
  elastic_graph_query_tracker: query_tracker,
67
- datastore_search_router: @datastore_search_router
69
+ datastore_search_router: @datastore_search_router,
70
+ nested_relationship_resolver_mode: @config.nested_relationship_resolver_mode
68
71
  }.compact)
69
72
  )
70
73
 
@@ -101,12 +104,16 @@ module ElasticGraph
101
104
  "query_fingerprint" => fingerprint_for(query),
102
105
  "query_name" => query.operation_name,
103
106
  "duration_ms" => duration,
104
- # Here we log how long the datastore queries took according to what the datastore itself reported.
107
+ # How long the datastore queries took according to what the datastore itself reported.
105
108
  "datastore_server_duration_ms" => query_tracker.datastore_query_server_duration_ms,
106
- # Here we log an estimate for how much overhead ElasticGraph added on top of how long the datastore took.
109
+ # An estimate for how much overhead ElasticGraph added on top of how long the datastore took.
107
110
  # This is based on the duration, excluding how long the datastore calls took from the client side
108
111
  # (e.g. accounting for network latency, serialization time, etc)
109
112
  "elasticgraph_overhead_ms" => duration - query_tracker.datastore_query_client_duration_ms,
113
+ # An estimate for the time spent on transport (network latency, JSON serialization, etc).
114
+ "datastore_request_transport_duration_ms" => query_tracker.datastore_request_transport_duration_ms,
115
+ # How many datastore shards were queried, in total. This is a measure of how much load the query caused on the datastore.
116
+ "queried_shard_count" => query_tracker.queried_shard_count,
110
117
  # According to https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html#metric-filters-extract-json,
111
118
  # > Value nodes can be strings or numbers...If a property selector points to an array or object, the metric filter won't match the log format.
112
119
  # So, to allow flexibility to deal with cloud watch metric filters, we coerce these values to a string here.
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -15,16 +15,11 @@ module ElasticGraph
15
15
  module Resolvers
16
16
  # Responsible for fetching a single field value from a document.
17
17
  class GetRecordFieldValue
18
- def initialize(schema_element_names:)
19
- @schema_element_names = schema_element_names
18
+ def initialize(elasticgraph_graphql:, config:)
19
+ @schema_element_names = elasticgraph_graphql.runtime_metadata.schema_element_names
20
20
  end
21
21
 
22
- def can_resolve?(field:, object:)
23
- object.is_a?(DatastoreResponse::Document) || object.is_a?(::Hash)
24
- end
25
-
26
- def resolve(field:, object:, args:, context:, lookahead:)
27
- field_name = field.name_in_index.to_s
22
+ def resolve(field:, object:, args:, context:)
28
23
  data =
29
24
  case object
30
25
  when DatastoreResponse::Document
@@ -33,9 +28,8 @@ module ElasticGraph
33
28
  object
34
29
  end
35
30
 
36
- value = Support::HashUtil.fetch_value_at_path(data, field_name) do
37
- field.type.list? ? [] : nil
38
- end
31
+ value = Support::HashUtil.fetch_value_at_path(data, field.path_in_index) { nil }
32
+ value = [] if value.nil? && field.type.list?
39
33
 
40
34
  if field.type.relay_connection?
41
35
  RelayConnection::ArrayAdapter.build(value, args, @schema_element_names, context)
@@ -0,0 +1,123 @@
1
+ # Copyright 2024 - 2025 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/errors"
10
+
11
+ module ElasticGraph
12
+ class GraphQL
13
+ module Resolvers
14
+ # Provides an adapter to the GraphQL gem by building a resolver implementation hash as documented here:
15
+ #
16
+ # https://graphql-ruby.org/schema/sdl.html
17
+ class GraphQLAdapterBuilder
18
+ def initialize(runtime_metadata:, named_resolvers:, query_adapter:)
19
+ @runtime_metadata = runtime_metadata
20
+ @named_resolvers = named_resolvers
21
+ @query_adapter = query_adapter
22
+ end
23
+
24
+ def build
25
+ scalar_type_hash
26
+ .merge(object_type_hash)
27
+ .merge({"resolve_type" => _ = ->(supertype, obj, ctx) { resolve_type(supertype, obj, ctx) }})
28
+ end
29
+
30
+ private
31
+
32
+ def scalar_type_hash
33
+ @runtime_metadata.scalar_types_by_name.transform_values do |scalar_type|
34
+ adapter = (_ = scalar_type.load_coercion_adapter.extension_class) # : SchemaArtifacts::_ScalarCoercionAdapter[untyped, untyped]
35
+ {
36
+ "coerce_input" => ->(value, ctx) { adapter.coerce_input(value, ctx) },
37
+ "coerce_result" => ->(value, ctx) { adapter.coerce_result(value, ctx) }
38
+ }
39
+ end
40
+ end
41
+
42
+ def object_type_hash
43
+ @runtime_metadata.object_types_by_name.filter_map do |type_name, type|
44
+ fields_hash = type.graphql_fields_by_name.filter_map do |field_name, field|
45
+ if (resolver_name = field.resolver)
46
+ resolver = @named_resolvers.fetch(resolver_name) do
47
+ raise Errors::SchemaError, "Resolver `#{resolver_name}` (for `#{type_name}.#{field_name}`) cannot be found."
48
+ end
49
+
50
+ resolver_lambda =
51
+ if resolver.method(:resolve).parameters.include?([:keyreq, :lookahead])
52
+ lambda do |object, args, context|
53
+ schema_field = context.fetch(:elastic_graph_schema).field_named(type_name, field_name)
54
+
55
+ # Extract the `:lookahead` extra that we have configured all fields to provide.
56
+ # See https://graphql-ruby.org/api-doc/1.10.8/GraphQL/Execution/Lookahead.html for more info.
57
+ # It is not a "real" arg in the schema and breaks `args_to_schema_form` when we call that
58
+ # so we need to peel it off here.
59
+ lookahead = args[:lookahead]
60
+
61
+ # Convert args to the form they were defined in the schema, undoing the normalization
62
+ # the GraphQL gem does to convert them to Ruby keyword args form.
63
+ args = schema_field.args_to_schema_form(args.except(:lookahead))
64
+
65
+ result = resolver.resolve(field: schema_field, object: object, args: args, context: context, lookahead: lookahead) do
66
+ @query_adapter.build_query_from(field: schema_field, args: args, lookahead: lookahead, context: context)
67
+ end
68
+
69
+ # Give the field a chance to coerce the result before returning it. Initially, this is only used to deal with
70
+ # enum value overrides (e.g. so that if `DayOfWeek.MONDAY` has been overridden to `DayOfWeek.MON`, we can coerce
71
+ # a `MONDAY` value being returned by a painless script to `MON`), but this is designed to be general purpose
72
+ # and we may use it for other coercions in the future.
73
+ #
74
+ # Note that coercion of scalar values is handled by the `coerce_result` callback below.
75
+ schema_field.coerce_result(result)
76
+ end
77
+ else
78
+ lambda do |object, args, context|
79
+ schema_field = context.fetch(:elastic_graph_schema).field_named(type_name, field_name)
80
+ # Convert args to the form they were defined in the schema, undoing the normalization
81
+ # the GraphQL gem does to convert them to Ruby keyword args form.
82
+ args = schema_field.args_to_schema_form(args)
83
+
84
+ result = resolver.resolve(field: schema_field, object: object, args: args, context: context)
85
+
86
+ # Give the field a chance to coerce the result before returning it. Initially, this is only used to deal with
87
+ # enum value overrides (e.g. so that if `DayOfWeek.MONDAY` has been overridden to `DayOfWeek.MON`, we can coerce
88
+ # a `MONDAY` value being returned by a painless script to `MON`), but this is designed to be general purpose
89
+ # and we may use it for other coercions in the future.
90
+ #
91
+ # Note that coercion of scalar values is handled by the `coerce_result` callback below.
92
+ schema_field.coerce_result(result)
93
+ end
94
+ end
95
+
96
+ [field_name, resolver_lambda]
97
+ end
98
+ end.to_h
99
+
100
+ unless fields_hash.empty?
101
+ [type_name, fields_hash]
102
+ end
103
+ end.to_h
104
+ end
105
+
106
+ # In order to support unions and interfaces, we must implement `resolve_type`.
107
+ def resolve_type(supertype, object, context)
108
+ schema = context.fetch(:elastic_graph_schema)
109
+ # If `__typename` is available, use that to resolve. It should be available on any embedded abstract types...
110
+ # (See `Inventor` in `config/schema.graphql` for an example of this kind of type union.)
111
+ if (typename = object["__typename"])
112
+ schema.graphql_schema.possible_types(supertype).find { |t| t.graphql_name == typename }
113
+ else
114
+ # ...otherwise infer the type based on what index the object came from. This is the case
115
+ # with unions/interfaces of individually indexed types.
116
+ # (See `Part` in `config/schema/widgets.rb` for an example of this kind of type union.)
117
+ schema.document_type_stored_in(object.index_definition_name).graphql_type
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -14,11 +14,11 @@ module ElasticGraph
14
14
  module Resolvers
15
15
  # Responsible for fetching a a list of records of a particular type
16
16
  class ListRecords
17
- def can_resolve?(field:, object:)
18
- field.parent_type.name == :Query && field.type.collection?
17
+ def initialize(elasticgraph_graphql:, config:)
18
+ # Nothing to initialize, but needs to be defined to satisfy the resolver interface.
19
19
  end
20
20
 
21
- def resolve(field:, context:, lookahead:, **)
21
+ def resolve(field:, object:, args:, context:, lookahead:)
22
22
  query = yield
23
23
  response = QuerySource.execute_one(query, for_context: context)
24
24
  RelayConnection.maybe_wrap(response, field: field, context: context, lookahead: lookahead, query: query)
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -6,7 +6,9 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
+ require "elastic_graph/graphql/resolvers/nested_relationships_source"
9
10
  require "elastic_graph/graphql/resolvers/relay_connection"
11
+ require "elastic_graph/graphql/datastore_response/search_response"
10
12
 
11
13
  module ElasticGraph
12
14
  class GraphQL
@@ -17,31 +19,32 @@ module ElasticGraph
17
19
  #
18
20
  # Most of the logic for this lives in ElasticGraph::Schema::RelationJoin.
19
21
  class NestedRelationships
20
- def initialize(schema_element_names:, logger:)
21
- @schema_element_names = schema_element_names
22
- @logger = logger
22
+ def initialize(elasticgraph_graphql:, config:)
23
+ @schema_element_names = elasticgraph_graphql.runtime_metadata.schema_element_names
24
+ @logger = elasticgraph_graphql.logger
25
+ @monotonic_clock = elasticgraph_graphql.monotonic_clock
26
+ @resolver_mode = elasticgraph_graphql.config.nested_relationship_resolver_mode
23
27
  end
24
28
 
25
- def can_resolve?(field:, object:)
26
- !!field.relation_join
27
- end
28
-
29
- def resolve(object:, field:, context:, lookahead:, **)
29
+ def resolve(field:, object:, args:, context:, lookahead:)
30
30
  log_warning = ->(**options) { log_field_problem_warning(field: field, **options) }
31
31
  join = field.relation_join
32
32
  id_or_ids = join.extract_id_or_ids_from(object, log_warning)
33
- filters = [
34
- build_filter(join.filter_id_field_name, nil, join.foreign_key_nested_paths, Array(id_or_ids)),
35
- join.additional_filter
36
- ].reject(&:empty?)
37
- query = yield.merge_with(filters: filters)
33
+ query = yield
38
34
 
39
35
  response =
40
36
  case id_or_ids
41
37
  when nil, []
42
38
  join.blank_value
43
39
  else
44
- initial_response = QuerySource.execute_one(query, for_context: context)
40
+ initial_response = try_synthesize_response_from_ids(field, id_or_ids, query) ||
41
+ NestedRelationshipsSource.execute_one(
42
+ Array(id_or_ids).to_set,
43
+ query:, join:, context:,
44
+ monotonic_clock: @monotonic_clock,
45
+ mode: @resolver_mode
46
+ )
47
+
45
48
  join.normalize_documents(initial_response) do |problem|
46
49
  log_warning.call(document: {"id" => id_or_ids}, problem: "got #{problem} from the datastore search query")
47
50
  end
@@ -52,22 +55,49 @@ module ElasticGraph
52
55
 
53
56
  private
54
57
 
58
+ ONLY_ID = ["id"]
59
+
60
+ # When a client requests only the `id` from a nested relationship, and we already have those ids,
61
+ # we want to avoid querying the datastore, and synthesize a response instead.
62
+ def try_synthesize_response_from_ids(field, id_or_ids, query)
63
+ # This optimization can only be used on a relationship with an outbound foreign key.
64
+ return nil if field.relation.direction == :in
65
+
66
+ # If the client is requesting any fields besides `id`, we can't do this.
67
+ return nil unless (query.requested_fields - ONLY_ID).empty?
68
+
69
+ pagination = query.document_paginator.to_datastore_body
70
+ search_after = pagination.dig(:search_after, 0)
71
+ ids = Array(id_or_ids)
72
+
73
+ sorted_ids =
74
+ case pagination.dig(:sort, 0, "id", "order")
75
+ when "asc"
76
+ ids.sort.select { |id| search_after.nil? || id > search_after }
77
+ when "desc"
78
+ ids.sort.reverse.select { |id| search_after.nil? || id < search_after }
79
+ else
80
+ if ids.size < 2
81
+ ids
82
+ else
83
+ # The client is sorting by something other than `id` and we have multiple ids.
84
+ # We aren't able to determine the correct order for the ids, so we can't synthesize
85
+ # a response.
86
+ return nil
87
+ end
88
+ end
89
+
90
+ DatastoreResponse::SearchResponse.synthesize_from_ids(
91
+ query.search_index_expression,
92
+ sorted_ids.first(pagination.fetch(:size)),
93
+ decoded_cursor_factory: query.send(:decoded_cursor_factory)
94
+ )
95
+ end
96
+
55
97
  def log_field_problem_warning(field:, document:, problem:)
56
98
  id = document.fetch("id", "<no id>")
57
99
  @logger.warn "#{field.parent_type.name}(id: #{id}).#{field.name} had a problem: #{problem}"
58
100
  end
59
-
60
- def build_filter(path, previous_nested_path, nested_paths, ids)
61
- if nested_paths.empty?
62
- path = path.delete_prefix("#{previous_nested_path}.") if previous_nested_path
63
- {path => {@schema_element_names.equal_to_any_of => ids}}
64
- else
65
- next_nested_path, *rest_nested_paths = nested_paths
66
- sub_filter = build_filter(path, next_nested_path, rest_nested_paths, ids)
67
- next_nested_path = next_nested_path.delete_prefix("#{previous_nested_path}.") if previous_nested_path
68
- {next_nested_path => {@schema_element_names.any_satisfy => sub_filter}}
69
- end
70
- end
71
101
  end
72
102
  end
73
103
  end