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
- Variables
- func Handler(s *Store) http.Handler
- func RequireAll(schema string, ids []TaxonomyID) (string, map[string]any, error)
- type AssignOptions
- type Assignment
- type AssignmentState
- type BrowseHit
- type BrowseOptions
- type BrowsePage
- type Count
- type Edge
- type EffectiveTag
- type EffectiveTagsOf
- type LanguageMode
- type ListOptions
- type MergeReport
- type Name
- type NameKind
- type Node
- type NodeDetail
- type NodeInput
- type NodePage
- type NodeRow
- type NodeUpdate
- type Options
- type Relation
- type Scope
- type Sort
- type State
- type Store
- func (s *Store) AddEdges(ctx context.Context, edges []Edge) error
- func (s *Store) AddNames(ctx context.Context, id TaxonomyID, names []Name) error
- func (s *Store) Assign(ctx context.Context, assignments []Assignment, opts AssignOptions) error
- func (s *Store) Assignments(ctx context.Context, refs []contentref.ContentRef) (map[contentref.ContentKey][]Assignment, error)
- func (s *Store) Browse(ctx context.Context, opts BrowseOptions) (BrowsePage, error)
- func (s *Store) BuildKeywordDocuments(ctx context.Context, tenant, kind, language string, ...) ([]search.KeywordDocument, error)
- func (s *Store) Builder(next worker.BuildKeywordDocuments) worker.BuildKeywordDocuments
- func (s *Store) Counts(ctx context.Context, ids []TaxonomyID) (map[TaxonomyID][]Count, error)
- func (s *Store) CreateNodes(ctx context.Context, inputs []NodeInput) ([]Node, error)
- func (s *Store) Edges(ctx context.Context, ids []TaxonomyID) ([]Edge, error)
- func (s *Store) EffectiveTags(ctx context.Context, refs []contentref.ContentRef) (map[contentref.ContentKey][]EffectiveTag, error)
- func (s *Store) Kinds() []string
- func (s *Store) ListContent(ctx context.Context, tenant, kind, language, cursor string, limit int) ([]contentref.ContentRef, string, bool, error)
- func (s *Store) ListNodes(ctx context.Context, opts ListOptions) (NodePage, error)
- func (s *Store) Lister(next worker.ListContentPage) worker.ListContentPage
- func (s *Store) Merge(ctx context.Context, from, into TaxonomyID) (MergeReport, error)
- func (s *Store) Node(ctx context.Context, id TaxonomyID) (NodeDetail, error)
- func (s *Store) Nodes(ctx context.Context, ids []TaxonomyID) ([]Node, error)
- func (s *Store) RebuildCounts(ctx context.Context) (int, error)
- func (s *Store) RecountContent(ctx context.Context, refs []contentref.ContentRef) error
- func (s *Store) RecountNodes(ctx context.Context, ids []TaxonomyID) error
- func (s *Store) RemoveEdges(ctx context.Context, edges []Edge) error
- func (s *Store) RemoveNames(ctx context.Context, id TaxonomyID, names []Name) error
- func (s *Store) SetImportedTimestamps(ctx context.Context, id TaxonomyID, createdAt, updatedAt *time.Time) error
- func (s *Store) SetNames(ctx context.Context, id TaxonomyID, names []Name) error
- func (s *Store) Tenant() string
- func (s *Store) Unassign(ctx context.Context, assignments []Assignment, opts AssignOptions) error
- func (s *Store) UpdateNode(ctx context.Context, id TaxonomyID, update NodeUpdate) (Node, error)
- func (s *Store) WithSQLTx(tx *sql.Tx) *Store
- func (s *Store) WithTx(tx pgx.Tx) *Store
- type TaxonomyID
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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".
type Scope ¶
type Scope string
Scope tells whether an effective tag came from the work or the 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.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is one tenant's catalog over the host schema.
func (*Store) AddEdges ¶
AddEdges upserts edges between nodes of the tenant; an endpoint outside the tenant is ErrNotFound.
func (*Store) AddNames ¶
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 ¶
func (s *Store) Builder(next worker.BuildKeywordDocuments) worker.BuildKeywordDocuments
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 ¶
CreateNodes creates nodes with their names in one transaction. A taken slug or id is ErrConflict; an unregistered kind is ErrInvalid.
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) 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 ¶
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 ¶
func (s *Store) Lister(next worker.ListContentPage) worker.ListContentPage
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) RebuildCounts ¶
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 ¶
RemoveEdges deletes edges; unknown edges are ignored.
func (*Store) RemoveNames ¶
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) 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.
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).