diff --git a/.changeset/fair-taxis-listen.md b/.changeset/fair-taxis-listen.md new file mode 100644 index 0000000000..25eb13a62c --- /dev/null +++ b/.changeset/fair-taxis-listen.md @@ -0,0 +1,6 @@ +--- +"@emdash-cms/admin": patch +"emdash": patch +--- + +Fixes localized taxonomy navigation, editor choices, and visible term counts so each surface follows the active content locale. diff --git a/packages/admin/src/components/ContentSettingsPanel.tsx b/packages/admin/src/components/ContentSettingsPanel.tsx index d90b5df563..099e1397e4 100644 --- a/packages/admin/src/components/ContentSettingsPanel.tsx +++ b/packages/admin/src/components/ContentSettingsPanel.tsx @@ -391,7 +391,12 @@ export const ContentSettingsPanel = React.memo(function ContentSettingsPanel({ const [publishedDate, setPublishedDate] = React.useState(storedPublishedDate); const [isReorderingSections, setIsReorderingSections] = React.useState(false); const showDiscard = !isNew && supportsDrafts && hasPendingChanges && !!onDiscardDraft; - const hasApplicableTaxonomies = useHasApplicableTaxonomies(collection); + const activeEntryLocale = item?.locale ?? entryLocale ?? undefined; + const hasApplicableTaxonomies = useHasApplicableTaxonomies( + collection, + activeEntryLocale, + i18n?.defaultLocale, + ); const canUpdatePublishedDate = item?.publishedAt != null && (currentUser?.role ?? 0) >= ROLE_EDITOR && !!onPublishedAtChange; @@ -657,7 +662,8 @@ export const ContentSettingsPanel = React.memo(function ContentSettingsPanel({ className="p-4" collection={collection} entryId={item.id} - entryLocale={item.locale ?? entryLocale} + entryLocale={activeEntryLocale} + defaultLocale={i18n?.defaultLocale} /> )} diff --git a/packages/admin/src/components/Shell.tsx b/packages/admin/src/components/Shell.tsx index bdc80f46ee..5fbd2d419d 100644 --- a/packages/admin/src/components/Shell.tsx +++ b/packages/admin/src/components/Shell.tsx @@ -31,9 +31,13 @@ export interface ShellProps { } >; taxonomies: Array<{ + id?: string; name: string; label: string; + locale?: string; + translationGroup?: string | null; }>; + i18n?: { defaultLocale: string; locales: string[] }; version?: string; }; } diff --git a/packages/admin/src/components/Sidebar.tsx b/packages/admin/src/components/Sidebar.tsx index d69ae3607f..367d7b0d1d 100644 --- a/packages/admin/src/components/Sidebar.tsx +++ b/packages/admin/src/components/Sidebar.tsx @@ -8,6 +8,10 @@ import * as React from "react"; import { fetchCommentCounts } from "../lib/api/comments"; import { useCurrentUser } from "../lib/api/current-user"; import { resolvePluginPagePath, usePluginAdmins } from "../lib/plugin-context"; +import { + resolveTaxonomyDefinitions, + type LocalizedTaxonomyDefinition, +} from "../lib/taxonomy-definitions.js"; import { ADMIN_NAV_ICONS, getCollectionNavIcon, @@ -76,9 +80,13 @@ export interface SidebarNavProps { } >; taxonomies: Array<{ + id?: string; name: string; label: string; + locale?: string; + translationGroup?: string | null; }>; + i18n?: { defaultLocale: string; locales: string[] }; version?: string; commit?: string; marketplace?: string; @@ -93,11 +101,21 @@ export interface SidebarNavProps { }; } +/** Locale-normalized taxonomy rows used by the global Manage navigation. */ +export function getSidebarTaxonomies( + taxonomies: readonly T[], + activeLocale?: string, + defaultLocale?: string, +): T[] { + return resolveTaxonomyDefinitions(taxonomies, activeLocale, defaultLocale); +} + interface NavItem { to: string; label: string; icon: React.ElementType; params?: Record; + search?: Record; /** Minimum role level required to see this item */ minRole?: number; /** Optional badge count (e.g., pending comments) */ @@ -162,13 +180,16 @@ export function resolvePluginPageLabel( } /** Resolves a nav item's route path by substituting $param placeholders. */ -function resolveItemPath(item: NavItem): string { +export function resolveItemPath(item: NavItem): string { let path = item.to; if (item.params) { for (const [key, value] of Object.entries(item.params)) { path = path.replace(`$${key}`, value); } } + if (item.search && Object.keys(item.search).length > 0) { + path += `?${new URLSearchParams(item.search).toString()}`; + } return path; } @@ -186,6 +207,8 @@ export function SidebarNav({ manifest }: SidebarNavProps) { const { t, i18n } = useLingui(); const location = useLocation(); const currentPath = location.pathname; + const routeLocale = + new URL(location.href, "http://emdash.local").searchParams.get("locale") ?? undefined; const pluginAdmins = usePluginAdmins(); const { data: user } = useCurrentUser(); @@ -232,13 +255,16 @@ export function SidebarNav({ manifest }: SidebarNavProps) { }, { to: "/widgets", label: t`Widgets`, icon: ADMIN_NAV_ICONS.widgets, minRole: ROLE_EDITOR }, { to: "/sections", label: t`Sections`, icon: ADMIN_NAV_ICONS.sections, minRole: ROLE_EDITOR }, - ...manifest.taxonomies.map((tax) => ({ - to: "/taxonomies/$taxonomy" as const, - label: tax.label, - icon: getTaxonomyNavIcon(tax.name), - params: { taxonomy: tax.name }, - minRole: ROLE_EDITOR, - })), + ...getSidebarTaxonomies(manifest.taxonomies, routeLocale, manifest.i18n?.defaultLocale).map( + (tax) => ({ + to: "/taxonomies/$taxonomy" as const, + label: tax.label, + icon: getTaxonomyNavIcon(tax.name), + params: { taxonomy: tax.name }, + search: routeLocale ? { locale: routeLocale } : undefined, + minRole: ROLE_EDITOR, + }), + ), { to: "/bylines", label: t`Bylines`, icon: ADMIN_NAV_ICONS.bylines, minRole: ROLE_EDITOR }, ]; diff --git a/packages/admin/src/components/TaxonomySidebar.tsx b/packages/admin/src/components/TaxonomySidebar.tsx index bbee14370b..d8f49ce174 100644 --- a/packages/admin/src/components/TaxonomySidebar.tsx +++ b/packages/admin/src/components/TaxonomySidebar.tsx @@ -16,6 +16,7 @@ import * as React from "react"; import { apiFetch, parseApiResponse, throwResponseError } from "../lib/api/client.js"; import { createTerm, withLocale } from "../lib/api/taxonomies.js"; +import { resolveTaxonomyDefinitions } from "../lib/taxonomy-definitions.js"; import { termExactMatches, termMatches } from "../lib/taxonomy-match.js"; import { cn, slugify } from "../lib/utils.js"; @@ -35,6 +36,8 @@ interface TaxonomyDef { labelSingular?: string; hierarchical: boolean; collections: string[]; + locale?: string; + translationGroup?: string | null; } interface TaxonomySidebarProps { @@ -43,6 +46,8 @@ interface TaxonomySidebarProps { /** Locale of the entry being edited. Scopes term reads/writes so only the * matching translation variants are shown — see issue #1218. */ entryLocale?: string; + /** Site default used when this logical taxonomy has no entry-locale definition. */ + defaultLocale?: string; onChange?: (taxonomyName: string, termIds: string[]) => void; /** Applied to the root when the section renders. Omitted when the section * is empty so the caller doesn't need to guess whether to draw chrome. */ @@ -63,17 +68,27 @@ async function fetchTaxonomyDefs(): Promise { return data.taxonomies; } -function useApplicableTaxonomies(collection: string): TaxonomyDef[] { +function useApplicableTaxonomies( + collection: string, + activeLocale?: string, + defaultLocale?: string, +): TaxonomyDef[] { const { data: taxonomies = [] } = useQuery({ queryKey: ["taxonomy-defs"], queryFn: fetchTaxonomyDefs, }); - return taxonomies.filter((taxonomy) => taxonomy.collections.includes(collection)); + return resolveTaxonomyDefinitions(taxonomies, activeLocale, defaultLocale).filter((taxonomy) => + taxonomy.collections.includes(collection), + ); } /** Whether the editor should include a taxonomy settings section. */ -export function useHasApplicableTaxonomies(collection: string): boolean { - return useApplicableTaxonomies(collection).length > 0; +export function useHasApplicableTaxonomies( + collection: string, + activeLocale?: string, + defaultLocale?: string, +): boolean { + return useApplicableTaxonomies(collection, activeLocale, defaultLocale).length > 0; } /** @@ -538,11 +553,12 @@ export function TaxonomySidebar({ collection, entryId, entryLocale, + defaultLocale, onChange, className, }: TaxonomySidebarProps) { const { t } = useLingui(); - const applicableTaxonomies = useApplicableTaxonomies(collection); + const applicableTaxonomies = useApplicableTaxonomies(collection, entryLocale, defaultLocale); if (applicableTaxonomies.length === 0) { return null; diff --git a/packages/admin/src/lib/api/client.ts b/packages/admin/src/lib/api/client.ts index 9b0ff18ef2..7024c8afc3 100644 --- a/packages/admin/src/lib/api/client.ts +++ b/packages/admin/src/lib/api/client.ts @@ -179,11 +179,14 @@ export interface AdminManifest { * Taxonomy definitions for the admin sidebar. */ taxonomies: Array<{ + id?: string; name: string; label: string; labelSingular?: string; hierarchical: boolean; collections: string[]; + locale?: string; + translationGroup?: string | null; }>; /** * Marketplace registry URL. Present when `marketplace` is configured diff --git a/packages/admin/src/lib/taxonomy-definitions.ts b/packages/admin/src/lib/taxonomy-definitions.ts new file mode 100644 index 0000000000..9379e3cbb9 --- /dev/null +++ b/packages/admin/src/lib/taxonomy-definitions.ts @@ -0,0 +1,59 @@ +export interface LocalizedTaxonomyDefinition { + id?: string; + name: string; + label: string; + locale?: string; + translationGroup?: string | null; +} + +function normalizedLocale(locale: string | undefined): string | undefined { + return locale?.trim().toLowerCase() || undefined; +} + +/** + * Collapse localized definition rows to one row per logical taxonomy. + * + * Selection prefers the active locale, then the configured default locale. + * If neither exists, the lexically first locale/id/label wins so incomplete + * translation groups remain usable and produce stable manifests and UI. + * Legacy definitions without translation metadata are grouped by `name`. + */ +export function resolveTaxonomyDefinitions( + definitions: readonly T[], + activeLocale?: string, + defaultLocale?: string, +): T[] { + const active = normalizedLocale(activeLocale); + const fallback = normalizedLocale(defaultLocale); + const groups = new Map(); + + for (const definition of definitions) { + const group = definition.translationGroup?.trim() || definition.name; + const variants = groups.get(group); + if (variants) variants.push(definition); + else groups.set(group, [definition]); + } + + return Array.from(groups.values(), (variants) => { + const exact = active + ? variants.find((definition) => normalizedLocale(definition.locale) === active) + : undefined; + if (exact) return exact; + + const defaultVariant = fallback + ? variants.find((definition) => normalizedLocale(definition.locale) === fallback) + : undefined; + if (defaultVariant) return defaultVariant; + + return variants.toSorted((left, right) => { + const leftKey = [normalizedLocale(left.locale) ?? "", left.id ?? "", left.name, left.label]; + const rightKey = [ + normalizedLocale(right.locale) ?? "", + right.id ?? "", + right.name, + right.label, + ]; + return leftKey.join("\0").localeCompare(rightKey.join("\0")); + })[0]!; + }); +} diff --git a/packages/admin/tests/components/Sidebar.test.tsx b/packages/admin/tests/components/Sidebar.test.tsx index 6371ce0f00..051002feb8 100644 --- a/packages/admin/tests/components/Sidebar.test.tsx +++ b/packages/admin/tests/components/Sidebar.test.tsx @@ -36,6 +36,8 @@ import { describe, it, expect } from "vitest"; import { BYLINE_SCHEMA_NAV_ITEM, filterNavItemsByRole, + getSidebarTaxonomies, + resolveItemPath, resolveNavIcon, resolvePluginPageLabel, toPhosphorIconName, @@ -50,6 +52,45 @@ const ROLE_AUTHOR = 30; const ROLE_EDITOR = 40; const ROLE_ADMIN = 50; +describe("getSidebarTaxonomies", () => { + const taxonomies = [ + { id: "course-en", name: "course", label: "Courses", locale: "en", translationGroup: "course" }, + { id: "course-de", name: "course", label: "Gänge", locale: "de", translationGroup: "course" }, + { + id: "course-fr", + name: "course", + label: "Types de plats", + locale: "fr", + translationGroup: "course", + }, + ]; + + it("renders one logical taxonomy using the active route locale", () => { + expect(getSidebarTaxonomies(taxonomies, "de").map((taxonomy) => taxonomy.label)).toEqual([ + "Gänge", + ]); + }); + + it("falls back to the configured default locale, then deterministically", () => { + expect(getSidebarTaxonomies(taxonomies, "it", "fr")[0]?.label).toBe("Types de plats"); + expect(getSidebarTaxonomies(taxonomies, "it")[0]?.label).toBe("Gänge"); + }); +}); + +describe("resolveItemPath", () => { + it("preserves the active locale on taxonomy-management links", () => { + expect( + resolveItemPath({ + to: "/taxonomies/$taxonomy", + label: "Gänge", + icon: Gear, + params: { taxonomy: "course" }, + search: { locale: "de" }, + }), + ).toBe("/taxonomies/course?locale=de"); + }); +}); + describe("BYLINE_SCHEMA_NAV_ITEM invariants", () => { it("points to the /byline-schema route", () => { expect(BYLINE_SCHEMA_NAV_ITEM.to).toBe("/byline-schema"); diff --git a/packages/admin/tests/components/TaxonomySidebar.test.tsx b/packages/admin/tests/components/TaxonomySidebar.test.tsx index 0f758eb2c3..cf0ee63a1f 100644 --- a/packages/admin/tests/components/TaxonomySidebar.test.tsx +++ b/packages/admin/tests/components/TaxonomySidebar.test.tsx @@ -20,6 +20,8 @@ interface TestTaxonomy { id: string; name: string; label: string; + locale?: string; + translationGroup?: string; labelSingular?: string; hierarchical: boolean; collections: string[]; @@ -192,4 +194,36 @@ describe("TaxonomySidebar", () => { await expect.element(screen.getByText("Alpha")).toBeInTheDocument(); expect(screen.getByLabelText("Add Categories").query()).toBeNull(); }); + + it("renders only the entry-locale definition for a translated taxonomy", async () => { + mockApiFetch({ + taxonomies: [ + { ...tagsTaxonomy, id: "tags-en", label: "Tags", locale: "en", translationGroup: "tags" }, + { + ...tagsTaxonomy, + id: "tags-de", + label: "Schlagwörter", + locale: "de", + translationGroup: "tags", + }, + { + ...tagsTaxonomy, + id: "tags-fr", + label: "Étiquettes", + locale: "fr", + translationGroup: "tags", + }, + ], + }); + + const screen = await render( + , + { wrapper: Wrapper }, + ); + + await expect.element(screen.getByText("Schlagwörter")).toBeInTheDocument(); + expect(screen.getByText("Tags").query()).toBeNull(); + expect(screen.getByText("Étiquettes").query()).toBeNull(); + await expect.element(screen.getByLabelText("Add Schlagwörter")).toBeInTheDocument(); + }); }); diff --git a/packages/core/src/api/handlers/taxonomies.ts b/packages/core/src/api/handlers/taxonomies.ts index 27852274c3..2d0242a224 100644 --- a/packages/core/src/api/handlers/taxonomies.ts +++ b/packages/core/src/api/handlers/taxonomies.ts @@ -389,12 +389,12 @@ export async function handleTermList( ): Promise> { try { // Definitions are per-locale but terms aren't bound to the def's locale — - // just ensure the taxonomy exists somewhere. - const lookup = await requireTaxonomyDef(db, taxonomyName); + // use the active definition for its collection scope. + const locale = options.locale ? resolveConfiguredLocale(options.locale) : undefined; + const lookup = await requireTaxonomyDef(db, taxonomyName, locale); if (!lookup.success) return lookup; const repo = new TaxonomyRepository(db); - const locale = options.locale ? resolveConfiguredLocale(options.locale) : undefined; const terms = await repo.findByName(taxonomyName, { locale }); // Counts match what visitors see on the public site: published (or @@ -404,7 +404,7 @@ export async function handleTermList( // look up by group and map back to each term's id. const includeCounts = options.includeCounts ?? true; const countsByGroup = includeCounts - ? await fetchVisibleTermCounts(db, taxonomyName, defCollections(lookup.def)) + ? await fetchVisibleTermCounts(db, taxonomyName, defCollections(lookup.def), locale) : undefined; const termData: TermWithCount[] = terms.map((term) => ({ @@ -656,11 +656,12 @@ export async function handleTermGet( // Count matches public visibility (published or scheduled-and-due, not // soft-deleted) scoped to the def's declared collections. The def lookup // is lenient: a term whose def is missing still resolves, with count 0. - const lookup = await requireTaxonomyDef(db, taxonomyName); + const lookup = await requireTaxonomyDef(db, taxonomyName, locale); const counts = await fetchVisibleTermCounts( db, taxonomyName, lookup.success ? defCollections(lookup.def) : [], + locale ?? term.locale, ); const count = counts.get(term.translationGroup ?? term.id) ?? 0; // Children share this term's translation_group as their parent_id; scope diff --git a/packages/core/src/astro/types.ts b/packages/core/src/astro/types.ts index 3ed85d1e3c..d7b8f41081 100644 --- a/packages/core/src/astro/types.ts +++ b/packages/core/src/astro/types.ts @@ -143,11 +143,14 @@ export interface EmDashManifest { * Taxonomy definitions for the admin sidebar. */ taxonomies: Array<{ + id: string; name: string; label: string; labelSingular?: string; hierarchical: boolean; collections: string[]; + locale: string; + translationGroup: string; }>; /** * Whether the plugin marketplace is configured. diff --git a/packages/core/src/emdash-runtime.ts b/packages/core/src/emdash-runtime.ts index fec729cc53..ccc96bc9f8 100644 --- a/packages/core/src/emdash-runtime.ts +++ b/packages/core/src/emdash-runtime.ts @@ -2478,11 +2478,14 @@ export class EmDashRuntime { // Build taxonomies from database let manifestTaxonomies: Array<{ + id: string; name: string; label: string; labelSingular?: string; hierarchical: boolean; collections: string[]; + locale: string; + translationGroup: string; }> = []; try { const rows = await this.db @@ -2491,11 +2494,14 @@ export class EmDashRuntime { .orderBy("name") .execute(); manifestTaxonomies = rows.map((row) => ({ + id: row.id, name: row.name, label: row.label, labelSingular: row.label_singular ?? undefined, hierarchical: row.hierarchical === 1, collections: parseStringArray(row.collections).toSorted(), + locale: row.locale, + translationGroup: row.translation_group ?? row.id, })); } catch (error) { console.debug("EmDash: Could not load taxonomy definitions:", error); diff --git a/packages/core/src/taxonomies/index.ts b/packages/core/src/taxonomies/index.ts index 1e594684ea..87f1f1a742 100644 --- a/packages/core/src/taxonomies/index.ts +++ b/packages/core/src/taxonomies/index.ts @@ -259,7 +259,7 @@ export async function getTaxonomyTerms( // The two are independent, so run them concurrently to save a round trip. const [terms, counts] = await Promise.all([ getTermList(def, locale), - getVisibleTermCounts(def.name, def.collections), + getVisibleTermCounts(def.name, def.collections, locale), ]); return withCounts(terms, counts); } @@ -331,28 +331,30 @@ async function loadTaxonomyTerms( /** * Per-translation-group visible-usage counts for one taxonomy, in a single - * round-trip (see `fetchVisibleTermCounts`). Counts are locale-independent - * (the pivot stores translation_group), and the request-cached map is shared - * by every consumer in the render — the widget (`getTaxonomyTerms`) and the - * single-term page (`getTerm`) never issue separate count queries. + * round-trip (see `fetchVisibleTermCounts`). The pivot identity is the + * translation group, while entry rows are scoped to the resolved locale. + * Request and object cache keys include that locale so translated views never + * reuse each other's visible counts. */ function getVisibleTermCounts( taxonomyName: string, collections: string[], + locale?: string, ): Promise> { // The collection scope is part of the key: per-locale rows of the same def // can drift in their declared collections, and a caller may pass a narrower // scope. Identical inputs (the widget + term-page hot path) still share one // entry. const scope = [...new Set(collections)].toSorted().join(","); - return requestCached(`taxonomy-term-counts:${taxonomyName}:${scope}`, async () => { + const localeScope = locale ?? "*"; + return requestCached(`taxonomy-term-counts:${taxonomyName}:${scope}:${localeScope}`, async () => { // A Map is not JSON-representable — cache the entries, rebuild on read. const entries = await cachedQuery({ namespace: termCountNamespaces(collections), - key: `termCounts:${taxonomyName}:${scope}`, + key: `termCounts:${taxonomyName}:${scope}:${localeScope}`, load: async (): Promise> => { const db = await getDb(); - return [...(await fetchVisibleTermCounts(db, taxonomyName, collections))]; + return [...(await fetchVisibleTermCounts(db, taxonomyName, collections, locale))]; }, }); return new Map(entries); @@ -426,7 +428,7 @@ async function loadTerm( // databases. The counts map is request-cached per taxonomy — on a page // that also renders the taxonomy widget it's a free Map lookup. const [counts, childRows] = await Promise.all([ - getVisibleTermCounts(taxonomyName, collections), + getVisibleTermCounts(taxonomyName, collections, chain[0] ?? row.locale), childrenQuery.execute(), ]); const count = counts.get(row.translation_group ?? row.id) ?? 0; diff --git a/packages/core/src/taxonomies/term-counts.ts b/packages/core/src/taxonomies/term-counts.ts index 3cbf5f513f..ad327130dc 100644 --- a/packages/core/src/taxonomies/term-counts.ts +++ b/packages/core/src/taxonomies/term-counts.ts @@ -30,8 +30,8 @@ interface CountRow { /** * Per-collection count branch. `taxonomy_id` stores the term's - * translation_group, so results are keyed by group (locale-independent) and - * each assignment is counted once no matter how many locales the term has. + * translation_group, so results are keyed by group while entry rows are + * optionally scoped to the active locale. * * Scoping to the taxonomy uses `translation_group IN (...)` rather than a * join on `taxonomies.id` — the anchor row (id == group) can be deleted while @@ -46,6 +46,7 @@ function collectionBranch( db: Kysely, taxonomyName: string, collection: string, + locale?: string, ): ReturnType { return sql` SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count @@ -54,6 +55,7 @@ function collectionBranch( WHERE e.id = ct.entry_id AND ct.collection = ${collection} AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ${taxonomyName}) + ${locale ? sql`AND e.locale = ${locale}` : sql``} AND ${buildStatusCondition(db, "published", "e")} AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id`; @@ -63,8 +65,11 @@ async function runCounts( db: Kysely, taxonomyName: string, collections: string[], + locale?: string, ): Promise> { - const branches = collections.map((collection) => collectionBranch(db, taxonomyName, collection)); + const branches = collections.map((collection) => + collectionBranch(db, taxonomyName, collection, locale), + ); const union = sql.join(branches, sql` UNION ALL `); const result = await sql` SELECT taxonomy_id, SUM(count) AS count @@ -79,6 +84,8 @@ async function runCounts( /** * Count publicly-visible term assignments for one taxonomy, keyed by the * term's translation_group (what `content_taxonomies.taxonomy_id` stores). + * When `locale` is provided, only entries in that locale contribute. Omitting + * it preserves the locale-agnostic API used by legacy callers. * * Counts are scoped to the taxonomy's declared collections — pass * `TaxonomyDef.collections` (`_emdash_taxonomy_defs.collections`). Collections @@ -95,13 +102,14 @@ export async function fetchVisibleTermCounts( db: Kysely, taxonomyName: string, collections: string[], + locale?: string, ): Promise> { const unique = [...new Set(collections)]; for (const collection of unique) validateIdentifier(collection, "collection slug"); if (unique.length === 0) return new Map(); try { - return await runCounts(db, taxonomyName, unique); + return await runCounts(db, taxonomyName, unique, locale); } catch (error) { if (!isMissingTableError(error)) throw error; } @@ -111,7 +119,7 @@ export async function fetchVisibleTermCounts( const counts = new Map(); for (const collection of unique) { try { - for (const [group, count] of await runCounts(db, taxonomyName, [collection])) { + for (const [group, count] of await runCounts(db, taxonomyName, [collection], locale)) { counts.set(group, (counts.get(group) ?? 0) + count); } } catch (error) { diff --git a/packages/core/tests/integration/taxonomy-term-counts-plan.test.ts b/packages/core/tests/integration/taxonomy-term-counts-plan.test.ts index 2245f0e8be..14f32bd5cb 100644 --- a/packages/core/tests/integration/taxonomy-term-counts-plan.test.ts +++ b/packages/core/tests/integration/taxonomy-term-counts-plan.test.ts @@ -87,7 +87,7 @@ function explain(query: CapturedQuery): string { async function countQueryPlan(): Promise { captured = []; - await fetchVisibleTermCounts(db, "category", ["post"]); + await fetchVisibleTermCounts(db, "category", ["post"], "en"); const query = captured.find((q) => q.sql.includes("content_taxonomies")); expect(query, "expected a term-count query against the pivot").toBeDefined(); return explain(query!); diff --git a/packages/core/tests/unit/runtime/manifest-build.test.ts b/packages/core/tests/unit/runtime/manifest-build.test.ts index ee9e379005..0c207417a5 100644 --- a/packages/core/tests/unit/runtime/manifest-build.test.ts +++ b/packages/core/tests/unit/runtime/manifest-build.test.ts @@ -158,4 +158,17 @@ describe("EmDashRuntime.getManifest()", () => { expect(manifest.collections[`coll_${i}`]?.fields.title?.kind).toBe("string"); } }); + + it("includes taxonomy locale identity for admin-side normalization", async () => { + const runtime = buildRuntime(db); + const manifest = await runtime.getManifest(); + const category = manifest.taxonomies.find((taxonomy) => taxonomy.name === "category"); + + expect(category).toMatchObject({ + id: expect.any(String), + locale: "en", + translationGroup: expect.any(String), + }); + expect(category?.translationGroup).toBe(category?.id); + }); }); diff --git a/packages/core/tests/unit/taxonomies/term-counts.test.ts b/packages/core/tests/unit/taxonomies/term-counts.test.ts index 55704d739d..8f2b38de0d 100644 --- a/packages/core/tests/unit/taxonomies/term-counts.test.ts +++ b/packages/core/tests/unit/taxonomies/term-counts.test.ts @@ -191,7 +191,7 @@ describeEachDialect("visible term counts (#581)", (dialect) => { expect(tagCounts.get(tag.translationGroup ?? tag.id)).toBe(1); }); - it("counts per entry row across locales, keyed by translation_group", async () => { + it("counts only entry rows in the requested locale, keyed by translation_group", async () => { // Defs are per-locale — translate the seeded `category` def into FR so // the FR widget view resolves (same declared collections). const enDef = await ctx.db @@ -212,6 +212,19 @@ describeEachDialect("visible term counts (#581)", (dialect) => { translation_group: enDef.translation_group ?? enDef.id, }) .execute(); + await ctx.db + .insertInto("_emdash_taxonomy_defs") + .values({ + id: ulid(), + name: "category", + label: "Kategorien", + label_singular: null, + hierarchical: enDef.hierarchical, + collections: enDef.collections, + locale: "de", + translation_group: enDef.translation_group ?? enDef.id, + }) + .execute(); const enTerm = await taxRepo.create({ name: "category", @@ -226,6 +239,13 @@ describeEachDialect("visible term counts (#581)", (dialect) => { locale: "fr", translationOf: enTerm.id, }); + const deTerm = await taxRepo.create({ + name: "category", + slug: "nachrichten", + label: "Nachrichten", + locale: "de", + translationOf: enTerm.id, + }); const enPost = await contentRepo.create({ type: "post", @@ -242,19 +262,31 @@ describeEachDialect("visible term counts (#581)", (dialect) => { locale: "fr", translationOf: enPost.id, }); + const dePost = await contentRepo.create({ + type: "post", + slug: "hallo", + status: "published", + data: { title: "Hallo" }, + locale: "de", + translationOf: enPost.id, + }); // Attaching via either locale's term id resolves to the shared group. await taxRepo.attachToEntry("post", enPost.id, enTerm.id); await taxRepo.attachToEntry("post", frPost.id, frTerm.id); + await taxRepo.attachToEntry("post", dePost.id, deTerm.id); - const counts = await fetchVisibleTermCounts(ctx.db, "category", ["post"]); - // One count per entry row, shared by every locale variant of the term. - expect(counts.get(enTerm.translationGroup ?? enTerm.id)).toBe(2); + for (const locale of ["en", "fr", "de"]) { + const counts = await fetchVisibleTermCounts(ctx.db, "category", ["post"], locale); + expect(counts.get(enTerm.translationGroup ?? enTerm.id)).toBe(1); + } - // Both locale views of the taxonomy surface the same group count. + // Each locale view surfaces the count for entries visible in that locale. const enTerms = await getTaxonomyTerms("category", { locale: "en" }); const frTerms = await getTaxonomyTerms("category", { locale: "fr" }); - expect(enTerms[0]!.count).toBe(2); - expect(frTerms[0]!.count).toBe(2); + const deTerms = await getTaxonomyTerms("category", { locale: "de" }); + expect(enTerms[0]!.count).toBe(1); + expect(frTerms[0]!.count).toBe(1); + expect(deTerms[0]!.count).toBe(1); }); it("skips missing ec_* tables and returns a partial count", async () => { @@ -300,8 +332,20 @@ describeEachDialect("visible term counts (#581)", (dialect) => { }); const post = await createEntry("post", "p1"); - const page1 = await createEntry("page", "g1"); - const page2 = await createEntry("page", "g2"); + const page1 = await contentRepo.create({ + type: "page", + slug: "g1", + status: "published", + data: { title: "g1" }, + locale: "fr", + }); + const page2 = await contentRepo.create({ + type: "page", + slug: "g2", + status: "published", + data: { title: "g2" }, + locale: "fr", + }); await taxRepo.attachToEntry("post", post.id, enTerm.id); await taxRepo.attachToEntry("page", page1.id, enTerm.id); await taxRepo.attachToEntry("page", page2.id, enTerm.id); @@ -313,6 +357,14 @@ describeEachDialect("visible term counts (#581)", (dialect) => { expect(enTerms[0]!.count).toBe(1); expect(frTerms[0]!.count).toBe(2); }); + + const frList = await handleTermList(ctx.db, "drifty", { locale: "fr" }); + if (!frList.success) throw new Error(frList.error.message); + expect(frList.data.terms[0]!.count).toBe(2); + + const frTerm = await handleTermGet(ctx.db, "drifty", "partage", { locale: "fr" }); + if (!frTerm.success) throw new Error(frTerm.error.message); + expect(frTerm.data.term.count).toBe(2); }); it("returns an empty map when the taxonomy declares no collections", async () => { diff --git a/scripts/query-counts.queries.d1.json b/scripts/query-counts.queries.d1.json index 9847908b08..985b5cea69 100644 --- a/scripts/query-counts.queries.d1.json +++ b/scripts/query-counts.queries.d1.json @@ -43,7 +43,7 @@ "select count(*) as \"count\" from \"_emdash_collections\"": 1, "SELECT COUNT(*) as count FROM \"_emdash_migrations\"": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "UPDATE _emdash_cron_tasks SET status = 'idle', locked_at = NULL WHERE status = 'running' AND locked_at < ?": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, @@ -57,7 +57,7 @@ "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /contributors (cold)": { @@ -273,7 +273,7 @@ "select count(*) as \"count\" from \"_emdash_collections\"": 1, "SELECT COUNT(*) as count FROM \"_emdash_migrations\"": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "UPDATE _emdash_cron_tasks SET status = 'idle', locked_at = NULL WHERE status = 'running' AND locked_at < ?": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, @@ -287,7 +287,7 @@ "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 } } diff --git a/scripts/query-counts.queries.sqlite.json b/scripts/query-counts.queries.sqlite.json index b29f9d21a6..167e343d43 100644 --- a/scripts/query-counts.queries.sqlite.json +++ b/scripts/query-counts.queries.sqlite.json @@ -26,7 +26,7 @@ "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /category/development (warm)": { @@ -39,7 +39,7 @@ "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /contributors (cold)": { @@ -184,7 +184,7 @@ "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 }, "GET /tag/webdev (warm)": { @@ -197,7 +197,7 @@ "select * from \"taxonomies\" where \"parent_id\" = ? and \"locale\" = ? order by \"label\" asc": 1, "SELECT *, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"ec_pages\".id AND t.locale = \"ec_pages\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"ec_pages\".id AND b.locale = \"ec_pages\".locale) AS \"_emdash_bylines\" FROM \"ec_pages\" WHERE deleted_at IS NULL AND (\"status\" = 'published' OR (\"status\" = 'scheduled' AND \"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY \"created_at\" DESC, \"id\" DESC": 1, "select distinct \"translation_group\" from \"taxonomies\" where \"name\" = ? and \"slug\" in (...)": 1, - "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, + "SELECT taxonomy_id, SUM(count) AS count FROM ( SELECT ct.taxonomy_id AS taxonomy_id, COUNT(*) AS count FROM content_taxonomies AS ct CROSS JOIN \"ec_posts\" AS e WHERE e.id = ct.entry_id AND ct.collection = ? AND ct.taxonomy_id IN (SELECT translation_group FROM taxonomies WHERE name = ?) AND e.locale = ? AND (\"e\".\"status\" = 'published' OR (\"e\".\"status\" = 'scheduled' AND \"e\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) AND e.deleted_at IS NULL GROUP BY ct.taxonomy_id) AS per_collection GROUP BY taxonomy_id": 1, "WITH picked AS ( SELECT ct.entry_id AS entry_id, \"ct\".\"published_at\" AS sortval FROM content_taxonomies ct WHERE ct.collection = ? AND \"ct\".\"taxonomy_id\" = ? AND ct.deleted_at IS NULL AND (\"ct\".\"status\" = 'published' OR (\"ct\".\"status\" = 'scheduled' AND \"ct\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY sortval DESC, ct.entry_id DESC ) SELECT r.*, (SELECT json_group_array(json_object('id', t.id, 'name', t.name, 'slug', t.slug, 'label', t.label, 'parent_id', t.parent_id, 'locale', t.locale, 'translation_group', t.translation_group)) FROM \"content_taxonomies\" AS ct CROSS JOIN \"taxonomies\" AS t ON t.translation_group = ct.taxonomy_id WHERE ct.collection = ? AND ct.entry_id = \"r\".id AND t.locale = \"r\".locale) AS \"_emdash_terms\", (SELECT json_group_array(json_object('roleLabel', cb.role_label, 'sortOrder', cb.sort_order, 'byline', json_object('id', b.id, 'slug', b.slug, 'displayName', b.display_name, 'bio', b.bio, 'avatarMediaId', b.avatar_media_id, 'avatarStorageKey', m.storage_key, 'avatarAlt', m.alt, 'avatarBlurhash', m.blurhash, 'avatarDominantColor', m.dominant_color, 'websiteUrl', b.website_url, 'userId', b.user_id, 'isGuest', b.is_guest, 'createdAt', b.created_at, 'updatedAt', b.updated_at, 'locale', b.locale, 'translationGroup', b.translation_group))) FROM \"_emdash_content_bylines\" AS cb CROSS JOIN \"_emdash_bylines\" AS b ON b.translation_group = cb.byline_id LEFT JOIN \"media\" AS m ON m.id = b.avatar_media_id WHERE cb.collection_slug = ? AND cb.content_id = \"r\".id AND b.locale = \"r\".locale) AS \"_emdash_bylines\" FROM picked JOIN \"ec_posts\" AS r ON r.id = picked.entry_id WHERE r.deleted_at IS NULL AND (\"r\".\"status\" = 'published' OR (\"r\".\"status\" = 'scheduled' AND \"r\".\"scheduled_at\" <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))) ORDER BY picked.sortval DESC, picked.entry_id DESC": 1 } }