elasticgraph-graphql 0.18.0.0

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