From 7c5db1a1486b96ec611b9d47519ff3f00a9c02b4 Mon Sep 17 00:00:00 2001 From: Frank Bartolitsch Date: Wed, 5 Aug 2026 13:09:56 +0200 Subject: [PATCH 1/3] fix(taxonomies): respect active locale in admin surfaces Group translated definitions by logical identity before rendering admin navigation and editor choices. Scope visible term counts and cache entries to the resolved content locale. --- .changeset/fair-taxis-listen.md | 6 ++ .../src/components/ContentSettingsPanel.tsx | 10 ++- packages/admin/src/components/Shell.tsx | 4 ++ packages/admin/src/components/Sidebar.tsx | 35 ++++++++--- .../admin/src/components/TaxonomySidebar.tsx | 26 ++++++-- packages/admin/src/lib/api/client.ts | 3 + .../admin/src/lib/taxonomy-definitions.ts | 59 ++++++++++++++++++ .../admin/tests/components/Sidebar.test.tsx | 26 ++++++++ .../tests/components/TaxonomySidebar.test.tsx | 34 ++++++++++ packages/core/src/api/handlers/taxonomies.ts | 3 +- packages/core/src/astro/types.ts | 3 + packages/core/src/emdash-runtime.ts | 6 ++ packages/core/src/taxonomies/index.ts | 20 +++--- packages/core/src/taxonomies/term-counts.ts | 18 ++++-- .../taxonomy-term-counts-plan.test.ts | 2 +- .../tests/unit/runtime/manifest-build.test.ts | 13 ++++ .../tests/unit/taxonomies/term-counts.test.ts | 62 ++++++++++++++++--- 17 files changed, 291 insertions(+), 39 deletions(-) create mode 100644 .changeset/fair-taxis-listen.md create mode 100644 packages/admin/src/lib/taxonomy-definitions.ts 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..27416fa373 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,6 +101,15 @@ 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; @@ -186,6 +203,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 +251,15 @@ 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 }, + 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..20bd8a9675 100644 --- a/packages/admin/tests/components/Sidebar.test.tsx +++ b/packages/admin/tests/components/Sidebar.test.tsx @@ -36,6 +36,7 @@ import { describe, it, expect } from "vitest"; import { BYLINE_SCHEMA_NAV_ITEM, filterNavItemsByRole, + getSidebarTaxonomies, resolveNavIcon, resolvePluginPageLabel, toPhosphorIconName, @@ -50,6 +51,31 @@ 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("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..1707b3a2b9 100644 --- a/packages/core/src/api/handlers/taxonomies.ts +++ b/packages/core/src/api/handlers/taxonomies.ts @@ -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) => ({ @@ -661,6 +661,7 @@ export async function handleTermGet( 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..a810a4c4b3 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); From f488ff5b939b8ddf84c74501acf0e4aa77790eb7 Mon Sep 17 00:00:00 2001 From: Frank Bartolitsch Date: Wed, 5 Aug 2026 13:32:50 +0200 Subject: [PATCH 2/3] fix(taxonomies): preserve locale in admin term views --- packages/admin/src/components/Sidebar.tsx | 7 ++++++- packages/admin/tests/components/Sidebar.test.tsx | 15 +++++++++++++++ packages/core/src/api/handlers/taxonomies.ts | 8 ++++---- .../tests/unit/taxonomies/term-counts.test.ts | 8 ++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/admin/src/components/Sidebar.tsx b/packages/admin/src/components/Sidebar.tsx index 27416fa373..367d7b0d1d 100644 --- a/packages/admin/src/components/Sidebar.tsx +++ b/packages/admin/src/components/Sidebar.tsx @@ -115,6 +115,7 @@ interface NavItem { 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) */ @@ -179,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; } @@ -257,6 +261,7 @@ export function SidebarNav({ manifest }: SidebarNavProps) { label: tax.label, icon: getTaxonomyNavIcon(tax.name), params: { taxonomy: tax.name }, + search: routeLocale ? { locale: routeLocale } : undefined, minRole: ROLE_EDITOR, }), ), diff --git a/packages/admin/tests/components/Sidebar.test.tsx b/packages/admin/tests/components/Sidebar.test.tsx index 20bd8a9675..051002feb8 100644 --- a/packages/admin/tests/components/Sidebar.test.tsx +++ b/packages/admin/tests/components/Sidebar.test.tsx @@ -37,6 +37,7 @@ import { BYLINE_SCHEMA_NAV_ITEM, filterNavItemsByRole, getSidebarTaxonomies, + resolveItemPath, resolveNavIcon, resolvePluginPageLabel, toPhosphorIconName, @@ -76,6 +77,20 @@ describe("getSidebarTaxonomies", () => { }); }); +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/core/src/api/handlers/taxonomies.ts b/packages/core/src/api/handlers/taxonomies.ts index 1707b3a2b9..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 @@ -656,7 +656,7 @@ 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, diff --git a/packages/core/tests/unit/taxonomies/term-counts.test.ts b/packages/core/tests/unit/taxonomies/term-counts.test.ts index a810a4c4b3..8f2b38de0d 100644 --- a/packages/core/tests/unit/taxonomies/term-counts.test.ts +++ b/packages/core/tests/unit/taxonomies/term-counts.test.ts @@ -357,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 () => { From b312464d313e6f6e258815c9aa1c28657fae782f Mon Sep 17 00:00:00 2001 From: "emdashbot[bot]" Date: Sat, 8 Aug 2026 08:46:15 +0000 Subject: [PATCH 3/3] ci: update query-count snapshots --- scripts/query-counts.queries.d1.json | 8 ++++---- scripts/query-counts.queries.sqlite.json | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 } }