diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index f17917f..8021e9a 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -16,7 +16,7 @@ import ( type TableReadController struct { tableService TableReadService - workspaceService *workspaces.Service + workspaceService *workspaces.ViewSnapshotService tableName string // state @@ -26,7 +26,7 @@ type TableReadController struct { //filter string } -func NewTableReadController(state *State, tableService TableReadService, workspaceService *workspaces.Service, tableName string) *TableReadController { +func NewTableReadController(state *State, tableService TableReadService, workspaceService *workspaces.ViewSnapshotService, tableName string) *TableReadController { return &TableReadController{ state: state, tableService: tableService, @@ -84,65 +84,107 @@ 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) + } + + /* + if value == "" { + return func() tea.Msg { + resultSet := c.state.ResultSet() + return c.doScan(context.Background(), resultSet, nil) + } } - } - 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) + expr, err := queryexpr.Parse(value) if err != nil { - return events.Error(err) + return events.SetError(err) } - if err := c.workspaceService.PushSnapshot(resultSet); err != nil { - log.Printf("cannot push snapshot: %v", err) - } - return c.setResultSetAndFilter(newResultSet, "") - }) + 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) + } + + if err := c.workspaceService.PushSnapshot(resultSet, ""); err != nil { + log.Printf("cannot push snapshot: %v", 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) + } + + expr, err := queryexpr.Parse(query) + if err != nil { + return events.SetError(err) + } + + return c.doIfNoneDirty(func() tea.Msg { + resultSet := c.state.ResultSet() + newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, expr) + if err != nil { + return events.Error(err) + } + + if pushSnapshot { + if err := c.workspaceService.PushSnapshot(resultSet, c.state.Filter()); err != nil { + log.Printf("cannot push snapshot: %v", err) + } + } + if newFilter != "" { + newResultSet = c.tableService.Filter(newResultSet, newFilter) + } + return c.setResultSetAndFilter(newResultSet, newFilter) + }) +} + +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) + }) + } } func (c *TableReadController) ExportCSV(filename string) tea.Cmd { @@ -186,6 +228,7 @@ func (c *TableReadController) doScan(ctx context.Context, resultSet *models.Resu return events.Error(err) } + c.workspaceService.PushSnapshot(resultSet, c.state.Filter()) newResultSet = c.tableService.Filter(newResultSet, c.state.Filter()) return c.setResultSetAndFilter(newResultSet, c.state.Filter()) @@ -214,6 +257,8 @@ func (c *TableReadController) Filter() tea.Cmd { OnDone: func(value string) tea.Cmd { return func() tea.Msg { resultSet := c.state.ResultSet() + + c.workspaceService.PushSnapshot(resultSet, c.state.Filter()) newResultSet := c.tableService.Filter(resultSet, value) return c.setResultSetAndFilter(newResultSet, value) @@ -222,3 +267,40 @@ func (c *TableReadController) Filter() tea.Cmd { } } } + +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) + } + + 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) + } +} diff --git a/internal/dynamo-browse/models/serialisable/resultset.go b/internal/dynamo-browse/models/serialisable/resultset.go deleted file mode 100644 index 2bdec08..0000000 --- a/internal/dynamo-browse/models/serialisable/resultset.go +++ /dev/null @@ -1,19 +0,0 @@ -package serialisable - -import ( - "github.com/lmika/audax/internal/dynamo-browse/models" - "time" -) - -type ResultSetSnapshot struct { - ID int64 `storm:"id,increment"` - BackLink int64 `storm:"index"` - Time time.Time - TableInfo *models.TableInfo - Query Query - Filter string -} - -type Query struct { - Expression string -} diff --git a/internal/dynamo-browse/models/serialisable/viewsnapshot.go b/internal/dynamo-browse/models/serialisable/viewsnapshot.go new file mode 100644 index 0000000..80cd04a --- /dev/null +++ b/internal/dynamo-browse/models/serialisable/viewsnapshot.go @@ -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 +} diff --git a/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go index 303f949..09ef0cf 100644 --- a/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go +++ b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go @@ -20,15 +20,22 @@ func NewResultSetSnapshotStore(ws *workspaces.Workspace) *ResultSetSnapshotStore } } -func (s *ResultSetSnapshotStore) Save(rs *serialisable.ResultSetSnapshot) error { +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") + 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") } @@ -36,13 +43,13 @@ func (s *ResultSetSnapshotStore) SetAsHead(resultSetID int64) error { return nil } -func (s *ResultSetSnapshotStore) Head() (*serialisable.ResultSetSnapshot, error) { +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.ResultSetSnapshot + var rss serialisable.ViewSnapshot if err := s.ws.One("ID", headResultSetID, &rss); err != nil { if errors.Is(err, storm.ErrNotFound) { return nil, nil @@ -53,3 +60,19 @@ func (s *ResultSetSnapshotStore) Head() (*serialisable.ResultSetSnapshot, error) 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 +} diff --git a/internal/dynamo-browse/services/workspaces/iface.go b/internal/dynamo-browse/services/workspaces/iface.go index 58c8d45..75a151a 100644 --- a/internal/dynamo-browse/services/workspaces/iface.go +++ b/internal/dynamo-browse/services/workspaces/iface.go @@ -2,8 +2,9 @@ package workspaces import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" -type ResultSetSnapshotStore interface { - Save(rs *serialisable.ResultSetSnapshot) error +type ViewSnapshotStore interface { + Save(rs *serialisable.ViewSnapshot) error SetAsHead(resultSetId int64) error - Head() (*serialisable.ResultSetSnapshot, error) + Head() (*serialisable.ViewSnapshot, error) + Remove(resultSetId int64) error } diff --git a/internal/dynamo-browse/services/workspaces/service.go b/internal/dynamo-browse/services/workspaces/service.go index 30ae078..540494a 100644 --- a/internal/dynamo-browse/services/workspaces/service.go +++ b/internal/dynamo-browse/services/workspaces/service.go @@ -7,24 +7,25 @@ import ( "time" ) -type Service struct { - store ResultSetSnapshotStore +type ViewSnapshotService struct { + store ViewSnapshotStore } -func NewService(store ResultSetSnapshotStore) *Service { - return &Service{ +func NewService(store ViewSnapshotStore) *ViewSnapshotService { + return &ViewSnapshotService{ store: store, } } -func (s *Service) PushSnapshot(rs *models.ResultSet) error { - newSnapshot := &serialisable.ResultSetSnapshot{ +func (s *ViewSnapshotService) PushSnapshot(rs *models.ResultSet, filter string) error { + newSnapshot := &serialisable.ViewSnapshot{ Time: time.Now(), - TableInfo: rs.TableInfo, + TableName: rs.TableInfo.Name, } if q := rs.Query; q != nil { - newSnapshot.Query.Expression = q.String() + newSnapshot.Query = q.String() } + newSnapshot.Filter = filter if head, err := s.store.Head(); head != nil { newSnapshot.BackLink = head.ID @@ -41,3 +42,22 @@ func (s *Service) PushSnapshot(rs *models.ResultSet) error { 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") + } + if vs == nil { + return vs, 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") + } + + return vs, nil +} diff --git a/internal/dynamo-browse/ui/model.go b/internal/dynamo-browse/ui/model.go index 3f746f5..eaefd2f 100644 --- a/internal/dynamo-browse/ui/model.go +++ b/internal/dynamo-browse/ui/model.go @@ -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