elasticgraph-graphql 0.18.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/elasticgraph-graphql.gemspec +23 -0
- data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +79 -0
- data/lib/elastic_graph/graphql/aggregation/computation.rb +39 -0
- data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +83 -0
- data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +47 -0
- data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +26 -0
- data/lib/elastic_graph/graphql/aggregation/key.rb +87 -0
- data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +37 -0
- data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +129 -0
- data/lib/elastic_graph/graphql/aggregation/path_segment.rb +31 -0
- data/lib/elastic_graph/graphql/aggregation/query.rb +172 -0
- data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +345 -0
- data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +187 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +41 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +44 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +30 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +64 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +83 -0
- data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +82 -0
- data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +32 -0
- data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +118 -0
- data/lib/elastic_graph/graphql/client.rb +43 -0
- data/lib/elastic_graph/graphql/config.rb +81 -0
- data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +100 -0
- data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +142 -0
- data/lib/elastic_graph/graphql/datastore_query/paginator.rb +199 -0
- data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +239 -0
- data/lib/elastic_graph/graphql/datastore_query.rb +372 -0
- data/lib/elastic_graph/graphql/datastore_response/document.rb +78 -0
- data/lib/elastic_graph/graphql/datastore_response/search_response.rb +79 -0
- data/lib/elastic_graph/graphql/datastore_search_router.rb +151 -0
- data/lib/elastic_graph/graphql/decoded_cursor.rb +120 -0
- data/lib/elastic_graph/graphql/filtering/boolean_query.rb +45 -0
- data/lib/elastic_graph/graphql/filtering/field_path.rb +81 -0
- data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +58 -0
- data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +526 -0
- data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +148 -0
- data/lib/elastic_graph/graphql/filtering/range_query.rb +56 -0
- data/lib/elastic_graph/graphql/http_endpoint.rb +229 -0
- data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +56 -0
- data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +48 -0
- data/lib/elastic_graph/graphql/query_adapter/filters.rb +161 -0
- data/lib/elastic_graph/graphql/query_adapter/pagination.rb +27 -0
- data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +124 -0
- data/lib/elastic_graph/graphql/query_adapter/sort.rb +32 -0
- data/lib/elastic_graph/graphql/query_details_tracker.rb +60 -0
- data/lib/elastic_graph/graphql/query_executor.rb +200 -0
- data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +49 -0
- data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +114 -0
- data/lib/elastic_graph/graphql/resolvers/list_records.rb +29 -0
- data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +74 -0
- data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +85 -0
- data/lib/elastic_graph/graphql/resolvers/query_source.rb +46 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +71 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +65 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +82 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +40 -0
- data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +42 -0
- data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +56 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +35 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +64 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +60 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +30 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +47 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +24 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +44 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +32 -0
- data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +634 -0
- data/lib/elastic_graph/graphql/schema/arguments.rb +78 -0
- data/lib/elastic_graph/graphql/schema/enum_value.rb +30 -0
- data/lib/elastic_graph/graphql/schema/field.rb +147 -0
- data/lib/elastic_graph/graphql/schema/relation_join.rb +103 -0
- data/lib/elastic_graph/graphql/schema/type.rb +263 -0
- data/lib/elastic_graph/graphql/schema.rb +164 -0
- data/lib/elastic_graph/graphql.rb +253 -0
- data/script/dump_time_zones +81 -0
- data/script/dump_time_zones.java +17 -0
- 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
|