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

userfields

package
v0.1.0-alpha.9 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 24 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
var ErrMismatch = errors.New("userfields: values belong to a different schema")

ErrMismatch reports Values or generated types from a different schema.

View Source
var ErrSchema = errors.New("userfields: invalid schema")

ErrSchema wraps every schema declaration failure returned by New.

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

const (
	Private   Access = iota + 1 // Trusted Go code only; never serialized to HTTP or by Values.
	ReadOnly                    // Returned to the signed-in owner; only trusted Go code writes it.
	ReadWrite                   // Returned to and updatable by the signed-in owner over HTTP.
)

func (Access) String

func (a Access) String() string

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

func Compare(released, next Schema) ([]Change, error)

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

func (c Change) Destructive() bool

Destructive reports whether applying the change deletes stored data.

func (Change) String

func (c Change) String() string

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.
)

func (Kind) String

func (k Kind) String() string

type Op

type Op uint8

Op is the kind of one declaration change.

const (
	Add      Op = iota + 1 // a new column with its default for existing rows
	Widen                  // looser CHECK bounds; every stored value is kept
	Remove                 // drops the column and every stored value (destructive)
	Describe               // description edit in a released version; no migration, regenerate types
)

func (Op) String

func (o Op) String() string

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

func New(s Schema) (*Plugin, error)

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

func (p *Plugin) Bind(a *sigil.Auth) error

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

func (p *Plugin) Current(ctx context.Context) (Values, error)

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) Defaults

func (p *Plugin) Defaults() Values

Defaults returns the values of a user with no stored row.

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) Fields

func (p *Plugin) Fields() []Field

Fields returns copies of every field in declaration order.

func (*Plugin) Fingerprint

func (p *Plugin) Fingerprint() string

Fingerprint identifies the declaration; generated Go types embed it.

func (*Plugin) GoSource

func (p *Plugin) GoSource(o GoOptions) ([]byte, error)

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

func (p *Plugin) History() []Change

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) ID

func (p *Plugin) ID() string

ID returns the plugin ID.

func (*Plugin) Read

func (p *Plugin) Read(tx storage.Tx, userID string) (Values, error)

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

func (p *Plugin) UpdateComponent() string

UpdateComponent names the owner update request: optional ReadWrite fields.

func (*Plugin) UpdateCurrent

func (p *Plugin) UpdateCurrent(ctx context.Context, changes map[string]any) (Values, error)

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

func (p *Plugin) UpdateOperation() plugin.Operation

UpdateOperation is the audited operation name of owner updates: ID.update.

func (*Plugin) Validate

func (p *Plugin) Validate(changes map[string]any) error

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

func (p *Plugin) ViewComponent() string

ViewComponent names the response component: the public fields.

func (*Plugin) Write

func (p *Plugin) Write(tx storage.Tx, userID string, changes map[string]any) (Values, error)

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) Boolean

func (v Values) Boolean(name string) (bool, bool)

Boolean returns a Boolean field.

func (Values) Changes

func (v Values) Changes() map[string]any

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

func (v Values) Fingerprint() string

Fingerprint identifies the schema that produced these values.

func (Values) Format

func (v Values) Format(w fmt.State, _ rune)

Format prints only the public fields, like MarshalJSON.

func (Values) Integer

func (v Values) Integer(name string) (int64, bool)

Integer returns an Integer field.

func (Values) LogValue

func (v Values) LogValue() slog.Value

LogValue logs only the public fields, like MarshalJSON.

func (Values) MarshalJSON

func (v Values) MarshalJSON() ([]byte, error)

MarshalJSON renders the public fields in declaration order: the same bytes the owner-facing HTTP endpoints return. Private fields are never included.

func (Values) String

func (v Values) String(name string) (string, bool)

String returns a String or Enum field.

func (Values) Timestamp

func (v Values) Timestamp(name string) (time.Time, bool)

Timestamp returns a Timestamp field; the zero time means null.

type Version

type Version struct {
	Name   string
	Fields []Field
	Widen  []Field
	Remove []string
}

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

func Diff(released Schema, name string, desired []Field) (Version, error)

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.

Jump to

Keyboard shortcuts

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