refactor: remove db package and move database logic to service layer
This commit is contained in:
15
cmd/add.go
15
cmd/add.go
@@ -1,7 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/models"
|
"axolotl/models"
|
||||||
"axolotl/output"
|
"axolotl/output"
|
||||||
"axolotl/service"
|
"axolotl/service"
|
||||||
@@ -19,12 +18,6 @@ var cTags, cRels []string
|
|||||||
var addCmd = &cobra.Command{
|
var addCmd = &cobra.Command{
|
||||||
Use: "add <title>", Short: "Create a new node", Args: cobra.ExactArgs(1),
|
Use: "add <title>", Short: "Create a new node", Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
d, err := db.GetDB()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !slices.ContainsFunc(cTags, func(e string) bool { return strings.HasPrefix(e, "_type::") }) {
|
if !slices.ContainsFunc(cTags, func(e string) bool { return strings.HasPrefix(e, "_type::") }) {
|
||||||
cTags = append(cTags, "_type::issue")
|
cTags = append(cTags, "_type::issue")
|
||||||
}
|
}
|
||||||
@@ -49,11 +42,13 @@ var addCmd = &cobra.Command{
|
|||||||
rels[models.RelInNamespace] = append(rels[models.RelInNamespace], cfg.GetUser())
|
rels[models.RelInNamespace] = append(rels[models.RelInNamespace], cfg.GetUser())
|
||||||
}
|
}
|
||||||
|
|
||||||
svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
|
svc, err := service.GetNodeService(cfg)
|
||||||
if n, err := svc.Create(args[0], cContent, cDue, cTags, rels); err != nil {
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "failed to create:", err)
|
||||||
|
} else if n, err := svc.Create(args[0], cContent, cDue, cTags, rels); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "failed to create:", err)
|
fmt.Fprintln(os.Stderr, "failed to create:", err)
|
||||||
} else {
|
} else {
|
||||||
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
|
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/output"
|
"axolotl/output"
|
||||||
"axolotl/service"
|
"axolotl/service"
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -16,12 +15,11 @@ var dForce bool
|
|||||||
var delCmd = &cobra.Command{
|
var delCmd = &cobra.Command{
|
||||||
Use: "del <id>", Short: "Delete a node", Args: cobra.ExactArgs(1),
|
Use: "del <id>", Short: "Delete a node", Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
d, err := db.GetDB()
|
svc, err := service.GetNodeService(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, "failed to create service: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
|
|
||||||
n, err := svc.GetByID(args[0])
|
n, err := svc.GetByID(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, " node not found:", args[0])
|
fmt.Fprintln(os.Stderr, " node not found:", args[0])
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/output"
|
"axolotl/output"
|
||||||
"axolotl/service"
|
"axolotl/service"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,12 +13,11 @@ import (
|
|||||||
var editCmd = &cobra.Command{
|
var editCmd = &cobra.Command{
|
||||||
Use: "edit <id>", Short: "Edit node content in $EDITOR", Args: cobra.ExactArgs(1),
|
Use: "edit <id>", Short: "Edit node content in $EDITOR", Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
d, err := db.GetDB()
|
svc, err := service.GetNodeService(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
|
|
||||||
n, err := svc.GetByID(args[0])
|
n, err := svc.GetByID(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "node not found:", args[0])
|
fmt.Fprintln(os.Stderr, "node not found:", args[0])
|
||||||
@@ -53,7 +51,7 @@ var editCmd = &cobra.Command{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
n, _ = svc.GetByID(args[0])
|
n, _ = svc.GetByID(args[0])
|
||||||
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
|
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintln(os.Stderr, "failed to read temp file:", err)
|
fmt.Fprintln(os.Stderr, "failed to read temp file:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/output"
|
"axolotl/output"
|
||||||
|
"axolotl/service"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -22,7 +22,7 @@ var initCmd = &cobra.Command{
|
|||||||
fmt.Fprintln(os.Stderr, "database already exists:", dbPath)
|
fmt.Fprintln(os.Stderr, "database already exists:", dbPath)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if err := db.Init(dbPath); err != nil {
|
if err := service.InitNodeService(dbPath); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "failed to initialize:", err)
|
fmt.Fprintln(os.Stderr, "failed to initialize:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/output"
|
"axolotl/output"
|
||||||
"axolotl/service"
|
"axolotl/service"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -17,12 +16,11 @@ var lMention string
|
|||||||
var listCmd = &cobra.Command{
|
var listCmd = &cobra.Command{
|
||||||
Use: "list", Short: "List nodes",
|
Use: "list", Short: "List nodes",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
d, err := db.GetDB()
|
svc, err := service.GetNodeService(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
|
|
||||||
opts := []service.ListOption{}
|
opts := []service.ListOption{}
|
||||||
if len(lTags) > 0 {
|
if len(lTags) > 0 {
|
||||||
opts = append(opts, service.WithTags(lTags...))
|
opts = append(opts, service.WithTags(lTags...))
|
||||||
@@ -35,7 +33,7 @@ var listCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nodes, err := svc.List(opts...); err == nil {
|
if nodes, err := svc.List(opts...); err == nil {
|
||||||
output.PrintNodes(cmd.OutOrStdout(), nodes, jsonFlag)
|
output.PrintNodes(cmd.OutOrStdout(), svc, nodes, jsonFlag)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "err: %v\n", err)
|
fmt.Fprintf(os.Stderr, "err: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
36
cmd/root.go
36
cmd/root.go
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{Use: "ax", Short: "The axolotl issue tracker"}
|
|||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
var err error
|
var err error
|
||||||
cfg, err = service.LoadConfig()
|
cfg, err = service.LoadConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "failed to load config:", err)
|
fmt.Fprintln(os.Stderr, "failed to load config:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -27,18 +27,48 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func expandAlias(alias *service.Alias, args []string, currentUser string) []string {
|
||||||
|
cmd := alias.Command
|
||||||
|
cmd = strings.ReplaceAll(cmd, "$me", currentUser)
|
||||||
|
|
||||||
|
parts := strings.Fields(cmd)
|
||||||
|
var result []string
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == "$@" {
|
||||||
|
result = append(result, args...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCatchAll := strings.Contains(part, "$@")
|
||||||
|
replaced := part
|
||||||
|
|
||||||
|
if hasCatchAll {
|
||||||
|
replaced = strings.ReplaceAll(replaced, "$@", strings.Join(args, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(args) - 1; i >= 0; i-- {
|
||||||
|
placeholder := fmt.Sprintf("$%d", i+1)
|
||||||
|
replaced = strings.ReplaceAll(replaced, placeholder, args[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, replaced)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func registerAliasCommands() {
|
func registerAliasCommands() {
|
||||||
rootCmd.AddGroup(&cobra.Group{ID: "aliases", Title: "Aliases:"})
|
rootCmd.AddGroup(&cobra.Group{ID: "aliases", Title: "Aliases:"})
|
||||||
aliases, _ := cfg.ListAliases()
|
aliases, _ := cfg.ListAliases()
|
||||||
for _, a := range aliases {
|
for _, a := range aliases {
|
||||||
a := a
|
|
||||||
rootCmd.AddCommand(&cobra.Command{
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
Use: a.Name,
|
Use: a.Name,
|
||||||
Short: a.Description,
|
Short: a.Description,
|
||||||
GroupID: "aliases",
|
GroupID: "aliases",
|
||||||
DisableFlagParsing: true,
|
DisableFlagParsing: true,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
expanded := service.ExpandAlias(a, args, cfg.GetUser())
|
expanded := expandAlias(a, args, cfg.GetUser())
|
||||||
rootCmd.SetArgs(transformArgs(expanded))
|
rootCmd.SetArgs(transformArgs(expanded))
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/output"
|
"axolotl/output"
|
||||||
"axolotl/service"
|
"axolotl/service"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -13,14 +12,13 @@ import (
|
|||||||
var showCmd = &cobra.Command{
|
var showCmd = &cobra.Command{
|
||||||
Use: "show <id>", Short: "Show node details", Args: cobra.ExactArgs(1),
|
Use: "show <id>", Short: "Show node details", Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
d, err := db.GetDB()
|
svc, err := service.GetNodeService(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
|
|
||||||
if n, err := svc.GetByID(args[0]); err == nil {
|
if n, err := svc.GetByID(args[0]); err == nil {
|
||||||
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
|
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintln(os.Stderr, "node not found:", args[0])
|
fmt.Fprintln(os.Stderr, "node not found:", args[0])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/models"
|
"axolotl/models"
|
||||||
"axolotl/output"
|
"axolotl/output"
|
||||||
"axolotl/service"
|
"axolotl/service"
|
||||||
@@ -14,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
uTitle, uContent, uDue string
|
uTitle, uContent, uDue string
|
||||||
uClearDue bool
|
uClearDue bool
|
||||||
uAddTags, uRmTags, uAddRels, uRmRels []string
|
uAddTags, uRmTags, uAddRels, uRmRels []string
|
||||||
)
|
)
|
||||||
@@ -22,12 +21,11 @@ var (
|
|||||||
var updateCmd = &cobra.Command{
|
var updateCmd = &cobra.Command{
|
||||||
Use: "update <id>", Short: "Update a node", Args: cobra.ExactArgs(1),
|
Use: "update <id>", Short: "Update a node", Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
d, err := db.GetDB()
|
svc, err := service.GetNodeService(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
|
|
||||||
|
|
||||||
node, err := svc.GetByID(args[0])
|
node, err := svc.GetByID(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,7 +63,7 @@ var updateCmd = &cobra.Command{
|
|||||||
} else if slices.Contains(uAddTags, "_status::open") {
|
} else if slices.Contains(uAddTags, "_status::open") {
|
||||||
uRmTags = append(uRmTags, "_status::done")
|
uRmTags = append(uRmTags, "_status::done")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, prefix := range []string{"_type::", "_status::", "_prio::", "_namespace::"} {
|
for _, prefix := range []string{"_type::", "_status::", "_prio::", "_namespace::"} {
|
||||||
if slices.ContainsFunc(uAddTags, func(e string) bool { return strings.HasPrefix(e, prefix) }) {
|
if slices.ContainsFunc(uAddTags, func(e string) bool { return strings.HasPrefix(e, prefix) }) {
|
||||||
for _, existing := range node.Tags {
|
for _, existing := range node.Tags {
|
||||||
@@ -76,7 +74,6 @@ var updateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if cmd.Flags().Changed("title") {
|
if cmd.Flags().Changed("title") {
|
||||||
node.Title = uTitle
|
node.Title = uTitle
|
||||||
}
|
}
|
||||||
@@ -117,7 +114,7 @@ var updateCmd = &cobra.Command{
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if n, err := svc.GetByID(args[0]); err == nil {
|
if n, err := svc.GetByID(args[0]); err == nil {
|
||||||
output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
|
output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintln(os.Stderr, "failed to fetch node:", err)
|
fmt.Fprintln(os.Stderr, "failed to fetch node:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
72
db/db.go
72
db/db.go
@@ -1,72 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DB struct {
|
|
||||||
*sql.DB
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
database *DB
|
|
||||||
migrations = []string{
|
|
||||||
`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT, due_date TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)`,
|
|
||||||
`CREATE TABLE IF NOT EXISTS tags (node_id TEXT NOT NULL, tag TEXT NOT NULL, PRIMARY KEY (node_id, tag), FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE)`,
|
|
||||||
`CREATE TABLE IF NOT EXISTS rels (from_id TEXT NOT NULL, to_id TEXT NOT NULL, rel_type TEXT NOT NULL, PRIMARY KEY (from_id, to_id, rel_type), FOREIGN KEY (from_id) REFERENCES nodes(id) ON DELETE CASCADE, FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE)`,
|
|
||||||
`CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag)`, `CREATE INDEX IF NOT EXISTS idx_rels_from ON rels(from_id)`, `CREATE INDEX IF NOT EXISTS idx_rels_to ON rels(to_id)`,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetDB() (*DB, error) {
|
|
||||||
if database != nil {
|
|
||||||
return database, nil
|
|
||||||
}
|
|
||||||
dir, err := filepath.Abs(".")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
if _, err := os.Stat(filepath.Join(dir, ".ax.db")); err == nil {
|
|
||||||
if database, err = Open(filepath.Join(dir, ".ax.db")); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
|
||||||
}
|
|
||||||
return database, nil
|
|
||||||
}
|
|
||||||
if parent := filepath.Dir(dir); parent == dir {
|
|
||||||
return nil, errors.New("no .ax.db found (run 'ax init' first)")
|
|
||||||
} else {
|
|
||||||
dir = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Init(path string) error {
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
database, err = Open(path)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Open(path string) (*DB, error) {
|
|
||||||
db, err := sql.Open("sqlite", path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, q := range append([]string{"PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=5000", "PRAGMA foreign_keys=ON"}, migrations...) {
|
|
||||||
if _, err := db.Exec(q); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &DB{DB: db, path: path}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
19
db/rel.go
19
db/rel.go
@@ -1,19 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import "axolotl/models"
|
|
||||||
|
|
||||||
func (db *DB) GetRelNames(n *models.Node, r models.RelType) ([]string, error) {
|
|
||||||
ids := n.Relations[string(r)]
|
|
||||||
if len(ids) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
result := make([]string, 0, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
var title string
|
|
||||||
if err := db.QueryRow("SELECT title FROM nodes WHERE id = ?", id).Scan(&title); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result = append(result, title)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -13,18 +13,6 @@ type Node struct {
|
|||||||
Relations map[string][]string `json:"relations,omitempty"`
|
Relations map[string][]string `json:"relations,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RelType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RelBlocks RelType = "blocks"
|
|
||||||
RelSubtask RelType = "subtask"
|
|
||||||
RelRelated RelType = "related"
|
|
||||||
RelCreated RelType = "created"
|
|
||||||
RelAssignee RelType = "assignee"
|
|
||||||
RelInNamespace RelType = "in_namespace"
|
|
||||||
RelMentions RelType = "mentions"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (n *Node) GetProperty(k string) string {
|
func (n *Node) GetProperty(k string) string {
|
||||||
for _, t := range n.Tags {
|
for _, t := range n.Tags {
|
||||||
if strings.HasPrefix(t, "_") {
|
if strings.HasPrefix(t, "_") {
|
||||||
|
|||||||
13
models/rel_type.go
Normal file
13
models/rel_type.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type RelType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RelBlocks RelType = "blocks"
|
||||||
|
RelSubtask RelType = "subtask"
|
||||||
|
RelRelated RelType = "related"
|
||||||
|
RelCreated RelType = "created"
|
||||||
|
RelAssignee RelType = "assignee"
|
||||||
|
RelInNamespace RelType = "in_namespace"
|
||||||
|
RelMentions RelType = "mentions"
|
||||||
|
)
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package output
|
package output
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"axolotl/db"
|
|
||||||
"axolotl/models"
|
"axolotl/models"
|
||||||
"axolotl/service"
|
"axolotl/service"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -59,21 +58,7 @@ const (
|
|||||||
iconNamespace = "\uf07b"
|
iconNamespace = "\uf07b"
|
||||||
)
|
)
|
||||||
|
|
||||||
func render(rm RenderMap, key string, short bool) string {
|
func PrintNodes(w io.Writer, svc service.NodeService, nodes []*models.Node, jsonOut bool) error {
|
||||||
v, ok := rm[key]
|
|
||||||
if !ok {
|
|
||||||
v, ok = rm[""]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if short {
|
|
||||||
return v.c.Sprint(v.s)
|
|
||||||
}
|
|
||||||
return v.c.Sprint(v.l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
|
|
||||||
if jsonOut {
|
if jsonOut {
|
||||||
return json.NewEncoder(w).Encode(nodes)
|
return json.NewEncoder(w).Encode(nodes)
|
||||||
}
|
}
|
||||||
@@ -82,11 +67,6 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := db.GetDB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
sort.Slice(nodes, func(i, j int) bool {
|
sort.Slice(nodes, func(i, j int) bool {
|
||||||
si, sj := nodes[i].GetProperty("status"), nodes[j].GetProperty("status")
|
si, sj := nodes[i].GetProperty("status"), nodes[j].GetProperty("status")
|
||||||
@@ -97,9 +77,14 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
ns_rels, err := d.GetRelNames(n, models.RelInNamespace)
|
ns_rel_node_ids := n.Relations[string(models.RelInNamespace)]
|
||||||
if err != nil {
|
ns_rel_node_titles := make([]string, 0, len(ns_rel_node_ids))
|
||||||
return err
|
for _, id := range ns_rel_node_ids {
|
||||||
|
ns_rel_node, err := svc.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "err: %v", err)
|
||||||
|
}
|
||||||
|
ns_rel_node_titles = append(ns_rel_node_titles, ns_rel_node.Title)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, " %s %s %s %s %s %s",
|
fmt.Fprintf(w, " %s %s %s %s %s %s",
|
||||||
cDim.Sprint(n.ID),
|
cDim.Sprint(n.ID),
|
||||||
@@ -107,7 +92,7 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
|
|||||||
render(statusRM, n.GetProperty("status"), true),
|
render(statusRM, n.GetProperty("status"), true),
|
||||||
render(typeRM, n.GetProperty("type"), true),
|
render(typeRM, n.GetProperty("type"), true),
|
||||||
cTitle.Sprint(truncate(n.Title, 80)),
|
cTitle.Sprint(truncate(n.Title, 80)),
|
||||||
cDim.Sprint("["+strings.Join(ns_rels, ",")+"]"),
|
cDim.Sprint("["+strings.Join(ns_rel_node_titles, ",")+"]"),
|
||||||
)
|
)
|
||||||
tags := n.GetDisplayTags()
|
tags := n.GetDisplayTags()
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
@@ -119,7 +104,7 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintNode(w io.Writer, n *models.Node, jsonOut bool) error {
|
func PrintNode(w io.Writer, svc service.NodeService, n *models.Node, jsonOut bool) error {
|
||||||
if jsonOut {
|
if jsonOut {
|
||||||
return json.NewEncoder(w).Encode(n)
|
return json.NewEncoder(w).Encode(n)
|
||||||
}
|
}
|
||||||
@@ -138,20 +123,17 @@ func PrintNode(w io.Writer, n *models.Node, jsonOut bool) error {
|
|||||||
fmt.Fprintf(w, "\n tags: %s\n", cPrimary.Sprint(strings.Join(tags, " • ")))
|
fmt.Fprintf(w, "\n tags: %s\n", cPrimary.Sprint(strings.Join(tags, " • ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
if db, err := db.GetDB(); err != nil {
|
for relType := range n.Relations {
|
||||||
fmt.Fprintf(w, "failed to attach to db: %v", err)
|
rel_node_ids := n.Relations[string(relType)]
|
||||||
} else {
|
if len(rel_node_ids) > 0 {
|
||||||
for relType := range n.Relations {
|
fmt.Fprintf(w, "\n %s\n", string(relType))
|
||||||
names, err := db.GetRelNames(n, models.RelType(relType))
|
}
|
||||||
|
for _, id := range rel_node_ids {
|
||||||
|
rel_node, err := svc.GetByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(w, "err: %v", err)
|
fmt.Fprintf(w, "err: %v", err)
|
||||||
}
|
}
|
||||||
if len(names) > 0 {
|
fmt.Fprintf(w, " %s %s\n", relIcons[relType], rel_node.Title)
|
||||||
fmt.Fprintf(w, "\n %s\n", string(relType))
|
|
||||||
}
|
|
||||||
for _, name := range names {
|
|
||||||
fmt.Fprintf(w, " %s %s\n", relIcons[relType], name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +183,20 @@ func PrintAction(w io.Writer, action, detail string, isError bool) {
|
|||||||
fmt.Fprintln(w, cGood.Sprint(icon+" "+action+" ")+cDim.Sprint(detail))
|
fmt.Fprintln(w, cGood.Sprint(icon+" "+action+" ")+cDim.Sprint(detail))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func render(rm RenderMap, key string, short bool) string {
|
||||||
|
v, ok := rm[key]
|
||||||
|
if !ok {
|
||||||
|
v, ok = rm[""]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if short {
|
||||||
|
return v.c.Sprint(v.s)
|
||||||
|
}
|
||||||
|
return v.c.Sprint(v.l)
|
||||||
|
}
|
||||||
|
|
||||||
func truncate(s string, max int) string {
|
func truncate(s string, max int) string {
|
||||||
if len(s) <= max {
|
if len(s) <= max {
|
||||||
return s
|
return s
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type fileConfig struct {
|
type fileConfig struct {
|
||||||
@@ -18,13 +16,12 @@ type fileConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var defaultAliases = []*Alias{
|
var defaultAliases = []*Alias{
|
||||||
{Name: "mine", Command: "list --assignee $me --tag _status::open", Description: "Show open tasks assigned to you"},
|
{Name: "mine", Command: "list --assignee $me --tag _type::issue --tag _status::open", Description: "Show open issues assigned to you"},
|
||||||
{Name: "due", Command: "list --tag _status::open --tag _due", Description: "Show open tasks with due dates"},
|
{Name: "due", Command: "list --tag _type::issue --tag _status::open", Description: "Show open issues"},
|
||||||
{Name: "new", Command: "add $@", Description: "Create a new task"},
|
|
||||||
{Name: "inbox", Command: "list --mention $me", Description: "Show your inbox"},
|
{Name: "inbox", Command: "list --mention $me", Description: "Show your inbox"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() (Config, error) {
|
func LoadConfigFile() (Config, error) {
|
||||||
path, err := findConfigPath()
|
path, err := findConfigPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -153,34 +150,3 @@ func (c *fileConfig) Save() error {
|
|||||||
}
|
}
|
||||||
return os.WriteFile(c.path, data, 0644)
|
return os.WriteFile(c.path, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpandAlias(alias *Alias, args []string, currentUser string) []string {
|
|
||||||
cmd := alias.Command
|
|
||||||
cmd = strings.ReplaceAll(cmd, "$me", currentUser)
|
|
||||||
|
|
||||||
parts := strings.Fields(cmd)
|
|
||||||
var result []string
|
|
||||||
|
|
||||||
for _, part := range parts {
|
|
||||||
if part == "$@" {
|
|
||||||
result = append(result, args...)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
hasCatchAll := strings.Contains(part, "$@")
|
|
||||||
replaced := part
|
|
||||||
|
|
||||||
if hasCatchAll {
|
|
||||||
replaced = strings.ReplaceAll(replaced, "$@", strings.Join(args, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := len(args) - 1; i >= 0; i-- {
|
|
||||||
placeholder := fmt.Sprintf("$%d", i+1)
|
|
||||||
replaced = strings.ReplaceAll(replaced, placeholder, args[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
result = append(result, replaced)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,18 @@ type NodeService interface {
|
|||||||
CanClose(id string) (bool, []string, error)
|
CanClose(id string) (bool, []string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitNodeService(path string) error {
|
||||||
|
return InitSqliteDB(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNodeService(cfg Config) (NodeService, error) {
|
||||||
|
db, err := GetSqliteDB(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sqliteNodeService{db: db, userID: cfg.GetUser()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type listFilter struct {
|
type listFilter struct {
|
||||||
tagPrefixes []string
|
tagPrefixes []string
|
||||||
assignee string
|
assignee string
|
||||||
|
|||||||
@@ -3,10 +3,16 @@ package service
|
|||||||
import (
|
import (
|
||||||
"axolotl/models"
|
"axolotl/models"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sqliteNodeService struct {
|
type sqliteNodeService struct {
|
||||||
@@ -14,8 +20,50 @@ type sqliteNodeService struct {
|
|||||||
userID string
|
userID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSQLiteNodeService(db *sql.DB, userID string) NodeService {
|
var migrations = []string{
|
||||||
return &sqliteNodeService{db: db, userID: userID}
|
`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT, due_date TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS tags (node_id TEXT NOT NULL, tag TEXT NOT NULL, PRIMARY KEY (node_id, tag), FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS rels (from_id TEXT NOT NULL, to_id TEXT NOT NULL, rel_type TEXT NOT NULL, PRIMARY KEY (from_id, to_id, rel_type), FOREIGN KEY (from_id) REFERENCES nodes(id) ON DELETE CASCADE, FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE)`,
|
||||||
|
`CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag)`, `CREATE INDEX IF NOT EXISTS idx_rels_from ON rels(from_id)`, `CREATE INDEX IF NOT EXISTS idx_rels_to ON rels(to_id)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitSqliteDB(path string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
db, err := sql.Open("sqlite", path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, q := range append([]string{"PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=5000", "PRAGMA foreign_keys=ON"}, migrations...) {
|
||||||
|
if _, err := db.Exec(q); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSqliteDB(cfg Config) (*sql.DB, error) {
|
||||||
|
dir, err := filepath.Abs(".")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
dbpath := filepath.Join(dir, ".ax.db")
|
||||||
|
if _, err := os.Stat(dbpath); err == nil {
|
||||||
|
db, err := sql.Open("sqlite", dbpath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
if parent := filepath.Dir(dir); parent == dir {
|
||||||
|
return nil, errors.New("no .ax.db found (run 'ax init' first)")
|
||||||
|
} else {
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sqliteNodeService) GetByID(id string) (*models.Node, error) {
|
func (s *sqliteNodeService) GetByID(id string) (*models.Node, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user