diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 2a7cc37..7400458 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -14,6 +14,7 @@ import ( "github.com/lmika/audax/internal/common/workspaces" "github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/internal/dynamo-browse/providers/dynamo" + "github.com/lmika/audax/internal/dynamo-browse/providers/settingstore" "github.com/lmika/audax/internal/dynamo-browse/providers/workspacestore" "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" keybindings_service "github.com/lmika/audax/internal/dynamo-browse/services/keybindings" @@ -32,6 +33,7 @@ func main() { var flagTable = flag.String("t", "", "dynamodb table name") var flagLocal = flag.String("local", "", "local endpoint") var flagDebug = flag.String("debug", "", "file to log debug messages") + var flagRO = flag.Bool("ro", false, "enable readonly mode") var flagWorkspace = flag.String("w", "", "workspace file") flag.Parse() @@ -73,14 +75,22 @@ func main() { uiStyles := styles.DefaultStyles dynamoProvider := dynamo.NewProvider(dynamoClient) resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) + settingStore := settingstore.New(ws) - tableService := tables.NewService(dynamoProvider) + if *flagRO { + if err := settingStore.SetReadOnly(*flagRO); err != nil { + cli.Fatalf("unable to set read-only mode: %v", err) + } + } + + tableService := tables.NewService(dynamoProvider, settingStore) workspaceService := workspaces_service.NewService(resultSetSnapshotStore) itemRendererService := itemrenderer.NewService(uiStyles.ItemView.FieldType, uiStyles.ItemView.MetaInfo) state := controllers.NewState() tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, itemRendererService, *flagTable, true) - tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController) + tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController, settingStore) + settingsController := controllers.NewSettingsController(settingStore) keyBindings := keybindings.Default() keyBindingService := keybindings_service.NewService(keyBindings) @@ -91,6 +101,7 @@ func main() { model := ui.NewModel( tableReadController, tableWriteController, + settingsController, itemRendererService, commandController, keyBindingController, diff --git a/internal/common/ui/events/errors.go b/internal/common/ui/events/errors.go index fd05570..3f0a2c9 100644 --- a/internal/common/ui/events/errors.go +++ b/internal/common/ui/events/errors.go @@ -10,6 +10,11 @@ type ErrorMsg error // Message indicates that a message should be shown to the user type StatusMsg string +type WrappedStatusMsg struct { + Message StatusMsg + Next tea.Msg +} + // ModeMessage indicates that the mode should be changed to the following type ModeMessage string diff --git a/internal/dynamo-browse/controllers/events.go b/internal/dynamo-browse/controllers/events.go index 9e1bbae..64fa042 100644 --- a/internal/dynamo-browse/controllers/events.go +++ b/internal/dynamo-browse/controllers/events.go @@ -10,6 +10,9 @@ type SetTableItemView struct { ViewIndex int } +type SettingsUpdated struct { +} + type NewResultSet struct { ResultSet *models.ResultSet currentFilter string @@ -44,10 +47,6 @@ func (rs NewResultSet) StatusMessage() string { } } -type SetReadWrite struct { - NewValue bool -} - type PromptForTableMsg struct { Tables []string OnSelected func(tableName string) tea.Msg diff --git a/internal/dynamo-browse/controllers/iface.go b/internal/dynamo-browse/controllers/iface.go index 390daeb..bf040d0 100644 --- a/internal/dynamo-browse/controllers/iface.go +++ b/internal/dynamo-browse/controllers/iface.go @@ -12,3 +12,8 @@ type TableReadService interface { Filter(resultSet *models.ResultSet, filter string) *models.ResultSet ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, query models.Queryable) (*models.ResultSet, error) } + +type SettingsProvider interface { + IsReadOnly() (bool, error) + SetReadOnly(ro bool) error +} diff --git a/internal/dynamo-browse/controllers/settings.go b/internal/dynamo-browse/controllers/settings.go new file mode 100644 index 0000000..afad5b7 --- /dev/null +++ b/internal/dynamo-browse/controllers/settings.go @@ -0,0 +1,49 @@ +package controllers + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/lmika/audax/internal/common/ui/events" + "github.com/pkg/errors" + "log" +) + +type SettingsController struct { + settings SettingsProvider +} + +func NewSettingsController(sp SettingsProvider) *SettingsController { + return &SettingsController{ + settings: sp, + } +} + +func (sc *SettingsController) SetSetting(name string, value string) tea.Msg { + switch name { + case "ro": + if err := sc.settings.SetReadOnly(true); err != nil { + return events.Error(err) + } + return events.WrappedStatusMsg{ + Message: "In read-only mode", + Next: SettingsUpdated{}, + } + case "rw": + if err := sc.settings.SetReadOnly(false); err != nil { + return events.Error(err) + } + return events.WrappedStatusMsg{ + Message: "In read-write mode", + Next: SettingsUpdated{}, + } + } + return events.Error(errors.Errorf("unrecognised setting: %v", name)) +} + +func (sc *SettingsController) IsReadOnly() bool { + ro, err := sc.settings.IsReadOnly() + if err != nil { + log.Printf("warn: unable to determine if R/O is available: %v", err) + return false + } + return ro +} diff --git a/internal/dynamo-browse/controllers/settings_test.go b/internal/dynamo-browse/controllers/settings_test.go new file mode 100644 index 0000000..544045e --- /dev/null +++ b/internal/dynamo-browse/controllers/settings_test.go @@ -0,0 +1,30 @@ +package controllers_test + +import ( + "github.com/lmika/audax/internal/common/ui/events" + "github.com/lmika/audax/internal/dynamo-browse/controllers" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSettingsController_SetSetting(t *testing.T) { + t.Run("read-only setting", func(t *testing.T) { + srv := newService(t, false) + + msg := invokeCommand(t, srv.settingsController.SetSetting("ro", "")) + + assert.True(t, srv.settingsController.IsReadOnly()) + assert.IsType(t, events.WrappedStatusMsg{}, msg) + assert.IsType(t, controllers.SettingsUpdated{}, msg.(events.WrappedStatusMsg).Next) + }) + + t.Run("read-write setting", func(t *testing.T) { + srv := newService(t, true) + + msg := invokeCommand(t, srv.settingsController.SetSetting("rw", "")) + + assert.False(t, srv.settingsController.IsReadOnly()) + assert.IsType(t, events.WrappedStatusMsg{}, msg) + assert.IsType(t, controllers.SettingsUpdated{}, msg.(events.WrappedStatusMsg).Next) + }) +} diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index cc5ffd9..32fce7d 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -26,7 +26,7 @@ func TestTableReadController_InitTable(t *testing.T) { itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + service := tables.NewService(provider, &mockedSetting{}) t.Run("should prompt for table if no table name provided", func(t *testing.T) { readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "", false) @@ -53,7 +53,7 @@ func TestTableReadController_ListTables(t *testing.T) { itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + service := tables.NewService(provider, &mockedSetting{}) readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "", false) t.Run("returns a list of tables", func(t *testing.T) { @@ -78,7 +78,7 @@ func TestTableReadController_Rescan(t *testing.T) { itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + service := tables.NewService(provider, &mockedSetting{}) state := controllers.NewState() readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "bravo-table", false) @@ -116,7 +116,7 @@ func TestTableReadController_ExportCSV(t *testing.T) { itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + service := tables.NewService(provider, &mockedSetting{}) readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table", false) t.Run("should export result set to CSV file", func(t *testing.T) { @@ -155,7 +155,7 @@ func TestTableReadController_Query(t *testing.T) { itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + service := tables.NewService(provider, &mockedSetting{}) readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "bravo-table", false) t.Run("should run scan with filter based on user query", func(t *testing.T) { diff --git a/internal/dynamo-browse/controllers/tablewrite.go b/internal/dynamo-browse/controllers/tablewrite.go index 1d574ad..9ff16f9 100644 --- a/internal/dynamo-browse/controllers/tablewrite.go +++ b/internal/dynamo-browse/controllers/tablewrite.go @@ -17,13 +17,15 @@ type TableWriteController struct { state *State tableService *tables.Service tableReadControllers *TableReadController + settingProvider SettingsProvider } -func NewTableWriteController(state *State, tableService *tables.Service, tableReadControllers *TableReadController) *TableWriteController { +func NewTableWriteController(state *State, tableService *tables.Service, tableReadControllers *TableReadController, settingProvider SettingsProvider) *TableWriteController { return &TableWriteController{ state: state, tableService: tableService, tableReadControllers: tableReadControllers, + settingProvider: settingProvider, } } @@ -36,6 +38,10 @@ func (twc *TableWriteController) ToggleMark(idx int) tea.Msg { } func (twc *TableWriteController) NewItem() tea.Msg { + if err := twc.assertReadWrite(); err != nil { + return events.Error(err) + } + // Work out which keys we need to prompt for rs := twc.state.ResultSet() @@ -226,6 +232,10 @@ func (twc *TableWriteController) DeleteAttribute(idx int, key string) tea.Msg { } func (twc *TableWriteController) PutItem(idx int) tea.Msg { + if err := twc.assertReadWrite(); err != nil { + return events.Error(err) + } + resultSet := twc.state.ResultSet() if !resultSet.IsDirty(idx) { return events.Error(errors.New("item is not dirty")) @@ -247,6 +257,10 @@ func (twc *TableWriteController) PutItem(idx int) tea.Msg { } func (twc *TableWriteController) PutItems() tea.Msg { + if err := twc.assertReadWrite(); err != nil { + return events.Error(err) + } + var ( markedItemCount int ) @@ -309,6 +323,10 @@ func (twc *TableWriteController) PutItems() tea.Msg { } func (twc *TableWriteController) TouchItem(idx int) tea.Msg { + if err := twc.assertReadWrite(); err != nil { + return events.Error(err) + } + resultSet := twc.state.ResultSet() if resultSet.IsDirty(idx) { return events.Error(errors.New("cannot touch dirty items")) @@ -330,6 +348,10 @@ func (twc *TableWriteController) TouchItem(idx int) tea.Msg { } func (twc *TableWriteController) NoisyTouchItem(idx int) tea.Msg { + if err := twc.assertReadWrite(); err != nil { + return events.Error(err) + } + resultSet := twc.state.ResultSet() if resultSet.IsDirty(idx) { return events.Error(errors.New("cannot noisy touch dirty items")) @@ -359,6 +381,10 @@ func (twc *TableWriteController) NoisyTouchItem(idx int) tea.Msg { } func (twc *TableWriteController) DeleteMarked() tea.Msg { + if err := twc.assertReadWrite(); err != nil { + return events.Error(err) + } + resultSet := twc.state.ResultSet() markedItems := resultSet.MarkedItems() @@ -385,6 +411,16 @@ func (twc *TableWriteController) DeleteMarked() tea.Msg { } } +func (twc *TableWriteController) assertReadWrite() error { + b, err := twc.settingProvider.IsReadOnly() + if err != nil { + return err + } else if b { + return models.ErrReadOnly + } + return nil +} + 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) diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 9118ea8..72b46cb 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -16,27 +16,16 @@ import ( ) func TestTableWriteController_NewItem(t *testing.T) { - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) - workspaceService := workspaces_service.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - t.Run("should add an item with pk and sk set at the end of the result set", func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) + srv := newService(t, false) - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) - assert.Len(t, state.ResultSet().Items(), 3) + invokeCommand(t, srv.readController.Init()) + assert.Len(t, srv.state.ResultSet().Items(), 3) // Prompt for keys - invokeCommandWithPrompts(t, writeController.NewItem(), "pk-value", "sk-value") + invokeCommandWithPrompts(t, srv.writeController.NewItem(), "pk-value", "sk-value") - newResultSet := state.ResultSet() + newResultSet := srv.state.ResultSet() assert.Len(t, newResultSet.Items(), 4) assert.Len(t, newResultSet.Items()[3], 2) @@ -47,13 +36,25 @@ func TestTableWriteController_NewItem(t *testing.T) { assert.True(t, newResultSet.IsNew(3)) assert.True(t, newResultSet.IsDirty(3)) }) + + t.Run("should do nothing when in read-only mode", func(t *testing.T) { + srv := newService(t, true) + + invokeCommand(t, srv.readController.Init()) + assert.Len(t, srv.state.ResultSet().Items(), 3) + + // Prompt for keys + invokeCommandExpectingError(t, srv.writeController.NewItem()) + + // Confirm no changes + invokeCommand(t, srv.readController.Rescan()) + + newResultSet := srv.state.ResultSet() + assert.Len(t, newResultSet.Items(), 3) + }) } func TestTableWriteController_SetAttributeValue(t *testing.T) { - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) - workspaceService := workspaces_service.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - t.Run("should preserve the type of the field if unspecified", func(t *testing.T) { scenarios := []struct { @@ -85,51 +86,32 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { for _, scenario := range scenarios { t.Run(fmt.Sprintf("should set value of field: %v", scenario.attrKey), func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) + srv := newService(t, false) - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + invokeCommand(t, srv.readController.Init()) + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.UnsetItemType, scenario.attrKey), scenario.attrValue) - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.UnsetItemType, scenario.attrKey), scenario.attrValue) - - after, _ := state.ResultSet().Items()[0][scenario.attrKey] + after, _ := srv.state.ResultSet().Items()[0][scenario.attrKey] assert.Equal(t, scenario.expected, after) - assert.True(t, state.ResultSet().IsDirty(0)) + assert.True(t, srv.state.ResultSet().IsDirty(0)) }) } }) t.Run("should use type of selected item for marked fields if unspecified", func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) + srv := newService(t, false) - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + invokeCommand(t, srv.readController.Init()) + invokeCommand(t, srv.writeController.ToggleMark(0)) + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(1, models.UnsetItemType, "alpha"), "a brand new value") - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) - invokeCommand(t, writeController.ToggleMark(0)) - invokeCommandWithPrompt(t, writeController.SetAttributeValue(1, models.UnsetItemType, "alpha"), "a brand new value") - - after1 := state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value + after1 := srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value assert.Equal(t, "a brand new value", after1) - assert.True(t, state.ResultSet().IsDirty(0)) - assert.False(t, state.ResultSet().IsDirty(1)) + assert.True(t, srv.state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(1)) }) t.Run("should change the value to a particular field if already present", func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) - - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - scenarios := []struct { attrType models.ItemType attrValue string @@ -164,97 +146,80 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { for _, scenario := range scenarios { t.Run(fmt.Sprintf("should change the value of a field to type %v", scenario.attrType), func(t *testing.T) { - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) - invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", before) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) if scenario.attrValue == "" { - invokeCommand(t, writeController.SetAttributeValue(0, scenario.attrType, "alpha")) + invokeCommand(t, srv.writeController.SetAttributeValue(0, scenario.attrType, "alpha")) } else { - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, scenario.attrType, "alpha"), scenario.attrValue) + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, scenario.attrType, "alpha"), scenario.attrValue) } - after, _ := state.ResultSet().Items()[0]["alpha"] + after, _ := srv.state.ResultSet().Items()[0]["alpha"] assert.Equal(t, scenario.expected, after) - assert.True(t, state.ResultSet().IsDirty(0)) + assert.True(t, srv.state.ResultSet().IsDirty(0)) }) t.Run(fmt.Sprintf("should change value of nested field to type %v", scenario.attrType), func(t *testing.T) { - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) - invokeCommand(t, readController.Init()) + invokeCommand(t, srv.readController.Init()) - beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) + beforeAddress := srv.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)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) if scenario.attrValue == "" { - invokeCommand(t, writeController.SetAttributeValue(0, scenario.attrType, "address.street")) + invokeCommand(t, srv.writeController.SetAttributeValue(0, scenario.attrType, "address.street")) } else { - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, scenario.attrType, "address.street"), scenario.attrValue) + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, scenario.attrType, "address.street"), scenario.attrValue) } - afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) + afterAddress := srv.state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) after := afterAddress.Value["street"] assert.Equal(t, scenario.expected, after) - assert.True(t, state.ResultSet().IsDirty(0)) + assert.True(t, srv.state.ResultSet().IsDirty(0)) }) } }) } func TestTableWriteController_DeleteAttribute(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) - - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) - workspaceService := workspaces_service.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - t.Run("should delete top level attribute", func(t *testing.T) { - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) - invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("age") + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("age") assert.Equal(t, "23", before) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) - invokeCommand(t, writeController.DeleteAttribute(0, "age")) + invokeCommand(t, srv.writeController.DeleteAttribute(0, "age")) - _, hasAge := state.ResultSet().Items()[0]["age"] + _, hasAge := srv.state.ResultSet().Items()[0]["age"] assert.False(t, hasAge) }) t.Run("should delete attribute of map", func(t *testing.T) { - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) - invokeCommand(t, readController.Init()) + invokeCommand(t, srv.readController.Init()) - beforeAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) + beforeAddress := srv.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)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) - invokeCommand(t, writeController.DeleteAttribute(0, "address.no")) + invokeCommand(t, srv.writeController.DeleteAttribute(0, "address.no")) - afterAddress := state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) + afterAddress := srv.state.ResultSet().Items()[0]["address"].(*types.AttributeValueMemberM) _, hasStreet := afterAddress.Value["no"] assert.False(t, hasStreet) @@ -262,296 +227,383 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) { } func TestTableWriteController_PutItem(t *testing.T) { - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) - workspaceService := workspaces_service.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - t.Run("should put the selected item if dirty", 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, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) // Read the table - invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", before) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) // Modify the item and put it - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") - invokeCommandWithPrompt(t, writeController.PutItem(0), "y") + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, srv.writeController.PutItem(0), "y") // Rescan the table - invokeCommand(t, readController.Rescan()) - after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Rescan()) + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "a new value", after) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) }) t.Run("should not put the selected item if user does not confirm", 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, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) // Read the table - invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", before) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) // Modify the item but do not put it - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") - invokeCommandWithPrompt(t, writeController.PutItem(0), "n") + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, srv.writeController.PutItem(0), "n") - current, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + current, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "a new value", current) - assert.True(t, state.ResultSet().IsDirty(0)) + assert.True(t, srv.state.ResultSet().IsDirty(0)) // Rescan the table to confirm item is not modified - invokeCommandWithPrompt(t, readController.Rescan(), "y") - after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommandWithPrompt(t, srv.readController.Rescan(), "y") + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", after) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) }) t.Run("should not put the selected item if not dirty", 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, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) // Read the table - invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", before) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) - invokeCommandExpectingError(t, writeController.PutItem(0)) + invokeCommandExpectingError(t, srv.writeController.PutItem(0)) + }) + + t.Run("should not put the selected item if in read-only mode", func(t *testing.T) { + srv := newService(t, true) + + // Read the table + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + + // Modify the item but do not put it + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandExpectingError(t, srv.writeController.PutItem(0)) + + current, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "a new value", current) + assert.True(t, srv.state.ResultSet().IsDirty(0)) + + // Rescan the table to confirm item is not modified + invokeCommandWithPrompt(t, srv.readController.Rescan(), "y") + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", after) + assert.False(t, srv.state.ResultSet().IsDirty(0)) }) } func TestTableWriteController_PutItems(t *testing.T) { - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) - workspaceService := workspaces_service.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - t.Run("should put all dirty items if none are marked", func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) + srv := newService(t, false) - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) + invokeCommand(t, srv.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, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(2, models.StringItemType, "alpha"), "another new value") - invokeCommandWithPrompt(t, writeController.PutItems(), "y") + invokeCommandWithPrompt(t, srv.writeController.PutItems(), "y") // Rescan the table - invokeCommand(t, readController.Rescan()) + invokeCommand(t, srv.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.Equal(t, "a new value", srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", srv.state.ResultSet().Items()[2]["alpha"].(*types.AttributeValueMemberS).Value) - assert.False(t, state.ResultSet().IsDirty(0)) - assert.False(t, state.ResultSet().IsDirty(2)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(2)) }) t.Run("only put marked items", func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) + srv := newService(t, false) - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) + invokeCommand(t, srv.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, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(2, models.StringItemType, "alpha"), "another new value") + invokeCommand(t, srv.writeController.ToggleMark(0)) - invokeCommandWithPrompt(t, writeController.PutItems(), "y") + invokeCommandWithPrompt(t, srv.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.Equal(t, "a new value", srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", srv.state.ResultSet().Items()[2]["alpha"].(*types.AttributeValueMemberS).Value) - assert.False(t, state.ResultSet().IsDirty(0)) - assert.True(t, state.ResultSet().IsDirty(2)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + assert.True(t, srv.state.ResultSet().IsDirty(2)) // Rescan the table and verify dirty items were not written - invokeCommandWithPrompt(t, readController.Rescan(), "y") + invokeCommandWithPrompt(t, srv.readController.Rescan(), "y") - assert.Equal(t, "a new value", state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) - assert.Nil(t, state.ResultSet().Items()[2]["alpha"]) + assert.Equal(t, "a new value", srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Nil(t, srv.state.ResultSet().Items()[2]["alpha"]) - assert.False(t, state.ResultSet().IsDirty(0)) - assert.False(t, state.ResultSet().IsDirty(2)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(2)) }) - t.Run("do not put marked items which are not diry", func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) + t.Run("do not put marked items which are not dirty", func(t *testing.T) { + srv := newService(t, false) - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) - - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) - - invokeCommand(t, readController.Init()) + invokeCommand(t, srv.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)) + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(2, models.StringItemType, "alpha"), "another new value") + invokeCommand(t, srv.writeController.ToggleMark(1)) - invokeCommand(t, writeController.PutItems()) + invokeCommand(t, srv.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.Equal(t, "a new value", srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", srv.state.ResultSet().Items()[2]["alpha"].(*types.AttributeValueMemberS).Value) - assert.True(t, state.ResultSet().IsDirty(0)) - assert.True(t, state.ResultSet().IsDirty(2)) + assert.True(t, srv.state.ResultSet().IsDirty(0)) + assert.True(t, srv.state.ResultSet().IsDirty(2)) // Rescan the table and verify dirty items were not written - invokeCommandWithPrompt(t, readController.Rescan(), "y") + invokeCommandWithPrompt(t, srv.readController.Rescan(), "y") - assert.Equal(t, "This is some value", state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) - assert.Nil(t, state.ResultSet().Items()[2]["alpha"]) + assert.Equal(t, "This is some value", srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Nil(t, srv.state.ResultSet().Items()[2]["alpha"]) - assert.False(t, state.ResultSet().IsDirty(0)) - assert.False(t, state.ResultSet().IsDirty(2)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(2)) + }) + + t.Run("do nothing if in read-only mode", func(t *testing.T) { + srv := newService(t, true) + + invokeCommand(t, srv.readController.Init()) + + // Modify the item and put it + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(2, models.StringItemType, "alpha"), "another new value") + invokeCommand(t, srv.writeController.ToggleMark(0)) + + invokeCommandExpectingError(t, srv.writeController.PutItems()) + + // Verify dirty items are unchanged + assert.Equal(t, "a new value", srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "another new value", srv.state.ResultSet().Items()[2]["alpha"].(*types.AttributeValueMemberS).Value) + + assert.True(t, srv.state.ResultSet().IsDirty(0)) + assert.True(t, srv.state.ResultSet().IsDirty(2)) + + // Rescan the table and verify dirty items were not written + invokeCommandWithPrompt(t, srv.readController.Rescan(), "y") + + assert.Equal(t, "This is some value", srv.state.ResultSet().Items()[0]["alpha"].(*types.AttributeValueMemberS).Value) + assert.Nil(t, srv.state.ResultSet().Items()[2]["alpha"]) + + assert.False(t, srv.state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(2)) }) } func TestTableWriteController_TouchItem(t *testing.T) { - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) - workspaceService := workspaces_service.NewService(resultSetSnapshotStore) - itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - t.Run("should put the selected item if unmodified", 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, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) // Read the table - invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", before) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) // Modify the item and put it - invokeCommandWithPrompt(t, writeController.TouchItem(0), "y") + invokeCommandWithPrompt(t, srv.writeController.TouchItem(0), "y") // Rescan the table - invokeCommand(t, readController.Rescan()) - after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Rescan()) + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", after) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) }) t.Run("should not put the selected item if modified", 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, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + srv := newService(t, false) // Read the table - invokeCommand(t, readController.Init()) - before, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") assert.Equal(t, "This is some value", before) - assert.False(t, state.ResultSet().IsDirty(0)) + assert.False(t, srv.state.ResultSet().IsDirty(0)) // Modify the item and put it - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") - invokeCommandExpectingError(t, writeController.TouchItem(0)) + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandExpectingError(t, srv.writeController.TouchItem(0)) + }) + + t.Run("should not put the selected item if in read-only mode", func(t *testing.T) { + srv := newService(t, true) + + // Read the table + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + + // Modify the item and put it + invokeCommandExpectingError(t, srv.writeController.TouchItem(0)) + + // Rescan the table + invokeCommand(t, srv.readController.Rescan()) + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", after) + assert.False(t, srv.state.ResultSet().IsDirty(0)) }) } func TestTableWriteController_NoisyTouchItem(t *testing.T) { + t.Run("should delete and put the selected item if unmodified", func(t *testing.T) { + srv := newService(t, false) + + // Read the table + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + + // Modify the item and put it + invokeCommandWithPrompt(t, srv.writeController.NoisyTouchItem(0), "y") + + // Rescan the table + invokeCommand(t, srv.readController.Rescan()) + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", after) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + }) + + t.Run("should not put the selected item if modified", func(t *testing.T) { + srv := newService(t, false) + + // Read the table + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + + // Modify the item and put it + invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") + invokeCommandExpectingError(t, srv.writeController.NoisyTouchItem(0)) + }) + + t.Run("should not put the selected item if in read-only mode", func(t *testing.T) { + srv := newService(t, true) + + // Read the table + invokeCommand(t, srv.readController.Init()) + before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", before) + assert.False(t, srv.state.ResultSet().IsDirty(0)) + + // Modify the item and put it + invokeCommandExpectingError(t, srv.writeController.NoisyTouchItem(0)) + }) +} + +func TestTableWriteController_DeleteMarked(t *testing.T) { + t.Run("should delete marked items", func(t *testing.T) { + srv := newService(t, false) + + // Read the table + invokeCommand(t, srv.readController.Init()) + assert.Len(t, srv.state.ResultSet().Items(), 3) + + // Mark some items + invokeCommand(t, srv.writeController.ToggleMark(0)) + invokeCommand(t, srv.writeController.ToggleMark(2)) + + // Delete it + invokeCommandWithPrompt(t, srv.writeController.DeleteMarked(), "y") + + // Rescan and confirm marked items are deleted + invokeCommand(t, srv.readController.Init()) + assert.Len(t, srv.state.ResultSet().Items(), 1) + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is another some value", after) + }) + + t.Run("should not delete marked items if in read-only mode", func(t *testing.T) { + srv := newService(t, true) + + // Read the table + invokeCommand(t, srv.readController.Init()) + assert.Len(t, srv.state.ResultSet().Items(), 3) + + // Mark some items + invokeCommand(t, srv.writeController.ToggleMark(0)) + invokeCommand(t, srv.writeController.ToggleMark(2)) + + // Delete it + invokeCommandExpectingError(t, srv.writeController.DeleteMarked()) + + // Rescan and confirm marked items are not deleted + invokeCommand(t, srv.readController.Init()) + assert.Len(t, srv.state.ResultSet().Items(), 3) + after, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") + assert.Equal(t, "This is some value", after) + }) +} + +type services struct { + state *controllers.State + readController *controllers.TableReadController + writeController *controllers.TableWriteController + settingsController *controllers.SettingsController +} + +func newService(t *testing.T, isReadOnly bool) *services { resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) workspaceService := workspaces_service.NewService(resultSetSnapshotStore) itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) - t.Run("should delete and put the selected item if unmodified", func(t *testing.T) { - client := testdynamo.SetupTestTable(t, testData) + client := testdynamo.SetupTestTable(t, testData) - provider := dynamo.NewProvider(client) - service := tables.NewService(provider) + settingProvider := &mockedSetting{isReadOnly: isReadOnly} + provider := dynamo.NewProvider(client) + service := tables.NewService(provider, settingProvider) - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) + state := controllers.NewState() + readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) + writeController := controllers.NewTableWriteController(state, service, readController, settingProvider) + settingsController := controllers.NewSettingsController(settingProvider) - // Read the table - 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)) - - // Modify the item and put it - invokeCommandWithPrompt(t, writeController.NoisyTouchItem(0), "y") - - // Rescan the table - invokeCommand(t, readController.Rescan()) - after, _ := state.ResultSet().Items()[0].AttributeValueAsString("alpha") - assert.Equal(t, "This is some value", after) - assert.False(t, state.ResultSet().IsDirty(0)) - }) - - t.Run("should not put the selected item if modified", 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, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController) - - // Read the table - 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)) - - // Modify the item and put it - invokeCommandWithPrompt(t, writeController.SetAttributeValue(0, models.StringItemType, "alpha"), "a new value") - invokeCommandExpectingError(t, writeController.NoisyTouchItem(0)) - }) + return &services{ + state: state, + readController: readController, + writeController: writeController, + settingsController: settingsController, + } +} + +type mockedSetting struct { + isReadOnly bool +} + +func (ms *mockedSetting) SetReadOnly(ro bool) error { + ms.isReadOnly = ro + return nil +} + +func (ms *mockedSetting) IsReadOnly() (bool, error) { + return ms.isReadOnly, nil } diff --git a/internal/dynamo-browse/models/errors.go b/internal/dynamo-browse/models/errors.go new file mode 100644 index 0000000..9b5275e --- /dev/null +++ b/internal/dynamo-browse/models/errors.go @@ -0,0 +1,5 @@ +package models + +import "github.com/pkg/errors" + +var ErrReadOnly = errors.New("in read-only mode") diff --git a/internal/dynamo-browse/providers/settingstore/settingstore.go b/internal/dynamo-browse/providers/settingstore/settingstore.go new file mode 100644 index 0000000..908f088 --- /dev/null +++ b/internal/dynamo-browse/providers/settingstore/settingstore.go @@ -0,0 +1,31 @@ +package settingstore + +import ( + "github.com/asdine/storm" + "github.com/lmika/audax/internal/common/workspaces" +) + +const settingBucket = "Settings" + +const ( + keyTableReadOnly = "table_ro" +) + +type SettingStore struct { + ws storm.Node +} + +func New(ws *workspaces.Workspace) *SettingStore { + return &SettingStore{ + ws: ws.DB(), + } +} + +func (c *SettingStore) IsReadOnly() (b bool, err error) { + err = c.ws.Get(settingBucket, keyTableReadOnly, &b) + return b, err +} + +func (c *SettingStore) SetReadOnly(ro bool) error { + return c.ws.Set(settingBucket, keyTableReadOnly, ro) +} diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go index 61d7ba3..f6448bf 100644 --- a/internal/dynamo-browse/services/tables/iface.go +++ b/internal/dynamo-browse/services/tables/iface.go @@ -17,3 +17,7 @@ type TableProvider interface { PutItem(ctx context.Context, name string, item models.Item) error PutItems(ctx context.Context, name string, items []models.Item) error } + +type ROProvider interface { + IsReadOnly() (bool, error) +} diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index edf1f10..4a6d5df 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -12,12 +12,14 @@ import ( ) type Service struct { - provider TableProvider + provider TableProvider + roProvider ROProvider } -func NewService(provider TableProvider) *Service { +func NewService(provider TableProvider, roProvider ROProvider) *Service { return &Service{ - provider: provider, + provider: provider, + roProvider: roProvider, } } @@ -75,10 +77,18 @@ func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr } func (s *Service) Put(ctx context.Context, tableInfo *models.TableInfo, item models.Item) error { + if err := s.assertReadWrite(); err != nil { + return err + } + return s.provider.PutItem(ctx, tableInfo.Name, item) } func (s *Service) PutItemAt(ctx context.Context, resultSet *models.ResultSet, index int) error { + if err := s.assertReadWrite(); err != nil { + return err + } + item := resultSet.Items()[index] if err := s.provider.PutItem(ctx, resultSet.TableInfo.Name, item); err != nil { return err @@ -90,6 +100,10 @@ func (s *Service) PutItemAt(ctx context.Context, resultSet *models.ResultSet, in } func (s *Service) PutSelectedItems(ctx context.Context, resultSet *models.ResultSet, markedItems []models.ItemIndex) error { + if err := s.assertReadWrite(); err != nil { + return err + } + if len(markedItems) == 0 { return nil } @@ -108,6 +122,10 @@ func (s *Service) PutSelectedItems(ctx context.Context, resultSet *models.Result } func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items []models.Item) error { + if err := s.assertReadWrite(); err != nil { + return err + } + for _, item := range items { if err := s.provider.DeleteItem(ctx, tableInfo.Name, item.KeyValue(tableInfo)); err != nil { return errors.Wrapf(err, "cannot delete item") @@ -120,6 +138,16 @@ func (s *Service) ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, return s.doScan(ctx, tableInfo, expr) } +func (s *Service) assertReadWrite() error { + b, err := s.roProvider.IsReadOnly() + if err != nil { + return err + } else if b { + return models.ErrReadOnly + } + return nil +} + // TODO: move into a new service func (s *Service) Filter(resultSet *models.ResultSet, filter string) *models.ResultSet { for i, item := range resultSet.Items() { diff --git a/internal/dynamo-browse/services/tables/service_test.go b/internal/dynamo-browse/services/tables/service_test.go index 91fa071..9e7bc2d 100644 --- a/internal/dynamo-browse/services/tables/service_test.go +++ b/internal/dynamo-browse/services/tables/service_test.go @@ -19,7 +19,7 @@ func TestService_Describe(t *testing.T) { t.Run("return details of the table", func(t *testing.T) { ctx := context.Background() - service := tables.NewService(provider) + service := tables.NewService(provider, mockedReadOnlyProvider{readOnly: false}) ti, err := service.Describe(ctx, tableName) assert.NoError(t, err) @@ -40,7 +40,7 @@ func TestService_Scan(t *testing.T) { t.Run("return all columns and fields in sorted order", func(t *testing.T) { ctx := context.Background() - service := tables.NewService(provider) + service := tables.NewService(provider, mockedReadOnlyProvider{readOnly: false}) ti, err := service.Describe(ctx, tableName) assert.NoError(t, err) @@ -77,3 +77,11 @@ var testData = []testdynamo.TestData{ }, }, } + +type mockedReadOnlyProvider struct { + readOnly bool +} + +func (m mockedReadOnlyProvider) IsReadOnly() (bool, error) { + return m.readOnly, nil +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index fd50a2c..a2b8132 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -39,6 +39,7 @@ const ( type Model struct { tableReadController *controllers.TableReadController tableWriteController *controllers.TableWriteController + settingsController *controllers.SettingsController commandController *commandctrl.CommandController itemEdit *dynamoitemedit.Model statusAndPrompt *statusandprompt.StatusAndPrompt @@ -56,6 +57,7 @@ type Model struct { func NewModel( rc *controllers.TableReadController, wc *controllers.TableWriteController, + settingsController *controllers.SettingsController, itemRendererService *itemrenderer.Service, cc *commandctrl.CommandController, keyBindingController *controllers.KeyBindingController, @@ -63,7 +65,7 @@ func NewModel( ) Model { uiStyles := styles.DefaultStyles - dtv := dynamotableview.New(defaultKeyMap.TableView, uiStyles) + dtv := dynamotableview.New(defaultKeyMap.TableView, settingsController, uiStyles) div := dynamoitemview.New(itemRendererService, uiStyles) mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div) @@ -141,6 +143,15 @@ func NewModel( } return events.StatusMsg(s.String()) }, + "set": func(ctx commandctrl.ExecContext, args []string) tea.Msg { + switch len(args) { + case 1: + return settingsController.SetSetting(args[0], "") + case 2: + return settingsController.SetSetting(args[0], args[1]) + } + return events.Error(errors.New("expected: settingName [value]")) + }, "rebind": func(ctx commandctrl.ExecContext, args []string) tea.Msg { if len(args) != 2 { return events.Error(errors.New("expected: bindingName newKey")) diff --git a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go index e70092b..1020ad2 100644 --- a/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go +++ b/internal/dynamo-browse/ui/teamodels/dynamotableview/model.go @@ -12,6 +12,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/styles" table "github.com/lmika/go-bubble-table" + "strings" ) var ( @@ -21,29 +22,38 @@ var ( Background(lipgloss.Color("#4479ff")) ) +type Setting interface { + IsReadOnly() bool +} + type Model struct { frameTitle frame.FrameTitle table table.Model w, h int keyBinding *keybindings.TableKeyBinding + setting Setting // model state - colOffset int - rows []table.Row - resultSet *models.ResultSet + isReadOnly bool + colOffset int + rows []table.Row + resultSet *models.ResultSet } -func New(keyBinding *keybindings.TableKeyBinding, uiStyles styles.Styles) *Model { +func New(keyBinding *keybindings.TableKeyBinding, setting Setting, uiStyles styles.Styles) *Model { tbl := table.New(table.SimpleColumns([]string{"pk", "sk"}), 100, 100) rows := make([]table.Row, 0) tbl.SetRows(rows) frameTitle := frame.NewFrameTitle("No table", true, uiStyles.Frames) + isReadOnly := setting.IsReadOnly() return &Model{ + isReadOnly: isReadOnly, frameTitle: frameTitle, table: tbl, keyBinding: keyBinding, + setting: setting, } } @@ -57,6 +67,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.resultSet = msg.ResultSet m.updateTable() return m, m.postSelectedItemChanged + case controllers.SettingsUpdated: + m.updateTableHeading() + return m, nil case tea.KeyMsg: switch { // Table nav @@ -113,10 +126,19 @@ func (m *Model) Resize(w, h int) layout.ResizingModel { return m } -func (m *Model) updateTable() { - m.colOffset = 0 +func (m *Model) updateTableHeading() { + tableName := new(strings.Builder) + tableName.WriteString("Table: " + m.resultSet.TableInfo.Name) + if m.setting.IsReadOnly() { + tableName.WriteString(" [RO]") + } - m.frameTitle.SetTitle("Table: " + m.resultSet.TableInfo.Name) + m.frameTitle.SetTitle(tableName.String()) +} + +func (m *Model) updateTable() { + m.updateTableHeading() + m.colOffset = 0 m.rebuildTable() } diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go index e2d2f4f..3c1b3dd 100644 --- a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go @@ -7,6 +7,7 @@ import ( "github.com/lmika/audax/internal/common/sliceutils" "github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/utils" "log" ) @@ -36,11 +37,16 @@ func (s *StatusAndPrompt) Init() tea.Cmd { } func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cc utils.CmdCollector + switch msg := msg.(type) { case events.ErrorMsg: s.statusMessage = "Error: " + msg.Error() case events.StatusMsg: s.statusMessage = string(msg) + case events.WrappedStatusMsg: + s.statusMessage = string(msg.Message) + cc.Add(func() tea.Msg { return msg.Next }) case events.ModeMessage: s.modeLine = string(msg) case events.MessageWithStatus: @@ -86,9 +92,9 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - newModel, cmd := s.model.Update(msg) + newModel := cc.Collect(s.model.Update(msg)) s.model = newModel.(layout.ResizingModel) - return s, cmd + return s, cc.Cmd() } func (s *StatusAndPrompt) InPrompt() bool { diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index deae11c..0d9b476 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -49,7 +49,7 @@ func main() { } dynamoProvider := dynamo.NewProvider(dynamoClient) - tableService := tables.NewService(dynamoProvider) + tableService := tables.NewService(dynamoProvider, notROService{}) _, _ = tableService, tableInfo @@ -103,3 +103,9 @@ func createTable(ctx context.Context, dynamoClient *dynamodb.Client, tableName s } return nil } + +type notROService struct{} + +func (n notROService) IsReadOnly() (bool, error) { + return false, nil +}