447 lines
12 KiB
Go
447 lines
12 KiB
Go
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/sliceutils"
|
|
"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"
|
|
"github.com/pkg/errors"
|
|
"strconv"
|
|
)
|
|
|
|
type TableWriteController struct {
|
|
state *State
|
|
tableService *tables.Service
|
|
tableReadControllers *TableReadController
|
|
}
|
|
|
|
func NewTableWriteController(state *State, tableService *tables.Service, tableReadControllers *TableReadController) *TableWriteController {
|
|
return &TableWriteController{
|
|
state: state,
|
|
tableService: tableService,
|
|
tableReadControllers: tableReadControllers,
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) ToggleMark(idx int) tea.Cmd {
|
|
return func() tea.Msg {
|
|
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 {
|
|
// Work out which keys we need to prompt for
|
|
rs := twc.state.ResultSet()
|
|
|
|
keyPrompts := &promptSequence{
|
|
prompts: []string{rs.TableInfo.Keys.PartitionKey + ": "},
|
|
}
|
|
if rs.TableInfo.Keys.SortKey != "" {
|
|
keyPrompts.prompts = append(keyPrompts.prompts, rs.TableInfo.Keys.SortKey+": ")
|
|
}
|
|
keyPrompts.onAllDone = func(values []string) tea.Msg {
|
|
twc.state.withResultSet(func(set *models.ResultSet) {
|
|
newItem := models.Item{}
|
|
|
|
// TODO: deal with keys of different type
|
|
newItem[rs.TableInfo.Keys.PartitionKey] = &types.AttributeValueMemberS{Value: values[0]}
|
|
if len(values) == 2 {
|
|
newItem[rs.TableInfo.Keys.SortKey] = &types.AttributeValueMemberS{Value: values[1]}
|
|
}
|
|
|
|
set.AddNewItem(newItem, models.ItemAttribute{
|
|
New: true,
|
|
Dirty: true,
|
|
})
|
|
})
|
|
return twc.state.buildNewResultSetMessage("New item added")
|
|
}
|
|
|
|
return keyPrompts.next()
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) SetAttributeValue(idx int, itemType models.ItemType, key string) tea.Cmd {
|
|
apPath := newAttrPath(key)
|
|
|
|
var attrValue types.AttributeValue
|
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) (err error) {
|
|
attrValue, err = apPath.follow(set.Items()[idx])
|
|
return err
|
|
}); err != nil {
|
|
return events.SetError(err)
|
|
}
|
|
|
|
switch itemType {
|
|
case models.UnsetItemType:
|
|
switch attrValue.(type) {
|
|
case *types.AttributeValueMemberS:
|
|
return twc.setStringValue(idx, apPath)
|
|
case *types.AttributeValueMemberN:
|
|
return twc.setNumberValue(idx, apPath)
|
|
case *types.AttributeValueMemberBOOL:
|
|
return twc.setBoolValue(idx, apPath)
|
|
default:
|
|
return events.SetError(errors.New("attribute type for key must be set"))
|
|
}
|
|
case models.StringItemType:
|
|
return twc.setStringValue(idx, apPath)
|
|
case models.NumberItemType:
|
|
return twc.setNumberValue(idx, apPath)
|
|
case models.BoolItemType:
|
|
return twc.setBoolValue(idx, apPath)
|
|
case models.NullItemType:
|
|
return twc.setNullValue(idx, apPath)
|
|
default:
|
|
return events.SetError(errors.New("unsupported attribute type"))
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) setStringValue(idx int, attr attrPath) tea.Cmd {
|
|
return func() tea.Msg {
|
|
return events.PromptForInputMsg{
|
|
Prompt: "string value: ",
|
|
OnDone: func(value string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
|
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
|
|
if err := attr.setAt(item, &types.AttributeValueMemberS{Value: value}); err != nil {
|
|
return err
|
|
}
|
|
set.SetDirty(idx, true)
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
set.RefreshColumns()
|
|
return nil
|
|
}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return ResultSetUpdated{}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) applyToItems(rs *models.ResultSet, selectedIndex int, applyFn func(idx int, item models.Item) error) error {
|
|
if markedItems := rs.MarkedItems(); len(markedItems) > 0 {
|
|
for _, mi := range markedItems {
|
|
if err := applyFn(mi.Index, mi.Item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return applyFn(selectedIndex, rs.Items()[selectedIndex])
|
|
}
|
|
|
|
func (twc *TableWriteController) setNumberValue(idx int, attr attrPath) tea.Cmd {
|
|
return func() tea.Msg {
|
|
return events.PromptForInputMsg{
|
|
Prompt: "number value: ",
|
|
OnDone: func(value string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
|
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
|
|
if err := attr.setAt(item, &types.AttributeValueMemberN{Value: value}); err != nil {
|
|
return err
|
|
}
|
|
set.SetDirty(idx, true)
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
set.RefreshColumns()
|
|
return nil
|
|
}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return ResultSetUpdated{}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) setBoolValue(idx int, attr attrPath) tea.Cmd {
|
|
return func() tea.Msg {
|
|
return events.PromptForInputMsg{
|
|
Prompt: "bool value: ",
|
|
OnDone: func(value string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
b, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
|
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
|
|
if err := attr.setAt(item, &types.AttributeValueMemberBOOL{Value: b}); err != nil {
|
|
return err
|
|
}
|
|
set.SetDirty(idx, true)
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
set.RefreshColumns()
|
|
return nil
|
|
}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return ResultSetUpdated{}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) setNullValue(idx int, attr attrPath) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
|
if err := twc.applyToItems(set, idx, func(idx int, item models.Item) error {
|
|
if err := attr.setAt(item, &types.AttributeValueMemberNULL{Value: true}); err != nil {
|
|
return err
|
|
}
|
|
set.SetDirty(idx, true)
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
set.RefreshColumns()
|
|
return nil
|
|
}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return ResultSetUpdated{}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
// Verify that the expression is valid
|
|
apPath := newAttrPath(key)
|
|
|
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
|
_, err := apPath.follow(set.Items()[idx])
|
|
return err
|
|
}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
if err := twc.state.withResultSetReturningError(func(set *models.ResultSet) error {
|
|
err := apPath.deleteAt(set.Items()[idx])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
set.SetDirty(idx, true)
|
|
set.RefreshColumns()
|
|
return nil
|
|
}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
return ResultSetUpdated{}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) PutItem(idx int) tea.Cmd {
|
|
return func() tea.Msg {
|
|
resultSet := twc.state.ResultSet()
|
|
if !resultSet.IsDirty(idx) {
|
|
return events.Error(errors.New("item is not dirty"))
|
|
}
|
|
|
|
return events.PromptForInputMsg{
|
|
Prompt: "put item? ",
|
|
OnDone: func(value string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if value != "y" {
|
|
return nil
|
|
}
|
|
|
|
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return ResultSetUpdated{}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) PutItems() tea.Cmd {
|
|
return func() tea.Msg {
|
|
var (
|
|
markedItemCount int
|
|
)
|
|
var itemsToPut []models.ItemIndex
|
|
|
|
twc.state.withResultSet(func(rs *models.ResultSet) {
|
|
if markedItems := rs.MarkedItems(); len(markedItems) > 0 {
|
|
for _, mi := range markedItems {
|
|
markedItemCount += 1
|
|
if rs.IsDirty(mi.Index) {
|
|
itemsToPut = append(itemsToPut, mi)
|
|
}
|
|
}
|
|
} else {
|
|
for i, itm := range rs.Items() {
|
|
if rs.IsDirty(i) {
|
|
itemsToPut = append(itemsToPut, models.ItemIndex{Item: itm, Index: i})
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if len(itemsToPut) == 0 {
|
|
if markedItemCount > 0 {
|
|
return events.StatusMsg("no marked items are modified")
|
|
} else {
|
|
return events.StatusMsg("no items are modified")
|
|
}
|
|
}
|
|
|
|
var promptMessage string
|
|
if markedItemCount > 0 {
|
|
promptMessage = applyToN("put ", len(itemsToPut), "marked item", "marked items", "? ")
|
|
} else {
|
|
promptMessage = applyToN("put ", len(itemsToPut), "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 {
|
|
err := twc.tableService.PutSelectedItems(context.Background(), rs, itemsToPut)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
return ResultSetUpdated{
|
|
statusMessage: applyToN("", len(itemsToPut), "item", "item", " put to table"),
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) TouchItem(idx int) tea.Cmd {
|
|
return func() tea.Msg {
|
|
resultSet := twc.state.ResultSet()
|
|
if resultSet.IsDirty(idx) {
|
|
return events.Error(errors.New("cannot touch dirty items"))
|
|
}
|
|
|
|
return events.PromptForInputMsg{
|
|
Prompt: "touch item? ",
|
|
OnDone: func(value string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
if value != "y" {
|
|
return nil
|
|
}
|
|
|
|
if err := twc.tableService.PutItemAt(context.Background(), resultSet, idx); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
return ResultSetUpdated{}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) NoisyTouchItem(idx int) tea.Cmd {
|
|
return func() tea.Msg {
|
|
resultSet := twc.state.ResultSet()
|
|
if resultSet.IsDirty(idx) {
|
|
return events.Error(errors.New("cannot noisy touch dirty items"))
|
|
}
|
|
|
|
return events.PromptForInputMsg{
|
|
Prompt: "noisy touch item? ",
|
|
OnDone: func(value string) tea.Cmd {
|
|
return func() tea.Msg {
|
|
ctx := context.Background()
|
|
|
|
if value != "y" {
|
|
return nil
|
|
}
|
|
|
|
item := resultSet.Items()[0]
|
|
if err := twc.tableService.Delete(ctx, resultSet.TableInfo, []models.Item{item}); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
if err := twc.tableService.Put(ctx, resultSet.TableInfo, item); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (twc *TableWriteController) DeleteMarked() tea.Cmd {
|
|
return func() tea.Msg {
|
|
resultSet := twc.state.ResultSet()
|
|
markedItems := resultSet.MarkedItems()
|
|
|
|
if len(markedItems) == 0 {
|
|
return events.StatusMsg("no marked items")
|
|
}
|
|
|
|
return events.PromptForInputMsg{
|
|
Prompt: applyToN("delete ", len(markedItems), "item", "items", "? "),
|
|
OnDone: func(value string) tea.Cmd {
|
|
if value != "y" {
|
|
return events.SetStatus("operation aborted")
|
|
}
|
|
|
|
return func() tea.Msg {
|
|
ctx := context.Background()
|
|
if err := twc.tableService.Delete(ctx, resultSet.TableInfo, sliceutils.Map(markedItems, func(index models.ItemIndex) models.Item {
|
|
return index.Item
|
|
})); err != nil {
|
|
return events.Error(err)
|
|
}
|
|
|
|
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|