package entry import ( "fmt" "regexp" "strings" "time" "code.chrissexton.org/cws/cabinet/db" "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" ) type Entry struct { db *db.Database ID int64 Slug string Title string Content string Tags []string Created time.Time Updated time.Time AuthorID int64 `db:"author_id"` } func PrepareTable(tx *sqlx.Tx) error { q := `create table if not exists entries ( id integer primary key, slug text unique not null, content text not null, created datetime not null, updated datetime not null, author_id integer )` _, err := tx.Exec(q) if err != nil { tx.Rollback() return err } q = `create table if not exists tags ( id integer primary key, name text not null, entry_id integer, foreign key(entry_id) references entries(id), constraint unique_name_id unique (name, entry_id) )` _, err = tx.Exec(q) if err != nil { tx.Rollback() return err } return nil } func NewFromMd(db *db.Database, body string) *Entry { e := New(db) e.Content = body return e } func New(db *db.Database) *Entry { e := Entry{ db: db, ID: -1, Created: time.Now(), Updated: time.Now(), Tags: []string{}, } e.Title = e.GenerateTitle() e.Slug = e.UniqueSlug() e.Content = "= " + e.Title return &e } func GetBySlug(db *db.Database, slug string) (Entry, error) { e := Entry{db: db} q := `select * from entries where slug = ?` if err := db.Get(&e, q, slug); err != nil { return e, err } e.Title = e.GenerateTitle() return e, e.populateTags() } func GetByID(db *db.Database, id int64) (Entry, error) { e := Entry{db: db} q := `select * from entries where id = ?` if err := db.Get(&e, q, id); err != nil { return e, err } e.Title = e.GenerateTitle() return e, e.populateTags() } func Search(db *db.Database, query string) ([]*Entry, error) { entries := []*Entry{} log.Debug().Str("query", query).Msg("searching") if query != "" { q := `select * from entries where content like ? order by updated desc` err := db.Select(&entries, q, "%"+query+"%") if err != nil { return nil, err } } else { q := `select * from entries order by updated desc` err := db.Select(&entries, q) if err != nil { return nil, err } } for _, e := range entries { e.db = db e.Title = e.GenerateTitle() e.populateTags() } return entries, nil } func RemoveBySlug(db *db.Database, slug string) error { tx, err := db.Begin() if err != nil { return err } e, err := GetBySlug(db, slug) if err != nil { return err } q := `delete from tags where entry_id = ?` if _, err := tx.Exec(q, e.ID); err != nil { tx.Rollback() return err } q = `delete from entries where id = ?` if _, err := tx.Exec(q, e.ID); err != nil { tx.Rollback() return err } return tx.Commit() } func (e *Entry) populateTags() error { q := `select name from tags where entry_id = ?` err := e.db.Select(&e.Tags, q, e.ID) log.Debug().Interface("entry", e).Msg("populating tags") return err } func (e *Entry) addTag(tag string) error { q := `insert into tags (name,entry_id) values (?,?)` _, err := e.db.Exec(q, tag, e.ID) return err } func (e *Entry) removeTag(tag string) error { q := `delete from tags where name=? and entry_id=?` _, err := e.db.Exec(q, tag, e.ID) return err } func (e *Entry) GenerateTitle() string { candidate := strings.Split(e.Content, "\n")[0] candidateNumber := 0 r := regexp.MustCompile(`[^a-zA-Z0-9 -]`) candidate = r.ReplaceAllString(candidate, "") candidate = strings.TrimSpace(candidate) if len(candidate) == 0 { candidate = "untitled" } q := `select slug from entries where slug like ?` slugs := []string{} if err := e.db.Select(&slugs, q, candidate+"%"); err != nil { log.Debug().Err(err).Msgf("Could not get candidate slugs: %s", err) return candidate } contains := func(s string, ss []string) bool { for _, e := range ss { if s == e { return true } } return false } tmpCandidate := candidate for contains(tmpCandidate, slugs) { candidateNumber++ tmpCandidate = fmt.Sprintf("%s-%d", candidate, candidateNumber) } return tmpCandidate } func (e *Entry) UniqueSlug() string { if e.Title == "" { e.Title = e.GenerateTitle() } candidate := e.Title r := regexp.MustCompile(`[^a-zA-Z0-9 -]`) candidate = r.ReplaceAllString(candidate, "") candidate = strings.TrimSpace(candidate) candidate = strings.ReplaceAll(candidate, " ", "-") candidate = strings.ToLower(candidate) return candidate } func (e *Entry) Update() error { if e.ID == -1 { return e.Create() } old, err := GetByID(e.db, e.ID) if err != nil { return err } e.Slug = e.UniqueSlug() q := `update entries set slug=?, content=?, updated=?, author_id=? where id=?` if _, err = e.db.Exec(q, e.Slug, e.Content, e.Updated.Unix(), e.AuthorID, e.ID); err != nil { return err } for _, t := range e.Tags { if !contains(old.Tags, t) { e.addTag(t) } } for _, t := range old.Tags { if !contains(e.Tags, t) { e.removeTag(t) } } return nil } func contains(items []string, entry string) bool { for _, e := range items { if e == entry { return true } } return false } func (e *Entry) Create() error { if e.ID != -1 { return e.Update() } tx, err := e.db.Begin() if err != nil { return err } q := `insert into entries (slug,content,created,updated,author_id) values (?,?,?,?,?)` res, err := tx.Exec(q, e.Slug, e.Content, e.Created.Unix(), e.Updated.Unix(), e.AuthorID) if err != nil { return err } e.ID, err = res.LastInsertId() if err != nil { tx.Rollback() e.ID = -1 return err } for _, t := range e.Tags { q = `insert into tags (name,entry_id) values (?,?)` _, err = tx.Exec(q, t, e.ID) if err != nil { tx.Rollback() return err } } tx.Commit() return nil }