item

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package item contains the business model and application service for items.

The package deliberately has no knowledge of HTTP, SQL, or any other delivery/persistence mechanism. Those concerns are adapters around the Store interface defined in service.go.

Index

Constants

View Source
const (
	// DefaultCursorTTL bounds how long a cursor remains usable when callers do
	// not provide an explicit retention policy.  Cursors are stateless, so a
	// bounded lifetime also limits how long an old ordering contract remains
	// valid after a deployment.
	DefaultCursorTTL = 24 * time.Hour

	// MaxCursorTTL prevents an accidental configuration value from turning a
	// cursor into an effectively permanent bearer token.  A zero TTL is allowed
	// and explicitly means that the codec does not expire tokens.
	MaxCursorTTL = 30 * 24 * time.Hour

	// MinCursorSigningKeyBytes is the minimum secret size accepted by the
	// signed codec.  HMAC itself accepts shorter values, but accepting a weak
	// deployment secret would undermine the integrity guarantee.
	MinCursorSigningKeyBytes = 32

	// MaxCursorBytes bounds both parsing work and the size of a value that a
	// caller can place in a URL.  The fixed v1 wire format is considerably
	// smaller; the larger bound leaves room for a future version while keeping
	// malformed requests cheap to reject.
	MaxCursorBytes = 512
)
View Source
const (
	// MaxIdempotencyKeyBytes bounds the client supplied HTTP token. Keeping the
	// value small makes validation, hashing, and transport headers predictable.
	MaxIdempotencyKeyBytes = 255

	// IdempotencyRecordRetention is the replay window for a completed create.
	// Adapters may remove records after this duration; retries outside the window
	// are treated as a new request.
	IdempotencyRecordRetention = 24 * time.Hour

	// MaxIdempotencyEntries is the hard resident-entry bound for adapters that
	// keep idempotency records in process memory. Durable adapters should enforce
	// an equivalent operational retention/cleanup policy.
	MaxIdempotencyEntries = 10_000
)
View Source
const (
	// DefaultPageSize is used when a list request does not specify a limit.
	DefaultPageSize = 20
	// MaxPageSize is the largest page a caller may request.
	MaxPageSize = 100

	// MinNameLength is the minimum number of Unicode characters in a name.
	MinNameLength = 1
	// MaxNameLength is the maximum number of Unicode characters in a name.
	MaxNameLength = 120
	// MaxDescriptionLength is the maximum number of Unicode characters in a description.
	MaxDescriptionLength = 2000
)

Variables

View Source
var (
	// ErrInvalidCursor means that a supplied cursor is malformed, expired, or
	// was not authenticated by the configured codec.  The error deliberately
	// does not include the supplied token.
	ErrInvalidCursor = errors.New("item: invalid cursor")

	// ErrCursorUnavailable means that a caller requested cursor pagination but
	// the configured Store does not implement the cohesive cursor capability or
	// the adapter cannot currently provide it.
	ErrCursorUnavailable = errors.New("item: cursor store unavailable")

	// ErrCursorState means that an adapter returned a value from which a safe
	// continuation position cannot be produced.  It is kept distinct from a
	// caller's malformed cursor so transports can fail closed with 503.
	ErrCursorState = errors.New("item: invalid cursor state")
)
View Source
var (
	ErrInvalidInput     = errors.New("item: invalid input")
	ErrNotFound         = errors.New("item: not found")
	ErrConflict         = errors.New("item: conflict")
	ErrStoreUnavailable = errors.New("item: store unavailable")
	ErrIDGeneration     = errors.New("item: generate id")
)

Sentinel errors are stable across adapters. Callers should use errors.Is/errors.As instead of matching error strings.

View Source
var (
	// ErrIdempotencyConflict means a key was already used with another request
	// fingerprint. The original response is never overwritten.
	ErrIdempotencyConflict = errors.New("item: idempotency key conflict")
	// ErrIdempotencyInProgress means another request currently owns the key.
	ErrIdempotencyInProgress = errors.New("item: idempotency request in progress")
	// ErrIdempotencyUnavailable means the caller requested idempotency but the
	// composition root has no cohesive atomic idempotency implementation, or the
	// implementation cannot accept another bounded record.
	ErrIdempotencyUnavailable = errors.New("item: idempotency store unavailable")
	// ErrIdempotencyState means a durable reservation references invalid state.
	ErrIdempotencyState = errors.New("item: invalid idempotency state")
)

Functions

func FingerprintCreateInput

func FingerprintCreateInput(input CreateInput) (string, error)

FingerprintCreateInput returns a stable SHA-256 fingerprint of canonical create fields. Equivalent edge padding therefore replays safely, while a changed name or description conflicts.

func IdempotencyScopeFromContext

func IdempotencyScopeFromContext(ctx context.Context) string

IdempotencyScopeFromContext returns the configured scope or the stable default used by direct (non-HTTP) callers.

func ValidateIdempotencyKey

func ValidateIdempotencyKey(key string) error

ValidateIdempotencyKey enforces an HTTP-token-shaped key. Restricting the alphabet avoids ambiguity from folded/quoted header values and prevents control characters from reaching logs or response headers.

func WithIdempotencyScope

func WithIdempotencyScope(ctx context.Context, scope string) context.Context

WithIdempotencyScope attaches a bounded caller scope to ctx. HTTP adapters should set this to a route plus an authenticated principal (or a trusted direct peer identity) so two clients cannot replay one another's response. The scope is hashed before it reaches an adapter; it is never persisted.

Types

type Clock

type Clock func() time.Time

Clock supplies creation times. Production uses time.Now; tests can inject a fixed clock without changing domain code.

type CreateInput

type CreateInput struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

CreateInput contains client-controlled fields for creating an Item. Identity and creation time are intentionally not accepted from callers.

func ValidateCreateInput

func ValidateCreateInput(input CreateInput) (CreateInput, error)

ValidateCreateInput validates and canonicalises client input. Name and Description are trimmed at the edges; internal whitespace and Unicode are preserved. A copy is returned so callers' input is never mutated.

type CursorCodec

type CursorCodec interface {
	Encode(position CursorPosition) (string, error)
	Decode(token string) (CursorPosition, error)
}

CursorCodec transports a validated position as an opaque token. The built-in implementation is signed; applications may provide another codec only when it preserves the same validation and expiry guarantees.

type CursorCodecOption

type CursorCodecOption func(*SignedCursorCodec)

CursorCodecOption configures SignedCursorCodec.

func WithCursorClock

func WithCursorClock(clock Clock) CursorCodecOption

WithCursorClock injects the clock used for token issuance and validation. It is intended for deterministic tests. A nil value restores time.Now.

func WithCursorPurpose

func WithCursorPurpose(purpose string) CursorCodecOption

WithCursorPurpose domain-separates tokens issued for different collection contracts. A token from one resource or sort order cannot be replayed at a codec configured for another purpose. Purpose is not stored in the token; it is covered by the MAC.

func WithCursorTTL

func WithCursorTTL(ttl time.Duration) CursorCodecOption

WithCursorTTL sets token retention. Zero disables expiry; positive values must not exceed MaxCursorTTL. Invalid values are reported by the constructor after all options are applied.

type CursorError

type CursorError struct {
	Reason string
}

CursorError identifies a cursor decoding/validation failure without retaining or echoing the untrusted token. It unwraps to ErrInvalidCursor so transports can map all malformed, forged, and expired cursors together.

func (*CursorError) Error

func (e *CursorError) Error() string

func (*CursorError) Unwrap

func (e *CursorError) Unwrap() error

type CursorListParams

type CursorListParams struct {
	Limit int
	After *CursorPosition
}

CursorListParams controls a keyset list operation. A nil After requests the first page. Service normalizes Limit and asks the Store for one extra row to calculate HasMore; adapters must return deterministic newest-first order and honor the supplied context.

type CursorLister

type CursorLister interface {
	ListCursor(ctx context.Context, request CursorRequest) (CursorPage, error)
}

CursorLister is the optional application capability consumed by transport adapters. Keeping it separate from Service's original List method lets existing offset callers and small fakes continue compiling while clients migrate to cursors. The alias name is retained for readability at HTTP seams.

type CursorPage

type CursorPage struct {
	Items      []Item
	Limit      int
	HasMore    bool
	NextCursor string
}

CursorPage is the transport-neutral result of a keyset list operation. The Service encodes the continuation position, so outer adapters only need to copy NextCursor to their response envelope. NextCursor is empty when HasMore is false or the page contains no rows.

type CursorPosition

type CursorPosition struct {
	CreatedAt time.Time
	ID        uuid.UUID
}

CursorPosition is the last Item position included in a page. Item pages are ordered by CreatedAt descending and ID descending; a subsequent page therefore selects rows strictly after this position in that descending order (created_at,id) < (position.created_at,position.id).

CreatedAt and ID are immutable Item fields. Callers should obtain a position from CursorPositionForItem or from CursorCodec.Decode rather than constructing one from arbitrary transport data.

func CursorPositionForItem

func CursorPositionForItem(value Item) (CursorPosition, error)

CursorPositionForItem derives a validated continuation position from an Item. It is useful to an HTTP adapter when it wants to expose a cursor for a first page produced by the legacy offset path.

func (CursorPosition) Validate

func (position CursorPosition) Validate() error

Validate checks the invariant needed by every cursor store. Time zones and monotonic clock readings are normalized by the codec; validation itself does not reject a legitimate pre-epoch instant.

type CursorRequest

type CursorRequest struct {
	Limit  int
	Cursor string
}

CursorRequest is the transport-neutral request for a keyset list operation. Cursor is the raw opaque token received from a transport; the Service decodes and authenticates it before it reaches a Store. An empty Cursor requests the first page.

type CursorService

type CursorService = CursorLister

CursorService is a descriptive alias for callers that prefer to make the optional application capability explicit.

type CursorStore

type CursorStore interface {
	ListAfter(ctx context.Context, params CursorListParams) ([]Item, error)
}

CursorStore is the optional persistence capability for keyset pagination. It must be implemented by the same concrete Store that persists Items; a Service must never combine an Item Store with a separate cursor backend. Implementations return at most params.Limit rows and apply the strict (created_at,id) boundary when After is non-nil.

type IDGenerator

type IDGenerator func() (uuid.UUID, error)

IDGenerator allows tests and applications that need deterministic identity generation to provide their own source. Production uses uuid.NewRandom.

type IdempotencyStore

type IdempotencyStore = IdempotentCreateStore

IdempotencyStore is retained as a descriptive alias for callers that prefer the shorter name. It deliberately represents the atomic seam, not a multi-step reservation protocol.

type IdempotentCreateStore

type IdempotentCreateStore interface {
	CreateIdempotent(ctx context.Context, value Item, key, fingerprint string) (created Item, replayed bool, err error)
}

IdempotentCreateStore is the persistence seam for an idempotent create. The operation must make the Item insert and its idempotency record one atomic unit. It returns replayed=true only when the stored response is replayed. Implementations must hash key/fingerprint before durable storage, bound memory/retention, serialize the same key across replicas, and honor ctx.

type IdempotentCreator

type IdempotentCreator interface {
	CreateIdempotent(ctx context.Context, input CreateInput, key string) (value Item, replayed bool, err error)
}

IdempotentCreator is the optional service capability consumed by the HTTP adapter when an Idempotency-Key header is present. replayed is true only for an earlier successful response.

type Item

type Item struct {
	ID          uuid.UUID `json:"id"`
	Name        string    `json:"name"`
	Description string    `json:"description"`
	CreatedAt   time.Time `json:"created_at"`
}

Item is the business representation persisted by a Store.

ID and CreatedAt are assigned by Service.Create. CreatedAt is always UTC for items produced by the service. The memory adapter also preserves the value exactly as supplied by a caller so that persistence adapters can own their usual round-trip semantics.

type ListParams

type ListParams struct {
	Limit  int `json:"limit"`
	Offset int `json:"offset"`
}

ListParams controls a paginated list operation. A zero Limit means DefaultPageSize. Negative values and values greater than MaxPageSize are invalid at the service boundary. Offset is zero based.

func NormalizeListParams

func NormalizeListParams(params ListParams) (ListParams, error)

NormalizeListParams validates and fills defaults for a list request. Limits above MaxPageSize are rejected rather than silently changed, making an accidentally expensive request visible to callers.

type Option

type Option func(*Service)

Option configures a Service. Options are intentionally small: adapters should not be able to alter validation or pagination policy.

func WithClock

func WithClock(clock Clock) Option

WithClock overrides the source used for CreatedAt.

func WithCursorCodec

func WithCursorCodec(codec CursorCodec) Option

WithCursorCodec enables the optional cursor-list capability. The codec is deliberately injected at the application seam so HTTP adapters never need to hold or parse a signing secret. A nil codec leaves cursor pagination unavailable while preserving the legacy offset API.

func WithIDGenerator

func WithIDGenerator(generator IDGenerator) Option

WithIDGenerator overrides the UUID source used by Create.

type Page

type Page struct {
	Items      []Item `json:"items"`
	Limit      int    `json:"limit"`
	Offset     int    `json:"offset"`
	HasMore    bool   `json:"has_more"`
	NextCursor string `json:"next_cursor,omitempty"`
}

Page is the stable, transport-neutral result of a list operation.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service owns Item use cases and domain policy. It is safe for concurrent use as long as its Store is safe for concurrent use (the memory adapter is).

func NewService

func NewService(store Store, options ...Option) *Service

NewService constructs an Item service. A nil Store is accepted so that configuration errors can be reported as ErrStoreUnavailable at call time rather than causing a process-start panic.

func (*Service) Create

func (service *Service) Create(ctx context.Context, input CreateInput) (Item, error)

Create validates input, assigns a UUIDv4 and UTC creation timestamp, and persists the resulting item.

func (*Service) CreateIdempotent

func (service *Service) CreateIdempotent(ctx context.Context, input CreateInput, key string) (Item, bool, error)

CreateIdempotent validates and canonicalises input, then delegates one atomic persistence operation. A service only advertises idempotency when its Store itself implements IdempotentCreateStore; this prevents split-brain item/idempotency backends.

func (*Service) Get

func (service *Service) Get(ctx context.Context, id uuid.UUID) (Item, error)

Get retrieves one item by UUID.

func (*Service) List

func (service *Service) List(ctx context.Context, params ListParams) (Page, error)

List returns a deterministic page. The service asks the Store for one additional row so HasMore remains correct when the result count equals the requested page size.

func (*Service) ListCursor

func (service *Service) ListCursor(ctx context.Context, request CursorRequest) (CursorPage, error)

ListCursor executes one keyset page through the same concrete Store used by ordinary Item operations. It deliberately remains separate from List so existing offset callers and fakes stay source-compatible during migration. The configured codec is the only component that sees the raw token; stores receive a validated CursorPosition and never parse transport data.

type SignedCursorCodec

type SignedCursorCodec struct {
	// contains filtered or unexported fields
}

SignedCursorCodec is a stateless, URL-safe, authenticated cursor codec. The secret is copied into a fixed-size HMAC key and is never exposed after construction. Tokens contain only a version, ordering position, and an optional expiry; they do not contain names, descriptions, caller identity, or raw query data.

func NewCursorCodec

func NewCursorCodec(secret []byte, options ...CursorCodecOption) (*SignedCursorCodec, error)

NewCursorCodec is the concise constructor used by composition roots.

func NewSignedCursorCodec

func NewSignedCursorCodec(secret []byte, options ...CursorCodecOption) (*SignedCursorCodec, error)

NewSignedCursorCodec constructs the v1 HMAC-SHA256 codec. A minimum 256-bit secret is required. The supplied bytes are hashed once to obtain a fixed-size key, so callers may safely pass a high-entropy encoded secret.

func (*SignedCursorCodec) Decode

func (codec *SignedCursorCodec) Decode(token string) (CursorPosition, error)

Decode authenticates and validates a token. All malformed, forged, and expired values unwrap to ErrInvalidCursor; the token itself is never put in the returned error.

func (*SignedCursorCodec) Encode

func (codec *SignedCursorCodec) Encode(position CursorPosition) (string, error)

Encode signs a position and returns a raw-base64url token. It never emits padding, which keeps the token safe in query strings without additional escaping.

type Store

type Store interface {
	Create(ctx context.Context, value Item) (Item, error)
	Get(ctx context.Context, id uuid.UUID) (Item, error)
	List(ctx context.Context, params ListParams) ([]Item, error)
}

Store is the persistence seam used by Service. Implementations must honor the supplied context and return list results in deterministic order: CreatedAt descending, then ID descending. List returns at most params.Limit rows; Service requests one extra row to calculate Page.HasMore. Stores may additionally implement CursorStore on the same concrete value to enable keyset pagination.

type ValidationError

type ValidationError struct {
	Field  string
	Reason string
}

ValidationError identifies a request field that violated a domain rule. It unwraps to ErrInvalidInput so transport adapters can map all validation failures consistently while still exposing a useful field/message locally.

func (*ValidationError) Error

func (e *ValidationError) Error() string

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() error

Directories

Path Synopsis
Package httpapi exposes the HTTP adapter for the item feature.
Package httpapi exposes the HTTP adapter for the item feature.
Package memory provides a race-safe in-memory item Store.
Package memory provides a race-safe in-memory item Store.
Package postgres persists items in PostgreSQL using pgx.
Package postgres persists items in PostgreSQL using pgx.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL