Added an export command to dynamo-browse
This commit is contained in:
parent
6df67ce93b
commit
f6e38bbdeb
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -3,4 +3,4 @@ services:
|
|||
dynamo:
|
||||
image: amazon/dynamodb-local:latest
|
||||
ports:
|
||||
- 8000:8000
|
||||
- 18000:8000
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
},
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in a new issue