From f6e38bbdeb24c19c3100c54e55ea7eb6affd0767 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 19 May 2022 10:48:47 +1000 Subject: [PATCH] Added an export command to dynamo-browse --- cmd/dynamo-browse/main.go | 2 +- docker-compose.yml | 2 +- .../dynamo-browse/controllers/tableread.go | 37 ++++++++++ .../controllers/tableread_test.go | 71 +++++++++++++++++++ internal/dynamo-browse/models/items.go | 4 +- internal/dynamo-browse/ui/model.go | 8 +++ test/testdynamo/client.go | 2 +- 7 files changed, 121 insertions(+), 5 deletions(-) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 3427e2d..f72de9c 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -34,7 +34,7 @@ func main() { var dynamoClient *dynamodb.Client if *flagLocal { dynamoClient = dynamodb.NewFromConfig(cfg, - dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000"))) + dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000"))) } else { dynamoClient = dynamodb.NewFromConfig(cfg) } diff --git a/docker-compose.yml b/docker-compose.yml index ff5b618..eb9239e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,4 +3,4 @@ services: dynamo: image: amazon/dynamodb-local:latest ports: - - 8000:8000 \ No newline at end of file + - 18000:8000 \ No newline at end of file diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index 6f019be..23b6d83 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -2,10 +2,12 @@ package controllers import ( "context" + "encoding/csv" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/pkg/errors" + "os" "sync" ) @@ -76,6 +78,41 @@ func (c *TableReadController) Rescan() tea.Cmd { } } +func (c *TableReadController) ExportCSV(filename string) tea.Cmd { + return func() tea.Msg { + resultSet := c.resultSet + if resultSet == nil { + return events.Error(errors.New("no result set")) + } + + f, err := os.Create(filename) + if err != nil { + return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename)) + } + defer f.Close() + + cw := csv.NewWriter(f) + defer cw.Flush() + + columns := resultSet.Columns + if err := cw.Write(columns); err != nil { + return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename)) + } + + row := make([]string, len(resultSet.Columns)) + for _, item := range resultSet.Items() { + for i, col := range columns { + row[i], _ = item.AttributeValueAsString(col) + } + if err := cw.Write(row); err != nil { + return events.Error(errors.Wrapf(err, "cannot export to '%v'", filename)) + } + } + + return nil + } +} + func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet) tea.Msg { newResultSet, err := c.tableService.Scan(ctx, resultSet.TableInfo) if err != nil { diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index e0ef160..006a390 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -1,11 +1,16 @@ package controllers_test import ( + "fmt" + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/dynamo-browse/controllers" "github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" "github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/lmika/awstools/test/testdynamo" "github.com/stretchr/testify/assert" + "os" + "strings" "testing" ) @@ -59,6 +64,72 @@ func TestTableReadController_ListTables(t *testing.T) { }) } +func TestTableReadController_ExportCSV(t *testing.T) { + client, cleanupFn := testdynamo.SetupTestTable(t, testData) + defer cleanupFn() + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + readController := controllers.NewTableReadController(service, "alpha-table") + + t.Run("should export result set to CSV file", func(t *testing.T) { + tempFile := tempFile(t) + + invokeCommand(t, readController.Init()) + invokeCommand(t, readController.ExportCSV(tempFile)) + + bts, err := os.ReadFile(tempFile) + assert.NoError(t, err) + + assert.Equal(t, string(bts), strings.Join([]string{ + "pk,sk,alpha,beta,gamma\n", + "abc,111,This is some value,,\n", + "abc,222,This is another some value,1231,\n", + "bbb,131,,2468,foobar\n", + }, "")) + }) + + t.Run("should return error if result set is not set", func(t *testing.T) { + tempFile := tempFile(t) + readController := controllers.NewTableReadController(service, "non-existant-table") + + invokeCommandExpectingError(t, readController.Init()) + invokeCommandExpectingError(t, readController.ExportCSV(tempFile)) + }) + + // Hidden items? +} + +func tempFile(t *testing.T) string { + t.Helper() + + tempFile, err := os.CreateTemp("", "export.csv") + assert.NoError(t, err) + tempFile.Close() + + t.Cleanup(func() { + os.Remove(tempFile.Name()) + }) + + return tempFile.Name() +} + +func invokeCommand(t *testing.T, cmd tea.Cmd) { + msg := cmd() + + err, isErr := msg.(events.ErrorMsg) + if isErr { + assert.Fail(t, fmt.Sprintf("expected no error but got one: %v", err)) + } +} + +func invokeCommandExpectingError(t *testing.T, cmd tea.Cmd) { + msg := cmd() + + _, isErr := msg.(events.ErrorMsg) + assert.True(t, isErr) +} + var testData = []testdynamo.TestData{ { TableName: "alpha-table", diff --git a/internal/dynamo-browse/models/items.go b/internal/dynamo-browse/models/items.go index 13a0b6a..49b16dd 100644 --- a/internal/dynamo-browse/models/items.go +++ b/internal/dynamo-browse/models/items.go @@ -25,6 +25,6 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue { return itemKey } -func (i Item) AttributeValueAsString(k string) (string, bool) { - return attributeToString(i[k]) +func (i Item) AttributeValueAsString(key string) (string, bool) { + return attributeToString(i[key]) } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 98e950f..d98b579 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -3,12 +3,14 @@ package ui import ( tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/common/ui/commandctrl" + "github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/dynamo-browse/controllers" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamotableview" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect" + "github.com/pkg/errors" ) type Model struct { @@ -38,6 +40,12 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon return rc.ScanTable(args[0]) } }, + "export": func(args []string) tea.Cmd { + if len(args) == 0 { + return events.SetError(errors.New("expected filename")) + } + return rc.ExportCSV(args[1]) + }, "unmark": commandctrl.NoArgCommand(rc.Unmark()), "delete": commandctrl.NoArgCommand(wc.DeleteMarked()), }, diff --git a/test/testdynamo/client.go b/test/testdynamo/client.go index af3842e..d50935c 100644 --- a/test/testdynamo/client.go +++ b/test/testdynamo/client.go @@ -28,7 +28,7 @@ func SetupTestTable(t *testing.T, testData []TestData) (*dynamodb.Client, func() assert.NoError(t, err) dynamoClient := dynamodb.NewFromConfig(cfg, - dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000"))) + dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000"))) for _, table := range testData { _, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{