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 +4 -4
- data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +16 -169
- data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +181 -0
- data/lib/elastic_graph/graphql/http_endpoint.rb +1 -1
- data/lib/elastic_graph/graphql/query_adapter/filters.rb +17 -20
- data/lib/elastic_graph/graphql.rb +14 -2
- metadata +17 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0cb73d5ec2940bb792cbcada0c615dd1415de774bb9111ffe09ae293cf2de38
|
4
|
+
data.tar.gz: fb0f23546ee15fd3c9f434518dde7ccb79df0838795392d6b0a8c9e60df01667
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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
|
-
#
|
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(:
|
23
|
+
FilterInterpreter = Support::MemoizableData.define(:filter_node_interpreter, :schema_names, :logger) do
|
27
24
|
# @implements FilterInterpreter
|
28
25
|
|
29
|
-
def initialize(
|
26
|
+
def initialize(filter_node_interpreter:, logger:)
|
30
27
|
super(
|
31
|
-
|
32
|
-
schema_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 `
|
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}
|
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
|
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
|
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
|
-
|
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(
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
if (
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
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(
|
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(
|
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|