elasticgraph-graphql 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +3 -0
  4. data/elasticgraph-graphql.gemspec +23 -0
  5. data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +79 -0
  6. data/lib/elastic_graph/graphql/aggregation/computation.rb +39 -0
  7. data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +83 -0
  8. data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +47 -0
  9. data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +26 -0
  10. data/lib/elastic_graph/graphql/aggregation/key.rb +87 -0
  11. data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +37 -0
  12. data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +129 -0
  13. data/lib/elastic_graph/graphql/aggregation/path_segment.rb +31 -0
  14. data/lib/elastic_graph/graphql/aggregation/query.rb +172 -0
  15. data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +345 -0
  16. data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +187 -0
  17. data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +41 -0
  18. data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +44 -0
  19. data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +30 -0
  20. data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +64 -0
  21. data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +83 -0
  22. data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +82 -0
  23. data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +32 -0
  24. data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +118 -0
  25. data/lib/elastic_graph/graphql/client.rb +43 -0
  26. data/lib/elastic_graph/graphql/config.rb +81 -0
  27. data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +100 -0
  28. data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +142 -0
  29. data/lib/elastic_graph/graphql/datastore_query/paginator.rb +199 -0
  30. data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +239 -0
  31. data/lib/elastic_graph/graphql/datastore_query.rb +372 -0
  32. data/lib/elastic_graph/graphql/datastore_response/document.rb +78 -0
  33. data/lib/elastic_graph/graphql/datastore_response/search_response.rb +79 -0
  34. data/lib/elastic_graph/graphql/datastore_search_router.rb +151 -0
  35. data/lib/elastic_graph/graphql/decoded_cursor.rb +120 -0
  36. data/lib/elastic_graph/graphql/filtering/boolean_query.rb +45 -0
  37. data/lib/elastic_graph/graphql/filtering/field_path.rb +81 -0
  38. data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +58 -0
  39. data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +526 -0
  40. data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +148 -0
  41. data/lib/elastic_graph/graphql/filtering/range_query.rb +56 -0
  42. data/lib/elastic_graph/graphql/http_endpoint.rb +229 -0
  43. data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +56 -0
  44. data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +48 -0
  45. data/lib/elastic_graph/graphql/query_adapter/filters.rb +161 -0
  46. data/lib/elastic_graph/graphql/query_adapter/pagination.rb +27 -0
  47. data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +124 -0
  48. data/lib/elastic_graph/graphql/query_adapter/sort.rb +32 -0
  49. data/lib/elastic_graph/graphql/query_details_tracker.rb +60 -0
  50. data/lib/elastic_graph/graphql/query_executor.rb +200 -0
  51. data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +49 -0
  52. data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +114 -0
  53. data/lib/elastic_graph/graphql/resolvers/list_records.rb +29 -0
  54. data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +74 -0
  55. data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +85 -0
  56. data/lib/elastic_graph/graphql/resolvers/query_source.rb +46 -0
  57. data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +71 -0
  58. data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +65 -0
  59. data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +82 -0
  60. data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +40 -0
  61. data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +42 -0
  62. data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +56 -0
  63. data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +35 -0
  64. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +64 -0
  65. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +60 -0
  66. data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +30 -0
  67. data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +47 -0
  68. data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +24 -0
  69. data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +44 -0
  70. data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +32 -0
  71. data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +634 -0
  72. data/lib/elastic_graph/graphql/schema/arguments.rb +78 -0
  73. data/lib/elastic_graph/graphql/schema/enum_value.rb +30 -0
  74. data/lib/elastic_graph/graphql/schema/field.rb +147 -0
  75. data/lib/elastic_graph/graphql/schema/relation_join.rb +103 -0
  76. data/lib/elastic_graph/graphql/schema/type.rb +263 -0
  77. data/lib/elastic_graph/graphql/schema.rb +164 -0
  78. data/lib/elastic_graph/graphql.rb +253 -0
  79. data/script/dump_time_zones +81 -0
  80. data/script/dump_time_zones.java +17 -0
  81. metadata +503 -0
@@ -0,0 +1,30 @@
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/error"
10
+
11
+ module ElasticGraph
12
+ class GraphQL
13
+ class Schema
14
+ # Represents an enum value within a GraphQL schema.
15
+ class EnumValue < ::Data.define(:name, :type, :runtime_metadata)
16
+ def sort_clauses
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.")
19
+
20
+ [sort_clause]
21
+ end
22
+
23
+ def to_s
24
+ "#<#{self.class.name} #{type.name}.#{name}>"
25
+ end
26
+ alias_method :inspect, :to_s
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,147 @@
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/error"
10
+ require "elastic_graph/graphql/schema/relation_join"
11
+ require "elastic_graph/graphql/schema/arguments"
12
+
13
+ module ElasticGraph
14
+ class GraphQL
15
+ class Schema
16
+ # Represents a field within a GraphQL type.
17
+ class Field
18
+ # The type in which the field resides.
19
+ attr_reader :parent_type
20
+
21
+ attr_reader :schema, :schema_element_names, :graphql_field, :name_in_index, :relation, :computation_detail
22
+
23
+ def initialize(schema, parent_type, graphql_field, runtime_metadata)
24
+ @schema = schema
25
+ @schema_element_names = schema.element_names
26
+ @parent_type = parent_type
27
+ @graphql_field = graphql_field
28
+ @relation = runtime_metadata&.relation
29
+ @computation_detail = runtime_metadata&.computation_detail
30
+ @name_in_index = runtime_metadata&.name_in_index&.to_sym || name
31
+
32
+ # Adds the :extras required by ElasticGraph. For now, this blindly adds `:lookahead`
33
+ # to each field so that we have access to what the child selections are, as described here:
34
+ #
35
+ # https://graphql-ruby.org/queries/lookahead
36
+ #
37
+ # Currently we only need this when building an `DatastoreQuery` (which is not done for all
38
+ # fields) so a future optimization may only add this to fields where we actually need it.
39
+ # For now we add it to all fields because it's simplest and it's not clear if there is
40
+ # any performance benefit to not adding it when we do not use it.
41
+ #
42
+ # Note: input fields do not respond to `extras`, which is why we do it conditionally here.
43
+ #
44
+ # Note: on GraphQL gem introspection types (e.g. `__Field`), the fields respond to `:extras`,
45
+ # but that later causes a weird error (`ArgumentError: unknown keyword: :lookahead`)
46
+ # when those types are accessed in a Query. We don't really want to mutate the fields on the
47
+ # built-in types by adding `:lookahead` so it's best to avoid setting that extra on the built
48
+ # in types.
49
+ if @graphql_field.respond_to?(:extras) && !BUILT_IN_TYPE_NAMES.include?(parent_type.name.to_s)
50
+ @graphql_field.extras([:lookahead])
51
+ end
52
+ end
53
+
54
+ def type
55
+ @type ||= @schema.type_from(@graphql_field.type)
56
+ end
57
+
58
+ def name
59
+ @name ||= @graphql_field.name.to_sym
60
+ end
61
+
62
+ # Returns an object that knows how this field joins to its relation.
63
+ # Used by ElasticGraph::Resolvers::NestedRelationships.
64
+ def relation_join
65
+ # Not every field has a join relation, so it can be nil. But we do not want
66
+ # to re-compute that on every call, so we return @relation_join if it's already
67
+ # defined rather than if its truthy.
68
+ return @relation_join if defined?(@relation_join)
69
+ @relation_join = RelationJoin.from(self)
70
+ end
71
+
72
+ # Given an array of sort enums, returns an array of datastore compatible sort clauses
73
+ def sort_clauses_for(sorts)
74
+ Array(sorts).flat_map { |sort| sort_argument_type.enum_value_named(sort).sort_clauses }
75
+ end
76
+
77
+ # Indicates if this is an aggregated field (used inside an `Aggregation` type).
78
+ def aggregated?
79
+ type.unwrap_non_null.elasticgraph_category == :scalar_aggregated_values
80
+ end
81
+
82
+ def args_to_schema_form(args)
83
+ Arguments.to_schema_form(args, @graphql_field)
84
+ end
85
+
86
+ # Returns a list of field names that are required from the datastore in order
87
+ # to resolve this field at GraphQL query handling time.
88
+ def index_field_names_for_resolution
89
+ # For an embedded object, we do not require any fields because it is the nested fields
90
+ # that we will request from the datastore, which will be required to resolve them. But
91
+ # we do not need to request the embedded object field itself.
92
+ return [] if type.embedded_object?
93
+ return [] if parent_type.relay_connection? || parent_type.relay_edge?
94
+ return index_id_field_names_for_relation if relation_join
95
+
96
+ [name_in_index.to_s]
97
+ end
98
+
99
+ # Indicates this field should be hidden in the GraphQL schema so as to not be queryable.
100
+ # We only hide a field if resolving it would require using a datastore cluster that
101
+ # we can't access. For the most part, this just delegates to `Type#hidden_from_queries?`
102
+ # which does the index accessibility check.
103
+ def hidden_from_queries?
104
+ # The type has logic to check if the backing datastore index is accessible, so we just
105
+ # delegate to that logic here.
106
+ type.unwrap_fully.hidden_from_queries?
107
+ end
108
+
109
+ def coerce_result(result)
110
+ return result unless parent_type.graphql_only_return_type
111
+ type.coerce_result(result)
112
+ end
113
+
114
+ def description
115
+ "#{@parent_type.name}.#{name}"
116
+ end
117
+
118
+ def to_s
119
+ "#<#{self.class.name} #{description}>"
120
+ end
121
+ alias_method :inspect, :to_s
122
+
123
+ private
124
+
125
+ # Returns the `order_by` arguments type field (unwrapped)
126
+ def sort_argument_type
127
+ @sort_argument_type ||= begin
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}`."
130
+ end
131
+ @schema.type_from(graphql_argument.type.unwrap)
132
+ end
133
+ end
134
+
135
+ def index_id_field_names_for_relation
136
+ if type.unwrap_fully == parent_type # means its a self-referential relation (e.g. child to parent of same type)
137
+ # Since it's self-referential, the `filter_id_field` (which lives on the "remote" type) also must
138
+ # exist as a field in our DatastoreCore::IndexDefinition.
139
+ [relation_join.document_id_field_name, relation_join.filter_id_field_name]
140
+ else
141
+ [relation_join.document_id_field_name]
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,103 @@
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/graphql/datastore_response/search_response"
10
+
11
+ module ElasticGraph
12
+ class GraphQL
13
+ class Schema
14
+ # Represents the join between documents for a relation.
15
+ #
16
+ # Note that this class assumes a valid, well-formed schema definition, and makes no
17
+ # attempt to provide user-friendly errors when that is not the case. For example,
18
+ # we assume that a nested relationship field has at most one relationship directive.
19
+ # The (as yet unwritten) schema linter should validate such things eventually.
20
+ # When we do encounter errors at runtime (such as getting a scalar where we expect
21
+ # a list, or vice-versa), this class attempts to deal with as best as it can (sometimes
22
+ # simply picking one record or id from many!) and logs a warning.
23
+ #
24
+ # Note: this class isn't driven directly by tests. It exist purely to serve the needs
25
+ # of ElasticGraph::Resolvers::NestedRelationships, and is driven by that class's tests.
26
+ # It lives here because it's useful to expose it off of a `Field` since it's a property
27
+ # of the field and that lets us memoize it on the field itself.
28
+ class RelationJoin < ::Data.define(:field, :document_id_field_name, :filter_id_field_name, :id_cardinality, :doc_cardinality, :additional_filter, :foreign_key_nested_paths)
29
+ def self.from(field)
30
+ return nil if (relation = field.relation).nil?
31
+
32
+ doc_cardinality = field.type.collection? ? Cardinality::Many : Cardinality::One
33
+
34
+ if relation.direction == :in
35
+ # An inbound foreign key has some field (such as `foo_id`) on another document that points
36
+ # back to the `id` field on the document with the relation.
37
+ #
38
+ # The cardinality of the document id field on an inbound relation is always 1 since
39
+ # it is always the primary key `id` field.
40
+ new(field, "id", relation.foreign_key, Cardinality::One, doc_cardinality, relation.additional_filter, relation.foreign_key_nested_paths)
41
+ else
42
+ # An outbound foreign key has some field (such as `foo_id`) on the document with the relation
43
+ # that point out to the `id` field of another document.
44
+ new(field, relation.foreign_key, "id", doc_cardinality, doc_cardinality, relation.additional_filter, relation.foreign_key_nested_paths)
45
+ end
46
+ end
47
+
48
+ def blank_value
49
+ doc_cardinality.blank_value
50
+ end
51
+
52
+ # Extracts a single id or a list of ids from the given document, as required by the relation.
53
+ def extract_id_or_ids_from(document, log_warning)
54
+ id_or_ids = document.fetch(document_id_field_name) do
55
+ log_warning.call(document: document, problem: "#{document_id_field_name} is missing from the document")
56
+ blank_value
57
+ end
58
+
59
+ normalize_ids(id_or_ids) do |problem|
60
+ log_warning.call(document: document, problem: "#{document_id_field_name}: #{problem}")
61
+ end
62
+ end
63
+
64
+ # Normalizes the given documents, ensuring it has the expected cardinality.
65
+ def normalize_documents(response, &handle_warning)
66
+ doc_cardinality.normalize(response, handle_warning: handle_warning, &:id)
67
+ end
68
+
69
+ private
70
+
71
+ def normalize_ids(id_or_ids, &handle_warning)
72
+ id_cardinality.normalize(id_or_ids, handle_warning: handle_warning, &:itself)
73
+ end
74
+
75
+ module Cardinality
76
+ module Many
77
+ def self.normalize(list_or_scalar, handle_warning:)
78
+ return list_or_scalar if list_or_scalar.is_a?(Enumerable)
79
+ handle_warning.call("scalar instead of a list")
80
+ Array(list_or_scalar)
81
+ end
82
+
83
+ def self.blank_value
84
+ DatastoreResponse::SearchResponse::EMPTY
85
+ end
86
+ end
87
+
88
+ module One
89
+ def self.normalize(list_or_scalar, handle_warning:, &deterministic_comparator)
90
+ return list_or_scalar unless list_or_scalar.is_a?(Enumerable)
91
+ handle_warning.call("list of more than one item instead of a scalar") if list_or_scalar.size > 1
92
+ list_or_scalar.min_by(&deterministic_comparator)
93
+ end
94
+
95
+ def self.blank_value
96
+ nil
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,263 @@
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/datastore_core/index_definition"
10
+ require "elastic_graph/error"
11
+ require "elastic_graph/graphql/schema/field"
12
+ require "elastic_graph/graphql/schema/enum_value"
13
+ require "forwardable"
14
+
15
+ module ElasticGraph
16
+ class GraphQL
17
+ class Schema
18
+ # Represents a GraphQL type.
19
+ class Type
20
+ attr_reader :graphql_type, :fields_by_name, :index_definitions, :elasticgraph_category, :graphql_only_return_type
21
+
22
+ def initialize(
23
+ schema,
24
+ graphql_type,
25
+ index_definitions,
26
+ object_runtime_metadata,
27
+ enum_runtime_metadata
28
+ )
29
+ @schema = schema
30
+ @graphql_type = graphql_type
31
+ @enum_values_by_name = Hash.new do |hash, key|
32
+ hash[key] = lookup_enum_value_by_name(key)
33
+ end
34
+
35
+ @index_definitions = index_definitions
36
+ @object_runtime_metadata = object_runtime_metadata
37
+ @elasticgraph_category = object_runtime_metadata&.elasticgraph_category
38
+ @graphql_only_return_type = object_runtime_metadata&.graphql_only_return_type
39
+ @enum_runtime_metadata = enum_runtime_metadata
40
+ @enum_value_names_by_original_name = (enum_runtime_metadata&.values_by_name || {}).to_h do |name, value|
41
+ [value.alternate_original_name || name, name]
42
+ end
43
+
44
+ @fields_by_name = build_fields_by_name_hash(schema, graphql_type).freeze
45
+ end
46
+
47
+ def name
48
+ @name ||= @graphql_type.to_type_signature.to_sym
49
+ end
50
+
51
+ # List of index definitions that should be searched for this type.
52
+ def search_index_definitions
53
+ @search_index_definitions ||=
54
+ if indexed_aggregation?
55
+ # For an indexed aggregation, we just delegate to its source type. This works better than
56
+ # dumping index definitions in the runtime metadata of the indexed aggregation type itself
57
+ # because of abstract (interface/union) types. The source document type handles that (since
58
+ # there is a supertype/subtype relationship on the document types) but that relationship
59
+ # does not exist on the indexed aggregation.
60
+ #
61
+ # For example, assume we have these indexed document types:
62
+ # - type Person {}
63
+ # - type Company {}
64
+ # - union Inventor = Person | Company
65
+ #
66
+ # We can go from `Inventor` to its subtypes to find the search indexes. However, `InventorAggregation`
67
+ # is NOT a union of `PersonAggregation` and `CompanyAggregation`, so we can't do the same thing on the
68
+ # indexed aggregation types. Delegating to the source type solves this case.
69
+ @schema.type_named(@object_runtime_metadata.source_type).search_index_definitions
70
+ else
71
+ @index_definitions.union(subtypes.flat_map(&:search_index_definitions))
72
+ end
73
+ end
74
+
75
+ # List of index definitions that should be indexed into for this type.
76
+ # For now this is just an alias for `search_index_definitions`, but
77
+ # in the future we expect to allow these to be different. We don't yet
78
+ # support defining multiple indices on one GraphQL type, though, which is
79
+ # where that would prove useful. Still, it's a useful abstraction to have
80
+ # this method available for callers now.
81
+ alias_method :indexing_index_definitions, :search_index_definitions
82
+
83
+ # Unwraps the non-null type wrapping, if this type is non-null. If this type is nullable,
84
+ # returns it as-is.
85
+ def unwrap_non_null
86
+ return self if nullable?
87
+ @schema.type_from(@graphql_type.of_type)
88
+ end
89
+
90
+ # Fully unwraps this type, in order to extracts the underlying type (an object or scalar)
91
+ # from its wrappings. As needed, this will unwrap any of these wrappings:
92
+ #
93
+ # - non-null
94
+ # - list
95
+ # - relay connection
96
+ def unwrap_fully
97
+ @unwrap_fully ||= begin
98
+ unwrapped = @schema.type_from(@graphql_type.unwrap)
99
+
100
+ if unwrapped.relay_connection?
101
+ unwrapped
102
+ .field_named(@schema.element_names.edges).type.unwrap_fully
103
+ .field_named(@schema.element_names.node).type.unwrap_fully
104
+ else
105
+ unwrapped
106
+ end
107
+ end
108
+ end
109
+
110
+ # Returns the subtypes of this type, if it has any. This is like `#possible_types` provided by the
111
+ # GraphQL gem, but that includes a type itself when you ask for the possible types of a non-abstract type.
112
+ def subtypes
113
+ @subtypes ||= @schema.graphql_schema.possible_types(graphql_type).map { |t| @schema.type_from(t) } - [self]
114
+ end
115
+
116
+ def field_named(field_name)
117
+ @fields_by_name.fetch(field_name.to_s)
118
+ rescue KeyError => e
119
+ msg = "No field named #{field_name} (on type #{name}) could be found"
120
+ msg += "; Possible alternatives: [#{e.corrections.join(", ").delete('"')}]." if e.corrections.any?
121
+ raise NotFoundError, msg
122
+ end
123
+
124
+ def enum_value_named(enum_value_name)
125
+ @enum_values_by_name[enum_value_name.to_s]
126
+ end
127
+
128
+ def coerce_result(result)
129
+ @enum_value_names_by_original_name.fetch(result, result)
130
+ end
131
+
132
+ def to_s
133
+ "#<#{self.class.name} #{name}>"
134
+ end
135
+ alias_method :inspect, :to_s
136
+
137
+ # ********************************************************************************************
138
+ # Predicates
139
+ #
140
+ # Below here are a bunch of predicates that can be used to ask questions of a type. GraphQL's
141
+ # "wrapping" type system (e.g. non-null wraps nullable; lists wrap objects or scalars) adds
142
+ # some complexity and nuance here. We have decided to implement these predicates to auto-unwrap
143
+ # non-null (e.g. SomeType! -> SomeType). For example, `object?` will return `true` from both a
144
+ # nullable and non-nullable object type, because both are fundamentally objects. Importantly,
145
+ # we do not ever auto-unwrap a type from its list or relay connection wrapping; if the caller
146
+ # wants that, they can manually unwrap before calling the predicate.
147
+ #
148
+ # Note also that `non_null?` and `nullable?` are an exception: since they check nullability,
149
+ # we do not auto-unwrap non-null on them, naturally.
150
+ # ********************************************************************************************
151
+
152
+ extend Forwardable
153
+ def_delegators :@graphql_type, :list?, :non_null?
154
+
155
+ def nullable?
156
+ !non_null?
157
+ end
158
+
159
+ def abstract?
160
+ return unwrap_non_null.abstract? if non_null?
161
+ @graphql_type.kind.abstract?
162
+ end
163
+
164
+ def enum?
165
+ return unwrap_non_null.enum? if non_null?
166
+ @graphql_type.kind.enum?
167
+ end
168
+
169
+ # Returns `true` if this type serializes as a JSON object, with sub-fields.
170
+ # Note this is slightly different from the GraphQL gem and GraphQL spec: it considers
171
+ # inputs to be distinct from objects, but for our purposes we consider inputs to be
172
+ # objects since they have sub-fields and serialize as JSON objects.
173
+ def object?
174
+ return unwrap_non_null.object? if non_null?
175
+ kind = @graphql_type.kind
176
+ kind.abstract? || kind.object? || kind.input_object?
177
+ end
178
+
179
+ # Is the type a user-defined document type directly indexed in the index?
180
+ def indexed_document?
181
+ return unwrap_non_null.indexed_document? if non_null?
182
+ return false if indexed_aggregation?
183
+ return true if subtypes.any? && subtypes.all?(&:indexed_document?)
184
+ @index_definitions.any?
185
+ end
186
+
187
+ def indexed_aggregation?
188
+ unwrapped_has_category?(:indexed_aggregation)
189
+ end
190
+
191
+ # Indicates if this type is an object type that is embedded in another indexed type
192
+ # in the index mapping. Note: we have avoided the term `nested` here because it
193
+ # is a specific Elasticsearch/OpenSearch mapping type that we will not necessarily be using:
194
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
195
+ def embedded_object?
196
+ return unwrap_non_null.embedded_object? if non_null?
197
+ return false if relay_edge? || relay_connection? || @graphql_type.kind.input_object?
198
+ object? && !indexed_document? && !indexed_aggregation?
199
+ end
200
+
201
+ def collection?
202
+ list? || relay_connection?
203
+ end
204
+
205
+ def relay_connection?
206
+ unwrapped_has_category?(:relay_connection)
207
+ end
208
+
209
+ def relay_edge?
210
+ unwrapped_has_category?(:relay_edge)
211
+ end
212
+
213
+ # Indicates this type should be hidden in the GraphQL schema so as to not be queryable.
214
+ # We only hide a type if both of the following are true:
215
+ #
216
+ # - It's backed by one or more search index definitions
217
+ # - None of the search index definitions are accessible from queries
218
+ def hidden_from_queries?
219
+ return false if search_index_definitions.empty?
220
+ search_index_definitions.none?(&:accessible_from_queries?)
221
+ end
222
+
223
+ private
224
+
225
+ def lookup_enum_value_by_name(enum_value_name)
226
+ graphql_enum_value = @graphql_type.values.fetch(enum_value_name)
227
+
228
+ EnumValue.new(
229
+ name: graphql_enum_value.graphql_name.to_sym,
230
+ type: self,
231
+ runtime_metadata: @enum_runtime_metadata&.values_by_name&.dig(enum_value_name)
232
+ )
233
+ rescue KeyError => e
234
+ msg = "No enum value named #{enum_value_name} (on type #{name}) could be found"
235
+ msg += "; Possible alternatives: [#{e.corrections.join(", ").delete('"')}]." if e.corrections.any?
236
+ raise NotFoundError, msg
237
+ end
238
+
239
+ def build_fields_by_name_hash(schema, graphql_type)
240
+ fields_hash =
241
+ if graphql_type.respond_to?(:fields)
242
+ graphql_type.fields
243
+ elsif graphql_type.kind.input_object?
244
+ # Unfortunately, input objects do not have a `fields` method; instead it is called `arguments`.
245
+ graphql_type.arguments
246
+ else
247
+ {}
248
+ end
249
+
250
+ # Eagerly fan out and instantiate all `Field` objects so that the :extras
251
+ # get added to each field as require before we execute the first query
252
+ fields_hash.each_with_object({}) do |(name, field), hash|
253
+ hash[name] = Field.new(schema, self, field, @object_runtime_metadata&.graphql_fields_by_name&.dig(name))
254
+ end
255
+ end
256
+
257
+ def unwrapped_has_category?(category)
258
+ unwrap_non_null.elasticgraph_category == category
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end