Some checks failed
Deploy to NAS / deploy (push) Failing after 26s
- Public feed (/) with infinite scroll via Intersection Observer - Self-registration (/register) - Admin area (/admin/entries, /admin/users) with user management - journal_entries: visibility (public/private) + hashtags fields - users: is_admin flag - DB schema updated (recreate DB to apply) - CI: run go test via docker run (golang:1.25-alpine) — fixes 'go not found' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
162 lines
4.9 KiB
Go
162 lines
4.9 KiB
Go
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
|
|
}
|