Skip to content

Instantly share code, notes, and snippets.

@jamiefdhurst
Last active July 7, 2024 19:42
Show Gist options
  • Save jamiefdhurst/6fc5990c588f89520f136ffc1c3ccbe5 to your computer and use it in GitHub Desktop.
Save jamiefdhurst/6fc5990c588f89520f136ffc1c3ccbe5 to your computer and use it in GitHub Desktop.
Updated dynamodb client to use query and sort key effectively.
package main
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
const gameName string = "standard"
const tableName string = "scoreboard"
func createClient(endpoint string) *dynamodb.Client {
cfg, err := config.LoadDefaultConfig(context.TODO(), func(o *config.LoadOptions) error {
o.Region = "eu-west-1"
return nil
})
if err != nil {
panic(err)
}
if endpoint == "" {
endpoint = "https://dynamodb.eu-west-1.amazonaws.com"
}
return dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {
o.BaseEndpoint = &endpoint
})
}
func createTable(c *dynamodb.Client) error {
_, err := c.CreateTable(context.TODO(), &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
BillingMode: types.BillingModePayPerRequest,
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("game"),
AttributeType: types.ScalarAttributeTypeS,
},
{
AttributeName: aws.String("score-name"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("game"),
KeyType: types.KeyTypeHash,
},
{
AttributeName: aws.String("score-name"),
KeyType: types.KeyTypeRange,
},
},
})
return err
}
func save(c *dynamodb.Client, name string, value uint32) error {
_, err := c.PutItem(context.TODO(), &dynamodb.PutItemInput{
TableName: aws.String(tableName),
Item: map[string]types.AttributeValue{
"game": &types.AttributeValueMemberS{Value: gameName},
"score-name": &types.AttributeValueMemberS{Value: fmt.Sprint(value) + "-" + name},
"name": &types.AttributeValueMemberS{Value: name},
"value": &types.AttributeValueMemberN{Value: fmt.Sprint(value)},
},
})
return err
}
func get(c *dynamodb.Client) []Score {
out, err := c.Query(context.TODO(), &dynamodb.QueryInput{
TableName: aws.String(tableName),
KeyConditionExpression: aws.String("game = :hashKey"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":hashKey": &types.AttributeValueMemberS{Value: gameName},
},
ScanIndexForward: aws.Bool(false),
})
if err != nil {
panic(err)
}
var result []Score
attributevalue.UnmarshalListOfMaps(out.Items, &result)
return result
}
package main
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
containers "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
// Create the test container and wait for it to be ready
func setupContainer(t *testing.T) (string, func(t *testing.T)) {
ctx := context.Background()
req := containers.ContainerRequest{
Image: "amazon/dynamodb-local:latest",
ExposedPorts: []string{"8000/tcp"},
WaitingFor: wait.ForExposedPort(),
}
container, err := containers.GenericContainer(ctx, containers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatalf("Could not start DynamoDB: %s", err)
}
endpoint, err := container.Endpoint(ctx, "")
if err != nil {
t.Fatalf("Could not get DynamoDB endpoint: %s", err)
}
return endpoint, func(t *testing.T) {
if err := container.Terminate(ctx); err != nil {
t.Fatalf("Could not stop DynamoDB: %s", err)
}
}
}
func connect(e string, t *testing.T) *dynamodb.Client {
client := createClient("http://" + e)
if err := createTable(client); err != nil {
t.Errorf("Expected to be able to create DynamoDB table, but received: %s", err)
}
return client
}
func fill(c *dynamodb.Client, t *testing.T) {
if err := save(c, "foo", 90); err != nil {
t.Errorf("Expected to be able to put item into DynamoDB, but received: %s", err)
}
if err := save(c, "bar", 75); err != nil {
t.Errorf("Expected to be able to put item into DynamoDB, but received: %s", err)
}
if err := save(c, "baz", 80); err != nil {
t.Errorf("Expected to be able to put item into DynamoDB, but received: %s", err)
}
}
func TestConnect(t *testing.T) {
ep, tearDown := setupContainer(t)
defer tearDown(t)
connect(ep, t)
}
func TestSave(t *testing.T) {
ep, tearDown := setupContainer(t)
defer tearDown(t)
c := connect(ep, t)
if err := save(c, "testing", 50); err != nil {
t.Errorf("Expected to be able to save item, but received error: %s", err)
}
}
func TestGet(t *testing.T) {
ep, tearDown := setupContainer(t)
defer tearDown(t)
c := connect(ep, t)
fill(c, t)
result := get(c)
if result[0].Name != "foo" {
t.Errorf("Expected entry 0 to be 'foo' but received: %s", result[0].Name)
}
if result[0].Value != 90 {
t.Errorf("Expected entry 0 to be '90' but received: %d", result[0].Value)
}
if result[1].Name != "baz" {
t.Errorf("Expected entry 1 to be 'baz' but received: %s", result[1].Name)
}
if result[1].Value != 80 {
t.Errorf("Expected entry 1 to be '80' but received: %d", result[1].Value)
}
if result[2].Name != "bar" {
t.Errorf("Expected entry 2 to be 'bar' but received: %s", result[2].Name)
}
if result[2].Value != 75 {
t.Errorf("Expected entry 2 to be '75' but received: %d", result[2].Value)
}
}
package main
import (
"encoding/json"
"log"
"net/http"
"os"
)
type Score struct {
Name string `json:"name"`
Value uint32 `json:"score"`
}
func main() {
dynamodb := createClient(os.Getenv("DYNAMODB_ENDPOINT"))
http.HandleFunc("/score", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
var s Score
// Try to decode the request body into the struct. If there is an error,
// respond to the client with the error message and a 400 status code.
err := json.NewDecoder(r.Body).Decode(&s)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = save(dynamodb, s.Name, s.Value)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
return
}
if r.Method == "GET" {
w.Header().Add("Content-Type", "application/json")
scores := get(dynamodb)
json.NewEncoder(w).Encode(scores)
return
}
http.Error(w, "Not found", http.StatusNotFound)
})
log.Println("Listening on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment