diff --git a/internal/common/sliceutils/map.go b/internal/common/sliceutils/map.go new file mode 100644 index 0000000..f74602c --- /dev/null +++ b/internal/common/sliceutils/map.go @@ -0,0 +1,9 @@ +package sliceutils + +func Map[T, U any](ts []T, fn func(t T) U) []U { + us := make([]U, len(ts)) + for i, t := range ts { + us[i] = fn(t) + } + return us +} diff --git a/internal/dynamo-browse/controllers/commands.go b/internal/dynamo-browse/controllers/commands.go index 82d242a..5b7c630 100644 --- a/internal/dynamo-browse/controllers/commands.go +++ b/internal/dynamo-browse/controllers/commands.go @@ -23,22 +23,3 @@ func (ps *promptSequence) next() tea.Msg { } return ps.onAllDone(ps.receivedValues) } - -//type SetAttributeArg struct { -// attrType models.ItemType -// attrName string -//} -// -//func ParseSetAttributeArgs(args []string) (attrArgs []SetAttributeArg, err error) { -// var currArg SetAttributeArg -// for _, arg := range args { -// if arg[0] == '-' { -// currArg.attrType = models.ItemType(arg[1:]) -// } else { -// currArg.attrName = arg -// attrArgs = append(attrArgs, currArg) -// currArg = SetAttributeArg{} -// } -// } -// return attrArgs, nil -//} diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index c8a3713..be3652e 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -49,4 +49,10 @@ type PromptForTableMsg struct { OnSelected func(tableName string) tea.Cmd } -type ResultSetUpdated struct{} +type ResultSetUpdated struct { + statusMessage string +} + +func (rs ResultSetUpdated) StatusMessage() string { + return rs.statusMessage +} diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index d4ee054..1705a9a 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -15,8 +15,7 @@ import ( ) func TestTableReadController_InitTable(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -41,8 +40,7 @@ func TestTableReadController_InitTable(t *testing.T) { } func TestTableReadController_ListTables(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -65,8 +63,7 @@ func TestTableReadController_ListTables(t *testing.T) { } func TestTableReadController_ExportCSV(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -101,8 +98,7 @@ func TestTableReadController_ExportCSV(t *testing.T) { } func TestTableReadController_Query(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index 87b4c06..a02d6b8 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -257,6 +257,68 @@ func (twc *TableWriteController) PutItem(idx int) tea.Cmd { } } +func (twc *TableWriteController) PutItems() tea.Cmd { + return func() tea.Msg { + var ( + expectedPuts int + markedItems int + ) + + twc.state.withResultSet(func(rs *models.ResultSet) { + markedItems = len(rs.MarkedItems()) + for i := range rs.Items() { + if rs.IsDirty(i) && (markedItems == 0 || rs.Marked(i)) { + expectedPuts++ + } + } + }) + + if expectedPuts == 0 { + if markedItems > 0 { + return events.StatusMsg("no marked items are modified") + } else { + return events.StatusMsg("no items are modified") + } + } + + var promptMessage string + if markedItems > 0 { + promptMessage = applyToN("put ", expectedPuts, "marked item", "marked items", "? ") + } else { + promptMessage = applyToN("put ", expectedPuts, "item", "items", "? ") + } + + return events.PromptForInputMsg{ + Prompt: promptMessage, + OnDone: func(value string) tea.Cmd { + if value != "y" { + return events.SetStatus("operation aborted") + } + + return func() tea.Msg { + if err := twc.state.withResultSetReturningError(func(rs *models.ResultSet) error { + updated, err := twc.tableService.PutSelectedItems(context.Background(), rs, func(idx int) bool { + return rs.IsDirty(idx) && (markedItems == 0 || rs.Marked(idx)) + }) + if err != nil { + return err + } else if updated != expectedPuts { + return errors.Errorf("expected %d updates but only %d were applied", expectedPuts, updated) + } + return nil + }); err != nil { + return events.Error(err) + } + + return ResultSetUpdated{ + statusMessage: applyToN("", expectedPuts, "item", "item", " put to table"), + } + } + }, + } + } +} + func (twc *TableWriteController) TouchItem(idx int) tea.Cmd { return func() tea.Msg { resultSet := twc.state.ResultSet() @@ -325,7 +387,7 @@ func (twc *TableWriteController) DeleteMarked() tea.Cmd { } return events.PromptForInputMsg{ - Prompt: fmt.Sprintf("delete %d items? ", len(markedItems)), + Prompt: applyToN("delete ", len(markedItems), "item", "items", "? "), OnDone: func(value string) tea.Cmd { if value != "y" { return events.SetStatus("operation aborted") @@ -343,3 +405,10 @@ func (twc *TableWriteController) DeleteMarked() tea.Cmd { } } } + +func applyToN(prefix string, n int, singular, plural, suffix string) string { + if n == 1 { + return fmt.Sprintf("%v%v %v%v", prefix, n, singular, suffix) + } + return fmt.Sprintf("%v%v %v%v", prefix, n, plural, suffix) +} diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 020ef7a..11316e5 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -14,8 +14,7 @@ import ( func TestTableWriteController_NewItem(t *testing.T) { t.Run("should add an item with pk and sk set at the end of the result set", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -45,8 +44,7 @@ func TestTableWriteController_NewItem(t *testing.T) { func TestTableWriteController_SetAttributeValue(t *testing.T) { t.Run("should preserve the type of the field if unspecified", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - t.Cleanup(cleanupFn) + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -99,8 +97,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { }) t.Run("should change the value to a particular field if already present", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - t.Cleanup(cleanupFn) + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -188,105 +185,8 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { }) } -/* -func TestTableWriteController_SetStringValue(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() - - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - - t.Run("should change the value of a string field if already present", 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.SetStringValue(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)) - }) - - t.Run("should change the value of a string field within a map if already present", func(t *testing.T) { - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, "alpha-table") - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) - - beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) - beforeStreet := beforeAddress.Value["street"].(*types.AttributeValueMemberS).Value - - assert.Equal(t, "Fake st.", beforeStreet) - assert.False(t, state.ResultSet().IsDirty(0)) - - invokeCommandWithPrompt(t, writeController.SetStringValue(0, "address.street"), "Fiction rd.") - - afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) - afterStreet := afterAddress.Value["street"].(*types.AttributeValueMemberS).Value - - assert.Equal(t, "Fiction rd.", afterStreet) - assert.True(t, state.ResultSet().IsDirty(0)) - }) -} - -func TestTableWriteController_SetNumberValue(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() - - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - - t.Run("should change the value of a number field if already present", 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("age") - assert.Equal(t, "23", before) - assert.False(t, state.ResultSet().IsDirty(0)) - - invokeCommandWithPrompt(t, writeController.SetNumberValue(0, "age"), "46") - - after, _ := state.ResultSet().Items()[0].AttributeValueAsString("age") - assert.Equal(t, "46", after) - assert.True(t, state.ResultSet().IsDirty(0)) - }) - - t.Run("should change the value of a number field within a map if already present", func(t *testing.T) { - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, "alpha-table") - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) - - beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) - beforeStreet := beforeAddress.Value["no"].(*types.AttributeValueMemberN).Value - - assert.Equal(t, "123", beforeStreet) - assert.False(t, state.ResultSet().IsDirty(0)) - - invokeCommandWithPrompt(t, writeController.SetNumberValue(0, "address.no"), "456") - - afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) - afterStreet := afterAddress.Value["no"].(*types.AttributeValueMemberN).Value - - assert.Equal(t, "456", afterStreet) - assert.True(t, state.ResultSet().IsDirty(0)) - }) -} -*/ - func TestTableWriteController_DeleteAttribute(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -331,8 +231,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) { func TestTableWriteController_PutItem(t *testing.T) { t.Run("should put the selected item if dirty", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -359,8 +258,7 @@ func TestTableWriteController_PutItem(t *testing.T) { }) t.Run("should not put the selected item if user does not confirm", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -391,8 +289,7 @@ func TestTableWriteController_PutItem(t *testing.T) { }) t.Run("should not put the selected item if not dirty", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -411,10 +308,111 @@ func TestTableWriteController_PutItem(t *testing.T) { }) } +func TestTableWriteController_PutItems(t *testing.T) { + t.Run("should put all dirty items if none are marked", func(t *testing.T) { + client := testdynamo.SetupTestTable(t, testData) + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + invokeCommand(t, readController.Init()) + + // Modify the item and put it + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetAttributeValue(2, models.StringItemType, "alpha"), "another new value") + + invokeCommandWithPrompt(t, writeController.PutItems(), "y") + + // Rescan the table + invokeCommand(t, readController.Rescan()) + + assert.Equal(t, "a new value", state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", state.ResultSet().Items()[2]["alpha"].(*types.AttributeValueMemberS).Value) + + assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, state.ResultSet().IsDirty(2)) + }) + + t.Run("only put marked items", func(t *testing.T) { + client := testdynamo.SetupTestTable(t, testData) + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + invokeCommand(t, readController.Init()) + + // Modify the item and put it + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetAttributeValue(2, models.StringItemType, "alpha"), "another new value") + invokeCommand(t, writeController.ToggleMark(0)) + + invokeCommandWithPrompt(t, writeController.PutItems(), "y") + + // Verify dirty items are unchanged + assert.Equal(t, "a new value", state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", state.ResultSet().Items()[2]["alpha"].(*types.AttributeValueMemberS).Value) + + assert.False(t, state.ResultSet().IsDirty(0)) + assert.True(t, state.ResultSet().IsDirty(2)) + + // Rescan the table and verify dirty items were not written + invokeCommand(t, readController.Rescan()) + + assert.Equal(t, "a new value", state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Nil(t, state.ResultSet().Items()[2]["alpha"]) + + assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, state.ResultSet().IsDirty(2)) + }) + + t.Run("do not put marked items which are not diry", func(t *testing.T) { + client := testdynamo.SetupTestTable(t, testData) + + provider := dynamo.NewProvider(client) + service := tables.NewService(provider) + + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, "alpha-table") + writeController := controllers.NewTableWriteController(state, service, readController) + + invokeCommand(t, readController.Init()) + + // Modify the item and put it + invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, writeController.SetAttributeValue(2, models.StringItemType, "alpha"), "another new value") + invokeCommand(t, writeController.ToggleMark(1)) + + invokeCommand(t, writeController.PutItems()) + + // Verify dirty items are unchanged + assert.Equal(t, "a new value", state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", state.ResultSet().Items()[2]["alpha"].(*types.AttributeValueMemberS).Value) + + assert.True(t, state.ResultSet().IsDirty(0)) + assert.True(t, state.ResultSet().IsDirty(2)) + + // Rescan the table and verify dirty items were not written + invokeCommand(t, readController.Rescan()) + + assert.Equal(t, "This is some value", state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Nil(t, state.ResultSet().Items()[2]["alpha"]) + + assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, state.ResultSet().IsDirty(2)) + }) +} + func TestTableWriteController_TouchItem(t *testing.T) { t.Run("should put the selected item if unmodified", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -440,8 +438,7 @@ func TestTableWriteController_TouchItem(t *testing.T) { }) t.Run("should not put the selected item if modified", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -464,8 +461,7 @@ func TestTableWriteController_TouchItem(t *testing.T) { func TestTableWriteController_NoisyTouchItem(t *testing.T) { t.Run("should delete and put the selected item if unmodified", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) @@ -491,8 +487,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) { }) t.Run("should not put the selected item if modified", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) service := tables.NewService(provider) diff --git a/internal/dynamo-browse/providers/dynamo/provider.go b/internal/dynamo-browse/providers/dynamo/provider.go index e05cd5f..0865bbb 100644 --- a/internal/dynamo-browse/providers/dynamo/provider.go +++ b/internal/dynamo-browse/providers/dynamo/provider.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/lmika/awstools/internal/common/sliceutils" "github.com/lmika/awstools/internal/dynamo-browse/models" "github.com/pkg/errors" ) @@ -14,6 +15,10 @@ type Provider struct { client *dynamodb.Client } +func NewProvider(client *dynamodb.Client) *Provider { + return &Provider{client: client} +} + func (p *Provider) ListTables(ctx context.Context) ([]string, error) { out, err := p.client.ListTables(ctx, &dynamodb.ListTablesInput{}) if err != nil { @@ -60,8 +65,33 @@ func (p *Provider) PutItem(ctx context.Context, name string, item models.Item) e return nil } -func NewProvider(client *dynamodb.Client) *Provider { - return &Provider{client: client} +func (p *Provider) PutItems(ctx context.Context, name string, items []models.Item) error { + return p.batchPutItems(ctx, name, items) +} + +func (p *Provider) batchPutItems(ctx context.Context, name string, items []models.Item) error { + reqs := len(items)/25 + 1 + for rn := 0; rn < reqs; rn++ { + s, f := rn*25, (rn+1)*25 + if f > len(items) { + f = len(items) + } + + itemsInThisRequest := items[s:f] + writeRequests := sliceutils.Map(itemsInThisRequest, func(item models.Item) types.WriteRequest { + return types.WriteRequest{PutRequest: &types.PutRequest{Item: item}} + }) + + _, err := p.client.BatchWriteItem(ctx, &dynamodb.BatchWriteItemInput{ + RequestItems: map[string][]types.WriteRequest{ + name: writeRequests, + }, + }) + if err != nil { + errors.Wrapf(err, "unable to put page %v of back puts", rn) + } + } + return nil } func (p *Provider) ScanItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error) { diff --git a/internal/dynamo-browse/providers/dynamo/provider_test.go b/internal/dynamo-browse/providers/dynamo/provider_test.go index 119da34..1297a77 100644 --- a/internal/dynamo-browse/providers/dynamo/provider_test.go +++ b/internal/dynamo-browse/providers/dynamo/provider_test.go @@ -2,6 +2,8 @@ package dynamo_test import ( "context" + "fmt" + "github.com/lmika/awstools/internal/dynamo-browse/models" "testing" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" @@ -13,8 +15,7 @@ import ( func TestProvider_ScanItems(t *testing.T) { tableName := "test-table" - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) t.Run("should return scanned items from the table", func(t *testing.T) { @@ -38,12 +39,62 @@ func TestProvider_ScanItems(t *testing.T) { }) } +func TestProvider_PutItems(t *testing.T) { + tableName := "test-table" + + scenarios := []struct { + maxItems int + }{ + {maxItems: 3}, + {maxItems: 13}, + {maxItems: 25}, + {maxItems: 48}, + {maxItems: 73}, + {maxItems: 103}, + {maxItems: 291}, + } + for _, scenario := range scenarios { + t.Run(fmt.Sprintf("should put items in batches: size %v", scenario.maxItems), func(t *testing.T) { + ctx := context.Background() + + client := testdynamo.SetupTestTable(t, []testdynamo.TestData{ + { + TableName: tableName, + }, + }) + + provider := dynamo.NewProvider(client) + + items := make([]models.Item, scenario.maxItems) + for i := 0; i < scenario.maxItems; i++ { + items[i] = models.Item{ + "pk": &types.AttributeValueMemberS{Value: fmt.Sprintf("K#%v", i)}, + "sk": &types.AttributeValueMemberS{Value: fmt.Sprintf("K#%v", i)}, + "a": &types.AttributeValueMemberN{Value: fmt.Sprintf("%v", i)}, + } + } + + // Write the data + err := provider.PutItems(ctx, tableName, items) + assert.NoError(t, err) + + // Verify the data + readItems, err := provider.ScanItems(ctx, tableName, nil, scenario.maxItems+5) + assert.NoError(t, err) + assert.Len(t, readItems, scenario.maxItems) + + for i := 0; i < scenario.maxItems; i++ { + assert.Contains(t, readItems, items[i]) + } + }) + } +} + func TestProvider_DeleteItem(t *testing.T) { tableName := "test-table" t.Run("should delete item if exists in table", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) ctx := context.Background() @@ -64,8 +115,7 @@ func TestProvider_DeleteItem(t *testing.T) { }) t.Run("should do nothing if key does not exist", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) ctx := context.Background() @@ -85,8 +135,7 @@ func TestProvider_DeleteItem(t *testing.T) { }) t.Run("should return error if table name does not exist", func(t *testing.T) { - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) ctx := context.Background() diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go index 58d63ec..568bcc9 100644 --- a/internal/dynamo-browse/services/tables/iface.go +++ b/internal/dynamo-browse/services/tables/iface.go @@ -14,4 +14,5 @@ type TableProvider interface { ScanItems(ctx context.Context, tableName string, filterExpr *expression.Expression, maxItems int) ([]models.Item, error) DeleteItem(ctx context.Context, tableName string, key map[string]types.AttributeValue) error PutItem(ctx context.Context, name string, item models.Item) error + PutItems(ctx context.Context, name string, items []models.Item) error } diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index d695c08..e5e58a6 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -3,6 +3,7 @@ package tables import ( "context" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" + "github.com/lmika/awstools/internal/common/sliceutils" "strings" "github.com/lmika/awstools/internal/dynamo-browse/models" @@ -114,6 +115,36 @@ func (s *Service) PutItemAt(ctx context.Context, resultSet *models.ResultSet, in return nil } +func (s *Service) PutSelectedItems(ctx context.Context, resultSet *models.ResultSet, shouldPut func(idx int) bool) (int, error) { + type dirtyItem struct { + item models.Item + idx int + } + + dirtyItems := make([]dirtyItem, 0) + for i, item := range resultSet.Items() { + if shouldPut(i) { + dirtyItems = append(dirtyItems, dirtyItem{item, i}) + } + } + + if len(dirtyItems) == 0 { + return 0, nil + } + + if err := s.provider.PutItems(ctx, resultSet.TableInfo.Name, sliceutils.Map(dirtyItems, func(t dirtyItem) models.Item { + return t.item + })); err != nil { + return 0, err + } + + for _, di := range dirtyItems { + resultSet.SetDirty(di.idx, false) + resultSet.SetNew(di.idx, false) + } + return len(dirtyItems), nil +} + func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items []models.Item) error { for _, item := range items { if err := s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)); err != nil { diff --git a/internal/dynamo-browse/services/tables/service_test.go b/internal/dynamo-browse/services/tables/service_test.go index 855ef1c..a592178 100644 --- a/internal/dynamo-browse/services/tables/service_test.go +++ b/internal/dynamo-browse/services/tables/service_test.go @@ -13,8 +13,7 @@ import ( func TestService_Describe(t *testing.T) { tableName := "service-test-data" - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) t.Run("return details of the table", func(t *testing.T) { @@ -35,8 +34,7 @@ func TestService_Describe(t *testing.T) { func TestService_Scan(t *testing.T) { tableName := "service-test-data" - client, cleanupFn := testdynamo.SetupTestTable(t, testData) - defer cleanupFn() + client := testdynamo.SetupTestTable(t, testData) provider := dynamo.NewProvider(client) t.Run("return all columns and fields in sorted order", func(t *testing.T) { @@ -52,9 +50,6 @@ func TestService_Scan(t *testing.T) { // Hash first, then range, then columns in alphabetic order assert.Equal(t, rs.TableInfo, ti) assert.Equal(t, rs.Columns(), []string{"pk", "sk", "alpha", "beta", "gamma"}) - //assert.Equal(t, rs.Items[0], testdynamo.TestRecordAsItem(t, testData[1])) - //assert.Equal(t, rs.Items[1], testdynamo.TestRecordAsItem(t, testData[0])) - //assert.Equal(t, rs.Items[2], testdynamo.TestRecordAsItem(t, testData[2])) }) } diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index ba558ec..b8df6df 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -95,7 +95,7 @@ func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteCon }, "put": func(args []string) tea.Cmd { - return wc.PutItem(dtv.SelectedItemIndex()) + return wc.PutItems() }, "touch": func(args []string) tea.Cmd { return wc.TouchItem(dtv.SelectedItemIndex()) diff --git a/test/testdynamo/client.go b/test/testdynamo/client.go index 1890af9..9f61ecc 100644 --- a/test/testdynamo/client.go +++ b/test/testdynamo/client.go @@ -18,7 +18,7 @@ type TestData struct { Data []map[string]interface{} } -func SetupTestTable(t *testing.T, testData []TestData) (*dynamodb.Client, func()) { +func SetupTestTable(t *testing.T, testData []TestData) *dynamodb.Client { t.Helper() ctx := context.Background() @@ -68,5 +68,5 @@ func SetupTestTable(t *testing.T, testData []TestData) (*dynamodb.Client, func() } }) - return dynamoClient, func() {} + return dynamoClient }