package gifts import ( "context" "database/sql" "errors" "fmt" ) type Repository struct { db *sql.DB } func NewRepository(db *sql.DB) *Repository { return &Repository{db: db} } func (r *Repository) GetPublicBySlug(ctx context.Context, slug string) (PublicGift, error) { var gift PublicGift err := r.db.QueryRowContext(ctx, ` SELECT id, slug, recipient_name, sender_name, title, intro_message, reveal_message FROM gifts WHERE slug = $1 AND status = 'published' `, slug).Scan( &gift.ID, &gift.Slug, &gift.RecipientName, &gift.SenderName, &gift.Title, &gift.IntroMessage, &gift.RevealMessage, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return PublicGift{}, ErrNotFound } return PublicGift{}, fmt.Errorf("find gift: %w", err) } rows, err := r.db.QueryContext(ctx, ` SELECT id, type, title, text, media_url, sort_order, metadata FROM gift_items WHERE gift_id = $1 ORDER BY sort_order ASC, id ASC `, gift.ID) if err != nil { return PublicGift{}, fmt.Errorf("find gift items: %w", err) } defer rows.Close() gift.Items = make([]PublicGiftItem, 0) for rows.Next() { var ( item PublicGiftItem title sql.NullString text sql.NullString mediaURL sql.NullString metadata []byte ) if err := rows.Scan( &item.ID, &item.Type, &title, &text, &mediaURL, &item.SortOrder, &metadata, ); err != nil { return PublicGift{}, fmt.Errorf("scan gift item: %w", err) } item.Title = title.String item.Text = text.String item.MediaURL = mediaURL.String if len(metadata) == 0 { item.Metadata = []byte(`{}`) } else { item.Metadata = metadata } gift.Items = append(gift.Items, item) } if err := rows.Err(); err != nil { return PublicGift{}, fmt.Errorf("iterate gift items: %w", err) } return gift, nil } // Store is the read contract used by the service. Keeping it small makes the // HTTP layer straightforward to test without a running database. type Store interface { GetPublicBySlug(context.Context, string) (PublicGift, error) } var _ Store = (*Repository)(nil)