Skip to content

Commit f8d2c6b

Browse files
committed
Move record id preparation into the JSON ingestion adapter
- The indexer copied the envelope `id` into the record for every format. Only the JSON adapter knows that `record` is a Hash, so the copy belongs there. This removes the last format-specific `is_a?` check from the operation factory. - `ValidationResult` now carries the event to build operations from, so an adapter can return a prepared copy of the event. - Prepares for a typed `Event` whose `record` type is a per-format generic parameter. - Narrows `validation_result.record_preparer`/`.event` with `# : ...` type comments instead of `or raise`: both are always present together by construction once `failure` is nil, so the assertion can't fire. Generated with Claude Code
1 parent 90d8c13 commit f8d2c6b

7 files changed

Lines changed: 37 additions & 23 deletions

File tree

‎elasticgraph-indexer/lib/elastic_graph/indexer/ingestion_adapter.rb‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ module IngestionAdapter
1717
class Interface
1818
# Validates the given event and resolves the record preparer appropriate for the event's
1919
# ingestion schema version. The indexer selects this adapter from the event's format tag.
20+
# The indexer builds operations from the event in the returned result, so an adapter can
21+
# return a prepared copy of the event.
2022
#
2123
# @param event [Hash<String, Object>] an ElasticGraph indexing event
2224
# @param skip_record_validation [Boolean] whether to skip record validation; the event envelope must still be validated
2325
# @return [ValidationResult] the result of validating the event
2426
def validate_event(event, skip_record_validation: false)
2527
# simplecov:disable -- must return a result to satisfy Steep type checking but never called
26-
ValidationResult.valid(RecordPreparer::Identity)
28+
ValidationResult.valid(event, RecordPreparer::Identity)
2729
# simplecov:enable
2830
end
2931
end
@@ -39,19 +41,22 @@ def validate_event(event, skip_record_validation: false)
3941
# Returned by {Interface#validate_event}. A non-nil `failure` indicates an invalid event,
4042
# and a non-nil `record_preparer` indicates a valid event.
4143
#
44+
# @!attribute [r] event
45+
# @return [Hash<String, Object>, nil] the event to build operations from, when the event is valid
4246
# @!attribute [r] record_preparer
4347
# @return [Object, nil] preparer for the event's record, when the event is valid
4448
# @!attribute [r] failure
4549
# @return [Failure, nil] description of the validation problem, when the event is invalid
46-
ValidationResult = ::Data.define(:record_preparer, :failure) do
50+
ValidationResult = ::Data.define(:event, :record_preparer, :failure) do
4751
# @implements ValidationResult
4852

4953
# Builds a result for a valid event.
5054
#
55+
# @param event [Hash<String, Object>] the event to build operations from; an adapter may return a prepared copy
5156
# @param record_preparer [Object] preparer for the event's record
5257
# @return [ValidationResult]
53-
def self.valid(record_preparer)
54-
new(record_preparer: record_preparer, failure: nil)
58+
def self.valid(event, record_preparer)
59+
new(event: event, record_preparer: record_preparer, failure: nil)
5560
end
5661

5762
# Builds a result for an invalid event.
@@ -60,7 +65,7 @@ def self.valid(record_preparer)
6065
# @param message [String] detailed validation failure message
6166
# @return [ValidationResult]
6267
def self.invalid(validation_target:, message:)
63-
new(record_preparer: nil, failure: Failure.new(validation_target: validation_target, message: message))
68+
new(event: nil, record_preparer: nil, failure: Failure.new(validation_target: validation_target, message: message))
6469
end
6570
end
6671
end

‎elasticgraph-indexer/lib/elastic_graph/indexer/operation/factory.rb‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ class Factory < Support::MemoizableData.define(
2525
:skip_record_validation_percents_by_type
2626
)
2727
def build(event)
28-
event = prepare_event(event)
2928
format = event.fetch(INGESTION_FORMAT_KEY, "json")
3029
adapter = ingestion_adapters_by_format[format]
3130

@@ -50,7 +49,8 @@ def build_for_adapter(event, adapter)
5049
if (failure = validation_result.failure)
5150
build_failed_result(event, failure.validation_target, failure.message)
5251
else
53-
record_preparer = validation_result.record_preparer or raise
52+
record_preparer = validation_result.record_preparer # : _RecordPreparer
53+
event = validation_result.event # : event
5454
if skip_record_validation
5555
build_success_result_isolating_malformed_records(event, record_preparer, adapter)
5656
else
@@ -85,13 +85,6 @@ def build_success_result_isolating_malformed_records(event, record_preparer, ada
8585
build_failed_result(event, failure.validation_target, failure.message)
8686
end
8787

88-
# This copies the `id` from event into the actual record
89-
# This is necessary because we want to index `id` as part of the record so that the datastore will include `id` in returned search payloads.
90-
def prepare_event(event)
91-
return event unless event["record"].is_a?(::Hash) && event["id"]
92-
event.merge("record" => event["record"].merge("id" => event.fetch("id")))
93-
end
94-
9588
# `Zlib.crc32` returns a value in `[0, 2**32)`. Pre-dividing that space by 100 lets us test a
9689
# configured percent with a single multiply instead of dividing on every event.
9790
CRC32_SPACE_PER_PERCENT = (1 << 32) / 100.0

‎elasticgraph-indexer/sig/elastic_graph/indexer/ingestion_adapter.rbs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ module ElasticGraph
2222
end
2323

2424
class ValidationResultSupertype < ::Data
25+
attr_reader event: event?
2526
attr_reader record_preparer: _RecordPreparer?
2627
attr_reader failure: Failure?
2728

28-
def initialize: (record_preparer: _RecordPreparer?, failure: Failure?) -> void
29-
def self.new: (record_preparer: _RecordPreparer?, failure: Failure?) -> instance
29+
def initialize: (event: event?, record_preparer: _RecordPreparer?, failure: Failure?) -> void
30+
def self.new: (event: event?, record_preparer: _RecordPreparer?, failure: Failure?) -> instance
3031
def self.members: () -> ::Array[::Symbol]
3132
end
3233

3334
class ValidationResult < ValidationResultSupertype
34-
def self.valid: (_RecordPreparer) -> ValidationResult
35+
def self.valid: (event, _RecordPreparer) -> ValidationResult
3536
def self.invalid: (validation_target: ::String, message: ::String) -> ValidationResult
3637
end
3738
end

‎elasticgraph-indexer/sig/elastic_graph/indexer/operation/factory.rbs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ module ElasticGraph
3636
private
3737

3838
def build_for_adapter: (event, IngestionAdapter::_Adapter) -> BuildResult
39-
def prepare_event: (event) -> event
4039
def build_success_result: (event, _RecordPreparer, type_with_skipped_validation: ::String?) -> BuildResult
4140
def build_success_result_isolating_malformed_records: (event, _RecordPreparer, IngestionAdapter::_Adapter) -> BuildResult
4241
CRC32_SPACE_PER_PERCENT: ::Float

‎elasticgraph-indexer/spec/unit/elastic_graph/indexer/operation/factory_spec.rb‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,16 +470,19 @@ def factory_whose_record_preparation_is_broken
470470
end
471471
end
472472

473-
it "uses a registered non-JSON adapter for events in that alternate format" do
473+
it "uses a registered non-JSON adapter for events in that alternate format, building operations from the event the adapter returns" do
474474
event = build_upsert_event(:component, id: "1", __version: 1).merge(INGESTION_FORMAT_KEY => "other")
475+
prepared_event = event.merge("record" => event.fetch("record").merge("name" => "prepared by adapter"))
475476
other_adapter = instance_spy(
476477
IngestionAdapter::Interface,
477-
validate_event: IngestionAdapter::ValidationResult.valid(RecordPreparer::Identity)
478+
validate_event: IngestionAdapter::ValidationResult.valid(prepared_event, RecordPreparer::Identity)
478479
)
479480
factory = indexer.operation_factory.with(ingestion_adapters_by_format: {"other" => other_adapter})
480481

481-
expect(factory.build(event).operations).not_to be_empty
482+
operations = factory.build(event).operations
482483

484+
expect(operations.size).to eq 1
485+
expect(operations.map(&:event)).to eq [prepared_event]
483486
expect(other_adapter).to have_received(:validate_event).with(event, skip_record_validation: false)
484487
end
485488

‎elasticgraph-json_ingestion/lib/elastic_graph/json_ingestion/ingestion_adapter.rb‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ def validate_event(event, skip_record_validation: false)
5252
return ValidationResult.invalid(validation_target: "event payload", message: error_message)
5353
end
5454

55-
record = event.fetch("record")
55+
# The datastore includes `id` in search payloads only when it is part of the indexed record.
56+
record = event.fetch("record").merge("id" => event.fetch("id"))
5657
graphql_type_name = event.fetch("type")
5758

5859
if !skip_record_validation && (error_message = validator(graphql_type_name, selected_json_schema_version).validate_with_error_message(record))
5960
return ValidationResult.invalid(validation_target: "#{graphql_type_name} record", message: error_message)
6061
end
6162

62-
ValidationResult.valid(@record_preparer_factory.for_json_schema_version(selected_json_schema_version))
63+
ValidationResult.valid(event.merge("record" => record), @record_preparer_factory.for_json_schema_version(selected_json_schema_version))
6364
end
6465

6566
private

‎elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/ingestion_adapter_spec.rb‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ module JSONIngestion
2525
expect(result.operations.map(&:doc_id)).to include("1")
2626
end
2727

28+
it "copies the envelope `id` into the record so that the datastore includes `id` in search payloads" do
29+
event = build_upsert_event(:component, id: "1")
30+
event.fetch("record").delete("id")
31+
32+
result = indexer.operation_factory.build(event)
33+
34+
expect(result.failed_event_error).to be nil
35+
expect(result.operations.size).to eq 1
36+
expect(result.operations.map(&:prepared_record)).to all include("id" => "1")
37+
expect(event.fetch("record")).to exclude("id")
38+
end
39+
2840
# The factory replaces an adapter's missing preparer with an identity preparer while
2941
# building its failed result, so this adapter contract must be asserted directly.
3042
it "returns no record preparer, plus the part of the event at fault, for an event it rejects" do

0 commit comments

Comments
 (0)