elasticgraph-graphql 0.18.0.3 → 0.18.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +12 -167
- 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
|
@@ -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
|
@@ -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
|