put-items: started adding some basic commands for putting items

This commit is contained in:
Leon Mika 2022-05-26 09:01:39 +10:00
parent 3319a9d4aa
commit 174bab36c3
10 changed files with 203 additions and 79 deletions

View file

@ -43,8 +43,9 @@ func main() {
tableService := tables.NewService(dynamoProvider)
tableReadController := controllers.NewTableReadController(tableService, *flagTable)
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController)
state := controllers.NewState()
tableReadController := controllers.NewTableReadController(state, tableService, *flagTable)
tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController)
commandController := commandctrl.NewCommandController()
model := ui.NewModel(tableReadController, tableWriteController, commandController)

View file

@ -1,28 +1,46 @@
package controllers
import (
"context"
"sync"
"github.com/lmika/awstools/internal/dynamo-browse/models"
)
type State struct {
ResultSet *models.ResultSet
SelectedItem models.Item
// InReadWriteMode indicates whether modifications can be made to the table
InReadWriteMode bool
mutex *sync.Mutex
resultSet *models.ResultSet
filter string
}
type stateContextKeyType struct{}
var stateContextKey = stateContextKeyType{}
func CurrentState(ctx context.Context) State {
state, _ := ctx.Value(stateContextKey).(State)
return state
func NewState() *State {
return &State{
mutex: new(sync.Mutex),
}
}
func ContextWithState(ctx context.Context, state State) context.Context {
return context.WithValue(ctx, stateContextKey, state)
func (s *State) ResultSet() *models.ResultSet {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.resultSet
}
func (s *State) Filter() string {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.filter
}
func (s *State) withResultSet(rs func(*models.ResultSet)) {
s.mutex.Lock()
defer s.mutex.Unlock()
rs(s.resultSet)
}
func (s *State) setResultSetAndFilter(resultSet *models.ResultSet, filter string) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.resultSet = resultSet
s.filter = filter
}

View file

@ -17,12 +17,14 @@ type TableReadController struct {
// state
mutex *sync.Mutex
resultSet *models.ResultSet
filter string
state *State
//resultSet *models.ResultSet
//filter string
}
func NewTableReadController(tableService TableReadService, tableName string) *TableReadController {
func NewTableReadController(state *State, tableService TableReadService, tableName string) *TableReadController {
return &TableReadController{
state: state,
tableService: tableService,
tableName: tableName,
mutex: new(sync.Mutex),
@ -68,19 +70,19 @@ func (c *TableReadController) ScanTable(name string) tea.Cmd {
return events.Error(err)
}
return c.setResultSetAndFilter(resultSet, c.filter)
return c.setResultSetAndFilter(resultSet, c.state.Filter())
}
}
func (c *TableReadController) Rescan() tea.Cmd {
return func() tea.Msg {
return c.doScan(context.Background(), c.resultSet)
return c.doScan(context.Background(), c.state.ResultSet())
}
}
func (c *TableReadController) ExportCSV(filename string) tea.Cmd {
return func() tea.Msg {
resultSet := c.resultSet
resultSet := c.state.ResultSet()
if resultSet == nil {
return events.Error(errors.New("no result set"))
}
@ -119,39 +121,30 @@ func (c *TableReadController) doScan(ctx context.Context, resultSet *models.Resu
return events.Error(err)
}
newResultSet = c.tableService.Filter(newResultSet, c.filter)
newResultSet = c.tableService.Filter(newResultSet, c.state.Filter())
return c.setResultSetAndFilter(newResultSet, c.filter)
return c.setResultSetAndFilter(newResultSet, c.state.Filter())
}
func (c *TableReadController) ResultSet() *models.ResultSet {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.resultSet
}
//func (c *TableReadController) ResultSet() *models.ResultSet {
// c.mutex.Lock()
// defer c.mutex.Unlock()
//
// return c.resultSet
//}
func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string) tea.Msg {
c.mutex.Lock()
defer c.mutex.Unlock()
c.resultSet = resultSet
c.filter = filter
c.state.setResultSetAndFilter(resultSet, filter)
return NewResultSet{resultSet}
}
func (c *TableReadController) Unmark() tea.Cmd {
return func() tea.Msg {
resultSet := c.ResultSet()
c.state.withResultSet(func(resultSet *models.ResultSet) {
for i := range resultSet.Items() {
resultSet.SetMark(i, false)
}
c.mutex.Lock()
defer c.mutex.Unlock()
c.resultSet = resultSet
})
return ResultSetUpdated{}
}
}
@ -162,7 +155,7 @@ func (c *TableReadController) Filter() tea.Cmd {
Prompt: "filter: ",
OnDone: func(value string) tea.Cmd {
return func() tea.Msg {
resultSet := c.ResultSet()
resultSet := c.state.ResultSet()
newResultSet := c.tableService.Filter(resultSet, value)
return c.setResultSetAndFilter(newResultSet, value)

View file

@ -22,7 +22,7 @@ func TestTableReadController_InitTable(t *testing.T) {
service := tables.NewService(provider)
t.Run("should prompt for table if no table name provided", func(t *testing.T) {
readController := controllers.NewTableReadController(service, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
cmd := readController.Init()
event := cmd()
@ -31,7 +31,7 @@ func TestTableReadController_InitTable(t *testing.T) {
})
t.Run("should scan table if table name provided", func(t *testing.T) {
readController := controllers.NewTableReadController(service, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
cmd := readController.Init()
event := cmd()
@ -46,7 +46,7 @@ func TestTableReadController_ListTables(t *testing.T) {
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(service, "")
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
t.Run("returns a list of tables", func(t *testing.T) {
cmd := readController.ListTables()
@ -70,7 +70,7 @@ func TestTableReadController_ExportCSV(t *testing.T) {
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
readController := controllers.NewTableReadController(service, "alpha-table")
readController := controllers.NewTableReadController(controllers.NewState(), service, "alpha-table")
t.Run("should export result set to CSV file", func(t *testing.T) {
tempFile := tempFile(t)
@ -91,7 +91,7 @@ func TestTableReadController_ExportCSV(t *testing.T) {
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")
readController := controllers.NewTableReadController(controllers.NewState(), service, "non-existant-table")
invokeCommandExpectingError(t, readController.Init())
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
@ -123,6 +123,17 @@ func invokeCommand(t *testing.T, cmd tea.Cmd) {
}
}
func invokeCommandWithPrompt(t *testing.T, cmd tea.Cmd, promptValue string) {
msg := cmd()
pi, isPi := msg.(events.PromptForInputMsg)
if !isPi {
assert.Fail(t, fmt.Sprintf("expected prompt for input but didn't get one"))
}
invokeCommand(t, pi.OnDone(promptValue))
}
func invokeCommandExpectingError(t *testing.T, cmd tea.Cmd) {
msg := cmd()

View file

@ -3,18 +3,22 @@ package controllers
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/events"
"github.com/lmika/awstools/internal/dynamo-browse/models"
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
)
type TableWriteController struct {
state *State
tableService *tables.Service
tableReadControllers *TableReadController
}
func NewTableWriteController(tableService *tables.Service, tableReadControllers *TableReadController) *TableWriteController {
func NewTableWriteController(state *State, tableService *tables.Service, tableReadControllers *TableReadController) *TableWriteController {
return &TableWriteController{
state: state,
tableService: tableService,
tableReadControllers: tableReadControllers,
}
@ -22,16 +26,46 @@ func NewTableWriteController(tableService *tables.Service, tableReadControllers
func (twc *TableWriteController) ToggleMark(idx int) tea.Cmd {
return func() tea.Msg {
resultSet := twc.tableReadControllers.ResultSet()
twc.state.withResultSet(func(resultSet *models.ResultSet) {
resultSet.SetMark(idx, !resultSet.Marked(idx))
})
return ResultSetUpdated{}
}
}
func (twc *TableWriteController) NewItem() tea.Cmd {
return func() tea.Msg {
twc.state.withResultSet(func(set *models.ResultSet) {
set.AddNewItem(models.Item{}, models.ItemAttribute{
New: true,
Dirty: true,
})
})
return NewResultSet{twc.state.ResultSet()}
}
}
func (twc *TableWriteController) SetItemValue(idx int, key string) tea.Cmd {
return func() tea.Msg {
return events.PromptForInputMsg{
Prompt: "string value: ",
OnDone: func(value string) tea.Cmd {
return func() tea.Msg {
twc.state.withResultSet(func(set *models.ResultSet) {
set.Items()[idx][key] = &types.AttributeValueMemberS{Value: value}
set.SetDirty(idx, true)
})
return ResultSetUpdated{}
}
},
}
}
}
func (twc *TableWriteController) DeleteMarked() tea.Cmd {
return func() tea.Msg {
resultSet := twc.tableReadControllers.ResultSet()
resultSet := twc.state.ResultSet()
markedItems := resultSet.MarkedItems()
if len(markedItems) == 0 {

View file

@ -1,6 +1,11 @@
package controllers_test
import (
"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"
"testing"
)
@ -173,24 +178,53 @@ func setupController(t *testing.T) (*controllers.TableWriteController, controlle
tableService: tableService,
}, cleanupFn
}
var testData = testdynamo.TestData{
{
"pk": "abc",
"sk": "222",
"alpha": "This is another some value",
"beta": 1231,
},
{
"pk": "abc",
"sk": "111",
"alpha": "This is some value",
},
{
"pk": "bbb",
"sk": "131",
"beta": 2468,
"gamma": "foobar",
},
}
*/
func TestTableWriteController_NewItem(t *testing.T) {
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
defer cleanupFn()
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
t.Run("should add a new empty item at the end of the result set", func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
assert.Len(t, state.ResultSet().Items(), 3)
invokeCommand(t, writeController.NewItem())
newResultSet := state.ResultSet()
assert.Len(t, newResultSet.Items(), 4)
assert.Len(t, newResultSet.Items()[3], 0)
assert.True(t, newResultSet.IsNew(3))
assert.True(t, newResultSet.IsDirty(3))
})
}
func TestTableWriteController_SetItemValue(t *testing.T) {
client, cleanupFn := testdynamo.SetupTestTable(t, testData)
defer cleanupFn()
provider := dynamo.NewProvider(client)
service := tables.NewService(provider)
t.Run("should add a new empty item at the end of the result set", func(t *testing.T) {
state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, "alpha-table")
writeController := controllers.NewTableWriteController(state, service, readController)
invokeCommand(t, readController.Init())
before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
assert.Equal(t, "This is some value", before)
assert.False(t, state.ResultSet().IsDirty(0))
invokeCommandWithPrompt(t, writeController.SetItemValue(0, "alpha"), "a new value")
after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha")
assert.Equal(t, "a new value", after)
assert.True(t, state.ResultSet().IsDirty(0))
})
}

View file

@ -10,6 +10,8 @@ type ResultSet struct {
type ItemAttribute struct {
Marked bool
Hidden bool
Dirty bool
New bool
}
func (rs *ResultSet) Items() []Item {
@ -21,6 +23,11 @@ func (rs *ResultSet) SetItems(items []Item) {
rs.attributes = make([]ItemAttribute, len(items))
}
func (rs *ResultSet) AddNewItem(item Item, attrs ItemAttribute) {
rs.items = append(rs.items, item)
rs.attributes = append(rs.attributes, attrs)
}
func (rs *ResultSet) SetMark(idx int, marked bool) {
rs.attributes[idx].Marked = marked
}
@ -29,6 +36,14 @@ func (rs *ResultSet) SetHidden(idx int, hidden bool) {
rs.attributes[idx].Hidden = hidden
}
func (rs *ResultSet) SetDirty(idx int, dirty bool) {
rs.attributes[idx].Dirty = dirty
}
func (rs *ResultSet) SetNew(idx int, isNew bool) {
rs.attributes[idx].New = isNew
}
func (rs *ResultSet) Marked(idx int) bool {
return rs.attributes[idx].Marked
}
@ -37,6 +52,14 @@ func (rs *ResultSet) Hidden(idx int) bool {
return rs.attributes[idx].Hidden
}
func (rs *ResultSet) IsDirty(idx int) bool {
return rs.attributes[idx].Dirty
}
func (rs *ResultSet) IsNew(idx int) bool {
return rs.attributes[idx].New
}
func (rs *ResultSet) MarkedItems() []Item {
items := make([]Item, 0)
for i, itemAttr := range rs.attributes {

View file

@ -48,6 +48,15 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon
},
"unmark": commandctrl.NoArgCommand(rc.Unmark()),
"delete": commandctrl.NoArgCommand(wc.DeleteMarked()),
// TEMP
"new-item": commandctrl.NoArgCommand(wc.NewItem()),
"set": func(args []string) tea.Cmd {
if len(args) != 1 {
return events.SetError(errors.New("expected attribute key"))
}
return wc.SetItemValue(dtv.SelectedItemIndex(), args[0])
},
},
})

View file

@ -135,5 +135,6 @@ func (m *Model) postSelectedItemChanged() tea.Msg {
}
func (m *Model) Refresh() {
m.table.SetRows(m.rows)
}

View file

@ -19,7 +19,7 @@ import (
func main() {
ctx := context.Background()
tableName := "awstools-test"
totalItems := 300
totalItems := 10
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
@ -27,7 +27,7 @@ func main() {
}
dynamoClient := dynamodb.NewFromConfig(cfg,
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:8000")))
dynamodb.WithEndpointResolver(dynamodb.EndpointResolverFromURL("http://localhost:18000")))
if _, err = dynamoClient.DeleteTable(ctx, &dynamodb.DeleteTableInput{
TableName: aws.String(tableName),