fix: harden API, DB schema, and CLI safety

- Add 'reminder' to glyph CHECK constraint (was accepted by parser but
  rejected by DB)
- Default serve bind to 127.0.0.1, add --host flag for LAN access
- Validate card_data as JSON in Store.Create/Update/Promote
- Return pagination envelope {data,total,limit,offset} from list endpoint
- Append absorb breadcrumb to source entity before soft-delete
- Add Levenshtein fuzzy match to catch command typos before routing to add
- Replace DDL string-matching migrations with versioned schema_version table
- Update web UI and API tests for envelope response format
This commit is contained in:
2026-05-19 18:30:17 -04:00
parent babf1d6620
commit e09919b679
9 changed files with 243 additions and 89 deletions
+28 -5
View File
@@ -104,6 +104,9 @@ type EntityUpdate struct {
}
func (s *Store) Create(e *Entity) error {
if e.CardData != nil && !json.Valid([]byte(*e.CardData)) {
return ErrInvalidCardData
}
now := time.Now().UTC()
e.ID = nibulid.New()
e.CreatedAt = now
@@ -179,7 +182,7 @@ func (s *Store) Get(id string) (*Entity, error) {
return e, nil
}
func (s *Store) List(params ListParams) ([]*Entity, error) {
func listWhere(params ListParams) (string, []any) {
var where []string
var args []any
@@ -214,10 +217,23 @@ func (s *Store) List(params ListParams) ([]*Entity, error) {
args = append(args, string(*params.CardTypeFilter))
}
whereClause := ""
clause := ""
if len(where) > 0 {
whereClause = "WHERE " + strings.Join(where, " AND ")
clause = "WHERE " + strings.Join(where, " AND ")
}
return clause, args
}
func (s *Store) Count(params ListParams) (int, error) {
whereClause, args := listWhere(params)
query := fmt.Sprintf("SELECT COUNT(*) FROM entities e %s", whereClause)
var count int
err := s.db.QueryRow(query, args...).Scan(&count)
return count, err
}
func (s *Store) List(params ListParams) ([]*Entity, error) {
whereClause, args := listWhere(params)
orderCol := "e.created_at"
switch params.Sort {
@@ -336,6 +352,9 @@ func (s *Store) Update(id string, u *EntityUpdate) error {
args = append(args, string(*u.CardType))
}
if u.CardData != nil {
if !json.Valid([]byte(*u.CardData)) {
return ErrInvalidCardData
}
sets = append(sets, "card_data = ?")
args = append(args, *u.CardData)
}
@@ -370,6 +389,9 @@ func (s *Store) Promote(id string, cardType CardType, cardData *string) error {
dataVal := "{}"
if cardData != nil {
if !json.Valid([]byte(*cardData)) {
return ErrInvalidCardData
}
dataVal = *cardData
}
@@ -473,8 +495,9 @@ func (s *Store) Absorb(targetID, sourceID string) error {
}
}
if _, err := tx.Exec("UPDATE entities SET deleted_at = ? WHERE id = ?",
now, sourceID); err != nil {
absorbNote := source.Body + "\n\n[absorbed into " + targetID + "]"
if _, err := tx.Exec("UPDATE entities SET body = ?, deleted_at = ?, modified_at = ? WHERE id = ?",
absorbNote, now, now, sourceID); err != nil {
return err
}