history

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package history is a transparent decorator around any r3.CRUD[T, ID] that records every Create, Update, Patch, and Delete as a ChangeRecord.

Change records and snapshots are themselves r3 entities, persisted via any r3.CRUD[ChangeRecord, string] and r3.CRUD[Snapshot, string], so history can live in the same backend as the entities or a different one (entities in PostgreSQL, history in MongoDB).

Key features:

  • Field-level diffs (what changed, old value, new value)
  • Opt-in full entity snapshots via configurable SnapshotRules
  • Tree/nested queries (e.g. City + its Locations + their Events)
  • Revert to any historical version (purely diff-based reconstruction)
  • r3.Actor attribution as first-class, queryable ChangeRecord columns
  • Context-based metadata (source, request ID, etc.)
  • Convenience query builders (QueryForRecord, QueryForType, QueryForTree, etc.)
  • Retention policies (age-based TTL, per-entity version limits)

Actor Integration

ActorID and ActorType are resolved from r3.GetActor(ctx) on every mutation and stored as real columns (not in the Metadata JSON blob), so the log can be filtered and grouped by actor (see QueryForActor). Set the actor with r3.WithActor(ctx, ...), typically in auth middleware; with none set, the zero/SystemActor (Type "system") is recorded. MetadataFunc carries only surrounding context (Source, Extra), not the actor.

Retention

RetentionEnforcer deletes old change records by age (MaxAge) or per-entity version count (MaxVersions), one-by-one via r3.CRUD Delete (slow for large datasets; a future BatchDelete could optimize this). RetentionEnforcer.Start runs a ticker-based loop whose first pass fires after the interval elapses, not immediately on start.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrVersionNotFound is returned when a requested version does not exist.
	ErrVersionNotFound = errors.New("r3history: version not found")

	// ErrRecordNotFound is returned when a requested change record does not exist.
	ErrRecordNotFound = errors.New("r3history: record not found")

	// ErrNoHistory is returned when attempting to revert an entity with no history.
	ErrNoHistory = errors.New("r3history: no history for this record")
)

Sentinel errors for history operations.

View Source
var ErrIDFuncRequired = errors.New("r3history: IDFunc is required; use WithIDFunc option")

ErrIDFuncRequired is returned when WithHistory is called without an IDFunc, which the decorator needs to extract an entity's primary key.

Functions

func MarshalSnapshot

func MarshalSnapshot(entity any) json.RawMessage

MarshalSnapshot serializes an entity to JSON bytes, returning nil if entity is nil or marshaling fails.

func QueryForActor

func QueryForActor(actorID string) r3.Query

QueryForActor filters change records by the actor who performed them, sorted by created_at descending ("everything user X did") - possible only because the actor is a first-class column, not buried in the Metadata JSON blob.

func QueryForRecord

func QueryForRecord(recordType, recordID string) r3.Query

QueryForRecord filters change records for one entity instance, sorted by version ascending.

func QueryForTree

func QueryForTree(scopes []TreeScope) r3.Query

QueryForTree matches change records across a parent-child hierarchy: each TreeScope is one level, combined with OR.

func QueryForType

func QueryForType(recordType string) r3.Query

QueryForType filters change records for all entities of a type, sorted by created_at descending.

func QueryForVersion

func QueryForVersion(recordType, recordID string, version int64) r3.Query

QueryForVersion matches one specific version of one specific entity.

func QueryLatestSnapshot

func QueryLatestSnapshot(recordType, recordID string) r3.Query

QueryLatestSnapshot builds a Query for the most recent snapshot of an entity.

func QueryLatestVersion

func QueryLatestVersion(recordType, recordID string) r3.Query

QueryLatestVersion returns the most recent change record for an entity (version desc, limit 1). Used to compute NextVersion and implement RevertLast.

func QueryListSnapshots

func QueryListSnapshots(recordType, recordID string) r3.Query

QueryListSnapshots lists all snapshots for an entity, version descending.

func QuerySnapshotForVersion

func QuerySnapshotForVersion(recordType, recordID string, version int64) r3.Query

QuerySnapshotForVersion builds a Query for retrieving a snapshot at a specific version.

func UnmarshalSnapshot

func UnmarshalSnapshot[T any](data json.RawMessage) (T, error)

UnmarshalSnapshot deserializes a JSON snapshot back into an entity of type T.

Types

type Action

type Action string

Action is the type of mutation an entry records.

const (
	// ActionCreate is a new entity insert.
	ActionCreate Action = "create"
	// ActionUpdate is a full entity update.
	ActionUpdate Action = "update"
	// ActionPatch is a partial update (specific fields only).
	ActionPatch Action = "patch"
	// ActionDelete is a delete (or soft-delete).
	ActionDelete Action = "delete"
	// ActionRevert is a revert to a previous version.
	ActionRevert Action = "revert"
	// ActionEvent is a domain/synthetic timeline event, not a field mutation - see
	// [ChangeRecord.EventType].
	ActionEvent Action = "event"
)

type CRUD

type CRUD[T any, ID comparable] struct {
	// contains filtered or unexported fields
}

CRUD records every mutation on the wrapped repo as a ChangeRecord in the history store. Reads (Get, List, Count) pass through unrecorded.

func WithHistory

func WithHistory[T any, ID comparable](
	inner r3.CRUD[T, ID],
	store r3.CRUD[ChangeRecord, string],
	optFns ...Option[T, ID],
) *CRUD[T, ID]

WithHistory wraps a repo with history tracking. store is any r3.CRUD[ChangeRecord, string]; the IDFunc option is required so the decorator can extract an entity's primary key.

repo := r3history.WithHistory[Order, int64](
    gormOrderCRUD,
    historyStore,
    r3history.WithIDFunc[Order, int64](func(o Order) int64 { return o.ID }),
    r3history.WithMetadataFunc[Order, int64](metadataFromCtx),
)

func (*CRUD[T, ID]) Aggregate added in v0.0.3

func (h *CRUD[T, ID]) Aggregate(ctx context.Context, qarg ...r3.Query) ([]r3.AggregateRow, error)

Aggregate passes through to the inner CRUD (read, unrecorded).

func (*CRUD[T, ID]) AggregateThroughRelation added in v0.0.5

func (h *CRUD[T, ID]) AggregateThroughRelation(
	ctx context.Context, relation string, qarg ...r3.Query,
) ([]r3.AggregateRow, error)

AggregateThroughRelation passes through to the inner CRUD (read, unrecorded).

func (*CRUD[T, ID]) Count

func (h *CRUD[T, ID]) Count(ctx context.Context, qarg ...r3.Query) (int64, error)

Count returns the number of matching entities. No history is recorded for reads.

func (*CRUD[T, ID]) Create

func (h *CRUD[T, ID]) Create(ctx context.Context, entity T) (T, error)

Create inserts a new entity and records a "create" change.

func (*CRUD[T, ID]) Delete

func (h *CRUD[T, ID]) Delete(ctx context.Context, id ID) error

Delete removes an entity and records a "delete" change.

func (*CRUD[T, ID]) Get

func (h *CRUD[T, ID]) Get(ctx context.Context, id ID, qarg ...r3.Query) (T, error)

Get retrieves an entity by ID. No history is recorded for reads.

func (*CRUD[T, ID]) History

func (h *CRUD[T, ID]) History() r3.CRUD[ChangeRecord, string]

History returns the change-record CRUD, for use with the query builders (QueryForRecord, QueryForType, ...).

func (*CRUD[T, ID]) Inner

func (h *CRUD[T, ID]) Inner() r3.CRUD[T, ID]

Inner returns the wrapped repo, bypassing history tracking.

func (*CRUD[T, ID]) List

func (h *CRUD[T, ID]) List(ctx context.Context, qarg ...r3.Query) ([]T, int64, error)

List retrieves entities. No history is recorded for reads.

func (*CRUD[T, ID]) Patch

func (h *CRUD[T, ID]) Patch(ctx context.Context, entity T, fields r3.Fields) (T, error)

Patch performs a partial update and records a "patch" change with field-level diff.

func (*CRUD[T, ID]) PatchWhere added in v0.0.4

func (h *CRUD[T, ID]) PatchWhere(
	ctx context.Context, filters r3.Filters, entity T, fields r3.Fields,
) (int64, error)

PatchWhere runs a bulk conditional update and records one ActionPatch per affected row: it snapshots matching rows before the write (List over the same filters) and re-reads each by PK afterward to diff old vs new, so a bulk sweep still leaves a per-row trail. Bounded work, for the small recovery sweeps this targets.

Per-row history requires IDFunc; without it the change cannot be attributed to specific rows, so PatchWhere reports the gap via the error handler (loud, never silent) and still performs the update.

func (*CRUD[T, ID]) RecordEvent

func (h *CRUD[T, ID]) RecordEvent(
	ctx context.Context, id ID, eventType string, data map[string]any,
) (ChangeRecord, error)

RecordEvent appends a timeline event (Action ActionEvent) to an entity, separate from field-level changes. It takes the next timeline version but is skipped by state reconstruction (no diff). The actor is resolved like any change.

func (*CRUD[T, ID]) RecordSyntheticCreate

func (h *CRUD[T, ID]) RecordSyntheticCreate(
	ctx context.Context, entity T, at time.Time,
) (ChangeRecord, error)

RecordSyntheticCreate writes a best-effort "create" for an entity that predates tracking: it captures the CURRENT state as the create diff, dates it `at`, and flags the record Synthetic. Use during backfill so the entity has an origin point.

func (*CRUD[T, ID]) Reverter

func (h *CRUD[T, ID]) Reverter() *Reverter[T, ID]

Reverter returns a Reverter for undo/revert on this entity type.

func (*CRUD[T, ID]) Rewrap

func (h *CRUD[T, ID]) Rewrap(inner r3.CRUD[T, ID]) r3.CRUD[T, ID]

Rewrap re-applies this layer over a different inner CRUD (e.g. a tx-bound one). The store, options, and version locks are shared so version assignment stays serialized across the original and tx-bound layers.

func (*CRUD[T, ID]) Unwrap

func (h *CRUD[T, ID]) Unwrap() r3.CRUD[T, ID]

Unwrap returns the wrapped CRUD for chain walking (capability detection, tx).

func (*CRUD[T, ID]) Update

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

Update modifies an entity and records an "update" change with field-level diff.

func (*CRUD[T, ID]) Upsert added in v0.0.4

func (h *CRUD[T, ID]) Upsert(ctx context.Context, entity T, opts ...r3.UpsertOption) (T, error)

Upsert inserts-or-updates via the inner CRUD and records the change. It fetches the pre-state before the write: a hit records an ActionUpdate with a field-level diff, a miss records an ActionCreate.

The pre-state is looked up through the SAME target the write conflicts on - r3.OnConflict's columns when it declares any, the PK otherwise - so an upsert that updates an existing row by unique column records the update it performed rather than a create.

type ChangeRecord

type ChangeRecord struct {
	// ID uniquely identifies this record (UUID).
	ID string `json:"id" db:"id,pk" bson:"_id"`

	// RecordType is the entity type name (e.g. "orders"). Derived from the struct
	// type or set explicitly via options.
	RecordType string `json:"record_type" db:"record_type" bson:"record_type"`

	// RecordID is the entity primary key, stringified.
	RecordID string `json:"record_id" db:"record_id" bson:"record_id"`

	// Action is the mutation type.
	Action Action `json:"action" db:"action" bson:"action"`

	// ActorID identifies who made the change (resolved from r3.GetActor(ctx)). A
	// first-class indexable column, not part of Metadata, so the log can be
	// filtered/grouped by actor. Empty for the system/anonymous actor.
	ActorID string `json:"actor_id,omitempty" db:"actor_id" bson:"actor_id,omitempty"`

	// ActorType categorizes the actor (e.g. "user", "system"); first-class column
	// alongside ActorID.
	ActorType string `json:"actor_type,omitempty" db:"actor_type" bson:"actor_type,omitempty"`

	// Version is a monotonic counter per (record_type, record_id); version 1 is
	// always the create. Enables ordered history and revert-to-version.
	Version int64 `json:"version" db:"version" bson:"version"`

	// Changes is the field-level diff: create -> every field with OldValue=nil;
	// delete -> every field with NewValue=nil; update/patch -> only changed fields;
	// revert -> pre-revert vs reverted-to. Reconstruction ([Reverter.Reconstruct])
	// works purely from diffs because v1 always carries every field. Stored as JSON.
	Changes r3.JSONColumn[[]FieldChange] `json:"changes" db:"changes" bson:"changes"`

	// ParentType is the parent entity's record type (for tree queries); empty if
	// no parent.
	ParentType string `json:"parent_type,omitempty" db:"parent_type" bson:"parent_type,omitempty"`

	// ParentID is the parent primary key, stringified; empty if no parent.
	ParentID string `json:"parent_id,omitempty" db:"parent_id" bson:"parent_id,omitempty"`

	// EventType, when non-empty, marks this entry as a timeline event (Action is
	// ActionEvent) rather than a field mutation - e.g. "comment_added". Changes is
	// usually empty; EventData carries the payload. Changes and events share one
	// ordered timeline.
	EventType string `json:"event_type,omitempty" db:"event_type" bson:"event_type,omitempty"`

	// EventData is an optional structured event payload. Stored as JSON.
	EventData r3.JSONColumn[map[string]any] `json:"event_data" db:"event_data" bson:"event_data,omitempty"`

	// Synthetic marks a record reconstructed rather than observed - e.g. a
	// backfilled create inferred from an entity's CURRENT state because tracking
	// began after it existed. Its Changes are best-effort, not the original values.
	Synthetic bool `json:"synthetic,omitempty" db:"synthetic" bson:"synthetic,omitempty"`

	// Note is an optional human remark (e.g. why a record is synthetic).
	Note string `json:"note,omitempty" db:"note" bson:"note,omitempty"`

	// Metadata carries surrounding context (source, request correlation). Stored as
	// JSON.
	Metadata r3.JSONColumn[Metadata] `json:"metadata" db:"metadata" bson:"metadata"`

	// CreatedAt is when the change was recorded.
	CreatedAt time.Time `json:"created_at" db:"created_at" bson:"created_at"`
}

ChangeRecord is one activity-log entry: what happened to an entity at a point in time, as field-level diffs. Snapshots are a separate opt-in feature - see SnapshotRule.

type DiffFunc

type DiffFunc[T any] func(old, cur T) []FieldChange

DiffFunc computes field-level changes between two entities, replacing the default reflection-based differ.

type FieldChange

type FieldChange struct {
	// Field is the changed column/field name (dot-notation for nested structs).
	Field string `json:"field"`

	// OldValue is the previous value (nil for create).
	OldValue any `json:"old_value"`

	// NewValue is the new value (nil for delete).
	NewValue any `json:"new_value"`
}

FieldChange is a single field-level change. Field uses flat dot-notation (e.g. "address.city") matching the storage column names.

func Diff

func Diff[T any](old, cur T) []FieldChange

Diff computes field-level changes between two entities, comparing exported fields with db/bson/r3 tags and flattening nested structs to dot-notation (e.g. "address.city"). Returns nil when nothing changed.

func DiffCreate

func DiffCreate[T any](entity T) []FieldChange

DiffCreate generates FieldChanges for a new entity: every field OldValue=nil, NewValue=<current value>.

func DiffDelete

func DiffDelete[T any](entity T) []FieldChange

DiffDelete generates FieldChanges for a deleted entity: every field OldValue=<current value>, NewValue=nil.

func DiffWithFields

func DiffWithFields[T any](old, cur T, fields []string) []FieldChange

DiffWithFields computes changes only for the named fields, given as column/field names (snake_case), not Go struct names.

func (FieldChange) String

func (fc FieldChange) String() string

String renders the field change as "field: old -> new".

type IDFunc

type IDFunc[T any, ID comparable] func(entity T) ID

IDFunc extracts an entity's primary key.

type Metadata

type Metadata struct {
	// Source is where the change originated (e.g. "api", "admin_ui", "worker").
	Source string `json:"source,omitempty"`

	// Extra holds arbitrary context pairs (e.g. request_id, ip_address).
	Extra map[string]string `json:"extra,omitempty"`
}

Metadata carries context surrounding a change (not the actor, which is first-class on ChangeRecord). Extracted from context via a user MetadataFunc.

type MetadataFunc

type MetadataFunc func(ctx context.Context) Metadata

MetadataFunc extracts surrounding-context Metadata (Source, Extra) from a context. The actor is NOT set here - it is a first-class field resolved from r3.GetActor(ctx).

func metadataFromCtx(ctx context.Context) Metadata {
    return r3history.Metadata{
        Source: "admin_ui",
        Extra:  map[string]string{"request_id": reqID(ctx)},
    }
}

type Option

type Option[T any, ID comparable] func(*Options[T, ID])

Option is a functional option for configuring CRUD.

func WithAsync

func WithAsync[T any, ID comparable]() Option[T, ID]

WithAsync enables asynchronous history recording.

func WithDiffFunc

func WithDiffFunc[T any, ID comparable](fn DiffFunc[T]) Option[T, ID]

WithDiffFunc sets a custom diff function.

func WithErrorHandler

func WithErrorHandler[T any, ID comparable](fn func(error)) Option[T, ID]

WithErrorHandler sets a handler for a failed change-record write, overriding default slog logging. It does not change whether the op fails - use WithFailOnError.

func WithFailOnError

func WithFailOnError[T any, ID comparable]() Option[T, ID]

WithFailOnError returns a failed change-record write's error from the CRUD op (sync mode). See Options.FailOnError for the caveat that the mutation has already been applied. Ignored in async mode.

func WithFixedActor

func WithFixedActor[T any, ID comparable](actor r3.Actor) Option[T, ID]

WithFixedActor attributes every change to actor, ignoring the context actor. See Options.FixedActor.

func WithIDFunc

func WithIDFunc[T any, ID comparable](fn IDFunc[T, ID]) Option[T, ID]

WithIDFunc sets the function that extracts the primary key from an entity.

func WithMetadataFunc

func WithMetadataFunc[T any, ID comparable](fn MetadataFunc) Option[T, ID]

WithMetadataFunc sets the function that extracts metadata from context.

func WithParentRef

func WithParentRef[T any, ID comparable](parentType, fkField string) Option[T, ID]

WithParentRef configures parent-child linking for tree queries.

func WithRecordType

func WithRecordType[T any, ID comparable](name string) Option[T, ID]

WithRecordType sets an explicit record type name.

func WithSnapshotRules

func WithSnapshotRules[T any, ID comparable](rules ...SnapshotRule[T]) Option[T, ID]

WithSnapshotRules configures opt-in snapshot rules (stored separately from change records).

type Options

type Options[T any, ID comparable] struct {
	// RecordType identifies this entity type in change records. If empty, derived
	// from T (e.g. Order -> "orders", OrderItem -> "order_items").
	RecordType string

	// MetadataFunc extracts surrounding-context metadata; it does not affect the
	// actor, which is a first-class field resolved from r3.GetActor(ctx).
	MetadataFunc MetadataFunc

	// FixedActor, when set, attributes every change to this actor instead of
	// r3.GetActor(ctx). Use for a system/worker repo whose background contexts
	// carry no principal. Nil resolves the actor per-call from context.
	FixedActor *r3.Actor

	// DiffFunc overrides the default reflection-based Diff[T].
	DiffFunc DiffFunc[T]

	// IDFunc extracts the primary key. Required - needed to fetch old state before
	// updates.
	IDFunc IDFunc[T, ID]

	// ParentRef links this entity to a parent for tree queries; nil omits parent
	// info.
	ParentRef *ParentRef

	// SnapshotRules gate opt-in full-entity snapshots, stored separately from
	// change records with their own trigger conditions. See [SnapshotRule].
	SnapshotRules []SnapshotRule[T]

	// Async records history in a background goroutine; the CRUD op returns
	// immediately and recording errors go to ErrorHandler only (FailOnError is
	// ignored). Default false.
	Async bool

	// ErrorHandler is invoked on a failed change-record write, overriding the
	// default slog logging. It does not control whether the op fails - see
	// FailOnError.
	ErrorHandler func(error)

	// FailOnError (sync mode) returns a failed change-record write's error from the
	// CRUD op, surfacing audit gaps.
	//
	// NOTE: history is recorded AFTER the mutation succeeds, so when this fires the
	// change has already been applied; the error signals it could not be audited,
	// not that the mutation rolled back. For atomic audit, use a transaction.
	// Ignored in async mode. Default false.
	FailOnError bool
}

Options configures a CRUD decorator.

type ParentRef

type ParentRef struct {
	// ParentType is the parent entity's RecordType (e.g. "cities").
	ParentType string

	// FKField is the child's Go struct field holding the parent ID (e.g.
	// "CityID"), read via reflection to populate ParentID.
	FKField string
}

ParentRef links a child entity to its parent so every change record carries the parent's type and ID, enabling ForTree() queries.

type RetentionEnforcer

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

RetentionEnforcer cleans up old change records per a RetentionPolicy.

enforcer := history.NewRetentionEnforcer(historyStore, history.RetentionPolicy{
    MaxAge:      90 * 24 * time.Hour,
    MaxVersions: 100,
})
enforcer.Enforce(ctx, "orders")

func NewRetentionEnforcer

func NewRetentionEnforcer(store r3.CRUD[ChangeRecord, string], policy RetentionPolicy) *RetentionEnforcer

NewRetentionEnforcer creates an enforcer with the given policy.

func (*RetentionEnforcer) Enforce

func (e *RetentionEnforcer) Enforce(ctx context.Context, recordType string) int64

Enforce runs one retention pass for a record type, returning the count deleted.

func (*RetentionEnforcer) Start

func (e *RetentionEnforcer) Start(ctx context.Context, recordType string, interval time.Duration) func()

Start runs periodic enforcement in a background goroutine, returning a stop function that cancels the loop.

type RetentionPolicy

type RetentionPolicy struct {
	// MaxAge deletes records older than this during enforcement.
	MaxAge time.Duration

	// MaxVersions caps versions kept per entity; the oldest are pruned first.
	MaxVersions int64
}

RetentionPolicy defines automatic cleanup rules for old change records. A zero field disables that dimension.

type Reverter

type Reverter[T any, ID comparable] struct {
	// contains filtered or unexported fields
}

Reverter provides revert/undo for a history-tracked entity. It reconstructs historical state purely from field diffs (replaying v1 forward - no snapshots) and applies it via the inner CRUD's Update. Every revert is itself recorded as an ActionRevert change, so the trail stays append-only and a revert is revertible.

func (*Reverter[T, ID]) Reconstruct

func (r *Reverter[T, ID]) Reconstruct(ctx context.Context, recordID string, version int64) (T, error)

Reconstruct rebuilds entity state at a version by replaying FieldChanges from v1 through the target. Purely diff-based: v1 (create) carries every field as the baseline, later versions apply on top.

func (*Reverter[T, ID]) RevertLast

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

RevertLast undoes the most recent change (RevertTo latestVersion-1). Returns ErrNoHistory if there is no history, or an error if only the initial create (version 1) exists.

func (*Reverter[T, ID]) RevertTo

func (r *Reverter[T, ID]) RevertTo(ctx context.Context, id ID, version int64) (T, error)

RevertTo restores an entity to its state at a specific version (reconstructed from diffs) and always records an ActionRevert change.

type Snapshot

type Snapshot struct {
	// ID uniquely identifies this snapshot.
	ID string `json:"id" db:"id,pk" bson:"_id"`

	// RecordType is the entity type name.
	RecordType string `json:"record_type" db:"record_type" bson:"record_type"`

	// RecordID is the entity primary key, stringified.
	RecordID string `json:"record_id" db:"record_id" bson:"record_id"`

	// Version is the change-record version that triggered this snapshot.
	Version int64 `json:"version" db:"version" bson:"version"`

	// RuleName is which SnapshotRule fired (disambiguates multiple rules per type).
	RuleName string `json:"rule_name,omitempty" db:"rule_name" bson:"rule_name,omitempty"`

	// ActorID / ActorType mirror the triggering change record.
	ActorID   string `json:"actor_id,omitempty"   db:"actor_id"   bson:"actor_id,omitempty"`
	ActorType string `json:"actor_type,omitempty" db:"actor_type" bson:"actor_type,omitempty"`

	// Data is the full entity state at snapshot time, JSON bytes for portability.
	Data json.RawMessage `json:"data" db:"data" bson:"data"`

	// Metadata mirrors the change record's context. Stored as JSON.
	Metadata r3.JSONColumn[Metadata] `json:"metadata" db:"metadata" bson:"metadata"`

	// CreatedAt is when the snapshot was taken.
	CreatedAt time.Time `json:"created_at" db:"created_at" bson:"created_at"`
}

Snapshot is a full point-in-time capture of an entity, separate from change records: its own storage, triggered only when a SnapshotRule condition is met (unlike change records, written on every mutation).

type SnapshotConditionFunc

type SnapshotConditionFunc[T any] func(action Action, old, cur T) bool

SnapshotConditionFunc reports whether to snapshot after a mutation. old is zero for create, cur is zero for delete.

func(action Action, old, cur BenefitSheet) bool {
    return old.Status == "draft" && cur.Status == "published"
}

type SnapshotRule

type SnapshotRule[T any] struct {
	// Name identifies this rule (logging and Snapshot.RuleName).
	Name string

	// Store persists snapshots from this rule. Only [r3.Commander] is required -
	// snapshots are write-only here.
	Store r3.Commander[Snapshot, string]

	// Condition gates the snapshot; nil disables the rule.
	Condition SnapshotConditionFunc[T]
}

SnapshotRule defines when and where to snapshot an entity type; multiple rules may exist per entity.

type TreeScope

type TreeScope struct {
	// RecordType is the entity type to match.
	RecordType string

	// RecordID limits to one instance; empty means all of this type.
	RecordID string

	// ParentType scopes to children of a parent type; empty means a root scope.
	ParentType string

	// ParentID scopes to children of a parent instance; empty with a set
	// ParentType means all children of that type.
	ParentID string
}

TreeScope scopes a history query across a parent-child hierarchy. Used with QueryForTree to retrieve changes for a record and all its descendants.

[]TreeScope{
    {RecordType: "cities", RecordID: "5"},
    {RecordType: "locations", ParentType: "cities", ParentID: "5"},
}

Jump to

Keyboard shortcuts

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