diff --git a/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/api_extension.rb b/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/api_extension.rb index 4ac69fbd5..77f7b969d 100644 --- a/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/api_extension.rb +++ b/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/api_extension.rb @@ -336,6 +336,13 @@ def define_apollo_schema_elements end end + # Add @key directives to all root document types with an id field. + # This happens after schema definition is complete so root_document_type? sees the complete type hierarchy. + state.object_types_by_name.values + .grep(ElasticGraph::SchemaDefinition::SchemaElements::ObjectType) + .select { |object_type| object_type.root_document_type? && object_type.graphql_fields_by_name.key?("id") } + .each { |object_type| object_type.apollo_key fields: "id" } + entity_types = state.object_types_by_name.values.select do |object_type| object_type.directives.any? do |directive| directive.name == "key" && directive.arguments.fetch(:resolvable, true) diff --git a/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/entity_type_extension.rb b/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/entity_type_extension.rb index a6bb4a292..1e4d2aa12 100644 --- a/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/entity_type_extension.rb +++ b/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/entity_type_extension.rb @@ -9,23 +9,29 @@ module ElasticGraph module Apollo module SchemaDefinition - # The Apollo `_Entity` type is a type union of _all_ entity subtypes in an ElasticGraph schema. - # However, unlike a normal union type: - # - # - `_Entity` is never a root document type, and should not be treated as one (even though its subtypes are all root document types, - # which would usually cause it to be treated as a root document type!). - # - A merged set of `graphql_fields_by_name` cannot be safely computed. That method raises errors if a field with the same name - # has conflicting definitions on different subtypes, but we must allow that on `_Entity` subtypes. + # The Apollo `_Entity` type is a union of all entity types in an ElasticGraph schema. These overrides + # prevent ElasticGraph from treating `_Entity` like a normal indexed union type, which would trigger + # unwanted derived schema generation and validation. # # @private module EntityTypeExtension + # A merged set of `graphql_fields_by_name` cannot be safely computed. That method raises errors if a field with + # the same name has conflicting definitions on different subtypes, but we must allow that on `_Entity` subtypes. def graphql_fields_by_name {} end + # `_Entity` is never a root document type, and should not be treated as one (even though its subtypes are all + # root document types, which would usually cause it to be treated as a root document type!). def root_document_type? false end + + # `_Entity` is never directly queryable from the root `Query` type. It's queried via the apollo + # `_entities(representations: ...)` field instead. + def directly_queryable? + false + end end end end diff --git a/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/factory_extension.rb b/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/factory_extension.rb index 1d6e1e3ad..c51405584 100644 --- a/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/factory_extension.rb +++ b/elasticgraph-apollo/lib/elastic_graph/apollo/schema_definition/factory_extension.rb @@ -66,17 +66,12 @@ def new_interface_type(name) end end - # Here we override `object_type` in order to automatically add the apollo `@key` directive to indexed types. def new_object_type(name) super(name) do |raw_type| raw_type.extend ObjectTypeExtension type = raw_type # : ElasticGraph::SchemaDefinition::SchemaElements::ObjectType & ObjectTypeExtension yield type if block_given? - - if type.root_document_type? && type.graphql_fields_by_name.key?("id") - type.apollo_key fields: "id" - end end end diff --git a/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb b/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb index 865f52da4..dbd4f1ee6 100644 --- a/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb +++ b/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb @@ -291,24 +291,27 @@ def self.with_both_casing_forms(&block) it "avoids including indexed interfaces in the `_Entity` union (and does not add `@key` to it) since unions can't include interfaces" do schema_string = graphql_schema_string do |schema| + # Define subtypes before their indexed supertype to verify type references + # resolve correctly regardless of definition order. schema.object_type "IndexedType1" do |t| t.implements "NamedEntity" t.field "graphql", "String", name_in_index: "index" t.field "id", "ID!" t.field "name", "String" - t.index "index1" + # Inherits index from NamedEntity end schema.object_type "IndexedType2" do |t| t.implements "NamedEntity" t.field "id", "ID!" t.field "name", "String" - t.index "index1" + # Inherits index from NamedEntity end schema.interface_type "NamedEntity" do |t| t.field "id", "ID!" t.field "name", "String" + t.index "named_entities" end end diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb index ef326adaf..7c474f07a 100644 --- a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb @@ -53,12 +53,22 @@ class Schema t.field "name", "String" end + schema.object_type "Temperature" do |t| + t.field "id", "ID" + t.field "value", "Float" + end + + schema.object_type "Pressure" do |t| + t.field "id", "ID" + t.field "amount", "Int" + end + schema.union_type "Attribute" do |t| - t.subtypes "Color", "Velocity" + t.subtypes "Color" end schema.union_type "IndexedAttribute" do |t| - t.subtypes "Color", "Velocity" + t.subtypes "Temperature", "Pressure" t.index "attributes" end @@ -266,6 +276,26 @@ class Schema expect(type.unwrap_non_null).to be type end + it "can model a type that inherits an index from a union" do + # Temperature doesn't have its own index, but inherits from IndexedAttribute union + type = schema.type_named("Temperature") + + expect(type.name).to eq "Temperature" + expect(type).to only_satisfy_predicates(:nullable?, :object?, :indexed_document?) + expect(type.unwrap_fully).to be schema.type_named("Temperature") + expect(type.unwrap_non_null).to be type + end + + it "can model a type that inherits an index from an interface" do + # Velocity doesn't have its own index, but inherits from DirectlyIndexedInterface + type = schema.type_named("Velocity") + + expect(type.name).to eq "Velocity" + expect(type).to only_satisfy_predicates(:nullable?, :object?, :indexed_document?) + expect(type.unwrap_fully).to be schema.type_named("Velocity") + expect(type.unwrap_non_null).to be type + end + it "can model an indexed aggregation type" do type = type_for("indexed_aggregation") diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_indices.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_indices.rb index 0b4c51ec5..e47d59257 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_indices.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_indices.rb @@ -100,9 +100,39 @@ def has_own_index_def? !@own_index_def.nil? end - # @return [Boolean] true if this type is a root document type that lives at the document root in the datastore. - # For types with `own_index_def`, returns true. For abstract types with indexed subtypes, overridden in {HasSubtypes}. + # Resolves this type's index definition. This will be one of: + # - This type's own_index_def (if it directly defines an index) + # - An inherited index from an abstract supertype (union/interface) that has an index + # + # This type can be a subtype of multiple abstract types (e.g., implements multiple interfaces), but unless it + # defines its own index, at most one of its supertypes may have an index. If multiple parent types are indexed, + # this method raises an error to prevent ambiguity about which index to inherit. + # + # @return [Indexing::Index, nil] the index definition, or nil if this type has no index + # @raise [Errors::SchemaError] if this type is a subtype of multiple indexed abstract types + def index_def + return own_index_def if has_own_index_def? + + indexed_supertypes = recursively_resolve_supertypes.select(&:has_own_index_def?) + + if indexed_supertypes.size > 1 + parent_names = indexed_supertypes.map { |p| p.own_index_def.name }.join(", ") + raise Errors::SchemaError, + "The `#{name}` type is a subtype of multiple indexed abstract types (#{parent_names}). " \ + "If a concrete type does not define an index, it may not be a member of multiple indexed abstract types." + end + + indexed_supertypes.first&.own_index_def + end + + # @return [Boolean] true if this type is a root document type that lives at a document root in the datastore (is indexed). + # This returns true for types with their own index definition or types that inherit an index from a supertype. def root_document_type? + !index_def.nil? + end + + # @return [Boolean] true if this type is directly queryable via a type-specific field on the root `Query` type. + def directly_queryable? has_own_index_def? end @@ -191,7 +221,7 @@ def override_runtime_metadata(**overrides) def runtime_metadata(extra_update_targets) SchemaArtifacts::RuntimeMetadata::ObjectType.new( update_targets: derived_indexed_types.map(&:runtime_metadata_for_source_type) + [self_update_target].compact + extra_update_targets, - index_definition_names: [own_index_def&.name].compact, + index_definition_names: [index_def&.name].compact, graphql_fields_by_name: runtime_metadata_graphql_fields_by_name, elasticgraph_category: nil, source_type: nil, @@ -284,7 +314,7 @@ def self_update_target [field, SchemaArtifacts::RuntimeMetadata::DynamicParam.new(source_path: field, cardinality: :one)] end - index_runtime_metadata = own_index_def.runtime_metadata + index_runtime_metadata = index_def.runtime_metadata Indexing::UpdateTargetFactory.new_normal_indexing_update_target( type: name, diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb index b2502fdb4..b5db57997 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb @@ -38,6 +38,13 @@ def root_document_type? super || subtypes_are_root_document_types? end + # An abstract type is queryable if all of its subtypes are root document types (via a direct or inherited index) + # even if those subtypes aren't themselves directly queryable. This is why this doesn't delegate to a + # subtypes_are_directly_queryable helper. + def directly_queryable? + super || subtypes_are_root_document_types? + end + def recursively_resolve_subtypes resolve_subtypes.flat_map do |type| type.is_a?(HasSubtypes) ? (_ = type).recursively_resolve_subtypes : [type] diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb index 9e352da03..2edcf32c2 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb @@ -117,6 +117,21 @@ def to_sdl(&field_arg_selector) generate_sdl(name_section: name_section, &field_arg_selector) end + + # Returns all supertypes of this type, including union memberships and interface ancestors. + # + # @return [Set] set of supertypes + # @private + def recursively_resolve_supertypes + union_memberships = schema_def_state.union_types_by_member_ref[type_ref] + + interface_supertypes = implemented_interfaces.flat_map do |interface_ref| + interface = interface_ref.resolved + [interface] + interface.recursively_resolve_supertypes.to_a + end.to_set + + union_memberships | interface_supertypes + end end end end diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/results.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/results.rb index 238d0b44f..0bd9ad604 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/results.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/results.rb @@ -119,7 +119,7 @@ def define_root_graphql_type query_type.documentation "The query entry point for the entire schema." query_type.resolve_fields_with nil - state.types_by_name.values.select(&:root_document_type?).sort_by(&:name).each do |type| + state.object_types_by_name.values.select(&:directly_queryable?).sort_by(&:name).each do |type| # @type var root_doc_type: Mixins::HasIndices & _Type root_doc_type = _ = type @@ -212,7 +212,7 @@ def build_runtime_metadata enum_generator = state.factory.new_enums_for_root_document_types sort_order_enum_types_by_name = state.object_types_by_name.values - .select(&:root_document_type?) + .select(&:directly_queryable?) .filter_map { |type| enum_generator.sort_order_enum_for(_ = type) } .to_h { |enum_type| [(_ = enum_type).name, (_ = enum_type).runtime_metadata] } diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb index e413318ce..0bdb27b60 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb @@ -161,11 +161,6 @@ def to_indexing_field_type Indexing::FieldType::Enum.new(values_by_name.keys) end - # @return [false] enum types are never root document types - def root_document_type? - false - end - # @return [EnumType] converts the enum type to its input form for when different naming is used for input vs output enums. def as_input input_name = type_ref diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/object_type.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/object_type.rb index c920ab6bc..348260053 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/object_type.rb @@ -38,6 +38,16 @@ class ObjectType < DelegateClass(TypeWithSubfields) include Mixins::ImplementsInterfaces include Mixins::HasReadableToSAndInspect.new { |t| t.name } + # @return [Hash] fields that will be indexed, including __typename for mixed-type indices (types + # that inherit an index from an abstract supertype) + # @private + def indexing_fields_by_name_in_index + return super if has_own_index_def? + return super unless root_document_type? + + super.merge("__typename" => schema_def_state.factory.new_field(name: "__typename", type: "String", parent_type: self)) + end + # @private def initialize(schema_def_state, name) field_factory = schema_def_state.factory.method(:new_field) diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb index 2d63b30eb..8e2703cd2 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb @@ -236,11 +236,6 @@ def derived_graphql_types end end - # @private - def root_document_type? - false - end - private EQUAL_TO_ANY_OF_DOC = <<~EOS diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb index 3449a0f9c..eb45743ac 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb @@ -470,11 +470,6 @@ def aggregated_values_type schema_def_state.type_ref("NonNumeric").as_aggregated_values end - # @private - def root_document_type? - false - end - # @private def to_indexing_field_type Indexing::FieldType::Object.new( diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/union_type.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/union_type.rb index b36458af5..b8c4b4d1b 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/union_type.rb @@ -94,6 +94,9 @@ def subtype(name) end subtype_refs << type_ref + + # Register reverse lookup so we can efficiently find which unions contain this type + schema_def_state.union_types_by_member_ref[type_ref] << self end # Defines multiple subtypes of this union type. @@ -128,6 +131,15 @@ def to_sdl "#{formatted_documentation}union #{name} #{directives_sdl(suffix_with: " ")}= #{subtype_refs.map(&:name).to_a.join(" | ")}" end + # Union types cannot themselves be members of other unions or implement interfaces, + # so they have no supertypes. + # + # @return [Set] empty set + # @private + def recursively_resolve_supertypes + Set[] + end + # @private def verify_graphql_correctness! # Nothing to verify. `verify_graphql_correctness!` will be called on each subtype automatically. diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/state.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/state.rb index 9be0171db..b8c73eedc 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/state.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/state.rb @@ -32,6 +32,7 @@ class State < Struct.new( :scalar_types_by_name, :enum_types_by_name, :implementations_by_interface_ref, + :union_types_by_member_ref, :sdl_parts, :paginated_collection_element_types, :user_defined_fields, @@ -79,6 +80,7 @@ def self.with( scalar_types_by_name: {}, enum_types_by_name: {}, implementations_by_interface_ref: ::Hash.new { |h, k| h[k] = ::Set.new }, + union_types_by_member_ref: ::Hash.new { |h, k| h[k] = ::Set.new }, sdl_parts: [], paginated_collection_element_types: ::Set.new, user_defined_fields: ::Set.new, diff --git a/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/has_indices.rbs b/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/has_indices.rbs index e879f397e..5502ede0e 100644 --- a/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/has_indices.rbs +++ b/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/has_indices.rbs @@ -4,7 +4,9 @@ module ElasticGraph module HasIndices def own_index_def: () -> Indexing::Index? def has_own_index_def?: () -> bool + def index_def: () -> Indexing::Index? def root_document_type?: () -> bool + def directly_queryable?: () -> bool attr_reader runtime_metadata_overrides: ::Hash[::Symbol, untyped] attr_reader default_graphql_resolver: SchemaArtifacts::RuntimeMetadata::ConfiguredGraphQLResolver? def index: (::String, ::Hash[::Symbol, ::String | ::Integer]) ?{ (Indexing::Index) -> void } -> Indexing::Index diff --git a/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/implements_interfaces.rbs b/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/implements_interfaces.rbs index b733b437b..2245d1356 100644 --- a/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/implements_interfaces.rbs +++ b/elasticgraph-schema_definition/sig/elastic_graph/schema_definition/mixins/implements_interfaces.rbs @@ -5,6 +5,7 @@ module ElasticGraph def implements: (*::String) -> void attr_reader implemented_interfaces: ::Array[SchemaElements::TypeReference] def verify_graphql_correctness!: () -> void + def recursively_resolve_supertypes: () -> ::Set[SchemaElements::UnionType | SchemaElements::InterfaceType] end end end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/index_definition_names_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/index_definition_names_spec.rb index 98de85ffa..5bcfda7ac 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/index_definition_names_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/index_definition_names_spec.rb @@ -107,6 +107,30 @@ module SchemaDefinition expect(metadata.index_definition_names).to eq ["things"] end + it "allows concrete subtypes to inherit the index from the supertype" do + widget_metadata, component_metadata = object_type_metadata_for("Widget", "Component") do |s| + s.object_type "Widget" do |t| + t.field "id", "ID!" + t.field "name", "String" + link_subtype_to_supertype(t, "Thing") + end + + s.object_type "Component" do |t| + t.field "id", "ID!" + t.field "size", "Int" + link_subtype_to_supertype(t, "Thing") + end + + s.public_send type_def_method, "Thing" do |t| + link_supertype_to_subtypes(t, "Widget", "Component") + t.index "things" + end + end + + expect(widget_metadata.index_definition_names).to eq ["things"] + expect(component_metadata.index_definition_names).to eq ["things"] + end + it "does not dump any when no direct index is defined on it (even if the subtypes have indices)" do metadata = object_type_metadata_for "Thing" do |s| s.object_type "Widget" do |t| @@ -155,6 +179,77 @@ module SchemaDefinition end }.to raise_error(ElasticGraph::Errors::SchemaError, a_string_including("Cannot define an index on `Thing` after initialization is complete")) end + + it "raises an error when a concrete type without an index is a subtype of multiple indexed abstract types" do + expect { + object_type_metadata_for "Widget" do |s| + s.object_type "Widget" do |t| + t.field "id", "ID!" + link_subtype_to_supertype(t, "ThingA") + link_subtype_to_supertype(t, "ThingB") + end + + s.public_send type_def_method, "ThingA" do |t| + link_supertype_to_subtypes(t, "Widget") + t.index "things_a" + end + + s.public_send type_def_method, "ThingB" do |t| + link_supertype_to_subtypes(t, "Widget") + t.index "things_b" + end + end + }.to raise_error(ElasticGraph::Errors::SchemaError, a_string_including("The `Widget` type is a subtype of multiple indexed abstract types", "things_a, things_b")) + end + end + + context "with transitive interface inheritance" do + it "allows a concrete type to inherit an index from a grandparent interface" do + poodle_metadata = object_type_metadata_for("Poodle") do |s| + s.object_type "Poodle" do |t| + t.field "id", "ID!" + t.field "name", "String" + t.field "breed", "String" + t.implements "Dog" + end + + s.interface_type "Dog" do |t| + t.field "name", "String" + t.field "breed", "String" + t.implements "Animal" + end + + s.interface_type "Animal" do |t| + t.field "name", "String" + t.index "animals" + end + end + + expect(poodle_metadata.index_definition_names).to eq ["animals"] + end + + it "raises an error when a concrete type's interface chain includes multiple indexed interfaces" do + expect { + object_type_metadata_for("Poodle") do |s| + s.object_type "Poodle" do |t| + t.field "id", "ID!" + t.field "name", "String" + t.implements "Dog" + end + + s.interface_type "Dog" do |t| + t.field "name", "String" + t.implements "Animal" + t.index "dogs" + end + + s.interface_type "Animal" do |t| + t.field "name", "String" + t.index "animals" + end + end + }.to raise_error(ElasticGraph::Errors::SchemaError, a_string_including("The `Poodle` type is a subtype of multiple indexed abstract types", "dogs, animals")) + end end end end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/update_targets_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/update_targets_spec.rb index 018dde3d5..f79d67ea2 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/update_targets_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/object_types_by_name/update_targets_spec.rb @@ -1559,6 +1559,112 @@ def update_targets_for( expect(metadata.update_targets.first.data_params).to eq({"id" => dynamic_param_with(source_path: "id", cardinality: :many)}) expect(metadata.update_targets.first.metadata_params).to eq({}) end + + it "inherits routing configuration from the parent abstract type's index when the concrete type has no direct index" do + widget_metadata, component_metadata = object_type_metadata_for("Widget", "Component") do |s| + s.object_type "Widget" do |t| + t.field "id", "ID!" + t.field "workspace_id", "ID!" + link_subtype_to_supertype(t, "Thing") + end + + s.object_type "Component" do |t| + t.field "id", "ID!" + t.field "workspace_id", "ID!" + link_subtype_to_supertype(t, "Thing") + end + + s.public_send type_def_method, "Thing" do |t| + link_supertype_to_subtypes(t, "Widget", "Component") + t.index "things" do |i| + i.route_with "workspace_id" + end + end + end + + widget_target = widget_metadata.update_targets.find { |t| t.type == "Widget" } + expect(widget_target.routing_value_source).to eq("workspace_id") + + component_target = component_metadata.update_targets.find { |t| t.type == "Component" } + expect(component_target.routing_value_source).to eq("workspace_id") + end + + it "inherits rollover configuration from the parent abstract type's index when the concrete type has no direct index" do + widget_metadata, component_metadata = object_type_metadata_for("Widget", "Component") do |s| + s.object_type "Widget" do |t| + t.field "id", "ID!" + t.field "created_at", "DateTime!" + link_subtype_to_supertype(t, "Thing") + end + + s.object_type "Component" do |t| + t.field "id", "ID!" + t.field "created_at", "DateTime!" + link_subtype_to_supertype(t, "Thing") + end + + s.public_send type_def_method, "Thing" do |t| + link_supertype_to_subtypes(t, "Widget", "Component") + t.index "things" do |i| + i.rollover :monthly, "created_at" + end + end + end + + widget_target = widget_metadata.update_targets.find { |t| t.type == "Widget" } + expect(widget_target.rollover_timestamp_value_source).to eq("created_at") + + component_target = component_metadata.update_targets.find { |t| t.type == "Component" } + expect(component_target.rollover_timestamp_value_source).to eq("created_at") + end + + it "defaults to standard routing when the parent abstract type's index has no custom routing or rollover configuration" do + widget_metadata = object_type_metadata_for("Widget") do |s| + s.object_type "Widget" do |t| + t.field "id", "ID!" + t.field "name", "String" + link_subtype_to_supertype(t, "Thing") + end + + s.public_send type_def_method, "Thing" do |t| + link_supertype_to_subtypes(t, "Widget") + t.index "things" + end + end + + widget_target = widget_metadata.update_targets.find { |t| t.type == "Widget" } + expect(widget_target.routing_value_source).to eq("id") # defaults to id when no custom routing + expect(widget_target.rollover_timestamp_value_source).to be_nil # no default for rollover + end + + it "includes __typename in data_params for types that inherit an index (needed for field extraction during indexing)" do + widget_metadata, component_metadata = object_type_metadata_for("Widget", "Component") do |s| + s.object_type "Widget" do |t| + t.field "id", "ID!" + t.field "name", "String" + link_subtype_to_supertype(t, "Thing") + end + + s.object_type "Component" do |t| + t.field "id", "ID!" + t.field "size", "Int" + link_subtype_to_supertype(t, "Thing") + end + + s.public_send type_def_method, "Thing" do |t| + link_supertype_to_subtypes(t, "Widget", "Component") + t.index "things" + end + end + + widget_target = widget_metadata.update_targets.find { |t| t.type == "Widget" } + expect(widget_target.data_params.keys).to include("__typename") + expect(widget_target.data_params["__typename"].source_path).to eq "__typename" + + component_target = component_metadata.update_targets.find { |t| t.type == "Component" } + expect(component_target.data_params.keys).to include("__typename") + expect(component_target.data_params["__typename"].source_path).to eq "__typename" + end end def standard_metadata_params(relationship:)