Skip to content

Commit 710045b

Browse files
authored
Merge pull request #143 from nyaruka/dynamo_get_put
Add basic get/put operations to dynamo service
2 parents 2d72c4b + 955c31f commit 710045b

File tree

5 files changed

+185
-21
lines changed

5 files changed

+185
-21
lines changed

aws/dynamo/marshal.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package dynamo
2+
3+
import (
4+
"bytes"
5+
"compress/gzip"
6+
"encoding/json"
7+
"fmt"
8+
9+
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
10+
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
11+
)
12+
13+
type Marshaler interface {
14+
MarshalDynamo() (map[string]types.AttributeValue, error)
15+
}
16+
17+
type Unmarshaler interface {
18+
UnmarshalDynamo(map[string]types.AttributeValue) error
19+
}
20+
21+
func marshal(v any) (map[string]types.AttributeValue, error) {
22+
marshaler, ok := v.(Marshaler)
23+
if ok {
24+
return marshaler.MarshalDynamo()
25+
}
26+
27+
return attributevalue.MarshalMap(v)
28+
}
29+
30+
func unmarshal(m map[string]types.AttributeValue, v any) error {
31+
unmarshaler, ok := v.(Unmarshaler)
32+
if ok {
33+
return unmarshaler.UnmarshalDynamo(m)
34+
}
35+
36+
return attributevalue.UnmarshalMap(m, v)
37+
}
38+
39+
func MarshalJSONGZ(v any) ([]byte, error) {
40+
buf := &bytes.Buffer{}
41+
w := gzip.NewWriter(buf)
42+
43+
if err := json.NewEncoder(w).Encode(v); err != nil {
44+
return nil, fmt.Errorf("error encoding value as json+gzip: %w", err)
45+
}
46+
47+
w.Close()
48+
49+
return buf.Bytes(), nil
50+
}
51+
52+
func UnmarshalJSONGZ(d []byte, v any) error {
53+
r, err := gzip.NewReader(bytes.NewReader(d))
54+
if err != nil {
55+
return err
56+
}
57+
58+
if err := json.NewDecoder(r).Decode(v); err != nil {
59+
return fmt.Errorf("error decoding value from json+gzip: %w", err)
60+
}
61+
62+
return nil
63+
}

aws/dynamo/service.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package dynamo
22

33
import (
44
"context"
5+
"fmt"
56

67
"github.com/aws/aws-sdk-go-v2/aws"
78
"github.com/aws/aws-sdk-go-v2/config"
89
"github.com/aws/aws-sdk-go-v2/credentials"
910
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
11+
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
1012
)
1113

1214
// Service is simple abstraction layer to work with a DynamoDB-compatible database
@@ -39,11 +41,48 @@ func NewService(accessKey, secretKey, region, endpoint, tablePrefix string) (*Se
3941
return &Service{Client: client, tablePrefix: tablePrefix}, nil
4042
}
4143

42-
func (s *Service) Test(ctx context.Context, table string) error {
43-
_, err := s.Client.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: aws.String(s.TableName(table))})
44+
// Test checks if the service is working by trying to list tables
45+
func (s *Service) Test(ctx context.Context) error {
46+
_, err := s.Client.ListTables(ctx, &dynamodb.ListTablesInput{})
4447
return err
4548
}
4649

50+
// TableName returns the full table name with the prefix
4751
func (s *Service) TableName(base string) string {
4852
return s.tablePrefix + base
4953
}
54+
55+
// GetItem retrieves an item from the given table
56+
func (s *Service) GetItem(ctx context.Context, table string, key map[string]types.AttributeValue, dst any) error {
57+
resp, err := s.Client.GetItem(ctx, &dynamodb.GetItemInput{
58+
TableName: aws.String(s.TableName(table)),
59+
Key: key,
60+
})
61+
if err != nil {
62+
return fmt.Errorf("error getting item from dynamo: %w", err)
63+
}
64+
65+
if err := unmarshal(resp.Item, dst); err != nil {
66+
return fmt.Errorf("error unmarshaling dynamo item: %w", err)
67+
}
68+
69+
return nil
70+
}
71+
72+
// PutItem puts an item into the given table
73+
func (s *Service) PutItem(ctx context.Context, table string, v any) error {
74+
item, err := marshal(v)
75+
if err != nil {
76+
return fmt.Errorf("error marshaling dynamo item: %w", err)
77+
}
78+
79+
_, err = s.Client.PutItem(ctx, &dynamodb.PutItemInput{
80+
TableName: aws.String(s.TableName(table)),
81+
Item: item,
82+
})
83+
if err != nil {
84+
return fmt.Errorf("error putting item to dynamo: %w", err)
85+
}
86+
87+
return nil
88+
}

aws/dynamo/service_test.go

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,72 @@ package dynamo_test
22

33
import (
44
"context"
5+
"fmt"
56
"testing"
67

78
"github.com/aws/aws-sdk-go-v2/aws"
9+
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
810
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
911
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
1012
"github.com/nyaruka/gocommon/aws/dynamo"
13+
"github.com/nyaruka/gocommon/uuids"
1114
"github.com/stretchr/testify/assert"
1215
)
1316

17+
type Thing struct {
18+
uuid uuids.UUID
19+
name string
20+
extra map[string]any
21+
}
22+
23+
type dyThing struct {
24+
UUID uuids.UUID `dynamodbav:"UUID"`
25+
Name string `dynamodbav:"Name"`
26+
Extra []byte `dynamodbav:"Extra"`
27+
}
28+
29+
func (t *Thing) MarshalDynamo() (map[string]types.AttributeValue, error) {
30+
e, err := dynamo.MarshalJSONGZ(t.extra)
31+
if err != nil {
32+
return nil, fmt.Errorf("error marshaling extra: %w", err)
33+
}
34+
35+
d := dyThing{UUID: t.uuid, Name: t.name, Extra: e}
36+
37+
return attributevalue.MarshalMap(d)
38+
}
39+
40+
func (t *Thing) UnmarshalDynamo(m map[string]types.AttributeValue) error {
41+
d := &dyThing{}
42+
43+
if err := attributevalue.UnmarshalMap(m, d); err != nil {
44+
return fmt.Errorf("error unmarshaling thing: %w", err)
45+
}
46+
47+
t.uuid = d.UUID
48+
t.name = d.Name
49+
50+
if err := dynamo.UnmarshalJSONGZ(d.Extra, &t.extra); err != nil {
51+
return fmt.Errorf("error unmarshaling extra: %w", err)
52+
}
53+
54+
return nil
55+
}
56+
1457
func TestService(t *testing.T) {
1558
ctx := context.Background()
1659

17-
svc, err := dynamo.NewService("root", "tembatemba", "us-east-1", "http://localhost:6000", "Test")
60+
svc, err := dynamo.NewService("root", "badkey", "us-east-1", "http://localhost:6666", "Test")
61+
assert.NoError(t, err)
62+
63+
err = svc.Test(ctx)
64+
assert.ErrorContains(t, err, "exceeded maximum number of attempts, 3")
65+
66+
svc, err = dynamo.NewService("root", "tembatemba", "us-east-1", "http://localhost:6000", "Test")
1867
assert.NoError(t, err)
1968

20-
err = svc.Test(ctx, "Things")
21-
assert.ErrorContains(t, err, "ResourceNotFoundException")
69+
err = svc.Test(ctx)
70+
assert.NoError(t, err)
2271

2372
_, err = svc.Client.CreateTable(ctx, &dynamodb.CreateTableInput{
2473
TableName: aws.String("TestThings"),
@@ -32,8 +81,15 @@ func TestService(t *testing.T) {
3281
})
3382
assert.NoError(t, err)
3483

35-
err = svc.Test(ctx, "Things")
84+
thing1 := &Thing{uuid: "9142d9d2-bbc3-4412-b0d5-25c729c4f231", name: "One", extra: map[string]any{"foo": "bar"}}
85+
86+
err = svc.PutItem(ctx, "Things", thing1)
87+
assert.NoError(t, err)
88+
89+
thing2 := &Thing{}
90+
err = svc.GetItem(ctx, "Things", map[string]types.AttributeValue{"UUID": &types.AttributeValueMemberS{Value: "9142d9d2-bbc3-4412-b0d5-25c729c4f231"}}, thing2)
3691
assert.NoError(t, err)
92+
assert.Equal(t, thing1, thing2)
3793

3894
_, err = svc.Client.DeleteTable(ctx, &dynamodb.DeleteTableInput{TableName: aws.String("TestThings")})
3995
assert.NoError(t, err)

go.mod

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ module github.com/nyaruka/gocommon
33
go 1.22
44

55
require (
6-
github.com/aws/aws-sdk-go-v2 v1.30.4
6+
github.com/aws/aws-sdk-go-v2 v1.30.5
77
github.com/aws/aws-sdk-go-v2/config v1.27.28
88
github.com/aws/aws-sdk-go-v2/credentials v1.17.28
9-
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.5
9+
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.3
10+
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.9
1011
github.com/aws/aws-sdk-go-v2/service/s3 v1.60.0
1112
github.com/gabriel-vasile/mimetype v1.4.5
1213
github.com/go-chi/chi/v5 v5.1.0
@@ -30,13 +31,14 @@ require (
3031
require (
3132
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect
3233
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 // indirect
33-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 // indirect
34-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 // indirect
34+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
35+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
3536
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
3637
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 // indirect
38+
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.7 // indirect
3739
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
3840
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 // indirect
39-
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.17 // indirect
41+
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.18 // indirect
4042
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 // indirect
4143
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 // indirect
4244
github.com/aws/aws-sdk-go-v2/service/sso v1.22.5 // indirect

go.sum

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,35 @@
11
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
22
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3-
github.com/aws/aws-sdk-go-v2 v1.30.4 h1:frhcagrVNrzmT95RJImMHgabt99vkXGslubDaDagTk8=
4-
github.com/aws/aws-sdk-go-v2 v1.30.4/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
3+
github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g=
4+
github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0=
55
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 h1:70PVAiL15/aBMh5LThwgXdSQorVr91L127ttckI9QQU=
66
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4/go.mod h1:/MQxMqci8tlqDH+pjmoLu1i0tbWCUP1hhyMRuFxpQCw=
77
github.com/aws/aws-sdk-go-v2/config v1.27.28 h1:OTxWGW/91C61QlneCtnD62NLb4W616/NM1jA8LhJqbg=
88
github.com/aws/aws-sdk-go-v2/config v1.27.28/go.mod h1:uzVRVtJSU5EFv6Fu82AoVFKozJi2ZCY6WRCXj06rbvs=
99
github.com/aws/aws-sdk-go-v2/credentials v1.17.28 h1:m8+AHY/ND8CMHJnPoH7PJIRakWGa4gbfbxuY9TGTUXM=
1010
github.com/aws/aws-sdk-go-v2/credentials v1.17.28/go.mod h1:6TF7dSc78ehD1SL6KpRIPKMA1GyyWflIkjqg+qmf4+c=
11+
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.3 h1:/BPXKQ6n1cDWPmc5FWF6fCSaUtK+dWkWd0x9dI4dgaI=
12+
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.15.3/go.mod h1:qabLXChRlJREypX5RN/Z47GU+RaMsjotNCZfZ85oD0M=
1113
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12 h1:yjwoSyDZF8Jth+mUk5lSPJCkMC0lMy6FaCD51jm6ayE=
1214
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.12/go.mod h1:fuR57fAgMk7ot3WcNQfb6rSEn+SUffl7ri+aa8uKysI=
13-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16 h1:TNyt/+X43KJ9IJJMjKfa3bNTiZbUP7DeCxfbTROESwY=
14-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.16/go.mod h1:2DwJF39FlNAUiX5pAc0UNeiz16lK2t7IaFcm0LFHEgc=
15-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16 h1:jYfy8UPmd+6kJW5YhY0L1/KftReOGxI/4NtVSTh9O/I=
16-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.16/go.mod h1:7ZfEPZxkW42Afq4uQB8H2E2e6ebh6mXTueEpYzjCzcs=
15+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ=
16+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE=
17+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc=
18+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU=
1719
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
1820
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
1921
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16 h1:mimdLQkIX1zr8GIPY1ZtALdBQGxcASiBd2MOp8m/dMc=
2022
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.16/go.mod h1:YHk6owoSwrIsok+cAH9PENCOGoH5PU2EllX4vLtSrsY=
21-
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.5 h1:Cm77yt+/CV7A6DglkENsWA3H1hq8+4ItJnFKrhxHkvg=
22-
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.5/go.mod h1:s2fYaueBuCnwv1XQn6T8TfShxJWusv5tWPMcL+GY6+g=
23+
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.9 h1:jbqgtdKfAXebx2/l2UhDEe/jmmCIhaCO3HFK71M7VzM=
24+
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.34.9/go.mod h1:N3YdUYxyxhiuAelUgCpSVBuBI1klobJxZrDtL+olu10=
25+
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.7 h1:VTBHXWkSeFgT3sfYB4U92qMgzHl0nz9H1tYNHHutLg0=
26+
github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.22.7/go.mod h1:F/ybU7YfgFcktSp+biKgiHjyscGhlZxOz4QFFQqHXGw=
2327
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 h1:KypMCbLPPHEmf9DgMGw51jMj77VfGPAN2Kv4cfhlfgI=
2428
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4/go.mod h1:Vz1JQXliGcQktFTN/LN6uGppAIRoLBR2bMvIMP0gOjc=
2529
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18 h1:GckUnpm4EJOAio1c8o25a+b3lVfwVzC9gnSBqiiNmZM=
2630
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.18/go.mod h1:Br6+bxfG33Dk3ynmkhsW2Z/t9D4+lRqdLDNCKi85w0U=
27-
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.17 h1:HDJGz1jlV7RokVgTPfx1UHBHANC0N5Uk++xgyYgz5E0=
28-
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.17/go.mod h1:5szDu6TWdRDytfDxUQVv2OYfpTQMKApVFyqpm+TcA98=
31+
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.18 h1:GACdEPdpBE59I7pbfvu0/Mw1wzstlP3QtPHklUxybFE=
32+
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.18/go.mod h1:K+xV06+Wni4TSaOOJ1Y35e5tYOCUBYbebLKmJQQa8yY=
2933
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18 h1:tJ5RnkHCiSH0jyd6gROjlJtNwov0eGYNz8s8nFcR0jQ=
3034
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.18/go.mod h1:++NHzT+nAF7ZPrHPsA+ENvsXkOO8wEu+C6RXltAG4/c=
3135
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.16 h1:jg16PhLPUiHIj8zYIW6bqzeQSuHVEiWnGA0Brz5Xv2I=

0 commit comments

Comments
 (0)