feat: add demo subcommand with seeded test data
nib demo starts server with temp DB populated from testdata/demo.json. Covers all glyphs, card types, tags, pins, completions, deletes, and template fill placeholders.
This commit is contained in:
+139
@@ -0,0 +1,139 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/lerko/nib/internal/db"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var demoCmd = &cobra.Command{
|
||||
Use: "demo",
|
||||
Short: "start server with pre-populated demo data",
|
||||
RunE: runDemo,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(demoCmd)
|
||||
}
|
||||
|
||||
type demoEntity struct {
|
||||
Body string `json:"body"`
|
||||
Glyph string `json:"glyph"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
TimeAnchor *string `json:"time_anchor,omitempty"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Completed bool `json:"completed"`
|
||||
Deleted bool `json:"deleted"`
|
||||
CardType *string `json:"card_type,omitempty"`
|
||||
CardData *string `json:"card_data,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func runDemo(_ *cobra.Command, _ []string) error {
|
||||
tmpDir, err := os.MkdirTemp("", "nib-demo-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbPath := filepath.Join(tmpDir, "demo.db")
|
||||
fmt.Printf("demo db: %s\n", dbPath)
|
||||
|
||||
store, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := seedDemo(store); err != nil {
|
||||
store.Close()
|
||||
return fmt.Errorf("seed demo data: %w", err)
|
||||
}
|
||||
store.Close()
|
||||
|
||||
os.Setenv("NIB_DB", dbPath)
|
||||
return runServe(nil, nil)
|
||||
}
|
||||
|
||||
func seedDemo(store *db.Store) error {
|
||||
data, err := findDemoFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var entries []demoEntity
|
||||
if err := json.Unmarshal(data, &entries); err != nil {
|
||||
return fmt.Errorf("parse demo.json: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
for i, entry := range entries {
|
||||
e := &db.Entity{
|
||||
Body: entry.Body,
|
||||
Glyph: db.Glyph(entry.Glyph),
|
||||
Tags: entry.Tags,
|
||||
}
|
||||
|
||||
if entry.Title != nil {
|
||||
e.Title = entry.Title
|
||||
}
|
||||
if entry.Description != nil {
|
||||
e.Description = entry.Description
|
||||
}
|
||||
if entry.TimeAnchor != nil {
|
||||
e.TimeAnchor = entry.TimeAnchor
|
||||
}
|
||||
if entry.Pinned {
|
||||
e.Pinned = true
|
||||
}
|
||||
if entry.Completed {
|
||||
t := now.Add(-time.Duration(i) * time.Hour)
|
||||
e.CompletedAt = &t
|
||||
}
|
||||
|
||||
if err := store.Create(e); err != nil {
|
||||
return fmt.Errorf("entity %d: %w", i, err)
|
||||
}
|
||||
|
||||
if entry.CardType != nil {
|
||||
ct := db.CardType(*entry.CardType)
|
||||
if err := store.Promote(e.ID, ct, entry.CardData); err != nil {
|
||||
return fmt.Errorf("promote entity %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if entry.Deleted {
|
||||
store.SoftDelete(e.ID)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("seeded %d entities\n", len(entries))
|
||||
return nil
|
||||
}
|
||||
|
||||
func findDemoFile() ([]byte, error) {
|
||||
candidates := []string{
|
||||
"testdata/demo.json",
|
||||
filepath.Join(execDir(), "testdata", "demo.json"),
|
||||
}
|
||||
|
||||
for _, path := range candidates {
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("demo.json not found (looked in: %v)", candidates)
|
||||
}
|
||||
|
||||
func execDir() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return filepath.Dir(exe)
|
||||
}
|
||||
Reference in New Issue
Block a user