// 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 }