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

person

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package person is the read model of people: every human and every byline is an entity of the builtin `person` content type.

A person is a normal entity (name, slug, attributes) so it gets a URL, a sitemap entry and revisions like any other content. Entities credit one through hatamoto_entities.byline_person_id (or byline_org for the site organisation), and a user account owns at most one person through hatamoto_user_people. Nothing here reads or exposes a user's email: only the person's own `public_email` attribute is ever public.

hatamoto_authors is kept only so a rollback still renders bylines; it is not the source of bylines any more (see internal/author).

Index

Constants

View Source
const SchemaSlug = "person"

SchemaSlug is the slug of the builtin person content type.

Variables

View Source
var (
	// ErrMergeSame means keep and drop are the same person.
	ErrMergeSame = errors.New("a person cannot be merged into themselves")
	// ErrMergeBothLogins means both people belong to a user account: merging
	// would leave one of the accounts without a profile.
	ErrMergeBothLogins = errors.New("both people have a login")
)

Merge errors.

View Source
var (
	ErrNotFound = errors.New("person not found")
	// ErrAlreadyLinked means the user already owns a person, or the person
	// already belongs to another user.
	ErrAlreadyLinked = errors.New("user or person already linked")
)

Errors.

CoreFields are the person type's own properties: a site can add fields next to them but never remove or retype them.

Functions

func Backfill

func Backfill(ctx context.Context, db SQLDB) error

Backfill makes every author a person and points the author's entities at it. It is idempotent: a second run finds everything done and changes nothing, so it also serves after importing an archive that predates people.

  • Each author whose name is not the company name gets one person entity (slug = author slug so /author/{slug} keeps working, bio, photo from the avatar, public when the author has published entities). The person carries metadata.import_key "author:<author id>" (the idempotency key) and metadata.legacy_author_id (the mirror row for rollback).
  • An author named like the company (exact, case-insensitive, trimmed) gets no person: its entities become byline_org instead. Nothing else is read to decide that.
  • Entities that had author_id and no byline yet get byline_person_id (or byline_org). author_id is left untouched.
  • Users are linked to a person by equal name; failing that, a site with exactly one user and one person-author links the two. Everyone else stays unlinked and creates their profile at sign-in.
  • People never carry page_sections: the key is stripped from every person entity, so the Page Builder is never the editor of a person.

It fails when a non-builtin schema already uses the slug `person`.

func Complete

func Complete(p *Person) bool

Complete reports whether a profile has what a signed-in user must provide: a name and a photo. The admin sends users with an incomplete profile to the "Complete your profile" step.

func Duplicates

func Duplicates(people []*Person) [][]*Person

Duplicates groups the people who look like the same human, for the Merge screen. Two people are alike when their names are equal ignoring case and spacing, or when their slugs share a first name and one slug starts with the other (alex-c and alex-carter). A group holds at least two people, in name order; groups are in name order of their first member. Nothing is merged here: a person decides.

Types

type Byline

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

Byline resolves byline text to people and keeps the hatamoto_authors mirror of each person. It implements entity.BylineResolver.

The mirror exists only for rollback: the previous binary renders bylines from author_id, so every person has an author row with the same slug and name (its id is kept in the person's metadata.legacy_author_id) and the plain-text attributes.author of their posts carries the display name.

func NewByline

func NewByline(db DB) *Byline

NewByline returns a resolver over a pool or transaction.

func (*Byline) Check

func (b *Byline) Check(ctx context.Context, id uuid.UUID) error

Check implements entity.BylineResolver.

func (*Byline) Mirror

func (b *Byline) Mirror(ctx context.Context, personID uuid.UUID) (*uuid.UUID, error)

Mirror implements entity.BylineResolver: it makes the mirror row when the person has none and brings its name up to date.

func (*Byline) Name

func (b *Byline) Name(ctx context.Context, personID *uuid.UUID) (string, error)

Name implements entity.BylineResolver.

func (*Byline) PersonChanged

func (b *Byline) PersonChanged(ctx context.Context, personID uuid.UUID) error

PersonChanged implements entity.BylineResolver: the mirror follows the person, and the plain-text byline of their posts follows the name.

func (*Byline) PersonDeleting

func (b *Byline) PersonDeleting(ctx context.Context, personID uuid.UUID) error

PersonDeleting implements entity.BylineResolver. The mirror author row is kept (rollback); the posts keep their plain-text byline.

func (*Byline) Resolve

func (b *Byline) Resolve(ctx context.Context, name string) (entity.Byline, error)

Resolve implements entity.BylineResolver. Text equal (case-insensitive) to the company name credits the organisation and never makes a person.

type Credit

type Credit struct {
	PersonID *uuid.UUID
	Org      bool
}

Credit is who an entity credits: a person, the site organisation, or (both zero) nobody.

type DB

type DB interface {
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}

DB is the slice of pgx the store needs; *pgxpool.Pool and pgx.Tx satisfy it, so the same store works inside a migration or import transaction.

type EntityWriter

type EntityWriter interface {
	GetEntity(ctx context.Context, id uuid.UUID) (*entity.Entity, error)
	CreateEntity(ctx context.Context, userID *uuid.UUID, in *entity.CreateEntityInput) (*entity.Entity, error)
	UpdateEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, in *entity.UpdateEntityInput) (*entity.Entity, error)
	DeleteEntity(ctx context.Context, id uuid.UUID) error
}

EntityWriter is the part of entity.Service that writes people. Going through it keeps revisions, validation and the author mirror (PersonChanged) working.

type Link struct{ Label, URL string }

Link is a profile link and the name shown for it.

type MergeResult

type MergeResult struct {
	Keep, Drop *Person
	// Posts is how many entities were credited to Drop and now credit Keep.
	Posts int
}

MergeResult is what a merge changed, for the caller's message.

type PathMover

type PathMover interface {
	MoveEntityPaths(ctx context.Context, moves []redirect.PathMove) error
}

PathMover records that a public path moved (redirect.Service).

type Person

type Person struct {
	ID   uuid.UUID
	Slug string
	Name string

	JobTitle    string
	ShortBio    string
	Bio         string
	Photo       string // URL of the photo, "" without one
	PublicEmail string
	// SlugFixed is true once the person has been public: the address then stays
	// when the name changes.
	SlugFixed bool
	// Public is the opt-in: only a public person has a page, a sitemap entry
	// and a linked byline.
	Public bool
	// OnTeam marks people the site lists as its team (the on_team attribute).
	OnTeam bool
	// Links holds the profile links by key: website, linkedin, x, github.
	Links map[string]string
	// Expertise are free-form topic labels.
	Expertise []string
}

Person is one profile.

func (p *Person) LabelledLinks() []Link

LabelledLinks returns the profile links with a visible label, in the order of SameAs.

func (*Person) SameAs

func (p *Person) SameAs() []string

SameAs returns the profile links in a stable order (website, LinkedIn, ...).

type ProfileInput

type ProfileInput struct {
	Name     string
	Photo    string
	JobTitle string
	ShortBio string
	LinkedIn string
	Public   bool
}

ProfileInput is what the profile forms edit. The person's other attributes (bio, public email, links other than LinkedIn, expertise) are left alone.

func (*ProfileInput) Validate

func (in *ProfileInput) Validate() *ValidationError

Validate trims the input and checks it. Name and photo are required; the rest is optional. It returns nil when the input is good.

type ProfileStore

type ProfileStore interface {
	SchemaID(ctx context.Context) (uuid.UUID, error)
	Get(ctx context.Context, id uuid.UUID) (*Person, error)
	ForUser(ctx context.Context, userID uuid.UUID) (*Person, error)
	Link(ctx context.Context, userID, personID uuid.UUID) error
}

ProfileStore is the part of Store the profile service reads and links with.

type Profiles

type Profiles struct {
	*Store
	// contains filtered or unexported fields
}

Profiles creates and edits the person a user account owns. The embedded Store gives it the read methods too.

func NewProfiles

func NewProfiles(store *Store, entities EntityWriter) *Profiles

NewProfiles returns the profile service over a store and the entity service.

func (*Profiles) Create

func (p *Profiles) Create(ctx context.Context, by *uuid.UUID, name string) (*Person, error)

Create makes a draft person that is not public and has no login (People, Add person). The name must be 1 to 120 characters; a bad one is a *ValidationError on the "name" field.

func (*Profiles) MergePeople

func (p *Profiles) MergePeople(ctx context.Context, keepID, dropID uuid.UUID) (*MergeResult, error)

MergePeople is Store.Merge with the redirect settings of WithRedirects.

func (*Profiles) Save

func (p *Profiles) Save(ctx context.Context, userID uuid.UUID, in ProfileInput) (*Person, error)

Save writes a profile into the user's person, making the person first (from the typed name) when the user has none. Invalid input returns a *ValidationError and writes nothing. Empty optional values remove their attribute; every other attribute is kept.

func (*Profiles) WithRedirects

func (p *Profiles) WithRedirects(pathOf func(ctx context.Context, slug string) string, mover PathMover) *Profiles

WithRedirects makes MergePeople leave a 301 behind: pathOf gives the public path of a person slug (their type's URL pattern), mover records the move.

type SQLDB

type SQLDB interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

SQLDB is the slice of database/sql Backfill needs; *sql.Tx and *sql.DB satisfy it, so it runs inside a goose migration and after an import.

type Store

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

Store reads people and their user links.

func NewPoolStore

func NewPoolStore(pool *pgxpool.Pool) *Store

NewPoolStore is NewStore for a pool.

func NewStore

func NewStore(db DB) *Store

NewStore returns a store over a pool or transaction.

func (*Store) ByIDs

func (s *Store) ByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*Person, error)

ByIDs returns the given people in one query, keyed by id. Unknown ids are absent. Use it to resolve the bylines of a whole listing.

func (*Store) Credits

func (s *Store) Credits(ctx context.Context, entityIDs []uuid.UUID) (map[uuid.UUID]Credit, error)

Credits returns the byline of each entity in one query. Entities without a person or organisation byline are absent.

func (*Store) ForUser

func (s *Store) ForUser(ctx context.Context, userID uuid.UUID) (*Person, error)

ForUser returns the person a user owns, or ErrNotFound when the user has none yet.

func (*Store) Get

func (s *Store) Get(ctx context.Context, id uuid.UUID) (*Person, error)

Get returns a person by entity id.

func (*Store) GetBySlug

func (s *Store) GetBySlug(ctx context.Context, slug string) (*Person, error)

GetBySlug returns a person by slug.

func (s *Store) Link(ctx context.Context, userID, personID uuid.UUID) error

Link makes personID the person of userID. It returns ErrAlreadyLinked when the user already owns a person or the person already has a login, and ErrNotFound when personID is not a person entity.

func (*Store) LinkedUser

func (s *Store) LinkedUser(ctx context.Context, personID uuid.UUID) (uuid.UUID, error)

LinkedUser returns the user that owns a person, or ErrNotFound for a person without a login.

func (*Store) LinkedUsers

func (s *Store) LinkedUsers(ctx context.Context) (map[uuid.UUID]uuid.UUID, error)

LinkedUsers returns the user id that owns each person; people without a login are absent.

func (*Store) List

func (s *Store) List(ctx context.Context) ([]*Person, error)

List returns every person by name.

func (*Store) Merge

func (s *Store) Merge(ctx context.Context, keepID, dropID uuid.UUID, pathOf func(ctx context.Context, slug string) string, redirects PathMover) (*MergeResult, error)

Merge folds the person drop into keep, in one transaction: every entity credited to drop is credited to keep (including the author mirror and the plain-text byline), drop's login moves to keep when keep has none, keep's empty profile fields are filled from drop, and drop is deleted. Two people that both have a login are refused.

Afterwards, when drop had a public page and keep has one too, drop's old path answers 301 to keep's. pathOf gives the public path of a slug; with redirects or pathOf nil no redirect is written. The redirect is written after the commit, so a failure there is returned with the result: the merge itself stands.

func (*Store) PostCounts

func (s *Store) PostCounts(ctx context.Context) (map[uuid.UUID]int, error)

PostCounts returns how many published entities each person is credited on. People without any are absent.

func (*Store) SchemaID

func (s *Store) SchemaID(ctx context.Context) (uuid.UUID, error)

SchemaID returns the id of the person content type.

type ValidationError

type ValidationError struct{ Fields map[string]string }

ValidationError lists the problems of a ProfileInput by form field name ("name", "photo", "job_title", "short_bio", "linkedin").

func (*ValidationError) Error

func (e *ValidationError) Error() string

Jump to

Keyboard shortcuts

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