diff --git a/elasticgraph-indexer/lib/elastic_graph/indexer/record_preparer.rb b/elasticgraph-indexer/lib/elastic_graph/indexer/record_preparer.rb index 5c0d25b98..1b619dd09 100644 --- a/elasticgraph-indexer/lib/elastic_graph/indexer/record_preparer.rb +++ b/elasticgraph-indexer/lib/elastic_graph/indexer/record_preparer.rb @@ -21,10 +21,18 @@ def initialize(schema_artifacts) hash[type_name] = scalar_types_by_name[type_name]&.load_indexing_preparer&.extension_class end # : ::Hash[::String, SchemaArtifacts::RuntimeMetadata::extensionClass?] + # Derive the set of types that have __typename in their index mappings to supplement the set of types that + # explicitly require __typename in the JSON schema + mappings = schema_artifacts.index_mappings_by_index_def_name + types_with_typename_in_index_mapping = schema_artifacts.runtime_metadata.object_types_by_name.filter_map do |type_name, meta| + type_name if meta.index_definition_names.any? { |idx| mappings.dig(idx, "properties", "__typename") } + end.to_set + @preparers_by_json_schema_version = ::Hash.new do |hash, version| hash[version] = RecordPreparer.new( indexing_preparer_by_scalar_type_name, - build_type_metas_from(@schema_artifacts.json_schemas_for(version)) + build_type_metas_from(@schema_artifacts.json_schemas_for(version)), + types_with_typename_in_index_mapping ) end end @@ -81,7 +89,7 @@ def self.prepare_for_index(type_name, record) end end - def initialize(indexing_preparer_by_scalar_type_name, type_metas) + def initialize(indexing_preparer_by_scalar_type_name, type_metas, types_with_typename_in_index_mapping) @indexing_preparer_by_scalar_type_name = indexing_preparer_by_scalar_type_name @eg_meta_by_field_name_by_concrete_type = type_metas.to_h do |meta| [meta.name, meta.eg_meta_by_field_name] @@ -89,7 +97,7 @@ def initialize(indexing_preparer_by_scalar_type_name, type_metas) @types_requiring_typename = type_metas.filter_map do |meta| meta.name if meta.requires_typename - end.to_set + end.to_set | types_with_typename_in_index_mapping end # Prepares the given payload for being indexed into the named index. @@ -133,7 +141,7 @@ def prepare_value_for_indexing(value, type_name) # what the concrete subtype is. `__typename` is required on abstract types and indicates that. eg_meta_by_field_name = @eg_meta_by_field_name_by_concrete_type.fetch(value["__typename"] || type_name) - value.filter_map do |field_name, field_value| + prepared_fields = value.filter_map do |field_name, field_value| if field_name == "__typename" # We only want to include __typename if it we're dealing with a type that requires it. # (This is the case for an abstract type, so it can differentiate between which subtype we have @@ -142,6 +150,13 @@ def prepare_value_for_indexing(value, type_name) [eg_meta.fetch("nameInIndex"), prepare_value_for_indexing(field_value, eg_meta.fetch("type"))] end end.to_h + + # Add __typename if required but absent (e.g. for a mixed-type index). + if @types_requiring_typename.include?(type_name) && !value.key?("__typename") + prepared_fields["__typename"] = type_name + end + + prepared_fields else # We won't have a registered preparer for enum types, since those aren't dumped in # runtime metadata `scalar_types_by_name`, and we can just return the value as-is in diff --git a/elasticgraph-indexer/spec/unit/elastic_graph/indexer/record_preparer_spec.rb b/elasticgraph-indexer/spec/unit/elastic_graph/indexer/record_preparer_spec.rb index 63ea4ec1f..39b661cb5 100644 --- a/elasticgraph-indexer/spec/unit/elastic_graph/indexer/record_preparer_spec.rb +++ b/elasticgraph-indexer/spec/unit/elastic_graph/indexer/record_preparer_spec.rb @@ -238,6 +238,39 @@ class Indexer expect(record).to eq({"id" => "1", "size" => 3, "__typename" => "TypeB"}) end + it "includes __typename for types indexed only via union/interface, omits for directly indexed types" do + preparer = build_preparer do |s| + s.object_type "TypeA" do |t| + t.field "id", "ID!" + t.field "name", "String" + end + + s.object_type "TypeB" do |t| + t.field "id", "ID!" + t.field "size", "Int" + t.index "type_b" + end + + s.union_type "TypeAOrB" do |t| + t.subtype "TypeA" + t.subtype "TypeB" + t.index "type_a_or_b" + end + end + + # Input record for TypeA without __typename + type_a_record = preparer.prepare_for_index("TypeA", {"id" => "1", "name" => "test"}) + + # __typename should be injected since the mixed supertype index requires it + expect(type_a_record).to eq({"id" => "1", "name" => "test", "__typename" => "TypeA"}) + + # Input record for TypeB with __typename + type_b_record = preparer.prepare_for_index("TypeB", {"id" => "1", "size" => 3, "__typename" => "TypeB"}) + + # __typename should be omitted since type_b is directly indexed + expect(type_b_record).to eq({"id" => "1", "size" => 3}) + end + it "handles nested abstract types, properly including `__typename` on them" do preparer = build_preparer do |s| s.object_type "Person" do |t|