{"version":3,"file":"taxonomies-BNtKkFgI.mjs","names":[],"sources":["../src/taxonomies/term-counts.ts","../src/taxonomies/index.ts"],"sourcesContent":["/**\n * Shared visible-usage counts for taxonomy terms (issue #581).\n *\n * `content_taxonomies` rows are written for every entry regardless of status\n * and are never pruned on unpublish/trash, so counting the pivot directly\n * inflates counts with drafts and soft-deleted entries. Every user-facing\n * count (public widget, single-term page, admin term list) must instead count\n * only entries that have completed publication:\n * `status = 'published'` AND `deleted_at IS NULL`.\n *\n * The public render path is latency-sensitive on D1, so per-collection counts\n * are combined with UNION ALL — one query per taxonomy, never one per\n * collection, split only where the backend caps compound-SELECT terms.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { buildStatusCondition, compoundSelectLimit } from \"../database/dialect-helpers.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { chunks } from \"../utils/chunks.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\n\ninterface CountRow {\n\ttaxonomy_id: string;\n\tcount: number | string | bigint;\n}\n\n/**\n * Per-collection count branch. `taxonomy_id` stores the term's\n * translation_group, so results are keyed by group (locale-independent) and\n * each assignment is counted once per content translation group. When a\n * locale is provided, only that group's matching content row contributes.\n *\n * Scoping to the taxonomy uses `translation_group IN (...)` rather than a\n * join on `taxonomies.id` — the anchor row (id == group) can be deleted while\n * sibling translations survive, and a plain join on `translation_group` would\n * multiply counts by the number of locales.\n *\n * CROSS JOIN with the join predicate in WHERE keeps stats-blind SQLite/D1 from\n * reordering content_taxonomies out of the outer position; it touches ec_* only\n * through the translation-group index. Postgres treats this as an ordinary\n * inner join and plans freely.\n */\nfunction collectionBranch(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\tcollection: string,\n\tlocale?: string,\n): ReturnType<typeof sql> {\n\treturn sql`\n\t\tSELECT ct.taxonomy_id AS taxonomy_id, COUNT(DISTINCT e.translation_group) AS count\n\t\tFROM content_taxonomies AS ct\n\t\tCROSS JOIN ${sql.ref(`ec_${collection}`)} AS e\n\t\tWHERE e.translation_group = ct.entry_id\n\t\t\tAND ct.collection = ${collection}\n\t\t\tAND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ${taxonomyName})\n\t\t\t${locale ? sql`AND e.locale = ${locale}` : sql``}\n\t\t\tAND ${buildStatusCondition(db, \"published\", \"e\")}\n\t\t\tAND e.deleted_at IS NULL\n\t\tGROUP BY ct.taxonomy_id`;\n}\n\nasync function runCounts(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\tcollections: string[],\n\tlocale?: string,\n): Promise<Map<string, number>> {\n\tconst branches = collections.map((collection) =>\n\t\tcollectionBranch(db, taxonomyName, collection, locale),\n\t);\n\tconst union = sql.join(branches, sql` UNION ALL `);\n\tconst result = await sql<CountRow>`\n\t\tSELECT taxonomy_id, SUM(count) AS count\n\t\tFROM (${union}) AS per_collection\n\t\tGROUP BY taxonomy_id`.execute(db);\n\n\tconst counts = new Map<string, number>();\n\tfor (const row of result.rows) counts.set(row.taxonomy_id, Number(row.count));\n\treturn counts;\n}\n\nfunction addCounts(into: Map<string, number>, from: Map<string, number>): void {\n\tfor (const [group, count] of from) into.set(group, (into.get(group) ?? 0) + count);\n}\n\n/**\n * Counts for one batch of collections, degrading to a query per collection\n * when a declared collection has no ec_* table so the rest still contribute.\n */\nasync function runBatch(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\tcollections: string[],\n\tlocale?: string,\n): Promise<Map<string, number>> {\n\ttry {\n\t\treturn await runCounts(db, taxonomyName, collections, locale);\n\t} catch (error) {\n\t\tif (!isMissingTableError(error)) throw error;\n\t}\n\n\tconst counts = new Map<string, number>();\n\tfor (const collection of collections) {\n\t\ttry {\n\t\t\taddCounts(counts, await runCounts(db, taxonomyName, [collection], locale));\n\t\t} catch (error) {\n\t\t\tif (!isMissingTableError(error)) throw error;\n\t\t}\n\t}\n\treturn counts;\n}\n\n/**\n * Count publicly-visible term assignments for one taxonomy, keyed by the\n * term's translation_group (what `content_taxonomies.taxonomy_id` stores).\n * When `locale` is provided, only entries in that locale contribute. Omitting\n * it preserves the locale-agnostic API used by legacy callers.\n *\n * Counts are scoped to the taxonomy's declared collections — pass\n * `TaxonomyDef.collections` (`_emdash_taxonomy_defs.collections`). Collections\n * whose `ec_*` table doesn't exist (pre-migration drift, a declared collection\n * that was never created) are skipped, yielding a partial-but-correct count\n * rather than a throw.\n *\n * One database round-trip for the whole taxonomy (UNION ALL across\n * collections). On a backend that caps compound-SELECT terms — D1 allows five\n * — the branches are split into one statement per batch, since a taxonomy\n * declaring more collections than that would otherwise take down every path\n * that shows counts. Per-collection sums are commutative, so batching cannot\n * change the total.\n *\n * Callers on the public render path should go through the request-cached\n * wrapper in `taxonomies/index.ts` so a page rendering both the widget and a\n * term detail shares one computation.\n */\nexport async function fetchVisibleTermCounts(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\tcollections: string[],\n\tlocale?: string,\n): Promise<Map<string, number>> {\n\tconst unique = [...new Set(collections)];\n\tfor (const collection of unique) validateIdentifier(collection, \"collection slug\");\n\tif (unique.length === 0) return new Map();\n\n\tconst limit = compoundSelectLimit(db);\n\tconst batched = limit === null ? [unique] : chunks(unique, limit);\n\tconst batches = await Promise.all(\n\t\tbatched.map((batch) => runBatch(db, taxonomyName, batch, locale)),\n\t);\n\n\tconst counts = new Map<string, number>();\n\tfor (const batch of batches) addCounts(counts, batch);\n\treturn counts;\n}\n","/**\n * Runtime API for taxonomies.\n *\n * All helpers are locale-aware. When a locale is not passed explicitly we fall\n * back to the request context or the configured `defaultLocale` (see\n * `i18n/resolve.ts`).\n *\n * Because `content_taxonomies.taxonomy_id` stores the translation_group (not a\n * specific term id), and `entry_id` stores the content translation_group, one\n * assignment resolves to the requested locale on both sides.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { getI18nConfig } from \"../i18n/config.js\";\nimport { resolveLocale, resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb, resetTaxonomyNamesCache } from \"../loader.js\";\nimport {\n\tcachedQuery,\n\tCacheNamespace,\n\tcontentCacheNamespaces,\n\tinvalidateTaxonomyObjectCache,\n\tisObjectCacheActive,\n} from \"../object-cache/index.js\";\nimport { peekRequestCache, requestCached, setRequestCacheEntry } from \"../request-cache.js\";\nimport { getRequestContext } from \"../request-context.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../utils/chunks.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\nimport { fetchVisibleTermCounts } from \"./term-counts.js\";\nimport type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from \"./types.js\";\n\nexport interface TaxonomyQueryOptions {\n\tlocale?: string;\n}\n\nexport interface TaxonomyTermsOptions extends TaxonomyQueryOptions {\n\t/**\n\t * Populate each term's `count`. Counts aggregate the whole\n\t * `content_taxonomies` pivot against every declared collection, so callers\n\t * that don't render a count should opt out. Defaults to `true`.\n\t */\n\tincludeCounts?: boolean;\n}\n\ninterface EntryTermRow {\n\tentry_id: string;\n\tid: string;\n\tname: string;\n\tslug: string;\n\tlabel: string;\n\tparent_id: string | null;\n\tlocale: string;\n\ttranslation_group: string | null;\n}\n\nasync function selectEntryTermRows(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryIds: string[],\n\ttaxonomyName?: string,\n\tlocale?: string,\n): Promise<EntryTermRow[]> {\n\tvalidateIdentifier(collection, \"collection slug\");\n\tconst tableName = `ec_${collection}`;\n\tconst preferredLocale = locale ? sql`${locale}` : sql`content.locale`;\n\tconst defaultLocale = getI18nConfig()?.defaultLocale ?? \"en\";\n\tconst result = await sql<EntryTermRow>`\n\t\tSELECT content.id AS entry_id,\n\t\t\tcoalesce(exact_term.id, default_term.id) AS id,\n\t\t\tcoalesce(exact_term.name, default_term.name) AS name,\n\t\t\tcoalesce(exact_term.slug, default_term.slug) AS slug,\n\t\t\tcoalesce(exact_term.label, default_term.label) AS label,\n\t\t\tcoalesce(exact_term.parent_id, default_term.parent_id) AS parent_id,\n\t\t\tcoalesce(exact_term.locale, default_term.locale) AS locale,\n\t\t\tcoalesce(exact_term.translation_group, default_term.translation_group) AS translation_group\n\t\tFROM ${sql.ref(tableName)} AS content\n\t\tINNER JOIN content_taxonomies AS pivot\n\t\t\tON pivot.entry_id = content.translation_group\n\t\t\tAND pivot.collection = ${collection}\n\t\tLEFT JOIN taxonomies AS exact_term\n\t\t\tON exact_term.translation_group = pivot.taxonomy_id\n\t\t\tAND exact_term.locale = ${preferredLocale}\n\t\tLEFT JOIN taxonomies AS default_term\n\t\t\tON default_term.translation_group = pivot.taxonomy_id\n\t\t\tAND default_term.locale = ${defaultLocale}\n\t\tWHERE content.id IN (${sql.join(entryIds.map((id) => sql`${id}`))})\n\t\t\tAND coalesce(exact_term.id, default_term.id) IS NOT NULL\n\t\t\t${taxonomyName ? sql`AND coalesce(exact_term.name, default_term.name) = ${taxonomyName}` : sql``}\n\t\tORDER BY coalesce(exact_term.label, default_term.label) ASC\n\t`.execute(db);\n\treturn result.rows;\n}\n\n/** Invalidate cached taxonomy term data and any content that hydrates terms. */\nexport function invalidateTermCache(): void {\n\tinvalidateTaxonomyObjectCache();\n}\n\n/**\n * Worker-isolate cache for taxonomy definitions, keyed by resolved locale.\n *\n * Taxonomy *definitions* (the \"category\"/\"tag\" taxonomies themselves, not\n * their terms) are read on every public render that hydrates entry terms —\n * `getAllTermsForEntries` → `getCollectionTaxonomyNames` → `getTaxonomyDefs` —\n * but change extremely rarely: they're written only by the admin API and by\n * seed application. Caching them across the isolate lifetime drops the\n * per-render `SELECT * FROM _emdash_taxonomy_defs` to once-per-isolate.\n *\n * Stored on globalThis behind a Symbol key (same pattern as\n * `settings/index.ts`) so the bundler duplicating this module across SSR\n * chunks can't produce two independent caches.\n *\n * When the distributed object cache is configured, it is authoritative and this\n * fallback is bypassed so cross-isolate epoch invalidation cannot be undercut\n * by stale per-isolate data on an object-cache miss.\n *\n * **Isolated databases bypass the cache.** Playground / DO preview requests\n * set `requestContext.dbIsIsolated`; they point at a divergent schema, so we\n * skip both reading and writing the global holder and fall back to the\n * per-request cache (same precedent as `getTaxonomyNames` / byline field defs).\n */\ninterface TaxonomyDefsHolder {\n\tversion: number;\n\t/** locale key (\"*\" for \"all locales\") → { version it was fetched at, promise }. */\n\tcache: Map<string, { version: number; promise: Promise<TaxonomyDef[]> }>;\n}\n\nconst TAXONOMY_DEFS_CACHE_KEY = Symbol.for(\"emdash:taxonomy-defs\");\nconst taxonomyDefsStore = globalThis as Record<symbol, unknown>;\nconst defsHolder: TaxonomyDefsHolder =\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see settings/index.ts)\n\t(taxonomyDefsStore[TAXONOMY_DEFS_CACHE_KEY] as TaxonomyDefsHolder | undefined) ??\n\t(() => {\n\t\tconst h: TaxonomyDefsHolder = { version: 0, cache: new Map() };\n\t\ttaxonomyDefsStore[TAXONOMY_DEFS_CACHE_KEY] = h;\n\t\treturn h;\n\t})();\n\n/**\n * Invalidate the isolate-wide taxonomy-definitions cache (and the related\n * loader taxonomy-names cache). Called from every taxonomy-def write path\n * (`handleTaxonomyCreate`/`Update`/`Delete`, seed application). Other isolates\n * refresh on their next recycle — staleness bounded by isolate lifetime.\n */\nexport function invalidateTaxonomyDefsCache(): void {\n\tdefsHolder.version++;\n\tdefsHolder.cache.clear();\n\tresetTaxonomyNamesCache();\n\tinvalidateTaxonomyObjectCache();\n}\n\n/**\n * Test/internal helper: clear the per-isolate taxonomy-defs cache. Useful for\n * unit tests that insert defs directly and need to force a refetch without\n * going through a write path. Production code should rely on\n * `invalidateTaxonomyDefsCache()`.\n */\nexport function resetTaxonomyDefsCacheForTests(): void {\n\tdefsHolder.version++;\n\tdefsHolder.cache.clear();\n}\n\n/**\n * Fetch taxonomy definitions straight from the database (no caching).\n */\nasync function fetchTaxonomyDefs(locale: string | undefined): Promise<TaxonomyDef[]> {\n\tconst db = await getDb();\n\tlet query = db.selectFrom(\"_emdash_taxonomy_defs\").selectAll();\n\tif (locale !== undefined) query = query.where(\"locale\", \"=\", locale);\n\tconst rows = await query.execute();\n\treturn rows.map(rowToTaxonomyDef);\n}\n\n/**\n * Resolve taxonomy defs through the isolate fallback cache, bypassing it for\n * isolated databases. The returned promise is cached (not the resolved value)\n * so concurrent cold-isolate readers share one in-flight query; a rejection\n * evicts the entry so the next caller retries.\n */\nfunction loadTaxonomyDefs(localeKey: string, locale: string | undefined): Promise<TaxonomyDef[]> {\n\tif (getRequestContext()?.dbIsIsolated === true) {\n\t\treturn fetchTaxonomyDefs(locale);\n\t}\n\tconst existing = defsHolder.cache.get(localeKey);\n\tif (existing && existing.version === defsHolder.version) {\n\t\treturn existing.promise;\n\t}\n\tconst version = defsHolder.version;\n\tconst promise = fetchTaxonomyDefs(locale).catch((error: unknown) => {\n\t\tconst current = defsHolder.cache.get(localeKey);\n\t\tif (current && current.promise === promise) {\n\t\t\tdefsHolder.cache.delete(localeKey);\n\t\t}\n\t\tthrow error;\n\t});\n\tdefsHolder.cache.set(localeKey, { version, promise });\n\treturn promise;\n}\n\n/**\n * Get every taxonomy definition. Definitions are per-locale (one row per\n * locale inside the same translation_group) — by default we resolve to the\n * active locale.\n *\n * Two-tier cache: per-request via `requestCached` (so a single render that\n * hydrates terms for several collections pays at most one call), then\n * per-isolate via the global holder (so warm renders issue zero queries).\n * The `requestCached` key is unchanged so `getTaxonomyDef`'s peek still hits.\n */\nexport async function getTaxonomyDefs(options: TaxonomyQueryOptions = {}): Promise<TaxonomyDef[]> {\n\tconst locale = resolveLocale(options.locale);\n\tconst localeKey = locale ?? \"*\";\n\treturn requestCached(`taxonomy-defs:${localeKey}`, async () => {\n\t\tif (await isObjectCacheActive()) {\n\t\t\treturn cachedQuery({\n\t\t\t\tnamespace: CacheNamespace.TAXONOMIES,\n\t\t\t\tkey: `defs:${localeKey}`,\n\t\t\t\tload: () => fetchTaxonomyDefs(locale),\n\t\t\t});\n\t\t}\n\t\treturn loadTaxonomyDefs(localeKey, locale);\n\t});\n}\n\n/**\n * Get a single taxonomy definition by name. Uses the fallback chain so even\n * if there is no translation for the active locale we still return something.\n *\n * If `getTaxonomyDefs()` has already loaded the full list in this request\n * (which happens during entry-term hydration on every page that renders a\n * collection), search the matching def in memory rather than running a\n * second query against `_emdash_taxonomy_defs`.\n */\nexport async function getTaxonomyDef(\n\tname: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyDef | null> {\n\tconst chain = resolveLocaleChain(options.locale);\n\tconst peekKey = `taxonomy-defs:${resolveLocale(options.locale) ?? \"*\"}`;\n\tconst allDefs = peekRequestCache<TaxonomyDef[]>(peekKey);\n\tif (allDefs) {\n\t\tconst defs = await allDefs;\n\t\tif (chain.length === 0) return defs.find((d) => d.name === name) ?? null;\n\t\tfor (const locale of chain) {\n\t\t\tconst found = defs.find((d) => d.name === name && d.locale === locale);\n\t\t\tif (found) return found;\n\t\t}\n\t\treturn null;\n\t}\n\n\treturn requestCached(`taxonomy-def:${name}:${chain.join(\",\")}`, async () => {\n\t\tconst db = await getDb();\n\n\t\tif (chain.length === 0) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t\t.executeTakeFirst();\n\t\t\treturn row ? rowToTaxonomyDef(row) : null;\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.where(\"locale\", \"=\", locale)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (row) return rowToTaxonomyDef(row);\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * Object-cache namespaces for values that embed visible term counts: the\n * taxonomy epoch (term/assignment writes) plus each counted collection's\n * content epoch, so publishing, unpublishing, or trashing an entry\n * invalidates the cached count promptly. Both content namespace generations\n * participate so rolling deployments preserve invalidation in either direction.\n */\nfunction termCountNamespaces(collections: string[]): string[] {\n\treturn [\n\t\t...[...new Set(collections)].flatMap((collection) => contentCacheNamespaces(collection)),\n\t\tCacheNamespace.TAXONOMIES,\n\t];\n}\n\n/**\n * All terms of a taxonomy in a specific locale (flat for non-hierarchical,\n * tree for hierarchical).\n *\n * The term list and the visible-entry counts are loaded and cached separately:\n * the list depends only on the taxonomy epoch, while the counts additionally\n * depend on every counted collection's content epoch and cost an aggregate over\n * the whole assignment pivot. Callers that don't render counts pass\n * `includeCounts: false` and skip that aggregate entirely, while still sharing\n * the term list with callers that do.\n */\nexport async function getTaxonomyTerms(\n\ttaxonomyName: string,\n\toptions: TaxonomyTermsOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\tconst def = await getTaxonomyDef(taxonomyName, options);\n\tif (!def) return [];\n\tif (options.includeCounts === false) return getTermList(def, locale);\n\n\t// The two are independent, so run them concurrently to save a round trip.\n\tconst [terms, counts] = await Promise.all([\n\t\tgetTermList(def, locale),\n\t\tgetVisibleTermCounts(def.name, def.collections, locale),\n\t]);\n\treturn withCounts(terms, counts);\n}\n\n/** Terms without counts, under the cache keys the layout prefetch warms. */\nfunction getTermList(def: TaxonomyDef, locale: string | undefined): Promise<TaxonomyTerm[]> {\n\tconst localeKey = locale ?? \"*\";\n\treturn requestCached(`taxonomy-terms:${def.name}:${localeKey}`, () =>\n\t\tcachedQuery({\n\t\t\tnamespace: CacheNamespace.TAXONOMIES,\n\t\t\tkey: `termList:${def.name}:${localeKey}`,\n\t\t\tload: () => loadTaxonomyTerms(def, locale),\n\t\t}),\n\t);\n}\n\n/**\n * Copy a term list with counts attached. Counts are keyed by translation_group\n * (what the pivot stores) and are locale-independent. Rebuilds every node so\n * the shared, cached count-free list is never mutated.\n */\nfunction withCounts(terms: TaxonomyTerm[], counts: Map<string, number>): TaxonomyTerm[] {\n\treturn terms.map((term) => ({\n\t\t...term,\n\t\tcount: counts.get(term.translationGroup ?? term.id) ?? 0,\n\t\tchildren: withCounts(term.children, counts),\n\t}));\n}\n\nasync function loadTaxonomyTerms(\n\tdef: TaxonomyDef,\n\tlocale: string | undefined,\n): Promise<TaxonomyTerm[]> {\n\tconst db = await getDb();\n\n\tlet termsQuery = db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t.where(\"name\", \"=\", def.name)\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.orderBy(\"label\", \"asc\");\n\tif (locale !== undefined) termsQuery = termsQuery.where(\"locale\", \"=\", locale);\n\n\tconst rows = await termsQuery.execute();\n\n\tconst flatTerms: TaxonomyTermRow[] = rows.map((row) => ({\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\tparent_id: row.parent_id,\n\t\tdata: row.data,\n\t\tlocale: row.locale,\n\t\ttranslation_group: row.translation_group,\n\t}));\n\n\tif (def.hierarchical) return buildTree(flatTerms);\n\n\treturn flatTerms.map((term) => ({\n\t\tid: term.id,\n\t\tname: term.name,\n\t\tslug: term.slug,\n\t\tlabel: term.label,\n\t\tdescription: term.data ? JSON.parse(term.data).description : undefined,\n\t\tchildren: [],\n\t\tlocale: term.locale,\n\t\ttranslationGroup: term.translation_group,\n\t}));\n}\n\n/**\n * Per-translation-group visible-usage counts for one taxonomy, in a single\n * round-trip (see `fetchVisibleTermCounts`). The pivot identity is the\n * translation group, while entry rows are scoped to the resolved locale.\n * Request and object cache keys include that locale so translated views never\n * reuse each other's visible counts.\n */\nfunction getVisibleTermCounts(\n\ttaxonomyName: string,\n\tcollections: string[],\n\tlocale?: string,\n): Promise<Map<string, number>> {\n\t// The collection scope is part of the key: per-locale rows of the same def\n\t// can drift in their declared collections, and a caller may pass a narrower\n\t// scope. Identical inputs (the widget + term-page hot path) still share one\n\t// entry.\n\tconst scope = [...new Set(collections)].toSorted().join(\",\");\n\tconst localeScope = locale ?? \"*\";\n\treturn requestCached(`taxonomy-term-counts:${taxonomyName}:${scope}:${localeScope}`, async () => {\n\t\t// A Map is not JSON-representable — cache the entries, rebuild on read.\n\t\tconst entries = await cachedQuery({\n\t\t\tnamespace: termCountNamespaces(collections),\n\t\t\tkey: `termCounts:${taxonomyName}:${scope}:${localeScope}`,\n\t\t\tload: async (): Promise<Array<[string, number]>> => {\n\t\t\t\tconst db = await getDb();\n\t\t\t\treturn [...(await fetchVisibleTermCounts(db, taxonomyName, collections, locale))];\n\t\t\t},\n\t\t});\n\t\treturn new Map(entries);\n\t});\n}\n\n/**\n * Get a single term by (taxonomy, slug). Honours the fallback chain — if the\n * slug exists in a fallback locale, we return that row (useful for deep-linking\n * to a term page when the translation is missing).\n */\nexport async function getTerm(\n\ttaxonomyName: string,\n\tslug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm | null> {\n\tconst chain = resolveLocaleChain(options.locale);\n\t// The def supplies the collections the visible count is scoped to. It is\n\t// resolved before cachedQuery so the entry lives under each collection's\n\t// content namespace — publishing or unpublishing an entry invalidates the\n\t// embedded count (see termCountNamespaces).\n\tconst def = await getTaxonomyDef(taxonomyName, options);\n\tconst collections = def?.collections ?? [];\n\treturn cachedQuery({\n\t\tnamespace: termCountNamespaces(collections),\n\t\tkey: `term:${taxonomyName}:${slug}:${chain.join(\",\")}`,\n\t\tload: () => loadTerm(taxonomyName, slug, chain, collections),\n\t});\n}\n\nasync function loadTerm(\n\ttaxonomyName: string,\n\tslug: string,\n\tchain: string[],\n\tcollections: string[],\n): Promise<TaxonomyTerm | null> {\n\tconst db = await getDb();\n\n\tlet row: Awaited<ReturnType<ReturnType<typeof selectTerm>[\"executeTakeFirst\"]>>;\n\tconst selectTerm = () =>\n\t\tdb\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t\t.where(\"slug\", \"=\", slug);\n\n\tif (chain.length === 0) {\n\t\trow = await selectTerm().orderBy(\"locale\", \"asc\").executeTakeFirst();\n\t} else {\n\t\trow = undefined;\n\t\tfor (const locale of chain) {\n\t\t\trow = await selectTerm().where(\"locale\", \"=\", locale).executeTakeFirst();\n\t\t\tif (row) break;\n\t\t}\n\t}\n\n\tif (!row) return null;\n\n\tlet childrenQuery = db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t// Children store the parent's translation_group in parent_id (not a row\n\t\t// id), so a translated parent still owns its children in its own locale.\n\t\t.where(\"parent_id\", \"=\", row.translation_group ?? row.id)\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.orderBy(\"label\", \"asc\");\n\tconst termLocale = row.locale;\n\tif (termLocale) childrenQuery = childrenQuery.where(\"locale\", \"=\", termLocale);\n\n\t// The visible-usage counts and children queries both depend only on the\n\t// term row, so run them concurrently to save a round trip on remote\n\t// databases. The counts map is request-cached per taxonomy — on a page\n\t// that also renders the taxonomy widget it's a free Map lookup.\n\tconst [counts, childRows] = await Promise.all([\n\t\tgetVisibleTermCounts(taxonomyName, collections, chain[0] ?? row.locale),\n\t\tchildrenQuery.execute(),\n\t]);\n\tconst count = counts.get(row.translation_group ?? row.id) ?? 0;\n\n\tconst children = childRows.map<TaxonomyTerm>((child) => ({\n\t\tid: child.id,\n\t\tname: child.name,\n\t\tslug: child.slug,\n\t\tlabel: child.label,\n\t\tparentId: child.parent_id ?? undefined,\n\t\tchildren: [],\n\t\tlocale: child.locale,\n\t\ttranslationGroup: child.translation_group,\n\t}));\n\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\tparentId: row.parent_id ?? undefined,\n\t\tdescription: row.data ? JSON.parse(row.data).description : undefined,\n\t\tchildren,\n\t\tcount,\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Terms assigned to a content entry, resolved into the active locale and then\n * the configured default locale.\n */\nexport function getEntryTerms(\n\tcollection: string,\n\tentryId: string,\n\ttaxonomyName?: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\t// requestCached short-circuits to values primed by getAllTermsForEntries\n\t// during entry hydration (same key shape). On a warm content-cache hit\n\t// hydration doesn't run, so the inner cachedQuery serves this from KV\n\t// instead of falling through to D1 on every request.\n\treturn requestCached(\n\t\t`terms:${collection}:${entryId}:${taxonomyName ?? \"*\"}:${locale ?? \"*\"}`,\n\t\t() =>\n\t\t\tcachedQuery({\n\t\t\t\tnamespace: [...contentCacheNamespaces(collection), CacheNamespace.TAXONOMIES],\n\t\t\t\tkey: `entryTerms:${collection}:${entryId}:${taxonomyName ?? \"*\"}:${locale ?? \"*\"}`,\n\t\t\t\tload: async () => {\n\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\tconst rows = await selectEntryTermRows(db, collection, [entryId], taxonomyName, locale);\n\t\t\t\t\treturn rows.map<TaxonomyTerm>((row) => ({\n\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\tname: row.name,\n\t\t\t\t\t\tslug: row.slug,\n\t\t\t\t\t\tlabel: row.label,\n\t\t\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\t\t\tchildren: [],\n\t\t\t\t\t\tlocale: row.locale,\n\t\t\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t}),\n\t);\n}\n\n/**\n * Terms for multiple entries of one taxonomy, single query.\n */\nexport async function getTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\ttaxonomyName: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, TaxonomyTerm[]>> {\n\tconst uniqueIds = [...new Set(entryIds)];\n\tif (uniqueIds.length === 0) return new Map();\n\tconst locale = resolveLocale(options.locale);\n\tconst localeKey = locale ?? \"*\";\n\n\t// The query result is a Map, which JSON can't represent — cache it as an\n\t// array of [entryId, terms] pairs and rebuild the Map on read.\n\tconst load = async (): Promise<Array<[string, TaxonomyTerm[]]>> => {\n\t\tconst result = new Map<string, TaxonomyTerm[]>();\n\t\tfor (const id of uniqueIds) result.set(id, []);\n\n\t\t// Entry-term hydration (getAllTermsForEntries -> primeEntryTermsCache)\n\t\t// seeds the per-entry cache under the same key getEntryTerms uses:\n\t\t// `terms:${collection}:${entryId}:${taxonomyName}:${localeKey}`, storing a\n\t\t// TaxonomyTerm[] (including `[]` for entries with no terms). Satisfy those\n\t\t// from cache and run the batched query only for the ids that missed.\n\t\tconst missedIds: string[] = [];\n\t\ttype CacheRead = { id: string; terms: TaxonomyTerm[] } | { id: string; miss: true };\n\t\tconst cacheReads: Array<Promise<CacheRead>> = [];\n\t\tfor (const id of uniqueIds) {\n\t\t\tconst cached = peekRequestCache<TaxonomyTerm[]>(\n\t\t\t\t`terms:${collection}:${id}:${taxonomyName}:${localeKey}`,\n\t\t\t);\n\t\t\tif (cached) {\n\t\t\t\t// A peeked promise can reject (e.g. a sibling getEntryTerms hit a\n\t\t\t\t// missing table). Treat a rejection as a cache miss so the batched\n\t\t\t\t// query path -- and its isMissingTableError guard below -- still runs,\n\t\t\t\t// rather than propagating an uncaught error.\n\t\t\t\tcacheReads.push(\n\t\t\t\t\tcached.then(\n\t\t\t\t\t\t(terms): CacheRead => ({ id, terms }),\n\t\t\t\t\t\t(): CacheRead => ({ id, miss: true }),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tmissedIds.push(id);\n\t\t\t}\n\t\t}\n\t\tfor (const read of await Promise.all(cacheReads)) {\n\t\t\tif (\"miss\" in read) {\n\t\t\t\tmissedIds.push(read.id);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Return a private copy. The cached array and its term objects are shared\n\t\t\t// with getEntryTerms/getAllTermsForEntries (primeEntryTermsCache stores\n\t\t\t// the same references), so a caller that mutates the result -- sorting in\n\t\t\t// place, pushing into `children` -- must not poison the cache. The\n\t\t\t// pre-cache implementation always returned freshly built arrays.\n\t\t\tresult.set(\n\t\t\t\tread.id,\n\t\t\t\tread.terms.map((t) => ({ ...t, children: [...t.children] })),\n\t\t\t);\n\t\t}\n\n\t\tif (missedIds.length === 0) return [...result.entries()];\n\n\t\tconst db = await getDb();\n\t\tfor (const chunk of chunks(missedIds, SQL_BATCH_SIZE)) {\n\t\t\tlet rows;\n\t\t\ttry {\n\t\t\t\trows = await selectEntryTermRows(db, collection, chunk, taxonomyName, locale);\n\t\t\t} catch (error) {\n\t\t\t\tif (isMissingTableError(error)) return [...result.entries()];\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\t\tid: row.id,\n\t\t\t\t\tname: row.name,\n\t\t\t\t\tslug: row.slug,\n\t\t\t\t\tlabel: row.label,\n\t\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\t\tchildren: [],\n\t\t\t\t\tlocale: row.locale,\n\t\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t\t};\n\t\t\t\tconst terms = result.get(row.entry_id);\n\t\t\t\tif (terms) terms.push(term);\n\t\t\t}\n\t\t}\n\n\t\treturn [...result.entries()];\n\t};\n\n\t// Key on the sorted unique ids. Bound the key length: very large batches\n\t// (rare; they come from collection hydration, already served by the content\n\t// cache) bypass the object cache rather than blow past KV's key limit.\n\tconst idKey = uniqueIds.toSorted().join(\",\");\n\tconst pairs =\n\t\tidKey.length <= 256\n\t\t\t? await cachedQuery({\n\t\t\t\t\tnamespace: [...contentCacheNamespaces(collection), CacheNamespace.TAXONOMIES],\n\t\t\t\t\tkey: `termsForEntries:${collection}:${taxonomyName}:${locale ?? \"*\"}:${idKey}`,\n\t\t\t\t\tload,\n\t\t\t\t})\n\t\t\t: await load();\n\n\treturn new Map(pairs);\n}\n\n/**\n * Batch-fetch terms for multiple entries across ALL taxonomies in one query.\n * Primes the request-cache for subsequent per-entry calls to `getEntryTerms`.\n */\nexport async function getAllTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, Record<string, TaxonomyTerm[]>>> {\n\tconst result = new Map<string, Record<string, TaxonomyTerm[]>>();\n\tconst uniqueIds = [...new Set(entryIds)];\n\tfor (const id of uniqueIds) result.set(id, {});\n\tif (uniqueIds.length === 0) return result;\n\n\tconst db = await getDb();\n\tconst locale = resolveLocale(options.locale);\n\tconst applicableTaxonomyNames = await getCollectionTaxonomyNames(collection, { locale });\n\n\tfor (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {\n\t\tlet rows;\n\t\ttry {\n\t\t\trows = await selectEntryTermRows(db, collection, chunk, undefined, locale);\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) {\n\t\t\t\tfor (const id of uniqueIds) {\n\t\t\t\t\tprimeEntryTermsCache(collection, id, {}, applicableTaxonomyNames, locale);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const row of rows) {\n\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t};\n\t\t\tconst byTaxonomy = result.get(row.entry_id);\n\t\t\tif (!byTaxonomy) continue;\n\t\t\tconst existing = byTaxonomy[row.name];\n\t\t\tif (existing) existing.push(term);\n\t\t\telse byTaxonomy[row.name] = [term];\n\t\t}\n\t}\n\n\tfor (const [entryId, byTaxonomy] of result) {\n\t\tprimeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames, locale);\n\t}\n\n\treturn result;\n}\n\n/**\n * Return the list of taxonomy names applicable to a collection, request-\n * cached so a page render only pays for it once.\n *\n * Returns an empty list when taxonomies haven't been defined yet.\n */\nasync function getCollectionTaxonomyNames(\n\tcollection: string,\n\toptions: TaxonomyQueryOptions,\n): Promise<string[]> {\n\ttry {\n\t\tconst defs = await getTaxonomyDefs(options);\n\t\treturn defs.filter((d) => d.collections.includes(collection)).map((d) => d.name);\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) return [];\n\t\tthrow error;\n\t}\n}\n\n/**\n * Pre-populate the request-cache for every getEntryTerms call-shape that\n * could hit this entry:\n *\n *   getEntryTerms(collection, entryId)                 -> key `terms:C:E:*`\n *   getEntryTerms(collection, entryId, \"tag\")          -> key `terms:C:E:tag`\n *   getEntryTerms(collection, entryId, \"category\")     -> key `terms:C:E:category`\n *   ...one per taxonomy that applies to this collection\n *\n * Taxonomies with no rows on this entry are seeded with `[]` so legacy\n * callers short-circuit to the cached empty array instead of re-querying.\n */\nfunction primeEntryTermsCache(\n\tcollection: string,\n\tentryId: string,\n\tbyTaxonomy: Record<string, TaxonomyTerm[]>,\n\tapplicableTaxonomyNames: string[],\n\tlocale: string | undefined,\n): void {\n\tconst localeKey = locale ?? \"*\";\n\tfor (const name of applicableTaxonomyNames) {\n\t\tsetRequestCacheEntry(\n\t\t\t`terms:${collection}:${entryId}:${name}:${localeKey}`,\n\t\t\tbyTaxonomy[name] ?? [],\n\t\t);\n\t}\n\tfor (const [name, terms] of Object.entries(byTaxonomy)) {\n\t\tsetRequestCacheEntry(`terms:${collection}:${entryId}:${name}:${localeKey}`, terms);\n\t}\n\tconst allTerms = Object.values(byTaxonomy).flat();\n\tsetRequestCacheEntry(`terms:${collection}:${entryId}:*:${localeKey}`, allTerms);\n}\n\n/**\n * Prime the per-entry request cache from terms that were folded into the\n * content query (query.ts `hydrateEntryTerms` fast path), so subsequent\n * `getEntryTerms` calls in the same render hit the cache instead of issuing an\n * N+1 query. Seeds the wildcard key and one key per taxonomy present on the\n * entry — purely from the folded data, with no DB lookup.\n *\n * Unlike `getAllTermsForEntries`, this deliberately does NOT seed `[]` for\n * taxonomies that apply to the collection but have no rows on the entry: doing\n * so would require a `getTaxonomyDefs` query, adding a round trip to every fold\n * render to serve the rarer `getEntryTerms(id, absentTaxonomy)` case from cache.\n * That call simply falls through to its own cached query. Keeping the key shape\n * here (rather than in query.ts) prevents the two from drifting.\n */\nexport function primeFoldedEntryTerms(\n\tcollection: string,\n\tperEntry: Array<{ entryId: string; byTaxonomy: Record<string, TaxonomyTerm[]> }>,\n\toptions: TaxonomyQueryOptions = {},\n): void {\n\tif (perEntry.length === 0) return;\n\tconst locale = resolveLocale(options.locale);\n\tfor (const { entryId, byTaxonomy } of perEntry) {\n\t\tprimeEntryTermsCache(collection, entryId, byTaxonomy, [], locale);\n\t}\n}\n\n/**\n * Get entries by term. Both the lookup (term slug in the active locale) and\n * the content query respect the active locale.\n */\nexport async function getEntriesByTerm(\n\tcollection: string,\n\ttaxonomyName: string,\n\ttermSlug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Array<{ id: string; data: Record<string, unknown> }>> {\n\tconst { getEmDashCollection } = await import(\"../query.js\");\n\n\tconst queryOptions: Record<string, unknown> = {\n\t\twhere: { [taxonomyName]: termSlug },\n\t};\n\tif (options.locale !== undefined) queryOptions.locale = options.locale;\n\tconst { entries } = await getEmDashCollection(collection, queryOptions);\n\treturn entries;\n}\n\nfunction rowToTaxonomyDef(row: {\n\tid: string;\n\tname: string;\n\tlabel: string;\n\tlabel_singular: string | null;\n\thierarchical: number;\n\tcollections: string | null;\n\tlocale: string;\n\ttranslation_group: string | null;\n}): TaxonomyDef {\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tlabel: row.label,\n\t\tlabelSingular: row.label_singular ?? undefined,\n\t\thierarchical: row.hierarchical === 1,\n\t\tcollections: row.collections ? JSON.parse(row.collections) : [],\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Build tree structure from flat terms\n */\nfunction buildTree(flatTerms: TaxonomyTermRow[]): TaxonomyTerm[] {\n\t// parent_id holds the parent's translation_group, so link children by it.\n\t// Key by (locale, group): a child's parent lives in the same locale, and an\n\t// unfiltered set mixes locales whose translated siblings share a group —\n\t// keying by group alone would collide and misattach children across locales.\n\tconst byLocaleGroup = new Map<string, TaxonomyTerm>();\n\tconst nodes: TaxonomyTerm[] = [];\n\tconst roots: TaxonomyTerm[] = [];\n\n\tfor (const term of flatTerms) {\n\t\tconst node: TaxonomyTerm = {\n\t\t\tid: term.id,\n\t\t\tname: term.name,\n\t\t\tslug: term.slug,\n\t\t\tlabel: term.label,\n\t\t\tparentId: term.parent_id ?? undefined,\n\t\t\tdescription: term.data ? JSON.parse(term.data).description : undefined,\n\t\t\tchildren: [],\n\t\t\tlocale: term.locale,\n\t\t\ttranslationGroup: term.translation_group,\n\t\t};\n\t\tbyLocaleGroup.set(`${term.locale}::${term.translation_group ?? term.id}`, node);\n\t\tnodes.push(node);\n\t}\n\n\tfor (const node of nodes) {\n\t\tconst parent = node.parentId\n\t\t\t? byLocaleGroup.get(`${node.locale}::${node.parentId}`)\n\t\t\t: undefined;\n\t\tif (parent) {\n\t\t\tparent.children.push(node);\n\t\t} else {\n\t\t\troots.push(node);\n\t\t}\n\t}\n\n\treturn roots;\n}\n"],"mappings":"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"}