elasticgraph-graphql 0.18.0.2 → 0.18.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b80c95effea6c96d77f52275854dcf2ef14fa9b56c56cbca44fef5b5e34da5d
4
- data.tar.gz: 6af6c4269f7d9b4822a16e0007c24574593261a585cc7de9227ec3584c50a5ed
3
+ metadata.gz: b0cb73d5ec2940bb792cbcada0c615dd1415de774bb9111ffe09ae293cf2de38
4
+ data.tar.gz: fb0f23546ee15fd3c9f434518dde7ccb79df0838795392d6b0a8c9e60df01667
5
5
  SHA512:
6
- metadata.gz: 33c540e6691fa35958a4b8c01984909f36a12092e05bb78e4cf91424a555d2932b79cdb50e8007756c2bd7d4a44bc176b36a7363cf5e5b2be75f3d5475d616d7
7
- data.tar.gz: 9ee9a9790c301d17f6467413d2ad02f6e01fd2305b5989eaed5fcbd78fdd8d84b9b8f21ef14cdd109444675a6e63df737555be7eb34be988bb44359a834ab0ea
6
+ metadata.gz: 4c21c60909ba34c7034b2c5506288f54d4e29aa715e9741e1f88a046b54d6a694082b657c00aa495da7d28eb771bec28c2a23d0c10b3c5dbfa46e20088c9802f
7
+ data.tar.gz: ecbd8f84362640638e36384875d43c8a9443a413bb407dd2fca5e1d18792bfe618ac5b15c14a9d0c7d2656b7234620d73ce95078bc18ed4cc72eecbec4958ad3
@@ -6,30 +6,27 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/constants"
10
- require "elastic_graph/graphql/filtering/boolean_query"
11
9
  require "elastic_graph/graphql/filtering/field_path"
12
- require "elastic_graph/graphql/filtering/range_query"
13
- require "elastic_graph/graphql/schema/enum_value"
10
+ require "elastic_graph/graphql/filtering/filter_node_interpreter"
14
11
  require "elastic_graph/support/graphql_formatter"
15
12
  require "elastic_graph/support/memoizable_data"
16
- require "elastic_graph/support/time_util"
17
13
  require "graphql"
18
14
 
19
15
  module ElasticGraph
20
16
  class GraphQL
21
17
  module Filtering
22
- # Contains all query logic related to filtering. Not tested directly; tests drive the `Query` interface instead.
18
+ # Responsible for interpreting a query's overall `filter`. Not tested directly; tests drive the `Query` interface instead.
19
+ #
23
20
  # For more info on how this works, see:
24
21
  # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
25
22
  # https://www.elastic.co/blog/lost-in-translation-boolean-operations-and-filters-in-the-bool-query
26
- FilterInterpreter = Support::MemoizableData.define(:runtime_metadata, :schema_names, :logger) do
23
+ FilterInterpreter = Support::MemoizableData.define(:filter_node_interpreter, :schema_names, :logger) do
27
24
  # @implements FilterInterpreter
28
25
 
29
- def initialize(runtime_metadata:, logger:)
26
+ def initialize(filter_node_interpreter:, logger:)
30
27
  super(
31
- runtime_metadata: runtime_metadata,
32
- schema_names: runtime_metadata.schema_element_names,
28
+ filter_node_interpreter: filter_node_interpreter,
29
+ schema_names: filter_node_interpreter.schema_names,
33
30
  logger: logger
34
31
  )
35
32
  end
@@ -49,12 +46,12 @@ module ElasticGraph
49
46
  end
50
47
 
51
48
  def to_s
52
- # The inspect/to_s output of `runtime_metadata` and `logger` can be quite large and noisy. We generally don't care about
49
+ # The inspect/to_s output of `filter_node_interpreter` and `logger` can be quite large and noisy. We generally don't care about
53
50
  # those details but want to be able to tell at a glance if two `FilterInterpreter` instances are equal or not--and, if they
54
51
  # aren't equal, which part is responsible for the inequality.
55
52
  #
56
53
  # Using the hash of the two initialize args provides us with that.
57
- "#<data #{FilterInterpreter.name} runtime_metadata=(hash: #{runtime_metadata.hash}) logger=(hash: #{logger.hash})>"
54
+ "#<data #{FilterInterpreter.name} filter_node_interpreter=(hash: #{filter_node_interpreter.hash}) logger=(hash: #{logger.hash})>"
58
55
  end
59
56
  alias_method :inspect, :to_s
60
57
 
@@ -67,7 +64,7 @@ module ElasticGraph
67
64
  # below having to be aware of possible `nil` predicates.
68
65
  expression = expression.compact if expression.is_a?(::Hash)
69
66
 
70
- case identify_expression_type(field_or_op, expression)
67
+ case filter_node_interpreter.identify_node_type(field_or_op, expression)
71
68
  when :empty
72
69
  # This is an "empty" filter predicate and we can ignore it.
73
70
  when :not
@@ -90,23 +87,11 @@ module ElasticGraph
90
87
  end
91
88
  end
92
89
 
93
- def identify_expression_type(field_or_op, expression)
94
- return :empty if expression.nil? || expression == {}
95
- return :not if field_or_op == schema_names.not
96
- return :list_any_filter if field_or_op == schema_names.any_satisfy
97
- return :all_of if field_or_op == schema_names.all_of
98
- return :any_of if field_or_op == schema_names.any_of
99
- return :operator if filter_operators.key?(field_or_op)
100
- return :list_count if field_or_op == LIST_COUNTS_FIELD
101
- return :sub_field if expression.is_a?(::Hash)
102
- :unknown
103
- end
104
-
105
90
  # Indicates if the given `expression` applies filtering to subfields or just applies
106
91
  # operators at the current field path.
107
92
  def filters_on_sub_fields?(expression)
108
93
  expression.any? do |field_or_op, sub_expression|
109
- case identify_expression_type(field_or_op, sub_expression)
94
+ case filter_node_interpreter.identify_node_type(field_or_op, sub_expression)
110
95
  when :sub_field
111
96
  true
112
97
  when :not, :list_any_filter
@@ -165,7 +150,9 @@ module ElasticGraph
165
150
  process_filter_hash(inner_node, filter, field_path.nested)
166
151
  end
167
152
 
168
- bool_node[:filter] << {nested: {path: field_path.from_root.join("."), query: sub_filter}}
153
+ if sub_filter
154
+ bool_node[:filter] << {nested: {path: field_path.from_root.join("."), query: sub_filter}}
155
+ end
169
156
  end
170
157
 
171
158
  # On a list-of-leaf-values field, `any_satisfy` doesn't _do_ anything: it just expresses
@@ -228,7 +215,7 @@ module ElasticGraph
228
215
  # `operator` is a filtering operator, and `expression` is the value the filtering
229
216
  # operator should be applied to. The `op_applicator` lambda, when called, will
230
217
  # return a Clause instance (defined in this module).
231
- bool_query = filter_operators.fetch(operator).call(field_path.from_root.join("."), expression)
218
+ bool_query = filter_node_interpreter.filter_operators.fetch(operator).call(field_path.from_root.join("."), expression)
232
219
  bool_query&.merge_into(bool_node)
233
220
  end
234
221
 
@@ -305,7 +292,7 @@ module ElasticGraph
305
292
  process_filter_hash(inner_node, expression, field_path)
306
293
  end
307
294
 
308
- bool_node[:filter] << sub_filter
295
+ bool_node[:filter] << sub_filter if sub_filter
309
296
  else
310
297
  process_filter_hash(bool_node, expression, field_path)
311
298
  end
@@ -367,146 +354,6 @@ module ElasticGraph
367
354
  end
368
355
  end
369
356
 
370
- def filter_operators
371
- @filter_operators ||= build_filter_operators(runtime_metadata)
372
- end
373
-
374
- def build_filter_operators(runtime_metadata)
375
- schema_names = runtime_metadata.schema_element_names
376
-
377
- filter_by_time_of_day_script_id = runtime_metadata
378
- .static_script_ids_by_scoped_name
379
- .fetch("filter/by_time_of_day")
380
-
381
- {
382
- schema_names.equal_to_any_of => ->(field_name, value) {
383
- values = to_datastore_value(value.compact.uniq) # : ::Array[untyped]
384
-
385
- equality_sub_expression =
386
- if field_name == "id"
387
- # Use specialized "ids" query when querying on ID field.
388
- # See: https://www.elastic.co/guide/en/elasticsearch/reference/7.15/query-dsl-ids-query.html
389
- #
390
- # We reject empty strings because we otherwise get an error from the datastore:
391
- # "failed to create query: Ids can't be empty"
392
- {ids: {values: values - [""]}}
393
- else
394
- {terms: {field_name => values}}
395
- end
396
-
397
- exists_sub_expression = {exists: {"field" => field_name}}
398
-
399
- if !value.empty? && value.all?(&:nil?)
400
- BooleanQuery.new(:must_not, [{bool: {filter: [exists_sub_expression]}}])
401
- elsif value.include?(nil)
402
- BooleanQuery.filter({bool: {
403
- minimum_should_match: 1,
404
- should: [
405
- {bool: {filter: [equality_sub_expression]}},
406
- {bool: {must_not: [{bool: {filter: [exists_sub_expression]}}]}}
407
- ]
408
- }})
409
- else
410
- BooleanQuery.filter(equality_sub_expression)
411
- end
412
- },
413
- schema_names.gt => ->(field_name, value) { RangeQuery.new(field_name, :gt, value) },
414
- schema_names.gte => ->(field_name, value) { RangeQuery.new(field_name, :gte, value) },
415
- schema_names.lt => ->(field_name, value) { RangeQuery.new(field_name, :lt, value) },
416
- schema_names.lte => ->(field_name, value) { RangeQuery.new(field_name, :lte, value) },
417
- schema_names.matches => ->(field_name, value) { BooleanQuery.must({match: {field_name => value}}) },
418
- schema_names.matches_query => ->(field_name, value) do
419
- allowed_edits_per_term = value.fetch(schema_names.allowed_edits_per_term).runtime_metadata.datastore_abbreviation
420
-
421
- BooleanQuery.must(
422
- {
423
- match: {
424
- field_name => {
425
- query: value.fetch(schema_names.query),
426
- # This is always a string field, even though the value is often an integer
427
- fuzziness: allowed_edits_per_term.to_s,
428
- operator: value[schema_names.require_all_terms] ? "AND" : "OR"
429
- }
430
- }
431
- }
432
- )
433
- end,
434
- schema_names.matches_phrase => ->(field_name, value) {
435
- BooleanQuery.must(
436
- {
437
- match_phrase_prefix: {
438
- field_name => {
439
- query: value.fetch(schema_names.phrase)
440
- }
441
- }
442
- }
443
- )
444
- },
445
-
446
- # This filter operator wraps a geo distance query:
447
- # https://www.elastic.co/guide/en/elasticsearch/reference/7.10/query-dsl-geo-distance-query.html
448
- schema_names.near => ->(field_name, value) do
449
- unit_abbreviation = value.fetch(schema_names.unit).runtime_metadata.datastore_abbreviation
450
-
451
- BooleanQuery.filter({geo_distance: {
452
- "distance" => "#{value.fetch(schema_names.max_distance)}#{unit_abbreviation}",
453
- field_name => {
454
- "lat" => value.fetch(schema_names.latitude),
455
- "lon" => value.fetch(schema_names.longitude)
456
- }
457
- }})
458
- end,
459
-
460
- schema_names.time_of_day => ->(field_name, value) do
461
- # To filter on time of day, we use the `filter/by_time_of_day` script. We accomplish
462
- # this with a script because Elasticsearch/OpenSearch do not support this natively, and it's
463
- # incredibly hard to implement correctly with respect to time zones without using a
464
- # script. We considered indexing the `time_of_day` as a separate index field
465
- # that we could directly filter on, but since we need the time of day to be relative
466
- # to a specific time zone, there's no way to make that work with the reality of
467
- # daylight savings time. For example, the `America/Los_Angeles` time zone has a -07:00
468
- # UTC offset for part of the year and a `America/Los_Angeles` -08:00 UTC offset for
469
- # part of the year. In a script we can use Java time zone APIs to handle this correctly.
470
- params = {
471
- field: field_name,
472
- equal_to_any_of: list_of_nanos_of_day_from(value, schema_names.equal_to_any_of),
473
- gt: nano_of_day_from(value, schema_names.gt),
474
- gte: nano_of_day_from(value, schema_names.gte),
475
- lt: nano_of_day_from(value, schema_names.lt),
476
- lte: nano_of_day_from(value, schema_names.lte),
477
- time_zone: value[schema_names.time_zone]
478
- }.compact
479
-
480
- # If there are no comparison operators, return `nil` instead of a `Clause` so that we avoid
481
- # invoking the script for no reason. Note that `field` and `time_zone` will always be in
482
- # `params` so we can't just check for an empty hash here.
483
- if (params.keys - [:field, :time_zone]).any?
484
- BooleanQuery.filter({script: {script: {id: filter_by_time_of_day_script_id, params: params}}})
485
- end
486
- end
487
- }.freeze
488
- end
489
-
490
- def to_datastore_value(value)
491
- case value
492
- when ::Array
493
- value.map { |v| to_datastore_value(v) }
494
- when Schema::EnumValue
495
- value.name.to_s
496
- else
497
- value
498
- end
499
- end
500
-
501
- def nano_of_day_from(value, field)
502
- local_time = value[field]
503
- Support::TimeUtil.nano_of_day_from_local_time(local_time) if local_time
504
- end
505
-
506
- def list_of_nanos_of_day_from(value, field)
507
- value[field]&.map { |t| Support::TimeUtil.nano_of_day_from_local_time(t) }
508
- end
509
-
510
357
  # Counts how many clauses in `bool_query` are required to match for a document to be a search hit.
511
358
  def required_matching_clause_count(bool_query)
512
359
  bool_query.reduce(0) do |count, (occurrence, clauses)|
@@ -0,0 +1,181 @@
1
+ # Copyright 2024 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/constants"
10
+ require "elastic_graph/graphql/filtering/boolean_query"
11
+ require "elastic_graph/graphql/filtering/range_query"
12
+ require "elastic_graph/graphql/schema/enum_value"
13
+ require "elastic_graph/support/memoizable_data"
14
+ require "elastic_graph/support/time_util"
15
+
16
+ module ElasticGraph
17
+ class GraphQL
18
+ module Filtering
19
+ # Responsible for interpreting a single `node` in a `filter` expression.
20
+ FilterNodeInterpreter = Support::MemoizableData.define(:runtime_metadata, :schema_names) do
21
+ # @implements FilterNodeInterpreter
22
+
23
+ def initialize(runtime_metadata:)
24
+ super(runtime_metadata: runtime_metadata, schema_names: runtime_metadata.schema_element_names)
25
+ end
26
+
27
+ def identify_node_type(field_or_op, sub_expression)
28
+ return :empty if sub_expression.nil? || sub_expression == {}
29
+ return :not if field_or_op == schema_names.not
30
+ return :list_any_filter if field_or_op == schema_names.any_satisfy
31
+ return :all_of if field_or_op == schema_names.all_of
32
+ return :any_of if field_or_op == schema_names.any_of
33
+ return :operator if filter_operators.key?(field_or_op)
34
+ return :list_count if field_or_op == LIST_COUNTS_FIELD
35
+ return :sub_field if sub_expression.is_a?(::Hash)
36
+ :unknown
37
+ end
38
+
39
+ def filter_operators
40
+ @filter_operators ||= build_filter_operators(runtime_metadata)
41
+ end
42
+
43
+ private
44
+
45
+ def build_filter_operators(runtime_metadata)
46
+ filter_by_time_of_day_script_id = runtime_metadata
47
+ .static_script_ids_by_scoped_name
48
+ .fetch("filter/by_time_of_day")
49
+
50
+ {
51
+ schema_names.equal_to_any_of => ->(field_name, value) {
52
+ values = to_datastore_value(value.compact.uniq) # : ::Array[untyped]
53
+
54
+ equality_sub_expression =
55
+ if field_name == "id"
56
+ # Use specialized "ids" query when querying on ID field.
57
+ # See: https://www.elastic.co/guide/en/elasticsearch/reference/7.15/query-dsl-ids-query.html
58
+ #
59
+ # We reject empty strings because we otherwise get an error from the datastore:
60
+ # "failed to create query: Ids can't be empty"
61
+ {ids: {values: values - [""]}}
62
+ else
63
+ {terms: {field_name => values}}
64
+ end
65
+
66
+ exists_sub_expression = {exists: {"field" => field_name}}
67
+
68
+ if !value.empty? && value.all?(&:nil?)
69
+ BooleanQuery.new(:must_not, [{bool: {filter: [exists_sub_expression]}}])
70
+ elsif value.include?(nil)
71
+ BooleanQuery.filter({bool: {
72
+ minimum_should_match: 1,
73
+ should: [
74
+ {bool: {filter: [equality_sub_expression]}},
75
+ {bool: {must_not: [{bool: {filter: [exists_sub_expression]}}]}}
76
+ ]
77
+ }})
78
+ else
79
+ BooleanQuery.filter(equality_sub_expression)
80
+ end
81
+ },
82
+ schema_names.gt => ->(field_name, value) { RangeQuery.new(field_name, :gt, value) },
83
+ schema_names.gte => ->(field_name, value) { RangeQuery.new(field_name, :gte, value) },
84
+ schema_names.lt => ->(field_name, value) { RangeQuery.new(field_name, :lt, value) },
85
+ schema_names.lte => ->(field_name, value) { RangeQuery.new(field_name, :lte, value) },
86
+ schema_names.matches => ->(field_name, value) { BooleanQuery.must({match: {field_name => value}}) },
87
+ schema_names.matches_query => ->(field_name, value) do
88
+ allowed_edits_per_term = value.fetch(schema_names.allowed_edits_per_term).runtime_metadata.datastore_abbreviation
89
+
90
+ BooleanQuery.must(
91
+ {
92
+ match: {
93
+ field_name => {
94
+ query: value.fetch(schema_names.query),
95
+ # This is always a string field, even though the value is often an integer
96
+ fuzziness: allowed_edits_per_term.to_s,
97
+ operator: value[schema_names.require_all_terms] ? "AND" : "OR"
98
+ }
99
+ }
100
+ }
101
+ )
102
+ end,
103
+ schema_names.matches_phrase => ->(field_name, value) {
104
+ BooleanQuery.must(
105
+ {
106
+ match_phrase_prefix: {
107
+ field_name => {
108
+ query: value.fetch(schema_names.phrase)
109
+ }
110
+ }
111
+ }
112
+ )
113
+ },
114
+
115
+ # This filter operator wraps a geo distance query:
116
+ # https://www.elastic.co/guide/en/elasticsearch/reference/7.10/query-dsl-geo-distance-query.html
117
+ schema_names.near => ->(field_name, value) do
118
+ unit_abbreviation = value.fetch(schema_names.unit).runtime_metadata.datastore_abbreviation
119
+
120
+ BooleanQuery.filter({geo_distance: {
121
+ "distance" => "#{value.fetch(schema_names.max_distance)}#{unit_abbreviation}",
122
+ field_name => {
123
+ "lat" => value.fetch(schema_names.latitude),
124
+ "lon" => value.fetch(schema_names.longitude)
125
+ }
126
+ }})
127
+ end,
128
+
129
+ schema_names.time_of_day => ->(field_name, value) do
130
+ # To filter on time of day, we use the `filter/by_time_of_day` script. We accomplish
131
+ # this with a script because Elasticsearch/OpenSearch do not support this natively, and it's
132
+ # incredibly hard to implement correctly with respect to time zones without using a
133
+ # script. We considered indexing the `time_of_day` as a separate index field
134
+ # that we could directly filter on, but since we need the time of day to be relative
135
+ # to a specific time zone, there's no way to make that work with the reality of
136
+ # daylight savings time. For example, the `America/Los_Angeles` time zone has a -07:00
137
+ # UTC offset for part of the year and a `America/Los_Angeles` -08:00 UTC offset for
138
+ # part of the year. In a script we can use Java time zone APIs to handle this correctly.
139
+ params = {
140
+ field: field_name,
141
+ equal_to_any_of: list_of_nanos_of_day_from(value, schema_names.equal_to_any_of),
142
+ gt: nano_of_day_from(value, schema_names.gt),
143
+ gte: nano_of_day_from(value, schema_names.gte),
144
+ lt: nano_of_day_from(value, schema_names.lt),
145
+ lte: nano_of_day_from(value, schema_names.lte),
146
+ time_zone: value[schema_names.time_zone]
147
+ }.compact
148
+
149
+ # If there are no comparison operators, return `nil` instead of a `Clause` so that we avoid
150
+ # invoking the script for no reason. Note that `field` and `time_zone` will always be in
151
+ # `params` so we can't just check for an empty hash here.
152
+ if (params.keys - [:field, :time_zone]).any?
153
+ BooleanQuery.filter({script: {script: {id: filter_by_time_of_day_script_id, params: params}}})
154
+ end
155
+ end
156
+ }.freeze
157
+ end
158
+
159
+ def to_datastore_value(value)
160
+ case value
161
+ when ::Array
162
+ value.map { |v| to_datastore_value(v) }
163
+ when Schema::EnumValue
164
+ value.name.to_s
165
+ else
166
+ value
167
+ end
168
+ end
169
+
170
+ def nano_of_day_from(value, field)
171
+ local_time = value[field]
172
+ Support::TimeUtil.nano_of_day_from_local_time(local_time) if local_time
173
+ end
174
+
175
+ def list_of_nanos_of_day_from(value, field)
176
+ value[field]&.map { |t| Support::TimeUtil.nano_of_day_from_local_time(t) }
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -222,7 +222,7 @@ module ElasticGraph
222
222
 
223
223
  # Steep weirdly expects them here...
224
224
  # @dynamic initialize, config, logger, runtime_metadata, graphql_schema_string, datastore_core, clock
225
- # @dynamic graphql_http_endpoint, graphql_query_executor, schema, datastore_search_router, filter_interpreter
225
+ # @dynamic graphql_http_endpoint, graphql_query_executor, schema, datastore_search_router, filter_interpreter, filter_node_interpreter
226
226
  # @dynamic datastore_query_builder, graphql_gem_plugins, graphql_resolvers, datastore_query_adapters, monotonic_clock
227
227
  # @dynamic load_dependencies_eagerly, self.from_parsed_yaml, filter_args_translator, sub_aggregation_grouping_adapter
228
228
  end
@@ -13,7 +13,7 @@ require "elastic_graph/support/memoizable_data"
13
13
  module ElasticGraph
14
14
  class GraphQL
15
15
  class QueryAdapter
16
- class Filters < Support::MemoizableData.define(:schema_element_names, :filter_args_translator)
16
+ class Filters < Support::MemoizableData.define(:schema_element_names, :filter_args_translator, :filter_node_interpreter)
17
17
  def call(field:, query:, args:, lookahead:, context:)
18
18
  filter_from_args = filter_args_translator.translate_filter_args(field: field, args: args)
19
19
  automatic_filter = build_automatic_filter(filter_from_args: filter_from_args, query: query)
@@ -69,28 +69,25 @@ module ElasticGraph
69
69
  #
70
70
  # - The field paths we are filtering on.
71
71
  # - The field paths that are sourced from `SELF_RELATIONSHIP_NAME`.
72
- def determine_paths_to_check(filter, index_fields_by_path, parent_path: "")
73
- filter.compact.flat_map do |field_name, value|
74
- path = parent_path + field_name
75
-
76
- if (index_field = index_fields_by_path[path])
77
- # We've recursed down to a field path. We want that path to be returned if the
78
- # field is sourced from SELF_RELATIONSHIP_NAME.
79
- (index_field.source == SELF_RELATIONSHIP_NAME) ? [path] : []
80
- elsif field_name == schema_element_names.any_of
81
- # `any_of` represents an OR and the value will be an array, so we have to flat map over it.
82
- value.flat_map do |sub_filter|
72
+ def determine_paths_to_check(expression, index_fields_by_path, parent_path: nil)
73
+ return [] unless expression.is_a?(::Hash)
74
+
75
+ expression.compact.flat_map do |field_or_op, sub_expression|
76
+ if filter_node_interpreter.identify_node_type(field_or_op, sub_expression) == :sub_field
77
+ path = parent_path ? "#{parent_path}.#{field_or_op}" : field_or_op
78
+ if (index_field = index_fields_by_path[path])
79
+ # We've recursed down to a leaf field path. We want that path to be returned if the
80
+ # field is sourced from SELF_RELATIONSHIP_NAME.
81
+ (index_field.source == SELF_RELATIONSHIP_NAME) ? [path] : []
82
+ else
83
+ determine_paths_to_check(sub_expression, index_fields_by_path, parent_path: path)
84
+ end
85
+ elsif sub_expression.is_a?(::Array)
86
+ sub_expression.flat_map do |sub_filter|
83
87
  determine_paths_to_check(sub_filter, index_fields_by_path, parent_path: parent_path)
84
88
  end
85
- elsif field_name == schema_element_names.not
86
- # While `not` represents negation, we don't have to negate anything here because the negation
87
- # is handled later (when we use `filter_value_set_extractor`). Here we are just determining the
88
- # paths to check. We want to recurse without adding `not` to the `parent_path` since it's not
89
- # part of the field path.
90
- determine_paths_to_check(value, index_fields_by_path, parent_path: parent_path)
91
89
  else
92
- # ...otherwise, `field_name` is a parent field and we need to recurse down through the children.
93
- determine_paths_to_check(value, index_fields_by_path, parent_path: "#{path}.")
90
+ determine_paths_to_check(sub_expression, index_fields_by_path, parent_path: parent_path)
94
91
  end
95
92
  end
96
93
  end
@@ -181,7 +181,11 @@ module ElasticGraph
181
181
 
182
182
  [
183
183
  GraphQL::QueryAdapter::Pagination.new(schema_element_names: schema_element_names),
184
- GraphQL::QueryAdapter::Filters.new(schema_element_names: schema_element_names, filter_args_translator: filter_args_translator),
184
+ GraphQL::QueryAdapter::Filters.new(
185
+ schema_element_names: schema_element_names,
186
+ filter_args_translator: filter_args_translator,
187
+ filter_node_interpreter: filter_node_interpreter
188
+ ),
185
189
  GraphQL::QueryAdapter::Sort.new(order_by_arg_name: schema_element_names.order_by),
186
190
  Aggregation::QueryAdapter.new(
187
191
  schema: schema,
@@ -199,7 +203,15 @@ module ElasticGraph
199
203
  def filter_interpreter
200
204
  @filter_interpreter ||= begin
201
205
  require "elastic_graph/graphql/filtering/filter_interpreter"
202
- Filtering::FilterInterpreter.new(runtime_metadata: runtime_metadata, logger: logger)
206
+ Filtering::FilterInterpreter.new(filter_node_interpreter: filter_node_interpreter, logger: logger)
207
+ end
208
+ end
209
+
210
+ # @private
211
+ def filter_node_interpreter
212
+ @filter_node_interpreter ||= begin
213
+ require "elastic_graph/graphql/filtering/filter_node_interpreter"
214
+ Filtering::FilterNodeInterpreter.new(runtime_metadata: runtime_metadata)
203
215
  end
204
216
  end
205
217
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticgraph-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0.2
4
+ version: 0.18.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Myron Marston
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-05 00:00:00.000000000 Z
11
+ date: 2024-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop-factory_bot
@@ -266,28 +266,28 @@ dependencies:
266
266
  requirements:
267
267
  - - '='
268
268
  - !ruby/object:Gem::Version
269
- version: 0.18.0.2
269
+ version: 0.18.0.4
270
270
  type: :runtime
271
271
  prerelease: false
272
272
  version_requirements: !ruby/object:Gem::Requirement
273
273
  requirements:
274
274
  - - '='
275
275
  - !ruby/object:Gem::Version
276
- version: 0.18.0.2
276
+ version: 0.18.0.4
277
277
  - !ruby/object:Gem::Dependency
278
278
  name: elasticgraph-schema_artifacts
279
279
  requirement: !ruby/object:Gem::Requirement
280
280
  requirements:
281
281
  - - '='
282
282
  - !ruby/object:Gem::Version
283
- version: 0.18.0.2
283
+ version: 0.18.0.4
284
284
  type: :runtime
285
285
  prerelease: false
286
286
  version_requirements: !ruby/object:Gem::Requirement
287
287
  requirements:
288
288
  - - '='
289
289
  - !ruby/object:Gem::Version
290
- version: 0.18.0.2
290
+ version: 0.18.0.4
291
291
  - !ruby/object:Gem::Dependency
292
292
  name: graphql
293
293
  requirement: !ruby/object:Gem::Requirement
@@ -308,70 +308,70 @@ dependencies:
308
308
  requirements:
309
309
  - - '='
310
310
  - !ruby/object:Gem::Version
311
- version: 0.18.0.2
311
+ version: 0.18.0.4
312
312
  type: :development
313
313
  prerelease: false
314
314
  version_requirements: !ruby/object:Gem::Requirement
315
315
  requirements:
316
316
  - - '='
317
317
  - !ruby/object:Gem::Version
318
- version: 0.18.0.2
318
+ version: 0.18.0.4
319
319
  - !ruby/object:Gem::Dependency
320
320
  name: elasticgraph-elasticsearch
321
321
  requirement: !ruby/object:Gem::Requirement
322
322
  requirements:
323
323
  - - '='
324
324
  - !ruby/object:Gem::Version
325
- version: 0.18.0.2
325
+ version: 0.18.0.4
326
326
  type: :development
327
327
  prerelease: false
328
328
  version_requirements: !ruby/object:Gem::Requirement
329
329
  requirements:
330
330
  - - '='
331
331
  - !ruby/object:Gem::Version
332
- version: 0.18.0.2
332
+ version: 0.18.0.4
333
333
  - !ruby/object:Gem::Dependency
334
334
  name: elasticgraph-opensearch
335
335
  requirement: !ruby/object:Gem::Requirement
336
336
  requirements:
337
337
  - - '='
338
338
  - !ruby/object:Gem::Version
339
- version: 0.18.0.2
339
+ version: 0.18.0.4
340
340
  type: :development
341
341
  prerelease: false
342
342
  version_requirements: !ruby/object:Gem::Requirement
343
343
  requirements:
344
344
  - - '='
345
345
  - !ruby/object:Gem::Version
346
- version: 0.18.0.2
346
+ version: 0.18.0.4
347
347
  - !ruby/object:Gem::Dependency
348
348
  name: elasticgraph-indexer
349
349
  requirement: !ruby/object:Gem::Requirement
350
350
  requirements:
351
351
  - - '='
352
352
  - !ruby/object:Gem::Version
353
- version: 0.18.0.2
353
+ version: 0.18.0.4
354
354
  type: :development
355
355
  prerelease: false
356
356
  version_requirements: !ruby/object:Gem::Requirement
357
357
  requirements:
358
358
  - - '='
359
359
  - !ruby/object:Gem::Version
360
- version: 0.18.0.2
360
+ version: 0.18.0.4
361
361
  - !ruby/object:Gem::Dependency
362
362
  name: elasticgraph-schema_definition
363
363
  requirement: !ruby/object:Gem::Requirement
364
364
  requirements:
365
365
  - - '='
366
366
  - !ruby/object:Gem::Version
367
- version: 0.18.0.2
367
+ version: 0.18.0.4
368
368
  type: :development
369
369
  prerelease: false
370
370
  version_requirements: !ruby/object:Gem::Requirement
371
371
  requirements:
372
372
  - - '='
373
373
  - !ruby/object:Gem::Version
374
- version: 0.18.0.2
374
+ version: 0.18.0.4
375
375
  description:
376
376
  email:
377
377
  - myron@squareup.com
@@ -418,6 +418,7 @@ files:
418
418
  - lib/elastic_graph/graphql/filtering/field_path.rb
419
419
  - lib/elastic_graph/graphql/filtering/filter_args_translator.rb
420
420
  - lib/elastic_graph/graphql/filtering/filter_interpreter.rb
421
+ - lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb
421
422
  - lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb
422
423
  - lib/elastic_graph/graphql/filtering/range_query.rb
423
424
  - lib/elastic_graph/graphql/http_endpoint.rb