taxonomy

package
v0.58.3 Latest Latest
Warning

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

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

Documentation

Overview

Package taxonomy is ContentKit's generic catalog: tenant-scoped nodes (tags, artists, creators, characters, series, seasons, voice actors, ...) with localized names and aliases, typed node relationships and typed assignments of host content (a work or one of its versions) to nodes. Effective tags of a version are the deduplicated union of its work's and its own assignments; a multi-node filter must hold on one eligible version. Per-language counts and typeahead documents derive from the host's keyword documents and eligibility join, never from triggers.

Index

Constants

View Source
const (
	CodeInvalidRequest = "invalid_request"
	CodeNotFound       = "not_found"
	CodeConflict       = "conflict"
	CodeInternal       = "internal_error"
)

Stable public error codes, the same vocabulary content uses. Clients branch on Code; Error is a human message and may change.

Variables

View Source
var (
	ErrInvalid  = errors.New("taxonomy: invalid input")
	ErrNotFound = errors.New("taxonomy: not found")
	ErrConflict = errors.New("taxonomy: conflict")
)

Sentinel errors; callers match them with errors.Is.

Functions

func Handler

func Handler(s *Store) http.Handler

Handler is the admin API of one tenant's store. Hosts mount it behind their own admin authorization; every route is scoped to the store's tenant and content ids stay opaque. Errors are JSON {"error", "code"} with 400 for ErrInvalid, 404 for ErrNotFound, 409 for ErrConflict and a sanitized 500 for everything else; the cause always reaches Options.Logger.

GET    /nodes?kind=&state=&id=&slug=&language=&language_mode=
              &name_prefix=&q=&content_kind=&min_count=
              &sort=&cursor=&offset=&limit=           -> NodePage
POST   /nodes                [NodeInput]          -> [Node]
GET    /nodes/{id}                                -> NodeDetail
PATCH  /nodes/{id}           NodeUpdate           -> Node
DELETE /nodes/{id}                                -> Node (state deleted)
PUT    /nodes/{id}/names     [Name]               replace
POST   /nodes/{id}/names     [Name]               add
DELETE /nodes/{id}/names     [Name]               remove
POST   /nodes/{id}/merge     {"into_taxonomy_id"} -> MergeReport
POST   /edges                [Edge]  ·  DELETE /edges [Edge]
POST   /assignments?suppress_counts=1 [Assignment] ·  DELETE /assignments [Assignment]
POST   /effective            [ContentRef]         -> [{content, tags}]
GET    /counts?taxonomy_id=…                      -> {id: [Count]}
POST   /counts/rebuild                            -> {"nodes": n}

func RequireAll

func RequireAll(schema string, ids []TaxonomyID) (string, map[string]any, error)

RequireAll returns a FilterSQL fragment and its args for search.Options and Browse: every node id must be effective (work or that version) on the candidate document sd, so a multi-node filter holds on one version. Combine with the host's own FilterSQL using AND. Reserved arg: taxonomy_ids.

Types

type AssignOptions

type AssignOptions struct {
	// SuppressCounts skips the per-node recount (bulk loads); run
	// RebuildCounts afterwards.
	SuppressCounts bool
}

AssignOptions controls count maintenance of one write.

type Assignment

type Assignment struct {
	contentref.ContentRef
	TaxonomyID     TaxonomyID      `json:"taxonomy_id"`
	Relation       string          `json:"relation"`
	State          AssignmentState `json:"state,omitempty"`
	SourceRevision int64           `json:"source_revision"`
}

Assignment links host content (the work, or one version when ContentVersionID is set) to a node under a host-defined relation such as tag, artist, publisher, character, series, installment, voice_actor, seller. An empty Relation defaults to the node's kind.

type AssignmentState

type AssignmentState string

AssignmentState: proposed assignments (for example from User Intelligence) are stored but never effective until a host accepts them.

const (
	AssignmentActive   AssignmentState = "active"
	AssignmentProposed AssignmentState = "proposed"
)

type BrowseHit

type BrowseHit struct {
	contentref.ContentRef
	Language string `json:"language"`
	Priority int32  `json:"priority"`
}

BrowseHit is one work represented by its lowest-priority eligible version.

type BrowseOptions

type BrowseOptions struct {
	ContentKind string
	Language    string
	RequireAll  []TaxonomyID
	// Eligibility is the request's host join (see search.Eligibility).
	// Reserved arg names: tenant, kind, language, limit, offset, taxonomy_ids.
	Eligibility *search.Eligibility
	// Limit defaults to 50 and is capped at search.MaxCandidateLimit.
	Limit  int
	Offset int
}

BrowseOptions lists works of one kind that have an eligible document in Language on which every RequireAll node is effective.

type BrowsePage

type BrowsePage struct {
	Hits    []BrowseHit `json:"hits"`
	HasMore bool        `json:"has_more"`
}

BrowsePage is one page of works ordered by content id.

type Count

type Count struct {
	ContentKind string `json:"content_kind"`
	Language    string `json:"language"`
	Count       int    `json:"count"`
}

Count is the number of distinct works of one content kind with an eligible document in one language whose effective assignments include the node.

type Edge

type Edge struct {
	From           TaxonomyID `json:"from_taxonomy_id"`
	Relation       Relation   `json:"relation"`
	To             TaxonomyID `json:"to_taxonomy_id"`
	SourceRevision int64      `json:"source_revision"`
}

Edge is one typed directed relationship between two nodes of a tenant.

type EffectiveTag

type EffectiveTag struct {
	TaxonomyID TaxonomyID `json:"taxonomy_id"`
	Kind       string     `json:"kind"`
	Slug       string     `json:"slug"`
	Relation   string     `json:"relation"`
	Scope      Scope      `json:"scope"`
}

EffectiveTag is one node effective on a reference.

type EffectiveTagsOf

type EffectiveTagsOf struct {
	Content contentref.ContentRef `json:"content"`
	Tags    []EffectiveTag        `json:"tags"`
}

EffectiveTagsOf is one reference with its effective tags.

type LanguageMode

type LanguageMode string

LanguageMode decides what a node without a canonical name in the request language gets.

const (
	// LanguageFallback takes the store's configured Languages in order, then
	// any language the node has. The default.
	LanguageFallback LanguageMode = ""
	// LanguageStrict lists the node with an empty Name.
	LanguageStrict LanguageMode = "strict"
	// LanguageRequired drops the node from the page.
	LanguageRequired LanguageMode = "required"
)

type ListOptions

type ListOptions struct {
	// Kind selects one registered node kind; empty lists every kind.
	Kind string
	// States lists nodes in any of these states; empty means active only.
	States []State
	// IDs selects exactly these nodes; empty does not filter.
	IDs []TaxonomyID
	// Slug selects one slug exactly.
	Slug string
	// Related keeps nodes with an edge pointing at this node; with Relation,
	// only that relation. The characters of a series are
	// {Related: seriesID, Relation: RelationMemberOf}.
	Related TaxonomyID
	// Relation narrows Related; empty accepts any relation.
	Relation Relation

	// Language resolves NodeRow.Name and scopes the counts Count, MinCount and
	// SortCount read. Empty uses the store's first configured language.
	Language string
	// LanguageMode decides the display name when the node has none in Language.
	LanguageMode LanguageMode
	// NamePrefix keeps nodes whose display name starts with it: the A-Z index.
	// Matched normalized, so "e" also matches "Étude".
	NamePrefix string
	// Query keeps nodes with a name or alias containing it, in any language.
	// Matched normalized; % and _ are literal.
	Query string
	// ContentKind scopes the count to one host content kind; empty sums them.
	ContentKind string
	// MinCount keeps nodes whose Count is at least this. 1 hides empty nodes.
	MinCount int

	// FilterSQL is trusted host SQL appended as AND (<FilterSQL>) against
	// node alias n. Use it for host-owned sidecar policy, never request SQL.
	// FilterArgs binds pgx @name placeholders. Library parameter names are
	// reserved even when their corresponding option is unset.
	FilterSQL  string
	FilterArgs map[string]any

	// OrderSQL is a trusted host ORDER BY expression for host-owned directory
	// ordering, such as phonetic names stored in a sidecar. It excludes Sort
	// and Cursor; use Offset. Node alias n is available, FilterArgs binds values,
	// and taxonomy_id is appended as a stable tiebreak. Never accept request SQL.
	OrderSQL string

	// Sort orders the page.
	Sort Sort

	// Cursor is the previous page's NextCursor. Keyset paging: it requires the
	// default Sort, excludes Offset, and skips the total — a full sync pages in
	// constant time instead of paying for a count per page.
	Cursor string
	// Offset pages from the start of the ordered result.
	Offset int
	// Limit defaults to 50 and is capped at 500.
	Limit int
}

ListOptions filters, orders and pages ListNodes. The zero value lists the tenant's active nodes by taxonomy_id, which is what a full admin sync wants; a catalog index page sets Language, Sort, ContentKind and Offset.

type MergeReport

type MergeReport struct {
	From              TaxonomyID `json:"from_taxonomy_id"`
	Into              TaxonomyID `json:"into_taxonomy_id"`
	AssignmentsMoved  int64      `json:"assignments_moved"`
	NamesMoved        int64      `json:"names_moved"`
	EdgesRewritten    int64      `json:"edges_rewritten"`
	AssignmentsMerged int64      `json:"assignments_merged"`
}

MergeReport summarizes one merge.

type Name

type Name struct {
	Language       string   `json:"language"`
	Kind           NameKind `json:"kind"`
	Name           string   `json:"name"`
	Normalized     string   `json:"normalized,omitempty"`
	SourceRevision int64    `json:"source_revision"`
}

Name is one localized canonical name or alias of a node.

type NameKind

type NameKind string

NameKind distinguishes the one canonical name per language from aliases.

const (
	NameCanonical NameKind = "name"
	NameAlias     NameKind = "alias"
)

type Node

type Node struct {
	TaxonomyID     TaxonomyID `json:"taxonomy_id"`
	TenantID       string     `json:"tenant_id"`
	Kind           string     `json:"kind"`
	Slug           string     `json:"slug"`
	State          State      `json:"state"`
	SourceRevision int64      `json:"source_revision"`
	CreatedAt      time.Time  `json:"created_at"`
	UpdatedAt      time.Time  `json:"updated_at"`
}

Node is one catalog record.

type NodeDetail

type NodeDetail struct {
	Node
	Names  []Name  `json:"names"`
	Edges  []Edge  `json:"edges"`
	Counts []Count `json:"counts"`
}

NodeDetail is a node with its names, edges in both directions and counts.

type NodeInput

type NodeInput struct {
	// TaxonomyID is optional; omitted ids are generated.
	TaxonomyID     TaxonomyID `json:"taxonomy_id,omitempty"`
	Kind           string     `json:"kind"`
	Slug           string     `json:"slug"`
	Names          []Name     `json:"names,omitempty"`
	SourceRevision int64      `json:"source_revision"`
}

NodeInput creates one node with its initial names.

type NodePage

type NodePage struct {
	Nodes []NodeRow `json:"nodes"`
	// NextCursor pages the default Sort; empty on the last page and under any
	// other order.
	NextCursor string `json:"next_cursor,omitempty"`
	// Total is the unpaged match count, zero on a Cursor page.
	Total int `json:"total"`
}

NodePage is one page of nodes.

type NodeRow

type NodeRow struct {
	Node
	// Name is the canonical name resolved for ListOptions.Language, empty when
	// the node has none (see LanguageMode).
	Name string `json:"name,omitempty"`
	// NameLanguage is the language Name came from.
	NameLanguage string `json:"name_language,omitempty"`
	// Count is the number of distinct works of ContentKind with an eligible
	// document in Language whose effective assignments include the node.
	Count int `json:"count"`
}

NodeRow is a listed node with its display name and content count.

type NodeUpdate

type NodeUpdate struct {
	Slug           *string `json:"slug,omitempty"`
	State          *State  `json:"state,omitempty"`
	SourceRevision *int64  `json:"source_revision,omitempty"`
}

NodeUpdate changes a node's slug or state; nil fields are unchanged.

type Options

type Options struct {
	Pool   *pgxpool.Pool
	Schema string
	Tenant string
	// Kinds are the node kinds this tenant registers (tag, artist, creator,
	// character, series, season, voice_actor, listing, ...). Required; a kind
	// must never collide with a host content kind.
	Kinds []string
	// Languages are the document languages typeahead documents are built in
	// (one document per node and language). Required.
	Languages []string
	// CountEligibility is the host's public visibility join used to derive
	// counts (see search.Eligibility). Without it every document counts.
	// Reserved arg names: tenant, ids, taxonomy_kinds.
	CountEligibility *search.Eligibility
	// Logger receives the admin API access log: each request at DEBUG, a 5xx
	// at ERROR, both with the cause the response withheld. nil -> slog.Default().
	Logger *slog.Logger
}

Options configures one tenant's store.

type Relation

type Relation string

Relation of an edge (from, relation, to): "from is <relation> of to".

const (
	RelationAliasOf  Relation = "alias_of"
	RelationMemberOf Relation = "member_of"
	RelationArtistOf Relation = "artist_of"
	RelationVoiceOf  Relation = "voice_of"
	RelationParent   Relation = "parent"
	RelationChild    Relation = "child"
	RelationSynonym  Relation = "synonym"
)

type Scope

type Scope string

Scope tells whether an effective tag came from the work or the version.

const (
	ScopeContent Scope = "content"
	ScopeVersion Scope = "version"
)

type Sort

type Sort string

Sort orders a node page. Every order is made total by taxonomy_id, so a page boundary never repeats or drops a row.

const (
	// SortID orders by taxonomy_id ascending. The default, and the only order
	// ListOptions.Cursor can page.
	SortID Sort = ""
	// SortName orders by the resolved display name; nameless nodes come last.
	SortName Sort = "name"
	// SortCount orders by content count, largest first.
	SortCount Sort = "count"
	// SortCreated orders by creation time, newest first.
	SortCreated Sort = "created"
	// SortOldest orders by creation time, oldest first.
	SortOldest Sort = "oldest"
	// SortUpdated orders by last change, most recent first.
	SortUpdated Sort = "updated"
)

type State

type State string

State of a node. A merged node keeps its row and an alias_of edge to the surviving node; deleted nodes keep their assignments but serve nothing.

const (
	StateActive  State = "active"
	StateMerged  State = "merged"
	StateDeleted State = "deleted"
)

type Store

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

Store is one tenant's catalog over the host schema.

func New

func New(opts Options) (*Store, error)

New validates the options and returns the store.

func (*Store) AddEdges

func (s *Store) AddEdges(ctx context.Context, edges []Edge) error

AddEdges upserts edges between nodes of the tenant; an endpoint outside the tenant is ErrNotFound.

func (*Store) AddNames

func (s *Store) AddNames(ctx context.Context, id TaxonomyID, names []Name) error

AddNames upserts names and aliases; a new canonical name demotes the previous canonical name of that language to an alias.

func (*Store) Assign

func (s *Store) Assign(ctx context.Context, assignments []Assignment, opts AssignOptions) error

Assign upserts assignments to active nodes of the tenant. A node that is missing, merged, deleted or of another tenant is ErrNotFound. Hosts mark their content documents dirty in the same transaction.

func (*Store) Assignments

func (s *Store) Assignments(ctx context.Context, refs []contentref.ContentRef) (map[contentref.ContentKey][]Assignment, error)

Assignments returns the stored assignments of each reference exactly as scoped: a work reference returns work rows, a version reference returns that version's rows. See EffectiveTags for the union.

func (*Store) Browse

func (s *Store) Browse(ctx context.Context, opts BrowseOptions) (BrowsePage, error)

Browse runs the same per-document join as keyword search over the tenant's documents: the host decides eligibility of each version row, RequireAll holds on that very row, and works are grouped before paging.

func (*Store) BuildKeywordDocuments

func (s *Store) BuildKeywordDocuments(ctx context.Context, tenant, kind, language string, refs []contentref.ContentRef) ([]search.KeywordDocument, error)

BuildKeywordDocuments is the worker.BuildKeywordDocuments of taxonomy kinds: one document per active node and language titled by that language's canonical name (falling back to English, then any language), with the language's aliases and every other name as aliases and overflow as keywords. Missing, merged and deleted nodes yield no document.

func (*Store) Builder

Builder returns a worker builder that serves this store's kinds and delegates every other kind to next (the host's content builder).

func (*Store) Counts

func (s *Store) Counts(ctx context.Context, ids []TaxonomyID) (map[TaxonomyID][]Count, error)

Counts returns the per-kind, per-language counts of the given nodes.

func (*Store) CreateNodes

func (s *Store) CreateNodes(ctx context.Context, inputs []NodeInput) ([]Node, error)

CreateNodes creates nodes with their names in one transaction. A taken slug or id is ErrConflict; an unregistered kind is ErrInvalid.

func (*Store) Edges

func (s *Store) Edges(ctx context.Context, ids []TaxonomyID) ([]Edge, error)

Edges returns every edge touching the given nodes, in either direction.

func (*Store) EffectiveTags

func (s *Store) EffectiveTags(ctx context.Context, refs []contentref.ContentRef) (map[contentref.ContentKey][]EffectiveTag, error)

EffectiveTags returns, per reference, the deduplicated union of the work's active assignments and, for a version reference, that version's own. Only active nodes are effective; a work row wins over a version row for the same node and relation.

func (*Store) Kinds

func (s *Store) Kinds() []string

Kinds returns the registered node kinds.

func (*Store) ListContent

func (s *Store) ListContent(ctx context.Context, tenant, kind, language, cursor string, limit int) ([]contentref.ContentRef, string, bool, error)

ListContent is the worker.ListContentPage of taxonomy kinds: active nodes of the kind in taxonomy_id order.

func (*Store) ListNodes

func (s *Store) ListNodes(ctx context.Context, opts ListOptions) (NodePage, error)

ListNodes pages the tenant's nodes with their display name in the request language and their content count. The zero ListOptions keeps the historical behavior: active nodes ordered by taxonomy_id, paged by Cursor.

func (*Store) Lister

Lister returns a worker lister that enumerates this store's kinds for backfill and delegates every other kind to next.

func (*Store) Merge

func (s *Store) Merge(ctx context.Context, from, into TaxonomyID) (MergeReport, error)

Merge folds node from into node into of the same kind in one transaction: assignments are rewritten (duplicates collapse), names become aliases of into, edges are re-pointed, from becomes merged with an alias_of edge to into, counts and documents of both are rebuilt.

func (*Store) Node

func (s *Store) Node(ctx context.Context, id TaxonomyID) (NodeDetail, error)

Node returns one node of the tenant with names, edges and counts.

func (*Store) Nodes

func (s *Store) Nodes(ctx context.Context, ids []TaxonomyID) ([]Node, error)

Nodes returns the requested nodes of the tenant that exist, in input order.

func (*Store) RebuildCounts

func (s *Store) RebuildCounts(ctx context.Context) (int, error)

RebuildCounts recomputes every node's counts of the tenant in pages and returns the number of nodes rebuilt. Use it after suppressed bulk writes, document backfills or an eligibility change.

func (*Store) RecountContent

func (s *Store) RecountContent(ctx context.Context, refs []contentref.ContentRef) error

RecountContent recomputes the counts of every node assigned to the given works (any version). Hosts call it when a work's versions, languages or visibility change.

func (*Store) RecountNodes

func (s *Store) RecountNodes(ctx context.Context, ids []TaxonomyID) error

RecountNodes recomputes the counts of the given nodes.

func (*Store) RemoveEdges

func (s *Store) RemoveEdges(ctx context.Context, edges []Edge) error

RemoveEdges deletes edges; unknown edges are ignored.

func (*Store) RemoveNames

func (s *Store) RemoveNames(ctx context.Context, id TaxonomyID, names []Name) error

RemoveNames deletes the given names (matched by language and normalized form); unknown names are ignored.

func (*Store) SetImportedTimestamps added in v0.13.3

func (s *Store) SetImportedTimestamps(ctx context.Context, id TaxonomyID, createdAt, updatedAt *time.Time) error

SetImportedTimestamps preserves source chronology during a trusted import. Nil leaves a timestamp unchanged; at least one nonzero timestamp is required. This operation is deliberately separate from the public HTTP mutation inputs. Call it after the imported node's other mutations, which normally set updated_at to the current transaction time. It neither changes source_revision nor commits a transaction borrowed through WithTx or WithSQLTx.

func (*Store) SetNames

func (s *Store) SetNames(ctx context.Context, id TaxonomyID, names []Name) error

SetNames replaces every name and alias of the node.

func (*Store) Tenant

func (s *Store) Tenant() string

Tenant returns the tenant this store is scoped to.

func (*Store) Unassign

func (s *Store) Unassign(ctx context.Context, assignments []Assignment, opts AssignOptions) error

Unassign deletes assignments matched by content reference, node and relation (an empty relation matches the node's kind); unknown ones are ignored.

func (*Store) UpdateNode

func (s *Store) UpdateNode(ctx context.Context, id TaxonomyID, update NodeUpdate) (Node, error)

UpdateNode changes slug and state. Deleting a node removes its counts and documents; restoring rebuilds them. A merged node is never updated.

func (*Store) WithSQLTx added in v0.13.2

func (s *Store) WithSQLTx(tx *sql.Tx) *Store

WithSQLTx borrows an existing database/sql transaction using pgx's stdlib driver. The host owns commit, rollback and savepoints. Catalog writes, counts and dirty documents participate in that same transaction.

func (*Store) WithTx

func (s *Store) WithTx(tx pgx.Tx) *Store

WithTx returns the store bound to the host's transaction so catalog writes commit with the content change that caused them.

type TaxonomyID

type TaxonomyID = contentref.TaxonomyID

TaxonomyID identifies one node within a tenant. Hosts adopting existing tables keep their ids as text; omitted ids are generated (uuid).

Jump to

Keyboard shortcuts

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