Added an export command to dynamo-browse

This commit is contained in:
Leon Mika 2022-05-19 10:48:47 +10:00
parent 6df67ce93b
commit f6e38bbdeb
7 changed files with 121 additions and 5 deletions

View file

@ -34,7 +34,7 @@ func main() {
var dynamoClient *dynamodb.Client var dynamoClient *dynamodb.Client
if *flagLocal { if *flagLocal {
dynamoClient = dynamodb.NewFromConfig(cfg, dynamoClient = dynamodb.NewFromConfig(cfg,
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000"))) dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000")))
} else { } else {
dynamoClient = dynamodb.NewFromConfig(cfg) dynamoClient = dynamodb.NewFromConfig(cfg)
} }

View file

@ -3,4 +3,4 @@ services:
dynamo: dynamo:
image: amazon/dynamodb-local:latest image: amazon/dynamodb-local:latest
ports: ports:
- 8000:8000 - 18000:8000

View file

@ -2,10 +2,12 @@ package controllers
import ( import (
"context" "context"
"encoding/csv"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/events" "github.com/lmika/awstools/internal/common/ui/events"
"github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/lmika/awstools/internal/dynamo-browse/models"
"github.com/pkg/errors" "github.com/pkg/errors"
"os"
"sync" "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 { func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet) tea.Msg {
newResultSet, err := c.tableService.Scan(ctx, resultSet.TableInfo) newResultSet, err := c.tableService.Scan(ctx, resultSet.TableInfo)
if err != nil { if err != nil {

View file

@ -1,11 +1,16 @@
package controllers_test package controllers_test
import ( 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/controllers"
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo" "github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
"github.com/lmika/awstools/internal/dynamo-browse/services/tables" "github.com/lmika/awstools/internal/dynamo-browse/services/tables"
"github.com/lmika/awstools/test/testdynamo" "github.com/lmika/awstools/test/testdynamo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"os"
"strings"
"testing" "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{ var testData = []testdynamo.TestData{
{ {
TableName: "alpha-table", TableName: "alpha-table",

View file

@ -25,6 +25,6 @@ func (i Item) KeyValue(info *TableInfo) map[string]types.AttributeValue {
return itemKey return itemKey
} }
func (i Item) AttributeValueAsString(k string) (string, bool) { func (i Item) AttributeValueAsString(key string) (string, bool) {
return attributeToString(i[k]) return attributeToString(i[key])
} }

View file

@ -3,12 +3,14 @@ package ui
import ( import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/commandctrl" "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/controllers"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview" "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/dynamotableview"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" "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/statusandprompt"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect"
"github.com/pkg/errors"
) )
type Model struct { type Model struct {
@ -38,6 +40,12 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon
return rc.ScanTable(args[0]) 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()), "unmark": commandctrl.NoArgCommand(rc.Unmark()),
"delete": commandctrl.NoArgCommand(wc.DeleteMarked()), "delete": commandctrl.NoArgCommand(wc.DeleteMarked()),
}, },

View file

@ -28,7 +28,7 @@ func SetupTestTable(t *testing.T, testData []TestData) (*dynamodb.Client, func()
assert.NoError(t, err) assert.NoError(t, err)
dynamoClient := dynamodb.NewFromConfig(cfg, dynamoClient := dynamodb.NewFromConfig(cfg,
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000"))) dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000")))
for _, table := range testData { for _, table := range testData {
_, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{ _, err = dynamoClient.CreateTable(ctx, &dynamodb.CreateTableInput{