Skip to content

Commit 3d1fc17

Browse files
authored
Merge pull request #49 from DIMO-Network/raw_event_id
Add raweventid as a CloudEvent "extras" field
2 parents 5baefe6 + 820bdff commit 3d1fc17

8 files changed

Lines changed: 173 additions & 6 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Each CloudEvent contains the following header fields:
8888
| DataSchema | URI pointing to a schema for the data field. |
8989
| DataVersion | An optional way for the data provider to specify the version of the data structure in the payload (e.g., "default/v1.0"). |
9090
| Signature | An optional cryptographic signature of the CloudEvent's data field for verification purposes. |
91+
| RawEventID | An optional identifier linking a parsed CloudEvent back to its raw image or PDF event. |
9192
| Tags | An optional list of tags that can be used to filter and categorize a cloudevents (currently these are only used by `dimo.attestation`). |
9293
| Extras | Additional custom fields. |
9394

@@ -96,6 +97,7 @@ The DIMO-specific extensions to the CloudEvents specification include:
9697
- `Producer`: Provides additional context about the specific instance, process, or device that created the event
9798
- `DataVersion`: A DIMO-specific extension that is unique to each source. This can be used by a source to determine the shape of the data field, enabling version-based data processing
9899
- `Signature`: An optional cryptographic signature field for verifying the integrity and authenticity of the CloudEvent's data
100+
- `RawEventID`: An optional identifier pointing from a parsed event to its raw source event
99101
- `Tags`: An optional list of tags for filtering and categorizing events, useful for event organization and query optimization
100102

101103
### Event Uniqueness

clickhouse/clickhouse_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ func TestAddNonColumnFieldsToExtras(t *testing.T) {
189189
SpecVersion: "1.0",
190190
DataSchema: "https://example.com/schema",
191191
Signature: "test-signature",
192+
RawEventID: "raw-event-123",
192193
Tags: []string{"tag1", "tag2"},
193194
}
194195

@@ -198,6 +199,7 @@ func TestAddNonColumnFieldsToExtras(t *testing.T) {
198199
assert.NotContains(t, extras, "specversion")
199200
assert.Equal(t, "https://example.com/schema", extras["dataschema"])
200201
assert.Equal(t, "test-signature", extras["signature"])
202+
assert.Equal(t, "raw-event-123", extras["raweventid"])
201203
assert.Equal(t, []string{"tag1", "tag2"}, extras["tags"])
202204
})
203205

@@ -207,6 +209,7 @@ func TestAddNonColumnFieldsToExtras(t *testing.T) {
207209
SpecVersion: "1.0",
208210
DataSchema: "https://example.com/schema",
209211
Signature: "test-signature",
212+
RawEventID: "raw-event-123",
210213
Tags: []string{"tag1", "tag2"},
211214
Extras: map[string]any{
212215
"existing": "value",
@@ -224,11 +227,13 @@ func TestAddNonColumnFieldsToExtras(t *testing.T) {
224227
assert.NotContains(t, extras, "specversion")
225228
assert.Equal(t, "https://example.com/schema", extras["dataschema"])
226229
assert.Equal(t, "test-signature", extras["signature"])
230+
assert.Equal(t, "raw-event-123", extras["raweventid"])
227231
assert.Equal(t, []string{"tag1", "tag2"}, extras["tags"])
228232

229233
// Verify original extras map is not modified
230234
assert.NotContains(t, event.Extras, "dataschema")
231235
assert.NotContains(t, event.Extras, "signature")
236+
assert.NotContains(t, event.Extras, "raweventid")
232237
assert.NotContains(t, event.Extras, "tags")
233238
})
234239

@@ -238,6 +243,7 @@ func TestAddNonColumnFieldsToExtras(t *testing.T) {
238243
SpecVersion: "", // zero value
239244
DataSchema: "", // zero value
240245
Signature: "", // zero value
246+
RawEventID: "", // zero value
241247
Tags: nil, // zero value
242248
}
243249

@@ -247,6 +253,7 @@ func TestAddNonColumnFieldsToExtras(t *testing.T) {
247253
assert.NotContains(t, extras, "specversion")
248254
assert.NotContains(t, extras, "dataschema")
249255
assert.NotContains(t, extras, "signature")
256+
assert.NotContains(t, extras, "raweventid")
250257
assert.NotContains(t, extras, "tags")
251258
})
252259

@@ -280,6 +287,7 @@ func TestRestoreNonColumnFields(t *testing.T) {
280287
assert.Equal(t, "1.0", event.SpecVersion)
281288
assert.Empty(t, event.DataSchema)
282289
assert.Empty(t, event.Signature)
290+
assert.Empty(t, event.RawEventID)
283291
assert.Nil(t, event.Tags)
284292
})
285293

@@ -295,6 +303,7 @@ func TestRestoreNonColumnFields(t *testing.T) {
295303
assert.Equal(t, "1.0", event.SpecVersion)
296304
assert.Empty(t, event.DataSchema)
297305
assert.Empty(t, event.Signature)
306+
assert.Empty(t, event.RawEventID)
298307
assert.Nil(t, event.Tags)
299308
})
300309

@@ -305,6 +314,7 @@ func TestRestoreNonColumnFields(t *testing.T) {
305314
"specversion": "1.0",
306315
"dataschema": "https://example.com/schema",
307316
"signature": "test-signature",
317+
"raweventid": "raw-event-123",
308318
"tags": []any{"tag1", "tag2"},
309319
"other": "should-remain",
310320
},
@@ -316,12 +326,14 @@ func TestRestoreNonColumnFields(t *testing.T) {
316326
assert.Equal(t, "1.0", event.SpecVersion)
317327
assert.Equal(t, "https://example.com/schema", event.DataSchema)
318328
assert.Equal(t, "test-signature", event.Signature)
329+
assert.Equal(t, "raw-event-123", event.RawEventID)
319330
assert.Equal(t, []string{"tag1", "tag2"}, event.Tags)
320331

321332
// Check that non-column fields are removed from extras
322333
assert.NotContains(t, event.Extras, "specversion")
323334
assert.NotContains(t, event.Extras, "dataschema")
324335
assert.NotContains(t, event.Extras, "signature")
336+
assert.NotContains(t, event.Extras, "raweventid")
325337
assert.NotContains(t, event.Extras, "tags")
326338
assert.Contains(t, event.Extras, "other") // other fields remain
327339
})
@@ -332,6 +344,7 @@ func TestRestoreNonColumnFields(t *testing.T) {
332344
Extras: map[string]any{
333345
"specversion": 123, // wrong type
334346
"dataschema": []int{}, // wrong type
347+
"raweventid": 999, // wrong type
335348
"tags": "not-a-slice", // wrong type
336349
},
337350
}
@@ -342,11 +355,13 @@ func TestRestoreNonColumnFields(t *testing.T) {
342355
// SpecVersion is always hardcoded to "1.0" regardless of extras
343356
assert.Equal(t, "1.0", event.SpecVersion)
344357
assert.Empty(t, event.DataSchema)
358+
assert.Empty(t, event.RawEventID)
345359
assert.Nil(t, event.Tags)
346360

347361
// Wrong-typed values should still be removed from extras for some fields
348362
assert.NotContains(t, event.Extras, "specversion")
349363
assert.NotContains(t, event.Extras, "dataschema")
364+
assert.NotContains(t, event.Extras, "raweventid")
350365
assert.NotContains(t, event.Extras, "tags")
351366
})
352367

@@ -355,7 +370,7 @@ func TestRestoreNonColumnFields(t *testing.T) {
355370
ID: "test-id",
356371
Extras: map[string]any{
357372
"specversion": "1.0",
358-
// missing dataschema, signature, tags
373+
// missing dataschema, signature, raweventid, tags
359374
"other": "value",
360375
},
361376
}
@@ -366,6 +381,7 @@ func TestRestoreNonColumnFields(t *testing.T) {
366381
assert.Equal(t, "1.0", event.SpecVersion)
367382
assert.Empty(t, event.DataSchema)
368383
assert.Empty(t, event.Signature)
384+
assert.Empty(t, event.RawEventID)
369385
assert.Nil(t, event.Tags)
370386

371387
// specversion should be removed, other should remain

cloudevent.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ type CloudEventHeader struct {
5656
// Signature hold the signature of the a cloudevent's data field.
5757
Signature string `json:"signature,omitempty"`
5858

59+
// RawEventID optionally links a parsed event to the ID of its backing raw event.
60+
RawEventID string `json:"raweventid,omitempty"`
61+
5962
// Tags are a list of tags that can be used to filter events.
6063
Tags []string `json:"tags,omitempty"`
6164

cloudevent_decoder.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
var knownHeaderFields = map[string]struct{}{
1313
"specversion": {}, "type": {}, "source": {}, "subject": {}, "id": {},
1414
"time": {}, "datacontenttype": {}, "dataschema": {}, "dataversion": {},
15-
"producer": {}, "signature": {}, "tags": {},
15+
"producer": {}, "signature": {}, "raweventid": {}, "tags": {},
1616
}
1717

1818
// unmarshalHeader parses CloudEvent JSON with gjson and returns the populated
@@ -34,6 +34,7 @@ func unmarshalHeader(data []byte) (CloudEventHeader, []byte, string, error) {
3434
header.DataSchema = result.Get("dataschema").Str
3535
header.DataVersion = result.Get("dataversion").Str
3636
header.Signature = result.Get("signature").Str
37+
header.RawEventID = result.Get("raweventid").Str
3738

3839
if tr := result.Get("time"); tr.Exists() {
3940
if tr.Type != gjson.String {

cloudevent_encoder.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ func (c *CloudEventHeader) marshalHeaderTo(buf *bytes.Buffer) error {
7070
if c.Signature != "" {
7171
writeStringField(buf, "signature", c.Signature)
7272
}
73+
if c.RawEventID != "" {
74+
writeStringField(buf, "raweventid", c.RawEventID)
75+
}
7376
if len(c.Tags) > 0 {
7477
buf.WriteString(`,"tags":[`)
7578
for i, tag := range c.Tags {

cloudevent_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,39 @@ func TestCloudEvent_MarshalJSON(t *testing.T) {
9090
}
9191
}`,
9292
},
93+
{
94+
name: "event with raw event id",
95+
event: cloudevent.CloudEvent[TestData]{
96+
CloudEventHeader: cloudevent.CloudEventHeader{
97+
ID: "789",
98+
Source: "test-source",
99+
Producer: "test-producer",
100+
SpecVersion: cloudevent.SpecVersion,
101+
Subject: "test-subject",
102+
Time: now,
103+
Type: cloudevent.TypeFingerprint,
104+
RawEventID: "raw-event-123",
105+
},
106+
Data: TestData{
107+
Message: "test",
108+
Count: 2,
109+
},
110+
},
111+
expected: `{
112+
"id": "789",
113+
"source": "test-source",
114+
"producer": "test-producer",
115+
"specversion": "1.0",
116+
"subject": "test-subject",
117+
"time": "` + now.Format(time.RFC3339Nano) + `",
118+
"type": "dimo.fingerprint",
119+
"raweventid": "raw-event-123",
120+
"data": {
121+
"message": "test",
122+
"count": 2
123+
}
124+
}`,
125+
},
93126
}
94127

95128
for _, tt := range tests {
@@ -184,6 +217,39 @@ func TestCloudEvent_UnmarshalJSON(t *testing.T) {
184217
},
185218
},
186219
},
220+
{
221+
name: "event with raw event id",
222+
json: `{
223+
"id": "789",
224+
"source": "test-source",
225+
"producer": "test-producer",
226+
"specversion": "1.0",
227+
"subject": "test-subject",
228+
"time": "` + now.Format(time.RFC3339Nano) + `",
229+
"type": "dimo.fingerprint",
230+
"raweventid": "raw-event-123",
231+
"data": {
232+
"message": "test",
233+
"count": 2
234+
}
235+
}`,
236+
expected: cloudevent.CloudEvent[TestData]{
237+
CloudEventHeader: cloudevent.CloudEventHeader{
238+
ID: "789",
239+
Source: "test-source",
240+
Producer: "test-producer",
241+
SpecVersion: cloudevent.SpecVersion,
242+
Subject: "test-subject",
243+
Time: now,
244+
Type: cloudevent.TypeFingerprint,
245+
RawEventID: "raw-event-123",
246+
},
247+
Data: TestData{
248+
Message: "test",
249+
Count: 2,
250+
},
251+
},
252+
},
187253
}
188254

189255
for _, tt := range tests {
@@ -253,6 +319,29 @@ func TestCloudEventHeader_MarshalJSON(t *testing.T) {
253319
"extra2": 123
254320
}`,
255321
},
322+
{
323+
name: "header with raw event id",
324+
header: cloudevent.CloudEventHeader{
325+
ID: "789",
326+
Source: "test-source",
327+
Producer: "test-producer",
328+
SpecVersion: cloudevent.SpecVersion,
329+
Subject: "test-subject",
330+
Time: now,
331+
Type: cloudevent.TypeFingerprint,
332+
RawEventID: "raw-event-123",
333+
},
334+
expected: `{
335+
"id": "789",
336+
"source": "test-source",
337+
"producer": "test-producer",
338+
"specversion": "1.0",
339+
"subject": "test-subject",
340+
"time": "` + now.Format(time.RFC3339Nano) + `",
341+
"type": "dimo.fingerprint",
342+
"raweventid": "raw-event-123"
343+
}`,
344+
},
256345
}
257346

258347
for _, tt := range tests {
@@ -333,6 +422,33 @@ func TestCloudEventHeader_UnmarshalJSON(t *testing.T) {
333422
},
334423
},
335424
},
425+
{
426+
name: "header with raw event id",
427+
json: `{
428+
"id": "789",
429+
"source": "test-source",
430+
"producer": "test-producer",
431+
"specversion": "1.0",
432+
"subject": "test-subject",
433+
"time": "` + now.Format(time.RFC3339Nano) + `",
434+
"type": "dimo.fingerprint",
435+
"raweventid": "raw-event-123",
436+
"extra1": "value1"
437+
}`,
438+
expected: cloudevent.CloudEventHeader{
439+
ID: "789",
440+
Source: "test-source",
441+
Producer: "test-producer",
442+
SpecVersion: cloudevent.SpecVersion,
443+
Subject: "test-subject",
444+
Time: now,
445+
Type: cloudevent.TypeFingerprint,
446+
RawEventID: "raw-event-123",
447+
Extras: map[string]any{
448+
"extra1": "value1",
449+
},
450+
},
451+
},
336452
}
337453

338454
for _, tt := range tests {

field_accessors.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ func RestoreNonColumnFields(event *CloudEventHeader) {
2121
}
2222
delete(event.Extras, "signature")
2323
}
24+
if val, ok := event.Extras["raweventid"]; ok {
25+
if typedVal, ok := val.(string); ok {
26+
event.RawEventID = typedVal
27+
}
28+
delete(event.Extras, "raweventid")
29+
}
2430
if val, ok := event.Extras["tags"]; ok {
2531
if anySlice, ok := val.([]any); ok {
2632
typedSlice := make([]string, len(anySlice))
@@ -36,7 +42,7 @@ func RestoreNonColumnFields(event *CloudEventHeader) {
3642
// AddNonColumnFieldsToExtras adds fields without dedicated columns to Extras.
3743
// Returns nil when there are no extras and no non-column fields to add.
3844
func AddNonColumnFieldsToExtras(event *CloudEventHeader) map[string]any {
39-
hasNonColumn := event.DataSchema != "" || event.Signature != "" || len(event.Tags) > 0
45+
hasNonColumn := event.DataSchema != "" || event.Signature != "" || event.RawEventID != "" || len(event.Tags) > 0
4046
if !hasNonColumn && len(event.Extras) == 0 {
4147
return nil
4248
}
@@ -52,6 +58,9 @@ func AddNonColumnFieldsToExtras(event *CloudEventHeader) map[string]any {
5258
if event.Signature != "" {
5359
extras["signature"] = event.Signature
5460
}
61+
if event.RawEventID != "" {
62+
extras["raweventid"] = event.RawEventID
63+
}
5564
if len(event.Tags) > 0 {
5665
extras["tags"] = event.Tags
5766
}

0 commit comments

Comments
 (0)