Documentation
¶
Overview ¶
Package userfields adds explicit, typed custom fields to user accounts.
A host declares a Schema: an ID and an append-only list of versions, each adding fields and, after the first, explicitly widening or removing earlier ones. New compiles it into a plugin.Definition whose migrations, API contract, validation and generated Go types are all derived from that one declaration, so they cannot disagree. Fields live in a plugin-owned table keyed by user ID; there is no free-form JSON column and no field is added to the core User view.
Every field declares its Access. Private fields are readable only through trusted Go code; they never appear in HTTP responses, and Values renders only public fields through encoding/json, fmt and log/slog. See docs/user-fields.md.
Index ¶
- Variables
- type Access
- type Change
- type Field
- type FieldError
- type GoOptions
- type Kind
- type Op
- type Plugin
- func (p *Plugin) Bind(a *sigil.Auth) error
- func (p *Plugin) Current(ctx context.Context) (Values, error)
- func (p *Plugin) Defaults() Values
- func (p *Plugin) Definition() plugin.Definition
- func (p *Plugin) Fields() []Field
- func (p *Plugin) Fingerprint() string
- func (p *Plugin) GoSource(o GoOptions) ([]byte, error)
- func (p *Plugin) History() []Change
- func (p *Plugin) ID() string
- func (p *Plugin) Read(tx storage.Tx, userID string) (Values, error)
- func (p *Plugin) UpdateComponent() string
- func (p *Plugin) UpdateCurrent(ctx context.Context, changes map[string]any) (Values, error)
- func (p *Plugin) UpdateOperation() plugin.Operation
- func (p *Plugin) Validate(changes map[string]any) error
- func (p *Plugin) ViewComponent() string
- func (p *Plugin) Write(tx storage.Tx, userID string, changes map[string]any) (Values, error)
- type Schema
- type Values
- func (v Values) Boolean(name string) (bool, bool)
- func (v Values) Changes() map[string]any
- func (v Values) Fingerprint() string
- func (v Values) Format(w fmt.State, _ rune)
- func (v Values) Integer(name string) (int64, bool)
- func (v Values) LogValue() slog.Value
- func (v Values) MarshalJSON() ([]byte, error)
- func (v Values) String(name string) (string, bool)
- func (v Values) Timestamp(name string) (time.Time, bool)
- type Version
Constants ¶
This section is empty.
Variables ¶
var ErrMismatch = errors.New("userfields: values belong to a different schema")
ErrMismatch reports Values or generated types from a different schema.
var ErrSchema = errors.New("userfields: invalid schema")
ErrSchema wraps every schema declaration failure returned by New.
var ErrUnsupportedChange = errors.New("userfields: unsupported declaration change")
ErrUnsupportedChange reports a declaration change outside the supported set: an edit to a released version, or a change Diff cannot derive. The message names the field, the attribute and the explicit alternative.
Functions ¶
This section is empty.
Types ¶
type Access ¶
type Access uint8
Access is the public-field allowlist entry for a field. The zero value is invalid, so no field becomes public by omission.
type Change ¶
type Change struct {
Version int // the version that makes the change; for Describe, the released one edited
VersionName string // that version's name
Op Op
Field string
Detail string // human-readable, for example "MaxLength 64 -> 128"
}
Change is one reviewed difference between two declarations.
func Compare ¶
Compare validates next as a successor of released and lists what next changes, in version order and, within a version, in migration order: removals, widenings, additions. Released versions must be unchanged except for field descriptions, which are reported as Describe; any other edit is ErrUnsupportedChange naming the field and attribute, which is the precise form of the startup check's plugin schema error. Both declarations must be valid (ErrSchema otherwise). An empty released declaration compares a fresh installation.
Run it in a host test against the last released declaration to review an upgrade before it ships, and to fail CI on an accidental edit.
func (Change) Destructive ¶
Destructive reports whether applying the change deletes stored data.
type Field ¶
type Field struct {
// Name is the lowerCamelCase wire and Go key; its snake_case form is the column.
Name string
Kind Kind
Access Access
Description string
// MinLength and MaxLength bound String values in Unicode code points.
// MaxLength is required (1–1024).
MinLength, MaxLength int
// Minimum and Maximum bound Integer values inclusively; Minimum < Maximum.
Minimum, Maximum int64
// Values lists the allowed Enum values.
Values []string
// Default is the value of a user without a stored row, and of a column
// added by a later version for existing rows. nil selects the zero value
// ("", 0, false, null); Enum requires an explicit default. The type must
// match the kind: string, int64 or int, bool; Timestamp defaults are null.
Default any
}
Field declares one typed column. Released fields must never change: their migration SQL is checksummed, so an edited field fails the startup check.
type FieldError ¶
type FieldError struct {
Field, Reason string
}
FieldError names a rejected field value. It unwraps to sigil.ErrBadRequest, so HTTP handlers serialize it as bad_request without its diagnostic text.
func (*FieldError) Error ¶
func (e *FieldError) Error() string
func (*FieldError) Unwrap ¶
func (e *FieldError) Unwrap() error
type GoOptions ¶
type GoOptions struct {
Package string // Go package name of the generated file.
Type string // Exported struct name; TypeUpdate is the partial-update struct.
}
GoOptions names the generated package and struct.
type Kind ¶
type Kind uint8
Kind is a field's value type. The zero value is invalid.
const ( String Kind = iota + 1 // Go string, SQL text; bounded length in code points. Integer // Go int64, SQL bigint; bounds within ±(2^53−1) so TypeScript numbers are exact. Boolean // Go bool, SQL boolean. Enum // Go string restricted to Values, SQL text with a CHECK constraint. Timestamp // Go time.Time (zero means null), SQL timestamp; the only nullable kind. )
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is a compiled, immutable extension. It is safe for concurrent use after Bind.
func New ¶
New validates and copies a schema. It rejects unsupported capabilities explicitly: nullable non-timestamp fields, writable timestamps, required fields without a valid default, unbounded strings, unsafe integer bounds, duplicate, reserved or reused names, empty versions, and changes to released fields other than Widen and Remove.
func (*Plugin) Bind ¶
Bind attaches the started Auth that registered this definition. It fails unless the instance enabled the plugin, and can succeed only once.
func (*Plugin) Current ¶
Current returns the signed-in user's values, including private fields, for trusted code. It requires an authenticated session; HTTP responses render only the public fields.
func (*Plugin) Definition ¶
func (p *Plugin) Definition() plugin.Definition
Definition returns the plugin registration. Use the same value for the operator migration registry and sigil.Config.Plugins.
func (*Plugin) Fingerprint ¶
Fingerprint identifies the declaration; generated Go types embed it.
func (*Plugin) GoSource ¶
GoSource generates typed Go declarations for the extension: a struct with every field, a pointer-field update struct, conversions to and from Values and Write changes, and a fingerprint check. The generated struct renders only public fields through encoding/json, fmt and log/slog, exactly like Values. Output depends only on the schema and options; commit it and compare it in a test so drift fails CI.
func (*Plugin) History ¶
History lists every Add, Widen and Remove the declaration makes, version by version in migration order: the change set a fresh installation applies.
func (*Plugin) Read ¶
Read returns every field of a user, including private ones, inside a trusted transaction. It locks the user, so call it only where locking that user is permitted: inside a store transaction for that user, or a WithAssurance/WithRecentSession callback for the actor. A user without a stored row reads as Defaults; a missing user is storage.ErrNotFound.
func (*Plugin) UpdateComponent ¶
UpdateComponent names the owner update request: optional ReadWrite fields.
func (*Plugin) UpdateCurrent ¶
UpdateCurrent applies owner changes for the signed-in user. Only ReadWrite fields are accepted; any other field is a FieldError before storage is touched. The update is audited as UpdateOperation and runs plugin hooks.
func (*Plugin) UpdateOperation ¶
UpdateOperation is the audited operation name of owner updates: ID.update.
func (*Plugin) Validate ¶
Validate checks a set of changes without storing them. Keys are field names; values use the Go types documented on Kind. It ignores access, which trusted writers may bypass; the owner-facing update path enforces it.
func (*Plugin) ViewComponent ¶
ViewComponent names the response component: the public fields.
func (*Plugin) Write ¶
Write validates and applies changes for a user inside a trusted transaction, under the user lock, and returns the complete new values. It may set any field, including private and read-only ones: authorization is the caller's. Fields not named keep their stored or default value.
type Schema ¶
type Schema struct {
// ID is the plugin ID, route prefix (/ID), table prefix (ID_fields) and,
// capitalized, the contract component prefix. Lowercase letters and digits.
ID string
Versions []Version
}
Schema is a complete extension declaration. Add versions to change it; never edit, reorder or remove released ones.
type Values ¶
type Values struct {
// contains filtered or unexported fields
}
Values is a complete, validated snapshot of one user's fields. Trusted code reads every field, including private ones, through the typed accessors. Automatic serialization renders only public fields: MarshalJSON produces exactly the HTTP response body, and fmt and log/slog print it too.
func (Values) Changes ¶
Changes returns every field, including private ones, as a map suitable for Write. It is an explicit export of private data; never serialize it.
func (Values) Fingerprint ¶
Fingerprint identifies the schema that produced these values.
func (Values) MarshalJSON ¶
MarshalJSON renders the public fields in declaration order: the same bytes the owner-facing HTTP endpoints return. Private fields are never included.
type Version ¶
Version is one append-only migration. Its position is its version number. The first version only adds fields. A later version may also change fields that earlier versions declared, and each change is explicit:
- Widen redeclares a live field with looser bounds: a longer MaxLength or shorter MinLength, a wider integer range, or more enum values. Kind, access and default must not change; the description may. Every stored value satisfies the looser declaration, so existing data is kept.
- Remove drops live fields, their columns and every stored value. The migration is marked destructive, and a removed name is never reused.
Anything else (tightening, retyping, renaming, changing access or default) is not a supported change: add a new field and copy values in host code.
func Diff ¶
Diff is automatic diffing over the supported change set. Given the released declaration and the complete list of fields the extension should have, it derives the next version: new fields become Add, strictly looser redeclarations become Widen, and unchanged fields are left alone. It refuses with ErrUnsupportedChange, naming the field, anything else:
- a live field missing from desired, because removal deletes data and must be declared explicitly with Version.Remove;
- a field that was removed earlier, because names are never reused;
- tightened bounds, removed enum values, or a changed kind, access or default, which no migration performs without rewriting or losing data;
- a description-only edit, which needs no migration: edit the description in the version that declares the field.
The result is validated by compiling released plus the new version, and it is deterministic: widenings and additions follow the order of desired.