From 721d3abe5ebbc2d3d4267e81c7c9cf979ede1db0 Mon Sep 17 00:00:00 2001
From: Leon Mika <lmika@lmika.org>
Date: Thu, 11 Aug 2022 22:23:39 +1000
Subject: [PATCH] backstack: added saving of backstack to workspace

---
 cmd/dynamo-browse/main.go                     | 17 +++++-
 internal/common/workspaces/manager.go         | 37 +++++++++++++
 internal/common/workspaces/workspaces.go      | 19 +++++++
 .../dynamo-browse/controllers/tableread.go    | 21 ++++---
 internal/dynamo-browse/models/models.go       |  5 +-
 .../models/serialisable/resultset.go          | 19 +++++++
 .../workspacestore/resultsetsnapshot.go       | 55 +++++++++++++++++++
 .../services/workspaces/iface.go              |  9 +++
 .../services/workspaces/service.go            | 43 +++++++++++++++
 9 files changed, 214 insertions(+), 11 deletions(-)
 create mode 100644 internal/common/workspaces/manager.go
 create mode 100644 internal/common/workspaces/workspaces.go
 create mode 100644 internal/dynamo-browse/models/serialisable/resultset.go
 create mode 100644 internal/dynamo-browse/providers/workspacestore/resultsetsnapshot.go
 create mode 100644 internal/dynamo-browse/services/workspaces/iface.go
 create mode 100644 internal/dynamo-browse/services/workspaces/service.go

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
+}