diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index e0695cf..b2213d2 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -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" @@ -34,6 +37,16 @@ func main() { cli.Fatalf("cannot load AWS config: %v", err) } + wsManager := workspaces.New(workspaces.MetaInfo{ + Command: "sqs-browse", + }) + //ws, err := wsManager.CreateTemp() + ws, err := wsManager.Open("temp.workspace") + 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 +66,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() diff --git a/internal/common/workspaces/manager.go b/internal/common/workspaces/manager.go new file mode 100644 index 0000000..da08a4d --- /dev/null +++ b/internal/common/workspaces/manager.go @@ -0,0 +1,37 @@ +package workspaces + +import ( + "github.com/asdine/storm" + "github.com/pkg/errors" + "os" +) + +type MetaInfo struct { + Command string +} + +type Manager struct { + metainfo MetaInfo +} + +func New(metaInfo MetaInfo) *Manager { + return &Manager{metainfo: metaInfo} +} + +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) + } + 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()) +} diff --git a/internal/common/workspaces/workspaces.go b/internal/common/workspaces/workspaces.go new file mode 100644 index 0000000..00bcc87 --- /dev/null +++ b/internal/common/workspaces/workspaces.go @@ -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() +} diff --git a/internal/dynamo-browse/controllers/tableread.go b/internal/dynamo-browse/controllers/tableread.go index c9a3861..f17917f 100644 --- a/internal/dynamo-browse/controllers/tableread.go +++ b/internal/dynamo-browse/controllers/tableread.go @@ -7,14 +7,17 @@ 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.Service + tableName string // state mutex *sync.Mutex @@ -23,12 +26,13 @@ type TableReadController struct { //filter string } -func NewTableReadController(state *State, tableService TableReadService, tableName string) *TableReadController { +func NewTableReadController(state *State, tableService TableReadService, workspaceService *workspaces.Service, 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), } } @@ -99,6 +103,9 @@ func (c *TableReadController) PromptForQuery() tea.Cmd { return events.Error(err) } + if err := c.workspaceService.PushSnapshot(resultSet); err != nil { + log.Printf("cannot push snapshot: %v", err) + } return c.setResultSetAndFilter(newResultSet, "") }) }, diff --git a/internal/dynamo-browse/models/models.go b/internal/dynamo-browse/models/models.go index 7bd9469..c13b901 100644 --- a/internal/dynamo-browse/models/models.go +++ b/internal/dynamo-browse/models/models.go @@ -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 diff --git a/internal/dynamo-browse/models/serialisable/resultset.go b/internal/dynamo-browse/models/serialisable/resultset.go new file mode 100644 index 0000000..2bdec08 --- /dev/null +++ b/internal/dynamo-browse/models/serialisable/resultset.go @@ -0,0 +1,19 @@ +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/providers/workspacestore/resultsetsnapshot.go b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go new file mode 100644 index 0000000..303f949 --- /dev/null +++ b/internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go @@ -0,0 +1,55 @@ +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.ResultSetSnapshot) error { + if err := s.ws.Save(rs); err != nil { + return errors.Wrap(err, "cannot save result set") + } + log.Printf("saved result set") + return nil +} + +func (s *ResultSetSnapshotStore) SetAsHead(resultSetID int64) error { + 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.ResultSetSnapshot, 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 + 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 +} diff --git a/internal/dynamo-browse/services/workspaces/iface.go b/internal/dynamo-browse/services/workspaces/iface.go new file mode 100644 index 0000000..58c8d45 --- /dev/null +++ b/internal/dynamo-browse/services/workspaces/iface.go @@ -0,0 +1,9 @@ +package workspaces + +import "github.com/lmika/audax/internal/dynamo-browse/models/serialisable" + +type ResultSetSnapshotStore interface { + Save(rs *serialisable.ResultSetSnapshot) error + SetAsHead(resultSetId int64) error + Head() (*serialisable.ResultSetSnapshot, error) +} diff --git a/internal/dynamo-browse/services/workspaces/service.go b/internal/dynamo-browse/services/workspaces/service.go new file mode 100644 index 0000000..30ae078 --- /dev/null +++ b/internal/dynamo-browse/services/workspaces/service.go @@ -0,0 +1,43 @@ +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 Service struct { + store ResultSetSnapshotStore +} + +func NewService(store ResultSetSnapshotStore) *Service { + return &Service{ + store: store, + } +} + +func (s *Service) PushSnapshot(rs *models.ResultSet) error { + newSnapshot := &serialisable.ResultSetSnapshot{ + Time: time.Now(), + TableInfo: rs.TableInfo, + } + if q := rs.Query; q != nil { + newSnapshot.Query.Expression = q.String() + } + + 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 +}