package db import ( "context" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/jacek/pamietnik/backend/internal/domain" ) type JournalStore struct { pool *pgxpool.Pool } func NewJournalStore(pool *pgxpool.Pool) *JournalStore { return &JournalStore{pool: pool} } // InsertEntry creates a new journal entry and returns it with the generated entry_id. func (s *JournalStore) InsertEntry(ctx context.Context, e domain.JournalEntry) (domain.JournalEntry, error) { if e.Visibility == "" { e.Visibility = "private" } if e.Hashtags == nil { e.Hashtags = []string{} } err := s.pool.QueryRow(ctx, `INSERT INTO journal_entries (user_id, entry_date, entry_time, title, description, lat, lon, visibility, hashtags) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING entry_id, created_at`, e.UserID, e.EntryDate, e.EntryTime, e.Title, e.Description, e.Lat, e.Lon, e.Visibility, e.Hashtags, ).Scan(&e.EntryID, &e.CreatedAt) return e, err } // InsertImage attaches an image record to an entry. func (s *JournalStore) InsertImage(ctx context.Context, img domain.JournalImage) (domain.JournalImage, error) { err := s.pool.QueryRow(ctx, `INSERT INTO journal_images (entry_id, filename, original_name, mime_type, size_bytes) VALUES ($1, $2, $3, $4, $5) RETURNING image_id, created_at`, img.EntryID, img.Filename, img.OriginalName, img.MimeType, img.SizeBytes, ).Scan(&img.ImageID, &img.CreatedAt) return img, err } // ListByDate returns all journal entries for a given date (YYYY-MM-DD), including their images. func (s *JournalStore) ListByDate(ctx context.Context, userID, date string) ([]domain.JournalEntry, error) { rows, err := s.pool.Query(ctx, `SELECT entry_id, user_id, entry_date::text, entry_time::text, title, description, lat, lon, visibility, hashtags, created_at FROM journal_entries WHERE user_id = $1 AND entry_date = $2 ORDER BY entry_time`, userID, date, ) if err != nil { return nil, err } defer rows.Close() entries, err := collectEntries(rows) if err != nil { return nil, err } return s.attachImages(ctx, entries) } // ListPublic returns public journal entries ordered by created_at DESC, for infinite scroll. func (s *JournalStore) ListPublic(ctx context.Context, limit, offset int) ([]domain.JournalEntry, error) { rows, err := s.pool.Query(ctx, `SELECT entry_id, user_id, entry_date::text, entry_time::text, title, description, lat, lon, visibility, hashtags, created_at FROM journal_entries WHERE visibility = 'public' ORDER BY created_at DESC LIMIT $1 OFFSET $2`, limit, offset, ) if err != nil { return nil, err } defer rows.Close() entries, err := collectEntries(rows) if err != nil { return nil, err } return s.attachImages(ctx, entries) } // ListByUser returns all entries for a user, ordered by entry_date DESC, entry_time DESC. func (s *JournalStore) ListByUser(ctx context.Context, userID string) ([]domain.JournalEntry, error) { rows, err := s.pool.Query(ctx, `SELECT entry_id, user_id, entry_date::text, entry_time::text, title, description, lat, lon, visibility, hashtags, created_at FROM journal_entries WHERE user_id = $1 ORDER BY entry_date DESC, entry_time DESC`, userID, ) if err != nil { return nil, err } defer rows.Close() entries, err := collectEntries(rows) if err != nil { return nil, err } return s.attachImages(ctx, entries) } func collectEntries(rows pgx.Rows) ([]domain.JournalEntry, error) { var entries []domain.JournalEntry for rows.Next() { var e domain.JournalEntry if err := rows.Scan( &e.EntryID, &e.UserID, &e.EntryDate, &e.EntryTime, &e.Title, &e.Description, &e.Lat, &e.Lon, &e.Visibility, &e.Hashtags, &e.CreatedAt, ); err != nil { return nil, err } entries = append(entries, e) } return entries, rows.Err() } // attachImages loads images for the given entries in a single query and populates .Images. func (s *JournalStore) attachImages(ctx context.Context, entries []domain.JournalEntry) ([]domain.JournalEntry, error) { if len(entries) == 0 { return entries, nil } entryIDs := make([]string, len(entries)) for i, e := range entries { entryIDs[i] = e.EntryID } imgRows, err := s.pool.Query(ctx, `SELECT image_id, entry_id, filename, original_name, mime_type, size_bytes, created_at FROM journal_images WHERE entry_id = ANY($1) ORDER BY created_at`, entryIDs, ) if err != nil { return nil, err } defer imgRows.Close() imgMap := make(map[string][]domain.JournalImage) for imgRows.Next() { var img domain.JournalImage if err := imgRows.Scan( &img.ImageID, &img.EntryID, &img.Filename, &img.OriginalName, &img.MimeType, &img.SizeBytes, &img.CreatedAt, ); err != nil { return nil, err } imgMap[img.EntryID] = append(imgMap[img.EntryID], img) } if err := imgRows.Err(); err != nil { return nil, err } for i, e := range entries { entries[i].Images = imgMap[e.EntryID] } return entries, nil }