Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions elasticgraph-graphql/lib/elastic_graph/graphql/schema/type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ def name
# List of index definitions that should be searched for this type.
def search_index_definitions
@search_index_definitions ||=
if indexed_aggregation?
# For an indexed aggregation, we just delegate to its source type. This works better than
# dumping index definitions in the runtime metadata of the indexed aggregation type itself
# because of abstract (interface/union) types. The source document type handles that (since
# there is a supertype/subtype relationship on the document types) but that relationship
# does not exist on the indexed aggregation.
if (st = source_type)
# When a type has a source type (a prime example being indexed aggregations), we delegate
# to the source type. This works better than dumping index definitions in the runtime metadata
# of the derived type itself because of abstract (interface/union) types. The source document
# type handles that (since there is a supertype/subtype relationship on the document types)
# but that relationship does not exist on derived types.
#
# For example, assume we have these indexed document types:
# - type Person {}
Expand All @@ -76,7 +76,7 @@ def search_index_definitions
# We can go from `Inventor` to its subtypes to find the search indexes. However, `InventorAggregation`
# is NOT a union of `PersonAggregation` and `CompanyAggregation`, so we can't do the same thing on the
# indexed aggregation types. Delegating to the source type solves this case.
@schema.type_named(@object_runtime_metadata.source_type).search_index_definitions
st.search_index_definitions
else
@index_definitions.union(subtypes.flat_map(&:search_index_definitions))
end
Expand Down Expand Up @@ -117,13 +117,37 @@ def unwrap_fully
end
end

# Returns the subtypes of this type, if it has any. This is like `#possible_types` provided by the
# Returns all concrete subtypes, at any depth. This is like `#possible_types` provided by the
# GraphQL gem, but that includes a type itself when you ask for the possible types of a non-abstract type.
def subtypes
@subtypes ||= @schema
.graphql_schema
.possible_types(graphql_type, visibility_profile: :boot)
.map { |t| @schema.type_from(t) } - [self]
.map { |t| @schema.type_from(t) }
.reject { |t| t == self }
.to_set
end

# For derived types (e.g. indexed aggregations), returns the underlying source document type.
# Returns `nil` for non-derived types.
def source_type
return @source_type if defined?(@source_type)
@source_type = @object_runtime_metadata&.source_type&.then { |st| @schema.type_named(st) }
end

# Returns the set of concrete (non-abstract) indexed document types that share any of this
# type's search indexes but are not subtypes of this type. Used to determine whether a
# `__typename` filter is needed when querying an abstract type.
#
# Abstract types are excluded because documents in the datastore are always associated
# with a concrete `__typename`. When filtering by `__typename`, only concrete types are
# relevant.
def concrete_non_subtypes_in_shared_index
@concrete_non_subtypes_in_shared_index ||=
search_index_definitions
.flat_map { |index_def| @schema.document_types_stored_in(index_def.name).to_a }
.reject { |t| t == self || subtypes.include?(t) || t.abstract? }
.to_set
end

def field_named(field_name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ module ElasticGraph
attr_reader graphql_type: ::GraphQL::Schema::_Type
attr_reader grouping_missing_value_placeholder: ::String? | ::Numeric?
def search_index_definitions: () -> ::Array[DatastoreCore::_IndexDefinition]
def source_type: () -> Type?
def subtypes: () -> ::Set[Type]
def concrete_non_subtypes_in_shared_index: () -> ::Set[Type]
def unwrap_fully: () -> Type
def field_named: (::String) -> Field
def fields_by_name_in_index: () -> ::Hash[::String, ::Array[Field]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -604,14 +604,14 @@ def type_for(field_name)
end
end

it "returns [] for object types" do
it "returns an empty set for object types" do
type = schema.type_named("Size")
expect(type.subtypes).to eq []
expect(type.subtypes).to be_empty
end

it "returns [] for scalar types" do
it "returns an empty set for scalar types" do
type = schema.type_named("Int")
expect(type.subtypes).to eq []
expect(type.subtypes).to be_empty
end

it "returns the subtypes of a union" do
Expand Down Expand Up @@ -758,6 +758,104 @@ def search_index_definitions_from(type_name: "TheType")
end
end

describe "#source_type" do
Comment thread
marcdaniels-toast marked this conversation as resolved.
attr_reader :schema

before(:context) do
@schema = define_schema(clients_by_name: {}) do |s|
s.enum_type "Status" do |t|
t.value "ACTIVE"
t.value "INACTIVE"
end

s.object_type "Thing" do |t|
t.field "id", "ID!"
t.field "status", "Status"
t.index "things"
end
end
end

it "returns the underlying document type for an indexed aggregation type" do
expect(schema.type_named("ThingAggregation").source_type).to be schema.type_named("Thing")
end

it "returns nil for a non-derived object type" do
expect(schema.type_named("Thing").source_type).to be_nil
end

it "returns nil for a scalar type" do
expect(schema.type_named("Int").source_type).to be_nil
end

it "returns nil for an enum type" do
expect(schema.type_named("Status").source_type).to be_nil
end
end

describe "#concrete_non_subtypes_in_shared_index" do
attr_reader :schema

before(:context) do
Comment thread
marcdaniels-toast marked this conversation as resolved.
@schema = define_schema(clients_by_name: {}) do |s|
# A root interface with an index shared by all sub-hierarchies.
s.interface_type "Channel" do |t|
t.field "id", "ID!"
t.index "channels"
end

# Sibling types — NOT under Store, but share the Channel index.
s.object_type "Wholesaler" do |t|
t.implements "Channel"
t.field "id", "ID!"
end

s.object_type "Distributor" do |t|
t.implements "Channel"
t.field "id", "ID!"
end

# A sub-interface and its concrete subtypes.
s.interface_type "Store" do |t|
t.implements "Channel"
t.field "id", "ID!"
end

s.object_type "OnlineStore" do |t|
t.implements "Store"
t.field "id", "ID!"
end

# PhysicalStore overrides to use a dedicated index.
s.object_type "PhysicalStore" do |t|
t.implements "Store"
t.field "id", "ID!"
t.index "physical_stores"
end
end
end

it "excludes the type itself and its subtypes, returning only concrete sibling types in the shared index" do
expect(schema.type_named("Store").concrete_non_subtypes_in_shared_index).to contain_exactly(
schema.type_named("Wholesaler"),
schema.type_named("Distributor")
)
end

it "excludes the type itself even when the type is a concrete type in a shared index" do
# Wholesaler is a concrete type stored in the "channels" index, so it appears in
# document_types_stored_in("channels"). Without the `t == self` guard it would include itself.
expect(schema.type_named("Wholesaler").concrete_non_subtypes_in_shared_index).to contain_exactly(
schema.type_named("Distributor"),
schema.type_named("OnlineStore")
)
end
Comment thread
marcdaniels-toast marked this conversation as resolved.

it "returns an empty set when all types sharing its indexes are subtypes" do
expect(schema.type_named("Channel").concrete_non_subtypes_in_shared_index).to be_empty
end
end
Comment thread
marcdaniels-toast marked this conversation as resolved.

describe "#hidden_from_queries?" do
it "returns `false` on a type that has no backing indexed types" do
schema = define_schema do |s|
Expand Down
Loading