go-places/api/internal/store/store.go

117 lines
5 KiB
Go
Raw Normal View History

2026-10-07 15:14:50 +02:00
// Package store defines persistence for the domain entities. The interface
// exists so that the query implementation can be swapped (a different SQL
// dialect, or an in-memory fake in tests) without touching the service layer.
package store
import (
"context"
"errors"
"go-places-api/internal/domain"
"go-places-api/internal/geo"
)
var (
// ErrNotFound is returned when a lookup matches no row.
ErrNotFound = errors.New("not found")
// ErrConflict is returned when a unique constraint would be violated.
ErrConflict = errors.New("conflict")
)
// ImageFilter narrows a picture search.
type ImageFilter struct {
// PlaceID restricts results to one place when non-empty.
PlaceID string
// CreatorID restricts results to one author when non-empty.
CreatorID string
// Within restricts results to pictures inside this box and radius. A nil
// Coordinate disables the geographic restriction.
Within *NearbyQuery
// Limit caps the number of rows. Zero means unlimited.
Limit int
// Offset skips rows for pagination.
Offset int
}
// NearbyQuery selects entities within a radius of a point.
type NearbyQuery struct {
Center domain.Coordinate
RadiusMeters float64
}
// Store is the persistence contract required by the service layer.
type Store interface {
// CreatePlace inserts a place and returns it as persisted.
CreatePlace(ctx context.Context, p domain.Place) (domain.Place, error)
// GetPlace loads a place, its pictures, comments and author.
GetPlace(ctx context.Context, id string) (domain.Place, error)
// UpdatePlace replaces the mutable fields of a place.
UpdatePlace(ctx context.Context, p domain.Place) (domain.Place, error)
// ListPlaces returns places newest first.
ListPlaces(ctx context.Context, limit, offset int) ([]domain.Place, error)
// DeletePlace removes a place and everything attached to it.
DeletePlace(ctx context.Context, id string) error
// CreateImage inserts a picture record.
CreateImage(ctx context.Context, img domain.Image) (domain.Image, error)
// GetImage loads one picture.
GetImage(ctx context.Context, id string) (domain.Image, error)
// GetImageByStorageKey loads the picture a blob key belongs to. The media
// route needs it to learn the declared content type and size of a stored
// object without trusting the file's own bytes.
GetImageByStorageKey(ctx context.Context, key string) (domain.Image, error)
// UpdateImage replaces the mutable fields of a picture.
UpdateImage(ctx context.Context, img domain.Image) (domain.Image, error)
// ListImages returns pictures matching the filter, newest first.
ListImages(ctx context.Context, f ImageFilter) ([]domain.Image, error)
// ImagesWithin returns pictures whose capture point lies inside the radius.
ImagesWithin(ctx context.Context, q NearbyQuery) ([]domain.Image, error)
// ReplaceBadges swaps the badge set for a picture.
ReplaceBadges(ctx context.Context, imageID string, badges []domain.Badge) error
// CreateImageLink records that two pictures were taken at the same place.
// Re-recording the same pair updates the measured distance.
CreateImageLink(ctx context.Context, imageID, relatedImageID string, distanceMeters float64, reason string) error
// ListImageLinks returns the recorded links for a picture in both
// directions, so a link is visible from either end.
ListImageLinks(ctx context.Context, imageID string) ([]domain.Link, error)
// SetAnalysisState records the outcome of the analysis pipeline.
SetAnalysisState(ctx context.Context, imageID string, status domain.AnalysisStatus, failure string) error
// DeleteImage removes a picture and its badges.
DeleteImage(ctx context.Context, id string) error
// AddComment inserts a comment and returns it as persisted.
AddComment(ctx context.Context, c domain.Comment) (domain.Comment, error)
// ListComments returns the comments on a place, oldest first.
ListComments(ctx context.Context, placeID string) ([]domain.Comment, error)
// UpsertUser stores the identity asserted by the auth layer.
UpsertUser(ctx context.Context, u domain.User) error
// GetUser loads a user by identifier.
GetUser(ctx context.Context, id string) (domain.User, error)
// Close releases resources held by the store.
Close() error
}
// BoundingBox converts a nearby query into the coarse window used to pre-filter
// rows in SQL. Exposed here so implementations and tests agree on the filter.
func (q NearbyQuery) BoundingBox() geo.BoundingBox {
return geo.Around(q.Center, q.RadiusMeters)
}
// ErrInvalidInput is returned when a write would violate a domain rule, such as
// linking a picture to itself.
var ErrInvalidInput = errors.New("invalid input")
// ImageGetter is the optional extra surface the service uses for storage-key
// lookups. It is separate from Store so an implementation that cannot map a key
// back to a record simply does not satisfy it.
type ImageGetter interface {
GetImageByStorageKey(ctx context.Context, key string) (domain.Image, error)
}
// Pinger is the optional extra surface used by the readiness probe.
type Pinger interface {
Ping(ctx context.Context) error
}