Help us shape the future of Go! Take the 2026 Go Developer Survey

activitylog

package module
v0.7.1 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 34 Imported by: 0

README

Arandu Activity Log

The activity log of spatie/laravel-activitylog v5, adapted to Arandu: record what happened, to what, by whom, with what changed — in the tenant of the Grant the act was authorized with.

go get github.com/hyz-is/arandu-activitylog@latest
aru migrate

Wiring

In bootstrap/app.go, beside everything else the application builds:

activity, err := activitylog.New(activitylog.Config{
	Tenant:                  cfg.Auth.Tenant,
	DefaultExceptAttributes: []string{"password", "token_ciphertext"},
	Policy:                  policies.ActivityPolicy{}, // the application's rules
}, db, sessions)
if err != nil {
	return err
}
kernel.Register(activity) // the table, the read routes and the daily clean

commands, _ := activitylog.Commands(activitylog.Deps{
	Service:  activity.Service(),
	Operator: operatorFor, // who a terminal runs as, per tenant
})

The shipped ActivityPolicy denies every read and every clean. Recording needs no policy: an entry is recorded under the Grant of the act it describes.

Logging activity

logger := activity.Logger()

logger.Activity(ctx, g).
	InLog("billing").
	Event("refunded").
	On(invoice).                      // any Hesape model, or OnRef(activitylog.Ref{...})
	By(operator).                     // default: the Grant's subject
	WithProperties(map[string]any{"amount": 1990}).
	Log(":causer.name refunded :subject.number (:properties.amount)")
Spatie Arandu
activity('log') logger.In(ctx, g, "log")
performedOn / on On(record), OnRef(ref)
causedBy / by By(record), ByRef(ref)
causedByAnonymous ByAnonymous()
event, withProperties, withProperty, withChanges, createdAt, tap the same names
when / unless When, Unless
Activity::defaultCauser($m, fn) activitylog.WithCauser(ctx, ref)
withoutLogging(fn) activitylog.WithoutLogging(ctx)
ACTIVITYLOG_ENABLED=false Config.Disabled
Activity::beforeLogging Config.BeforeLogging
LogActivityAction::transformChanges Config.TransformChanges
CauserResolver::resolveUsing Config.ResolveCauser
buffer ctx, buffer := logger.Buffered(ctx) … buffer.Flush(ctx)

Logging a record's changes

A record says how it is logged by implementing ActivityLogOptions, and is saved through the logger, which writes the row and its entry in one transaction:

func (c *Customer) ActivityLogOptions() activitylog.LogOptions {
	return activitylog.DefaultLogOptions().
		LogOnly("name", "email", "address->city").
		LogOnlyDirty().
		DontLogIfAttributesChangedOnly("updated_at").
		UseLogName("customers")
}

logger.Save(ctx, g, customer)    // created or updated
logger.Delete(ctx, g, customer)  // deleted
logger.Restore(ctx, g, customer) // restored, for soft deletes

The entry's attribute_changes is {attributes, old}, as in v5. A record may also implement ActivityEvents() []string (Spatie's $recordEvents), BeforeActivityLogged(a, event) and ActivityLoggingDisabled() bool, and ActivityType() string when its kind is not its table's name.

Spatie hooks Eloquent's events. A Hesape model event hands a callback the row, but not the context or the Grant of the write, so a hook could not write the entry in the same transaction and tenant without hidden state. Saving through the logger is that hook, written where it can be read.

Everything the application already records

Two bridges log what an Arandu application already does, with no change to the code that does it:

// Every committed domain event of the outbox, as an entry of its tenant: its
// name, its aggregate, who authorized it, when. Delivered at least once,
// recorded once.
// recorder is an events.Publisher: hand it to the relay, alone or beside the
// application's other publishers.
recorder := activitylog.NewEventRecorder(activity.Logger())
relay := events.NewRelay(events.NewOutbox(db), recorder, events.RelayOptions{Locker: locker})

// Every message the mailer sends, sent or refused, in the "mail" log of the
// application's own tenant.
transport, _ := activitylog.NewMailTransport(mailTransport(cfg.Mail), activity.Logger(), cfg.Auth.Tenant)
mailer := mail.New(transport, renderer, from)

The entry keeps the envelope: recipients, subject, template tags, transport and the refusal. To keep the rendered body too, hand the transport the application's redactor, which returns the HTML and the text with every secret -- a one-time code, a sign-in link, a token -- replaced, or declines the message:

transport.WithBody(func(m mail.Message) (activitylog.MailBody, bool) {
	return redactByTemplate(m) // the application knows which template carries what
})

The body lands under the properties html and text, with body_redacted. Only what the redactor returns is stored; the message handed on is untouched.

Permissions

activitylog.Actions() are the log's actions; declare them in arandu-permission's catalogue beside the application's own, and pass activitylog.PermissionPolicy{} as Config.Policy so an organization grants reading or cleaning its log to whom it decides.

The platform administrator reads every tenant's log

A user reads their own entries through Config.Policy, in their tenant. The platform's administrator reads every entry of every tenant through ActivityService.ReadAll, which asks Config.PlatformPolicy -- and no other policy -- for activitylog.ActivityViewAll:

activity, err := activitylog.New(activitylog.Config{
	Policy:         activitylog.PermissionPolicy{}, // an organization's own log
	PlatformPolicy: policies.PlatformActivity{},    // the application's rule for its platform managers
}, db, sessions)

page, err := activity.Service().ReadAll(ctx, actor, activitylog.PlatformQuery{
	Tenants: []string{"acme"},                     // empty: every tenant
	Filter:  activitylog.Filter{LogNames: []string{"auth"}},
	Cursor:  cursor,                               // page.NextCursor of the page before
	Limit:   50,
})
// page.Entries[i].Tenant names whose entry it is.

entry, err := activity.Service().ReadEntry(ctx, actor, id) // one entry, of any tenant
A person reads their own entries

The other half of the rule: a person reads what they caused, and what an operator did inside their account, in every tenant, and nobody else's entries. ReadOwn needs no policy and no grant -- every signed-in subject holds it over its own entries -- and narrows the read to them whatever the query says:

page, err := activity.Service().ReadOwn(ctx, subject, activitylog.OwnQuery{
	Filter: activitylog.Filter{LogNames: []string{"billing"}},
	Cursor: cursor,
})
entry, err := activity.Service().ReadOwnEntry(ctx, subject, id) // ErrNotFound when it is not theirs

Nil PlatformPolicy refuses everybody, and so do ActivityPolicy and PermissionPolicy: the read exists once the application writes the rule for its platform administrators. activitylog.PlatformActions() are declared apart from Actions() so no organization's group is ever offered them. A subject inside somebody else's account is refused before any policy. ReadAll answers PlatformEntry snapshots, which hold no connection, so nothing read across tenants can be saved or deleted through them; it pages by the same keyset as List and counts nothing. There is no route for it: the application mounts the platform screen where its platform administrators sign in.

Screens

With Config.Screens, the read routes draw the log -- filtered, paged, with what each entry is about and who caused it -- and one entry with what changed before and after. Publish and compile them, then import the compiled package:

aru vendor:publish --apply
aru view:build

Config.Locale picks the words ("en", "pt-BR"), and Config.Name names a subject or a causer -- a user by their name rather than their identifier.

The screens number their pages with Paginate, which counts every entry the filter keeps on every page drawn: the count grows with the log. With Config.SimplePagination they page with SimplePaginate instead -- Previous and Next, no total -- and cost what the page costs. Publish the views again when turning it on, or an older index view prints "of 0 · 0".

Reading and cleaning

entries, err := activity.Service().List(ctx, actor, activitylog.Filter{
	LogNames: []string{"billing"},           // inLog
	Subject:  activitylog.Ref{Type: "invoices", ID: id}, // forSubject
	Causer:   activitylog.Ref{Type: "user", ID: userID}, // causedBy
	Event:    "refunded",                    // forEvent
}, data.Query{Limit: 50})

// What a person did, and what an operator did inside their account: the
// "mine" a person reads. causer_id or on_behalf_of is the ID.
mine, err := activity.Service().List(ctx, actor, activitylog.Filter{
	Involves: activitylog.Ref{Type: "user", ID: actor.ID},
}, data.Query{Limit: 50})

// Pages for a screen: Paginate counts the log (Page.Pages, Page.Total);
// SimplePaginate does not, and says only Page.HasMore. List pages by a cursor
// -- keyset on (created_at, id) -- and costs the same on any page.
page, err := activity.Service().SimplePaginate(ctx, actor, activitylog.Filter{}, 2, 50)

removed, err := activity.Service().Clean(ctx, actor, 365, "") // activitylog:clean

activitylog:clean {log?} --tenant= --days= --force does the same from a terminal, and the module schedules it daily per tenant (Config.CleanSpec, "-" for none, Config.CleanAfterDays, 365 by default).

An entry recorded while an operator views or acts in somebody's account names the operator as its causer and the account in its on_behalf_of column (and, as before, in the impersonation property). The column was added after v0.3.0 and is not backfilled: older entries are found by Causer, as the operator's, and not by Involves as the account's.

GET /activity-log and GET /activity-log/{id} answer the same reads as JSON, under Config.Prefix. ?involves_type= and ?involves_id= are Filter.Involves.

License

MIT. See LICENSE.md.

Documentation

Overview

Package activitylog is an Arandu module: the activity log of an application, on the pattern of spatie/laravel-activitylog v5, adapted to Arandu.

The files are laid out by role:

module.go   -> registration, read routes, migration and scheduled clean
config.go   -> what the application passes in
model.go    -> the entry, its changes and its properties
logger.go   -> recording: the fluent chain, placeholders, context scopes
record.go   -> logging a record's changes, and the options that shape it
values.go   -> what a value becomes in the log
policy.go   -> who may read and clean
service.go  -> reading and cleaning, after the policy
platform.go -> the read of every tenant, for the platform administrator
commands.go -> what an operator runs from a terminal
bridges.go  -> the domain events and the mail an application already has

What is Spatie's, and what is Arandu's

Everything Spatie v5 records, this records, in the same shape: a log name, a description with :subject, :causer and :properties placeholders, a subject and a causer, an event, the record's change as attribute_changes ({attributes, old}) and free properties. LogOptions are Spatie's: logAll, logOnly with relation and JSON paths, logExcept, logOnlyDirty, dontLogIfAttributesChangedOnly, dontLogEmptyChanges, useLogName, setDescriptionForEvent and useAttributeRawValues; a record may also choose its events, adjust each entry about it and turn its own logging off. The beforeLogging hooks, transformChanges, withoutLogging, defaultCauser, the causer resolver, the buffer and the clean are here too.

Three things differ, each because Arandu asks it:

  • The tenant. Every entry carries the tenant of the Grant it was recorded under, data.Tenant(g), and every read is scoped by the Grant the policy issued. Nothing a caller names chooses whose log an entry lands in.
  • The trigger. Spatie hooks Eloquent's events; a Hesape model event hands a callback the row and neither the context nor the Grant of the write, so a hook could not write in the same transaction and tenant without hidden state. A record's change is logged by saving it through the Logger -- logger.Save(ctx, g, record) in place of record.Save(ctx, g) -- which writes the row and its entry in one transaction.
  • The scopes. Spatie's request-scoped switches and default causer are, in Go, values the context carries: WithoutLogging, WithCauser, Buffered.

Index

Constants

View Source
const (
	// EventsLogName is where EventRecorder writes the domain events.
	EventsLogName = "events"
	// MailLogName is where MailTransport writes the mail.
	MailLogName = "mail"
)

The logs the two bridges write to by default.

View Source
const (
	// DefaultLogName is the log an entry belongs to when nothing names one.
	DefaultLogName = "default"
	// DefaultCleanAfterDays is how old an entry is when the clean removes it.
	DefaultCleanAfterDays = 365
	// DefaultCauserType is the kind a Grant's subject is recorded as.
	DefaultCauserType = "user"
	// DefaultPrefix is where the read routes are mounted when Config leaves
	// Prefix empty.
	DefaultPrefix = "/activity-log"
	// DefaultPageSize is how many entries one page answers with.
	DefaultPageSize = 50
	// MaxPageSize is the ceiling PageSize is refused above.
	MaxPageSize = 200
	// DefaultCleanSpec is when the scheduled clean runs: every day at 03:10.
	DefaultCleanSpec = "10 3 * * *"
)

The defaults for the optional settings. They are constants rather than literals inside Config.withDefaults, so the value a reader finds here is the value the package uses.

View Source
const (
	EventCreated  = "created"
	EventUpdated  = "updated"
	EventDeleted  = "deleted"
	EventRestored = "restored"
)

The events a record's changes are logged under, as Spatie's ActivityEvent names them.

View Source
const (
	// ActivityView is reading one entry, or paging through the log.
	ActivityView security.Action = "activity.view"
	// ActivityClean is removing old entries.
	ActivityClean security.Action = "activity.clean"
	// ActivityViewAll is reading the log of every tenant at once: the platform
	// administrator's, through ActivityService.ReadAll. It is asked of
	// Config.PlatformPolicy and of no other policy, and neither policy this
	// package ships allows it.
	ActivityViewAll security.Action = "activity.view_all"
	// ActivityViewOwn is a person reading their own entries, of every tenant:
	// what they caused, and what an operator did inside their account --
	// ActivityService.ReadOwn. Nobody grants it: every signed-in subject
	// holds it over its own entries, and over nobody else's.
	ActivityViewOwn security.Action = "activity.view_own"
)

The actions the policy answers about. Recording is not one of them: an entry is recorded under the Grant of the act it describes.

View Source
const (
	ViewIndex = "modules.activitylog.index"
	ViewShow  = "modules.activitylog.show"
)

The names the screens are registered under once compiled.

View Source
const ActivityRecord security.Action = "activity.record"

ActivityRecord is the action of the Grants this package records under on its own: the events it is handed and the mail it watches leave. An entry an application records takes the Grant of the act it describes instead.

View Source
const ActivityTable = "activity_log"

ActivityTable is the table this package owns. It is a constant because it is written in the migration, in the model and in the tests, and three spellings of one table disagree on the day one of them is changed.

View Source
const CommandPrefix = "activitylog:"

CommandPrefix is what every command of this package is called under.

View Source
const DateFormat = "2006-01-02T15:04:05.000000Z"

DateFormat is how a time is written in an entry: Laravel's serializeDate.

View Source
const ImpersonationProperty = "impersonation"

ImpersonationProperty is the property an entry recorded while an operator viewed or acted in somebody's account carries: who the operator was, whose account it was, and whether they could write in it.

View Source
const PublishCommand = "aru vendor:publish --apply"

PublishCommand is what an application runs to take the screens over.

Variables

View Source
var (
	// ErrNotFound is returned when no entry matches, including when it exists
	// in another tenant. The two cases are deliberately indistinguishable.
	ErrNotFound = errors.New("activitylog: entry not found")

	// ErrNoGrant is returned when an entry is recorded with a Grant that names
	// no tenant: an entry belongs to somebody, and the Grant is where who comes
	// from.
	ErrNoGrant = errors.New("activitylog: an entry is recorded under a Grant that names its tenant")
)

The errors this package answers with.

DefaultEvents are the events a record logs when its options name none.

Functions

func Actions added in v0.2.0

func Actions() []security.Action

Actions are the actions this package's policy answers about, for an application to declare beside its own -- in arandu-permission's catalogue, so an organization grants reading or cleaning its log to whom it decides, as permission.Actions() is declared.

func Commands

func Commands(deps Deps) ([]console.Command, error)

Commands builds every command of this package.

func PlatformActions added in v0.4.0

func PlatformActions() []security.Action

PlatformActions are the actions of the platform rather than of an organization: ActivityViewAll. They are declared apart from Actions so a catalogue an organization grants from never holds them; an application gives them to its platform administrators only, and its Config.PlatformPolicy decides.

func PublishedPaths added in v0.2.0

func PublishedPaths() []string

PublishedPaths are where the sources land in the application.

func ViewNames added in v0.2.0

func ViewNames() []string

ViewNames are the names the screens register once compiled.

func ViewPackages added in v0.2.0

func ViewPackages() []string

ViewPackages are the compiled packages an application imports so the screens are linked.

func WithCauser

func WithCauser(ctx context.Context, ref Ref) context.Context

WithCauser returns a context whose entries are caused by ref unless a chain names another: Spatie's Activity::defaultCauser. The zero Ref makes them anonymous.

func WithLogging

func WithLogging(ctx context.Context) context.Context

WithLogging undoes WithoutLogging for what the returned context reaches.

func WithoutLogging

func WithoutLogging(ctx context.Context) context.Context

WithoutLogging returns a context under which nothing is recorded -- neither a manual entry nor a record's change: Spatie's withoutLogging, scoped to what the context reaches rather than to a process-wide switch.

Types

type Activity

type Activity struct {
	model.Model

	// ID is a version 7 UUID, so the identifiers of entries written in one
	// instant still sort in the order they were written.
	ID string `db:"id"`

	// TenantID is the customer the entry belongs to. It is written from the
	// Grant the entry was recorded with, never from anything a caller names.
	TenantID string `db:"tenant_id"`

	// LogName is the log the entry belongs to; one table holds as many logs
	// as an application keeps.
	LogName *string `db:"log_name"`

	// Description is what happened, in words, after its placeholders were
	// replaced.
	Description string `db:"description"`

	// SubjectType and SubjectID name what the entry is about.
	SubjectType *string `db:"subject_type"`
	SubjectID   *string `db:"subject_id"`

	// Event is the kind of change, for an entry about a record's change.
	Event *string `db:"event"`

	// CauserType and CauserID name who caused it; both are null for an entry
	// with no causer.
	CauserType *string `db:"causer_type"`
	CauserID   *string `db:"causer_id"`

	// OnBehalfOf is whose account the entry was made in when an operator
	// viewed or acted there: the identifier of the Grant's subject, the
	// account's owner, while CauserID names the operator. It is null on every
	// other entry, and on the entries written before the column existed, which
	// carry the same identifier only in the impersonation property.
	OnBehalfOf *string `db:"on_behalf_of"`

	// AttributeChanges is the JSON of a record's change: what its attributes
	// became and what they were. See Changes.
	AttributeChanges *string `db:"attribute_changes"`

	// PropertiesJSON is the JSON of whatever the caller attached. See
	// Properties.
	PropertiesJSON *string `db:"properties"`

	// CreatedAt is when it happened, in UTC.
	CreatedAt time.Time `db:"created_at"`

	// UpdatedAt is when the entry was last written, in UTC.
	UpdatedAt time.Time `db:"updated_at"`
}

Activity is one entry of the log: something that happened, to what, by whom.

It embeds the model, so a row returned by a query carries its connection. The two JSON columns are kept as text, which every engine stores the same way, and are read and written through the methods below rather than by hand.

func (*Activity) Causer

func (a *Activity) Causer() Ref

Causer is who caused the entry, or the zero Ref for an anonymous one.

func (*Activity) Changes

func (a *Activity) Changes() Changes

Changes returns the record's change the entry carries.

func (*Activity) Fresh

func (row *Activity) Fresh(ctx context.Context, g auth.Grant, with ...string) (*Activity, error)

Fresh reads the row again, as a new Activity; it shadows the untyped one the embedded model promotes.

func (*Activity) GetProperty

func (a *Activity) GetProperty(path string, fallback any) any

GetProperty reads one property by its dot path -- "address.city" -- and answers fallback when the path leads nowhere.

func (Activity) LogValue

func (a Activity) LogValue() slog.Value

LogValue keeps what identifies an entry, and leaves out what it carries: an entry's properties and changes are exactly the data somebody chose to keep out of the ordinary logs.

func (*Activity) Properties

func (a *Activity) Properties() map[string]any

Properties returns what the caller attached to the entry.

func (*Activity) Replicate

func (row *Activity) Replicate(except ...string) (*Activity, error)

Replicate returns an unsaved copy of the row without its key; it shadows the untyped one the embedded model promotes.

func (*Activity) SetChanges

func (a *Activity) SetChanges(changes Changes) error

SetChanges replaces the record's change the entry carries. An empty change is stored as null.

func (*Activity) SetProperties

func (a *Activity) SetProperties(properties map[string]any) error

SetProperties replaces what the entry carries. No property is stored as null.

func (*Activity) SetProperty

func (a *Activity) SetProperty(key string, value any) error

SetProperty adds one property to what the entry carries.

func (*Activity) Subject

func (a *Activity) Subject() Ref

Subject is what the entry is about, or the zero Ref.

type ActivityCollection

type ActivityCollection []*Activity

ActivityCollection is rows of Activity, as a query returns them.

func ActivityCollectionOf

func ActivityCollectionOf(rows model.Rows) ActivityCollection

ActivityCollectionOf converts rows of the table of Activity. A nil input is a nil collection.

func (ActivityCollection) Contains

func (c ActivityCollection) Contains(key any) bool

func (ActivityCollection) Except

func (c ActivityCollection) Except(keys ...any) ActivityCollection

func (ActivityCollection) Find

func (c ActivityCollection) Find(key any) *Activity

func (ActivityCollection) FindOrFail

func (c ActivityCollection) FindOrFail(key any) (*Activity, error)

func (ActivityCollection) Fresh

func (ActivityCollection) Load

func (c ActivityCollection) Load(ctx context.Context, g auth.Grant, relations ...string) error

func (ActivityCollection) LoadCount

func (c ActivityCollection) LoadCount(ctx context.Context, g auth.Grant, relations ...string) error

func (ActivityCollection) LoadMissing

func (c ActivityCollection) LoadMissing(ctx context.Context, g auth.Grant, relations ...string) error

func (ActivityCollection) ModelKeys

func (c ActivityCollection) ModelKeys() []any

func (ActivityCollection) Only

func (c ActivityCollection) Only(keys ...any) ActivityCollection

func (ActivityCollection) Pluck

func (c ActivityCollection) Pluck(column string) []any

func (ActivityCollection) Rows

func (c ActivityCollection) Rows() model.Rows

Rows converts the collection back to the core's rows.

func (ActivityCollection) ToArray

func (c ActivityCollection) ToArray() []map[string]any

func (ActivityCollection) Unique

type ActivityPolicy

type ActivityPolicy struct{}

ActivityPolicy is the policy this package ships, and it denies everything.

An application opens the log by passing its own policy in Config.Policy, beside the rest of its authorization; until it does, nobody reads or cleans the log through this package, which is the state to start from.

func (ActivityPolicy) Can

Can refuses every action. A record of another tenant is refused first, so a policy written by copying this one keeps the line that matters most.

func (ActivityPolicy) Writes added in v0.7.0

func (ActivityPolicy) Writes(a security.Action) bool

Writes reports whether a Grant issued for the action may change anything, so a policy written by copying this one keeps the reads read-only.

type ActivityQuery

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

ActivityQuery is a query on the table of Activity.

The methods that build return the query, so they chain. The methods that run take a context and an auth.Grant, and return Activity rows or a value; there is no way to run one without a Grant.

func Activities

func Activities(db model.DB) *ActivityQuery

Activities starts a query on the table of Activity, through db.

Global scopes are on and, unless the table is Global, every statement is filtered by the tenant of the Grant its terminal takes.

func (*ActivityQuery) AddSelect

func (q *ActivityQuery) AddSelect(columns ...any) *ActivityQuery

func (*ActivityQuery) Avg

func (q *ActivityQuery) Avg(ctx context.Context, g auth.Grant, column any) (any, error)

func (*ActivityQuery) Base

func (q *ActivityQuery) Base() *model.Builder

Base returns the core builder the query forwards to. It is for code in this package -- a local scope in the custom block of model.go -- and for the factory, which builds rows through it.

func (*ActivityQuery) Chunk

func (q *ActivityQuery) Chunk(ctx context.Context, g auth.Grant, count int, callback func(ActivityCollection, int) (bool, error)) error

func (*ActivityQuery) Clone

func (q *ActivityQuery) Clone() *ActivityQuery

Clone returns an independent copy of the query.

func (*ActivityQuery) Count

func (q *ActivityQuery) Count(ctx context.Context, g auth.Grant, columns ...any) (int64, error)

func (*ActivityQuery) Create

func (q *ActivityQuery) Create(ctx context.Context, g auth.Grant, attributes map[string]any) (*Activity, error)

func (*ActivityQuery) Cursor

func (q *ActivityQuery) Cursor(ctx context.Context, g auth.Grant) iter.Seq2[*Activity, error]

func (*ActivityQuery) CursorPaginate

func (q *ActivityQuery) CursorPaginate(ctx context.Context, g auth.Grant, perPage int, cursor *pagination.Cursor, opts pagination.Options, columns ...any) (ActivityCollection, *pagination.CursorPage, error)

func (*ActivityQuery) Decrement

func (q *ActivityQuery) Decrement(ctx context.Context, g auth.Grant, column string, amount any, extra map[string]any) (int64, error)

func (*ActivityQuery) Delete

func (q *ActivityQuery) Delete(ctx context.Context, g auth.Grant) (int64, error)

func (*ActivityQuery) DoesntExist

func (q *ActivityQuery) DoesntExist(ctx context.Context, g auth.Grant) (bool, error)

func (*ActivityQuery) DoesntHave

func (q *ActivityQuery) DoesntHave(relation, boolean string, callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) Each

func (q *ActivityQuery) Each(ctx context.Context, g auth.Grant, count int, callback func(*Activity, int) (bool, error)) error

func (*ActivityQuery) Exists

func (q *ActivityQuery) Exists(ctx context.Context, g auth.Grant) (bool, error)

func (*ActivityQuery) Find

func (q *ActivityQuery) Find(ctx context.Context, g auth.Grant, id any, columns ...any) (*Activity, error)

func (*ActivityQuery) FindMany

func (q *ActivityQuery) FindMany(ctx context.Context, g auth.Grant, ids []any, columns ...any) (ActivityCollection, error)

func (*ActivityQuery) FindOrFail

func (q *ActivityQuery) FindOrFail(ctx context.Context, g auth.Grant, id any, columns ...any) (*Activity, error)

func (*ActivityQuery) FindOrNew

func (q *ActivityQuery) FindOrNew(ctx context.Context, g auth.Grant, id any, columns ...any) (*Activity, error)

func (*ActivityQuery) First

func (q *ActivityQuery) First(ctx context.Context, g auth.Grant, columns ...any) (*Activity, error)

First returns the first row, or nil and no error when there is none.

func (*ActivityQuery) FirstOrCreate

func (q *ActivityQuery) FirstOrCreate(ctx context.Context, g auth.Grant, attributes, values map[string]any) (*Activity, error)

func (*ActivityQuery) FirstOrFail

func (q *ActivityQuery) FirstOrFail(ctx context.Context, g auth.Grant, columns ...any) (*Activity, error)

func (*ActivityQuery) FirstOrNew

func (q *ActivityQuery) FirstOrNew(ctx context.Context, g auth.Grant, attributes, values map[string]any) (*Activity, error)

func (*ActivityQuery) FirstWhere

func (q *ActivityQuery) FirstWhere(ctx context.Context, g auth.Grant, column any, args ...any) (*Activity, error)

func (*ActivityQuery) ForPage

func (q *ActivityQuery) ForPage(page, perPage int) *ActivityQuery

func (*ActivityQuery) ForceCreate

func (q *ActivityQuery) ForceCreate(ctx context.Context, g auth.Grant, attributes map[string]any) (*Activity, error)

func (*ActivityQuery) Get

func (q *ActivityQuery) Get(ctx context.Context, g auth.Grant, columns ...any) (ActivityCollection, error)

Get runs the query and returns the rows.

func (*ActivityQuery) GroupBy

func (q *ActivityQuery) GroupBy(groups ...any) *ActivityQuery

func (*ActivityQuery) Has

func (q *ActivityQuery) Has(relation, operator string, count int, boolean string, callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) Increment

func (q *ActivityQuery) Increment(ctx context.Context, g auth.Grant, column string, amount any, extra map[string]any) (int64, error)

func (*ActivityQuery) Insert

func (q *ActivityQuery) Insert(ctx context.Context, g auth.Grant, values ...map[string]any) (bool, error)

func (*ActivityQuery) InsertOrIgnore

func (q *ActivityQuery) InsertOrIgnore(ctx context.Context, g auth.Grant, values ...map[string]any) (bool, error)

func (*ActivityQuery) Join

func (q *ActivityQuery) Join(table any, first any, args ...any) *ActivityQuery

func (*ActivityQuery) Latest

func (q *ActivityQuery) Latest(column ...string) *ActivityQuery

func (*ActivityQuery) Limit

func (q *ActivityQuery) Limit(value int) *ActivityQuery

func (*ActivityQuery) Max

func (q *ActivityQuery) Max(ctx context.Context, g auth.Grant, column any) (any, error)

func (*ActivityQuery) Min

func (q *ActivityQuery) Min(ctx context.Context, g auth.Grant, column any) (any, error)

func (*ActivityQuery) New

func (q *ActivityQuery) New() (*Activity, error)

New returns an empty Activity on the query's connection, not yet saved, filled with what WithAttributes recorded on the query.

func (*ActivityQuery) Offset

func (q *ActivityQuery) Offset(value int) *ActivityQuery

func (*ActivityQuery) Oldest

func (q *ActivityQuery) Oldest(column ...string) *ActivityQuery

func (*ActivityQuery) OrDoesntHave

func (q *ActivityQuery) OrDoesntHave(relation string) *ActivityQuery

func (*ActivityQuery) OrHas

func (q *ActivityQuery) OrHas(relation, operator string, count int) *ActivityQuery

func (*ActivityQuery) OrWhere

func (q *ActivityQuery) OrWhere(column any, args ...any) *ActivityQuery

OrWhere adds an or-where clause; a func(*ActivityQuery) in place of the column adds a group.

func (*ActivityQuery) OrWhereDoesntHave

func (q *ActivityQuery) OrWhereDoesntHave(relation string, callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) OrWhereHas

func (q *ActivityQuery) OrWhereHas(relation string, callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) OrWhereNot

func (q *ActivityQuery) OrWhereNot(column any, args ...any) *ActivityQuery

OrWhereNot adds an or-joined negated where clause, or a negated group.

func (*ActivityQuery) OrWhereRelation

func (q *ActivityQuery) OrWhereRelation(relation string, column any, args ...any) *ActivityQuery

func (*ActivityQuery) OrderBy

func (q *ActivityQuery) OrderBy(column any, direction ...string) *ActivityQuery

func (*ActivityQuery) OrderByDesc

func (q *ActivityQuery) OrderByDesc(column any) *ActivityQuery

func (*ActivityQuery) Paginate

func (q *ActivityQuery) Paginate(ctx context.Context, g auth.Grant, perPage, page int, opts pagination.Options, columns ...any) (ActivityCollection, *pagination.LengthAwarePage, error)

func (*ActivityQuery) Pluck

func (q *ActivityQuery) Pluck(ctx context.Context, g auth.Grant, column string) ([]any, error)

func (*ActivityQuery) Select

func (q *ActivityQuery) Select(columns ...any) *ActivityQuery

func (*ActivityQuery) SelectRaw

func (q *ActivityQuery) SelectRaw(expression string, bindings ...any) *ActivityQuery

func (*ActivityQuery) SimplePaginate

func (q *ActivityQuery) SimplePaginate(ctx context.Context, g auth.Grant, perPage, page int, opts pagination.Options, columns ...any) (ActivityCollection, *pagination.Page, error)

func (*ActivityQuery) Sole

func (q *ActivityQuery) Sole(ctx context.Context, g auth.Grant, columns ...any) (*Activity, error)

func (*ActivityQuery) SoleValue

func (q *ActivityQuery) SoleValue(ctx context.Context, g auth.Grant, column string) (any, error)

func (*ActivityQuery) Sum

func (q *ActivityQuery) Sum(ctx context.Context, g auth.Grant, column any) (any, error)

func (*ActivityQuery) Touch

func (q *ActivityQuery) Touch(ctx context.Context, g auth.Grant, column ...string) (int64, error)

func (*ActivityQuery) Update

func (q *ActivityQuery) Update(ctx context.Context, g auth.Grant, values map[string]any) (int64, error)

func (*ActivityQuery) UpdateOrCreate

func (q *ActivityQuery) UpdateOrCreate(ctx context.Context, g auth.Grant, attributes, values map[string]any) (*Activity, error)

func (*ActivityQuery) Upsert

func (q *ActivityQuery) Upsert(ctx context.Context, g auth.Grant, values []map[string]any, uniqueBy, update []string) (int64, error)

func (*ActivityQuery) Value

func (q *ActivityQuery) Value(ctx context.Context, g auth.Grant, column string) (any, error)

func (*ActivityQuery) ValueOrFail

func (q *ActivityQuery) ValueOrFail(ctx context.Context, g auth.Grant, column string) (any, error)

func (*ActivityQuery) Where

func (q *ActivityQuery) Where(column any, args ...any) *ActivityQuery

Where adds a where clause; a func(*ActivityQuery) in place of the column adds a group.

func (*ActivityQuery) WhereBelongsTo

func (q *ActivityQuery) WhereBelongsTo(relationshipName string, related ...model.Entity) *ActivityQuery

func (*ActivityQuery) WhereBetween

func (q *ActivityQuery) WhereBetween(column any, from, to any) *ActivityQuery

func (*ActivityQuery) WhereColumn

func (q *ActivityQuery) WhereColumn(first any, args ...any) *ActivityQuery

func (*ActivityQuery) WhereDoesntHave

func (q *ActivityQuery) WhereDoesntHave(relation string, callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) WhereExists

func (q *ActivityQuery) WhereExists(callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) WhereHas

func (q *ActivityQuery) WhereHas(relation string, callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) WhereHasCount

func (q *ActivityQuery) WhereHasCount(relation string, callback func(*query.Builder), operator string, count int) *ActivityQuery

func (*ActivityQuery) WhereIn

func (q *ActivityQuery) WhereIn(column any, values []any) *ActivityQuery

func (*ActivityQuery) WhereKey

func (q *ActivityQuery) WhereKey(id any) *ActivityQuery

func (*ActivityQuery) WhereNot

func (q *ActivityQuery) WhereNot(column any, args ...any) *ActivityQuery

WhereNot adds a negated where clause, or a negated group.

func (*ActivityQuery) WhereNotExists

func (q *ActivityQuery) WhereNotExists(callback func(*query.Builder)) *ActivityQuery

func (*ActivityQuery) WhereNotIn

func (q *ActivityQuery) WhereNotIn(column any, values []any) *ActivityQuery

func (*ActivityQuery) WhereNotNull

func (q *ActivityQuery) WhereNotNull(columns ...any) *ActivityQuery

func (*ActivityQuery) WhereNull

func (q *ActivityQuery) WhereNull(columns ...any) *ActivityQuery

func (*ActivityQuery) WhereRelation

func (q *ActivityQuery) WhereRelation(relation string, column any, args ...any) *ActivityQuery

func (*ActivityQuery) With

func (q *ActivityQuery) With(relations ...string) *ActivityQuery

func (*ActivityQuery) WithAvg

func (q *ActivityQuery) WithAvg(relation, column string) *ActivityQuery

func (*ActivityQuery) WithCount

func (q *ActivityQuery) WithCount(relations ...string) *ActivityQuery

func (*ActivityQuery) WithExists

func (q *ActivityQuery) WithExists(relation string) *ActivityQuery

func (*ActivityQuery) WithMax

func (q *ActivityQuery) WithMax(relation, column string) *ActivityQuery

func (*ActivityQuery) WithMin

func (q *ActivityQuery) WithMin(relation, column string) *ActivityQuery

func (*ActivityQuery) WithSum

func (q *ActivityQuery) WithSum(relation, column string) *ActivityQuery

func (*ActivityQuery) Without

func (q *ActivityQuery) Without(relations ...string) *ActivityQuery

func (*ActivityQuery) WithoutGlobalScope

func (q *ActivityQuery) WithoutGlobalScope(identifier string) *ActivityQuery

func (*ActivityQuery) WithoutGlobalScopes

func (q *ActivityQuery) WithoutGlobalScopes(identifiers ...string) *ActivityQuery

type ActivityService

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

ActivityService reads and cleans the log, after its policy answers.

It is the half of the package that a person reaches -- a screen, a terminal -- and every method authorizes before the first statement. The Logger is the other half, reached by code that already holds the Grant of what it records.

func NewActivityService

func NewActivityService(db *data.DB, cfg Config) (*ActivityService, error)

NewActivityService wires the service over the application's database handle and the configured policy, ActivityPolicy when the configuration names none.

func (*ActivityService) CausedBy added in v0.2.0

func (s *ActivityService) CausedBy(ctx context.Context, actor security.Subject, record Record, q data.Query) ([]*Activity, error)

CausedBy pages through the entries a record caused, newest first -- Spatie's activitiesAsCauser.

func (*ActivityService) Clean

func (s *ActivityService) Clean(ctx context.Context, actor security.Subject, days int, logName string) (int64, error)

Clean removes the entries older than days -- the configured CleanAfterDays when days is zero -- from one log, or from every log when logName is empty: Spatie's activitylog:clean. It answers how many were removed.

func (*ActivityService) Count

func (s *ActivityService) Count(ctx context.Context, actor security.Subject, filter Filter) (int64, error)

Count is how many entries a filter keeps.

func (*ActivityService) Find

func (s *ActivityService) Find(ctx context.Context, actor security.Subject, id string) (*Activity, error)

Find reads one entry.

func (*ActivityService) ForSubject added in v0.2.0

func (s *ActivityService) ForSubject(ctx context.Context, actor security.Subject, record Record, q data.Query) ([]*Activity, error)

ForSubject pages through the entries about a record, newest first -- Spatie's activitiesAsSubject.

func (*ActivityService) List

func (s *ActivityService) List(ctx context.Context, actor security.Subject, filter Filter, q data.Query) ([]*Activity, error)

List pages through the log, newest first: by when each entry happened, and by identifier within one instant. The cursor is the identifier of the last entry of the previous page.

func (*ActivityService) Paginate

func (s *ActivityService) Paginate(ctx context.Context, actor security.Subject, filter Filter, page, perPage int) (Page, error)

Paginate reads one numbered page of the log, newest first: Eloquent's paginate, which Spatie's activity queries end in. A page past the last is the last.

func (*ActivityService) ReadAll added in v0.4.0

ReadAll pages through the log of every tenant, or of the tenants q names, newest first: the platform administrator's read, who reads every entry where a user reads only their own.

It asks Config.PlatformPolicy, and no other policy, for ActivityViewAll. Nil there refuses everybody, as the two policies this package ships do, so the read exists only once the application wrote the rule that opens it for its platform administrators. A subject inside somebody else's account is refused before any policy: an operator acts there as the account, and the account reads no other tenant.

It pages by a keyset on (created_at, id), the order List pages in, and counts nothing. The cursor is the identifier of the last entry of the previous page; a page that ends on an entry removed since then is the last.

func (*ActivityService) ReadEntry added in v0.4.1

func (s *ActivityService) ReadEntry(ctx context.Context, actor security.Subject, id string) (PlatformEntry, error)

ReadEntry is one entry of any tenant's log, by its identifier: the platform administrator's read of a single entry, as ReadAll is of a page. It asks Config.PlatformPolicy for ActivityViewAll, refuses a subject inside somebody else's account before any policy, and answers ErrNotFound when no tenant holds the entry. The answer is a PlatformEntry snapshot, which holds no connection.

func (*ActivityService) ReadOwn added in v0.4.2

ReadOwn pages through a person's own entries in every tenant, newest first: what the acting subject caused, and what an operator did inside its account. It is the read a person has of their history -- "a user reads only their own entries" -- beside the platform administrator's ReadAll.

Every signed-in subject may read its own entries and nobody else's: the read is narrowed to Filter.Involves naming the subject, as the Grant's subject, and q.Filter.Involves is replaced. An operator inside somebody's account reads the account's, which is what the account's owner sees. A guest is refused.

func (*ActivityService) ReadOwnEntry added in v0.4.2

func (s *ActivityService) ReadOwnEntry(ctx context.Context, actor security.Subject, id string) (PlatformEntry, error)

ReadOwnEntry is one of a person's own entries, of any tenant: ErrNotFound for an entry that does not exist and for one that is somebody else's alike, so the answer says nothing about entries the reader may not see.

func (*ActivityService) SimplePaginate added in v0.4.0

func (s *ActivityService) SimplePaginate(ctx context.Context, actor security.Subject, filter Filter, page, perPage int) (Page, error)

SimplePaginate reads one numbered page of the log, newest first, without counting the log: Eloquent's simplePaginate. It reads one entry past the page to say whether another follows, and leaves Pages and Total at zero.

Paginate counts every entry the filter keeps, on every page, and that count grows with the log; this one costs what the page costs. A page past the last is empty rather than the last, since nothing says where the last is. List, which pages by a cursor rather than an offset, costs the same on the thousandth page as on the first.

type BeforeActivityLogged

type BeforeActivityLogged interface {
	BeforeActivityLogged(a *Activity, event string)
}

BeforeActivityLogged is implemented by a record that adjusts every entry about it, manual or automatic, right before it is written.

type Buffer

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

Buffer holds the entries recorded under a context made by Buffered, and writes them together when flushed: Spatie's buffer, scoped to the context rather than to the request lifecycle.

func (*Buffer) Flush

func (b *Buffer) Flush(ctx context.Context) error

Flush writes every waiting entry in one transaction, each under the Grant it was recorded with, and empties the buffer. When the write fails, the entries are kept for the next flush, as Spatie's buffer keeps them. ctx should not be the buffered context: the writes are the flush's own.

func (*Buffer) Len

func (b *Buffer) Len() int

Len is how many entries are waiting.

type ChangeRow added in v0.2.0

type ChangeRow struct{ Attribute, Old, New string }

ChangeRow is one attribute of a record's change.

type Changes

type Changes struct {
	Attributes map[string]any `json:"attributes,omitempty"`
	Old        map[string]any `json:"old,omitempty"`
}

Changes is a record's change: Attributes is what its logged attributes became, and Old is what they were. A created or restored record carries only Attributes; a deleted one only Old.

func (Changes) IsEmpty

func (c Changes) IsEmpty() bool

IsEmpty reports whether the change carries nothing.

type Collection

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

Collection is one page of entries, and the cursor of the next page.

func (Collection) ToArray

func (c Collection) ToArray() map[string]any

ToArray returns the page.

func (Collection) With

func (c Collection) With() map[string]any

With is the framework's envelope hook; a collection adds nothing to it.

type Config

type Config struct {
	// Tenant is the customer a visitor with no session is read as, on the
	// read routes. It comes from the application's own configuration, never
	// from the request.
	Tenant string

	// Disabled turns recording off. Nothing is written while it is true, and
	// Log answers a nil entry, as Spatie's ACTIVITYLOG_ENABLED=false does.
	Disabled bool

	// DefaultLogName is the log an entry belongs to when nothing names one.
	// Empty means DefaultLogName.
	DefaultLogName string

	// CleanAfterDays is how old an entry is when the clean removes it. Zero
	// means DefaultCleanAfterDays.
	CleanAfterDays int

	// CleanSpec is the five-field cron expression the scheduled clean runs
	// on. Empty means DefaultCleanSpec; "-" schedules no clean.
	CleanSpec string

	// CleanTenants are tenants the scheduled clean also reaches, beside the
	// ones the scheduler lists for per-tenant work: the application's own
	// tenant, where the mail log lives, is usually one.
	CleanTenants []string

	// DefaultExceptAttributes are never logged from any record, whatever its
	// options say: the place for a password hash, a token, a secret.
	DefaultExceptAttributes []string

	// CauserType is the kind the Grant's subject is recorded as when an entry
	// names no causer. Empty means DefaultCauserType.
	CauserType string

	// ResolveCauser decides who caused an entry that names no causer, in
	// place of the Grant's subject -- Spatie's CauserResolver::resolveUsing.
	// It may answer the zero Ref for an anonymous entry.
	ResolveCauser func(ctx context.Context, g security.Grant) Ref

	// BeforeLogging runs on every entry, in order, right before it is
	// written -- Spatie's Activity::beforeLogging. It may change anything on
	// the entry, such as adding the request's address to its properties. An
	// error stops the entry and is answered to the caller.
	BeforeLogging []func(ctx context.Context, a *Activity) error

	// TransformChanges runs on every entry that carries a record's change,
	// before BeforeLogging -- Spatie's LogActivityAction::transformChanges,
	// the place to redact a field.
	TransformChanges func(a *Activity)

	// Policy decides who may read and clean the log. Nil is ActivityPolicy,
	// which denies everything: a wiring that says nothing about rules gets no
	// access at all, which is the state to start from. Recording needs no
	// policy: it takes the Grant the caller's own action was authorized with.
	Policy security.Policy[Activity]

	// PlatformPolicy decides who reads the log of every tenant at once --
	// ActivityService.ReadAll, under ActivityViewAll: the platform
	// administrator, who reads every entry where a user reads only their own.
	// It is the only policy ReadAll asks, so a Policy that opens the log of an
	// organization, however widely, opens nothing across organizations. Nil
	// refuses everybody, and so do ActivityPolicy and PermissionPolicy: the
	// application writes this one for its platform administrators.
	PlatformPolicy security.Policy[Activity]

	// Prefix is the path the read routes are mounted under. Empty means
	// DefaultPrefix.
	Prefix string

	// Screens draws the read routes as pages -- the log and one entry -- for
	// a request that does not ask for JSON. The application publishes and
	// compiles the views first (see PublishCommand); Boot refuses to start
	// without them.
	Screens bool

	// Locale is the language of the screens: "en" or "pt-BR". Empty is "en".
	Locale string

	// Name names a subject or a causer on the screens -- a user by their name
	// rather than their identifier. Nil, or an empty answer, shows the kind and
	// a short identifier.
	Name func(ctx context.Context, ref Ref) string

	// PageSize is how many entries one page answers with. Zero means
	// DefaultPageSize, and anything above MaxPageSize is refused.
	PageSize int

	// SimplePagination pages the screens with SimplePaginate: Previous and
	// Next, and no page count or total. Paginate counts every entry of the
	// tenant's log the filter keeps on every page drawn, and that count grows
	// with the log; this costs what the page costs. False keeps the numbered
	// pages. The JSON route pages by cursor either way and counts nothing.
	//
	// A published index view older than this option prints "of 0 · 0" under
	// it; publishing the views again draws the page without the count.
	SimplePagination bool
}

Config is what the application passes when it wires this package: Spatie's config/activitylog.php, as a typed struct. A misspelled key in a map is a setting that silently keeps its default; a field that does not exist does not compile.

func (Config) Validate

func (c Config) Validate() error

Validate reports what the configuration cannot be used with.

type Deps

type Deps struct {
	// Service is the same service the routes call, so a clean from a terminal
	// passes the same policy as one from anywhere else.
	Service *ActivityService

	// Operator says who a command runs as, for the customer it names. A
	// command has no session; who it runs as is the application's answer.
	Operator func(tenant string) security.Subject
}

Deps is what the commands need, built by the application where it wires everything else.

func (Deps) Validate

func (d Deps) Validate() error

Validate reports what the dependencies cannot be used with.

type DoesNotRecordEvents added in v0.2.0

type DoesNotRecordEvents interface {
	ActivityEventsNotRecorded() []string
}

DoesNotRecordEvents is implemented by a record that leaves some events out of the ones it records -- Spatie's static $doNotRecordEvents.

type EventRecorder

type EventRecorder struct {

	// LogName decides the log of an event; nil writes every event to
	// EventsLogName.
	LogName func(e events.Stored) string
	// Describe decides the description of an event; nil uses its name.
	Describe func(e events.Stored) string
	// Payload keeps the event's payload in the entry's properties, under
	// "payload". It is off by default: a payload is whatever the producer
	// stored, and the log is read by more people than the outbox is.
	Payload bool
	// contains filtered or unexported fields
}

EventRecorder writes every committed domain event of the outbox into the activity log, so everything an application already records as an event -- in its own transaction, with the tenant, the subject whose Grant authorized it and the action -- is also in the log, read by Spatie's scopes.

It is an events.Publisher: the relay hands it each event once the write that produced it committed. The relay delivers at least once, so the entry takes the event's identifier and a second delivery finds it written and does nothing.

func NewEventRecorder

func NewEventRecorder(logger *Logger) *EventRecorder

NewEventRecorder returns the recorder over the logger.

func (*EventRecorder) Publish

func (r *EventRecorder) Publish(ctx context.Context, e events.Stored) error

Publish writes one committed event as an entry of its tenant: the event's name, its aggregate as the subject, the subject that authorized it as the causer, and when it occurred.

type Filter

type Filter struct {
	// LogNames keeps the entries of these logs.
	LogNames []string
	// Event keeps the entries of this event.
	Event string
	// Subject keeps the entries about this thing; a Ref with only a Type
	// keeps every entry about that kind.
	Subject Ref
	// Causer keeps the entries caused by this one; a Ref with only a Type
	// keeps every entry caused by that kind.
	Causer Ref
	// Involves keeps the entries this one caused and the entries an operator
	// made inside its account -- causer_id, or on_behalf_of, is its ID: what a
	// person reads as "what was done by me or for me". Its Type narrows the
	// causer as Causer's does; on_behalf_of names an account of the kind
	// Config.CauserType records, so a Type of another kind matches only as
	// causer. A Ref with no ID narrows nothing.
	//
	// The column on_behalf_of is not backfilled: an entry made inside an
	// account before the column was added is found as the operator's, by
	// Causer, and not as the account's.
	Involves Ref
	// Since and Until bound when the entries happened, Since inclusive and
	// Until exclusive.
	Since, Until time.Time
	// Search keeps the entries whose description contains this.
	Search string
}

Filter narrows a listing: Spatie's inLog, causedBy, forSubject and forEvent scopes, and a window of time. Every field left empty narrows nothing.

type ForceDeletable added in v0.2.0

type ForceDeletable interface {
	Record
	ForceDelete(ctx context.Context, g security.Grant) (bool, error)
}

ForceDeletable is a record of a table with soft deletes that can be removed for good.

type IndexPageData added in v0.2.0

type IndexPageData struct {
	view.Page
	Prefix, Log, Event, Search string
	Labels                     Labels
	Rows                       []Row
	PageNumber, Pages, Total   int
	PreviousURL, NextURL       string
	// Counted says Pages and Total were counted. Under
	// Config.SimplePagination they were not, and the page draws only its
	// number.
	Counted bool
}

IndexPageData is the log, one page of it.

type Labels added in v0.2.0

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

Labels are the words of the screens, in one locale.

func LabelsFor added in v0.2.0

func LabelsFor(locale string) Labels

LabelsFor are the words of a locale this package carries; English for any other.

func (Labels) T added in v0.2.0

func (l Labels) T(key string) string

T is the word for key, or the key itself when the locale has none.

type LogOptions

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

LogOptions say which of a record's attributes are logged and when: Spatie's LogOptions, as a value built by chaining.

func DefaultLogOptions

func DefaultLogOptions() LogOptions

DefaultLogOptions logs the events with no attribute, and logs an entry even when its change is empty.

func (LogOptions) DontLogEmptyChanges

func (o LogOptions) DontLogEmptyChanges() LogOptions

DontLogEmptyChanges skips an entry whose change carries nothing.

func (LogOptions) DontLogIfAttributesChangedOnly

func (o LogOptions) DontLogIfAttributesChangedOnly(attributes ...string) LogOptions

DontLogIfAttributesChangedOnly skips an update that changed nothing but the named attributes -- typically updated_at.

func (LogOptions) LogAll

func (o LogOptions) LogAll() LogOptions

LogAll logs every attribute.

func (LogOptions) LogEmptyChanges

func (o LogOptions) LogEmptyChanges() LogOptions

LogEmptyChanges logs an entry even when its change carries nothing.

func (LogOptions) LogExcept

func (o LogOptions) LogExcept(attributes ...string) LogOptions

LogExcept never logs the named attributes.

func (LogOptions) LogOnly

func (o LogOptions) LogOnly(attributes ...string) LogOptions

LogOnly logs the named attributes, in place of any named before, as Spatie's logOnly replaces. "a.b" reads b on the relation a and is kept under the flat key "a.b"; "a->b" reads b inside the JSON column a and is kept nested.

func (LogOptions) LogOnlyDirty

func (o LogOptions) LogOnlyDirty() LogOptions

LogOnlyDirty keeps, in an update, only the attributes whose value changed.

func (LogOptions) SetDescriptionForEvent

func (o LogOptions) SetDescriptionForEvent(fn func(event string) string) LogOptions

SetDescriptionForEvent describes the record's entries; the default is the event's name. An empty description skips the entry.

func (LogOptions) UseAttributeRawValues

func (o LogOptions) UseAttributeRawValues(attributes ...string) LogOptions

UseAttributeRawValues keeps the named attributes as the record holds them, without the normalization every other value gets.

func (LogOptions) UseLogName

func (o LogOptions) UseLogName(logName string) LogOptions

UseLogName puts the record's entries in the named log.

type Logger

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

Logger records entries: Spatie's ActivityLogger, reached by a value the application holds rather than by a global helper.

An entry is recorded under the Grant the caller's own action was authorized with. That is the whole of its authorization, and it is enough: the entry is a side effect of an act the policy already allowed, it lands in that act's tenant -- data.Tenant(g), never a tenant somebody names -- and in that act's transaction when the context carries one. Reading and cleaning the log are separate acts with their own policy; see ActivityService.

func NewLogger

func NewLogger(db *data.DB, cfg Config) (*Logger, error)

NewLogger wires the logger over the application's database handle.

func (*Logger) Activity

func (l *Logger) Activity(ctx context.Context, g security.Grant) *PendingActivity

Activity starts an entry in the default log, recorded under g: Spatie's activity(). Nothing is written until Log.

func (*Logger) BufferRequests added in v0.2.0

func (l *Logger) BufferRequests(onError func(r *http.Request, err error)) func(http.Handler) http.Handler

BufferRequests is a middleware that buffers what each request records and flushes it once the response is written -- Spatie's flush on terminate. A failed flush is reported to onError, which may be nil.

func (*Logger) Buffered

func (l *Logger) Buffered(ctx context.Context) (context.Context, *Buffer)

Buffered returns a context whose entries are kept rather than written, and the buffer that keeps them. Flush writes them; an entry kept and never flushed is never written.

func (*Logger) Delete

func (l *Logger) Delete(ctx context.Context, g security.Grant, record Record) (bool, error)

Delete deletes the record under g and logs it, with what its logged attributes were, in one transaction. On a table with soft deletes the row stays, stamped, and the entry is the same.

func (*Logger) Enabled

func (l *Logger) Enabled(ctx context.Context) bool

Enabled says whether an entry recorded with this context would be written.

func (*Logger) ForceDelete added in v0.2.0

func (l *Logger) ForceDelete(ctx context.Context, g security.Grant, record ForceDeletable) (bool, error)

ForceDelete removes the record for good under g and logs it as deleted, as Spatie logs a forceDelete.

func (*Logger) In

func (l *Logger) In(ctx context.Context, g security.Grant, logName string) *PendingActivity

In starts an entry in the named log: Spatie's activity('name'). An empty name is the default log, as a falsy name is in Spatie.

func (*Logger) Restore

func (l *Logger) Restore(ctx context.Context, g security.Grant, record Restorable) (bool, error)

Restore restores a soft-deleted record under g and logs it, in one transaction.

func (*Logger) Save

func (l *Logger) Save(ctx context.Context, g security.Grant, record Record) (bool, error)

Save saves the record under g and logs its creation or its update, in one transaction: the row and the entry about it are written together or not at all. It answers what the record's own Save answers.

What the attributes were is read from the row as stored before the write, and what they became is read back after it, both with the relations the options name -- Spatie's getRawOriginal and fresh(). Clearing deleted_at is a restore, and is not logged as an update, as Spatie's isRestoring.

func (*Logger) WithBuffer added in v0.2.0

func (l *Logger) WithBuffer(ctx context.Context, fn func(ctx context.Context) error) error

WithBuffer runs fn under a buffered context and flushes what it recorded when fn returns, whether or not it failed -- Spatie's flush after a job, processed or failed. The flush's error is answered when fn's is nil.

type LoggingDisabled

type LoggingDisabled interface {
	ActivityLoggingDisabled() bool
}

LoggingDisabled is implemented by a record that can turn its own logging off -- Spatie's $model->disableLogging().

type LogsActivity

type LogsActivity interface {
	ActivityLogOptions() LogOptions
}

LogsActivity is implemented by a record that says how its changes are logged -- Spatie's getActivitylogOptions. A record that does not implement it logs its events with no attribute.

type MailBody added in v0.6.0

type MailBody struct {
	HTML, Text string
}

MailBody is what of a rendered message a MailTransport keeps in its entry: the HTML and the text, as a MailRedactor returned them.

type MailRedactor added in v0.6.0

type MailRedactor func(m mail.Message) (MailBody, bool)

MailRedactor decides what of a rendered message the mail log may keep: the body with every secret it carries -- a one-time code, a sign-in link, a token -- replaced, and whether to keep it at all. It is the application's, because only the application knows which of its templates carries a secret and in which field; the log is read by more people than the mailbox is.

type MailTransport

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

MailTransport is a mail transport that writes every message it is handed into the activity log of one tenant -- the mail log -- and hands it on. A message the next transport refused is logged as failed, with the reason, and the refusal is answered to the sender as it would have been.

func NewMailTransport

func NewMailTransport(next mail.Transport, logger *Logger, tenant string) (*MailTransport, error)

NewMailTransport watches next, writing to the log of tenant -- the application's own, from its configuration, never from a message.

func (*MailTransport) Name

func (t *MailTransport) Name() string

Name is the watched transport's.

func (*MailTransport) Send

func (t *MailTransport) Send(ctx context.Context, m mail.Message) error

Send hands the message on, then writes the entry. A failure to write the entry is not the message's failure: the message is already gone.

func (*MailTransport) Unwrap

func (t *MailTransport) Unwrap() mail.Transport

Unwrap is the watched transport.

func (*MailTransport) WithBody added in v0.6.0

func (t *MailTransport) WithBody(redact MailRedactor) *MailTransport

WithBody makes the transport keep each message's rendered body, as redact returns it, under the entry's properties "html" and "text", beside "body_redacted": true. A message redact declines keeps the envelope alone, and so does every message of a transport without one, as before.

type Module

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

Module is what the application registers.

func New

func New(cfg Config, db *data.DB, sessions *security.SessionStore) (*Module, error)

New returns the module, or the reason it cannot be built. sessions is where the read routes take their subject from; it may be nil for an application that mounts no route of this package.

func (*Module) Boot added in v0.2.0

func (m *Module) Boot(context.Context) error

Boot refuses to start an application that asked for the screens and did not publish and compile them: one refusal at start-up that names the views and the command, rather than an error on the first page somebody opened.

func (*Module) Logger

func (m *Module) Logger() *Logger

Logger records entries. It is the one Logger of the module, so an entry recorded by code and one read through a route are the same rows.

func (*Module) Migrations

func (m *Module) Migrations() []foundation.Migration

Migrations declares the schema this module owns.

func (*Module) Name

func (m *Module) Name() string

Name is the module identifier.

func (*Module) Publishes added in v0.2.0

func (m *Module) Publishes() []foundation.Publication

Publishes declares the screens' sources for `aru vendor:publish`.

func (*Module) Routes

func (m *Module) Routes(r *fhttp.Router)

Routes registers the read routes under the configured prefix: the log, one entry. They answer JSON, and the policy decides who may read.

func (*Module) Schedule

func (m *Module) Schedule() []foundation.Task

Schedule declares the daily clean, per tenant, under the clean action: Spatie's scheduled activitylog:clean. CleanSpec "-" declares none. The tenants in CleanTenants are cleaned by a second task, once each.

func (*Module) Service

func (m *Module) Service() *ActivityService

Service reads and cleans the log.

type OwnQuery added in v0.4.2

type OwnQuery struct {
	Filter
	// Cursor is NextCursor of the page before; empty is the first page.
	Cursor string
	// Limit is how many entries a page holds; zero is the default.
	Limit int
}

OwnQuery is what ReadOwn reads: the narrowing every listing takes and the page. Filter.Involves is the reader's, whatever it says.

type Page

type Page struct {
	Items              []*Activity
	Page, Pages, Total int
	// HasMore says a page follows this one.
	HasMore bool
}

Page is one numbered page of the log and where it sits among the rest -- what a screen with page numbers draws.

Paginate fills every field. SimplePaginate counts nothing, so it leaves Pages and Total at zero and says only whether a page follows.

type PendingActivity

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

PendingActivity is an entry being described: Spatie's PendingActivityLog. Every method writes into the entry at once and returns the chain, so a tap sees what the chain set before it, and a method after a tap overrides it, as in Spatie. Log writes it and starts a fresh one.

It is not safe for concurrent use; it is meant to live for one chain.

func (*PendingActivity) By

func (p *PendingActivity) By(record Record) *PendingActivity

By says who caused the entry: a record, whose kind and key become the causer and whose attributes the :causer placeholders read. A nil record changes nothing, as causedBy(null) does in Spatie. CausedBy is the same method under Spatie's other name.

func (*PendingActivity) ByAnonymous

func (p *PendingActivity) ByAnonymous() *PendingActivity

ByAnonymous records the entry with no causer, whoever holds the Grant. CausedByAnonymous is the same method under Spatie's other name.

func (*PendingActivity) ByRef

func (p *PendingActivity) ByRef(ref Ref) *PendingActivity

ByRef says who caused the entry by reference.

func (*PendingActivity) CausedBy

func (p *PendingActivity) CausedBy(record Record) *PendingActivity

CausedBy is By.

func (*PendingActivity) CausedByAnonymous

func (p *PendingActivity) CausedByAnonymous() *PendingActivity

CausedByAnonymous is ByAnonymous.

func (*PendingActivity) CreatedAt

func (p *PendingActivity) CreatedAt(at time.Time) *PendingActivity

CreatedAt dates the entry; the default is now.

func (*PendingActivity) Entry added in v0.2.0

func (p *PendingActivity) Entry() *Activity

Entry is the entry being described, for what the chain has no method for.

func (*PendingActivity) Event

func (p *PendingActivity) Event(event string) *PendingActivity

Event names the kind of change the entry records. SetEvent is the same method under Spatie's other name.

func (*PendingActivity) InLog

func (p *PendingActivity) InLog(logName string) *PendingActivity

InLog puts the entry in the named log; an empty name leaves the entry with no log, as Spatie's useLog(null). UseLog is the same method under Spatie's other name.

func (*PendingActivity) Log

func (p *PendingActivity) Log(description string) (*Activity, error)

Log writes the entry and answers it. Its description is one a tap already set or else description, with its placeholders replaced. While logging is off -- by configuration or by WithoutLogging -- nothing is written, the answer is nil with no error, and the entry being described is kept, as in Spatie.

Inside a context made by Buffered the entry is kept and written when the buffer is flushed; its identifier is already set.

func (*PendingActivity) On

func (p *PendingActivity) On(record Record) *PendingActivity

On says what the entry is about: a record, whose kind and key become the subject and whose attributes the :subject placeholders read. PerformedOn is the same method under Spatie's other name.

func (*PendingActivity) OnRef

func (p *PendingActivity) OnRef(ref Ref) *PendingActivity

OnRef says what the entry is about by reference, for a thing that is not a row of this database.

func (*PendingActivity) PerformedOn

func (p *PendingActivity) PerformedOn(record Record) *PendingActivity

PerformedOn is On.

func (*PendingActivity) SetEvent added in v0.2.0

func (p *PendingActivity) SetEvent(event string) *PendingActivity

SetEvent is Event.

func (*PendingActivity) Tap

func (p *PendingActivity) Tap(fn func(a *Activity, event string)) *PendingActivity

Tap runs fn on the entry now, with the chain's event -- the place to set anything the chain has no method for. As in Spatie, a method called after the tap overrides what the tap set.

func (*PendingActivity) Unless

func (p *PendingActivity) Unless(condition bool, fn func(*PendingActivity) *PendingActivity, otherwise ...func(*PendingActivity) *PendingActivity) *PendingActivity

Unless applies fn to the chain when condition does not hold, and otherwise the optional default.

func (*PendingActivity) UseLog

func (p *PendingActivity) UseLog(logName string) *PendingActivity

UseLog is InLog.

func (*PendingActivity) When

func (p *PendingActivity) When(condition bool, fn func(*PendingActivity) *PendingActivity, otherwise ...func(*PendingActivity) *PendingActivity) *PendingActivity

When applies fn to the chain when condition holds, and otherwise the optional default -- Spatie's Conditionable.

func (*PendingActivity) WithChanges

func (p *PendingActivity) WithChanges(changes Changes) *PendingActivity

WithChanges sets the record's change the entry carries.

func (*PendingActivity) WithProperties

func (p *PendingActivity) WithProperties(properties map[string]any) *PendingActivity

WithProperties replaces what the entry carries.

func (*PendingActivity) WithProperty

func (p *PendingActivity) WithProperty(key string, value any) *PendingActivity

WithProperty adds one property to what the entry carries.

type PermissionPolicy added in v0.2.0

type PermissionPolicy struct{}

PermissionPolicy decides by the subject's own actions, which arandu-permission fills: it allows an action the subject was granted, on an entry of the subject's tenant. It is the policy for an application that administers its permissions there.

func (PermissionPolicy) Can added in v0.2.0

Can allows a granted action on an entry of the subject's tenant. It never allows ActivityViewAll: an action an organization's group could be given is not one that reads every organization, and the platform read is opened only by a policy the application writes for its platform administrators.

func (PermissionPolicy) Writes added in v0.7.0

Writes reports whether a Grant issued for the action may change anything. Only ActivityClean writes; every read is issued a Grant that only reads.

type PlatformEntry added in v0.4.0

type PlatformEntry struct {
	// Tenant is the customer the entry belongs to -- what a platform screen
	// shows beside it, and the one field no tenant's own listing answers.
	Tenant      string
	ID          string
	LogName     string
	Description string
	Event       string
	Subject     Ref
	Causer      Ref
	// OnBehalfOf is whose account an operator made the entry in, or empty.
	OnBehalfOf string
	Changes    Changes
	Properties map[string]any
	CreatedAt  time.Time
}

PlatformEntry is one entry of any tenant's log as ReadAll answers it.

It is a snapshot and not an Activity: an Activity carries the connection it was read with, and one read without the tenant filter could be saved or deleted through it in a tenant nobody authorized. A PlatformEntry holds no connection, so the platform read stays a read.

type PlatformPage added in v0.4.0

type PlatformPage struct {
	Entries    []PlatformEntry
	NextCursor string
}

PlatformPage is one page of ReadAll, newest first, and the cursor of the next. NextCursor is empty on the last page.

type PlatformQuery added in v0.4.0

type PlatformQuery struct {
	Filter
	// Tenants keeps the entries of these tenants. Empty keeps every tenant's.
	Tenants []string
	// Cursor is the NextCursor of the previous page; empty is the first.
	Cursor string
	// Limit is how many entries a page holds: zero means 50, and more than 200
	// is 200.
	Limit int
}

PlatformQuery is what ReadAll reads: the narrowing every listing takes, the tenants to keep, and the page.

type PropertyRow added in v0.2.0

type PropertyRow struct{ Key, Value string }

PropertyRow is one property of an entry.

type Record

type Record interface {
	model.Entity
	Exists() bool
	GetKey() any
	Table() *model.Table
	GetAttributes() map[string]any
	GetOriginal() map[string]any
	GetDirty() map[string]any
	ToArray() map[string]any
	Save(ctx context.Context, g security.Grant) (bool, error)
	Delete(ctx context.Context, g security.Grant) (bool, error)
}

Record is a row of the application's that an entry can be about or caused by: any struct embedding the Hesape Model, whose methods it promotes.

type RecordsEvents

type RecordsEvents interface {
	ActivityEvents() []string
}

RecordsEvents is implemented by a record that logs other events than DefaultEvents -- Spatie's static $recordEvents.

type Ref

type Ref struct {
	Type string
	ID   string
}

Ref names one thing an entry is about or was caused by: its kind and its identifier within that kind. Neither is a Go type: this package never loads the row on the other side.

func RefOf added in v0.2.0

func RefOf(record Record) Ref

RefOf is a record's kind and key, as an entry names it: ActivityType when the record says, its table's name otherwise. It is what a Filter takes to read the entries about a record or caused by one -- Spatie's forSubject and causedBy.

func (Ref) IsZero

func (r Ref) IsZero() bool

IsZero reports whether the reference names nothing.

type Resource

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

Resource is the list of fields one entry answers with. The tenant is not one of them: it names a customer and belongs in no response.

func NewResource

func NewResource(record *Activity) Resource

NewResource snapshots one entry for the response.

func (Resource) ToArray

func (r Resource) ToArray() map[string]any

ToArray returns the fields that may leave, by name.

func (Resource) With

func (r Resource) With() map[string]any

With is the framework's envelope hook; a resource adds nothing to it.

type Restorable

type Restorable interface {
	Record
	Restore(ctx context.Context, g security.Grant) (bool, error)
}

Restorable is a record of a table with soft deletes.

type Row added in v0.2.0

type Row struct {
	ID, URL, When, Log, Event, Description string
	Subject, Causer                        string
}

Row is one entry as a screen shows it.

type ShowPageData added in v0.2.0

type ShowPageData struct {
	view.Page
	Prefix     string
	Labels     Labels
	Row        Row
	Changes    []ChangeRow
	Properties []PropertyRow
}

ShowPageData is one entry.

type Typed

type Typed interface {
	ActivityType() string
}

Typed is implemented by a record whose kind is not its table's name -- the morph name Spatie reads from getMorphClass.

Directories

Path Synopsis
internal
everytenant
Package everytenant is the activity table read without the tenant filter: the one read of arandu-activitylog that crosses tenants, ActivityService.ReadAll, after the application's platform policy allowed it.
Package everytenant is the activity table read without the tenant filter: the one read of arandu-activitylog that crosses tenants, ActivityService.ReadAll, after the application's platform policy allowed it.

Jump to

Keyboard shortcuts

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