feat(cli): add CLI for managing RSVPs and photo uploads

* implement runCLI function to handle subcommands
* add runRSVPCLI and runPhotosCLI for RSVP and photo management
* include usage instructions for CLI commands
* integrate CLI execution in main function
This commit is contained in:
2026-08-23 11:45:48 +02:00
parent 79e4c11a16
commit f96465b798
4 changed files with 347 additions and 4 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ func (m *Mailer) send(subject, body string) error {
)
recipients := []string{m.cfg.To}
for _, bcc := range strings.Split(m.cfg.Bcc, ",") {
for bcc := range strings.SplitSeq(m.cfg.Bcc, ",") {
if bcc = strings.TrimSpace(bcc); bcc != "" {
recipients = append(recipients, bcc)
}
+162
View File
@@ -102,6 +102,76 @@ func (s *Store) ListGuests() ([]Guest, error) {
return guests, rows.Err()
}
type RSVPRecord struct {
ID int64
Message string
CreatedAt time.Time
Guests []Guest
}
// ListRSVPs returns every RSVP submission (not flattened per-guest), oldest first.
func (s *Store) ListRSVPs() ([]RSVPRecord, error) {
rows, err := s.db.Query(
`SELECT r.id, r.message, r.created_at, g.name, g.is_child
FROM rsvps r
LEFT JOIN rsvp_guests g ON g.rsvp_id = r.id
ORDER BY r.created_at ASC, r.id ASC, g.id ASC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []RSVPRecord
index := map[int64]int{}
for rows.Next() {
var id int64
var message string
var createdAt time.Time
var name sql.NullString
var isChild sql.NullBool
if err := rows.Scan(&id, &message, &createdAt, &name, &isChild); err != nil {
return nil, err
}
i, ok := index[id]
if !ok {
out = append(out, RSVPRecord{ID: id, Message: message, CreatedAt: createdAt})
i = len(out) - 1
index[id] = i
}
if name.Valid {
out[i].Guests = append(out[i].Guests, Guest{Name: name.String, IsChild: isChild.Bool})
}
}
return out, rows.Err()
}
// DeleteRSVP removes an RSVP submission and its guests.
func (s *Store) DeleteRSVP(id int64) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`DELETE FROM rsvp_guests WHERE rsvp_id = ?`, id); err != nil {
return err
}
res, err := tx.Exec(`DELETE FROM rsvps WHERE id = ?`, id)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return fmt.Errorf("rsvp %d not found", id)
}
return tx.Commit()
}
func (s *Store) InsertRSVP(r RSVP) error {
tx, err := s.db.Begin()
if err != nil {
@@ -189,6 +259,98 @@ func (s *Store) ListPhotoFiles() ([]PhotoFileRecord, error) {
return out, rows.Err()
}
type PhotoUploadRecord struct {
ID int64
Name string
Message string
CreatedAt time.Time
Files []PhotoFile
}
// ListPhotoUploads returns every upload submission (not flattened per-file), oldest first.
func (s *Store) ListPhotoUploads() ([]PhotoUploadRecord, error) {
rows, err := s.db.Query(
`SELECT u.id, u.name, u.message, u.created_at, f.filename, f.path, f.url
FROM photo_uploads u
LEFT JOIN photo_files f ON f.upload_id = u.id
ORDER BY u.created_at ASC, u.id ASC, f.id ASC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []PhotoUploadRecord
index := map[int64]int{}
for rows.Next() {
var id int64
var name, message string
var createdAt time.Time
var filename, path, url sql.NullString
if err := rows.Scan(&id, &name, &message, &createdAt, &filename, &path, &url); err != nil {
return nil, err
}
i, ok := index[id]
if !ok {
out = append(out, PhotoUploadRecord{ID: id, Name: name, Message: message, CreatedAt: createdAt})
i = len(out) - 1
index[id] = i
}
if filename.Valid {
out[i].Files = append(out[i].Files, PhotoFile{Filename: filename.String, Path: path.String, URL: url.String})
}
}
return out, rows.Err()
}
// DeletePhotoUpload removes an upload's DB rows and returns the files it had,
// so the caller can also remove them from disk.
func (s *Store) DeletePhotoUpload(id int64) ([]PhotoFile, error) {
tx, err := s.db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
rows, err := tx.Query(`SELECT filename, path, url FROM photo_files WHERE upload_id = ?`, id)
if err != nil {
return nil, err
}
var files []PhotoFile
for rows.Next() {
var f PhotoFile
if err := rows.Scan(&f.Filename, &f.Path, &f.URL); err != nil {
rows.Close()
return nil, err
}
files = append(files, f)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
if _, err := tx.Exec(`DELETE FROM photo_files WHERE upload_id = ?`, id); err != nil {
return nil, err
}
res, err := tx.Exec(`DELETE FROM photo_uploads WHERE id = ?`, id)
if err != nil {
return nil, err
}
n, err := res.RowsAffected()
if err != nil {
return nil, err
}
if n == 0 {
return nil, fmt.Errorf("photo upload %d not found", id)
}
if err := tx.Commit(); err != nil {
return nil, err
}
return files, nil
}
func (s *Store) AddPhotoFiles(uploadID int64, files []PhotoFile) error {
tx, err := s.db.Begin()
if err != nil {