models

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
const (
	OpCreate     = "create"
	OpUpdate     = "update"
	OpDelete     = "delete"
	OpSoftDelete = "soft_delete"
	OpRestore    = "restore"
	OpQuery      = "query"
)

Operation type constants

View Source
const RequestIDKey contextKey = "request_id"

RequestIDKey is the context key for request ID.

Variables

View Source
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.

View Source
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

func BoundingBox(lat, lon, radiusKm float64) (minLat, maxLat, minLon, maxLon float64)

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

func CosApprox(degrees float64) float64

CosApprox provides a simple cosine approximation for geo calculations. Uses Taylor series approximation for small angles.

func DBFrom

func DBFrom(ctx context.Context, fallback *gorm.DB) *gorm.DB

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

func GenerateSlug(s string) string

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

func GetRequestIDFromContext(ctx context.Context) string

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

func NewValidationError(field, message string) error

NewValidationError creates a new validation error with field and message.

func NormalizePhone

func NormalizePhone(raw, errorField string) (string, error)

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

func ValidateAbbreviation(abbr string) (string, error)

ValidateAbbreviation validates and normalizes a 2-character abbreviation. Returns the normalized uppercase abbreviation.

func ValidateCoordinates

func ValidateCoordinates(lat, lon *float64) error

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

func ValidateGeoParams(lat, lon, radiusKm float64) error

ValidateGeoParams validates geo query parameters.

func ValidateNonNegative

func ValidateNonNegative(value *int64, fieldName string) error

ValidateNonNegative validates an int64 pointer is non-negative.

func ValidateOptionalURL

func ValidateOptionalURL(urlStr *string, fieldName string) error

ValidateOptionalURL validates an optional URL field. Returns nil if the URL is nil/empty or valid, error if invalid.

func ValidateOptionalUUID

func ValidateOptionalUUID(uuidPtr *string, fieldName string) error

ValidateOptionalUUID validates an optional UUID pointer field. Returns nil if the UUID is nil/empty or valid, validation error if invalid.

func ValidateRequired

func ValidateRequired(value, fieldName string) (string, error)

ValidateRequired trims and validates a required string field. Returns the trimmed value and a validation error if empty.

func ValidateSlug

func ValidateSlug(slug string) (string, error)

ValidateSlug normalizes and validates a slug. Returns the normalized lowercase slug.

func ValidateUUID

func ValidateUUID(id string) error

ValidateUUID checks if the given string is a valid UUID.

func WithClock

func WithClock(ctx context.Context, clk Clock) context.Context

WithClock returns a child context carrying clk.

func WithTx

func WithTx(ctx context.Context, tx *gorm.DB) context.Context

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

func WrapDBError(err error) error

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

type Auditable interface {
	GetID() string
	TableName() string
}

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

func (b *BaseModel[ID]) BeforeCreate(_ *gorm.DB) error

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.

func (*BaseModel[ID]) GetID

func (b *BaseModel[ID]) GetID() string

GetID returns the model's ID as a string. Satisfies the Auditable interface so audit hooks can log a uniform string identifier across all typed-ID models.

func (*BaseModel[ID]) IsDeleted

func (b *BaseModel[ID]) IsDeleted() bool

IsDeleted returns true if the record has been soft-deleted.

type Clock

type Clock interface {
	Now(ctx context.Context) time.Time
}

Clock abstracts "now" so callers can be given deterministic time.

func ClockFrom

func ClockFrom(ctx context.Context) Clock

ClockFrom returns the Clock attached to ctx, or a real-time default clock when none is attached.

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.

func (FixedClock) Now

Now returns the anchored time.

type GeoLocatable

type GeoLocatable interface {
	GetLatitude() *float64
	GetLongitude() *float64
}

GeoLocatable indicates a model has latitude/longitude coordinates.

type HookFunc

type HookFunc func(tx *gorm.DB, model any) error

HookFunc is the signature for lifecycle hooks.

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

type QueryOption func(*gorm.DB) *gorm.DB

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

type Repository[T any, ID ~string] struct {
	// contains filtered or unexported fields
}

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.

func (*Repository[T, ID]) Restore

func (r *Repository[T, ID]) Restore(ctx context.Context, id ID) error

Restore un-deletes a soft-deleted record.

func (*Repository[T, ID]) Update

func (r *Repository[T, ID]) Update(ctx context.Context, entity *T) error

Update saves changes to an existing record.

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

type ValidationError struct {
	Field   string
	Message string
}

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.

Jump to

Keyboard shortcuts

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