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

entity

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: 35 Imported by: 0

Documentation

Overview

Package entity provides a schema-driven JSON-LD generator that reads x-schema-org-type and x-schema-org-property annotations from JSON Schema definitions to produce structured data conforming to Schema.org vocabulary.

Package entity — plugin extension point for JSON-LD generation.

Core ships this registry EMPTY: no default builders are registered here for any schema slug. Plugins that add a content type (e.g. case-study, team-member) register their own JSON-LD builder via RegisterJSONLDBuilder from their Wire().

Package entity provides entity domain models and repository interfaces for the Entity Management system.

Package entity provides entity domain models and repository interfaces.

Package entity — plugin extension point for public entity URL construction, used by JSON-LD generation (BuildEntityURLForJSONLD).

Modeled on jsonld_hooks.go's registration pattern. Core ships this registry EMPTY: no default builders are registered here for any schema slug. Plugins that add a content type register their own URL builder via RegisterEntityURLBuilder from their Wire().

Index

Constants

View Source
const (
	RolePage  = "page"
	RoleBlock = "block"
	RolePart  = "part"
)

Editor roles: what the builder is editing.

View Source
const (
	BlockSchemaSlug = "block"
	PartSchemaSlug  = "theme_part"
)

The two built-in types of the builder's reuse feature. Both hold a page_doc and are never public: a block is embedded by reference (a ref node), a theme part fills a layout slot of an opted-in theme.

View Source
const (
	MaxArchiveTitleLen       = 200
	MaxArchiveDescriptionLen = 500
)

Length limits (in characters, after trimming) for the archive settings.

View Source
const AutosaveSummary = "Autosaved in the blog editor"

AutosaveSummary is the change summary of a background save; versions with it are pruned before any explicit version.

View Source
const AutosaveWindow = 5 * time.Minute

AutosaveWindow is how long one person's autosaves share a version.

View Source
const EditorPost = "post"

EditorPost is the settings.editor value that opts a type into the post editor.

View Source
const EmbedHostsEnv = "HATAMOTO_EMBED_HOSTS"

EmbedHostsEnv names the environment variable that lists the hosts an iframe may embed (comma-separated host names, no scheme or port).

View Source
const HiddenPersonSQL = `` /* 196-byte string literal not displayed */

HiddenPersonSQL is false for a person entity (schema aliased s, entity e) whose attributes.public is not true, and for any entity of the built-in block or theme_part type whatever its status (they are used by reference, never served). Every public read adds it, so these are absent from pages, sitemaps, llms.txt, search, theme queries and MCP public search alike.

It also hides every entity whose locale is not the site language: the public site serves one language, so other locales are stored but not public.

View Source
const MaxImportBatch = 1000

MaxImportBatch bounds the items of one Import call; bigger sources are sent in several batches (each emits its own import.completed).

View Source
const MaxListOffset = 1000

MaxListOffset caps ListFilter.Offset.

View Source
const MaxRefUsers = 200

MaxRefUsers caps the usage list of a block.

View Source
const NoIndexSQL = `` /* 178-byte string literal not displayed */

NoIndexSQL is true for an entity row aliased e that must stay out of sitemaps and search: metadata.no_index, attributes.no_index or an attributes.seo.robots value containing "noindex".

View Source
const PersonSchemaSlug = "person"

PersonSchemaSlug is the slug of the built-in person type, the one built-in type a site may extend (add fields, change URL and archive settings).

Variables

View Source
var (
	ErrImportUnsupported = errors.New("the entity repository does not support import")
	ErrImportEmpty       = errors.New("import batch is empty")
	ErrImportTooLarge    = fmt.Errorf("import batch has more than %d items", MaxImportBatch)
)

Import errors.

View Source
var (
	ErrSchemaNotFound    = errors.New("entity schema not found")
	ErrEntityNotFound    = errors.New("entity not found")
	ErrVersionConflict   = errors.New("entity was saved by someone else")
	ErrSlugAlreadyExists = errors.New("slug already exists")
	ErrInvalidStatus     = errors.New("invalid entity status")
	ErrInvalidSchema     = errors.New("invalid JSON schema")
	ErrValidationFailed  = errors.New("attribute validation failed")
	ErrVersionNotFound   = errors.New("entity version not found")
	// ErrRestoreInvalid marks a version whose attributes no longer fit the
	// entity type's current schema.
	ErrRestoreInvalid      = errors.New("version does not fit the current schema")
	ErrCannotDeleteBuiltin = errors.New("cannot delete built-in schema")
	ErrCannotModifyBuiltin = errors.New("cannot modify built-in schema")
	ErrSchemaInUse         = errors.New("schema is in use by entities")
	ErrParentNotFound      = errors.New("parent entity not found")
	ErrParentCycle         = errors.New("parent would make the entity its own ancestor")
	ErrParentTooDeep       = errors.New("entity nesting is too deep")
	ErrAmbiguousSlug       = errors.New("several entities of different schemas share this slug: say which schema")
	ErrHasChildren         = errors.New("entity has child entities: move or delete them first")
	ErrAuthorNotFound      = errors.New("author not found")
	// ErrInvalidDoc marks an attributes.page_doc that fails validation. It does
	// not wrap ErrValidationFailed: the API maps it to 422, not 400.
	ErrInvalidDoc = errors.New("invalid page_doc")
	// ErrPageSectionsRemoved rejects a write that changes the layout through
	// page_sections on a page that has a page_doc (maps to 422).
	ErrPageSectionsRemoved = errors.New("page_sections can no longer change a page that has a page_doc; send attributes.page_doc (docs/BUILDER.md)")
)

Common errors.

View Source
var AllowedSchemaOrgTypes = []string{
	"Article", "BlogPosting", "NewsArticle", "DefinedTerm", "Person", "Service",
	"Course", "HowTo", "WebPage", "CreativeWork", "Organization", "LocalBusiness",
}

AllowedSchemaOrgTypes is the allowlist for settings.schema_org_type.

View Source
var ErrRejectedByPlugin = errors.New("save rejected by plugin")

ErrRejectedByPlugin is returned (wrapped) when a before_save hook rejects a save.

View Source
var ErrScheduleInPast = errors.New("scheduled time must be in the future")

ErrScheduleInPast is returned when a scheduled time is not in the future.

View Source
var PersonCoreFields = []string{
	"job_title", "short_bio", "bio", "photo", "public", "public_email",
	"website", "linkedin", "x", "github", "expertise", "on_team",
}

PersonCoreFields are the properties the person type ships with. Code reads them (profiles, bylines, JSON-LD, the profile form), so a site can neither remove nor retype them. Kept equal to the seeded schema by a test.

Functions

func AllowedInlineHref

func AllowedInlineHref(raw string) bool

AllowedInlineHref reports whether raw is a link target the inline profile keeps: http, https, mailto, tel, sms or a relative reference (path, ?query, #fragment). It is the render safeURL rule (render cannot be imported here); internal/entity/testdata/inline_href_cases.json keeps the two in step.

func ApplyCanonicalToJSONLD

func ApplyCanonicalToJSONLD(doc map[string]any, schemaSlug, canonical string)

ApplyCanonicalToJSONLD makes the entity's own node agree with the page's canonical URL: url becomes canonical, "@id" keeps its fragment (or defaults to the lower-cased @type) on top of canonical, and nested references to the previously emitted url are rewritten. Organization/company schemas keep their own url (the "website" attribute), and @graph documents are skipped.

func BuildEntityURLForJSONLD

func BuildEntityURLForJSONLD(ent *Entity, baseURL string) string

BuildEntityURLForJSONLD constructs the canonical URL for an entity. Exported so callers in other packages can resolve internal-source citation URLs when building JSON-LD payloads.

func CheckSanitisedLength

func CheckSanitisedLength(doc *builder.Doc) error

CheckSanitisedLength refuses a doc whose html prop grew past the schema's maxLength while it was sanitised (the doc was validated before). Storing it would leave a doc that no longer parses.

func DerivedAttributes

func DerivedAttributes(raw json.RawMessage) json.RawMessage

DerivedAttributes is the read-side view of a page's attributes for consumers that want section-shaped data (public JSON, plugins, hooks): page_sections is the sections of page_doc as of now, never the frozen stored mirror. raw is returned untouched when it has no page_doc; an invalid one drops page_sections (fail closed, as the renderer does); a builder-only doc (v1 cannot express it) has no page_sections, so the key is removed. Pure.

func EligibleForPageDoc

func EligibleForPageDoc(sc *EntitySchema) bool

EligibleForPageDoc reports whether entities of sc take part in page_sections to page_doc conversion: everything except post-editor types and persons, whose page_sections (if any) is ordinary data.

func EnrichJSONLDWithEditorial

func EnrichJSONLDWithEditorial(jsonLD map[string]any, editorial json.RawMessage)

EnrichJSONLDWithEditorial adds editorial provenance data to JSON-LD output. It reads the entity's Editorial field and adds editor and citation properties.

func EnrichJSONLDWithEvidence

func EnrichJSONLDWithEvidence(jsonLD map[string]any, links []EvidenceLinkForJSONLD)

EnrichJSONLDWithEvidence adds claim/evidence annotations to JSON-LD output. It adds:

  • "citation" entries (Schema.org CreativeWork) for every external evidence URL
  • "mentions" entries (Schema.org Claim) for every field with evidence, each carrying its own per-field "citation" subset
  • "evidenceCount" and "evidenceLevel" aggregate signals (non-standard but readable by LLM crawlers)
  • "lastReviewed" timestamp from the most recent verification

Note: this does not emit ClaimReview rich-result markup — that requires the page to be a dedicated fact-check page. Plain Claim under mentions is the maximally compatible structure that survives audit by quality raters and gets picked up by AI Overview citation pipelines.

func EntityRole

func EntityRole(ctx context.Context, st BuilderStore, e *Entity) string

EntityRole is the editor role of e, from its schema (loaded when e has none).

func FlattenAttributes

func FlattenAttributes(raw json.RawMessage) map[string]string

FlattenAttributes returns the top-level scalar attributes as strings, the form pattern overrides compare against.

func GenerateAICrawlerComment

func GenerateAICrawlerComment(src AICommentSource, tenantName, baseURL string) string

GenerateAICrawlerComment builds the HTML comment block that we embed in every public entity page for AI/LLM crawlers. The block is a structured, human-readable summary that doesn't depend on Schema.org parsing.

Returns an empty string when the editor has opted the entity out via the SEO tab's "Include in /llms.txt" toggle — same flag that gates the /llms.txt and /api/v1/public/knowledge-graph feeds, so the surface is consistent for crawlers.

tenantName is the human-readable site/agency name (e.g. "Acme"). baseURL is the canonical site origin (e.g. "https://example.com"), used to build the absolute llms.txt URL in the footer of the comment.

func GenerateJSONLD

func GenerateJSONLD(entity *Entity, baseURL string) (map[string]any, error)

GenerateJSONLD generates JSON-LD structured data for an entity. It first attempts the schema-driven generator (using x-schema-org-* annotations). If the schema lacks annotations, it falls back to the legacy switch-based generator. Supports: WebPage, Article, LocalBusiness, FAQPage, Service, Person, Organization, Product.

func GenerateJSONLDWithFAQ

func GenerateJSONLDWithFAQ(ent *Entity, baseURL string, faqItems []FAQItem) (map[string]any, error)

GenerateJSONLDWithFAQ produces JSON-LD, and if FAQ items are provided, wraps the result in a @graph array that includes both the entity's own JSON-LD and a FAQPage object.

func GenerateSchemaDrivernJSONLD

func GenerateSchemaDrivernJSONLD(ent *Entity, baseURL string) map[string]any

GenerateSchemaDrivernJSONLD produces JSON-LD by walking the schema annotations. It reads x-schema-org-type from the root to determine @type, then recursively maps attribute values using x-schema-org-property annotations on each property.

Returns nil if the schema has no x-schema-org-type annotation (caller should fall back to the legacy switch-based generator).

func IframeSrcAllowed

func IframeSrcAllowed(src string) bool

IframeSrcAllowed reports whether src is an iframe source the sanitiser policies keep.

func IsAllowedSchemaOrgType

func IsAllowedSchemaOrgType(t string) bool

IsAllowedSchemaOrgType reports whether t is in AllowedSchemaOrgTypes.

func IsBlockSchema

func IsBlockSchema(sc *EntitySchema) bool

IsBlockSchema reports whether sc is the built-in block type.

func IsPartSchema

func IsPartSchema(sc *EntitySchema) bool

IsPartSchema reports whether sc is the built-in theme part type.

func IsPostEditorSchema

func IsPostEditorSchema(sc *EntitySchema) bool

IsPostEditorSchema reports whether sc is edited in the post editor: a built-in post slug, or a schema whose settings.editor is "post".

func IsReuseSchema

func IsReuseSchema(sc *EntitySchema) bool

IsReuseSchema reports whether sc is the built-in block or theme part type. A site type that took the slug is not one: it keeps its normal behaviour.

func IsReuseSlug

func IsReuseSlug(slug string) bool

IsReuseSlug reports whether slug is the slug of a block or a theme part.

func LegacyPageDoc

func LegacyPageDoc(attrs json.RawMessage) (*builder.Doc, bool)

LegacyPageDoc converts the page_sections stored in attrs into a doc, the same deterministic conversion a write applies, without touching anything. ok is false when attrs has no page_sections array or it does not convert. The caller decides whether a page_doc also exists.

func LegacyPageDocErr

func LegacyPageDocErr(attrs json.RawMessage) (*builder.Doc, error)

LegacyPageDocErr is LegacyPageDoc with the reason: (nil, nil) when attrs has no page_sections array, the conversion error when it does not convert.

func LoadDoc

func LoadDoc(ctx context.Context, st BuilderStore, id, slug string) (*builder.Doc, bool)

LoadDoc reads the page_doc of the entity id when it is of the built-in schema slug.

func NormalizeAttributes

func NormalizeAttributes(stored, in json.RawMessage) (json.RawMessage, error)

NormalizeAttributes is the page_doc write seam as a pure function: the attributes UpdateEntity would store for in against the stored ones. `builder convert` verifies the render of exactly this result and writes it verbatim.

func NormalizeLocale

func NormalizeLocale(tag string) (string, error)

NormalizeLocale returns the canonical BCP 47 form of tag, exactly as the site language setting canonicalizes it (language.Parse then String): "_" becomes "-", case is fixed and deprecated codes are replaced ("iw" is "he", "tl" is "fil"). The public filter compares entity locale to the site language as strings, so the two must never disagree. An invalid or over-long tag wraps ErrValidationFailed.

func PersonDescription

func PersonDescription(shortBio, bio string) string

PersonDescription is the plain-text description of a person: the short bio, else the bio with its markup removed, cut to 300 runes.

func PlainText

func PlainText(markup string, n int) string

PlainText turns stored HTML into plain text for a card excerpt: block boundaries become spaces, script and style content is dropped, entities are decoded and whitespace is collapsed. It keeps at most n runes. The result is only ever rendered escaped.

func RegisterEntityURLBuilder

func RegisterEntityURLBuilder(schemaSlug string, fn EntityURLBuilder)

RegisterEntityURLBuilder installs a URL builder for the given schema slug. Intended to be called from a plugin's Wire().

func RegisterJSONLDBuilder

func RegisterJSONLDBuilder(schemaSlug string, fn JSONLDBuilder)

RegisterJSONLDBuilder installs a JSON-LD builder for the given schema slug. Intended to be called from a plugin's Wire().

func ReuseRole

func ReuseRole(sc *EntitySchema) string

ReuseRole is what the builder edits for a schema of the entity.

func SanitizeAttributesAgainst

func SanitizeAttributesAgainst(raw, stored json.RawMessage) (json.RawMessage, error)

SanitizeAttributesAgainst is the exported form of sanitizeAttributesAgainst for write paths outside this package (the render tests use it).

func SanitizeDocComponents

func SanitizeDocComponents(doc *builder.Doc, olds ...*builder.Doc) bool

SanitizeDocComponents applies the write-boundary sanitiser to the content of every component node of doc (rich-text `content` strings and html-section bodies) and to the `html` prop of every richtext, html, heading and text node. Values equal to the node with the same id in one of olds (the first match wins; nil entries are ignored) are left alone, as for page_sections. It reports whether anything changed.

func SanitizeHTMLSectionBody

func SanitizeHTMLSectionBody(s string) string

SanitizeHTMLSectionBody sanitizes the body of an html section, or any HTML fragment a theme builds, with the html-section policy and then strips every data-hm-* attribute. The policy keeps class, id, role, style (checked), aria-* and data-* attributes; it never keeps script, event handlers, forms or an iframe off the embed allowlist (see EmbedHostsEnv).

func SanitizeHTMLSections

func SanitizeHTMLSections(raw json.RawMessage) (json.RawMessage, int, error)

SanitizeHTMLSections rewrites attributes.page_sections[type=html or an HTML_SECTION_TYPE_ALIASES type].content.body to its sanitised (canonical) form. It returns the new attributes and the number of sections whose body changed. When nothing changed the input is returned verbatim. Only changed sections (and the containers on the path to them) are re-marshalled, with key order preserved and every other value byte-identical. (Postgres jsonb re-orders keys on storage regardless.) Malformed or unexpected shapes are left alone.

func SanitizeInlineHTML

func SanitizeInlineHTML(s string) string

SanitizeInlineHTML sanitizes the `html` prop of a heading or text node: strong em b i u s code br and links, nothing else. Links keep href (only an AllowedInlineHref one; otherwise the link is unwrapped and its text kept) and target=_blank, which always carries rel="noopener noreferrer". The result is re-parsed in a <div> and rendered again, so tags are balanced and sanitising twice changes nothing.

func SanitizeRichText

func SanitizeRichText(s string) string

SanitizeRichText sanitizes rich text with the rich-text policy and then strips every data-hm-* attribute.

func SanitizeSectionContent

func SanitizeSectionContent(sectionType string, content map[string]any) map[string]any

SanitizeSectionContent returns a sanitized deep copy of a page section's content for render-time use (e.g. draft preview of unsaved attributes). For html-type sections content.body is treated as html; any string stored under a key named "content" is treated as rich text, matching the write path.

func SanitizeStoredAttributes

func SanitizeStoredAttributes(raw json.RawMessage) (json.RawMessage, int, error)

SanitizeStoredAttributes is the raw-SQL maintenance form of the write-boundary sanitiser (sanitize-html-sections): html section bodies in page_sections plus the component content of page_doc. A changed doc counts as one in the returned total. The frozen page_sections mirror is only cleaned in place (never created, never re-derived from the doc). Nothing changed returns the input verbatim. Malformed shapes are left alone.

func SaveBuilderDoc

func SaveBuilderDoc(ctx context.Context, st BuilderStore, actor *uuid.UUID, e *Entity, doc json.RawMessage, opts SaveOpts) (*SavedDoc, *DocProblem, error)

SaveBuilderDoc stores doc as e's page_doc: the shared checks, a merge into the stored attributes and UpdateEntity (which sanitises and normalises), then optionally the publish status change. The version is checked by the store when opts.ExpectedVersion is set. A refusal of the document is a *DocProblem; other errors come from the store.

func SchemaOrgTypeForEntity

func SchemaOrgTypeForEntity(ent *Entity) string

SchemaOrgTypeForEntity returns the Schema.org @type that best matches an entity's schema slug, used when emitting internal-source citation blocks. Falls back to "CreativeWork" for unknown slugs.

func SectionsFromAttributes

func SectionsFromAttributes(raw json.RawMessage) ([]builder.Section, error)

SectionsFromAttributes decodes attributes.page_sections with the shared reader, numbers kept exact (used by `builder convert`).

func SetFieldKindResolver

func SetFieldKindResolver(r FieldKindResolver)

SetFieldKindResolver registers the active theme's field-kind lookup. The render engine depends on this package, so the server wires it at boot rather than entity importing render. nil clears it.

Save-time schema sanitising exists only while a resolver is registered, and the server registers one only when Go theme rendering is on (a theme loaded). With rendering off there is no schema to consult, so only the schema-free passes run on save; schema-marked fields are then not sanitised until a theme is enabled and the content is saved again (render-time sanitising still protects the served HTML).

func SetJSONLDEnricher

func SetJSONLDEnricher(f JSONLDEnricher)

SetJSONLDEnricher installs (or, with nil, clears) the enricher. Package level because GenerateJSONLD is a plain function with four call sites.

func SetURLRegistry

func SetURLRegistry(r *URLRegistry)

SetURLRegistry installs the registry used by BuildEntityURLForJSONLD. The server calls it once at startup; without it the defaults apply.

func ValidAttrKey

func ValidAttrKey(k string) bool

ValidAttrKey reports whether k can be used as a filter or order attribute.

func ValidStatus

func ValidStatus(s EntityStatus) bool

ValidStatus checks if a status is valid.

func ValidateEditorSetting

func ValidateEditorSetting(slug string, schema, settings json.RawMessage) error

ValidateEditorSetting is the write-time check of settings.editor against the schema it applies to: the post editor writes attributes.content as a string, so the schema must declare one, and the person schema has its own form.

func ValidateSchemaSettings

func ValidateSchemaSettings(raw json.RawMessage) error

ValidateSchemaSettings checks the structural rules for schema settings: the value must be a JSON object, patterns must start with "/" and contain {slug} (see urlpattern.Parse), and archive_path must start with "/".

func ValidateSchemaSettingsForWrite

func ValidateSchemaSettingsForWrite(raw json.RawMessage) error

ValidateSchemaSettingsForWrite is ValidateSchemaSettings plus the checks that apply only when settings are saved. schema_org_type is validated here and not on load: a stale or hand-edited value must only disable JSON-LD typing (the generator re-checks the allowlist), never reset the schema's URL routing. The same holds for editor, which is also checked only here.

func ValidateSchemaShape

func ValidateSchemaShape(raw json.RawMessage) error

ValidateSchemaShape enforces editor-renderability invariants on a user-authored JSON Schema. It is intentionally narrower than the JSON Schema spec — it codifies what the visual schema builder and the field-renderer can round-trip without surprise.

Rules enforced:

  • Payload is at most maxSchemaBytes (64 KiB) — anti-DoS guard.
  • Root parses as a JSON object.
  • Root has a `type` field and the value is one of supportedSchemaTypes.
  • If root type is `object`, `properties` must be present and an object.
  • Every property has a `type` of a supported kind.
  • Nested `object` properties recurse with the same rules.
  • `array` properties must declare `items` that is itself a valid property shape, AND items.type must NOT be `array` (repeaters are single-level only by design — the UI cannot render nested repeaters, and the raw-JSON escape hatch must agree).
  • `additionalProperties` and `patternProperties` are rejected outright — both bypass the closed-shape contract that the field renderer and downstream consumers rely on. Admins should add fields via `properties`, not escape hatches.
  • Property names match `^[A-Za-z_][A-Za-z0-9_]*$` AND are not on the prototype-pollution denylist (`__proto__`, `constructor`, etc.).

Errors are wrapped with ErrInvalidSchema and include a dotted path (e.g. `properties.faq.items.type missing`) so the admin builder can highlight the offending field.

Types

type AICommentSource

type AICommentSource struct {
	Name       string
	Slug       string
	SchemaSlug string
	Attributes json.RawMessage
	UpdatedAt  time.Time
}

AICommentSource is the minimal entity-shaped data the comment generator needs, adapted from an *Entity by AICommentSourceFromEntity.

func AICommentSourceFromEntity

func AICommentSourceFromEntity(ent *Entity) AICommentSource

FromEntity adapts a published *Entity into an AICommentSource.

type AttrValueCount

type AttrValueCount struct {
	Value string
	Count int
}

AttrValueCount is one distinct attribute value and how many published entities carry it.

type BeforeSaveFunc

type BeforeSaveFunc func(ctx context.Context, in *BeforeSaveInput) error

BeforeSaveFunc runs before an entity is persisted. A returned error rejects the save (wrapped with ErrRejectedByPlugin by the service).

It runs outside the repository transaction: the transaction lives inside the repository's Create/Update calls.

type BeforeSaveInput

type BeforeSaveInput struct {
	Operation  string
	ID         *uuid.UUID
	SchemaSlug string
	Name       string
	Slug       string
	// Locale is the entity's BCP 47 tag, informational only (never read back).
	// Empty on create or import when the caller did not set one: the entity then
	// takes the site language.
	Locale     string
	Attributes json.RawMessage
}

BeforeSaveInput is the draft handed to the before_save hook. Operation is "create", "update" or "restore"; ID is nil for create. The hook may mutate Name, Slug and Attributes in place, or return an error to reject the save.

type BreadcrumbPathItem struct {
	ID   uuid.UUID `json:"id"`
	Name string    `json:"name"`
	Slug string    `json:"slug"`
}

BreadcrumbPathItem represents a single item in an entity's ancestor chain.

type BuilderStore

type BuilderStore interface {
	GetEntity(ctx context.Context, id uuid.UUID) (*Entity, error)
	GetSchema(ctx context.Context, id uuid.UUID) (*EntitySchema, error)
	UpdateEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, in *UpdateEntityInput) (*Entity, error)
	UpdateEntityStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
}

BuilderStore is what the shared save needs of the entity service.

type Byline

type Byline struct {
	PersonID *uuid.UUID
	Org      bool
	Name     string
}

Byline is who a post is credited to: a person entity, or the site organisation. Name is the display name (the company name for the organisation).

type BylineResolver

type BylineResolver interface {
	// Resolve maps byline text to a credit: the site organisation when it is
	// the company name, else the person with that slug or name, creating a
	// non-public person without a login when there is none.
	Resolve(ctx context.Context, name string) (Byline, error)
	// Name is the display name of the credit: the person's name, or the
	// organisation's when personID is nil.
	Name(ctx context.Context, personID *uuid.UUID) (string, error)
	// Check returns ErrAuthorNotFound when id is not a person.
	Check(ctx context.Context, id uuid.UUID) error
	// Mirror returns the hatamoto_authors row that mirrors the person (made on
	// first use). author_id is dual-written with it so a rollback still shows
	// bylines.
	Mirror(ctx context.Context, personID uuid.UUID) (*uuid.UUID, error)
	// PersonChanged is called after a person entity was created or saved: it
	// refreshes the mirror row and the plain-text byline on their posts.
	PersonChanged(ctx context.Context, personID uuid.UUID) error
	// PersonDeleting is called before a person entity is deleted: their posts
	// lose the reference and keep the plain-text byline.
	PersonDeleting(ctx context.Context, personID uuid.UUID) error
}

BylineResolver is the slice of the people store the entity service needs. People are entities of the `person` type, but this package never imports internal/person.

type CreateEntityInput

type CreateEntityInput struct {
	SchemaID   uuid.UUID       `json:"schema_id"`
	Name       string          `json:"name"`
	Slug       string          `json:"slug,omitempty"`   // Auto-generated if empty
	Locale     string          `json:"locale,omitempty"` // BCP 47; empty stores the site language
	Attributes json.RawMessage `json:"attributes"`
	Metadata   json.RawMessage `json:"metadata,omitempty"`
	ParentID   *uuid.UUID      `json:"parent_id,omitempty"`
	AuthorID   *uuid.UUID      `json:"author_id,omitempty"`
	// BylinePersonID and BylineOrg set the byline (see Entity).
	BylinePersonID *uuid.UUID `json:"byline_person_id,omitempty"`
	BylineOrg      bool       `json:"byline_org,omitempty"`
	// Status is set by the service for a person or a block or part (see lockedStatus); a caller
	// cannot choose it. Unset stores a draft.
	Status *EntityStatus `json:"-"`
}

CreateEntityInput represents input for creating a new entity.

type CreateSchemaInput

type CreateSchemaInput struct {
	Name        string          `json:"name"`
	Slug        string          `json:"slug,omitempty"` // Auto-generated if empty
	Description *string         `json:"description,omitempty"`
	Schema      json.RawMessage `json:"schema"`
	Settings    json.RawMessage `json:"settings,omitempty"`
}

CreateSchemaInput represents input for creating a new entity schema.

type Descendant

type Descendant struct {
	Entity *Entity
	// Between holds the slugs of the rows between the starting entity and
	// this one, root first (empty for a direct child).
	Between []string
}

Descendant is a published row below another entity.

type DocProblem

type DocProblem struct {
	Status int
	Code   string
	Msg    string
}

DocProblem is a refusal of a document: an HTTP-style status, a stable code and the message. Msg is not yet phrased as a sentence for people.

func CheckDocFor

func CheckDocFor(ctx context.Context, st BuilderStore, e *Entity, doc json.RawMessage) (*builder.Doc, string, *DocProblem)

CheckDocFor parses doc and runs every rule a stored page_doc of e must meet before the entity service sees it: structure, settings, slot and reference rules. It returns the parsed doc.

func CheckReuseDoc

func CheckReuseDoc(role string, e *Entity, d *builder.Doc, lookup func(id string) (*builder.Doc, bool)) *DocProblem

CheckReuseDoc applies the save rules of the reuse types to a doc about to be stored on e: a slot belongs to layout parts only (and a single or archive part has exactly one), and references must not loop. lookup reads a block's doc.

func ParseDoc

func ParseDoc(doc json.RawMessage) (*builder.Doc, *DocProblem)

ParseDoc parses doc and checks its settings: the first rules every stored page_doc meets.

func (*DocProblem) Error

func (p *DocProblem) Error() string

type DocState

type DocState int

DocState is what an entity's stored page_doc is to the editor choice.

const (
	DocNone   DocState = iota // no page_doc (yet)
	DocOK                     // a layout the builder opens
	DocBroken                 // a page_doc that does not parse
	DocLegacy                 // no page_doc, and page_sections that do not convert
)

func BuilderDoc

func BuilderDoc(e *Entity) (*builder.Doc, DocState)

BuilderDoc reads the page_doc stored on e.

type EditorSetting

type EditorSetting string

EditorSetting is settings.editor as stored. It decodes leniently: a value that is not a JSON string reads as "", so a stale or hand-edited setting can never make URLSettings fail and reset the schema's URL routing.

func (*EditorSetting) UnmarshalJSON

func (e *EditorSetting) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler.

type EditorialProvenance

type EditorialProvenance struct {
	ReviewedBy     string   `json:"reviewed_by,omitempty"`
	ReviewedAt     string   `json:"reviewed_at,omitempty"`
	FactChecked    bool     `json:"fact_checked,omitempty"`
	Sources        []string `json:"sources,omitempty"`
	LastVerifiedAt string   `json:"last_verified_at,omitempty"`
}

EditorialProvenance represents the editorial metadata stored on an entity.

type Entity

type Entity struct {
	ID          uuid.UUID       `json:"id"`
	SchemaID    uuid.UUID       `json:"schema_id"`
	Name        string          `json:"name"`
	Slug        string          `json:"slug"`
	Locale      string          `json:"locale"`
	Status      EntityStatus    `json:"status"`
	Attributes  json.RawMessage `json:"attributes"`
	Metadata    json.RawMessage `json:"metadata,omitempty"`
	Editorial   json.RawMessage `json:"editorial,omitempty"`
	Version     int             `json:"version"`
	CreatedBy   *uuid.UUID      `json:"created_by,omitempty"`
	UpdatedBy   *uuid.UUID      `json:"updated_by,omitempty"`
	CreatedAt   time.Time       `json:"created_at"`
	UpdatedAt   time.Time       `json:"updated_at"`
	PublishedAt *time.Time      `json:"published_at,omitempty"`

	// Hierarchy and content classification fields
	ParentID *uuid.UUID `json:"parent_id,omitempty"`
	// AuthorID references hatamoto_authors (a byline, not a user account).
	AuthorID *uuid.UUID `json:"author_id,omitempty"`
	// BylinePersonID is the person entity credited as the author; BylineOrg
	// credits the site organisation instead. They are exclusive. This is the
	// source of bylines; AuthorID is only dual-written so a rollback still
	// renders them. BylinePersonID has no foreign key: integrity is kept in code.
	BylinePersonID *uuid.UUID `json:"byline_person_id,omitempty"`
	BylineOrg      bool       `json:"byline_org,omitempty"`

	// Joined fields (not stored in DB)
	Schema *EntitySchema `json:"schema,omitempty"`
}

Entity represents a data entity following a schema.

type EntityLinkSync

type EntityLinkSync interface {
	// Reindex refreshes hatamoto_entity_link_index for sourceEntityID based on
	// whatever <a data-entity-id> tags currently live in its stored content.
	// See internal/linking/v2/indexer.go.
	Reindex(ctx context.Context, sourceEntityID uuid.UUID) error

	// RewriteForEntity rewrites the href of every <a data-entity-id="X">
	// across the codebase to newCanonicalURL when X's own slug changes. See internal/linking/v2/rewriter.go.
	RewriteForEntity(ctx context.Context, targetEntityID uuid.UUID, newCanonicalURL, reason string, actor *uuid.UUID) error

	// ResolveCanonical returns the post-update canonical URL for the entity
	// whose slug changed, so the rewriter has a destination href.
	ResolveCanonical(ent *Entity) (string, error)
}

EntityLinkSync is the v2 linking surface the entity service calls after a successful update. Kept as an interface so:

  • entity does not need to import internal/linking/v2 (one-way dep)
  • tests can pass a noop and assert without DB

Both methods must accept a fresh context.Background() — the request context the HTTP handler holds is cancelled when the response writes, and the v2 sync runs in a goroutine that outlives the request.

type EntityLocation

type EntityLocation struct {
	// Path is the canonical URL path ("/blog/x", "/about").
	Path string
	// Section is true for a section landing page served at /{slug}.
	Section bool
	// ArchivePath and ArchiveName describe the section index crumb that
	// precedes the entity; empty when the entity has none (no archive_path,
	// a section page, or a per-entity override).
	ArchivePath string
	ArchiveName string
}

EntityLocation is where one entity lives on the public site.

type EntityRepository

type EntityRepository interface {
	Create(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)
	GetByID(ctx context.Context, id uuid.UUID) (*Entity, error)
	GetBySlug(ctx context.Context, slug string) (*Entity, error)
	Update(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)
	UpdateStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)
	Delete(ctx context.Context, id uuid.UUID) error
	List(ctx context.Context, filter *ListEntitiesFilter, page *KeysetPage) (*PagedResult[*Entity], error)
	ListWithOffset(ctx context.Context, filter *ListEntitiesFilter, page *OffsetPage) (*OffsetPagedResult[*Entity], error)
	CountBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)
	CountByParent(ctx context.Context, limit int) ([]ParentCount, error)
	CountChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)

	// Version operations
	CreateVersion(ctx context.Context, entityID uuid.UUID, version int, attributes json.RawMessage, changedBy *uuid.UUID, changeSummary *string) (*EntityVersion, error)
	PruneVersions(ctx context.Context, keep int) (int64, error)
	GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)
	ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)
}

EntityRepository defines the interface for entity data operations.

type EntitySchema

type EntitySchema struct {
	ID          uuid.UUID       `json:"id"`
	Name        string          `json:"name"`
	Slug        string          `json:"slug"`
	Description *string         `json:"description,omitempty"`
	Schema      json.RawMessage `json:"schema"`
	IsBuiltin   bool            `json:"is_builtin"`
	CreatedAt   time.Time       `json:"created_at"`
	UpdatedAt   time.Time       `json:"updated_at"`
	// Settings holds optional per-schema routing configuration (see
	// SchemaURLSettings). An empty object means "use the built-in defaults".
	Settings json.RawMessage `json:"settings"`
}

EntitySchema represents a JSON Schema definition for entities.

func (*EntitySchema) URLSettings

func (s *EntitySchema) URLSettings() (SchemaURLSettings, error)

URLSettings decodes Settings. An empty or null value yields zero settings.

type EntityStatus

type EntityStatus string

EntityStatus represents the status of an entity.

const (
	StatusDraft EntityStatus = "draft"
	// StatusScheduled holds an entity that is finished but not yet due.
	// The public repository filters on status = 'published', so a scheduled
	// row is invisible to readers for free — no date guard is needed on the
	// eight public query sites. worker.PublishWorker flips it to published
	// once published_at falls due, preserving published_at rather than
	// stamping now() the way UpdateStatus does.
	StatusScheduled EntityStatus = "scheduled"
	StatusPublished EntityStatus = "published"
	StatusArchived  EntityStatus = "archived"
)

Available entity statuses.

func ParseStatus

func ParseStatus(s string) (EntityStatus, error)

ParseStatus parses a string into an EntityStatus.

type EntityURLBuilder

type EntityURLBuilder func(baseURL, slug string) string

EntityURLBuilder returns the canonical public URL for an entity given the request's baseURL and the entity's slug. baseURL is per-request (Server.publicBaseURL(r): PUBLIC_BASE_URL, else the request origin) and per-tenant, so it must be supplied to the builder rather than assumed — every other branch of BuildEntityURLForJSONLD returns an absolute baseURL+path URL, and a relative path here would produce an invalid relative @id/url in JSON-LD.

func LookupEntityURLBuilder

func LookupEntityURLBuilder(schemaSlug string) (EntityURLBuilder, bool)

LookupEntityURLBuilder returns the registered URL builder for the given schema slug, or ok=false when none is registered.

type EntityVersion

type EntityVersion struct {
	ID            uuid.UUID       `json:"id"`
	EntityID      uuid.UUID       `json:"entity_id"`
	Version       int             `json:"version"`
	Attributes    json.RawMessage `json:"attributes"`
	ChangedBy     *uuid.UUID      `json:"changed_by,omitempty"`
	ChangeSummary *string         `json:"change_summary,omitempty"`
	CreatedAt     time.Time       `json:"created_at"`
}

EntityVersion represents a historical version of an entity's attributes.

type Event

type Event struct {
	Kind       EventKind
	ID         uuid.UUID
	SchemaSlug string
	Slug       string
	Locale     string
	Name       string
	Status     string
	Version    int
	// Import is set for EventImportCompleted only; the entity fields are zero.
	Import *ImportSummary
}

Event describes a completed entity mutation.

type EventKind

type EventKind string

EventKind names an entity lifecycle event.

const (
	EventCreated   EventKind = "entity.created"
	EventUpdated   EventKind = "entity.updated"
	EventPublished EventKind = "entity.published"
	EventDeleted   EventKind = "entity.deleted"
	// EventImportCompleted is emitted once per Import batch, in place of the
	// per-entity created/published events.
	EventImportCompleted EventKind = "import.completed"
)

type EventSink

type EventSink interface {
	Emit(ctx context.Context, ev Event)
}

EventSink receives entity events after a mutation has been persisted. Emit is called inline from the service, so implementations must return quickly and hand real work to a goroutine; the request context may be cancelled as soon as the response is written.

type EvidenceLinkForJSONLD

type EvidenceLinkForJSONLD struct {
	FieldPath       string
	LinkType        string
	ConfidenceScore int
	VerifiedAt      *string // ISO 8601 timestamp, nil if not verified
	SourceTitle     string
	SourceType      string
	// SourceURL is the URL used in citation blocks. For external_url sources
	// this is the user-supplied URL; for internal entity-backed sources the
	// server layer resolves the entity's canonical URL before passing in.
	SourceURL string
	// SourceURLType is the Schema.org @type of the citation block. Defaults
	// to "CreativeWork" when empty. Server layer sets this to "Article" /
	// "Person" / etc. for internal entity sources so LLMs and crawlers
	// see a specific type instead of the generic fallback.
	SourceURLType string
}

EvidenceLinkForJSONLD is a lightweight struct for passing evidence data into the JSON-LD enrichment function, avoiding a circular import of the evidence package.

type FAQItem

type FAQItem struct {
	Question string
	Answer   string
}

FAQItem represents a question-answer pair for FAQ JSON-LD generation.

func ExtractFAQItems

func ExtractFAQItems(content string) []FAQItem

ExtractFAQItems reads the question and answer pairs of the FAQ blocks the blog editor writes into rich text:

<div class="faq"><div class="faq-item"><h3 class="faq-q">Why?</h3><div class="faq-a"><p>Because.</p></div></div></div>

Items without both a question and an answer are skipped. The public page and the editor's SEO panel both use it, so what the panel promises (FAQPage) is what the page emits.

type FieldKindResolver

type FieldKindResolver func(sectionType string, path []string) string

FieldKindResolver reports the x-hm-field kind ("richtext", "html", ...) of the section-content field at path for a section type, or "" when the type or field is unknown. path holds object keys only; array indices are skipped.

type HierarchyStore

type HierarchyStore interface {
	// Ancestors returns the parent chain of id, root first, without id itself.
	// A chain deeper than urlpattern.MaxPathDepth is cut at that depth, which
	// is also what ends a cycle.
	Ancestors(ctx context.Context, id uuid.UUID) ([]BreadcrumbPathItem, error)
	// Descendants returns every published descendant of id (children,
	// grandchildren, ...), parents before children.
	Descendants(ctx context.Context, id uuid.UUID) ([]*Descendant, error)
	// SubtreeHeight is how many levels of rows (any status) lie below id: 0
	// for a leaf, 1 with children only. Counting stops at twice
	// urlpattern.MaxPathDepth, which is also what ends a cycle.
	SubtreeHeight(ctx context.Context, id uuid.UUID) (int, error)
	// HasChildren reports whether any row has id as its parent.
	HasChildren(ctx context.Context, id uuid.UUID) (bool, error)
	// Child returns the base row (no variant) with this slug directly under
	// parent, or a root row when parent is nil; ErrEntityNotFound when none.
	// Slugs are unique per schema, not among all siblings: schemaSlug narrows
	// the lookup to one schema, and without it ErrAmbiguousSlug is returned
	// when rows of several schemas match.
	Child(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)
}

HierarchyStore reads the parent tree. PostgresEntityRepository implements it; the service uses it when its repository does.

type ImportBatch

type ImportBatch struct {
	// Source labels the origin ("wordpress", "archive"); it is stored in the
	// entity metadata and sent in import.completed.
	Source string
	Items  []ImportItem
}

ImportBatch is what one Import call stores.

type ImportItem

type ImportItem struct {
	// Key is the idempotency key, unique per source entity (for example
	// "wp:post:123"). An item whose key was imported before updates that
	// entity in place instead of creating another.
	Key        string
	SchemaSlug string
	Name       string
	Slug       string // generated from Name when empty
	Locale     string // BCP 47 tag; empty is the site language on insert and unchanged on update
	Status     EntityStatus
	Attributes json.RawMessage
	Metadata   json.RawMessage

	// ParentKey names the parent by import key (a row of this batch or of an
	// earlier one); ParentID by id. At most one is set.
	ParentKey string
	ParentID  *uuid.UUID

	// AuthorID is a legacy hatamoto_authors id; AuthorName resolves a person by name.
	// Without either, a plain-text attributes.author is resolved like a save.
	AuthorID   *uuid.UUID
	AuthorName string

	// BylinePersonID credits a person entity; BylineOrg the site organisation.
	BylinePersonID *uuid.UUID
	BylineOrg      bool

	// Dates; zero values default to now (created/updated) and, for a
	// published item, to its creation date (published).
	PublishedAt, CreatedAt, UpdatedAt time.Time
}

ImportItem is one entity to import.

type ImportItemResult

type ImportItemResult struct {
	Key     string
	ID      uuid.UUID
	Created bool
	Err     error
}

ImportItemResult is the outcome of one item.

type ImportRecord

type ImportRecord struct {
	ID         *uuid.UUID // existing entity to update; nil inserts
	SchemaID   uuid.UUID
	Name, Slug string
	Status     EntityStatus
	Attributes json.RawMessage
	Metadata   json.RawMessage
	ParentID   *uuid.UUID
	AuthorID   *uuid.UUID
	ActorID    *uuid.UUID

	BylinePersonID *uuid.UUID
	BylineOrg      bool

	// Locale is a BCP 47 tag; empty inserts the site language and keeps the
	// stored value on update.
	Locale string

	CreatedAt, UpdatedAt, PublishedAt time.Time
}

ImportRecord is one validated, sanitised row ready to store.

type ImportResult

type ImportResult struct {
	Created, Updated, Failed int
	Items                    []ImportItemResult
}

ImportResult summarises a batch. Items are in the order of the input.

type ImportStore

type ImportStore interface {
	// GetByImportKey returns the entity imported under key, ErrEntityNotFound
	// when there is none.
	GetByImportKey(ctx context.Context, key string) (*Entity, error)
	// ImportUpsert inserts rec, or updates the entity rec.ID names, keeping
	// the dates rec carries (the trigger-free import path). created is true
	// for an insert.
	ImportUpsert(ctx context.Context, rec *ImportRecord) (e *Entity, created bool, err error)
}

ImportStore is the persistence the importer needs. PostgresEntityRepository implements it.

type ImportSummary

type ImportSummary struct {
	Source                   string
	Created, Updated, Failed int
}

ImportSummary is the payload of the import.completed event.

type JSONLDBuilder

type JSONLDBuilder func(jsonLD map[string]any, entity *Entity, attrs map[string]any, baseURL string)

JSONLDBuilder renders JSON-LD fields for an entity of a given schema slug directly into jsonLD, mirroring the shape of the in-package buildXJSONLD helpers (e.g. buildArticleJSONLD).

func LookupJSONLDBuilder

func LookupJSONLDBuilder(schemaSlug string) (JSONLDBuilder, bool)

LookupJSONLDBuilder returns the registered JSON-LD builder for the given schema slug, or ok=false when none is registered.

type JSONLDEnricher

type JSONLDEnricher func(ent *Entity, baseURL string, doc map[string]any)

JSONLDEnricher post-processes a generated JSON-LD document in place. It is the host's single seam for the jsonld.enrich filter; GenerateJSONLD has no context, so the host binding supplies its own (see pluginhost.BindHostHooks).

type KeysetPage

type KeysetPage struct {
	Cursor    *string `json:"cursor,omitempty"` // Base64-encoded cursor
	Limit     int     `json:"limit"`
	Direction string  `json:"direction"` // "next" or "prev"
}

KeysetPage represents keyset pagination parameters.

type ListEntitiesFilter

type ListEntitiesFilter struct {
	SchemaID *uuid.UUID `json:"schema_id,omitempty"`
	// ExcludeSchemaIDs drops the entities of these types.
	ExcludeSchemaIDs []uuid.UUID   `json:"exclude_schema_ids,omitempty"`
	Status           *EntityStatus `json:"status,omitempty"`
	Search           string        `json:"search,omitempty"`
	// AuthorID keeps entities with this byline.
	AuthorID *uuid.UUID `json:"author_id,omitempty"`
	// PersonID keeps entities credited to this person.
	PersonID *uuid.UUID `json:"person_id,omitempty"`
	// TermID keeps entities tagged with this term or one of its descendants.
	TermID *uuid.UUID `json:"term_id,omitempty"`
	// Under keeps the descendants (children, grandchildren, ...) of this entity.
	Under *uuid.UUID `json:"under,omitempty"`
	// UpdatedFrom (inclusive) and UpdatedBefore (exclusive) bound updated_at.
	UpdatedFrom   *time.Time `json:"updated_from,omitempty"`
	UpdatedBefore *time.Time `json:"updated_before,omitempty"`
	// Sort is name | status | updated | created (default updated, newest
	// first); Asc flips the direction. Only the offset listing honours it.
	Sort string `json:"sort,omitempty"`
	Asc  bool   `json:"asc,omitempty"`
}

ListEntitiesFilter represents filter options for listing entities.

type ListFilter

type ListFilter struct {
	// SchemaSlugs restricts to these schemas (empty: any schema).
	SchemaSlugs []string
	// TermID restricts to entities attached to the term.
	TermID uuid.UUID
	// AuthorID restricts to entities written by this author.
	AuthorID uuid.UUID
	// PersonID restricts to entities credited to this person.
	PersonID uuid.UUID
	// Search is a websearch-syntax query matched against the entity's
	// full-text vector (name, excerpt, content), best match first. Entities
	// marked no_index never appear in search results.
	Search string
	// Attr restricts to entities whose attribute Attr equals AttrEq (a string,
	// or the number/boolean AttrEq spells), or that have the attribute at all
	// when AttrEq is empty and AttrEqSet is false. With AttrEqSet an empty
	// AttrEq matches the empty string only. The match is a JSONB containment test (@>), served
	// by the GIN index on attributes. An invalid key matches nothing.
	Attr, AttrEq string
	AttrEqSet    bool
	// IncludeScheduled adds scheduled entities (published_at is their date) to
	// the published ones. Drafts and archived entities never appear.
	IncludeScheduled bool
	// Order is "" (newest first), "-published_at", "published_at", "name",
	// "-name", "updated_at", "-updated_at", "attr:<key>" or "-attr:<key>" (numeric values sort as numbers,
	// before the rest; entities lacking the attribute last). Anything else
	// gives the default order.
	Order string
	// ExcludeID leaves this entity out (e.id <> ExcludeID) when set.
	ExcludeID uuid.UUID
	// Offset skips this many more rows on top of (page-1)*perPage; clamped to
	// 0..MaxListOffset. The returned total stays the raw match count.
	Offset int
}

ListFilter narrows a public listing. Only published base rows (never variants) are listed; every set field must match.

type LiveFunc

type LiveFunc func(ctx context.Context, ids []uuid.UUID)

LiveFunc is told about entities that may just have started to answer at their URL: created, published, or imported. The server wires it to retire the automatic redirects of their path.

type OffsetPage

type OffsetPage struct {
	Page  int `json:"page"`
	Limit int `json:"limit"`
}

OffsetPage represents offset pagination parameters.

type OffsetPagedResult

type OffsetPagedResult[T any] struct {
	Items      []T `json:"items"`
	Total      int `json:"total"`
	Page       int `json:"page"`
	Limit      int `json:"limit"`
	TotalPages int `json:"total_pages"`
}

OffsetPagedResult represents a paginated result set using offset pagination.

type PagedResult

type PagedResult[T any] struct {
	Items      []T     `json:"items"`
	NextCursor *string `json:"next_cursor,omitempty"`
	PrevCursor *string `json:"prev_cursor,omitempty"`
	HasMore    bool    `json:"has_more"`
	Total      *int    `json:"total,omitempty"` // Only populated when requested
}

PagedResult represents a paginated result set.

type ParentCount

type ParentCount struct {
	Parent *Entity
	Count  int
}

ParentCount is one parent entity and how many entities sit directly under it.

type PathChangeFunc

type PathChangeFunc func(ctx context.Context, before, after *Entity, actor *uuid.UUID)

PathChangeFunc is told about an update that may have moved an entity (and, with it, its descendants) to another URL: a new slug or a new parent. The server wires it to create redirects and refresh internal links.

type PostgresEntityRepository

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

PostgresEntityRepository implements EntityRepository using PostgreSQL.

func NewPostgresEntityRepository

func NewPostgresEntityRepository(db *database.DB) *PostgresEntityRepository

NewPostgresEntityRepository creates a new PostgreSQL entity repository.

func (*PostgresEntityRepository) Ancestors

Ancestors implements HierarchyStore.

func (*PostgresEntityRepository) Child

func (r *PostgresEntityRepository) Child(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)

Child implements HierarchyStore.

func (*PostgresEntityRepository) CountByParent

func (r *PostgresEntityRepository) CountByParent(ctx context.Context, limit int) ([]ParentCount, error)

CountByParent returns the limit parents with the most children (most first), each with its child count, in one query (the admin list's Parent filter).

func (*PostgresEntityRepository) CountBySchemaStatus

func (r *PostgresEntityRepository) CountBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)

CountBySchemaStatus counts the catalog entities per schema and status in one grouped query (the admin list's type tabs and status select).

func (*PostgresEntityRepository) CountChildren

func (r *PostgresEntityRepository) CountChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)

CountChildren returns how many entities sit directly under each of parentIDs (redirected ones excluded, as in CountByParent); parents with none are absent.

func (*PostgresEntityRepository) Create

func (r *PostgresEntityRepository) Create(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)

Create inserts a new entity.

func (*PostgresEntityRepository) CreateVersion

func (r *PostgresEntityRepository) CreateVersion(ctx context.Context, entityID uuid.UUID, version int, attributes json.RawMessage, changedBy *uuid.UUID, changeSummary *string) (*EntityVersion, error)

CreateVersion creates a new version record for an entity.

The tenantID parameter is retained for API compatibility but no longer participates in the SQL (Path B Migration 079 dropped the column).

func (*PostgresEntityRepository) Delete

Delete removes an entity.

func (*PostgresEntityRepository) Descendants

func (r *PostgresEntityRepository) Descendants(ctx context.Context, id uuid.UUID) ([]*Descendant, error)

Descendants implements HierarchyStore.

func (*PostgresEntityRepository) GetByID

func (r *PostgresEntityRepository) GetByID(ctx context.Context, id uuid.UUID) (*Entity, error)

GetByID retrieves an entity by ID with its schema.

func (*PostgresEntityRepository) GetByImportKey

func (r *PostgresEntityRepository) GetByImportKey(ctx context.Context, key string) (*Entity, error)

GetByImportKey implements ImportStore.

func (*PostgresEntityRepository) GetBySlug

func (r *PostgresEntityRepository) GetBySlug(ctx context.Context, slug string) (*Entity, error)

GetBySlug retrieves an entity by slug.

func (*PostgresEntityRepository) GetVersion

func (r *PostgresEntityRepository) GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)

GetVersion retrieves a specific version of an entity.

func (*PostgresEntityRepository) HasChildren

func (r *PostgresEntityRepository) HasChildren(ctx context.Context, id uuid.UUID) (bool, error)

HasChildren implements HierarchyStore.

func (*PostgresEntityRepository) ImportUpsert

func (r *PostgresEntityRepository) ImportUpsert(ctx context.Context, rec *ImportRecord) (*Entity, bool, error)

ImportUpsert implements ImportStore.

func (*PostgresEntityRepository) List

List retrieves entities with filtering and keyset pagination.

func (*PostgresEntityRepository) ListVersions

func (r *PostgresEntityRepository) ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)

ListVersions retrieves version history for an entity.

func (*PostgresEntityRepository) ListWithOffset

ListWithOffset retrieves entities with filtering and offset pagination.

func (*PostgresEntityRepository) PruneVersions

func (r *PostgresEntityRepository) PruneVersions(ctx context.Context, keep int) (int64, error)

PruneVersions coalesces identical-consecutive versions and then keeps only the newest `keep` versions per entity. Returns the number of rows deleted.

func (*PostgresEntityRepository) ScanMentioning

func (r *PostgresEntityRepository) ScanMentioning(ctx context.Context, id uuid.UUID, fn func(*Entity) bool) error

ScanMentioning calls fn, newest first, for each entity other than id whose attributes contain the id as text, until fn returns false. It is a sequence scan: used only by the block usage list, never on the render path.

func (*PostgresEntityRepository) Schedule

func (r *PostgresEntityRepository) Schedule(ctx context.Context, id uuid.UUID, userID *uuid.UUID, at time.Time) (*Entity, error)

Schedule marks an entity scheduled for at: status scheduled and published_at at, which worker.PublishWorker promotes once it falls due.

func (*PostgresEntityRepository) SubtreeHeight

func (r *PostgresEntityRepository) SubtreeHeight(ctx context.Context, id uuid.UUID) (int, error)

SubtreeHeight implements HierarchyStore.

func (*PostgresEntityRepository) Update

func (r *PostgresEntityRepository) Update(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)

Update modifies an entity (creates a version snapshot first).

func (*PostgresEntityRepository) UpdateStatus

func (r *PostgresEntityRepository) UpdateStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)

UpdateStatus changes an entity's status.

type PostgresPublicRepository

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

PostgresPublicRepository implements PublicRepository using PostgreSQL.

func NewPostgresPublicRepository

func NewPostgresPublicRepository(db *database.DB) *PostgresPublicRepository

NewPostgresPublicRepository creates a new public repository.

func (*PostgresPublicRepository) GetAncestorSlugs

func (r *PostgresPublicRepository) GetAncestorSlugs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID][]string, error)

GetAncestorSlugs returns, for each of ids that has a parent, the slugs of its ancestors, root first (self excluded), in one recursive query. Ids without a parent are absent from the map. The walk is bounded like GetEntityPath.

func (*PostgresPublicRepository) GetEntityPath

func (r *PostgresPublicRepository) GetEntityPath(ctx context.Context, entityID uuid.UUID) ([]BreadcrumbPathItem, error)

GetEntityPath returns the ancestor chain for an entity using a recursive CTE. The result is ordered from root to the entity itself (e.g., [Home, Services, SEO, Enterprise SEO]).

func (*PostgresPublicRepository) GetHomepage

func (r *PostgresPublicRepository) GetHomepage(ctx context.Context) (*Entity, error)

GetHomepage retrieves the homepage entity. Resolution order:

  1. Explicit homepage_entity_id from tenant settings (if set and entity exists/published)
  2. Slug convention fallback: "home" → "homepage" → first published page

func (*PostgresPublicRepository) GetPublishedByID

func (r *PostgresPublicRepository) GetPublishedByID(ctx context.Context, entityID uuid.UUID) (*Entity, error)

GetPublishedByID retrieves a published entity by ID.

func (*PostgresPublicRepository) GetPublishedByIDs

func (r *PostgresPublicRepository) GetPublishedByIDs(ctx context.Context, ids []uuid.UUID) ([]*Entity, error)

GetPublishedByIDs retrieves multiple published entities by their IDs.

func (*PostgresPublicRepository) GetPublishedByPath

func (r *PostgresPublicRepository) GetPublishedByPath(ctx context.Context, segments []string, schemaSlugs []string) (*Entity, error)

GetPublishedByPath retrieves the published base entity of a schema in schemaSlugs that sits at the end of a parent chain: segments are slugs, root first, the last being the entity's own. Ancestors may be of any schema and status (a draft region page still gives its children their URL). Slugs are only unique among siblings, so the chain is walked from the root.

func (*PostgresPublicRepository) GetPublishedBySlug

func (r *PostgresPublicRepository) GetPublishedBySlug(ctx context.Context, slug string) (*Entity, error)

GetPublishedBySlug retrieves a published entity by slug.

func (*PostgresPublicRepository) GetPublishedBySlugAndSchema

func (r *PostgresPublicRepository) GetPublishedBySlugAndSchema(ctx context.Context, slug string, schemaSlug string) (*Entity, error)

GetPublishedBySlugAndSchema retrieves a published base entity by slug, restricted to a specific schema slug.

func (*PostgresPublicRepository) GetPublishedBySlugAndSchemas

func (r *PostgresPublicRepository) GetPublishedBySlugAndSchemas(ctx context.Context, slug string, schemaSlugs []string) (*Entity, error)

GetPublishedBySlugAndSchemas retrieves a published base entity by slug whose schema slug is in schemaSlugs. Deterministic when several rows share a slug.

func (*PostgresPublicRepository) ListAllPublished

func (r *PostgresPublicRepository) ListAllPublished(ctx context.Context) ([]*SitemapEntry, error)

ListAllPublished retrieves all published entities for sitemap generation.

func (*PostgresPublicRepository) ListAllPublishedWithAttrs

func (r *PostgresPublicRepository) ListAllPublishedWithAttrs(ctx context.Context, attrKeys []string) ([]*SitemapEntry, error)

ListAllPublishedWithAttrs implements PublicRepository.

func (*PostgresPublicRepository) ListAttrValues

func (r *PostgresPublicRepository) ListAttrValues(ctx context.Context, schemaSlugs []string, attrKey string, limit int) ([]AttrValueCount, error)

ListAttrValues returns the distinct non-empty string values of the attribute attrKey across the published base entities of the given schemas, with counts, sorted by value, at most limit (1..500). Entities where the attribute is not a string are ignored. One aggregate query; the key presence test uses the GIN index on attributes.

func (*PostgresPublicRepository) ListPublished

func (r *PostgresPublicRepository) ListPublished(ctx context.Context, schemaSlug string, page, perPage int) (*PublicPagedResult, error)

ListPublished retrieves paginated published base entities by schema.

func (*PostgresPublicRepository) ListPublishedByAttribute

func (r *PostgresPublicRepository) ListPublishedByAttribute(ctx context.Context, schemaSlugs []string, attrKey string, attrValue string, limit int, offset int) ([]*Entity, int, error)

ListPublishedByAttribute retrieves published entities where a JSONB attribute key matches the given value. Entities are filtered to the given schema slugs, ordered by published_at DESC, and limited to `limit` results. Only attribute keys in allowedAttributeKeys are permitted for security.

func (*PostgresPublicRepository) ListPublishedByAuthor

func (r *PostgresPublicRepository) ListPublishedByAuthor(ctx context.Context, schemaSlugs []string, memberSlug, memberName string, page, perPage int) (*PublicPagedResult, error)

ListPublishedByAuthor lists published base entities of schemaSlugs written by a team member, newest first, with the total. The match: author_entity_slug equals the slug; else an author object whose slug or name matches; else (no author object) an author string or author_name equal to the member's name.

func (*PostgresPublicRepository) ListPublishedBySchemas

func (r *PostgresPublicRepository) ListPublishedBySchemas(ctx context.Context, schemaSlugs []string, page, perPage int) (*PublicPagedResult, error)

ListPublishedBySchemas retrieves paginated published base entities (variant rows are plugin-owned and never listed) whose schema slug is in schemaSlugs.

func (*PostgresPublicRepository) ListPublishedBySchemasNoCount

func (r *PostgresPublicRepository) ListPublishedBySchemasNoCount(ctx context.Context, schemaSlugs []string, limit int) ([]*Entity, error)

ListPublishedBySchemasNoCount is the first page of ListPublishedBySchemas without the COUNT(*) query, for callers that only need the rows.

func (*PostgresPublicRepository) ListPublishedFiltered

func (r *PostgresPublicRepository) ListPublishedFiltered(ctx context.Context, f ListFilter, page, perPage int) ([]*Entity, int, error)

ListPublishedFiltered returns one page of published entities matching f and the total number of matches. Without a search the order is newest first.

func (*PostgresPublicRepository) ListSeriesRows

func (r *PostgresPublicRepository) ListSeriesRows(ctx context.Context, schemaSlugs []string, seriesSlug string) ([]SeriesRow, error)

ListSeriesRows returns the published and scheduled posts of schemaSlugs with attributes.series_slug = seriesSlug, oldest first. Scheduled parts are included so a series index can show what is coming; callers must not link them.

type PostgresSchemaRepository

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

PostgresSchemaRepository implements SchemaRepository using PostgreSQL.

func NewPostgresSchemaRepository

func NewPostgresSchemaRepository(db *database.DB) *PostgresSchemaRepository

NewPostgresSchemaRepository creates a new PostgreSQL schema repository.

func (*PostgresSchemaRepository) CountEntities

func (r *PostgresSchemaRepository) CountEntities(ctx context.Context, schemaID uuid.UUID) (int, error)

CountEntities returns the number of entities using a schema.

func (*PostgresSchemaRepository) Create

Create inserts a new entity schema.

func (*PostgresSchemaRepository) Delete

Delete removes an entity schema.

func (*PostgresSchemaRepository) GetByID

GetByID retrieves an entity schema by ID.

func (*PostgresSchemaRepository) GetBySlug

func (r *PostgresSchemaRepository) GetBySlug(ctx context.Context, slug string) (*EntitySchema, error)

GetBySlug retrieves an entity schema by slug.

func (*PostgresSchemaRepository) List

List retrieves all entity schemas for the tenant.

func (*PostgresSchemaRepository) Update

Update modifies an entity schema.

type PublicPagedResult

type PublicPagedResult struct {
	Items       []*Entity `json:"items"`
	Total       int       `json:"total"`
	Page        int       `json:"page"`
	PerPage     int       `json:"per_page"`
	TotalPages  int       `json:"total_pages"`
	HasNext     bool      `json:"has_next"`
	HasPrevious bool      `json:"has_previous"`
}

PublicPagedResult represents a paginated result for public pages.

type PublicRepository

type PublicRepository interface {
	// GetPublishedBySlug retrieves a published entity by slug for a tenant.
	GetPublishedBySlug(ctx context.Context, slug string) (*Entity, error)

	// GetPublishedByID retrieves a published entity by ID.
	GetPublishedByID(ctx context.Context, entityID uuid.UUID) (*Entity, error)

	// GetPublishedByIDs retrieves multiple published entities by their IDs.
	GetPublishedByIDs(ctx context.Context, ids []uuid.UUID) ([]*Entity, error)

	// ListPublished retrieves paginated published base entities of one schema.
	ListPublished(ctx context.Context, schemaSlug string, page, perPage int) (*PublicPagedResult, error)

	// ListPublishedBySchemas is ListPublished over several schema slugs (a
	// schema and its aliases).
	ListPublishedBySchemas(ctx context.Context, schemaSlugs []string, page, perPage int) (*PublicPagedResult, error)

	// ListAllPublished retrieves all published entities for sitemap generation.
	ListAllPublished(ctx context.Context) ([]*SitemapEntry, error)

	// ListAllPublishedWithAttrs is ListAllPublished that also hydrates
	// SitemapEntry.Attrs with the named top-level attributes (the ones schema
	// URL overrides compare against).
	ListAllPublishedWithAttrs(ctx context.Context, attrKeys []string) ([]*SitemapEntry, error)

	// GetHomepage retrieves the homepage entity (slug = "home" or first published page).
	GetHomepage(ctx context.Context) (*Entity, error)

	// GetPublishedBySlugAndSchema retrieves a published entity by slug filtered to a specific schema slug.
	GetPublishedBySlugAndSchema(ctx context.Context, slug string, schemaSlug string) (*Entity, error)

	// GetPublishedBySlugAndSchemas is GetPublishedBySlugAndSchema over several
	// schema slugs (a schema and its aliases).
	GetPublishedBySlugAndSchemas(ctx context.Context, slug string, schemaSlugs []string) (*Entity, error)

	// ListPublishedByAttribute retrieves published entities where a specific JSONB attribute matches a value.
	// Returns the matching entities, the total count, and any error.
	ListPublishedByAttribute(ctx context.Context, schemaSlugs []string, attrKey string, attrValue string, limit int, offset int) ([]*Entity, int, error)
}

PublicRepository provides read-only access to published entities. This bypasses RLS for public-facing content.

type RefUser

type RefUser struct {
	ID         uuid.UUID
	Name, Slug string
	SchemaSlug string
	Status     EntityStatus
	UpdatedAt  time.Time
}

RefUser is one entity that embeds a block: a page, another block or a part.

type ReuseStatus

type ReuseStatus struct {
	Block, Part bool
}

ReuseStatus says which reuse types are usable on this site. A type is unavailable when a site type of its own already owns the slug.

func EnsureReuseSchemas

func EnsureReuseSchemas(ctx context.Context, db SQLDB, log *slog.Logger) (ReuseStatus, error)

EnsureReuseSchemas creates the block and theme_part types as built-in rows when they are missing. It is plain data (no migration), idempotent and safe with two processes starting at once. A slug already owned by a site's own (non-built-in) type is left alone, logged, and reported unavailable.

type SQLDB

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

SQLDB is the slice of pgx EnsureReuseSchemas needs; *pgxpool.Pool and pgx.Tx satisfy it.

type SaveOpts

type SaveOpts struct {
	ChangeSummary string // "" keeps "Updated in the builder"
	Autosave      bool   // a background save: recorded as AutosaveSummary, coalesced with the last one
	Publish       bool   // flip the page live after the save
	// ExpectedVersion makes the write atomic with the version check: the store
	// compares it under its row lock and returns a *VersionConflictError.
	ExpectedVersion *int

	// AfterParse runs once the doc parsed and the role is known, before the
	// reuse rules; BeforeWrite runs after every check, before the write. A
	// non-nil result refuses the save.
	AfterParse  func(role string) *DocProblem
	BeforeWrite func() *DocProblem
	// Tune changes the update before it is written (the admin sets the title).
	Tune func(in *UpdateEntityInput)
	// PublishGate returns why a requested publish is skipped ("" publishes).
	PublishGate func(saved *Entity) string
}

SaveOpts tunes SaveBuilderDoc. Every field is optional.

type SavedDoc

type SavedDoc struct {
	Entity *Entity // as the service returned it, published when it was
	Parsed *builder.Doc
	// TakenSlug is the address Tune asked for that was in use; the entity kept its old one.
	TakenSlug string
	// PublishSkipped is the PublishGate's reason; PublishErr a failed status
	// change. Either way the doc is stored.
	PublishSkipped string
	PublishErr     error
}

SavedDoc is the outcome of a stored document.

type SchemaDefinition

type SchemaDefinition struct {
	Type          string                     `json:"type"`
	SchemaOrgType string                     `json:"x-schema-org-type,omitempty"`
	Properties    map[string]*SchemaProperty `json:"properties,omitempty"`
	Required      []string                   `json:"required,omitempty"`
}

SchemaDefinition represents the top-level JSON Schema with semantic annotations.

type SchemaLister

type SchemaLister interface {
	List(ctx context.Context) ([]*EntitySchema, error)
}

SchemaLister is the slice of SchemaRepository the registry needs.

type SchemaPatternFunc

type SchemaPatternFunc func(ctx context.Context, before, after *EntitySchema)

SchemaPatternFunc is told after a schema update changed where its entities live (its URL pattern or overrides). before and after are the schema as it was and as it is. The server wires it to leave a 301 at every old path.

type SchemaProperty

type SchemaProperty struct {
	Type              string                     `json:"type"`
	Format            string                     `json:"format,omitempty"`
	Title             string                     `json:"title,omitempty"`
	SchemaOrgType     string                     `json:"x-schema-org-type,omitempty"`
	SchemaOrgProperty string                     `json:"x-schema-org-property,omitempty"`
	Properties        map[string]*SchemaProperty `json:"properties,omitempty"`
	Items             *SchemaProperty            `json:"items,omitempty"`
	Required          []string                   `json:"required,omitempty"`
}

SchemaProperty represents a parsed JSON Schema property with its semantic annotations.

type SchemaRepository

type SchemaRepository interface {
	Create(ctx context.Context, input *CreateSchemaInput) (*EntitySchema, error)
	GetByID(ctx context.Context, id uuid.UUID) (*EntitySchema, error)
	GetBySlug(ctx context.Context, slug string) (*EntitySchema, error)
	Update(ctx context.Context, id uuid.UUID, input *UpdateSchemaInput) (*EntitySchema, error)
	Delete(ctx context.Context, id uuid.UUID) error
	List(ctx context.Context) ([]*EntitySchema, error)
	CountEntities(ctx context.Context, schemaID uuid.UUID) (int, error)
}

SchemaRepository defines the interface for entity schema data operations.

type SchemaSitemapSettings

type SchemaSitemapSettings struct {
	Priority   *float64 `json:"priority,omitempty"`
	ChangeFreq string   `json:"changefreq,omitempty"`
	Exclude    bool     `json:"exclude,omitempty"`
}

SchemaSitemapSettings tunes sitemap output for a schema.

type SchemaStatusCount

type SchemaStatusCount struct {
	SchemaID uuid.UUID
	Status   EntityStatus
	Count    int
}

SchemaStatusCount is how many entities of one schema have one status.

type SchemaURLConfig

type SchemaURLConfig struct {
	SchemaSlug string
	Name       string
	// Aliases are extra schema slugs resolving to this schema.
	Aliases     []string
	Pattern     *urlpattern.Pattern
	ArchivePath string
	// SitemapGroup is the sitemap group the schema's entries belong to.
	SitemapGroup string
	Overrides    []SchemaURLOverride
	Sitemap      SchemaSitemapSettings
	SectionPages bool
	// ArchiveTitle and ArchiveDescription are the trimmed settings values;
	// empty when unset.
	ArchiveTitle       string
	ArchiveDescription string
	// ArchiveCrumbLabel is the trimmed archive_crumb_label setting.
	ArchiveCrumbLabel string
	// Explicit is true when the schema's settings supplied url_pattern.
	Explicit bool
}

SchemaURLConfig is the effective routing configuration of one schema (settings merged over the built-in defaults).

type SchemaURLOverride

type SchemaURLOverride struct {
	Attribute    string `json:"attribute"`
	Equals       string `json:"equals"`
	URLPattern   string `json:"url_pattern"`
	SitemapGroup string `json:"sitemap_group,omitempty"`
}

SchemaURLOverride swaps the URL pattern (and sitemap group) for entities whose attribute `Attribute` equals `Equals`.

type SchemaURLSettings

type SchemaURLSettings struct {
	URLPattern   string                `json:"url_pattern,omitempty"`
	ArchivePath  string                `json:"archive_path,omitempty"`
	SitemapGroup string                `json:"sitemap_group,omitempty"`
	Aliases      []string              `json:"aliases,omitempty"`
	Overrides    []SchemaURLOverride   `json:"overrides,omitempty"`
	Sitemap      SchemaSitemapSettings `json:"sitemap"`
	SectionPages bool                  `json:"section_pages,omitempty"`
	// ArchiveTitle and ArchiveDescription override the archive page heading
	// and meta description (see MaxArchiveTitleLen / MaxArchiveDescriptionLen).
	ArchiveTitle       string `json:"archive_title,omitempty"`
	ArchiveDescription string `json:"archive_description,omitempty"`
	// ArchiveCrumbLabel is the section label in an entity's breadcrumb; it
	// falls back to ArchiveTitle, then the schema name.
	ArchiveCrumbLabel string `json:"archive_crumb_label,omitempty"`
	// SchemaOrgType is the JSON-LD @type for entities of schemas that have no
	// x-schema-org annotations and no plugin builder. Must be one of
	// AllowedSchemaOrgTypes (checked on write only). Types that require
	// fields the generic document cannot supply (FAQPage, Product, Event) are
	// excluded; use annotations or a plugin builder for those.
	SchemaOrgType string `json:"schema_org_type,omitempty"`
	// Editor selects the admin editing screen for the type: "" is the generic
	// form, EditorPost is the post (TipTap) editor used by blog posts.
	// Any other value, including a non-string, reads as "" (checked on write only).
	Editor EditorSetting `json:"editor,omitempty"`
}

SchemaURLSettings is the typed view of EntitySchema.Settings. All keys are optional; unknown keys are ignored so a site may carry extra data.

type SeriesRow

type SeriesRow struct {
	ID, Name, Slug string
	Status         EntityStatus
	Attributes     []byte
	PublishedAt    *time.Time
}

SeriesRow is one post of an editorial series, identity and timing only.

type Service

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

Service provides business logic for entity management.

func NewService

func NewService(schemaRepo SchemaRepository, entityRepo EntityRepository, logger *slog.Logger) *Service

NewService creates a new entity service.

func (*Service) ComputeDiff

func (s *Service) ComputeDiff(ctx context.Context, entityID uuid.UUID, fromVersion, toVersion int) ([]VersionDiff, error)

ComputeDiff computes the differences between two versions.

func (*Service) CountEntitiesByParent

func (s *Service) CountEntitiesByParent(ctx context.Context, limit int) ([]ParentCount, error)

CountEntitiesByParent returns the limit parents with the most children, with their counts.

func (*Service) CountEntitiesBySchemaStatus

func (s *Service) CountEntitiesBySchemaStatus(ctx context.Context) ([]SchemaStatusCount, error)

CountEntitiesBySchemaStatus counts entities per schema and status in one query.

func (*Service) CountEntityChildren

func (s *Service) CountEntityChildren(ctx context.Context, parentIDs []uuid.UUID) (map[uuid.UUID]int, error)

CountEntityChildren returns the direct child count of each of parentIDs (absent = none).

func (*Service) CreateEntity

func (s *Service) CreateEntity(ctx context.Context, userID *uuid.UUID, input *CreateEntityInput) (*Entity, error)

CreateEntity creates a new entity.

func (*Service) CreateSchema

func (s *Service) CreateSchema(ctx context.Context, input *CreateSchemaInput) (*EntitySchema, error)

CreateSchema creates a new entity schema.

func (*Service) DeleteEntity

func (s *Service) DeleteEntity(ctx context.Context, id uuid.UUID) error

DeleteEntity removes an entity.

func (*Service) DeleteSchema

func (s *Service) DeleteSchema(ctx context.Context, id uuid.UUID) error

DeleteSchema removes an entity schema.

func (*Service) GetEntity

func (s *Service) GetEntity(ctx context.Context, id uuid.UUID) (*Entity, error)

GetEntity retrieves an entity by ID.

func (*Service) GetEntityByParentSlug

func (s *Service) GetEntityByParentSlug(ctx context.Context, parent *uuid.UUID, slug, schemaSlug string) (*Entity, error)

GetEntityByParentSlug retrieves the row with slug directly under parent (a root row when parent is nil). Slugs are unique per schema, not among all siblings: with schemaSlug the lookup is exact; without it, rows of several schemas sharing the slug give ErrAmbiguousSlug rather than a guess.

func (*Service) GetEntityBySlug

func (s *Service) GetEntityBySlug(ctx context.Context, slug string) (*Entity, error)

GetEntityBySlug retrieves an entity by slug.

func (*Service) GetSchema

func (s *Service) GetSchema(ctx context.Context, id uuid.UUID) (*EntitySchema, error)

GetSchema retrieves an entity schema by ID.

func (*Service) GetSchemaBySlug

func (s *Service) GetSchemaBySlug(ctx context.Context, slug string) (*EntitySchema, error)

GetSchemaBySlug retrieves an entity schema by slug.

func (*Service) GetVersion

func (s *Service) GetVersion(ctx context.Context, entityID uuid.UUID, version int) (*EntityVersion, error)

GetVersion retrieves a specific version of an entity.

func (*Service) Import

func (s *Service) Import(ctx context.Context, actor *uuid.UUID, batch ImportBatch) (*ImportResult, error)

Import stores a batch of entities without any per-entity event: it sets dates, status, parent and author, validates attributes against the schema, sanitises them like a normal save, and emits ONE import.completed event for the whole batch (never entity.created / entity.published). Items fail individually (see ImportResult); the call itself fails only for an empty or oversized batch, or a repository without import support.

func (*Service) ListEntities

func (s *Service) ListEntities(ctx context.Context, filter *ListEntitiesFilter, page *KeysetPage) (*PagedResult[*Entity], error)

ListEntities retrieves entities with filtering and keyset pagination.

func (*Service) ListEntitiesWithOffset

func (s *Service) ListEntitiesWithOffset(ctx context.Context, filter *ListEntitiesFilter, page *OffsetPage) (*OffsetPagedResult[*Entity], error)

ListEntitiesWithOffset retrieves entities with filtering and offset pagination.

func (*Service) ListSchemas

func (s *Service) ListSchemas(ctx context.Context) ([]*EntitySchema, error)

ListSchemas retrieves all entity schemas.

func (*Service) ListVersions

func (s *Service) ListVersions(ctx context.Context, entityID uuid.UUID, limit int) ([]*EntityVersion, error)

ListVersions retrieves version history for an entity.

func (*Service) RefUsers

func (s *Service) RefUsers(ctx context.Context, blockID uuid.UUID, limit int) (users []RefUser, capped bool, err error)

RefUsers lists the entities whose page_doc holds a ref node to blockID, newest first, at most limit (MaxRefUsers when limit <= 0). capped is true when more exist. The block itself is never listed, and neither is an entity that only carries the uuid as text.

func (*Service) RestoreVersion

func (s *Service) RestoreVersion(ctx context.Context, entityID uuid.UUID, userID *uuid.UUID, version int) (*Entity, error)

RestoreVersion restores an entity to a previous version.

func (*Service) ReuseStatus

func (s *Service) ReuseStatus() ReuseStatus

ReuseStatus reports which reuse types are available.

func (*Service) ScheduleEntity

func (s *Service) ScheduleEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, at time.Time) (*Entity, error)

ScheduleEntity sets the entity to publish automatically at the given time.

func (*Service) SetBeforeSave

func (s *Service) SetBeforeSave(f BeforeSaveFunc)

SetBeforeSave installs the before_save hook; nil disables it.

func (*Service) SetBylines

func (s *Service) SetBylines(b BylineResolver)

SetBylines installs the people resolver; without one bylines stay plain text.

func (*Service) SetEvents

func (s *Service) SetEvents(e EventSink)

SetEvents installs the entity event sink. Safe to call before any request flows through the service; nil disables emission.

func (*Service) SetLinkSync

func (s *Service) SetLinkSync(sync EntityLinkSync)

SetLinkSync installs the v2 linking sync hook. Called by the server during dependency wiring (server.go) after both the entity service and the v2 package are constructed. Safe to call before any request flows through UpdateEntity.

func (*Service) SetLiveObserver

func (s *Service) SetLiveObserver(f LiveFunc)

SetLiveObserver installs the callback run after an entity was created, published or imported; nil disables it.

func (*Service) SetOnSchemaChange

func (s *Service) SetOnSchemaChange(fn func())

SetOnSchemaChange registers a callback run after a schema is created, updated or deleted (after the URL registry was invalidated), for example to purge caches derived from schema settings.

func (*Service) SetPathObserver

func (s *Service) SetPathObserver(f PathChangeFunc)

SetPathObserver installs the callback run after an update changed an entity's slug or parent; nil disables it.

func (*Service) SetReuseStatus

func (s *Service) SetReuseStatus(st ReuseStatus)

SetReuseStatus records what EnsureReuseSchemas found. It is called once at boot, before requests are served.

func (*Service) SetSchemaPatternObserver

func (s *Service) SetSchemaPatternObserver(f SchemaPatternFunc)

SetSchemaPatternObserver installs the callback run after a schema update changed its URL pattern or overrides; nil disables it.

func (*Service) URLRegistry

func (s *Service) URLRegistry() *URLRegistry

URLRegistry exposes the schema URL registry. It is invalidated by CreateSchema, UpdateSchema and DeleteSchema.

func (*Service) UpdateEntity

func (s *Service) UpdateEntity(ctx context.Context, id uuid.UUID, userID *uuid.UUID, input *UpdateEntityInput) (*Entity, error)

UpdateEntity modifies an entity.

func (*Service) UpdateEntityStatus

func (s *Service) UpdateEntityStatus(ctx context.Context, id uuid.UUID, userID *uuid.UUID, status EntityStatus) (*Entity, error)

UpdateEntityStatus changes an entity's status.

func (*Service) UpdateSchema

func (s *Service) UpdateSchema(ctx context.Context, id uuid.UUID, input *UpdateSchemaInput) (*EntitySchema, error)

UpdateSchema modifies an entity schema.

func (*Service) ValidateEntityAttributes

func (s *Service) ValidateEntityAttributes(ctx context.Context, schemaID uuid.UUID, attributes json.RawMessage) *ValidationResult

ValidateEntityAttributes validates attributes against a schema without creating an entity.

type SettingsError

type SettingsError struct{ Field, Msg string }

SettingsError is a schema settings value that is not acceptable. Field names the setting at fault ("url_pattern", "archive_path", "aliases", ...; empty when the settings as a whole are malformed), so callers need not parse the message.

func (*SettingsError) Error

func (e *SettingsError) Error() string

func (*SettingsError) Unwrap

func (e *SettingsError) Unwrap() error

type SitemapEntry

type SitemapEntry struct {
	// ID is the entity row id (not serialised); the paths endpoint matches it
	// against the configured homepage.
	ID            uuid.UUID `json:"-"`
	Slug          string    `json:"slug"`
	SchemaSlug    string    `json:"schema_slug"`
	UpdatedAt     string    `json:"updated_at"`
	Priority      float64   `json:"priority"`
	ChangeFreq    string    `json:"change_freq"`
	NoIndex       bool      `json:"no_index"`
	IncludeInLLMs bool      `json:"include_in_llms"`
	// EntityFormat is the value of attributes.format, used by the sitemap
	// generator to route playbook entities with format='online_report' to
	// /reports/{slug} instead of /playbooks/{slug} — reports have their
	// own canonical URL family even though they share the playbook schema.
	// Empty string for entities where attributes.format is unset or not a
	// string. Only playbook-schema entities use this today.
	EntityFormat string `json:"entity_format,omitempty"`
	// ParentSlug is the slug of the entity's parent ("" for roots); it fills
	// {parent} in URL patterns.
	ParentSlug string `json:"parent_slug,omitempty"`
	// AncestorPath is the slash-joined slugs of every ancestor, root first
	// ("" for roots); it fills {path}. ParentSlug is its last segment.
	AncestorPath string `json:"ancestor_path,omitempty"`
	// PublishedAt (created_at when unset) fills {year}, {month} and {day}.
	PublishedAt time.Time `json:"-"`
	// Attrs holds the top-level attributes requested through
	// ListAllPublishedWithAttrs, flattened to strings.
	Attrs map[string]string `json:"-"`
}

SitemapEntry represents an entry in the sitemap.

type URLRegistry

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

URLRegistry loads schema routing settings, caches the resulting router and is invalidated whenever a schema is created, updated or deleted. A short TTL bounds staleness for changes made by other processes.

func NewURLRegistry

func NewURLRegistry(lister SchemaLister, logger *slog.Logger) *URLRegistry

NewURLRegistry creates a registry over lister. logger may be nil.

func (*URLRegistry) Invalidate

func (r *URLRegistry) Invalidate()

Invalidate drops the cached snapshot; the next call reloads.

func (*URLRegistry) SetTimezone

func (r *URLRegistry) SetTimezone(fn func(context.Context) string)

SetTimezone installs the source of the site time zone used for date URL tokens. It takes effect when the snapshot next reloads (at most urlRegistryTTL), or at once after Invalidate.

func (*URLRegistry) Snapshot

func (r *URLRegistry) Snapshot(ctx context.Context) (*URLSnapshot, error)

Snapshot returns the cached snapshot, reloading when missing or expired.

func (*URLRegistry) URLFor

func (r *URLRegistry) URLFor(ctx context.Context, schemaSlug string, attrs map[string]string, v urlpattern.Vars) (string, bool, error)

URLFor renders the canonical path for an entity of schemaSlug (own slug or alias). The bool is false when no schema matches.

type URLSnapshot

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

URLSnapshot is an immutable view of every schema's routing configuration.

Locale: a snapshot resolves paths without a language. Entities carry a locale, but only those in the site language are public, and URLs never contain the locale. A second public locale would need the changes listed in the urlpattern package doc (a path token, Resolve returning the locale, a per-locale filter, hreflang, per-locale sitemaps and feeds, translation links, an admin switcher); none of it is built.

func BuildURLSnapshot

func BuildURLSnapshot(schemas []*EntitySchema, logger *slog.Logger) (*URLSnapshot, error)

BuildURLSnapshot merges schema settings over the defaults and builds the router. A schema whose stored settings are invalid is logged and routed with the defaults so one bad row cannot take the public site down.

func DefaultURLSnapshot

func DefaultURLSnapshot() *URLSnapshot

DefaultURLSnapshot is the routing of an install without schema settings: page-like schemas at /{slug}, the post family at /blog/{slug}.

func (*URLSnapshot) BuildURL

func (s *URLSnapshot) BuildURL(schemaSlug string, attrs map[string]string, v urlpattern.Vars) (string, bool, error)

BuildURL makes the snapshot usable as a linking URLBuilder: it renders the path for an entity of schemaSlug (own slug or alias). Unlike EntityPath it never falls back, so callers can tell an unknown schema from a real URL.

func (*URLSnapshot) Config

func (s *URLSnapshot) Config(schemaSlug string) (*SchemaURLConfig, bool)

Config returns the configuration for a schema slug or alias.

func (*URLSnapshot) Configs

func (s *URLSnapshot) Configs() []*SchemaURLConfig

Configs returns every schema's configuration ordered by schema slug.

func (*URLSnapshot) EntityPath

func (s *URLSnapshot) EntityPath(ent *Entity, ancestors []string) string

EntityPath is Locate for a loaded entity whose ancestor slugs (root first) are given. It falls back to "/{slug}" (the pre-pattern behaviour) when the schema is unknown or the slug is empty.

func (*URLSnapshot) IsSectionPage

func (s *URLSnapshot) IsSectionPage(schemaSlug, slug string) bool

IsSectionPage reports whether an entity of schemaSlug with the given slug is a section landing page: its schema has SectionPages and the slug equals a section prefix.

func (*URLSnapshot) Locate

func (s *URLSnapshot) Locate(schemaSlug string, attrs map[string]string, v urlpattern.Vars) (loc EntityLocation, found bool, err error)

Locate resolves the public path of an entity of schemaSlug (own slug or alias) from its URL variables. Precedence: section pages, then explicit schema settings (including a matching override), then a plugin-registered EntityURLBuilder, then the built-in defaults. found is false when nothing knows the schema; err is a urlpattern error (for example ErrMissingVar for an empty slug).

func (*URLSnapshot) Location

func (s *URLSnapshot) Location() *time.Location

Location is the time zone date URL tokens are rendered in.

func (*URLSnapshot) NeedsAncestors

func (s *URLSnapshot) NeedsAncestors(schemaSlug string, attrs map[string]string) bool

NeedsAncestors reports whether the pattern serving an entity of schemaSlug with attrs uses {parent} or {path}, i.e. whether the caller should load the entity's ancestor slugs.

func (*URLSnapshot) NeedsEntityVars

func (s *URLSnapshot) NeedsEntityVars(schemaSlug string) bool

NeedsEntityVars reports whether the pattern serving schemaSlug (base pattern or any override) uses {parent}, {path} or a date token, i.e. whether a URL can only be built from the loaded entity and not from its slug alone.

func (*URLSnapshot) OverrideAttributes

func (s *URLSnapshot) OverrideAttributes() []string

OverrideAttributes returns the attribute names any schema override compares against (sorted, unique), i.e. the attributes a bulk listing must carry to pick each entity's pattern.

func (*URLSnapshot) PostArchivePath

func (s *URLSnapshot) PostArchivePath() string

PostArchivePath is the post archive path of the site ("/blog" by default).

func (*URLSnapshot) PostPath

func (s *URLSnapshot) PostPath(slug string) string

PostPath is the canonical path of the post with the given slug.

func (*URLSnapshot) Router

func (s *URLSnapshot) Router() *urlpattern.Router

Router returns the pattern router built from the schemas.

func (*URLSnapshot) SectionPageSchemas

func (s *URLSnapshot) SectionPageSchemas() []string

SectionPageSchemas returns the schema slugs (own slug plus aliases) of every schema with SectionPages enabled.

func (*URLSnapshot) SectionPrefixes

func (s *URLSnapshot) SectionPrefixes() map[string]bool

SectionPrefixes returns the first literal segment of every pattern (for example "post" for /post/{slug}). A page-like schema with SectionPages set serves a row whose slug equals such a prefix at /{slug}.

func (*URLSnapshot) VarsFor

func (s *URLSnapshot) VarsFor(ent *Entity, ancestors []string) urlpattern.Vars

VarsFor builds the URL variables of ent. ancestors are the slugs of its parent chain, root first (nil for a root). The date tokens read published_at (created_at while it is unset, for drafts and previews) in the site time zone.

func (*URLSnapshot) VarsForSitemap

func (s *URLSnapshot) VarsForSitemap(e *SitemapEntry) urlpattern.Vars

VarsForSitemap is VarsFor for a sitemap row.

type UpdateEntityInput

type UpdateEntityInput struct {
	Name          *string          `json:"name,omitempty"`
	Slug          *string          `json:"slug,omitempty"`
	Locale        *string          `json:"locale,omitempty"` // re-tags the entity; nil leaves it as stored
	Attributes    *json.RawMessage `json:"attributes,omitempty"`
	Metadata      *json.RawMessage `json:"metadata,omitempty"`
	ChangeSummary *string          `json:"change_summary,omitempty"`
	// Autosave marks a background save. It is recorded with AutosaveSummary,
	// and a run of autosaves by one person within AutosaveWindow keeps a single
	// version (the state before the run), so history holds checkpoints, not
	// every pause in typing. Explicit saves always create a version.
	Autosave bool `json:"-"`
	// ExpectedVersion, when set, makes the write conditional: the repository
	// compares it with the stored version under its row lock and returns a
	// *VersionConflictError instead of writing when they differ.
	ExpectedVersion *int `json:"-"`
	// KeepUpdatedAt leaves updated_at as it is (the version still advances).
	// Internal: used by `builder convert`, so a lossless shape
	// change does not look like a content edit (JSON-LD dateModified, sitemap lastmod).
	KeepUpdatedAt bool `json:"-"`
	// ParentID moves the entity under a parent; ClearParent makes it a root.
	// Both unset leaves the parent alone. The same holds for AuthorID.
	ParentID    *uuid.UUID `json:"parent_id,omitempty"`
	ClearParent bool       `json:"clear_parent,omitempty"`
	AuthorID    *uuid.UUID `json:"author_id,omitempty"`
	ClearAuthor bool       `json:"clear_author,omitempty"`
	// BylinePersonID sets the credited person and ClearBylinePerson removes
	// it; BylineOrg sets or clears the site-organisation byline. Both unset
	// leaves the byline alone. The columns are independent: a caller that
	// switches from a person to the organisation sets both.
	BylinePersonID    *uuid.UUID `json:"byline_person_id,omitempty"`
	ClearBylinePerson bool       `json:"clear_byline_person,omitempty"`
	BylineOrg         *bool      `json:"byline_org,omitempty"`
	// Status is set by the service for a person or a block/part (see lockedStatus); unset
	// leaves the status alone.
	Status *EntityStatus `json:"-"`
}

UpdateEntityInput represents input for updating an entity.

type UpdateSchemaInput

type UpdateSchemaInput struct {
	Name        *string          `json:"name,omitempty"`
	Description *string          `json:"description,omitempty"`
	Schema      *json.RawMessage `json:"schema,omitempty"`
	Settings    *json.RawMessage `json:"settings,omitempty"`
}

UpdateSchemaInput represents input for updating an entity schema.

type UpdateStatusInput

type UpdateStatusInput struct {
	Status EntityStatus `json:"status"`
}

UpdateStatusInput represents input for changing an entity's status.

type ValidationError

type ValidationError struct {
	Path    string `json:"path"`
	Message string `json:"message"`
	// Keyword is the JSON Schema keyword that failed ("required", "maxLength",
	// ...), set on the leaf errors; the admin forms map it to friendly text.
	Keyword string `json:"keyword,omitempty"`
}

ValidationError represents a single validation error.

type ValidationResult

type ValidationResult struct {
	Valid  bool              `json:"valid"`
	Errors []ValidationError `json:"errors,omitempty"`
}

ValidationResult holds detailed validation results.

type Validator

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

Validator validates entity attributes against JSON schemas.

func NewValidator

func NewValidator() *Validator

NewValidator creates a new JSON Schema validator.

func (*Validator) ValidateAttributes

func (v *Validator) ValidateAttributes(schemaJSON, attributesJSON json.RawMessage) error

ValidateAttributes validates entity attributes against a schema.

func (*Validator) ValidateSchema

func (v *Validator) ValidateSchema(schemaJSON json.RawMessage) error

ValidateSchema validates that a JSON schema is itself valid.

It performs three checks in order:

  1. The bytes parse as JSON and the root has a `type`.
  2. The shape is something the Phase 3 visual builder can edit (ValidateSchemaShape) — bounded primitive set, every property typed, nested objects/arrays well-formed, no surprises that would crash the editor at render time.
  3. The schema compiles as JSON Schema Draft 2020-12 so the entity validator can later use it to check instances.

Any failure returns ErrInvalidSchema wrapped with a path-prefixed reason so the admin UI can surface "properties.foo.type missing" rather than a generic compile error.

func (*Validator) ValidateWithDetails

func (v *Validator) ValidateWithDetails(schemaJSON, attributesJSON json.RawMessage) *ValidationResult

ValidateWithDetails validates attributes and returns detailed results.

type VersionConflictError

type VersionConflictError struct{ Current int }

VersionConflictError is a conditional write (UpdateEntityInput.ExpectedVersion) that found another version stored. It matches ErrVersionConflict.

func (*VersionConflictError) Error

func (e *VersionConflictError) Error() string

func (*VersionConflictError) Is

func (e *VersionConflictError) Is(target error) bool

type VersionDiff

type VersionDiff struct {
	Field    string `json:"field"`
	OldValue any    `json:"old_value"`
	NewValue any    `json:"new_value"`
}

VersionDiff represents a difference between two entity versions.

Jump to

Keyboard shortcuts

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