fix: correct GetProperty bug, init to use .ax/, add default aliases, split e2e tests, add due date tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 01:58:48 +02:00
parent 5969a2591c
commit 921f4913f8
13 changed files with 1121 additions and 971 deletions
+26 -1
View File
@@ -3,12 +3,28 @@ package store
import (
"encoding/json"
"errors"
"fmt"
"os"
"os/user"
"path/filepath"
"slices"
)
var builtinAliases = []*Alias{
{Name: "mine", Command: "list --assignee $me", Description: "My assigned issues"},
{Name: "due", Command: "list --status open", Description: "Open issues"},
{Name: "inbox", Command: "list --mention $me", Description: "My mentions"},
}
func isBuiltinAlias(name string) bool {
for _, a := range builtinAliases {
if a.Name == name {
return true
}
}
return false
}
type Alias struct {
Name string `json:"name"`
Command string `json:"command"`
@@ -128,6 +144,9 @@ func (c *Config) SetAlias(alias *Alias) error {
}
func (c *Config) DeleteAlias(name string) error {
if isBuiltinAlias(name) {
return fmt.Errorf("cannot delete built-in alias %q", name)
}
for i, a := range c.UserAliases {
if a.Name == name {
c.UserAliases = slices.Delete(c.UserAliases, i, i+1)
@@ -140,10 +159,16 @@ func (c *Config) DeleteAlias(name string) error {
func (c *Config) ListAliases() ([]*Alias, error) {
seen := make(map[string]bool)
var result []*Alias
for _, a := range c.UserAliases {
for _, a := range builtinAliases {
result = append(result, a)
seen[a.Name] = true
}
for _, a := range c.UserAliases {
if !seen[a.Name] {
result = append(result, a)
seen[a.Name] = true
}
}
return result, nil
}
+6 -3
View File
@@ -1,15 +1,18 @@
package store
import "axolotl/models"
import (
"axolotl/models"
"time"
)
// GraphStore is a primitive graph persistence interface. It provides basic
// operations for nodes and directed rels. No business logic lives here.
// "Tag" rels are rels with an empty toID (e.g. "_type::issue" or "backend").
type GraphStore interface {
// Nodes
AddNode(id, title, content, dueDate, createdAt, updatedAt string) error
AddNode(id, title, content string, dueDate *time.Time, createdAt, updatedAt string) error
GetNode(id string) (*models.Node, error) // returns node with tags and rels populated
UpdateNode(id, title, content, dueDate, updatedAt string) error // empty dueDate stores NULL
UpdateNode(id, title, content string, dueDate *time.Time, updatedAt string) error // nil dueDate stores NULL
DeleteNode(id string) error
NodeExists(id string) (bool, error)
GenerateID() (string, error) // returns a random 5-char ID guaranteed unique in the store
+61 -12
View File
@@ -8,12 +8,13 @@ import (
"os"
"path/filepath"
"strings"
"time"
_ "modernc.org/sqlite"
)
var 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 nodes (id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT, due_date DATETIME, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)`,
`CREATE TABLE IF NOT EXISTS rels (from_id TEXT NOT NULL, rel_name TEXT NOT NULL, to_id TEXT NOT NULL DEFAULT '', PRIMARY KEY (from_id, rel_name, to_id), FOREIGN KEY (from_id) REFERENCES nodes(id) ON DELETE CASCADE)`,
`CREATE INDEX IF NOT EXISTS idx_rels_from ON rels(from_id)`,
`CREATE INDEX IF NOT EXISTS idx_rels_to ON rels(to_id)`,
@@ -129,6 +130,10 @@ func NewSQLiteStore(path string) (GraphStore, error) {
db.Close()
return nil, fmt.Errorf("schema migration failed: %w", err)
}
if err := migrateDueDateColumn(db); err != nil {
db.Close()
return nil, fmt.Errorf("due_date column migration failed: %w", err)
}
if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
db.Close()
return nil, err
@@ -163,6 +168,44 @@ func migrateSchema(db *sql.DB) error {
return nil
}
// migrateDueDateColumn converts the due_date column from TEXT to DATETIME affinity
// for databases created before this schema change. It is a no-op when already migrated.
func migrateDueDateColumn(db *sql.DB) error {
rows, err := db.Query("PRAGMA table_info(nodes)")
if err != nil {
return err
}
defer rows.Close()
var colType string
for rows.Next() {
var cid, notNull, pk int
var name, typ string
var dfltVal any
if err := rows.Scan(&cid, &name, &typ, &notNull, &dfltVal, &pk); err != nil {
return err
}
if name == "due_date" {
colType = strings.ToUpper(typ)
break
}
}
rows.Close()
if colType == "DATETIME" || colType == "" {
return nil // already on new schema or column missing
}
for _, stmt := range []string{
`CREATE TABLE nodes_new (id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT, due_date DATETIME, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)`,
`INSERT INTO nodes_new SELECT id, title, content, due_date, created_at, updated_at FROM nodes`,
`DROP TABLE nodes`,
`ALTER TABLE nodes_new RENAME TO nodes`,
} {
if _, err := db.Exec(stmt); err != nil {
return err
}
}
return nil
}
// --- Transaction ---
func (s *GraphStoreSqlite) Transaction(fn func(GraphStore) error) error {
@@ -183,10 +226,10 @@ func (s *txStore) Transaction(fn func(GraphStore) error) error {
// --- Node operations ---
func addNode(q querier, id, title, content, dueDate, createdAt, updatedAt string) error {
func addNode(q querier, id, title, content string, dueDate *time.Time, createdAt, updatedAt string) error {
var dd any
if dueDate != "" {
dd = dueDate
if dueDate != nil {
dd = dueDate.UTC().Format("2006-01-02")
}
_, err := q.Exec(
"INSERT INTO nodes (id, title, content, due_date, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
@@ -197,12 +240,18 @@ func addNode(q querier, id, title, content, dueDate, createdAt, updatedAt string
func getNode(q querier, id string) (*models.Node, error) {
n := models.NewNode()
var dueDateStr string
err := q.QueryRow(
"SELECT id, title, COALESCE(content, ''), COALESCE(due_date, ''), created_at, updated_at FROM nodes WHERE id = ?", id,
).Scan(&n.ID, &n.Title, &n.Content, &n.DueDate, &n.CreatedAt, &n.UpdatedAt)
).Scan(&n.ID, &n.Title, &n.Content, &dueDateStr, &n.CreatedAt, &n.UpdatedAt)
if err != nil {
return nil, err
}
if dueDateStr != "" {
if d, err := models.ParseDate(dueDateStr); err == nil {
n.DueDate = &d
}
}
rows, err := q.Query("SELECT rel_name, to_id FROM rels WHERE from_id = ?", id)
if err != nil {
return nil, err
@@ -220,10 +269,10 @@ func getNode(q querier, id string) (*models.Node, error) {
return n, nil
}
func updateNode(q querier, id, title, content, dueDate, updatedAt string) error {
func updateNode(q querier, id, title, content string, dueDate *time.Time, updatedAt string) error {
var dd any
if dueDate != "" {
dd = dueDate
if dueDate != nil {
dd = dueDate.UTC().Format("2006-01-02")
}
_, err := q.Exec(
"UPDATE nodes SET title = ?, content = ?, due_date = ?, updated_at = ? WHERE id = ?",
@@ -243,21 +292,21 @@ func nodeExists(q querier, id string) (bool, error) {
return e, err
}
func (s *GraphStoreSqlite) AddNode(id, title, content, dueDate, createdAt, updatedAt string) error {
func (s *GraphStoreSqlite) AddNode(id, title, content string, dueDate *time.Time, createdAt, updatedAt string) error {
return addNode(s.db, id, title, content, dueDate, createdAt, updatedAt)
}
func (s *GraphStoreSqlite) GetNode(id string) (*models.Node, error) { return getNode(s.db, id) }
func (s *GraphStoreSqlite) UpdateNode(id, title, content, dueDate, updatedAt string) error {
func (s *GraphStoreSqlite) UpdateNode(id, title, content string, dueDate *time.Time, updatedAt string) error {
return updateNode(s.db, id, title, content, dueDate, updatedAt)
}
func (s *GraphStoreSqlite) DeleteNode(id string) error { return deleteNode(s.db, id) }
func (s *GraphStoreSqlite) NodeExists(id string) (bool, error) { return nodeExists(s.db, id) }
func (s *txStore) AddNode(id, title, content, dueDate, createdAt, updatedAt string) error {
func (s *txStore) AddNode(id, title, content string, dueDate *time.Time, createdAt, updatedAt string) error {
return addNode(s.tx, id, title, content, dueDate, createdAt, updatedAt)
}
func (s *txStore) GetNode(id string) (*models.Node, error) { return getNode(s.tx, id) }
func (s *txStore) UpdateNode(id, title, content, dueDate, updatedAt string) error {
func (s *txStore) UpdateNode(id, title, content string, dueDate *time.Time, updatedAt string) error {
return updateNode(s.tx, id, title, content, dueDate, updatedAt)
}
func (s *txStore) DeleteNode(id string) error { return deleteNode(s.tx, id) }