elasticgraph-graphql 0.18.0.3 → 0.18.0.5

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: 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