issue-28: added default limit as a setting (#29)

This commit is contained in:
Leon Mika 2022-09-30 22:28:59 +10:00 committed by GitHub
parent 93ec519127
commit efdc7f9e25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 194 additions and 148 deletions

View file

@ -34,6 +34,7 @@ func main() {
var flagLocal = flag.String("local", "", "local endpoint") var flagLocal = flag.String("local", "", "local endpoint")
var flagDebug = flag.String("debug", "", "file to log debug messages") var flagDebug = flag.String("debug", "", "file to log debug messages")
var flagRO = flag.Bool("ro", false, "enable readonly mode") 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") var flagWorkspace = flag.String("w", "", "workspace file")
flag.Parse() flag.Parse()
@ -82,6 +83,11 @@ func main() {
cli.Fatalf("unable to set read-only mode: %v", err) 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) tableService := tables.NewService(dynamoProvider, settingStore)
workspaceService := workspaces_service.NewService(resultSetSnapshotStore) workspaceService := workspaces_service.NewService(resultSetSnapshotStore)

View file

@ -16,4 +16,6 @@ type TableReadService interface {
type SettingsProvider interface { type SettingsProvider interface {
IsReadOnly() (bool, error) IsReadOnly() (bool, error)
SetReadOnly(ro bool) error SetReadOnly(ro bool) error
DefaultLimit() (limit int)
SetDefaultLimit(limit int) error
} }

View file

@ -1,10 +1,12 @@
package controllers package controllers
import ( import (
"fmt"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/audax/internal/common/ui/events"
"github.com/pkg/errors" "github.com/pkg/errors"
"log" "log"
"strconv"
) )
type SettingsController struct { type SettingsController struct {
@ -35,7 +37,21 @@ func (sc *SettingsController) SetSetting(name string, value string) tea.Msg {
Message: "In read-write mode", Message: "In read-write mode",
Next: SettingsUpdated{}, 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)) return events.Error(errors.Errorf("unrecognised setting: %v", name))
} }

View file

@ -9,7 +9,7 @@ import (
func TestSettingsController_SetSetting(t *testing.T) { func TestSettingsController_SetSetting(t *testing.T) {
t.Run("read-only setting", func(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", "")) 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) { 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", "")) 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, events.WrappedStatusMsg{}, msg)
assert.IsType(t, controllers.SettingsUpdated{}, msg.(events.WrappedStatusMsg).Next) 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())
})
} }

View file

@ -6,11 +6,6 @@ import (
"github.com/lmika/audax/internal/common/ui/events" "github.com/lmika/audax/internal/common/ui/events"
"github.com/lmika/audax/internal/common/workspaces" "github.com/lmika/audax/internal/common/workspaces"
"github.com/lmika/audax/internal/dynamo-browse/controllers" "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/lmika/audax/test/testdynamo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"os" "os"
@ -19,45 +14,28 @@ import (
) )
func TestTableReadController_InitTable(t *testing.T) { 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) { 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) assert.IsType(t, controllers.PromptForTableMsg{}, event)
}) })
t.Run("should scan table if table name provided", func(t *testing.T) { 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) assert.IsType(t, controllers.PromptForTableMsg{}, event)
}) })
} }
func TestTableReadController_ListTables(t *testing.T) { 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) { 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) 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) { 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) { t.Run("should perform a rescan", func(t *testing.T) {
invokeCommand(t, readController.Init()) srv := newService(t, serviceConfig{tableName: "bravo-table"})
invokeCommand(t, readController.Rescan())
invokeCommand(t, srv.readController.Init())
invokeCommand(t, srv.readController.Rescan())
}) })
t.Run("should prompt to rescan if any dirty rows", func(t *testing.T) { 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) { 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) { 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) { t.Run("should export result set to CSV file", func(t *testing.T) {
srv := newService(t, serviceConfig{tableName: "bravo-table"})
tempFile := tempFile(t) tempFile := tempFile(t)
invokeCommand(t, readController.Init()) invokeCommand(t, srv.readController.Init())
invokeCommand(t, readController.ExportCSV(tempFile)) invokeCommand(t, srv.readController.ExportCSV(tempFile))
bts, err := os.ReadFile(tempFile) bts, err := os.ReadFile(tempFile)
assert.NoError(t, err) 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) { t.Run("should return error if result set is not set", func(t *testing.T) {
tempFile := tempFile(t) srv := newService(t, serviceConfig{tableName: "non-existant-table"})
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table", false)
invokeCommandExpectingError(t, readController.Init()) tempFile := tempFile(t)
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
invokeCommandExpectingError(t, srv.readController.Init())
invokeCommandExpectingError(t, srv.readController.ExportCSV(tempFile))
}) })
// Hidden items? // Hidden items?
} }
func TestTableReadController_Query(t *testing.T) { 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) { 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) tempFile := tempFile(t)
invokeCommand(t, readController.Init()) invokeCommand(t, srv.readController.Init())
invokeCommandWithPrompts(t, readController.PromptForQuery(), `pk ^= "abc"`) invokeCommandWithPrompts(t, srv.readController.PromptForQuery(), `pk ^= "abc"`)
invokeCommand(t, readController.ExportCSV(tempFile)) invokeCommand(t, srv.readController.ExportCSV(tempFile))
bts, err := os.ReadFile(tempFile) bts, err := os.ReadFile(tempFile)
assert.NoError(t, err) 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) { t.Run("should return error if result set is not set", func(t *testing.T) {
tempFile := tempFile(t) srv := newService(t, serviceConfig{tableName: "non-existant-table"})
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, itemRendererService, "non-existant-table", false)
invokeCommandExpectingError(t, readController.Init()) tempFile := tempFile(t)
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
invokeCommandExpectingError(t, srv.readController.Init())
invokeCommandExpectingError(t, srv.readController.ExportCSV(tempFile))
}) })
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/lmika/audax/internal/dynamo-browse/controllers" "github.com/lmika/audax/internal/dynamo-browse/controllers"
"github.com/lmika/audax/internal/dynamo-browse/models" "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/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/providers/workspacestore"
"github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer" "github.com/lmika/audax/internal/dynamo-browse/services/itemrenderer"
"github.com/lmika/audax/internal/dynamo-browse/services/tables" "github.com/lmika/audax/internal/dynamo-browse/services/tables"
@ -17,7 +18,7 @@ import (
func TestTableWriteController_NewItem(t *testing.T) { 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) { 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()) invokeCommand(t, srv.readController.Init())
assert.Len(t, srv.state.ResultSet().Items(), 3) 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) { 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()) invokeCommand(t, srv.readController.Init())
assert.Len(t, srv.state.ResultSet().Items(), 3) assert.Len(t, srv.state.ResultSet().Items(), 3)
@ -86,7 +87,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(fmt.Sprintf("should set value of field: %v", scenario.attrKey), func(t *testing.T) { 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()) invokeCommand(t, srv.readController.Init())
invokeCommandWithPrompt(t, srv.writeController.SetAttributeValue(0, models.UnsetItemType, scenario.attrKey), scenario.attrValue) 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) { 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.readController.Init())
invokeCommand(t, srv.writeController.ToggleMark(0)) invokeCommand(t, srv.writeController.ToggleMark(0))
@ -146,7 +147,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(fmt.Sprintf("should change the value of a field to type %v", scenario.attrType), func(t *testing.T) { 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()) invokeCommand(t, srv.readController.Init())
before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("alpha") 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) { 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()) invokeCommand(t, srv.readController.Init())
@ -193,7 +194,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
func TestTableWriteController_DeleteAttribute(t *testing.T) { func TestTableWriteController_DeleteAttribute(t *testing.T) {
t.Run("should delete top level attribute", func(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()) invokeCommand(t, srv.readController.Init())
before, _ := srv.state.ResultSet().Items()[0].AttributeValueAsString("age") 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) { 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()) invokeCommand(t, srv.readController.Init())
@ -228,7 +229,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
func TestTableWriteController_PutItem(t *testing.T) { func TestTableWriteController_PutItem(t *testing.T) {
t.Run("should put the selected item if dirty", func(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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.readController.Init())
@ -310,7 +311,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
func TestTableWriteController_PutItems(t *testing.T) { func TestTableWriteController_PutItems(t *testing.T) {
t.Run("should put all dirty items if none are marked", func(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()) 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) { 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()) 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) { 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()) 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) { 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()) invokeCommand(t, srv.readController.Init())
@ -420,7 +421,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
func TestTableWriteController_TouchItem(t *testing.T) { func TestTableWriteController_TouchItem(t *testing.T) {
t.Run("should put the selected item if unmodified", func(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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.readController.Init())
@ -474,7 +475,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
func TestTableWriteController_NoisyTouchItem(t *testing.T) { func TestTableWriteController_NoisyTouchItem(t *testing.T) {
t.Run("should delete and put the selected item if unmodified", func(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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.readController.Init())
@ -522,7 +523,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) {
func TestTableWriteController_DeleteMarked(t *testing.T) { func TestTableWriteController_DeleteMarked(t *testing.T) {
t.Run("should delete marked items", func(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 // Read the table
invokeCommand(t, srv.readController.Init()) 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) { 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 // Read the table
invokeCommand(t, srv.readController.Init()) invokeCommand(t, srv.readController.Init())
@ -566,44 +567,46 @@ func TestTableWriteController_DeleteMarked(t *testing.T) {
type services struct { type services struct {
state *controllers.State state *controllers.State
settingProvider controllers.SettingsProvider
readController *controllers.TableReadController readController *controllers.TableReadController
writeController *controllers.TableWriteController writeController *controllers.TableWriteController
settingsController *controllers.SettingsController settingsController *controllers.SettingsController
} }
func newService(t *testing.T, isReadOnly bool) *services { type serviceConfig struct {
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t)) 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) workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer()) itemRendererService := itemrenderer.NewService(itemrenderer.PlainTextRenderer(), itemrenderer.PlainTextRenderer())
client := testdynamo.SetupTestTable(t, testData) client := testdynamo.SetupTestTable(t, testData)
settingProvider := &mockedSetting{isReadOnly: isReadOnly}
provider := dynamo.NewProvider(client) provider := dynamo.NewProvider(client)
service := tables.NewService(provider, settingProvider) service := tables.NewService(provider, settingStore)
state := controllers.NewState() state := controllers.NewState()
readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, "alpha-table", false) readController := controllers.NewTableReadController(state, service, workspaceService, itemRendererService, cfg.tableName, false)
writeController := controllers.NewTableWriteController(state, service, readController, settingProvider) writeController := controllers.NewTableWriteController(state, service, readController, settingStore)
settingsController := controllers.NewSettingsController(settingProvider) settingsController := controllers.NewSettingsController(settingStore)
if cfg.isReadOnly {
if err := settingStore.SetReadOnly(cfg.isReadOnly); err != nil {
t.Errorf("cannot set ro: %v", err)
}
}
return &services{ return &services{
state: state, state: state,
settingProvider: settingStore,
readController: readController, readController: readController,
writeController: writeController, writeController: writeController,
settingsController: settingsController, 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
}

View file

@ -3,12 +3,17 @@ package settingstore
import ( import (
"github.com/asdine/storm" "github.com/asdine/storm"
"github.com/lmika/audax/internal/common/workspaces" "github.com/lmika/audax/internal/common/workspaces"
"github.com/pkg/errors"
"log"
) )
const settingBucket = "Settings" const settingBucket = "Settings"
const ( const (
keyTableReadOnly = "table_ro" keyTableReadOnly = "ro"
keyTableDefaultLimit = "default_limit"
defaultsDefaultLimit = 1000
) )
type SettingStore struct { type SettingStore struct {
@ -22,10 +27,30 @@ func New(ws *workspaces.Workspace) *SettingStore {
} }
func (c *SettingStore) IsReadOnly() (b bool, err error) { 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 return b, err
} }
func (c *SettingStore) SetReadOnly(ro bool) error { func (c *SettingStore) SetReadOnly(ro bool) error {
return c.ws.Set(settingBucket, keyTableReadOnly, ro) 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)
}

View file

@ -18,6 +18,7 @@ type TableProvider interface {
PutItems(ctx context.Context, name string, items []models.Item) error PutItems(ctx context.Context, name string, items []models.Item) error
} }
type ROProvider interface { type ConfigProvider interface {
IsReadOnly() (bool, error) IsReadOnly() (bool, error)
DefaultLimit() int
} }

View file

@ -12,14 +12,14 @@ import (
) )
type Service struct { type Service struct {
provider TableProvider provider TableProvider
roProvider ROProvider configProvider ConfigProvider
} }
func NewService(provider TableProvider, roProvider ROProvider) *Service { func NewService(provider TableProvider, roProvider ConfigProvider) *Service {
return &Service{ return &Service{
provider: provider, provider: provider,
roProvider: roProvider, 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) { 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 ( var (
filterExpr *expression.Expression filterExpr *expression.Expression
runAsQuery bool runAsQuery bool
@ -54,10 +54,10 @@ func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr
var results []models.Item var results []models.Item
if runAsQuery { if runAsQuery {
log.Printf("executing query") 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 { } else {
log.Printf("executing scan") 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 { 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) { 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 { func (s *Service) assertReadWrite() error {
b, err := s.roProvider.IsReadOnly() b, err := s.configProvider.IsReadOnly()
if err != nil { if err != nil {
return err return err
} else if b { } else if b {

View file

@ -19,7 +19,7 @@ func TestService_Describe(t *testing.T) {
t.Run("return details of the table", func(t *testing.T) { t.Run("return details of the table", func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
service := tables.NewService(provider, mockedReadOnlyProvider{readOnly: false}) service := tables.NewService(provider, mockedConfigProvider{readOnly: false})
ti, err := service.Describe(ctx, tableName) ti, err := service.Describe(ctx, tableName)
assert.NoError(t, err) 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) { t.Run("return all columns and fields in sorted order", func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
service := tables.NewService(provider, mockedReadOnlyProvider{readOnly: false}) service := tables.NewService(provider, mockedConfigProvider{readOnly: false})
ti, err := service.Describe(ctx, tableName) ti, err := service.Describe(ctx, tableName)
assert.NoError(t, err) assert.NoError(t, err)
rs, err := service.Scan(ctx, ti) rs, err := service.Scan(ctx, ti)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, rs.Items(), 3)
// Hash first, then range, then columns in alphabetic order // Hash first, then range, then columns in alphabetic order
assert.Equal(t, rs.TableInfo, ti) assert.Equal(t, rs.TableInfo, ti)
assert.Equal(t, rs.Columns(), []string{"pk", "sk", "alpha", "beta", "gamma"}) 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{ var testData = []testdynamo.TestData{
@ -78,10 +91,18 @@ var testData = []testdynamo.TestData{
}, },
} }
type mockedReadOnlyProvider struct { type mockedConfigProvider struct {
readOnly bool readOnly bool
defaultLimit int
} }
func (m mockedReadOnlyProvider) IsReadOnly() (bool, error) { func (m mockedConfigProvider) IsReadOnly() (bool, error) {
return m.readOnly, nil return m.readOnly, nil
} }
func (m mockedConfigProvider) DefaultLimit() int {
if m.defaultLimit == 0 {
return 1000
}
return m.defaultLimit
}

View file

@ -106,6 +106,10 @@ func createTable(ctx context.Context, dynamoClient *dynamodb.Client, tableName s
type notROService struct{} type notROService struct{}
func (n notROService) DefaultLimit() int {
return 1000
}
func (n notROService) IsReadOnly() (bool, error) { func (n notROService) IsReadOnly() (bool, error) {
return false, nil return false, nil
} }