90 lines
2.8 KiB
Go
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 }
|