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
- Variables
- func FingerprintCreateInput(input CreateInput) (string, error)
- func IdempotencyScopeFromContext(ctx context.Context) string
- func ValidateIdempotencyKey(key string) error
- func WithIdempotencyScope(ctx context.Context, scope string) context.Context
- type Clock
- type CreateInput
- type CursorCodec
- type CursorCodecOption
- type CursorError
- type CursorListParams
- type CursorLister
- type CursorPage
- type CursorPosition
- type CursorRequest
- type CursorService
- type CursorStore
- type IDGenerator
- type IdempotencyStore
- type IdempotentCreateStore
- type IdempotentCreator
- type Item
- type ListParams
- type Option
- type Page
- type Service
- func (service *Service) Create(ctx context.Context, input CreateInput) (Item, error)
- func (service *Service) CreateIdempotent(ctx context.Context, input CreateInput, key string) (Item, bool, error)
- func (service *Service) Get(ctx context.Context, id uuid.UUID) (Item, error)
- func (service *Service) List(ctx context.Context, params ListParams) (Page, error)
- func (service *Service) ListCursor(ctx context.Context, request CursorRequest) (CursorPage, error)
- type SignedCursorCodec
- type Store
- type ValidationError
Constants ¶
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 )
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 )
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 ¶
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") // 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") )
var ( ErrInvalidInput = errors.New("item: invalid input") ErrNotFound = errors.New("item: not found") ErrConflict = errors.New("item: conflict") ErrIDGeneration = errors.New("item: generate id") )
Sentinel errors are stable across adapters. Callers should use errors.Is/errors.As instead of matching error strings.
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") // 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 ¶
IdempotencyScopeFromContext returns the configured scope or the stable default used by direct (non-HTTP) callers.
func ValidateIdempotencyKey ¶
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 ¶
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 ¶
Clock supplies creation times. Production uses time.Now; tests can inject a fixed clock without changing domain code.
type CreateInput ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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) List ¶
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 ¶
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. |