Skip to content

Commit ec0cb6b

Browse files
committed
Allow omitted JSON schema versions
1 parent 198a1cb commit ec0cb6b

6 files changed

Lines changed: 50 additions & 31 deletions

File tree

elasticgraph-indexer/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,7 @@ def decode(payload)
7474
# return an array of ElasticGraph indexing event hashes
7575
end
7676
```
77+
78+
Decoded event hashes do not need to provide a schema version. When a version is omitted, the latest
79+
available JSON schema artifact version is used for validation and record preparation. JSON ingestion
80+
payloads may still include `json_schema_version` to request a specific JSON schema version.

elasticgraph-indexer/lib/elastic_graph/indexer/indexing_event_decoder.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def initialize(config:, schema_artifacts:, logger:)
2424
end
2525

2626
# @param payload [String] a raw payload from the transport
27-
# @return [Array<Hash<String, Object>>] the decoded ElasticGraph indexing events
27+
# @return [Array<Hash<String, Object>>] the decoded ElasticGraph indexing events. Events do not
28+
# need to include a schema version; when omitted, the latest available JSON schema version is used.
2829
def decode(payload)
2930
# :nocov: -- must return an array to satisfy Steep type checking but never called
3031
[]

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ class Factory < Support::MemoizableData.define(
2828
def build(event)
2929
event = prepare_event(event)
3030

31-
selected_json_schema_version = select_json_schema_version(event) { |failure| return failure }
31+
requested_json_schema_version = json_schema_version_from(event)
32+
selected_json_schema_version = select_json_schema_version(event, requested_json_schema_version) { |failure| return failure }
33+
event = event.merge(JSON_SCHEMA_VERSION_KEY => requested_json_schema_version)
3234

33-
# Because the `select_json_schema_version` picks the closest-matching json schema version, the incoming
35+
# Because `select_json_schema_version` picks the closest-matching json schema version, the incoming
3436
# event might not match the expected json_schema_version value in the json schema (which is a `const` field).
3537
# This is by design, since we're picking a schema based on best-effort, so to avoid that by-design validation error,
3638
# performing the envelope validation on a "patched" version of the event.
@@ -49,15 +51,10 @@ def build(event)
4951

5052
private
5153

52-
def select_json_schema_version(event)
54+
def select_json_schema_version(event, requested_json_schema_version)
5355
available_json_schema_versions = schema_artifacts.available_json_schema_versions
5456

55-
requested_json_schema_version = event[JSON_SCHEMA_VERSION_KEY]
56-
57-
# First check that a valid value has been requested (a positive integer)
58-
if !event.key?(JSON_SCHEMA_VERSION_KEY)
59-
yield build_failed_result(event, JSON_SCHEMA_VERSION_KEY, "Event lacks a `#{JSON_SCHEMA_VERSION_KEY}`")
60-
elsif !requested_json_schema_version.is_a?(Integer) || requested_json_schema_version < 1
57+
unless requested_json_schema_version.is_a?(Integer) && requested_json_schema_version >= 1
6158
yield build_failed_result(event, JSON_SCHEMA_VERSION_KEY, "#{JSON_SCHEMA_VERSION_KEY} (#{requested_json_schema_version}) must be a positive integer.")
6259
end
6360

@@ -86,7 +83,7 @@ def select_json_schema_version(event)
8683
if selected_json_schema_version.nil?
8784
yield build_failed_result(
8885
event, JSON_SCHEMA_VERSION_KEY,
89-
"Failed to select json schema version. Requested version: #{event[JSON_SCHEMA_VERSION_KEY]}. \
86+
"Failed to select json schema version. Requested version: #{requested_json_schema_version}. \
9087
Available json schema versions: #{available_json_schema_versions.sort.join(", ")}"
9188
)
9289
end
@@ -117,6 +114,10 @@ def prepare_event(event)
117114
event.merge("record" => event["record"].merge("id" => event.fetch("id")))
118115
end
119116

117+
def json_schema_version_from(event)
118+
event.fetch(JSON_SCHEMA_VERSION_KEY) { schema_artifacts.available_json_schema_versions.max }
119+
end
120+
120121
def validate_record_returning_failure(event, selected_json_schema_version)
121122
record = event.fetch("record")
122123
graphql_type_name = event.fetch("type")

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ module ElasticGraph
4242
@validator_factories_by_version: ::Hash[::Integer, Support::JSONSchema::ValidatorFactory]?
4343
def validator_factories_by_version: () -> ::Hash[::Integer, Support::JSONSchema::ValidatorFactory]
4444

45-
def select_json_schema_version: (event) { (BuildResult) -> bot } -> (::Integer | bot)
45+
def select_json_schema_version: (event, untyped) { (BuildResult) -> bot } -> (::Integer | bot)
4646
def prepare_event: (event) -> event
47+
def json_schema_version_from: (event) -> untyped
4748
def validate_record_returning_failure: (event, ::Integer) -> BuildResult?
4849
def build_failed_result: (event, ::String, ::String) -> BuildResult
4950
def build_all_operations_for: (event, _RecordPreparer) -> ::Array[_Operation]

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,22 @@ module Operation
193193
expect_failed_event_error(event, "missing_keys", "version")
194194
end
195195

196-
it "notifies an error on missing `#{JSON_SCHEMA_VERSION_KEY}`" do
196+
it "defaults to the latest json schema version when no json schema version is specified" do
197197
event = build_upsert_event(:component).except(JSON_SCHEMA_VERSION_KEY)
198198

199-
expect_failed_event_error(event, JSON_SCHEMA_VERSION_KEY)
199+
expect(build_expecting_success(event)).to contain_exactly(new_primary_indexing_operation(
200+
event.merge(JSON_SCHEMA_VERSION_KEY => 1)
201+
))
202+
end
203+
204+
it "accepts json_schema_version as an input alias for JSON ingestion events" do
205+
event = build_upsert_event(:component, id: "1", __version: 1)
206+
.except(JSON_SCHEMA_VERSION_KEY)
207+
.merge(JSON_SCHEMA_VERSION_KEY => 1)
208+
209+
expect(build_expecting_success(event)).to eq([new_primary_indexing_operation(
210+
event.except(JSON_SCHEMA_VERSION_KEY).merge(JSON_SCHEMA_VERSION_KEY => 1)
211+
)])
200212
end
201213

202214
it "notifies an error on wrong field types" do
@@ -249,7 +261,7 @@ module Operation
249261
context "when the indexer has json schemas v2 and v4 (v4 adds yellow color)" do
250262
before do
251263
# With the "real" version one as a baseline, create a separate version with a small schema change.
252-
# Tests will then specify the desired json_schema_version in the event payload to test the schema-choosing
264+
# Tests will then specify the desired json schema version in the event payload to test the schema-choosing
253265
# behavior of the `factory` class.
254266
schemas = {
255267
2 => indexer.schema_artifacts.json_schemas_for(1),
@@ -269,7 +281,7 @@ module Operation
269281
end
270282

271283
it "validates against an older version of a json schema if specified" do
272-
# YELLOW doesn't exist in schema version 2. So expect an error when json_schema_version is set to 2.
284+
# YELLOW doesn't exist in json schema version 2. So expect an error when json_schema_version is set to 2.
273285
event = build_upsert_event(:widget, id: "1", __version: 1, __json_schema_version: 2)
274286
event["record"]["options"]["color"] = "YELLOW"
275287

@@ -355,13 +367,13 @@ module Operation
355367
)
356368
end
357369

358-
it "notifies an error if an invalid (e.g. negative) json_schema_version is specified" do
370+
it "notifies an error if an invalid (e.g. negative) json schema version is specified" do
359371
event = build_upsert_event(:widget, id: "1", __version: 1, __json_schema_version: -1)
360372

361373
expect_failed_event_error(event, "must be a positive integer", "(-1)")
362374
end
363375

364-
it "notifies an error if it's unable to select a json_schema_version" do
376+
it "notifies an error if it's unable to select a json schema version" do
365377
event = build_upsert_event(:component, id: "1", __version: 1)
366378
event["record"]["name"] = 123
367379

elasticgraph-warehouse_lambda/spec/unit/elastic_graph/warehouse_lambda/warehouse_dumper_spec.rb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,22 @@ class WarehouseLambda
3030
"type" => "Widget",
3131
"id" => "1",
3232
"version" => 3,
33-
"json_schema_version" => 1,
33+
JSON_SCHEMA_VERSION_KEY => 1,
3434
"record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}
3535
})
3636
end
3737

3838
it "writes operations to S3 as gzipped JSONL files and returns success results" do
39-
op1 = new_primary_indexing_operation({"type" => "Widget", "id" => "1", "version" => 3, "json_schema_version" => 1, "record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}})
40-
op2 = new_primary_indexing_operation({"type" => "Widget", "id" => "2", "version" => 5, "json_schema_version" => 2, "record" => {"id" => "2", "dayOfWeek" => "TUE", "created_at" => "2024-09-15T13:30:12Z", "workspace_id" => "ws-2"}})
39+
op1 = new_primary_indexing_operation({"type" => "Widget", "id" => "1", "version" => 3, JSON_SCHEMA_VERSION_KEY => 1, "record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}})
40+
op2 = new_primary_indexing_operation({"type" => "Widget", "id" => "2", "version" => 5, JSON_SCHEMA_VERSION_KEY => 2, "record" => {"id" => "2", "dayOfWeek" => "TUE", "created_at" => "2024-09-15T13:30:12Z", "workspace_id" => "ws-2"}})
4141
operations = [op1, op2]
4242

4343
results = warehouse_dumper.bulk(operations)
4444

45-
# Verify S3 uploads - should have 2 files (one for json_schema_version 1, one for json_schema_version 2)
45+
# Verify S3 uploads - should have 2 files (one for json schema version 1, one for json schema version 2)
4646
expect(s3_client.api_requests.map { |req| req[:operation_name] }).to eq [:put_object, :put_object]
4747

48-
# Verify first file (json_schema_version 1)
48+
# Verify first file (json schema version 1)
4949
params1 = s3_client.api_requests[0].fetch(:params)
5050
expect(params1[:bucket]).to eq s3_bucket_name
5151
expect(params1[:key]).to match %r{Data0001/Widget/v1/2024-09-15/[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\.jsonl\.gz}
@@ -61,7 +61,7 @@ class WarehouseLambda
6161
lines1 = jsonl_content1.split("\n")
6262
expect(lines1.size).to eq 1
6363

64-
# Verify second file (json_schema_version 2)
64+
# Verify second file (json schema version 2)
6565
params2 = s3_client.api_requests[1].fetch(:params)
6666
expect(params2[:key]).to match %r{Data0001/Widget/v2/2024-09-15/[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\.jsonl\.gz}
6767

@@ -99,8 +99,8 @@ class WarehouseLambda
9999
end
100100

101101
it "writes operations of different types to separate S3 files" do
102-
widget_op = new_primary_indexing_operation({"type" => "Widget", "id" => "1", "version" => 3, "json_schema_version" => 1, "record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}})
103-
component_op = new_primary_indexing_operation({"type" => "Component", "id" => "c1", "version" => 2, "json_schema_version" => 1, "record" => {"id" => "c1", "created_at" => "2024-09-15T12:30:12Z"}})
102+
widget_op = new_primary_indexing_operation({"type" => "Widget", "id" => "1", "version" => 3, JSON_SCHEMA_VERSION_KEY => 1, "record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}})
103+
component_op = new_primary_indexing_operation({"type" => "Component", "id" => "c1", "version" => 2, JSON_SCHEMA_VERSION_KEY => 1, "record" => {"id" => "c1", "created_at" => "2024-09-15T12:30:12Z"}})
104104
operations = [widget_op, component_op]
105105

106106
warehouse_dumper.bulk(operations)
@@ -113,8 +113,8 @@ class WarehouseLambda
113113
end
114114

115115
it "logs structured information about received batch and dumped files" do
116-
widget_op = new_primary_indexing_operation({"type" => "Widget", "id" => "1", "version" => 3, "json_schema_version" => 1, "record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}})
117-
component_op = new_primary_indexing_operation({"type" => "Component", "id" => "c1", "version" => 2, "json_schema_version" => 1, "record" => {"id" => "c1", "created_at" => "2024-09-15T12:30:12Z"}})
116+
widget_op = new_primary_indexing_operation({"type" => "Widget", "id" => "1", "version" => 3, JSON_SCHEMA_VERSION_KEY => 1, "record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}})
117+
component_op = new_primary_indexing_operation({"type" => "Component", "id" => "c1", "version" => 2, JSON_SCHEMA_VERSION_KEY => 1, "record" => {"id" => "c1", "created_at" => "2024-09-15T12:30:12Z"}})
118118
operations = [widget_op, component_op]
119119

120120
warehouse_dumper.bulk(operations)
@@ -127,13 +127,13 @@ class WarehouseLambda
127127
a_hash_including({
128128
"s3_bucket" => s3_bucket_name,
129129
"type" => "Widget",
130-
"json_schema_version" => 1,
130+
JSON_SCHEMA_VERSION_KEY => 1,
131131
"record_count" => 1
132132
}),
133133
a_hash_including({
134134
"s3_bucket" => s3_bucket_name,
135135
"type" => "Component",
136-
"json_schema_version" => 1,
136+
JSON_SCHEMA_VERSION_KEY => 1,
137137
"record_count" => 1
138138
})
139139
]
@@ -183,7 +183,7 @@ class WarehouseLambda
183183
"type" => "Widget",
184184
"id" => "1",
185185
"version" => 3,
186-
"json_schema_version" => 1,
186+
JSON_SCHEMA_VERSION_KEY => 1,
187187
"record" => {"id" => "1", "dayOfWeek" => "MON", "created_at" => "2024-09-15T12:30:12Z", "workspace_id" => "ws-1"}
188188
})
189189

0 commit comments

Comments
 (0)