Files
gibbz/backend/internal/items/item.go
T
2026-09-22 08:41:20 +02:00

90 lines
2.8 KiB
Go

// Package items is the reference vertical slice: model, repository
// interface, Bun implementation, and service. To add a new resource, copy
// this package's shape and register its routes in internal/api.
package items
import (
"context"
"errors"
"time"
"github.com/uptrace/bun"
)
// Status is the lifecycle state of an item, stored as text.
type Status string
const (
StatusActive Status = "active"
StatusArchived Status = "archived"
)
// Valid reports whether s is a known status value.
func (s Status) Valid() bool {
return s == StatusActive || s == StatusArchived
}
// Item is the persisted model. Struct tags are Bun's mapping; json tags
// define the wire format (camelCase, matching the frontend).
type Item struct {
bun.BaseModel `bun:"table:items,alias:i"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
Title string `bun:"title,notnull" json:"title"`
Description string `bun:"description,nullzero" json:"description"`
Status Status `bun:"status,notnull,default:'active'" json:"status"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updatedAt"`
}
// BeforeAppendModel stamps timestamps on INSERT and UPDATE queries.
// Postgres stores microsecond UTC; truncate + UTC so values set in Go
// (PATCH responses) match what round-trips through the database.
func (i *Item) BeforeAppendModel(_ context.Context, query bun.Query) error {
now := time.Now().UTC().Truncate(time.Microsecond)
switch query.(type) {
case *bun.InsertQuery:
i.CreatedAt, i.UpdatedAt = now, now
case *bun.UpdateQuery:
i.UpdatedAt = now
}
return nil
}
// Filter narrows and pages List queries. Apply normalizeFilter (via the
// service) before handing one to a repository.
type Filter struct {
Status Status
Limit int
Offset int
}
// List result paging defaults, applied by normalizeFilter.
const (
DefaultLimit = 50
MaxLimit = 100
)
// Repository is the persistence contract for items. The service depends on
// this interface — not on Bun — so tests use in-memory fakes and other
// backends remain possible.
type Repository interface {
List(ctx context.Context, f Filter) ([]Item, error)
Get(ctx context.Context, id int64) (Item, error)
Create(ctx context.Context, item *Item) error
Update(ctx context.Context, item *Item) error
Delete(ctx context.Context, id int64) error
}
// ErrNotFound is returned by repositories and services when an item does
// not exist. Handlers map it to 404.
var ErrNotFound = errors.New("item not found")
// ValidationError reports an invalid field value. Handlers map it to 400.
type ValidationError struct {
Field string
Reason string
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Reason }