Documentation
¶
Overview ¶
Package models provides core infrastructure for GORM-based domain models.
This package contains the building blocks that all domain models depend on:
- BaseModel: Embedded struct providing ID, timestamps, soft-delete, metadata
- Repository[T]: Generic CRUD repository with query options
- Trait interfaces: Sluggable, GeoLocatable, Nameable, Auditable
- Errors: Sentinel errors and ValidationError for consistent error handling
- Hooks: Lifecycle hook system for audit logging and extensibility
Usage ¶
Domain models embed BaseModel and implement desired traits:
type State struct {
models.BaseModel
Name string `gorm:"size:100;not null"`
Abbreviation string `gorm:"size:2;uniqueIndex"`
}
func (s *State) GetName() string { return s.Name } // Implements Nameable
Repositories embed the generic Repository:
type stateRepository struct {
*models.Repository[State]
}
Query Options ¶
Use QueryOption functions to build flexible queries:
states, err := repo.FindAll(ctx,
models.WithLimit(10),
models.WithOrderBy("name", false),
models.WithPreload("Cities"),
)
Database Compatibility ¶
All components are designed for PostgreSQL (production) and SQLite (testing). See BaseModel documentation for type mapping details.
Index ¶
- Constants
- Variables
- func ApplyOptions(db *gorm.DB, opts ...QueryOption) *gorm.DB
- func BoundingBox(lat, lon, radiusKm float64) (minLat, maxLat, minLon, maxLon float64)
- func CosApprox(degrees float64) float64
- func DBFrom(ctx context.Context, fallback *gorm.DB) *gorm.DB
- func GenerateSlug(s string) string
- func GetRequestIDFromContext(ctx context.Context) string
- func NewID() string
- func NewValidationError(field, message string) error
- func NormalizePhone(raw, errorField string) (string, error)
- func ValidateAbbreviation(abbr string) (string, error)
- func ValidateCoordinates(lat, lon *float64) error
- func ValidateExternalSource(e *ExternalSource) error
- func ValidateGeoParams(lat, lon, radiusKm float64) error
- func ValidateNonNegative(value *int64, fieldName string) error
- func ValidateOptionalURL(urlStr *string, fieldName string) error
- func ValidateOptionalUUID(uuidPtr *string, fieldName string) error
- func ValidateRequired(value, fieldName string) (string, error)
- func ValidateSlug(slug string) (string, error)
- func ValidateUUID(id string) error
- func WithClock(ctx context.Context, clk Clock) context.Context
- func WithTx(ctx context.Context, tx *gorm.DB) context.Context
- func WrapDBError(err error) error
- type Auditable
- type BaseModel
- type Clock
- type DBLogger
- type DBOperation
- type DefaultDBLogger
- type ExternalSource
- type FixedClock
- type GeoLocatable
- type HookFunc
- type HookOption
- type HookRunner
- type Nameable
- type NopDBLogger
- type NormalizedEmail
- type ProviderRule
- type QueryOption
- func FindBySlugOpt(slug string) (QueryOption, error)
- func FindNearbyOpt(lat, lon, radiusKm float64) (QueryOption, error)
- func SearchByNameOpt(query string) QueryOption
- func WithCondition(query any, args ...any) QueryOption
- func WithIncludeDeleted(include bool) QueryOption
- func WithLimit(limit int) QueryOption
- func WithOffset(offset int) QueryOption
- func WithOrderBy(field string, desc bool) QueryOption
- func WithPreload(association string, args ...any) QueryOption
- func WithSelect(fields ...string) QueryOption
- type Repository
- func (r *Repository[T, ID]) Count(ctx context.Context, opts ...QueryOption) (int64, error)
- func (r *Repository[T, ID]) Create(ctx context.Context, entity *T) error
- func (r *Repository[T, ID]) DB() *gorm.DB
- func (r *Repository[T, ID]) Delete(ctx context.Context, id ID) error
- func (r *Repository[T, ID]) Exists(ctx context.Context, id ID) (bool, error)
- func (r *Repository[T, ID]) Find(ctx context.Context, id ID, opts ...QueryOption) (*T, error)
- func (r *Repository[T, ID]) FindAll(ctx context.Context, opts ...QueryOption) ([]T, error)
- func (r *Repository[T, ID]) FindOne(ctx context.Context, opts ...QueryOption) (*T, error)
- func (r *Repository[T, ID]) HardDelete(ctx context.Context, id ID) error
- func (r *Repository[T, ID]) ReadDB() *gorm.DB
- func (r *Repository[T, ID]) Restore(ctx context.Context, id ID) error
- func (r *Repository[T, ID]) Update(ctx context.Context, entity *T) error
- type Sluggable
- type TemporalEdge
- type Transactor
- type ValidationError
Constants ¶
const ( OpCreate = "create" OpUpdate = "update" OpDelete = "delete" OpSoftDelete = "soft_delete" OpRestore = "restore" OpQuery = "query" )
Operation type constants
const RequestIDKey contextKey = "request_id"
RequestIDKey is the context key for request ID.
Variables ¶
var ( // ErrNotFound indicates the requested resource was not found. ErrNotFound = errors.New("resource not found") // ErrValidation indicates the input data failed validation. ErrValidation = errors.New("validation failed") // ErrDuplicateKey indicates a unique constraint violation occurred. ErrDuplicateKey = errors.New("duplicate key violation") // ErrForeignKey indicates a foreign key constraint violation occurred. ErrForeignKey = errors.New("foreign key constraint violated") // ErrDatabaseError indicates a general database operation failure. ErrDatabaseError = errors.New("database operation failed") // ErrCascadeDelete indicates deletion was blocked due to dependent records. ErrCascadeDelete = errors.New("cannot delete: dependent records exist") // ErrInvalidID indicates the provided ID is not a valid UUID. ErrInvalidID = errors.New("invalid id format") // ErrHook indicates a hook execution failed. ErrHook = errors.New("hook error") )
Sentinel errors for model operations. Use errors.Is() to check for these errors in handlers and services.
var ProviderRules = map[string]ProviderRule{ "gmail.com": {Separator: '+', StripDots: true}, "outlook.com": {Separator: '+'}, "hotmail.com": {Separator: '+'}, "hotmail.co.uk": {Separator: '+'}, "hotmail.fr": {Separator: '+'}, "hotmail.de": {Separator: '+'}, "live.com": {Separator: '+'}, "msn.com": {Separator: '+'}, "yahoo.com": {Separator: '+'}, "yahoo.co.uk": {Separator: '+'}, "yahoo.fr": {Separator: '+'}, "yahoo.de": {Separator: '+'}, "fastmail.com": {Separator: '+'}, "fastmail.fm": {Separator: '+'}, "icloud.com": {Separator: '+'}, "proton.me": {Separator: '+'}, "protonmail.com": {Separator: '+'}, "protonmail.ch": {Separator: '+'}, "pm.me": {Separator: '+'}, }
ProviderRules holds the per-provider alias collapse policy, keyed by the canonical domain (post domainAliases lookup).
Functions ¶
func ApplyOptions ¶
func ApplyOptions(db *gorm.DB, opts ...QueryOption) *gorm.DB
ApplyOptions applies all query options to the database connection.
func BoundingBox ¶
BoundingBox calculates lat/lng bounds for a center point and radius. Returns (minLat, maxLat, minLon, maxLon). Uses the approximation that 1 degree latitude ≈ 111km.
func CosApprox ¶
CosApprox provides a simple cosine approximation for geo calculations. Uses Taylor series approximation for small angles.
func DBFrom ¶
DBFrom returns the *gorm.DB attached to ctx, or fallback when none is attached. Repositories call DBFrom(ctx, r.db) so that calls made inside a Transactor.WithinTx block automatically pick up the transaction handle.
func GenerateSlug ¶
GenerateSlug creates a URL-friendly slug from the given string. It converts to lowercase, replaces spaces/underscores with hyphens, removes non-alphanumeric characters, and cleans up consecutive hyphens.
func GetRequestIDFromContext ¶
GetRequestIDFromContext extracts the request ID from context for log correlation.
func NewID ¶
func NewID() string
NewID returns a freshly minted UUID v7 string. v7 IDs embed a millisecond timestamp, so they sort lexicographically in creation order — which lets later phases paginate by id alone instead of composite cursors.
If the underlying RNG fails (a process-level fault, not a runtime condition on supported platforms), NewID panics.
func NewValidationError ¶
NewValidationError creates a new validation error with field and message.
func NormalizePhone ¶
NormalizePhone canonicalizes a phone string to E.164. A bare 10-digit number is treated as North American. An input that begins with '+' is preserved verbatim apart from non-digit stripping; anything else is prefixed with '+'.
Returns a ValidationError (wrapping ErrValidation) on empty input or on a result that fails the E.164 regex. The errorField argument is the field name reported in the validation error (e.g. "phone" or "e164") so the caller can shape the error to match its own DTO.
func ValidateAbbreviation ¶
ValidateAbbreviation validates and normalizes a 2-character abbreviation. Returns the normalized uppercase abbreviation.
func ValidateCoordinates ¶
ValidateCoordinates validates latitude and longitude if provided. Latitude must be between -90 and 90, longitude between -180 and 180.
func ValidateExternalSource ¶
func ValidateExternalSource(e *ExternalSource) error
ValidateExternalSource validates the ExternalSource fields against the storage constraints and the dedup-key contract:
- Provider (when set) must be at most 50 characters.
- ExternalID (when set) must be at most 500 characters.
- When Provider is set, ExternalID is required: the two together form the provider dedup key, so a provider without an external identifier cannot be deduplicated.
A nil receiver and a fully-empty source are both valid (user-created entities carry no provenance). Returns a ValidationError wrapping ErrValidation.
func ValidateGeoParams ¶
ValidateGeoParams validates geo query parameters.
func ValidateNonNegative ¶
ValidateNonNegative validates an int64 pointer is non-negative.
func ValidateOptionalURL ¶
ValidateOptionalURL validates an optional URL field. Returns nil if the URL is nil/empty or valid, error if invalid.
func ValidateOptionalUUID ¶
ValidateOptionalUUID validates an optional UUID pointer field. Returns nil if the UUID is nil/empty or valid, validation error if invalid.
func ValidateRequired ¶
ValidateRequired trims and validates a required string field. Returns the trimmed value and a validation error if empty.
func ValidateSlug ¶
ValidateSlug normalizes and validates a slug. Returns the normalized lowercase slug.
func ValidateUUID ¶
ValidateUUID checks if the given string is a valid UUID.
func WithTx ¶
WithTx returns a child context carrying the active GORM transaction handle. Repositories read this via DBFrom so that callers do not need to thread a transactional manager through every method signature.
func WrapDBError ¶
WrapDBError converts GORM and driver errors to domain errors. It uses typed `errors.As` matching for both the PostgreSQL (pgx) and SQLite drivers, falling back to a string-based match only as a defensive last resort — any string fallback emits a slog.Warn so we can detect when a driver upgrade changes error shapes.
Types ¶
type Auditable ¶
Auditable indicates a model supports audit logging. Models implementing this interface will have their lifecycle events logged.
type BaseModel ¶
type BaseModel[ID ~string] struct { // ID is the UUID primary key, auto-generated on create if empty. ID ID `gorm:"type:uuid;primaryKey" json:"id"` // CreatedAt is automatically set when the record is created. CreatedAt time.Time `json:"created_at"` // UpdatedAt is automatically updated when the record is modified. UpdatedAt time.Time `json:"updated_at"` // DeletedAt enables soft-delete functionality - records are filtered by default. DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` // Metadata allows storing arbitrary JSON data for extensibility. Metadata datatypes.JSON `gorm:"type:jsonb;default:'{}'" json:"metadata,omitempty"` }
BaseModel provides common fields for all models in the system. It is generic over a typed ID (any `~string` alias) so that each bounded context can declare its own ID type (e.g. people.PersonID, channels.PhoneID) without losing compile-time safety.
Embed the typed form:
type Person struct {
models.BaseModel[PersonID]
GivenName string
}
PostgreSQL vs SQLite Considerations ¶
This package is designed to work with both PostgreSQL (production) and SQLite (testing). Key compatibility considerations:
## UUID Type
PostgreSQL has native UUID type support, while SQLite stores UUIDs as TEXT. Typed IDs (e.g. PersonID) are string aliases — GORM's reflection sees the underlying string and serializes correctly on both drivers.
## JSONB Type
The datatypes.JSON type handles cross-database JSON storage:
- PostgreSQL: Uses JSONB column type with indexing support
- SQLite: Uses TEXT column with JSON stored as string
## Timestamps
GORM's automatic timestamp handling works on both databases.
## Soft Delete Filtering
The gorm.DeletedAt type provides automatic soft-delete filtering on both.
func (*BaseModel[ID]) BeforeCreate ¶
BeforeCreate is a GORM hook that generates a UUID v7 for the ID field if not already set. v7 IDs are time-ordered; an explicitly-set ID is preserved unchanged so replay tooling can supply its own identifiers.
type DBLogger ¶
type DBLogger interface {
// LogOperation logs a database operation with context.
LogOperation(ctx context.Context, op DBOperation)
}
DBLogger defines an interface for database operation logging. Implementations can use this for audit trails, metrics, or debugging.
type DBOperation ¶
type DBOperation struct {
// Timestamp is when the operation occurred (UTC)
Timestamp time.Time `json:"timestamp"`
// Type is the operation type: "create", "update", "delete", "query"
Type string `json:"type"`
// Model is the model name: "user", "account", "order"
Model string `json:"model"`
// ID is the record ID (if applicable)
ID string `json:"id,omitempty"`
// Duration is how long the operation took
DurationMS int64 `json:"duration_ms,omitempty"`
// RowsAffected is the number of rows affected (for write operations)
RowsAffected int64 `json:"rows_affected,omitempty"`
// Error is the error message (if operation failed)
Error string `json:"error,omitempty"`
// RequestID is the request ID from context (for correlation)
RequestID string `json:"request_id,omitempty"`
}
DBOperation represents a database operation for logging.
type DefaultDBLogger ¶
type DefaultDBLogger struct{}
DefaultDBLogger forwards DB operations to slog.Default() as a single structured "db_operation" line. The handler installed by the binary (typically observability.Init) controls level filtering, output, and encoding.
func (*DefaultDBLogger) LogOperation ¶
func (l *DefaultDBLogger) LogOperation(ctx context.Context, op DBOperation)
LogOperation logs the database operation through slog.
type ExternalSource ¶
type ExternalSource struct {
// Provider is the external provider name (e.g., "leaguelinq").
Provider *string `gorm:"size:50;index" json:"provider,omitempty"`
// ExternalID is the provider-assigned unique identifier used as a dedup key.
ExternalID *string `gorm:"size:500" json:"external_id,omitempty"`
// LastSeenAt tracks the last time this entity was observed from the provider.
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
}
ExternalSource is an embeddable struct that provides provenance tracking for entities created by external data providers. All fields are optional (pointers) so models can be either user-created (nil provider) or externally-sourced.
Embed anonymously in models so fields are promoted (e.g., game.Provider):
type League struct {
models.BaseModel
models.ExternalSource
Name string `gorm:"size:255;not null" json:"name"`
}
func (*ExternalSource) IsExternal ¶
func (e *ExternalSource) IsExternal() bool
IsExternal returns true if this entity was created by an external provider.
type FixedClock ¶
type FixedClock struct {
// contains filtered or unexported fields
}
FixedClock is a Clock that always returns a constant time, regardless of the context or the wall clock.
func NewFixedClock ¶
func NewFixedClock(t time.Time) FixedClock
NewFixedClock returns a FixedClock anchored at t.
type GeoLocatable ¶
GeoLocatable indicates a model has latitude/longitude coordinates.
type HookOption ¶
type HookOption func(*HookRunner)
HookOption configures a HookRunner.
func WithAfterCreate ¶
func WithAfterCreate(h HookFunc) HookOption
WithAfterCreate adds an AfterCreate hook to the runner.
func WithAfterDelete ¶
func WithAfterDelete(h HookFunc) HookOption
WithAfterDelete adds an AfterDelete hook to the runner.
func WithAfterUpdate ¶
func WithAfterUpdate(h HookFunc) HookOption
WithAfterUpdate adds an AfterUpdate hook to the runner.
type HookRunner ¶
type HookRunner struct {
// contains filtered or unexported fields
}
HookRunner manages and executes lifecycle hooks. It allows models to run a chain of hooks and optionally add custom logic.
func DefaultHooks ¶
func DefaultHooks() *HookRunner
DefaultHooks returns an empty HookRunner with no pre-registered hooks. Models can use this as a starting point and extend with custom logic via WithAfterCreate, WithAfterUpdate, and WithAfterDelete.
func NewHookRunner ¶
func NewHookRunner(opts ...HookOption) *HookRunner
NewHookRunner creates a HookRunner with the given options.
func (*HookRunner) RunAfterCreate ¶
func (r *HookRunner) RunAfterCreate(tx *gorm.DB, model any) error
RunAfterCreate executes all AfterCreate hooks in order. Returns the first error encountered, or nil if all hooks succeed.
func (*HookRunner) RunAfterDelete ¶
func (r *HookRunner) RunAfterDelete(tx *gorm.DB, model any) error
RunAfterDelete executes all AfterDelete hooks in order. Returns the first error encountered, or nil if all hooks succeed.
func (*HookRunner) RunAfterUpdate ¶
func (r *HookRunner) RunAfterUpdate(tx *gorm.DB, model any) error
RunAfterUpdate executes all AfterUpdate hooks in order. Returns the first error encountered, or nil if all hooks succeed.
type Nameable ¶
type Nameable interface {
GetName() string
}
Nameable indicates a model has a searchable name field.
type NopDBLogger ¶
type NopDBLogger struct{}
NopDBLogger is a no-op logger for testing or when logging is disabled.
func (*NopDBLogger) LogOperation ¶
func (l *NopDBLogger) LogOperation(_ context.Context, _ DBOperation)
LogOperation does nothing.
type NormalizedEmail ¶
type NormalizedEmail struct {
// Address is the canonical per-row form: lowercased ASCII domain (IDN
// Punycode), PRECIS-folded local part. Plus tags are preserved here.
// Quoted local parts keep their surrounding quotes verbatim.
Address string
// Root is the alias-collapsed form: provider rules applied (e.g. plus
// tag stripped, Gmail dots removed). May equal Address when no rules apply.
// For quoted local parts, Root equals Address (provider rules are skipped).
Root string
// Domain is the ASCII (Punycode) canonical domain, post-aliasing
// (e.g. googlemail.com → gmail.com).
Domain string
// DisplayLocal is the local part as it appeared in the input
// (post-whitespace-trim, pre-folding). Useful for display.
DisplayLocal string
// IsQuoted is true when the local part was a quoted string ("...@...").
// Quoted local parts are opaque per RFC 5321 §4.1.2 and skip provider rules.
IsQuoted bool
}
NormalizedEmail is the result of NormalizeEmail. It carries both the per-row canonical form (Address) used as the storage key and the alias-collapsed Root used to group equivalent mailboxes (e.g. Gmail's plus-tags and dot-insensitivity).
func NormalizeEmail ¶
func NormalizeEmail(raw string) (NormalizedEmail, error)
NormalizeEmail validates an email address and produces its canonical and alias-root forms. It accepts the broad RFC 5321/5322 punctuation set, quoted local parts, full Unicode local parts via PRECIS (RFC 8265), and internationalized domain names via IDNA (RFC 5891).
Returns a ValidationError (wrapping ErrValidation) on any parse, length, PRECIS, or IDNA failure.
type ProviderRule ¶
type ProviderRule struct {
// Separator is the rune that begins a tag suffix on the local part. Everything
// from the first occurrence onward is stripped when computing the alias root.
Separator rune
// StripDots removes every "." from the local part when computing the alias
// root. Only Gmail/Googlemail behaves this way.
StripDots bool
}
ProviderRule describes how a mailbox provider (or class of providers) collapses distinct addresses to the same underlying mailbox. The default rule treats "+" as a tag separator and preserves dots; provider-specific rules override.
Caveats:
- Outlook's "+" aliasing is config-dependent at the upstream MTA; treating "+" as an alias separator here may collapse rows the provider actually delivers separately. We accept that risk since the result is only used as a queryable alias key — the original address is preserved as the unique row identity.
- Yahoo historically used "-" as a Disposable Address Plus separator, but that conflicts with legitimate hyphenated local parts (e.g. "mary-anne" would collapse to "mary"). We only honor "+" for Yahoo.
- FastMail's subdomain alias trick (user@alias.fastmail.com) is not handled; only "+" tagging is collapsed.
func LookupProviderRule ¶
func LookupProviderRule(domain string) (canonicalDomain string, rule ProviderRule)
LookupProviderRule resolves a lowercased domain to its canonical form and the provider rule that governs its alias-root computation. Unknown domains return (domain, defaultProviderRule).
type QueryOption ¶
QueryOption is a functional option for customizing database queries. Options are applied in order when building queries.
func FindBySlugOpt ¶
func FindBySlugOpt(slug string) (QueryOption, error)
FindBySlugOpt returns a query option for slug lookup. Returns an error if the slug is empty after normalization.
func FindNearbyOpt ¶
func FindNearbyOpt(lat, lon, radiusKm float64) (QueryOption, error)
FindNearbyOpt returns a query option for bounding box geo queries. Validates coordinates and calculates the bounding box.
func SearchByNameOpt ¶
func SearchByNameOpt(query string) QueryOption
SearchByNameOpt returns a query option for case-insensitive name search. If the query is empty, returns a no-op option.
func WithCondition ¶
func WithCondition(query any, args ...any) QueryOption
WithCondition adds a custom WHERE condition to the query. This allows for arbitrary filtering without predefined methods.
Example:
cities, err := repo.FindAll(ctx, WithCondition("population > ?", 1000000))
func WithIncludeDeleted ¶
func WithIncludeDeleted(include bool) QueryOption
WithIncludeDeleted includes soft-deleted records in the query results.
func WithLimit ¶
func WithLimit(limit int) QueryOption
WithLimit sets the maximum number of records to return.
func WithOffset ¶
func WithOffset(offset int) QueryOption
WithOffset sets the number of records to skip before returning results.
func WithOrderBy ¶
func WithOrderBy(field string, desc bool) QueryOption
WithOrderBy adds an ordering clause to the query. The desc parameter determines if the order is descending (true) or ascending (false).
Example:
states, err := repo.FindAll(ctx, WithOrderBy("name", false)) // ORDER BY name ASC
cities, err := repo.FindAll(ctx, WithOrderBy("created_at", true)) // ORDER BY created_at DESC
func WithPreload ¶
func WithPreload(association string, args ...any) QueryOption
WithPreload eagerly loads the specified association. Multiple WithPreload options can be chained to load multiple associations.
Example:
cities, err := repo.FindAll(ctx, WithPreload("State"), WithPreload("Neighborhoods"))
func WithSelect ¶
func WithSelect(fields ...string) QueryOption
WithSelect specifies which fields to retrieve. By default, all fields are selected.
Example:
states, err := repo.FindAll(ctx, WithSelect("id", "name"))
type Repository ¶
Repository provides generic CRUD operations for models. It is doubly generic — over the entity type T and over the entity's typed ID — so per-context repositories inherit fully-typed CRUD without per-entity shim methods. The active database handle is read from ctx via DBFrom, so a repository call made inside Transactor.WithinTx automatically picks up the transaction.
A repository may be configured with a separate read replica (see NewRepositoryWithReadWrite): read methods then target the replica while write methods target the primary. Reads issued inside an active transaction still use the transaction's connection (the primary), preserving read-your-writes consistency. When no replica is configured, reads and writes share one handle.
func NewRepository ¶
func NewRepository[T any, ID ~string](db *gorm.DB) *Repository[T, ID]
NewRepository creates a new generic repository instance bound to db. Reads and writes share the single connection.
func NewRepositoryWithReadWrite ¶ added in v0.1.4
func NewRepositoryWithReadWrite[T any, ID ~string](writeDB, readDB *gorm.DB) *Repository[T, ID]
NewRepositoryWithReadWrite creates a repository that routes writes to writeDB and reads to readDB (a read replica). Reads made inside an active transaction still use the transaction's connection, so they observe uncommitted writes. A nil readDB falls back to writeDB, making this equivalent to NewRepository.
func (*Repository[T, ID]) Count ¶
func (r *Repository[T, ID]) Count(ctx context.Context, opts ...QueryOption) (int64, error)
Count returns the number of records matching the query options.
func (*Repository[T, ID]) Create ¶
func (r *Repository[T, ID]) Create(ctx context.Context, entity *T) error
Create inserts a new record into the database.
func (*Repository[T, ID]) DB ¶
func (r *Repository[T, ID]) DB() *gorm.DB
DB returns the bound primary/write database handle. Repositories that need direct GORM access (custom joins, raw SQL) can use this. Prefer DBFrom(ctx, r.DB()) so that an active transaction is picked up.
func (*Repository[T, ID]) Delete ¶
func (r *Repository[T, ID]) Delete(ctx context.Context, id ID) error
Delete soft-deletes a record by its typed ID.
func (*Repository[T, ID]) Exists ¶
func (r *Repository[T, ID]) Exists(ctx context.Context, id ID) (bool, error)
Exists checks if a record with the given ID exists.
func (*Repository[T, ID]) Find ¶
func (r *Repository[T, ID]) Find(ctx context.Context, id ID, opts ...QueryOption) (*T, error)
Find retrieves a record by its typed UUID. Validates the ID format.
func (*Repository[T, ID]) FindAll ¶
func (r *Repository[T, ID]) FindAll(ctx context.Context, opts ...QueryOption) ([]T, error)
FindAll retrieves all records matching the query options.
func (*Repository[T, ID]) FindOne ¶
func (r *Repository[T, ID]) FindOne(ctx context.Context, opts ...QueryOption) (*T, error)
FindOne retrieves a single record matching the query options.
func (*Repository[T, ID]) HardDelete ¶
func (r *Repository[T, ID]) HardDelete(ctx context.Context, id ID) error
HardDelete permanently removes a record from the database. Use with caution — this operation is irreversible.
func (*Repository[T, ID]) ReadDB ¶ added in v0.1.4
func (r *Repository[T, ID]) ReadDB() *gorm.DB
ReadDB returns the read database handle (the read replica when configured, otherwise the primary). Prefer DBFrom(ctx, r.ReadDB()) so that an active transaction is picked up and observes uncommitted writes.
type Sluggable ¶
type Sluggable interface {
GetSlug() string
}
Sluggable indicates a model has a URL slug field.
type TemporalEdge ¶
type TemporalEdge[ID ~string] struct { // ID is the UUID v7 primary key, minted on create when empty. ID ID `gorm:"type:uuid;primaryKey" json:"id"` // ValidFrom is the real-world start of the relationship. It is never // auto-defaulted — a zero value signals a writer defect. ValidFrom time.Time `json:"valid_from"` // ValidTo is the real-world end; nil means the edge is still active. ValidTo *time.Time `json:"valid_to,omitempty"` // RecordedAt is the system time the edge was written; auto-defaulted to // the context clock's now when zero. RecordedAt time.Time `json:"recorded_at"` // EventTime is the source-asserted real-world event time; nil when the // source publishes none. EventTime *time.Time `json:"event_time,omitempty"` // SupersededByID points to the row that replaces this one after a correction. SupersededByID *ID `gorm:"type:uuid" json:"superseded_by_id,omitempty"` // SuppressedAt is set when the edge is operator-suppressed. SuppressedAt *time.Time `json:"suppressed_at,omitempty"` // Source is the human-readable adapter/source name. Source string `json:"source"` // ByteHash is the SHA-256 of the bronze byte range the edge derives from. ByteHash string `json:"byte_hash"` // RuleVersion is the version of the derivation rule that produced the edge. RuleVersion string `json:"rule_version"` // PipelineVersionHash is sha256(adapter_version‖rule_version‖fixture_version). PipelineVersionHash string `json:"pipeline_version_hash"` // Confidence is the source-published confidence; nil means not set, which // is distinct from a real 0.0 confidence. Confidence *float64 `json:"confidence,omitempty"` // VerificationStatus defaults to "unverified" when written empty. VerificationStatus string `json:"verification_status"` // CreatedAt is the row insert time; equals RecordedAt unless backdated by // replay tooling. Auto-defaulted to the context clock's now when zero. CreatedAt time.Time `json:"created_at"` }
TemporalEdge is the substrate for time-bounded relationships. Embed the typed form in any edge model (ownership, lien, mailing intent, communication, agent decision). It carries real-world validity, system time, source event time, append-only correction fields, and inline provenance.
TemporalEdge is generic over its own ID type so each edge model gets a distinct typed ID and a typed SupersededByID self-reference:
type PartyPhone struct {
models.TemporalEdge[PartyPhoneID]
PersonID people.PersonID
PhoneID PhoneID
}
TemporalEdge does NOT embed BaseModel — entities and relationships are independent substrates. Corrections are append-only (SupersededByID, SuppressedAt), so it has no soft-delete and no metadata blob.
func (*TemporalEdge[ID]) BeforeCreate ¶
func (e *TemporalEdge[ID]) BeforeCreate(tx *gorm.DB) error
BeforeCreate is the GORM hook that mints the ID and applies defaults. An explicitly-set ID, RecordedAt, or CreatedAt is preserved so replay tooling can supply its own. ValidFrom is never defaulted.
type Transactor ¶
type Transactor interface {
WithinTx(ctx context.Context, fn func(ctx context.Context) error) error
}
Transactor runs fn inside a database transaction. The ctx passed to fn carries the transaction handle, which Repository.* methods read via DBFrom. Nested calls to WithinTx use GORM savepoints, so services can compose without coordinating who owns the outer transaction.
func NewTransactor ¶
func NewTransactor(db *gorm.DB) Transactor
NewTransactor returns a Transactor backed by the given write database. When the ctx passed to WithinTx already carries a transaction, GORM creates a savepoint instead of starting a new top-level transaction.
type ValidationError ¶
ValidationError wraps ErrValidation with a specific message.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
Error returns the formatted validation error message.
func (*ValidationError) Unwrap ¶
func (e *ValidationError) Unwrap() error
Unwrap returns the underlying sentinel error for errors.Is() support.