diff --git a/db/db.go b/db/db.go index b04d199a7..b13b72fbb 100644 --- a/db/db.go +++ b/db/db.go @@ -7,3 +7,6 @@ import "embed" //go:embed migrations-blockstore/*.sql var BlockstoreMigrationFS embed.FS + +//go:embed migrations-wstore/*.sql +var WStoreMigrationFS embed.FS diff --git a/db/migrations-wstore/000001_init.up.sql b/db/migrations-wstore/000001_init.up.sql index 8880f644e..b8a3e9275 100644 --- a/db/migrations-wstore/000001_init.up.sql +++ b/db/migrations-wstore/000001_init.up.sql @@ -15,6 +15,5 @@ CREATE TABLE db_tab ( CREATE TABLE db_block ( blockid varchar(36) PRIMARY KEY, - tabid varchar(36) NOT NULL, -- the tab this block belongs to data json NOT NULL ); diff --git a/main.go b/main.go index 27ee260b0..6995a7027 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/service/blockservice" "github.com/wavetermdev/thenextwave/pkg/service/fileservice" "github.com/wavetermdev/thenextwave/pkg/wavebase" + "github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events" @@ -104,6 +105,11 @@ func main() { log.Printf("error initializing blockstore: %v\n", err) return } + err = wstore.InitWStore() + if err != nil { + log.Printf("error initializing wstore: %v\n", err) + return + } app := application.New(application.Options{ Name: "NextWave", diff --git a/pkg/service/blockservice/blockservice.go b/pkg/service/blockservice/blockservice.go index 81df911fa..0815d814f 100644 --- a/pkg/service/blockservice/blockservice.go +++ b/pkg/service/blockservice/blockservice.go @@ -4,8 +4,10 @@ package blockservice import ( + "context" "fmt" "strings" + "time" "github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/util/utilfn" @@ -41,7 +43,12 @@ func (bs *BlockService) CloseBlock(blockId string) { } func (bs *BlockService) GetBlockData(blockId string) (map[string]any, error) { - blockData := wstore.BlockMap.Get(blockId) + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + blockData, err := wstore.BlockGet(ctx, blockId) + if err != nil { + return nil, fmt.Errorf("error getting block data: %w", err) + } if blockData == nil { return nil, nil } diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index f1c5466f5..62265770d 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -4,6 +4,7 @@ package wstore import ( + "context" "fmt" "sync" @@ -23,6 +24,7 @@ type Client struct { type Workspace struct { Lock *sync.Mutex `json:"-"` WorkspaceId string `json:"workspaceid"` + Name string `json:"name"` TabIds []string `json:"tabids"` } @@ -118,3 +120,33 @@ func CreateWorkspace() (*Workspace, error) { } return ws, nil } + +func EnsureWorkspace(ctx context.Context) error { + wsCount, err := WorkspaceCount(ctx) + if err != nil { + return fmt.Errorf("error getting workspace count: %w", err) + } + if wsCount > 0 { + return nil + } + ws := &Workspace{ + Lock: &sync.Mutex{}, + WorkspaceId: uuid.New().String(), + Name: "default", + } + err = WorkspaceInsert(ctx, ws) + if err != nil { + return fmt.Errorf("error inserting workspace: %w", err) + } + tab := &Tab{ + Lock: &sync.Mutex{}, + TabId: uuid.New().String(), + Name: "Tab 1", + BlockIds: []string{}, + } + err = TabInsert(ctx, tab, ws.WorkspaceId) + if err != nil { + return fmt.Errorf("error inserting tab: %w", err) + } + return nil +} diff --git a/pkg/wstore/wstore_dbops.go b/pkg/wstore/wstore_dbops.go new file mode 100644 index 000000000..3840c8935 --- /dev/null +++ b/pkg/wstore/wstore_dbops.go @@ -0,0 +1,97 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wstore + +import ( + "context" + "fmt" + + "github.com/google/uuid" +) + +func WorkspaceCount(ctx context.Context) (int, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (int, error) { + query := "SELECT count(*) FROM workspace" + return tx.GetInt(query), nil + }) +} + +func WorkspaceInsert(ctx context.Context, ws *Workspace) error { + if ws.WorkspaceId == "" { + ws.WorkspaceId = uuid.New().String() + } + return WithTx(ctx, func(tx *TxWrap) error { + query := "INSERT INTO workspace (workspaceid, data) VALUES (?, ?)" + tx.Exec(query, ws.WorkspaceId, TxJson(tx, ws)) + return nil + }) +} + +func WorkspaceGet(ctx context.Context, workspaceId string) (*Workspace, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*Workspace, error) { + query := "SELECT data FROM workspace WHERE workspaceid = ?" + jsonData := tx.GetString(query, workspaceId) + return TxReadJson[Workspace](tx, jsonData), nil + }) +} + +func WorkspaceUpdate(ctx context.Context, ws *Workspace) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := "UPDATE workspace SET data = ? WHERE workspaceid = ?" + tx.Exec(query, TxJson(tx, ws), ws.WorkspaceId) + return nil + }) +} + +func addTabToWorkspace(ctx context.Context, workspaceId string, tabId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + ws, err := WorkspaceGet(tx.Context(), workspaceId) + if err != nil { + return err + } + if ws == nil { + return fmt.Errorf("workspace not found: %s", workspaceId) + } + ws.TabIds = append(ws.TabIds, tabId) + return WorkspaceUpdate(tx.Context(), ws) + }) +} + +func TabInsert(ctx context.Context, tab *Tab, workspaceId string) error { + if tab.TabId == "" { + tab.TabId = uuid.New().String() + } + return WithTx(ctx, func(tx *TxWrap) error { + query := "INSERT INTO tab (tabid, data) VALUES (?, ?)" + tx.Exec(query, tab.TabId, TxJson(tx, tab)) + return addTabToWorkspace(tx.Context(), workspaceId, tab.TabId) + }) +} + +func BlockGet(ctx context.Context, blockId string) (*Block, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*Block, error) { + query := "SELECT data FROM block WHERE blockid = ?" + jsonData := tx.GetString(query, blockId) + return TxReadJson[Block](tx, jsonData), nil + }) +} + +func BlockDelete(ctx context.Context, blockId string) error { + return WithTx(ctx, func(tx *TxWrap) error { + query := "DELETE FROM block WHERE blockid = ?" + tx.Exec(query, blockId) + return nil + }) +} + +func BlockInsert(ctx context.Context, block *Block) error { + if block.BlockId == "" { + block.BlockId = uuid.New().String() + } + return WithTx(ctx, func(tx *TxWrap) error { + query := "INSERT INTO block (blockid, data) VALUES (?, ?)" + tx.Exec(query, block.BlockId, TxJson(tx, block)) + return nil + }) +} diff --git a/pkg/wstore/wstore_dbsetup.go b/pkg/wstore/wstore_dbsetup.go index 1b97352f9..3e58417d4 100644 --- a/pkg/wstore/wstore_dbsetup.go +++ b/pkg/wstore/wstore_dbsetup.go @@ -5,14 +5,20 @@ package wstore import ( "context" + "encoding/json" "fmt" "log" "path" "time" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/source/iofs" "github.com/jmoiron/sqlx" "github.com/sawka/txwrap" "github.com/wavetermdev/thenextwave/pkg/wavebase" + + sqlite3migrate "github.com/golang-migrate/migrate/v4/database/sqlite3" + dbfs "github.com/wavetermdev/thenextwave/db" ) const WStoreDBName = "waveterm.db" @@ -55,3 +61,63 @@ func MakeDB(ctx context.Context) (*sqlx.DB, error) { func MigrateWStore() error { return nil } + +func MakeWStoreMigrate() (*migrate.Migrate, error) { + fsVar, err := iofs.New(dbfs.WStoreMigrationFS, "migrations-wstore") + if err != nil { + return nil, fmt.Errorf("opening iofs: %w", err) + } + mdriver, err := sqlite3migrate.WithInstance(globalDB.DB, &sqlite3migrate.Config{}) + if err != nil { + return nil, fmt.Errorf("making blockstore migration driver: %w", err) + } + m, err := migrate.NewWithInstance("iofs", fsVar, "sqlite3", mdriver) + if err != nil { + return nil, fmt.Errorf("making blockstore migration db[%s]: %w", GetDBName(), err) + } + return m, nil +} + +func GetMigrateVersion(m *migrate.Migrate) (uint, bool, error) { + if m == nil { + var err error + m, err = MakeWStoreMigrate() + if err != nil { + return 0, false, err + } + } + curVersion, dirty, err := m.Version() + if err == migrate.ErrNilVersion { + return 0, false, nil + } + return curVersion, dirty, err +} + +func WithTx(ctx context.Context, fn func(tx *TxWrap) error) error { + return txwrap.WithTx(ctx, globalDB, fn) +} + +func WithTxRtn[RT any](ctx context.Context, fn func(tx *TxWrap) (RT, error)) (RT, error) { + return txwrap.WithTxRtn(ctx, globalDB, fn) +} + +func TxJson(tx *TxWrap, v any) string { + barr, err := json.Marshal(v) + if err != nil { + tx.SetErr(fmt.Errorf("json marshal (%T): %w", v, err)) + return "" + } + return string(barr) +} + +func TxReadJson[T any](tx *TxWrap, jsonData string) *T { + if jsonData == "" { + return nil + } + var v T + err := json.Unmarshal([]byte(jsonData), &v) + if err != nil { + tx.SetErr(fmt.Errorf("json unmarshal (%T): %w", v, err)) + } + return &v +}