Skip to content

Commit 92686c2

Browse files
committed
feat(lambda): add starter for DDB stream event
1 parent 435d270 commit 92686c2

4 files changed

Lines changed: 166 additions & 12 deletions

File tree

lambda/ddb.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package lambda
2+
3+
import (
4+
"context"
5+
6+
"github.com/aws/aws-lambda-go/events"
7+
"github.com/aws/aws-lambda-go/lambda"
8+
dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
9+
"github.com/nguyengg/go-aws-commons/metrics"
10+
)
11+
12+
// StartDynamodbEventHandler starts the Lambda loop for handling DynamoDB stream events in a batch.
13+
//
14+
// See https://docs.aws.amazon.com/lambda/latest/dg/services-ddb-batchfailurereporting.html.
15+
//
16+
// The handler works on one record at a time and sequentially. If the handler returns a non-nil error, the wrapper will
17+
// automatically add an events.DynamoDBBatchItemFailure so that only failed records get retried later.
18+
func StartDynamodbEventHandler(handler func(context.Context, events.DynamoDBEventRecord) error, options ...lambda.Option) {
19+
StartHandlerFunc(func(ctx context.Context, req events.DynamoDBEvent) (events.DynamoDBEventResponse, error) {
20+
m := metrics.Get(ctx)
21+
22+
res := events.DynamoDBEventResponse{
23+
BatchItemFailures: make([]events.DynamoDBBatchItemFailure, 0),
24+
}
25+
26+
m.AddCounter("recordCount", int64(len(req.Records)), "failureCount")
27+
28+
for _, record := range req.Records {
29+
if err := handler(ctx, record); err != nil {
30+
m.AddCounter("failureCount", 1)
31+
res.BatchItemFailures = append(res.BatchItemFailures, events.DynamoDBBatchItemFailure{ItemIdentifier: record.Change.SequenceNumber})
32+
}
33+
}
34+
35+
// very important that nil error is returned here.
36+
return res, nil
37+
}, options...)
38+
}
39+
40+
// StreamToDynamoDBAttributeValue converts a DynamoDB Stream event attribute value (from
41+
// https://pkg.go.dev/github.com/aws/aws-lambda-go/events) to an equivalent DynamoDB attribute value (from
42+
// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/dynamodb/types).
43+
//
44+
// See StreamToDynamoDBItem for usage.
45+
func StreamToDynamoDBAttributeValue(av events.DynamoDBAttributeValue) dynamodbtypes.AttributeValue {
46+
// TODO as an exercise, remove recursion.
47+
48+
switch av.DataType() {
49+
case events.DataTypeBinary:
50+
return &dynamodbtypes.AttributeValueMemberB{Value: av.Binary()}
51+
case events.DataTypeBoolean:
52+
return &dynamodbtypes.AttributeValueMemberBOOL{Value: av.Boolean()}
53+
case events.DataTypeBinarySet:
54+
return &dynamodbtypes.AttributeValueMemberBS{Value: av.BinarySet()}
55+
case events.DataTypeList:
56+
l := av.List()
57+
value := make([]dynamodbtypes.AttributeValue, len(l))
58+
for i, v := range l {
59+
value[i] = StreamToDynamoDBAttributeValue(v)
60+
}
61+
return &dynamodbtypes.AttributeValueMemberL{Value: value}
62+
case events.DataTypeMap:
63+
value := make(map[string]dynamodbtypes.AttributeValue)
64+
for k, v := range av.Map() {
65+
value[k] = StreamToDynamoDBAttributeValue(v)
66+
}
67+
return &dynamodbtypes.AttributeValueMemberM{Value: value}
68+
case events.DataTypeNumber:
69+
return &dynamodbtypes.AttributeValueMemberN{Value: av.Number()}
70+
case events.DataTypeNumberSet:
71+
return &dynamodbtypes.AttributeValueMemberNS{Value: av.NumberSet()}
72+
case events.DataTypeNull:
73+
return &dynamodbtypes.AttributeValueMemberNULL{Value: av.IsNull()}
74+
case events.DataTypeString:
75+
return &dynamodbtypes.AttributeValueMemberS{Value: av.String()}
76+
case events.DataTypeStringSet:
77+
return &dynamodbtypes.AttributeValueMemberSS{Value: av.StringSet()}
78+
default:
79+
// should panic?
80+
return nil
81+
}
82+
}
83+
84+
// StreamToDynamoDBItem uses StreamToDynamoDBAttributeValue to convert an item from a DynamoDB Stream event to an item
85+
// in DynamoDB.
86+
//
87+
// Useful if you're implementing a DynamoDB Stream event handler, and you need to convert the old and/or new image to
88+
// the tagged struct by way of https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue:
89+
//
90+
// item := &MyStruct{}
91+
// err := attributevalue.UnmarshalMap(StreamToDynamoDBItem(record.Change.NewImage), item)
92+
func StreamToDynamoDBItem(in map[string]events.DynamoDBAttributeValue) map[string]dynamodbtypes.AttributeValue {
93+
out := make(map[string]dynamodbtypes.AttributeValue)
94+
for k, v := range in {
95+
out[k] = StreamToDynamoDBAttributeValue(v)
96+
}
97+
return out
98+
}

lambda/ddb_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package lambda
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/aws/aws-lambda-go/events"
8+
dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
9+
)
10+
11+
func TestStreamToDynamoDBItem_success(t *testing.T) {
12+
type args struct {
13+
item map[string]events.DynamoDBAttributeValue
14+
}
15+
tests := []struct {
16+
name string
17+
args args
18+
want map[string]dynamodbtypes.AttributeValue
19+
}{
20+
{
21+
name: "Basic test",
22+
args: args{
23+
item: map[string]events.DynamoDBAttributeValue{
24+
"version": events.NewNumberAttribute("123"),
25+
"hello": events.NewStringAttribute("world"),
26+
"numberSet": events.NewNumberSetAttribute([]string{"45", "67"}),
27+
"stringSet": events.NewStringSetAttribute([]string{"hello", "world"}),
28+
"list": events.NewListAttribute([]events.DynamoDBAttributeValue{
29+
events.NewNumberAttribute("12"),
30+
events.NewStringAttribute("34"),
31+
}),
32+
},
33+
},
34+
want: map[string]dynamodbtypes.AttributeValue{
35+
"version": &dynamodbtypes.AttributeValueMemberN{Value: "123"},
36+
"hello": &dynamodbtypes.AttributeValueMemberS{Value: "world"},
37+
"numberSet": &dynamodbtypes.AttributeValueMemberNS{Value: []string{"45", "67"}},
38+
"stringSet": &dynamodbtypes.AttributeValueMemberSS{Value: []string{"hello", "world"}},
39+
"list": &dynamodbtypes.AttributeValueMemberL{Value: []dynamodbtypes.AttributeValue{
40+
&dynamodbtypes.AttributeValueMemberN{Value: "12"},
41+
&dynamodbtypes.AttributeValueMemberS{Value: "34"},
42+
}},
43+
},
44+
},
45+
}
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
if got := StreamToDynamoDBItem(tt.args.item); !reflect.DeepEqual(got, tt.want) {
49+
t.Errorf("StreamToDynamoDBItem() = %v, want %v", got, tt.want)
50+
}
51+
})
52+
}
53+
}

lambda/go.mod

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ go 1.25
44

55
require (
66
github.com/aws/aws-lambda-go v1.47.0
7-
github.com/aws/aws-sdk-go-v2 v1.41.1
7+
github.com/aws/aws-sdk-go-v2 v1.41.2
8+
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0
89
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.18
910
github.com/aws/aws-sdk-go-v2/service/ssm v1.56.12
1011
github.com/nguyengg/go-aws-commons/metrics v0.5.4
@@ -16,10 +17,10 @@ require (
1617
github.com/aws/aws-sdk-go-v2/config v1.32.9 // indirect
1718
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 // indirect
1819
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
19-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
20-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
20+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
21+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
2122
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
22-
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
23+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
2324
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
2425
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
2526
github.com/aws/aws-sdk-go-v2/service/sso v1.30.10 // indirect

lambda/go.sum

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
github.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI=
22
github.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
3-
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
4-
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
3+
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
4+
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
55
github.com/aws/aws-sdk-go-v2/config v1.32.9 h1:ktda/mtAydeObvJXlHzyGpK1xcsLaP16zfUPDGoW90A=
66
github.com/aws/aws-sdk-go-v2/config v1.32.9/go.mod h1:U+fCQ+9QKsLW786BCfEjYRj34VVTbPdsLP3CHSYXMOI=
77
github.com/aws/aws-sdk-go-v2/credentials v1.19.9 h1:sWvTKsyrMlJGEuj/WgrwilpoJ6Xa1+KhIpGdzw7mMU8=
88
github.com/aws/aws-sdk-go-v2/credentials v1.19.9/go.mod h1:+J44MBhmfVY/lETFiKI+klz0Vym2aCmIjqgClMmW82w=
99
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
1010
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
11-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
12-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
13-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
14-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
11+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
12+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
13+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
14+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
1515
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
1616
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
17-
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
18-
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
17+
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0 h1:n5BubZVgbYyweQmdqMT+HMhH07wCxmMyBAQy/VhinoU=
18+
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0/go.mod h1:IFMlDGLL3eM098XqgRk27wateJOnrzp7zz93Wh/F9qk=
19+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
20+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
1921
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
2022
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
2123
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.18 h1:U/gg5eOAPx9vzip9A6cQ2GkIAPBthHMaKDfZ/WWEuj0=

0 commit comments

Comments
 (0)