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