package store import ( "context" "time" ) // Module is one building block of a blog's layout: a menu, an archive, a // block of custom HTML… placed in one of the areas of the page. type Module struct { ID int64 BlogID int64 Area string // header | left | right | above | below | footer Kind string // title | logo | menu | archive | recent | html | rss | text | sitemap Title string // optional heading (side columns and footer) Body string // custom HTML, or the footer text Count int // recent posts to show SortOrder int CreatedAt time.Time UpdatedAt time.Time } const moduleCols = `id, blog_id, area, kind, title, body, count, sort_order, created_at, updated_at` func scanModule(row interface{ Scan(...any) error }) (*Module, error) { var m Module err := row.Scan(&m.ID, &m.BlogID, &m.Area, &m.Kind, &m.Title, &m.Body, &m.Count, &m.SortOrder, &m.CreatedAt, &m.UpdatedAt) if err != nil { return nil, wrap(err) } return &m, nil } // ListModules returns every module of a blog, grouped by area in display order. func (s *Store) ListModules(ctx context.Context, blogID int64) ([]Module, error) { rows, err := s.db.Query(ctx, `SELECT `+moduleCols+` FROM modules WHERE blog_id=$1 ORDER BY area, sort_order, id`, blogID) if err != nil { return nil, err } defer rows.Close() var out []Module for rows.Next() { m, err := scanModule(rows) if err != nil { return nil, err } out = append(out, *m) } return out, rows.Err() } func (s *Store) ModuleByID(ctx context.Context, blogID, id int64) (*Module, error) { return scanModule(s.db.QueryRow(ctx, `SELECT `+moduleCols+` FROM modules WHERE blog_id=$1 AND id=$2`, blogID, id)) } // CreateModule appends a module to the end of its area. func (s *Store) CreateModule(ctx context.Context, m *Module) (*Module, error) { return scanModule(s.db.QueryRow(ctx, `INSERT INTO modules (blog_id, area, kind, title, body, count, sort_order) VALUES ($1,$2,$3,$4,$5,$6,(SELECT coalesce(max(sort_order),-1)+1 FROM modules WHERE blog_id=$1 AND area=$2)) RETURNING `+moduleCols, m.BlogID, m.Area, m.Kind, m.Title, m.Body, m.Count)) } // UpdateModule saves the editable settings; area and kind never change. func (s *Store) UpdateModule(ctx context.Context, m *Module) error { _, err := s.db.Exec(ctx, `UPDATE modules SET title=$3, body=$4, count=$5, updated_at=now() WHERE blog_id=$1 AND id=$2`, m.BlogID, m.ID, m.Title, m.Body, m.Count) return wrap(err) } func (s *Store) DeleteModule(ctx context.Context, blogID, id int64) error { _, err := s.db.Exec(ctx, `DELETE FROM modules WHERE blog_id=$1 AND id=$2`, blogID, id) return err } // MoveModule swaps sort_order with the neighbouring module of the same area (dir = -1 up, +1 down). func (s *Store) MoveModule(ctx context.Context, blogID, id int64, dir int) error { cur, err := s.ModuleByID(ctx, blogID, id) if err != nil { return err } all, err := s.ListModules(ctx, blogID) if err != nil { return err } var mods []Module idx := -1 for _, m := range all { if m.Area != cur.Area { continue } if m.ID == id { idx = len(mods) } mods = append(mods, m) } j := idx + dir if idx < 0 || j < 0 || j >= len(mods) { return nil } mods[idx], mods[j] = mods[j], mods[idx] tx, err := s.db.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) for i, m := range mods { // renumber the area; keeps orders dense if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order=$3 WHERE blog_id=$1 AND id=$2`, blogID, m.ID, i); err != nil { return err } } return tx.Commit(ctx) } // ResetModules puts the layout back to what a new blog starts with. func (s *Store) ResetModules(ctx context.Context, blogID int64) error { tx, err := s.db.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) if _, err := tx.Exec(ctx, `DELETE FROM modules WHERE blog_id=$1`, blogID); err != nil { return err } if err := insertDefaultModules(ctx, tx, blogID); err != nil { return err } return tx.Commit(ctx) } // insertDefaultModules is the starting layout: title and menu in the header, // an RSS link in the footer. Must match what the layout migration gave old blogs. func insertDefaultModules(ctx context.Context, tx querier, blogID int64) error { _, err := tx.Exec(ctx, `INSERT INTO modules (blog_id, area, kind, sort_order) VALUES ($1,'header','title',0), ($1,'header','menu',1), ($1,'footer','rss',0)`, blogID) return err }