import { type Kysely } from 'kysely'; import type { BatchStatement } from '../db/batch.js'; import type { Database, TaxonomyRow, TermRow } from '../db/schema.js'; /** * Taxonomies and their term trees. * * Terms deliberately carry no materialised path, unlike content items — see the 0003 migration for * the reasoning. Every tree query here runs off `parent_id` with a recursive CTE, the same * `WITH RECURSIVE` form that works identically on both drivers. */ export declare class TaxonomyError extends Error { readonly code: 'not_found' | 'duplicate_api_id' | 'cycle' | 'invalid_parent' | 'not_hierarchical' | 'in_use'; name: string; constructor(message: string, code?: 'not_found' | 'duplicate_api_id' | 'cycle' | 'invalid_parent' | 'not_hierarchical' | 'in_use'); } export declare function listTaxonomies(db: Kysely): Promise; export declare function getTaxonomy(db: Kysely, id: string): Promise; export declare function getTaxonomyByApiId(db: Kysely, apiId: string): Promise; export interface TaxonomyInput { api_id: string; name: string; name_plural: string; description?: string | null; hierarchical?: boolean; } export declare function createTaxonomy(db: Kysely, input: TaxonomyInput): Promise; /** `api_id` is immutable for the same reason a content type's is: code and integrations use it. */ export declare function updateTaxonomy(db: Kysely, id: string, input: Partial>): Promise; /** * Delete a taxonomy. * * Refuses while a field still points at it. The FK would cascade terms away happily, but every * content item tagged with them would keep stale ids in `data` and the field would silently * reference a taxonomy that no longer exists. */ export declare function deleteTaxonomy(db: Kysely, id: string): Promise; export interface TermNode extends TermRow { children: TermNode[]; } export declare function listTerms(db: Kysely, taxonomyId: string): Promise; export declare function getTerm(db: Kysely, id: string): Promise; /** Assemble the flat rows into a tree. Done in memory — one query, no N+1 over depth. */ export declare function buildTermTree(terms: TermRow[]): TermNode[]; /** * A term and every descendant, in one recursive query. * * "Everything under Academics" has to be one round trip rather than a walk, because it backs * ordinary reads: filtering a content list by a branch, and counting a term's usage before * offering to delete it. */ export declare function getTermSubtree(db: Kysely, rootId: string): Promise<{ id: string; depth: number; }[]>; export interface CreateTermInput { name: string; slug?: string; parentId?: string | null; description?: string | null; } export declare function createTerm(db: Kysely, taxonomyId: string, input: CreateTermInput): Promise; export interface UpdateTermInput { name?: string; slug?: string; parentId?: string | null; description?: string | null; } /** * Update a term, re-depthing its subtree when it moves. * * Without a materialised path there is nothing to rewrite on a move except `depth`, which still * has to cascade — a branch dragged one level up leaves every descendant claiming a depth that no * longer matches its position, and the admin renders the tree from it. */ export declare function updateTerm(handle: { db: Kysely; batch(statements: BatchStatement[]): Promise; }, id: string, input: UpdateTermInput): Promise; /** * Delete a term. * * Children are re-parented to the deleted term's parent rather than cascaded away — the FK is * `set null`, which would silently promote a whole branch to the root and lose its place in the * tree. Assignments to the term go with it, which is the intended meaning of removing a tag. */ export declare function deleteTerm(handle: { db: Kysely; batch(statements: BatchStatement[]): Promise; }, id: string): Promise; /** Persist a new sibling order. Positions are rewritten to match array order. */ export declare function reorderTerms(handle: { db: Kysely; batch(statements: BatchStatement[]): Promise; }, orderedTermIds: string[]): Promise; /** * Every item id carrying any term in a branch. * * Classification only. A term says what content is about, never who may edit it: roles are flat * and site-wide, and nothing anywhere derives a permission from a term. See SCOPE.md. */ /** * A term and everything beneath it, as ids. * * What a term filter actually means: filing something under "Sciences" should find it when someone * filters by "Academics". Separated from `itemIdsInTermBranch` because the item list wants to * *narrow a query* by the branch rather than pull every member id into memory and filter in JS — * which is what the public term archive does, and what stops being reasonable at a few thousand * items. */ export declare function termIdsForBranch(db: Kysely, rootTermId: string): Promise; export declare function itemIdsInTermBranch(db: Kysely, rootTermId: string): Promise; /** * Read a taxonomy field's value out of an item's `data` as a list of term ids. * * The field config decides whether the stored value is a single id or an array, so both shapes * have to be accepted here rather than assuming one — and a value saved before the config was * flipped between them still has to read correctly. */ export declare function termIdsFromValue(value: unknown): string[]; export interface AssignmentPlan { statements: BatchStatement[]; /** Referenced term ids that do not exist, keyed by the field `api_id` that referenced them. */ missing: Record; } /** * Build the statements that rebuild one item's assignment index from its authored data. * * The delete is unconditional rather than scoped to the taxonomy fields currently on the type. * Removing a taxonomy field from a content type would otherwise strand that field's rows in the * index forever, and a stale row here is invisible until it wrongly answers a filtered listing — * showing an editor content that is no longer tagged the way the index claims. * * Returned as statements so the index lands in the same atomic batch as the item write. The reads * it needs — checking the referenced terms exist — all happen here, before the batch is built. */ export declare function planAssignmentIndex(db: Kysely, contentItemId: string, fields: { api_id: string; type: string; }[], data: Record): Promise; /** The terms assigned to one item, across every taxonomy field it has. */ export declare function termsForItem(db: Kysely, contentItemId: string): Promise<(TermRow & { field_api_id: string; })[]>;