diff --git a/events/dynamodb.go b/events/dynamodb.go index 66754c40..a9c80036 100644 --- a/events/dynamodb.go +++ b/events/dynamodb.go @@ -2,6 +2,11 @@ package events +import ( + "encoding/json" + "time" +) + // The DynamoDBEvent stream event handled to Lambda // http://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-ddb-update type DynamoDBEvent struct { @@ -84,6 +89,10 @@ type DynamoDBStreamRecord struct { // epoch time (http://www.epochconverter.com/) format. ApproximateCreationDateTime SecondsEpochTime `json:"ApproximateCreationDateTime,omitempty"` + // The precision of ApproximateCreationDateTime when the timestamp is not in + // seconds. + ApproximateCreationDateTimePrecision string `json:"ApproximateCreationDateTimePrecision,omitempty"` + // The primary key attribute(s) for the DynamoDB item that was modified. Keys map[string]DynamoDBAttributeValue `json:"Keys,omitempty"` @@ -104,6 +113,74 @@ type DynamoDBStreamRecord struct { StreamViewType string `json:"StreamViewType"` } +const ( + dynamoDBApproximateCreationDateTimePrecisionMillisecond = "MILLISECOND" + dynamoDBApproximateCreationDateTimePrecisionMicrosecond = "MICROSECOND" +) + +func (r DynamoDBStreamRecord) MarshalJSON() ([]byte, error) { + type dynamoDBStreamRecord DynamoDBStreamRecord + type dynamoDBStreamRecordJSON struct { + ApproximateCreationDateTime interface{} `json:"ApproximateCreationDateTime,omitempty"` + *dynamoDBStreamRecord + } + + record := dynamoDBStreamRecord(r) + v := dynamoDBStreamRecordJSON{ + ApproximateCreationDateTime: r.ApproximateCreationDateTime, + dynamoDBStreamRecord: &record, + } + + switch r.ApproximateCreationDateTimePrecision { + case dynamoDBApproximateCreationDateTimePrecisionMillisecond: + v.ApproximateCreationDateTime = r.ApproximateCreationDateTime.UnixNano() / int64(time.Millisecond) + case dynamoDBApproximateCreationDateTimePrecisionMicrosecond: + v.ApproximateCreationDateTime = r.ApproximateCreationDateTime.UnixNano() / int64(time.Microsecond) + } + + return json.Marshal(v) +} + +func (r *DynamoDBStreamRecord) UnmarshalJSON(data []byte) error { + type dynamoDBStreamRecord DynamoDBStreamRecord + type dynamoDBStreamRecordJSON struct { + ApproximateCreationDateTime json.RawMessage `json:"ApproximateCreationDateTime,omitempty"` + *dynamoDBStreamRecord + } + + record := dynamoDBStreamRecord(*r) + v := dynamoDBStreamRecordJSON{dynamoDBStreamRecord: &record} + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + *r = DynamoDBStreamRecord(record) + if len(v.ApproximateCreationDateTime) == 0 { + return nil + } + + switch r.ApproximateCreationDateTimePrecision { + case dynamoDBApproximateCreationDateTimePrecisionMillisecond: + var epoch int64 + if err := json.Unmarshal(v.ApproximateCreationDateTime, &epoch); err != nil { + return err + } + r.ApproximateCreationDateTime = SecondsEpochTime{time.Unix(0, epoch*int64(time.Millisecond))} + case dynamoDBApproximateCreationDateTimePrecisionMicrosecond: + var epoch int64 + if err := json.Unmarshal(v.ApproximateCreationDateTime, &epoch); err != nil { + return err + } + r.ApproximateCreationDateTime = SecondsEpochTime{time.Unix(0, epoch*int64(time.Microsecond))} + default: + if err := r.ApproximateCreationDateTime.UnmarshalJSON(v.ApproximateCreationDateTime); err != nil { + return err + } + } + + return nil +} + type DynamoDBKeyType string const ( diff --git a/events/dynamodb_test.go b/events/dynamodb_test.go index e364cb02..bb71f024 100644 --- a/events/dynamodb_test.go +++ b/events/dynamodb_test.go @@ -5,6 +5,7 @@ package events import ( "encoding/json" "testing" + "time" "github.com/aws/aws-lambda-go/events/test" "github.com/stretchr/testify/assert" @@ -35,6 +36,31 @@ func TestDynamoDBEventMarshalingMalformedJson(t *testing.T) { test.TestMalformedJson(t, DynamoDBEvent{}) } +func TestDynamoDBStreamRecordUnmarshalMicrosecondCreationDateTime(t *testing.T) { + inputJSON := []byte(`{ + "ApproximateCreationDateTime": 1731101300058336, + "ApproximateCreationDateTimePrecision": "MICROSECOND", + "SequenceNumber": "1", + "SizeBytes": 1, + "StreamViewType": "NEW_IMAGE" + }`) + + var inputRecord DynamoDBStreamRecord + if err := json.Unmarshal(inputJSON, &inputRecord); err != nil { + t.Errorf("could not unmarshal stream record. details: %v", err) + } + + creationDateTime := inputRecord.ApproximateCreationDateTime + assert.Equal(t, time.Date(2024, time.November, 8, 21, 28, 20, 58336000, time.UTC), creationDateTime.UTC()) + assert.Equal(t, "MICROSECOND", inputRecord.ApproximateCreationDateTimePrecision) + + outputJSON, err := json.Marshal(inputRecord) + if err != nil { + t.Errorf("could not marshal stream record. details: %v", err) + } + assert.JSONEq(t, string(inputJSON), string(outputJSON)) +} + func TestDynamoDBTimeWindowEventMarshaling(t *testing.T) { // 1. read JSON from file inputJSON := test.ReadJSONFromFile(t, "./testdata/dynamodb-time-window-event.json")