From efdc7f9e258df6350573c44725823f722784b0ee Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 30 Sep 2022 22:28:59 +1000 Subject: [PATCH] issue-28: added default limit as a setting (#29) --- cmd/dynamo-browse/main.go | 6 + internal/dynamo-browse/controllers/iface.go | 2 + .../dynamo-browse/controllers/settings.go | 16 +++ .../controllers/settings_test.go | 13 +- .../controllers/tableread_test.go | 125 ++++++------------ .../controllers/tablewrite_test.go | 91 +++++++------ .../providers/settingstore/settingstore.go | 29 +++- .../dynamo-browse/services/tables/iface.go | 3 +- .../dynamo-browse/services/tables/service.go | 22 +-- .../services/tables/service_test.go | 31 ++++- test/cmd/load-test-table/main.go | 4 + 11 files changed, 194 insertions(+), 148 deletions(-) diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 7400458..5a7bd63 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -34,6 +34,7 @@ func main() { 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 flagDefaultLimit = flag.Int("default-limit", 0, "default limit for queries and scans") var flagWorkspace = flag.String("w", "", "workspace file") flag.Parse() @@ -82,6 +83,11 @@ func main() { cli.Fatalf("unable to set read-only mode: %v", err) } } + if *flagDefaultLimit > 0 { + if err := settingStore.SetDefaultLimit(*flagDefaultLimit); err != nil { + cli.Fatalf("unable to set default limit: %v", err) + } + } tableService := tables.NewService(dynamoProvider, settingStore) workspaceService := workspaces_service.NewService(resultSetSnapshotStore) diff --git a/internal/dynamo-browse/controllers/iface.go b/internal/dynamo-browse/controllers/iface.go index bf040d0..0cb8ec2 100644 --- a/internal/dynamo-browse/controllers/iface.go +++ b/internal/dynamo-browse/controllers/iface.go @@ -16,4 +16,6 @@ type TableReadService interface { type SettingsProvider interface { IsReadOnly() (bool, error) SetReadOnly(ro bool) error + DefaultLimit() (limit int) + SetDefaultLimit(limit int) error } diff --git a/internal/dynamo-browse/controllers/settings.go b/internal/dynamo-browse/controllers/settings.go index afad5b7..21aedd2 100644 --- a/internal/dynamo-browse/controllers/settings.go +++ b/internal/dynamo-browse/controllers/settings.go @@ -1,10 +1,12 @@ package controllers import ( + "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/audax/internal/common/ui/events" "github.com/pkg/errors" "log" + "strconv" ) type SettingsController struct { @@ -35,7 +37,21 @@ func (sc *SettingsController) SetSetting(name string, value string) tea.Msg { Message: "In read-write mode", Next: SettingsUpdated{}, } + case "default-limit": + newLimit, err := strconv.Atoi(value) + if err != nil { + return errors.Wrapf(err, "bad value: %v", value) + } + + if err := sc.settings.SetDefaultLimit(newLimit); err != nil { + return events.Error(err) + } + return events.WrappedStatusMsg{ + Message: events.StatusMsg(fmt.Sprintf("Default query limit now %v", newLimit)), + Next: SettingsUpdated{}, + } } + return events.Error(errors.Errorf("unrecognised setting: %v", name)) } diff --git a/internal/dynamo-browse/controllers/settings_test.go b/internal/dynamo-browse/controllers/settings_test.go index 544045e..f2682da 100644 --- a/internal/dynamo-browse/controllers/settings_test.go +++ b/internal/dynamo-browse/controllers/settings_test.go @@ -9,7 +9,7 @@ import ( func TestSettingsController_SetSetting(t *testing.T) { t.Run("read-only setting", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{}) msg := invokeCommand(t, srv.settingsController.SetSetting("ro", "")) @@ -19,7 +19,7 @@ func TestSettingsController_SetSetting(t *testing.T) { }) t.Run("read-write setting", func(t *testing.T) { - srv := newService(t, true) + srv := newService(t, serviceConfig{isReadOnly: true}) msg := invokeCommand(t, srv.settingsController.SetSetting("rw", "")) @@ -27,4 +27,13 @@ func TestSettingsController_SetSetting(t *testing.T) { assert.IsType(t, events.WrappedStatusMsg{}, msg) assert.IsType(t, controllers.SettingsUpdated{}, msg.(events.WrappedStatusMsg).Next) }) + + t.Run("set default limit", func(t *testing.T) { + srv := newService(t, serviceConfig{}) + + assert.Equal(t, 1000, srv.settingProvider.DefaultLimit()) + invokeCommand(t, srv.settingsController.SetSetting("default-limit", "20")) + + assert.Equal(t, 20, srv.settingProvider.DefaultLimit()) + }) } diff --git a/internal/dynamo-browse/controllers/tableread_test.go b/internal/dynamo-browse/controllers/tableread_test.go index 32fce7d..46dd4a0 100644 --- a/internal/dynamo-browse/controllers/tableread_test.go +++ b/internal/dynamo-browse/controllers/tableread_test.go @@ -6,11 +6,6 @@ import ( "github.com/lmika/audax/internal/common/ui/events" "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/workspacestore" - "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" - "github.com/lmika/audax/internal/dynamo-browse/services/tables" - workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces" "github.com/lmika/audax/test/testdynamo" "github.com/stretchr/testify/assert" "os" @@ -19,45 +14,28 @@ import ( ) func TestTableReadController_InitTable(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, &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) + srv := newService(t, serviceConfig{}) - event := readController.Init() + event := srv.readController.Init() assert.IsType(t, controllers.PromptForTableMsg{}, event) }) t.Run("should scan table if table name provided", func(t *testing.T) { - readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "", false) + srv := newService(t, serviceConfig{}) - event := readController.Init() + event := srv.readController.Init() assert.IsType(t, controllers.PromptForTableMsg{}, event) }) } func TestTableReadController_ListTables(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, &mockedSetting{}) - readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "", false) - t.Run("returns a list of tables", func(t *testing.T) { - event := readController.ListTables().(controllers.PromptForTableMsg) + srv := newService(t, serviceConfig{}) + + event := srv.readController.ListTables().(controllers.PromptForTableMsg) assert.Equal(t, []string{"alpha-table", "bravo-table"}, event.Tables) @@ -71,59 +49,46 @@ func TestTableReadController_ListTables(t *testing.T) { } func TestTableReadController_Rescan(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, &mockedSetting{}) - state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "bravo-table", false) - t.Run("should perform a rescan", func(t *testing.T) { - invokeCommand(t, readController.Init()) - invokeCommand(t, readController.Rescan()) + srv := newService(t, serviceConfig{tableName: "bravo-table"}) + + invokeCommand(t, srv.readController.Init()) + invokeCommand(t, srv.readController.Rescan()) }) t.Run("should prompt to rescan if any dirty rows", func(t *testing.T) { - invokeCommand(t, readController.Init()) + srv := newService(t, serviceConfig{tableName: "bravo-table"}) - state.ResultSet().SetDirty(0, true) + invokeCommand(t, srv.readController.Init()) - invokeCommandWithPrompt(t, readController.Rescan(), "y") + srv.state.ResultSet().SetDirty(0, true) - assert.False(t, state.ResultSet().IsDirty(0)) + invokeCommandWithPrompt(t, srv.readController.Rescan(), "y") + + assert.False(t, srv.state.ResultSet().IsDirty(0)) }) t.Run("should not rescan if any dirty rows", func(t *testing.T) { - invokeCommand(t, readController.Init()) + srv := newService(t, serviceConfig{tableName: "bravo-table"}) - state.ResultSet().SetDirty(0, true) + invokeCommand(t, srv.readController.Init()) - invokeCommandWithPrompt(t, readController.Rescan(), "n") + srv.state.ResultSet().SetDirty(0, true) - assert.True(t, state.ResultSet().IsDirty(0)) + invokeCommandWithPrompt(t, srv.readController.Rescan(), "n") + + assert.True(t, srv.state.ResultSet().IsDirty(0)) }) } func TestTableReadController_ExportCSV(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, &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) { + srv := newService(t, serviceConfig{tableName: "bravo-table"}) + tempFile := tempFile(t) - invokeCommand(t, readController.Init()) - invokeCommand(t, readController.ExportCSV(tempFile)) + invokeCommand(t, srv.readController.Init()) + invokeCommand(t, srv.readController.ExportCSV(tempFile)) bts, err := os.ReadFile(tempFile) assert.NoError(t, err) @@ -137,33 +102,26 @@ func TestTableReadController_ExportCSV(t *testing.T) { }) t.Run("should return error if result set is not set", func(t *testing.T) { - tempFile := tempFile(t) - readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table", false) + srv := newService(t, serviceConfig{tableName: "non-existant-table"}) - invokeCommandExpectingError(t, readController.Init()) - invokeCommandExpectingError(t, readController.ExportCSV(tempFile)) + tempFile := tempFile(t) + + invokeCommandExpectingError(t, srv.readController.Init()) + invokeCommandExpectingError(t, srv.readController.ExportCSV(tempFile)) }) // Hidden items? } func TestTableReadController_Query(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, &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) { + srv := newService(t, serviceConfig{tableName: "bravo-table"}) + tempFile := tempFile(t) - invokeCommand(t, readController.Init()) - invokeCommandWithPrompts(t, readController.PromptForQuery(), `pk ^= "abc"`) - invokeCommand(t, readController.ExportCSV(tempFile)) + invokeCommand(t, srv.readController.Init()) + invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `pk ^= "abc"`) + invokeCommand(t, srv.readController.ExportCSV(tempFile)) bts, err := os.ReadFile(tempFile) assert.NoError(t, err) @@ -175,11 +133,12 @@ func TestTableReadController_Query(t *testing.T) { }) t.Run("should return error if result set is not set", func(t *testing.T) { - tempFile := tempFile(t) - readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table", false) + srv := newService(t, serviceConfig{tableName: "non-existant-table"}) - invokeCommandExpectingError(t, readController.Init()) - invokeCommandExpectingError(t, readController.ExportCSV(tempFile)) + tempFile := tempFile(t) + + invokeCommandExpectingError(t, srv.readController.Init()) + invokeCommandExpectingError(t, srv.readController.ExportCSV(tempFile)) }) } diff --git a/internal/dynamo-browse/controllers/tablewrite_test.go b/internal/dynamo-browse/controllers/tablewrite_test.go index 72b46cb..ce1bc67 100644 --- a/internal/dynamo-browse/controllers/tablewrite_test.go +++ b/internal/dynamo-browse/controllers/tablewrite_test.go @@ -6,6 +6,7 @@ import ( "github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/internal/dynamo-browse/models" "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" "github.com/lmika/audax/internal/dynamo-browse/services/tables" @@ -17,7 +18,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) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) assert.Len(t, srv.state.ResultSet().Items(), 3) @@ -38,7 +39,7 @@ func TestTableWriteController_NewItem(t *testing.T) { }) t.Run("should do nothing when in read-only mode", func(t *testing.T) { - srv := newService(t, true) + srv := newService(t, serviceConfig{tableName: "alpha-table", isReadOnly: true}) invokeCommand(t, srv.readController.Init()) assert.Len(t, srv.state.ResultSet().Items(), 3) @@ -86,7 +87,7 @@ 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) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.UnsetItemType, scenario.attrKey), scenario.attrValue) @@ -99,7 +100,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { }) t.Run("should use type of selected item for marked fields if unspecified", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.writeController.ToggleMark(0)) @@ -146,7 +147,7 @@ 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) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") @@ -165,7 +166,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { }) t.Run(fmt.Sprintf("should change value of nested field to type %v", scenario.attrType), func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) @@ -193,7 +194,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) { func TestTableWriteController_DeleteAttribute(t *testing.T) { t.Run("should delete top level attribute", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("age") @@ -207,7 +208,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) { }) t.Run("should delete attribute of map", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) @@ -228,7 +229,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) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -248,7 +249,7 @@ func TestTableWriteController_PutItem(t *testing.T) { }) t.Run("should not put the selected item if user does not confirm", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -272,7 +273,7 @@ func TestTableWriteController_PutItem(t *testing.T) { }) t.Run("should not put the selected item if not dirty", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -284,7 +285,7 @@ func TestTableWriteController_PutItem(t *testing.T) { }) t.Run("should not put the selected item if in read-only mode", func(t *testing.T) { - srv := newService(t, true) + srv := newService(t, serviceConfig{tableName: "alpha-table", isReadOnly: true}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -310,7 +311,7 @@ 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) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) @@ -331,7 +332,7 @@ func TestTableWriteController_PutItems(t *testing.T) { }) t.Run("only put marked items", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) @@ -360,7 +361,7 @@ func TestTableWriteController_PutItems(t *testing.T) { }) t.Run("do not put marked items which are not dirty", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) invokeCommand(t, srv.readController.Init()) @@ -389,7 +390,7 @@ func TestTableWriteController_PutItems(t *testing.T) { }) t.Run("do nothing if in read-only mode", func(t *testing.T) { - srv := newService(t, true) + srv := newService(t, serviceConfig{tableName: "alpha-table", isReadOnly: true}) invokeCommand(t, srv.readController.Init()) @@ -420,7 +421,7 @@ func TestTableWriteController_PutItems(t *testing.T) { func TestTableWriteController_TouchItem(t *testing.T) { t.Run("should put the selected item if unmodified", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -439,7 +440,7 @@ func TestTableWriteController_TouchItem(t *testing.T) { }) t.Run("should not put the selected item if modified", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -453,7 +454,7 @@ func TestTableWriteController_TouchItem(t *testing.T) { }) t.Run("should not put the selected item if in read-only mode", func(t *testing.T) { - srv := newService(t, true) + srv := newService(t, serviceConfig{tableName: "alpha-table", isReadOnly: true}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -474,7 +475,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) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -493,7 +494,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) { }) t.Run("should not put the selected item if modified", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -507,7 +508,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) { }) t.Run("should not put the selected item if in read-only mode", func(t *testing.T) { - srv := newService(t, true) + srv := newService(t, serviceConfig{tableName: "alpha-table", isReadOnly: true}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -522,7 +523,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) { func TestTableWriteController_DeleteMarked(t *testing.T) { t.Run("should delete marked items", func(t *testing.T) { - srv := newService(t, false) + srv := newService(t, serviceConfig{tableName: "alpha-table"}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -543,7 +544,7 @@ func TestTableWriteController_DeleteMarked(t *testing.T) { }) t.Run("should not delete marked items if in read-only mode", func(t *testing.T) { - srv := newService(t, true) + srv := newService(t, serviceConfig{tableName: "alpha-table", isReadOnly: true}) // Read the table invokeCommand(t, srv.readController.Init()) @@ -566,44 +567,46 @@ func TestTableWriteController_DeleteMarked(t *testing.T) { type services struct { state *controllers.State + settingProvider controllers.SettingsProvider readController *controllers.TableReadController writeController *controllers.TableWriteController settingsController *controllers.SettingsController } -func newService(t *testing.T, isReadOnly bool) *services { - resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) +type serviceConfig struct { + tableName string + isReadOnly bool +} + +func newService(t *testing.T, cfg serviceConfig) *services { + ws := testWorkspace(t) + + resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws) + settingStore := settingstore.New(ws) workspaceService := workspaces_service.NewService(resultSetSnapshotStore) itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) client := testdynamo.SetupTestTable(t, testData) - settingProvider := &mockedSetting{isReadOnly: isReadOnly} provider := dynamo.NewProvider(client) - service := tables.NewService(provider, settingProvider) + service := tables.NewService(provider, settingStore) state := controllers.NewState() - readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) - writeController := controllers.NewTableWriteController(state, service, readController, settingProvider) - settingsController := controllers.NewSettingsController(settingProvider) + readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, cfg.tableName, false) + writeController := controllers.NewTableWriteController(state, service, readController, settingStore) + settingsController := controllers.NewSettingsController(settingStore) + + if cfg.isReadOnly { + if err := settingStore.SetReadOnly(cfg.isReadOnly); err != nil { + t.Errorf("cannot set ro: %v", err) + } + } return &services{ state: state, + settingProvider: settingStore, 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/providers/settingstore/settingstore.go b/internal/dynamo-browse/providers/settingstore/settingstore.go index 908f088..f938be5 100644 --- a/internal/dynamo-browse/providers/settingstore/settingstore.go +++ b/internal/dynamo-browse/providers/settingstore/settingstore.go @@ -3,12 +3,17 @@ package settingstore import ( "github.com/asdine/storm" "github.com/lmika/audax/internal/common/workspaces" + "github.com/pkg/errors" + "log" ) const settingBucket = "Settings" const ( - keyTableReadOnly = "table_ro" + keyTableReadOnly = "ro" + keyTableDefaultLimit = "default_limit" + + defaultsDefaultLimit = 1000 ) type SettingStore struct { @@ -22,10 +27,30 @@ func New(ws *workspaces.Workspace) *SettingStore { } func (c *SettingStore) IsReadOnly() (b bool, err error) { - err = c.ws.Get(settingBucket, keyTableReadOnly, &b) + if err := c.ws.Get(settingBucket, keyTableReadOnly, &b); err != nil { + if errors.Is(err, storm.ErrNotFound) { + return false, nil + } + return false, err + } return b, err } func (c *SettingStore) SetReadOnly(ro bool) error { return c.ws.Set(settingBucket, keyTableReadOnly, ro) } + +func (c *SettingStore) DefaultLimit() (limit int) { + err := c.ws.Get(settingBucket, keyTableDefaultLimit, &limit) + if err != nil { + if !errors.Is(err, storm.ErrNotFound) { + log.Printf("warn: cannot get default limit from workspace, using default value: %v", err) + } + return defaultsDefaultLimit + } + return limit +} + +func (c *SettingStore) SetDefaultLimit(limit int) error { + return errors.Wrapf(c.ws.Set(settingBucket, keyTableDefaultLimit, &limit), "cannot set default limit to %v", limit) +} diff --git a/internal/dynamo-browse/services/tables/iface.go b/internal/dynamo-browse/services/tables/iface.go index f6448bf..ef44826 100644 --- a/internal/dynamo-browse/services/tables/iface.go +++ b/internal/dynamo-browse/services/tables/iface.go @@ -18,6 +18,7 @@ type TableProvider interface { PutItems(ctx context.Context, name string, items []models.Item) error } -type ROProvider interface { +type ConfigProvider interface { IsReadOnly() (bool, error) + DefaultLimit() int } diff --git a/internal/dynamo-browse/services/tables/service.go b/internal/dynamo-browse/services/tables/service.go index 4a6d5df..257dbc5 100644 --- a/internal/dynamo-browse/services/tables/service.go +++ b/internal/dynamo-browse/services/tables/service.go @@ -12,14 +12,14 @@ import ( ) type Service struct { - provider TableProvider - roProvider ROProvider + provider TableProvider + configProvider ConfigProvider } -func NewService(provider TableProvider, roProvider ROProvider) *Service { +func NewService(provider TableProvider, roProvider ConfigProvider) *Service { return &Service{ - provider: provider, - roProvider: roProvider, + provider: provider, + configProvider: roProvider, } } @@ -32,10 +32,10 @@ func (s *Service) Describe(ctx context.Context, table string) (*models.TableInfo } func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*models.ResultSet, error) { - return s.doScan(ctx, tableInfo, nil) + return s.doScan(ctx, tableInfo, nil, s.configProvider.DefaultLimit()) } -func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable) (*models.ResultSet, error) { +func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable, limit int) (*models.ResultSet, error) { var ( filterExpr *expression.Expression runAsQuery bool @@ -54,10 +54,10 @@ func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr var results []models.Item if runAsQuery { log.Printf("executing query") - results, err = s.provider.QueryItems(ctx, tableInfo.Name, filterExpr, 1000) + results, err = s.provider.QueryItems(ctx, tableInfo.Name, filterExpr, limit) } else { log.Printf("executing scan") - results, err = s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, 1000) + results, err = s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, limit) } if err != nil { @@ -135,11 +135,11 @@ func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items } func (s *Service) ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable) (*models.ResultSet, error) { - return s.doScan(ctx, tableInfo, expr) + return s.doScan(ctx, tableInfo, expr, s.configProvider.DefaultLimit()) } func (s *Service) assertReadWrite() error { - b, err := s.roProvider.IsReadOnly() + b, err := s.configProvider.IsReadOnly() if err != nil { return err } else if b { diff --git a/internal/dynamo-browse/services/tables/service_test.go b/internal/dynamo-browse/services/tables/service_test.go index 9e7bc2d..3135b5d 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, mockedReadOnlyProvider{readOnly: false}) + service := tables.NewService(provider, mockedConfigProvider{readOnly: false}) ti, err := service.Describe(ctx, tableName) assert.NoError(t, err) @@ -40,17 +40,30 @@ 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, mockedReadOnlyProvider{readOnly: false}) + service := tables.NewService(provider, mockedConfigProvider{readOnly: false}) ti, err := service.Describe(ctx, tableName) assert.NoError(t, err) rs, err := service.Scan(ctx, ti) assert.NoError(t, err) + assert.Len(t, rs.Items(), 3) // 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"}) }) + + t.Run("should honour default limits", func(t *testing.T) { + ctx := context.Background() + + service := tables.NewService(provider, mockedConfigProvider{readOnly: false, defaultLimit: 2}) + ti, err := service.Describe(ctx, tableName) + assert.NoError(t, err) + + rs, err := service.Scan(ctx, ti) + assert.NoError(t, err) + assert.Len(t, rs.Items(), 2) + }) } var testData = []testdynamo.TestData{ @@ -78,10 +91,18 @@ var testData = []testdynamo.TestData{ }, } -type mockedReadOnlyProvider struct { - readOnly bool +type mockedConfigProvider struct { + readOnly bool + defaultLimit int } -func (m mockedReadOnlyProvider) IsReadOnly() (bool, error) { +func (m mockedConfigProvider) IsReadOnly() (bool, error) { return m.readOnly, nil } + +func (m mockedConfigProvider) DefaultLimit() int { + if m.defaultLimit == 0 { + return 1000 + } + return m.defaultLimit +} diff --git a/test/cmd/load-test-table/main.go b/test/cmd/load-test-table/main.go index 0d9b476..8a7f7c2 100644 --- a/test/cmd/load-test-table/main.go +++ b/test/cmd/load-test-table/main.go @@ -106,6 +106,10 @@ func createTable(ctx context.Context, dynamoClient *dynamodb.Client, tableName s type notROService struct{} +func (n notROService) DefaultLimit() int { + return 1000 +} + func (n notROService) IsReadOnly() (bool, error) { return false, nil }