Skip to content

Commit 7857f68

Browse files
Add GraphQL::Schema::Type helpers needed for query-time __typename filtering
This is a companion/pre-requisite PR to add methods supporting __typename filter injection when querying mixed-type indexes via index inheritance. * `source_type`: resolves the underlying document type for derived types (e.g. indexed aggregations) so the filter can look up search indexes on the source rather than the aggregation type * `subtypes`: declared in RBS so Steep-checked callers can use it * `non_subtypes_in_shared_index`: identifies concrete types sharing an index with this type that are not subtypes — the presence of any such types is what triggers the need for a __typename filter
1 parent ad61890 commit 7857f68

3 files changed

Lines changed: 114 additions & 2 deletions

File tree

  • elasticgraph-graphql

elasticgraph-graphql/lib/elastic_graph/graphql/schema/type.rb

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def search_index_definitions
7676
# We can go from `Inventor` to its subtypes to find the search indexes. However, `InventorAggregation`
7777
# is NOT a union of `PersonAggregation` and `CompanyAggregation`, so we can't do the same thing on the
7878
# indexed aggregation types. Delegating to the source type solves this case.
79-
@schema.type_named(@object_runtime_metadata.source_type).search_index_definitions
79+
source_type.search_index_definitions
8080
else
8181
@index_definitions.union(subtypes.flat_map(&:search_index_definitions))
8282
end
@@ -117,7 +117,7 @@ def unwrap_fully
117117
end
118118
end
119119

120-
# Returns the subtypes of this type, if it has any. This is like `#possible_types` provided by the
120+
# Returns all concrete subtypes, at any depth. This is like `#possible_types` provided by the
121121
# GraphQL gem, but that includes a type itself when you ask for the possible types of a non-abstract type.
122122
def subtypes
123123
@subtypes ||= @schema
@@ -126,6 +126,34 @@ def subtypes
126126
.map { |t| @schema.type_from(t) } - [self]
127127
end
128128

129+
# For derived types (e.g. indexed aggregations), returns the underlying source document type.
130+
# For non-derived types, returns `self`.
131+
def source_type
132+
@source_type ||=
133+
if (st = @object_runtime_metadata.source_type)
134+
@schema.type_named(st)
135+
else
136+
self
137+
end
138+
end
139+
140+
# Returns the set of concrete indexed document types that share any of this type's search
141+
# indexes but are not subtypes of this type. Used to determine whether a `__typename`
142+
# filter is needed when querying an abstract type.
143+
#
144+
# Abstract types are excluded because documents in the datastore are always associated
145+
# with a concrete `__typename`. When filtering by `__typename`, only concrete types are
146+
# relevant.
147+
def non_subtypes_in_shared_index
148+
@non_subtypes_in_shared_index ||= begin
149+
all_subtypes = subtypes.to_set # all concrete subtypes at any depth
150+
search_index_definitions
151+
.flat_map { |index_def| @schema.document_types_stored_in(index_def.name).to_a }
152+
.reject { |t| t == self || all_subtypes.include?(t) || t.abstract? }
153+
.to_set
154+
end
155+
end
156+
129157
def field_named(field_name)
130158
@fields_by_name.fetch(field_name)
131159
rescue KeyError => e

elasticgraph-graphql/sig/elastic_graph/graphql/schema/type.rbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ module ElasticGraph
99
attr_reader graphql_type: ::GraphQL::Schema::_Type
1010
attr_reader grouping_missing_value_placeholder: ::String? | ::Numeric?
1111
def search_index_definitions: () -> ::Array[DatastoreCore::_IndexDefinition]
12+
def source_type: () -> Type
13+
def subtypes: () -> ::Array[Type]
14+
def non_subtypes_in_shared_index: () -> ::Set[Type]
1215
def unwrap_fully: () -> Type
1316
def field_named: (::String) -> Field
1417
def fields_by_name_in_index: () -> ::Hash[::String, ::Array[Field]]

elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,87 @@ def search_index_definitions_from(type_name: "TheType")
758758
end
759759
end
760760

761+
describe "#source_type" do
762+
it "returns the underlying document type for an indexed aggregation type" do
763+
schema = define_schema do |s|
764+
s.object_type "Thing" do |t|
765+
t.field "id", "ID!"
766+
t.index "things"
767+
end
768+
end
769+
770+
aggregation_type = schema.type_named("ThingAggregation")
771+
expect(aggregation_type.source_type).to be schema.type_named("Thing")
772+
end
773+
774+
it "returns self for a non-derived type" do
775+
schema = define_schema do |s|
776+
s.object_type "Thing" do |t|
777+
t.field "id", "ID!"
778+
t.index "things"
779+
end
780+
end
781+
782+
thing_type = schema.type_named("Thing")
783+
expect(thing_type.source_type).to be thing_type
784+
end
785+
end
786+
787+
describe "#non_subtypes_in_shared_index" do
788+
attr_reader :schema
789+
790+
before(:context) do
791+
@schema = define_schema(clients_by_name: {}) do |s|
792+
# A root interface with an index shared by all sub-hierarchies.
793+
s.interface_type "Channel" do |t|
794+
t.field "id", "ID!"
795+
t.index "channels"
796+
end
797+
798+
# A sibling type — NOT under Store, but shares the Channel index.
799+
s.object_type "Wholesaler" do |t|
800+
t.implements "Channel"
801+
t.field "id", "ID!"
802+
end
803+
804+
# A sub-interface and its concrete subtypes.
805+
s.interface_type "Store" do |t|
806+
t.implements "Channel"
807+
t.field "id", "ID!"
808+
end
809+
810+
s.object_type "OnlineStore" do |t|
811+
t.implements "Store"
812+
t.field "id", "ID!"
813+
end
814+
815+
# PhysicalStore overrides to use a dedicated index.
816+
s.object_type "PhysicalStore" do |t|
817+
t.implements "Store"
818+
t.field "id", "ID!"
819+
t.index "physical_stores"
820+
end
821+
end
822+
end
823+
824+
it "excludes the type itself and its subtypes" do
825+
store = schema.type_named("Store")
826+
827+
result = store.non_subtypes_in_shared_index
828+
expect(result).not_to include(store)
829+
expect(result).not_to include(schema.type_named("OnlineStore"))
830+
expect(result).not_to include(schema.type_named("PhysicalStore"))
831+
end
832+
833+
it "includes concrete sibling types that share the same index" do
834+
expect(schema.type_named("Store").non_subtypes_in_shared_index).to include(schema.type_named("Wholesaler"))
835+
end
836+
837+
it "returns an empty set when all types sharing its indexes are subtypes" do
838+
expect(schema.type_named("Channel").non_subtypes_in_shared_index).to be_empty
839+
end
840+
end
841+
761842
describe "#hidden_from_queries?" do
762843
it "returns `false` on a type that has no backing indexed types" do
763844
schema = define_schema do |s|

0 commit comments

Comments
 (0)