elasticgraph-graphql 0.19.1.1 → 0.19.2.1

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +1 -1
  4. data/lib/elastic_graph/graphql/aggregation/computation.rb +1 -1
  5. data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +1 -1
  6. data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +1 -1
  7. data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +1 -1
  8. data/lib/elastic_graph/graphql/aggregation/key.rb +1 -1
  9. data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +1 -1
  10. data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +2 -2
  11. data/lib/elastic_graph/graphql/aggregation/path_segment.rb +2 -2
  12. data/lib/elastic_graph/graphql/aggregation/query.rb +1 -1
  13. data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +33 -6
  14. data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +1 -1
  15. data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +2 -6
  16. data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +1 -1
  17. data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +26 -6
  18. data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +1 -1
  19. data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +5 -6
  20. data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +10 -8
  21. data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +1 -1
  22. data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +2 -2
  23. data/lib/elastic_graph/graphql/client.rb +1 -1
  24. data/lib/elastic_graph/graphql/config.rb +21 -6
  25. data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +10 -5
  26. data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +2 -3
  27. data/lib/elastic_graph/graphql/datastore_query/paginator.rb +1 -1
  28. data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +2 -3
  29. data/lib/elastic_graph/graphql/datastore_query.rb +66 -74
  30. data/lib/elastic_graph/graphql/datastore_response/document.rb +1 -1
  31. data/lib/elastic_graph/graphql/datastore_response/search_response.rb +83 -9
  32. data/lib/elastic_graph/graphql/datastore_search_router.rb +19 -4
  33. data/lib/elastic_graph/graphql/decoded_cursor.rb +1 -1
  34. data/lib/elastic_graph/graphql/filtering/boolean_query.rb +1 -1
  35. data/lib/elastic_graph/graphql/filtering/field_path.rb +1 -1
  36. data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +2 -2
  37. data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +10 -5
  38. data/lib/elastic_graph/graphql/filtering/filter_node_interpreter.rb +2 -2
  39. data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +17 -2
  40. data/lib/elastic_graph/graphql/filtering/range_query.rb +1 -1
  41. data/lib/elastic_graph/graphql/http_endpoint.rb +2 -2
  42. data/lib/elastic_graph/graphql/query_adapter/filters.rb +1 -1
  43. data/lib/elastic_graph/graphql/query_adapter/pagination.rb +1 -1
  44. data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +18 -3
  45. data/lib/elastic_graph/graphql/query_adapter/sort.rb +1 -1
  46. data/lib/elastic_graph/graphql/query_details_tracker.rb +11 -14
  47. data/lib/elastic_graph/graphql/query_executor.rb +10 -16
  48. data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +6 -12
  49. data/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb +126 -0
  50. data/lib/elastic_graph/graphql/resolvers/list_records.rb +4 -4
  51. data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +57 -27
  52. data/lib/elastic_graph/graphql/resolvers/nested_relationships_source.rb +325 -0
  53. data/lib/elastic_graph/graphql/resolvers/object.rb +36 -0
  54. data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +2 -2
  55. data/lib/elastic_graph/graphql/resolvers/query_source.rb +6 -3
  56. data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +1 -1
  57. data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +1 -1
  58. data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +1 -1
  59. data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +1 -1
  60. data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +2 -2
  61. data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +2 -7
  62. data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +1 -1
  63. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +1 -1
  64. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +1 -1
  65. data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +1 -1
  66. data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +1 -1
  67. data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +1 -1
  68. data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +1 -1
  69. data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +1 -1
  70. data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +1 -1
  71. data/lib/elastic_graph/graphql/schema/arguments.rb +1 -1
  72. data/lib/elastic_graph/graphql/schema/enum_value.rb +1 -1
  73. data/lib/elastic_graph/graphql/schema/field.rb +12 -27
  74. data/lib/elastic_graph/graphql/schema/relation_join.rb +17 -9
  75. data/lib/elastic_graph/graphql/schema/type.rb +19 -8
  76. data/lib/elastic_graph/graphql/schema.rb +83 -29
  77. data/lib/elastic_graph/graphql.rb +56 -43
  78. data/script/dump_time_zones +1 -1
  79. metadata +59 -29
  80. data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +0 -56
  81. data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +0 -48
  82. data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +0 -114
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -6,31 +6,27 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
- require "elastic_graph/graphql/client"
10
- require "elastic_graph/support/hash_util"
11
- require "graphql"
12
-
13
9
  module ElasticGraph
14
10
  class GraphQL
15
11
  # Class used to track details of what happens during a single GraphQL query for the purposes of logging.
16
12
  # Here we use `Struct` instead of `Data` specifically because it is designed to be mutable.
17
13
  class QueryDetailsTracker < Struct.new(
18
- :hidden_types,
19
14
  :shard_routing_values,
20
15
  :search_index_expressions,
21
16
  :query_counts_per_datastore_request,
22
17
  :datastore_query_server_duration_ms,
23
18
  :datastore_query_client_duration_ms,
19
+ :queried_shard_count,
24
20
  :mutex
25
21
  )
26
22
  def self.empty
27
23
  new(
28
- hidden_types: ::Set.new,
29
24
  shard_routing_values: ::Set.new,
30
25
  search_index_expressions: ::Set.new,
31
26
  query_counts_per_datastore_request: [],
32
27
  datastore_query_server_duration_ms: 0,
33
28
  datastore_query_client_duration_ms: 0,
29
+ queried_shard_count: 0,
34
30
  mutex: ::Thread::Mutex.new
35
31
  )
36
32
  end
@@ -43,17 +39,18 @@ module ElasticGraph
43
39
  end
44
40
  end
45
41
 
46
- def record_hidden_type(type)
42
+ def record_datastore_query_metrics(client_duration_ms:, server_duration_ms:, queried_shard_count:)
47
43
  mutex.synchronize do
48
- hidden_types << type
44
+ self.datastore_query_client_duration_ms += client_duration_ms
45
+ self.datastore_query_server_duration_ms += server_duration_ms if server_duration_ms
46
+ self.queried_shard_count += queried_shard_count
49
47
  end
50
48
  end
51
49
 
52
- def record_datastore_query_duration_ms(client:, server:)
53
- mutex.synchronize do
54
- self.datastore_query_client_duration_ms += client
55
- self.datastore_query_server_duration_ms += server if server
56
- end
50
+ # Indicates how long was spent on transport between the client and the datastore server, including
51
+ # network time, JSON serialization time, etc.
52
+ def datastore_request_transport_duration_ms
53
+ datastore_query_client_duration_ms - datastore_query_server_duration_ms
57
54
  end
58
55
  end
59
56
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -9,7 +9,6 @@
9
9
  require "elastic_graph/graphql/client"
10
10
  require "elastic_graph/graphql/query_details_tracker"
11
11
  require "elastic_graph/support/hash_util"
12
- require "graphql"
13
12
 
14
13
  module ElasticGraph
15
14
  class GraphQL
@@ -18,12 +17,11 @@ module ElasticGraph
18
17
  # @dynamic schema
19
18
  attr_reader :schema
20
19
 
21
- def initialize(schema:, monotonic_clock:, logger:, slow_query_threshold_ms:, datastore_search_router:)
20
+ def initialize(schema:, monotonic_clock:, logger:, slow_query_threshold_ms:)
22
21
  @schema = schema
23
22
  @monotonic_clock = monotonic_clock
24
23
  @logger = logger
25
24
  @slow_query_threshold_ms = slow_query_threshold_ms
26
- @datastore_search_router = datastore_search_router
27
25
  end
28
26
 
29
27
  # Executes the given `query_string` using the provided `variables`.
@@ -61,10 +59,7 @@ module ElasticGraph
61
59
  client: client,
62
60
  context: context.merge({
63
61
  monotonic_clock_deadline: timeout_in_ms&.+(start_time_in_ms),
64
- elastic_graph_schema: @schema,
65
- schema_element_names: @schema.element_names,
66
- elastic_graph_query_tracker: query_tracker,
67
- datastore_search_router: @datastore_search_router
62
+ elastic_graph_query_tracker: query_tracker
68
63
  }.compact)
69
64
  )
70
65
 
@@ -78,10 +73,6 @@ module ElasticGraph
78
73
  EOS
79
74
  end
80
75
 
81
- unless query_tracker.hidden_types.empty?
82
- @logger.warn "#{query_tracker.hidden_types.size} GraphQL types were hidden from the schema due to their backing indices being inaccessible: #{query_tracker.hidden_types.sort.join(", ")}"
83
- end
84
-
85
76
  duration = @monotonic_clock.now_in_ms - start_time_in_ms
86
77
 
87
78
  # Note: I also wanted to log the sanitized query if `result` has `errors`, but `GraphQL::Query#sanitized_query`
@@ -101,12 +92,16 @@ module ElasticGraph
101
92
  "query_fingerprint" => fingerprint_for(query),
102
93
  "query_name" => query.operation_name,
103
94
  "duration_ms" => duration,
104
- # Here we log how long the datastore queries took according to what the datastore itself reported.
95
+ # How long the datastore queries took according to what the datastore itself reported.
105
96
  "datastore_server_duration_ms" => query_tracker.datastore_query_server_duration_ms,
106
- # Here we log an estimate for how much overhead ElasticGraph added on top of how long the datastore took.
97
+ # An estimate for how much overhead ElasticGraph added on top of how long the datastore took.
107
98
  # This is based on the duration, excluding how long the datastore calls took from the client side
108
99
  # (e.g. accounting for network latency, serialization time, etc)
109
100
  "elasticgraph_overhead_ms" => duration - query_tracker.datastore_query_client_duration_ms,
101
+ # An estimate for the time spent on transport (network latency, JSON serialization, etc).
102
+ "datastore_request_transport_duration_ms" => query_tracker.datastore_request_transport_duration_ms,
103
+ # How many datastore shards were queried, in total. This is a measure of how much load the query caused on the datastore.
104
+ "queried_shard_count" => query_tracker.queried_shard_count,
110
105
  # According to https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html#metric-filters-extract-json,
111
106
  # > Value nodes can be strings or numbers...If a property selector points to an array or object, the metric filter won't match the log format.
112
107
  # So, to allow flexibility to deal with cloud watch metric filters, we coerce these values to a string here.
@@ -133,8 +128,7 @@ module ElasticGraph
133
128
  # Note: this is designed so that `elasticgraph-query_registry` can hook into this method. It needs to be able
134
129
  # to override how the query is built and executed.
135
130
  def build_and_execute_query(query_string:, variables:, operation_name:, context:, client:)
136
- query = ::GraphQL::Query.new(
137
- @schema.graphql_schema,
131
+ query = @schema.new_graphql_query(
138
132
  query_string,
139
133
  variables: variables,
140
134
  operation_name: operation_name,
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -15,16 +15,11 @@ module ElasticGraph
15
15
  module Resolvers
16
16
  # Responsible for fetching a single field value from a document.
17
17
  class GetRecordFieldValue
18
- def initialize(schema_element_names:)
19
- @schema_element_names = schema_element_names
18
+ def initialize(elasticgraph_graphql:, config:)
19
+ @schema_element_names = elasticgraph_graphql.runtime_metadata.schema_element_names
20
20
  end
21
21
 
22
- def can_resolve?(field:, object:)
23
- object.is_a?(DatastoreResponse::Document) || object.is_a?(::Hash)
24
- end
25
-
26
- def resolve(field:, object:, args:, context:, lookahead:)
27
- field_name = field.name_in_index.to_s
22
+ def resolve(field:, object:, args:, context:)
28
23
  data =
29
24
  case object
30
25
  when DatastoreResponse::Document
@@ -33,9 +28,8 @@ module ElasticGraph
33
28
  object
34
29
  end
35
30
 
36
- value = Support::HashUtil.fetch_value_at_path(data, field_name) do
37
- field.type.list? ? [] : nil
38
- end
31
+ value = Support::HashUtil.fetch_value_at_path(data, field.path_in_index) { nil }
32
+ value = [] if value.nil? && field.type.list?
39
33
 
40
34
  if field.type.relay_connection?
41
35
  RelayConnection::ArrayAdapter.build(value, args, @schema_element_names, context)
@@ -0,0 +1,126 @@
1
+ # Copyright 2024 - 2025 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/errors"
10
+
11
+ module ElasticGraph
12
+ class GraphQL
13
+ module Resolvers
14
+ # Provides an adapter to the GraphQL gem by building a resolver implementation hash as documented here:
15
+ #
16
+ # https://graphql-ruby.org/schema/sdl.html
17
+ class GraphQLAdapterBuilder
18
+ def initialize(runtime_metadata:, named_resolvers:, query_adapter:)
19
+ @runtime_metadata = runtime_metadata
20
+ @named_resolvers = named_resolvers
21
+ @query_adapter = query_adapter
22
+ end
23
+
24
+ def build
25
+ scalar_type_hash
26
+ .merge(object_type_hash)
27
+ .merge({"resolve_type" => _ = ->(supertype, obj, ctx) { resolve_type(supertype, obj, ctx) }})
28
+ end
29
+
30
+ private
31
+
32
+ def scalar_type_hash
33
+ @runtime_metadata.scalar_types_by_name.transform_values do |scalar_type|
34
+ adapter = (_ = scalar_type.load_coercion_adapter.extension_class) # : SchemaArtifacts::_ScalarCoercionAdapter[untyped, untyped]
35
+ {
36
+ "coerce_input" => ->(value, ctx) { adapter.coerce_input(value, ctx) },
37
+ "coerce_result" => ->(value, ctx) { adapter.coerce_result(value, ctx) }
38
+ }
39
+ end
40
+ end
41
+
42
+ def object_type_hash
43
+ @runtime_metadata.object_types_by_name.filter_map do |type_name, type|
44
+ fields_hash = type.graphql_fields_by_name.filter_map do |field_name, field|
45
+ if (resolver_name = field.resolver)
46
+ resolver = @named_resolvers.fetch(resolver_name) do
47
+ raise Errors::SchemaError, "Resolver `#{resolver_name}` (for `#{type_name}.#{field_name}`) cannot be found."
48
+ end
49
+
50
+ resolver_lambda =
51
+ if resolver.method(:resolve).parameters.include?([:keyreq, :lookahead])
52
+ lambda do |object, args, context|
53
+ schema_field = context.fetch(:elastic_graph_schema).field_named(type_name, field_name)
54
+
55
+ # Extract the `:lookahead` extra that we have configured all fields to provide.
56
+ # See https://graphql-ruby.org/api-doc/1.10.8/GraphQL/Execution/Lookahead.html for more info.
57
+ # It is not a "real" arg in the schema and breaks `args_to_schema_form` when we call that
58
+ # so we need to peel it off here.
59
+ lookahead = args[:lookahead]
60
+
61
+ # Convert args to the form they were defined in the schema, undoing the normalization
62
+ # the GraphQL gem does to convert them to Ruby keyword args form.
63
+ args = schema_field.args_to_schema_form(args.except(:lookahead))
64
+
65
+ result = resolver.resolve(field: schema_field, object: object, args: args, context: context, lookahead: lookahead) do
66
+ @query_adapter.build_query_from(field: schema_field, args: args, lookahead: lookahead, context: context)
67
+ end
68
+
69
+ # Give the field a chance to coerce the result before returning it. Initially, this is only used to deal with
70
+ # enum value overrides (e.g. so that if `DayOfWeek.MONDAY` has been overridden to `DayOfWeek.MON`, we can coerce
71
+ # a `MONDAY` value being returned by a painless script to `MON`), but this is designed to be general purpose
72
+ # and we may use it for other coercions in the future.
73
+ #
74
+ # Note that coercion of scalar values is handled by the `coerce_result` callback below.
75
+ schema_field.coerce_result(result)
76
+ end
77
+ else
78
+ lambda do |object, args, context|
79
+ schema_field = context.fetch(:elastic_graph_schema).field_named(type_name, field_name)
80
+ # Convert args to the form they were defined in the schema, undoing the normalization
81
+ # the GraphQL gem does to convert them to Ruby keyword args form.
82
+ args = schema_field.args_to_schema_form(args)
83
+
84
+ result = resolver.resolve(field: schema_field, object: object, args: args, context: context)
85
+
86
+ # Give the field a chance to coerce the result before returning it. Initially, this is only used to deal with
87
+ # enum value overrides (e.g. so that if `DayOfWeek.MONDAY` has been overridden to `DayOfWeek.MON`, we can coerce
88
+ # a `MONDAY` value being returned by a painless script to `MON`), but this is designed to be general purpose
89
+ # and we may use it for other coercions in the future.
90
+ #
91
+ # Note that coercion of scalar values is handled by the `coerce_result` callback below.
92
+ schema_field.coerce_result(result)
93
+ end
94
+ end
95
+
96
+ [field_name, resolver_lambda]
97
+ end
98
+ end.to_h
99
+
100
+ unless fields_hash.empty?
101
+ [type_name, fields_hash]
102
+ end
103
+ end.to_h
104
+ end
105
+
106
+ # In order to support unions and interfaces, we must implement `resolve_type`.
107
+ def resolve_type(supertype, object, context)
108
+ schema = context.fetch(:elastic_graph_schema)
109
+ # If `__typename` is available, use that to resolve. It should be available on any embedded abstract types...
110
+ # (See `Inventor` in `config/schema.graphql` for an example of this kind of type union.)
111
+ if (typename = object["__typename"])
112
+ schema
113
+ .graphql_schema
114
+ .possible_types(supertype, visibility_profile: VISIBILITY_PROFILE)
115
+ .find { |t| t.graphql_name == typename }
116
+ else
117
+ # ...otherwise infer the type based on what index the object came from. This is the case
118
+ # with unions/interfaces of individually indexed types.
119
+ # (See `Part` in `config/schema/widgets.rb` for an example of this kind of type union.)
120
+ schema.document_type_stored_in(object.index_definition_name).graphql_type
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -14,11 +14,11 @@ module ElasticGraph
14
14
  module Resolvers
15
15
  # Responsible for fetching a a list of records of a particular type
16
16
  class ListRecords
17
- def can_resolve?(field:, object:)
18
- field.parent_type.name == :Query && field.type.collection?
17
+ def initialize(elasticgraph_graphql:, config:)
18
+ # Nothing to initialize, but needs to be defined to satisfy the resolver interface.
19
19
  end
20
20
 
21
- def resolve(field:, context:, lookahead:, **)
21
+ def resolve(field:, object:, args:, context:, lookahead:)
22
22
  query = yield
23
23
  response = QuerySource.execute_one(query, for_context: context)
24
24
  RelayConnection.maybe_wrap(response, field: field, context: context, lookahead: lookahead, query: query)
@@ -1,4 +1,4 @@
1
- # Copyright 2024 Block, Inc.
1
+ # Copyright 2024 - 2025 Block, Inc.
2
2
  #
3
3
  # Use of this source code is governed by an MIT-style
4
4
  # license that can be found in the LICENSE file or at
@@ -6,7 +6,9 @@
6
6
  #
7
7
  # frozen_string_literal: true
8
8
 
9
+ require "elastic_graph/graphql/resolvers/nested_relationships_source"
9
10
  require "elastic_graph/graphql/resolvers/relay_connection"
11
+ require "elastic_graph/graphql/datastore_response/search_response"
10
12
 
11
13
  module ElasticGraph
12
14
  class GraphQL
@@ -17,31 +19,32 @@ module ElasticGraph
17
19
  #
18
20
  # Most of the logic for this lives in ElasticGraph::Schema::RelationJoin.
19
21
  class NestedRelationships
20
- def initialize(schema_element_names:, logger:)
21
- @schema_element_names = schema_element_names
22
- @logger = logger
22
+ def initialize(elasticgraph_graphql:, config:)
23
+ @schema_element_names = elasticgraph_graphql.runtime_metadata.schema_element_names
24
+ @logger = elasticgraph_graphql.logger
25
+ @monotonic_clock = elasticgraph_graphql.monotonic_clock
26
+ @resolver_mode = elasticgraph_graphql.config.nested_relationship_resolver_mode
23
27
  end
24
28
 
25
- def can_resolve?(field:, object:)
26
- !!field.relation_join
27
- end
28
-
29
- def resolve(object:, field:, context:, lookahead:, **)
29
+ def resolve(field:, object:, args:, context:, lookahead:)
30
30
  log_warning = ->(**options) { log_field_problem_warning(field: field, **options) }
31
31
  join = field.relation_join
32
32
  id_or_ids = join.extract_id_or_ids_from(object, log_warning)
33
- filters = [
34
- build_filter(join.filter_id_field_name, nil, join.foreign_key_nested_paths, Array(id_or_ids)),
35
- join.additional_filter
36
- ].reject(&:empty?)
37
- query = yield.merge_with(filters: filters)
33
+ query = yield
38
34
 
39
35
  response =
40
36
  case id_or_ids
41
37
  when nil, []
42
38
  join.blank_value
43
39
  else
44
- initial_response = QuerySource.execute_one(query, for_context: context)
40
+ initial_response = try_synthesize_response_from_ids(field, id_or_ids, query) ||
41
+ NestedRelationshipsSource.execute_one(
42
+ Array(id_or_ids).to_set,
43
+ query:, join:, context:,
44
+ monotonic_clock: @monotonic_clock,
45
+ mode: @resolver_mode
46
+ )
47
+
45
48
  join.normalize_documents(initial_response) do |problem|
46
49
  log_warning.call(document: {"id" => id_or_ids}, problem: "got #{problem} from the datastore search query")
47
50
  end
@@ -52,22 +55,49 @@ module ElasticGraph
52
55
 
53
56
  private
54
57
 
58
+ ONLY_ID = ["id"]
59
+
60
+ # When a client requests only the `id` from a nested relationship, and we already have those ids,
61
+ # we want to avoid querying the datastore, and synthesize a response instead.
62
+ def try_synthesize_response_from_ids(field, id_or_ids, query)
63
+ # This optimization can only be used on a relationship with an outbound foreign key.
64
+ return nil if field.relation.direction == :in
65
+
66
+ # If the client is requesting any fields besides `id`, we can't do this.
67
+ return nil unless (query.requested_fields - ONLY_ID).empty?
68
+
69
+ pagination = query.document_paginator.to_datastore_body
70
+ search_after = pagination.dig(:search_after, 0)
71
+ ids = Array(id_or_ids)
72
+
73
+ sorted_ids =
74
+ case pagination.dig(:sort, 0, "id", "order")
75
+ when "asc"
76
+ ids.sort.select { |id| search_after.nil? || id > search_after }
77
+ when "desc"
78
+ ids.sort.reverse.select { |id| search_after.nil? || id < search_after }
79
+ else
80
+ if ids.size < 2
81
+ ids
82
+ else
83
+ # The client is sorting by something other than `id` and we have multiple ids.
84
+ # We aren't able to determine the correct order for the ids, so we can't synthesize
85
+ # a response.
86
+ return nil
87
+ end
88
+ end
89
+
90
+ DatastoreResponse::SearchResponse.synthesize_from_ids(
91
+ query.search_index_expression,
92
+ sorted_ids.first(pagination.fetch(:size)),
93
+ decoded_cursor_factory: query.send(:decoded_cursor_factory)
94
+ )
95
+ end
96
+
55
97
  def log_field_problem_warning(field:, document:, problem:)
56
98
  id = document.fetch("id", "<no id>")
57
99
  @logger.warn "#{field.parent_type.name}(id: #{id}).#{field.name} had a problem: #{problem}"
58
100
  end
59
-
60
- def build_filter(path, previous_nested_path, nested_paths, ids)
61
- if nested_paths.empty?
62
- path = path.delete_prefix("#{previous_nested_path}.") if previous_nested_path
63
- {path => {@schema_element_names.equal_to_any_of => ids}}
64
- else
65
- next_nested_path, *rest_nested_paths = nested_paths
66
- sub_filter = build_filter(path, next_nested_path, rest_nested_paths, ids)
67
- next_nested_path = next_nested_path.delete_prefix("#{previous_nested_path}.") if previous_nested_path
68
- {next_nested_path => {@schema_element_names.any_satisfy => sub_filter}}
69
- end
70
- end
71
101
  end
72
102
  end
73
103
  end