Merge pull request #7 from lmika/feature/backstack
Added the backstack to dynamo-browse
This commit is contained in:
commit
931b11cd0d
|
@ -11,9 +11,12 @@ import (
|
|||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/common/ui/logging"
|
||||
"github.com/lmika/audax/internal/common/ui/osstyle"
|
||||
"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/tables"
|
||||
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"log"
|
||||
|
@ -25,6 +28,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 flagWorkspace = flag.String("w", "", "workspace file")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
|
@ -34,6 +38,16 @@ func main() {
|
|||
cli.Fatalf("cannot load AWS config: %v", err)
|
||||
}
|
||||
|
||||
closeFn := logging.EnableLogging(*flagDebug)
|
||||
defer closeFn()
|
||||
|
||||
wsManager := workspaces.New(workspaces.MetaInfo{Command: "dynamo-browse"})
|
||||
ws, err := wsManager.OpenOrCreate(*flagWorkspace)
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot create workspace: %v", ws)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
var dynamoClient *dynamodb.Client
|
||||
if *flagLocal != "" {
|
||||
host, port, err := net.SplitHostPort(*flagLocal)
|
||||
|
@ -53,11 +67,13 @@ func main() {
|
|||
}
|
||||
|
||||
dynamoProvider := dynamo.NewProvider(dynamoClient)
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(ws)
|
||||
|
||||
tableService := tables.NewService(dynamoProvider)
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
state := controllers.NewState()
|
||||
tableReadController := controllers.NewTableReadController(state, tableService, *flagTable)
|
||||
tableReadController := controllers.NewTableReadController(state, tableService, workspaceService, *flagTable)
|
||||
tableWriteController := controllers.NewTableWriteController(state, tableService, tableReadController)
|
||||
|
||||
commandController := commandctrl.NewCommandController()
|
||||
|
@ -68,9 +84,6 @@ func main() {
|
|||
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
|
||||
closeFn := logging.EnableLogging(*flagDebug)
|
||||
defer closeFn()
|
||||
|
||||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||
if lipgloss.HasDarkBackground() {
|
||||
if colorScheme := osstyle.CurrentColorScheme(); colorScheme == osstyle.ColorSchemeLightMode {
|
||||
|
|
46
internal/common/workspaces/manager.go
Normal file
46
internal/common/workspaces/manager.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package workspaces
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type MetaInfo struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
metainfo MetaInfo
|
||||
}
|
||||
|
||||
func New(metaInfo MetaInfo) *Manager {
|
||||
return &Manager{metainfo: metaInfo}
|
||||
}
|
||||
|
||||
func (m *Manager) OpenOrCreate(filename string) (*Workspace, error) {
|
||||
if filename == "" {
|
||||
return m.CreateTemp()
|
||||
}
|
||||
return m.Open(filename)
|
||||
}
|
||||
|
||||
func (m *Manager) Open(filename string) (*Workspace, error) {
|
||||
db, err := storm.Open(filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot open workspace at %v", filename)
|
||||
}
|
||||
log.Printf("open workspace: %v", filename)
|
||||
return &Workspace{db: db}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) CreateTemp() (*Workspace, error) {
|
||||
workspaceFile, err := os.CreateTemp("", m.metainfo.Command+"*.workspace")
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot create workspace file")
|
||||
}
|
||||
workspaceFile.Close() // We just need the filename
|
||||
|
||||
return m.Open(workspaceFile.Name())
|
||||
}
|
19
internal/common/workspaces/workspaces.go
Normal file
19
internal/common/workspaces/workspaces.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package workspaces
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Workspace struct {
|
||||
db *storm.DB
|
||||
}
|
||||
|
||||
func (ws *Workspace) DB() *storm.DB {
|
||||
return ws.db
|
||||
}
|
||||
|
||||
func (ws *Workspace) Close() {
|
||||
log.Printf("close workspace")
|
||||
ws.db.Close()
|
||||
}
|
|
@ -7,28 +7,30 @@ import (
|
|||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/queryexpr"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TableReadController struct {
|
||||
tableService TableReadService
|
||||
tableName string
|
||||
tableService TableReadService
|
||||
workspaceService *workspaces.ViewSnapshotService
|
||||
tableName string
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
state *State
|
||||
//resultSet *models.ResultSet
|
||||
//filter string
|
||||
}
|
||||
|
||||
func NewTableReadController(state *State, tableService TableReadService, tableName string) *TableReadController {
|
||||
func NewTableReadController(state *State, tableService TableReadService, workspaceService *workspaces.ViewSnapshotService, tableName string) *TableReadController {
|
||||
return &TableReadController{
|
||||
state: state,
|
||||
tableService: tableService,
|
||||
tableName: tableName,
|
||||
mutex: new(sync.Mutex),
|
||||
state: state,
|
||||
tableService: tableService,
|
||||
workspaceService: workspaceService,
|
||||
tableName: tableName,
|
||||
mutex: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,8 +72,9 @@ func (c *TableReadController) ScanTable(name string) tea.Cmd {
|
|||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
resultSet = c.tableService.Filter(resultSet, c.state.Filter())
|
||||
|
||||
return c.setResultSetAndFilter(resultSet, c.state.Filter())
|
||||
return c.setResultSetAndFilter(resultSet, c.state.Filter(), true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,62 +83,75 @@ func (c *TableReadController) PromptForQuery() tea.Cmd {
|
|||
return events.PromptForInputMsg{
|
||||
Prompt: "query: ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
if value == "" {
|
||||
return func() tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
return c.doScan(context.Background(), resultSet, nil)
|
||||
}
|
||||
return func() tea.Msg {
|
||||
return c.runQuery(c.state.ResultSet().TableInfo, value, "", true)
|
||||
}
|
||||
|
||||
expr, err := queryexpr.Parse(value)
|
||||
if err != nil {
|
||||
return events.SetError(err)
|
||||
}
|
||||
|
||||
return c.doIfNoneDirty(func() tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), resultSet.TableInfo, expr)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return c.setResultSetAndFilter(newResultSet, "")
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) doIfNoneDirty(cmd tea.Cmd) tea.Cmd {
|
||||
func (c *TableReadController) runQuery(tableInfo *models.TableInfo, query, newFilter string, pushSnapshot bool) tea.Msg {
|
||||
if query == "" {
|
||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, nil)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
if newFilter != "" {
|
||||
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
||||
}
|
||||
|
||||
return c.setResultSetAndFilter(newResultSet, newFilter, pushSnapshot)
|
||||
}
|
||||
|
||||
expr, err := queryexpr.Parse(query)
|
||||
if err != nil {
|
||||
return events.SetError(err)
|
||||
}
|
||||
|
||||
return c.doIfNoneDirty(func() tea.Msg {
|
||||
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, expr)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
if newFilter != "" {
|
||||
newResultSet = c.tableService.Filter(newResultSet, newFilter)
|
||||
}
|
||||
return c.setResultSetAndFilter(newResultSet, newFilter, pushSnapshot)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *TableReadController) doIfNoneDirty(cmd tea.Cmd) tea.Msg {
|
||||
var anyDirty = false
|
||||
for i := 0; i < len(c.state.ResultSet().Items()); i++ {
|
||||
anyDirty = anyDirty || c.state.ResultSet().IsDirty(i)
|
||||
}
|
||||
|
||||
if !anyDirty {
|
||||
return cmd
|
||||
return cmd()
|
||||
}
|
||||
|
||||
return func() tea.Msg {
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "reset modified items? ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
if value != "y" {
|
||||
return events.SetStatus("operation aborted")
|
||||
}
|
||||
return events.PromptForInputMsg{
|
||||
Prompt: "reset modified items? ",
|
||||
OnDone: func(value string) tea.Cmd {
|
||||
if value != "y" {
|
||||
return events.SetStatus("operation aborted")
|
||||
}
|
||||
|
||||
return cmd
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *TableReadController) Rescan() tea.Cmd {
|
||||
return c.doIfNoneDirty(func() tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
return c.doScan(context.Background(), resultSet, resultSet.Query)
|
||||
})
|
||||
return func() tea.Msg {
|
||||
return c.doIfNoneDirty(func() tea.Msg {
|
||||
resultSet := c.state.ResultSet()
|
||||
return c.doScan(context.Background(), resultSet, resultSet.Query, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) ExportCSV(filename string) tea.Cmd {
|
||||
|
@ -173,7 +189,7 @@ func (c *TableReadController) ExportCSV(filename string) tea.Cmd {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet, query models.Queryable) tea.Msg {
|
||||
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet, query models.Queryable, pushBackstack bool) tea.Msg {
|
||||
newResultSet, err := c.tableService.ScanOrQuery(ctx, resultSet.TableInfo, query)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
|
@ -181,10 +197,16 @@ func (c *TableReadController) doScan(ctx context.Context, resultSet *models.Resu
|
|||
|
||||
newResultSet = c.tableService.Filter(newResultSet, c.state.Filter())
|
||||
|
||||
return c.setResultSetAndFilter(newResultSet, c.state.Filter())
|
||||
return c.setResultSetAndFilter(newResultSet, c.state.Filter(), pushBackstack)
|
||||
}
|
||||
|
||||
func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string) tea.Msg {
|
||||
func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string, pushBackstack bool) tea.Msg {
|
||||
if pushBackstack {
|
||||
if err := c.workspaceService.PushSnapshot(resultSet, filter); err != nil {
|
||||
log.Printf("cannot push snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.state.setResultSetAndFilter(resultSet, filter)
|
||||
return c.state.buildNewResultSetMessage("")
|
||||
}
|
||||
|
@ -209,9 +231,46 @@ func (c *TableReadController) Filter() tea.Cmd {
|
|||
resultSet := c.state.ResultSet()
|
||||
newResultSet := c.tableService.Filter(resultSet, value)
|
||||
|
||||
return c.setResultSetAndFilter(newResultSet, value)
|
||||
return c.setResultSetAndFilter(newResultSet, value, true)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TableReadController) ViewBack() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
viewSnapshot, err := c.workspaceService.PopSnapshot()
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
} else if viewSnapshot == nil {
|
||||
return events.StatusMsg("Backstack is empty")
|
||||
}
|
||||
|
||||
currentResultSet := c.state.ResultSet()
|
||||
|
||||
var currentQueryExpr string
|
||||
if currentResultSet.Query != nil {
|
||||
currentQueryExpr = currentResultSet.Query.String()
|
||||
}
|
||||
|
||||
if viewSnapshot.TableName == currentResultSet.TableInfo.Name && viewSnapshot.Query == currentQueryExpr {
|
||||
log.Printf("backstack: setting filter to '%v'", viewSnapshot.Filter)
|
||||
|
||||
newResultSet := c.tableService.Filter(currentResultSet, viewSnapshot.Filter)
|
||||
return c.setResultSetAndFilter(newResultSet, viewSnapshot.Filter, false)
|
||||
}
|
||||
|
||||
tableInfo := currentResultSet.TableInfo
|
||||
if viewSnapshot.TableName != currentResultSet.TableInfo.Name {
|
||||
tableInfo, err = c.tableService.Describe(context.Background(), viewSnapshot.TableName)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("backstack: running query: table = '%v', query = '%v', filter = '%v'",
|
||||
tableInfo.Name, viewSnapshot.Query, viewSnapshot.Filter)
|
||||
return c.runQuery(tableInfo, viewSnapshot.Query, viewSnapshot.Filter, false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ import (
|
|||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"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/tables"
|
||||
workspaces_service "github.com/lmika/audax/internal/dynamo-browse/services/workspaces"
|
||||
"github.com/lmika/audax/test/testdynamo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
|
@ -17,11 +20,14 @@ import (
|
|||
func TestTableReadController_InitTable(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
|
||||
t.Run("should prompt for table if no table name provided", func(t *testing.T) {
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "")
|
||||
|
||||
cmd := readController.Init()
|
||||
event := cmd()
|
||||
|
@ -30,7 +36,7 @@ func TestTableReadController_InitTable(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("should scan table if table name provided", func(t *testing.T) {
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "")
|
||||
|
||||
cmd := readController.Init()
|
||||
event := cmd()
|
||||
|
@ -42,9 +48,12 @@ func TestTableReadController_InitTable(t *testing.T) {
|
|||
func TestTableReadController_ListTables(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "")
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "")
|
||||
|
||||
t.Run("returns a list of tables", func(t *testing.T) {
|
||||
cmd := readController.ListTables()
|
||||
|
@ -65,10 +74,13 @@ 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)
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "bravo-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "bravo-table")
|
||||
|
||||
t.Run("should perform a rescan", func(t *testing.T) {
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -99,9 +111,12 @@ func TestTableReadController_Rescan(t *testing.T) {
|
|||
func TestTableReadController_ExportCSV(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "bravo-table")
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "bravo-table")
|
||||
|
||||
t.Run("should export result set to CSV file", func(t *testing.T) {
|
||||
tempFile := tempFile(t)
|
||||
|
@ -122,7 +137,7 @@ 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, "non-existant-table")
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "non-existant-table")
|
||||
|
||||
invokeCommandExpectingError(t, readController.Init())
|
||||
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
|
||||
|
@ -134,9 +149,12 @@ func TestTableReadController_ExportCSV(t *testing.T) {
|
|||
func TestTableReadController_Query(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
provider := dynamo.NewProvider(client)
|
||||
service := tables.NewService(provider)
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, "bravo-table")
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "bravo-table")
|
||||
|
||||
t.Run("should run scan with filter based on user query", func(t *testing.T) {
|
||||
tempFile := tempFile(t)
|
||||
|
@ -156,7 +174,7 @@ 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, "non-existant-table")
|
||||
readController := controllers.NewTableReadController(controllers.NewState(), service, workspaceService, "non-existant-table")
|
||||
|
||||
invokeCommandExpectingError(t, readController.Init())
|
||||
invokeCommandExpectingError(t, readController.ExportCSV(tempFile))
|
||||
|
@ -177,6 +195,19 @@ func tempFile(t *testing.T) string {
|
|||
return tempFile.Name()
|
||||
}
|
||||
|
||||
func testWorkspace(t *testing.T) *workspaces.Workspace {
|
||||
wsTempFile := tempFile(t)
|
||||
|
||||
wsManager := workspaces.New(workspaces.MetaInfo{Command: "dynamo-browse"})
|
||||
ws, err := wsManager.Open(wsTempFile)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create workspace manager: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { ws.Close() })
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
func invokeCommand(t *testing.T, cmd tea.Cmd) tea.Msg {
|
||||
msg := cmd()
|
||||
|
||||
|
|
|
@ -400,7 +400,7 @@ func (twc *TableWriteController) NoisyTouchItem(idx int) tea.Cmd {
|
|||
return events.Error(err)
|
||||
}
|
||||
|
||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
|
||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query, false)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -431,7 +431,7 @@ func (twc *TableWriteController) DeleteMarked() tea.Cmd {
|
|||
return events.Error(err)
|
||||
}
|
||||
|
||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
|
||||
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query, false)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -6,13 +6,18 @@ 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/workspacestore"
|
||||
"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"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTableWriteController_NewItem(t *testing.T) {
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -20,7 +25,7 @@ func TestTableWriteController_NewItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -43,6 +48,9 @@ func TestTableWriteController_NewItem(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableWriteController_SetAttributeValue(t *testing.T) {
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
t.Run("should preserve the type of the field if unspecified", func(t *testing.T) {
|
||||
|
||||
scenarios := []struct {
|
||||
|
@ -80,7 +88,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -100,7 +108,7 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -154,7 +162,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) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -175,7 +183,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) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -205,12 +213,15 @@ func TestTableWriteController_SetAttributeValue(t *testing.T) {
|
|||
func TestTableWriteController_DeleteAttribute(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
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, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -226,7 +237,7 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
|
|||
|
||||
t.Run("should delete attribute of map", func(t *testing.T) {
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -247,6 +258,9 @@ func TestTableWriteController_DeleteAttribute(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableWriteController_PutItem(t *testing.T) {
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
t.Run("should put the selected item if dirty", func(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
|
@ -254,7 +268,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
|
@ -281,7 +295,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
|
@ -312,7 +326,7 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
|
@ -326,6 +340,9 @@ func TestTableWriteController_PutItem(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableWriteController_PutItems(t *testing.T) {
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
t.Run("should put all dirty items if none are marked", func(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
|
@ -333,7 +350,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -361,7 +378,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -397,7 +414,7 @@ func TestTableWriteController_PutItems(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
invokeCommand(t, readController.Init())
|
||||
|
@ -428,6 +445,9 @@ func TestTableWriteController_PutItems(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableWriteController_TouchItem(t *testing.T) {
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
t.Run("should put the selected item if unmodified", func(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
|
@ -435,7 +455,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
|
@ -461,7 +481,7 @@ func TestTableWriteController_TouchItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
|
@ -477,6 +497,9 @@ func TestTableWriteController_TouchItem(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTableWriteController_NoisyTouchItem(t *testing.T) {
|
||||
resultSetSnapshotStore := workspacestore.NewResultSetSnapshotStore(testWorkspace(t))
|
||||
workspaceService := workspaces_service.NewService(resultSetSnapshotStore)
|
||||
|
||||
t.Run("should delete and put the selected item if unmodified", func(t *testing.T) {
|
||||
client := testdynamo.SetupTestTable(t, testData)
|
||||
|
||||
|
@ -484,7 +507,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
|
@ -510,7 +533,7 @@ func TestTableWriteController_NoisyTouchItem(t *testing.T) {
|
|||
service := tables.NewService(provider)
|
||||
|
||||
state := controllers.NewState()
|
||||
readController := controllers.NewTableReadController(state, service, "alpha-table")
|
||||
readController := controllers.NewTableReadController(state, service, workspaceService, "alpha-table")
|
||||
writeController := controllers.NewTableWriteController(state, service, readController)
|
||||
|
||||
// Read the table
|
||||
|
|
|
@ -3,9 +3,8 @@ package models
|
|||
import "sort"
|
||||
|
||||
type ResultSet struct {
|
||||
TableInfo *TableInfo
|
||||
Query Queryable
|
||||
//Columns []string
|
||||
TableInfo *TableInfo
|
||||
Query Queryable
|
||||
items []Item
|
||||
attributes []ItemAttribute
|
||||
|
||||
|
|
14
internal/dynamo-browse/models/serialisable/viewsnapshot.go
Normal file
14
internal/dynamo-browse/models/serialisable/viewsnapshot.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package serialisable
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ViewSnapshot struct {
|
||||
ID int64 `storm:"id,increment"`
|
||||
BackLink int64 `storm:"index"`
|
||||
Time time.Time
|
||||
TableName string
|
||||
Query string
|
||||
Filter string
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package workspacestore
|
||||
|
||||
import (
|
||||
"github.com/asdine/storm"
|
||||
"github.com/lmika/audax/internal/common/workspaces"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
const resultSetSnapshotsBucket = "ResultSetSnapshots"
|
||||
|
||||
type ResultSetSnapshotStore struct {
|
||||
ws storm.Node
|
||||
}
|
||||
|
||||
func NewResultSetSnapshotStore(ws *workspaces.Workspace) *ResultSetSnapshotStore {
|
||||
return &ResultSetSnapshotStore{
|
||||
ws: ws.DB().From(resultSetSnapshotsBucket),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ResultSetSnapshotStore) Save(rs *serialisable.ViewSnapshot) error {
|
||||
if err := s.ws.Save(rs); err != nil {
|
||||
return errors.Wrap(err, "cannot save result set")
|
||||
}
|
||||
log.Printf("saved result set: table='%v', query='%v', filter='%v'", rs.TableName, rs.Query, rs.Filter)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ResultSetSnapshotStore) SetAsHead(resultSetID int64) error {
|
||||
if resultSetID == 0 {
|
||||
if err := s.ws.Delete("head", "id"); err != nil {
|
||||
return errors.Wrap(err, "cannot remove head")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.ws.Set("head", "id", resultSetID); err != nil {
|
||||
return errors.Wrap(err, "cannot set as head")
|
||||
}
|
||||
log.Printf("saved result set head")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ResultSetSnapshotStore) Head() (*serialisable.ViewSnapshot, error) {
|
||||
var headResultSetID int64
|
||||
if err := s.ws.Get("head", "id", &headResultSetID); err != nil && !errors.Is(err, storm.ErrNotFound) {
|
||||
return nil, errors.Wrap(err, "cannot get head")
|
||||
}
|
||||
|
||||
var rss serialisable.ViewSnapshot
|
||||
if err := s.ws.One("ID", headResultSetID, &rss); err != nil {
|
||||
if errors.Is(err, storm.ErrNotFound) {
|
||||
return nil, nil
|
||||
} else {
|
||||
return nil, errors.Wrap(err, "cannot get head")
|
||||
}
|
||||
}
|
||||
|
||||
return &rss, nil
|
||||
}
|
||||
|
||||
func (s *ResultSetSnapshotStore) Remove(resultSetId int64) error {
|
||||
var rss serialisable.ViewSnapshot
|
||||
if err := s.ws.One("ID", resultSetId, &rss); err != nil {
|
||||
if errors.Is(err, storm.ErrNotFound) {
|
||||
return nil
|
||||
} else {
|
||||
return errors.Wrapf(err, "cannot get snapshot with ID %v", resultSetId)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.ws.DeleteStruct(&rss); err != nil {
|
||||
return errors.Wrap(err, "cannot delete snapshot")
|
||||
}
|
||||
return nil
|
||||
}
|
10
internal/dynamo-browse/services/workspaces/iface.go
Normal file
10
internal/dynamo-browse/services/workspaces/iface.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package workspaces
|
||||
|
||||
import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
|
||||
|
||||
type ViewSnapshotStore interface {
|
||||
Save(rs *serialisable.ViewSnapshot) error
|
||||
SetAsHead(resultSetId int64) error
|
||||
Head() (*serialisable.ViewSnapshot, error)
|
||||
Remove(resultSetId int64) error
|
||||
}
|
69
internal/dynamo-browse/services/workspaces/service.go
Normal file
69
internal/dynamo-browse/services/workspaces/service.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package workspaces
|
||||
|
||||
import (
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/models/serialisable"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ViewSnapshotService struct {
|
||||
store ViewSnapshotStore
|
||||
}
|
||||
|
||||
func NewService(store ViewSnapshotStore) *ViewSnapshotService {
|
||||
return &ViewSnapshotService{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ViewSnapshotService) PushSnapshot(rs *models.ResultSet, filter string) error {
|
||||
newSnapshot := &serialisable.ViewSnapshot{
|
||||
Time: time.Now(),
|
||||
TableName: rs.TableInfo.Name,
|
||||
}
|
||||
if q := rs.Query; q != nil {
|
||||
newSnapshot.Query = q.String()
|
||||
}
|
||||
newSnapshot.Filter = filter
|
||||
|
||||
if head, err := s.store.Head(); head != nil {
|
||||
newSnapshot.BackLink = head.ID
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "cannot get head result set")
|
||||
}
|
||||
|
||||
if err := s.store.Save(newSnapshot); err != nil {
|
||||
return errors.Wrap(err, "cannot save snapshot")
|
||||
}
|
||||
if err := s.store.SetAsHead(newSnapshot.ID); err != nil {
|
||||
return errors.Wrap(err, "cannot set new snapshot as head")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ViewSnapshotService) PopSnapshot() (*serialisable.ViewSnapshot, error) {
|
||||
vs, err := s.store.Head()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get snapshot head")
|
||||
} else if vs == nil || vs.BackLink == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := s.store.SetAsHead(vs.BackLink); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot set new head")
|
||||
}
|
||||
if err := s.store.Remove(vs.ID); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot remove old ID")
|
||||
}
|
||||
|
||||
vs, err = s.store.Head()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get snapshot head")
|
||||
} else if vs == nil || vs.BackLink == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return vs, nil
|
||||
}
|
|
@ -149,6 +149,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, m.tableReadController.PromptForQuery()
|
||||
case "/":
|
||||
return m, m.tableReadController.Filter()
|
||||
case "backspace":
|
||||
return m, m.tableReadController.ViewBack()
|
||||
//case "e":
|
||||
// m.itemEdit.Visible()
|
||||
// return m, nil
|
||||
|
|
Loading…
Reference in a new issue