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