elasticgraph-graphql 0.18.0.3 → 0.18.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a520631b7964685b34d5089eb26772a9754e955ac53ce7706d9a844a06c281c3
4
- data.tar.gz: 02e9144c3197c43a208c100fcc3a340301caf041fd12783b4cd08f37d3f28645
3
+ metadata.gz: 53e5c759fafb5fe181ae65912c42b21d6d6dbe8cbd3558962cb5a56bc94944b6
4
+ data.tar.gz: 862a18dd839b8ee841b91430eb7188612c5483c5701930cf23cfbb22ce44e876
5
5
  SHA512:
6
- metadata.gz: 9540df01b507f1b3bcdae32699c3cf830b900507953f70504515afd0c5c7bf7160fb13b78750a272da823e81795d247c56c47ba7bff0d977193dad744ed7e6ba
7
- data.tar.gz: 68e4cee0d55d42693641901bafc0e811a7f596b831d438ce9882ae259c0006ac12580726785c9fa2918e1abb0cb5b804da088f7bef4f83a4220958b07b73fbec
6
+ metadata.gz: 4739bc5c2978a75528777b58073ba9e606a5964ff1adcfbb1b19c77eb43e29c5bb19e5aa137d2b530bd2937ca5499538149959b23cf55a7912f9b04b50809e3f
7
+ data.tar.gz: fb6b7226810f2f58bd7aedca80a9e1be097ffbbc8dbb5532805f9d98392006e2c4f0db7344c64250c3bd6adcdb917972e230324cc876c0639bba3a40d0601067
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
 
11
11
  module ElasticGraph
12
12
  class GraphQL
@@ -38,7 +38,7 @@ module ElasticGraph
38
38
 
39
39
  private_class_method def self.verify_delimiters(str)
40
40
  if str.to_s.include?(DELIMITER)
41
- raise InvalidArgumentValueError, %("#{str}" contains delimiter: "#{DELIMITER}")
41
+ raise Errors::InvalidArgumentValueError, %("#{str}" contains delimiter: "#{DELIMITER}")
42
42
  end
43
43
  end
44
44
  end
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "elastic_graph/graphql/aggregation/field_path_encoder"
11
11
 
12
12
  module ElasticGraph
@@ -77,7 +77,7 @@ module ElasticGraph
77
77
  def self.verify_no_delimiter_in(*parts)
78
78
  parts.each do |part|
79
79
  if part.to_s.include?(DELIMITER)
80
- raise InvalidArgumentValueError, %("#{part}" contains delimiter: "#{DELIMITER}")
80
+ raise Errors::InvalidArgumentValueError, %("#{part}" contains delimiter: "#{DELIMITER}")
81
81
  end
82
82
  end
83
83
  end
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "elastic_graph/graphql/client"
11
11
  require "elastic_graph/schema_artifacts/runtime_metadata/extension_loader"
12
12
 
@@ -33,14 +33,14 @@ module ElasticGraph
33
33
  extra_keys = parsed_yaml.keys - EXPECTED_KEYS
34
34
 
35
35
  unless extra_keys.empty?
36
- raise ConfigError, "Unknown `graphql` config settings: #{extra_keys.join(", ")}"
36
+ raise Errors::ConfigError, "Unknown `graphql` config settings: #{extra_keys.join(", ")}"
37
37
  end
38
38
 
39
39
  extension_loader = SchemaArtifacts::RuntimeMetadata::ExtensionLoader.new(::Module.new)
40
40
  extension_mods = parsed_yaml.fetch("extension_modules", []).map do |mod_hash|
41
41
  extension_loader.load(mod_hash.fetch("extension_name"), from: mod_hash.fetch("require_path"), config: {}).extension_class.tap do |mod|
42
42
  unless mod.instance_of?(::Module)
43
- raise ConfigError, "`#{mod_hash.fetch("extension_name")}` is not a module, but all application extension modules must be modules."
43
+ raise Errors::ConfigError, "`#{mod_hash.fetch("extension_name")}` is not a module, but all application extension modules must be modules."
44
44
  end
45
45
  end
46
46
  end
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "graphql"
11
11
 
12
12
  module ElasticGraph
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "elastic_graph/support/memoizable_data"
11
11
 
12
12
  module ElasticGraph
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "elastic_graph/graphql/aggregation/query"
11
11
  require "elastic_graph/graphql/aggregation/query_optimizer"
12
12
  require "elastic_graph/graphql/decoded_cursor"
@@ -65,7 +65,7 @@ module ElasticGraph
65
65
  )
66
66
 
67
67
  if search_index_definitions.empty?
68
- raise SearchFailedError, "Query is invalid, since it contains no `search_index_definitions`."
68
+ raise Errors::SearchFailedError, "Query is invalid, since it contains no `search_index_definitions`."
69
69
  end
70
70
  end
71
71
  }
@@ -121,7 +121,7 @@ module ElasticGraph
121
121
  missing_queries = expected_queries - actual_queries
122
122
  extra_queries = actual_queries - expected_queries
123
123
 
124
- raise SearchFailedError, "The `responses_hash` does not have the expected set of queries as keys. " \
124
+ raise Errors::SearchFailedError, "The `responses_hash` does not have the expected set of queries as keys. " \
125
125
  "This can cause problems for the `GraphQL::Dataloader` and suggests a bug in the logic that should be fixed.\n\n" \
126
126
  "Missing queries (#{missing_queries.size}):\n#{missing_queries.map(&:inspect).join("\n")}.\n\n" \
127
127
  "Extra queries (#{extra_queries.size}): #{extra_queries.map(&:inspect).join("\n")}"
@@ -133,7 +133,7 @@ module ElasticGraph
133
133
  # Both query objects are left unchanged.
134
134
  def merge(other_query)
135
135
  if search_index_definitions != other_query.search_index_definitions
136
- raise ElasticGraph::InvalidMergeError, "`search_index_definitions` conflict while merging between " \
136
+ raise ElasticGraph::Errors::InvalidMergeError, "`search_index_definitions` conflict while merging between " \
137
137
  "#{search_index_definitions} and #{other_query.search_index_definitions}"
138
138
  end
139
139
 
@@ -177,11 +177,11 @@ module ElasticGraph
177
177
  end
178
178
 
179
179
  # Returns the name of the datastore cluster as a String where this query should be setn.
180
- # Unless exactly 1 cluster name is found, this method raises a ConfigError.
180
+ # Unless exactly 1 cluster name is found, this method raises a Errors::ConfigError.
181
181
  def cluster_name
182
182
  cluster_name = search_index_definitions.map(&:cluster_to_query).uniq
183
183
  return cluster_name.first if cluster_name.size == 1
184
- raise ConfigError, "Found different datastore clusters (#{cluster_name}) to query " \
184
+ raise Errors::ConfigError, "Found different datastore clusters (#{cluster_name}) to query " \
185
185
  "for query targeting indices: #{search_index_definitions}"
186
186
  end
187
187
 
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "elastic_graph/graphql/decoded_cursor"
11
11
  require "elastic_graph/graphql/datastore_response/document"
12
12
  require "forwardable"
@@ -66,7 +66,7 @@ module ElasticGraph
66
66
  end
67
67
 
68
68
  def total_document_count
69
- super || raise(CountUnavailableError, "#{__method__} is unavailable; set `query.total_document_count_needed = true` to make it available")
69
+ super || raise(Errors::CountUnavailableError, "#{__method__} is unavailable; set `query.total_document_count_needed = true` to make it available")
70
70
  end
71
71
 
72
72
  def to_s
@@ -7,7 +7,7 @@
7
7
  # frozen_string_literal: true
8
8
 
9
9
  require "elastic_graph/constants"
10
- require "elastic_graph/error"
10
+ require "elastic_graph/errors"
11
11
  require "elastic_graph/graphql/datastore_response/search_response"
12
12
  require "elastic_graph/graphql/query_details_tracker"
13
13
  require "elastic_graph/support/threading"
@@ -108,7 +108,7 @@ module ElasticGraph
108
108
 
109
109
  (min_query_deadline - @monotonic_clock.now_in_ms).tap do |timeout|
110
110
  if timeout <= 0
111
- raise RequestExceededDeadlineError, "It is already #{timeout.abs} ms past the search deadline."
111
+ raise Errors::RequestExceededDeadlineError, "It is already #{timeout.abs} ms past the search deadline."
112
112
  end
113
113
  end
114
114
  end
@@ -127,7 +127,7 @@ module ElasticGraph
127
127
  ERROR
128
128
  end.join("\n\n")
129
129
 
130
- raise SearchFailedError, "Got #{failures.size} search failure(s):\n\n#{formatted_failures}"
130
+ raise Errors::SearchFailedError, "Got #{failures.size} search failure(s):\n\n#{formatted_failures}"
131
131
  end
132
132
 
133
133
  # Examine successful query responses and log any shard failure they encounter
@@ -8,7 +8,7 @@
8
8
 
9
9
  require "base64"
10
10
  require "elastic_graph/constants"
11
- require "elastic_graph/error"
11
+ require "elastic_graph/errors"
12
12
  require "elastic_graph/support/memoizable_data"
13
13
  require "json"
14
14
 
@@ -39,17 +39,17 @@ module ElasticGraph
39
39
  # Tries to decode the given string cursor, returning `nil` if it is invalid.
40
40
  def self.try_decode(string)
41
41
  decode!(string)
42
- rescue InvalidCursorError
42
+ rescue Errors::InvalidCursorError
43
43
  nil
44
44
  end
45
45
 
46
- # Tries to decode the given string cursor, raising an `InvalidCursorError` if it's invalid.
46
+ # Tries to decode the given string cursor, raising an `Errors::InvalidCursorError` if it's invalid.
47
47
  def self.decode!(string)
48
48
  return SINGLETON if string == SINGLETON_CURSOR
49
49
  json = ::Base64.urlsafe_decode64(string)
50
50
  new(::JSON.parse(json))
51
51
  rescue ::ArgumentError, ::JSON::ParserError
52
- raise InvalidCursorError, "`#{string}` is an invalid cursor."
52
+ raise Errors::InvalidCursorError, "`#{string}` is an invalid cursor."
53
53
  end
54
54
 
55
55
  # Encodes the cursor to a string using JSON and Base64 encoding.
@@ -79,7 +79,7 @@ module ElasticGraph
79
79
  def self.from_sort_list(sort_list)
80
80
  sort_fields = sort_list.map do |hash|
81
81
  if hash.values.any? { |v| !v.is_a?(::Hash) } || hash.values.flat_map(&:keys) != ["order"]
82
- raise InvalidSortFieldsError,
82
+ raise Errors::InvalidSortFieldsError,
83
83
  "Given `sort_list` contained an invalid entry. Each must be a flat hash with one entry. Got: #{sort_list.inspect}"
84
84
  end
85
85
 
@@ -89,7 +89,7 @@ module ElasticGraph
89
89
  end
90
90
 
91
91
  if sort_fields.uniq.size < sort_fields.size
92
- raise InvalidSortFieldsError,
92
+ raise Errors::InvalidSortFieldsError,
93
93
  "Given `sort_list` contains a duplicate field, which the CursorEncoder cannot handler. " \
94
94
  "The caller is responsible for de-duplicating the sort list fist. Got: #{sort_list.inspect}"
95
95
  end
@@ -99,7 +99,7 @@ module ElasticGraph
99
99
 
100
100
  def build(sort_values)
101
101
  unless sort_values.size == sort_fields.size
102
- raise CursorEncodingError,
102
+ raise Errors::CursorEncodingError,
103
103
  "size of sort values (#{sort_values.inspect}) does not match the " \
104
104
  "size of sort fields (#{sort_fields.inspect})"
105
105
  end
@@ -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
@@ -230,7 +215,7 @@ module ElasticGraph
230
215
  # `operator` is a filtering operator, and `expression` is the value the filtering
231
216
  # operator should be applied to. The `op_applicator` lambda, when called, will
232
217
  # return a Clause instance (defined in this module).
233
- 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)
234
219
  bool_query&.merge_into(bool_node)
235
220
  end
236
221
 
@@ -369,146 +354,6 @@ module ElasticGraph
369
354
  end
370
355
  end
371
356
 
372
- def filter_operators
373
- @filter_operators ||= build_filter_operators(runtime_metadata)
374
- end
375
-
376
- def build_filter_operators(runtime_metadata)
377
- schema_names = runtime_metadata.schema_element_names
378
-
379
- filter_by_time_of_day_script_id = runtime_metadata
380
- .static_script_ids_by_scoped_name
381
- .fetch("filter/by_time_of_day")
382
-
383
- {
384
- schema_names.equal_to_any_of => ->(field_name, value) {
385
- values = to_datastore_value(value.compact.uniq) # : ::Array[untyped]
386
-
387
- equality_sub_expression =
388
- if field_name == "id"
389
- # Use specialized "ids" query when querying on ID field.
390
- # See: https://www.elastic.co/guide/en/elasticsearch/reference/7.15/query-dsl-ids-query.html
391
- #
392
- # We reject empty strings because we otherwise get an error from the datastore:
393
- # "failed to create query: Ids can't be empty"
394
- {ids: {values: values - [""]}}
395
- else
396
- {terms: {field_name => values}}
397
- end
398
-
399
- exists_sub_expression = {exists: {"field" => field_name}}
400
-
401
- if !value.empty? && value.all?(&:nil?)
402
- BooleanQuery.new(:must_not, [{bool: {filter: [exists_sub_expression]}}])
403
- elsif value.include?(nil)
404
- BooleanQuery.filter({bool: {
405
- minimum_should_match: 1,
406
- should: [
407
- {bool: {filter: [equality_sub_expression]}},
408
- {bool: {must_not: [{bool: {filter: [exists_sub_expression]}}]}}
409
- ]
410
- }})
411
- else
412
- BooleanQuery.filter(equality_sub_expression)
413
- end
414
- },
415
- schema_names.gt => ->(field_name, value) { RangeQuery.new(field_name, :gt, value) },
416
- schema_names.gte => ->(field_name, value) { RangeQuery.new(field_name, :gte, value) },
417
- schema_names.lt => ->(field_name, value) { RangeQuery.new(field_name, :lt, value) },
418
- schema_names.lte => ->(field_name, value) { RangeQuery.new(field_name, :lte, value) },
419
- schema_names.matches => ->(field_name, value) { BooleanQuery.must({match: {field_name => value}}) },
420
- schema_names.matches_query => ->(field_name, value) do
421
- allowed_edits_per_term = value.fetch(schema_names.allowed_edits_per_term).runtime_metadata.datastore_abbreviation
422
-
423
- BooleanQuery.must(
424
- {
425
- match: {
426
- field_name => {
427
- query: value.fetch(schema_names.query),
428
- # This is always a string field, even though the value is often an integer
429
- fuzziness: allowed_edits_per_term.to_s,
430
- operator: value[schema_names.require_all_terms] ? "AND" : "OR"
431
- }
432
- }
433
- }
434
- )
435
- end,
436
- schema_names.matches_phrase => ->(field_name, value) {
437
- BooleanQuery.must(
438
- {
439
- match_phrase_prefix: {
440
- field_name => {
441
- query: value.fetch(schema_names.phrase)
442
- }
443
- }
444
- }
445
- )
446
- },
447
-
448
- # This filter operator wraps a geo distance query:
449
- # https://www.elastic.co/guide/en/elasticsearch/reference/7.10/query-dsl-geo-distance-query.html
450
- schema_names.near => ->(field_name, value) do
451
- unit_abbreviation = value.fetch(schema_names.unit).runtime_metadata.datastore_abbreviation
452
-
453
- BooleanQuery.filter({geo_distance: {
454
- "distance" => "#{value.fetch(schema_names.max_distance)}#{unit_abbreviation}",
455
- field_name => {
456
- "lat" => value.fetch(schema_names.latitude),
457
- "lon" => value.fetch(schema_names.longitude)
458
- }
459
- }})
460
- end,
461
-
462
- schema_names.time_of_day => ->(field_name, value) do
463
- # To filter on time of day, we use the `filter/by_time_of_day` script. We accomplish
464
- # this with a script because Elasticsearch/OpenSearch do not support this natively, and it's
465
- # incredibly hard to implement correctly with respect to time zones without using a
466
- # script. We considered indexing the `time_of_day` as a separate index field
467
- # that we could directly filter on, but since we need the time of day to be relative
468
- # to a specific time zone, there's no way to make that work with the reality of
469
- # daylight savings time. For example, the `America/Los_Angeles` time zone has a -07:00
470
- # UTC offset for part of the year and a `America/Los_Angeles` -08:00 UTC offset for
471
- # part of the year. In a script we can use Java time zone APIs to handle this correctly.
472
- params = {
473
- field: field_name,
474
- equal_to_any_of: list_of_nanos_of_day_from(value, schema_names.equal_to_any_of),
475
- gt: nano_of_day_from(value, schema_names.gt),
476
- gte: nano_of_day_from(value, schema_names.gte),
477
- lt: nano_of_day_from(value, schema_names.lt),
478
- lte: nano_of_day_from(value, schema_names.lte),
479
- time_zone: value[schema_names.time_zone]
480
- }.compact
481
-
482
- # If there are no comparison operators, return `nil` instead of a `Clause` so that we avoid
483
- # invoking the script for no reason. Note that `field` and `time_zone` will always be in
484
- # `params` so we can't just check for an empty hash here.
485
- if (params.keys - [:field, :time_zone]).any?
486
- BooleanQuery.filter({script: {script: {id: filter_by_time_of_day_script_id, params: params}}})
487
- end
488
- end
489
- }.freeze
490
- end
491
-
492
- def to_datastore_value(value)
493
- case value
494
- when ::Array
495
- value.map { |v| to_datastore_value(v) }
496
- when Schema::EnumValue
497
- value.name.to_s
498
- else
499
- value
500
- end
501
- end
502
-
503
- def nano_of_day_from(value, field)
504
- local_time = value[field]
505
- Support::TimeUtil.nano_of_day_from_local_time(local_time) if local_time
506
- end
507
-
508
- def list_of_nanos_of_day_from(value, field)
509
- value[field]&.map { |t| Support::TimeUtil.nano_of_day_from_local_time(t) }
510
- end
511
-
512
357
  # Counts how many clauses in `bool_query` are required to match for a document to be a search hit.
513
358
  def required_matching_clause_count(bool_query)
514
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
@@ -62,7 +62,7 @@ module ElasticGraph
62
62
 
63
63
  HTTPResponse.json(200, result.to_h)
64
64
  end
65
- rescue RequestExceededDeadlineError
65
+ rescue Errors::RequestExceededDeadlineError
66
66
  HTTPResponse.error(504, "Search exceeded requested timeout.")
67
67
  end
68
68
 
@@ -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
@@ -29,7 +29,7 @@ module ElasticGraph
29
29
  # Executes the given `query_string` using the provided `variables`.
30
30
  #
31
31
  # `timeout_in_ms` can be provided to limit how long the query runs for. If the timeout
32
- # is exceeded, `RequestExceededDeadlineError` will be raised. Note that `timeout_in_ms`
32
+ # is exceeded, `Errors::RequestExceededDeadlineError` will be raised. Note that `timeout_in_ms`
33
33
  # does not provide an absolute guarantee that the query will take no longer than the
34
34
  # provided value; it is only used to halt datastore queries. In process computation
35
35
  # can make the total query time exceeded the specified timeout.
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "elastic_graph/support/memoizable_data"
11
11
 
12
12
  module ElasticGraph
@@ -48,7 +48,7 @@ module ElasticGraph
48
48
 
49
49
  def canonical_name_for(name, element_type)
50
50
  schema_element_names.canonical_name_for(name) ||
51
- raise(SchemaError, "#{element_type} `#{name}` is not a defined schema element")
51
+ raise(Errors::SchemaError, "#{element_type} `#{name}` is not a defined schema element")
52
52
  end
53
53
  end
54
54
  end
@@ -58,7 +58,7 @@ module ElasticGraph
58
58
  # extra memory allocation and GC for the hash.
59
59
  arg_defn = arg_defns.find do |a|
60
60
  a.keyword == key
61
- end || raise(SchemaError, "Cannot find an argument definition for #{key.inspect} on `#{args_owner.name}`")
61
+ end || raise(Errors::SchemaError, "Cannot find an argument definition for #{key.inspect} on `#{args_owner.name}`")
62
62
 
63
63
  next_owner = arg_defn.type.unwrap
64
64
  accumulator[arg_defn.name] = to_schema_form(value, next_owner)
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
 
11
11
  module ElasticGraph
12
12
  class GraphQL
@@ -15,7 +15,7 @@ module ElasticGraph
15
15
  class EnumValue < ::Data.define(:name, :type, :runtime_metadata)
16
16
  def sort_clauses
17
17
  sort_clause = runtime_metadata&.sort_field&.then { |sf| {sf.field_path => {"order" => sf.direction.to_s}} } ||
18
- raise(SchemaError, "Runtime metadata provides no `sort_field` for #{type.name}.#{name} enum value.")
18
+ raise(Errors::SchemaError, "Runtime metadata provides no `sort_field` for #{type.name}.#{name} enum value.")
19
19
 
20
20
  [sort_clause]
21
21
  end
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/error"
9
+ require "elastic_graph/errors"
10
10
  require "elastic_graph/graphql/schema/relation_join"
11
11
  require "elastic_graph/graphql/schema/arguments"
12
12
 
@@ -126,7 +126,7 @@ module ElasticGraph
126
126
  def sort_argument_type
127
127
  @sort_argument_type ||= begin
128
128
  graphql_argument = @graphql_field.arguments.fetch(schema_element_names.order_by) do
129
- raise SchemaError, "`#{schema_element_names.order_by}` argument not defined for field `#{parent_type.name}.#{name}`."
129
+ raise Errors::SchemaError, "`#{schema_element_names.order_by}` argument not defined for field `#{parent_type.name}.#{name}`."
130
130
  end
131
131
  @schema.type_from(graphql_argument.type.unwrap)
132
132
  end
@@ -7,7 +7,7 @@
7
7
  # frozen_string_literal: true
8
8
 
9
9
  require "elastic_graph/datastore_core/index_definition"
10
- require "elastic_graph/error"
10
+ require "elastic_graph/errors"
11
11
  require "elastic_graph/graphql/schema/field"
12
12
  require "elastic_graph/graphql/schema/enum_value"
13
13
  require "forwardable"
@@ -118,7 +118,7 @@ module ElasticGraph
118
118
  rescue KeyError => e
119
119
  msg = "No field named #{field_name} (on type #{name}) could be found"
120
120
  msg += "; Possible alternatives: [#{e.corrections.join(", ").delete('"')}]." if e.corrections.any?
121
- raise NotFoundError, msg
121
+ raise Errors::NotFoundError, msg
122
122
  end
123
123
 
124
124
  def enum_value_named(enum_value_name)
@@ -233,7 +233,7 @@ module ElasticGraph
233
233
  rescue KeyError => e
234
234
  msg = "No enum value named #{enum_value_name} (on type #{name}) could be found"
235
235
  msg += "; Possible alternatives: [#{e.corrections.join(", ").delete('"')}]." if e.corrections.any?
236
- raise NotFoundError, msg
236
+ raise Errors::NotFoundError, msg
237
237
  end
238
238
 
239
239
  def build_fields_by_name_hash(schema, graphql_type)
@@ -10,7 +10,7 @@ require "digest/md5"
10
10
  require "forwardable"
11
11
  require "graphql"
12
12
  require "elastic_graph/constants"
13
- require "elastic_graph/error"
13
+ require "elastic_graph/errors"
14
14
  require "elastic_graph/graphql/monkey_patches/schema_field"
15
15
  require "elastic_graph/graphql/monkey_patches/schema_object"
16
16
  require "elastic_graph/graphql/schema/field"
@@ -88,7 +88,7 @@ module ElasticGraph
88
88
  if index_definition_name.include?(ROLLOVER_INDEX_INFIX_MARKER)
89
89
  raise ArgumentError, "`#{index_definition_name}` is the name of a rollover index; pass the name of the parent index definition instead."
90
90
  else
91
- raise NotFoundError, "The index definition `#{index_definition_name}` does not appear to exist. Is it misspelled?"
91
+ raise Errors::NotFoundError, "The index definition `#{index_definition_name}` does not appear to exist. Is it misspelled?"
92
92
  end
93
93
  end
94
94
  end
@@ -133,7 +133,7 @@ module ElasticGraph
133
133
  rescue KeyError => e
134
134
  msg = "No type named #{type_name} could be found"
135
135
  msg += "; Possible alternatives: [#{e.corrections.join(", ").delete('"')}]." if e.corrections.any?
136
- raise NotFoundError, msg
136
+ raise Errors::NotFoundError, msg
137
137
  end
138
138
 
139
139
  def resolver
@@ -152,7 +152,7 @@ module ElasticGraph
152
152
  @indexed_document_types_by_index_definition_name ||= indexed_document_types.each_with_object({}) do |type, hash|
153
153
  type.index_definitions.each do |index_def|
154
154
  if hash.key?(index_def.name)
155
- raise SchemaError, "DatastoreCore::IndexDefinition #{index_def.name} is used multiple times: #{type} vs #{hash[index_def.name]}"
155
+ raise Errors::SchemaError, "DatastoreCore::IndexDefinition #{index_def.name} is used multiple times: #{type} vs #{hash[index_def.name]}"
156
156
  end
157
157
 
158
158
  hash[index_def.name] = type
@@ -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,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticgraph-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0.3
4
+ version: 0.18.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Myron Marston
8
+ - Ben VandenBos
9
+ - Square Engineering
8
10
  autorequire:
9
11
  bindir: exe
10
12
  cert_chain: []
11
- date: 2024-09-05 00:00:00.000000000 Z
13
+ date: 2024-09-20 00:00:00.000000000 Z
12
14
  dependencies:
13
15
  - !ruby/object:Gem::Dependency
14
16
  name: rubocop-factory_bot
@@ -266,28 +268,28 @@ dependencies:
266
268
  requirements:
267
269
  - - '='
268
270
  - !ruby/object:Gem::Version
269
- version: 0.18.0.3
271
+ version: 0.18.0.5
270
272
  type: :runtime
271
273
  prerelease: false
272
274
  version_requirements: !ruby/object:Gem::Requirement
273
275
  requirements:
274
276
  - - '='
275
277
  - !ruby/object:Gem::Version
276
- version: 0.18.0.3
278
+ version: 0.18.0.5
277
279
  - !ruby/object:Gem::Dependency
278
280
  name: elasticgraph-schema_artifacts
279
281
  requirement: !ruby/object:Gem::Requirement
280
282
  requirements:
281
283
  - - '='
282
284
  - !ruby/object:Gem::Version
283
- version: 0.18.0.3
285
+ version: 0.18.0.5
284
286
  type: :runtime
285
287
  prerelease: false
286
288
  version_requirements: !ruby/object:Gem::Requirement
287
289
  requirements:
288
290
  - - '='
289
291
  - !ruby/object:Gem::Version
290
- version: 0.18.0.3
292
+ version: 0.18.0.5
291
293
  - !ruby/object:Gem::Dependency
292
294
  name: graphql
293
295
  requirement: !ruby/object:Gem::Requirement
@@ -308,70 +310,70 @@ dependencies:
308
310
  requirements:
309
311
  - - '='
310
312
  - !ruby/object:Gem::Version
311
- version: 0.18.0.3
313
+ version: 0.18.0.5
312
314
  type: :development
313
315
  prerelease: false
314
316
  version_requirements: !ruby/object:Gem::Requirement
315
317
  requirements:
316
318
  - - '='
317
319
  - !ruby/object:Gem::Version
318
- version: 0.18.0.3
320
+ version: 0.18.0.5
319
321
  - !ruby/object:Gem::Dependency
320
322
  name: elasticgraph-elasticsearch
321
323
  requirement: !ruby/object:Gem::Requirement
322
324
  requirements:
323
325
  - - '='
324
326
  - !ruby/object:Gem::Version
325
- version: 0.18.0.3
327
+ version: 0.18.0.5
326
328
  type: :development
327
329
  prerelease: false
328
330
  version_requirements: !ruby/object:Gem::Requirement
329
331
  requirements:
330
332
  - - '='
331
333
  - !ruby/object:Gem::Version
332
- version: 0.18.0.3
334
+ version: 0.18.0.5
333
335
  - !ruby/object:Gem::Dependency
334
336
  name: elasticgraph-opensearch
335
337
  requirement: !ruby/object:Gem::Requirement
336
338
  requirements:
337
339
  - - '='
338
340
  - !ruby/object:Gem::Version
339
- version: 0.18.0.3
341
+ version: 0.18.0.5
340
342
  type: :development
341
343
  prerelease: false
342
344
  version_requirements: !ruby/object:Gem::Requirement
343
345
  requirements:
344
346
  - - '='
345
347
  - !ruby/object:Gem::Version
346
- version: 0.18.0.3
348
+ version: 0.18.0.5
347
349
  - !ruby/object:Gem::Dependency
348
350
  name: elasticgraph-indexer
349
351
  requirement: !ruby/object:Gem::Requirement
350
352
  requirements:
351
353
  - - '='
352
354
  - !ruby/object:Gem::Version
353
- version: 0.18.0.3
355
+ version: 0.18.0.5
354
356
  type: :development
355
357
  prerelease: false
356
358
  version_requirements: !ruby/object:Gem::Requirement
357
359
  requirements:
358
360
  - - '='
359
361
  - !ruby/object:Gem::Version
360
- version: 0.18.0.3
362
+ version: 0.18.0.5
361
363
  - !ruby/object:Gem::Dependency
362
364
  name: elasticgraph-schema_definition
363
365
  requirement: !ruby/object:Gem::Requirement
364
366
  requirements:
365
367
  - - '='
366
368
  - !ruby/object:Gem::Version
367
- version: 0.18.0.3
369
+ version: 0.18.0.5
368
370
  type: :development
369
371
  prerelease: false
370
372
  version_requirements: !ruby/object:Gem::Requirement
371
373
  requirements:
372
374
  - - '='
373
375
  - !ruby/object:Gem::Version
374
- version: 0.18.0.3
376
+ version: 0.18.0.5
375
377
  description:
376
378
  email:
377
379
  - myron@squareup.com
@@ -418,6 +420,7 @@ files:
418
420
  - lib/elastic_graph/graphql/filtering/field_path.rb
419
421
  - lib/elastic_graph/graphql/filtering/filter_args_translator.rb
420
422
  - lib/elastic_graph/graphql/filtering/filter_interpreter.rb
423
+ - lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb
421
424
  - lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb
422
425
  - lib/elastic_graph/graphql/filtering/range_query.rb
423
426
  - lib/elastic_graph/graphql/http_endpoint.rb