From f2753f3066489d56040079643d4a09a0169d5c90 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Mon, 25 May 2026 07:30:47 +0530 Subject: [PATCH 1/2] fix(core): resolve entry slug to canonical id when reading content terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /_emdash/api/content/{collection}/{id}/terms/{taxonomy} passed the raw URL `id` straight to the taxonomy repository, but content_taxonomies rows are keyed by the canonical content ULID. The POST handler already resolves a slug to that ULID via handleContentGet before writing, so a slug-addressed read matched no rows and returned an empty list even when terms were assigned — only requests using the ULID returned the assignments. Resolve the slug the same way in the GET handler before querying, falling back to the raw id when the entry can't be resolved so the existing 'no such entry -> empty list' behaviour is preserved. Closes #1045 --- .changeset/fix-terms-get-slug-resolution.md | 5 + .../[collection]/[id]/terms/[taxonomy].ts | 9 +- .../content-terms-slug-resolution.test.ts | 116 ++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-terms-get-slug-resolution.md create mode 100644 packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts diff --git a/.changeset/fix-terms-get-slug-resolution.md b/.changeset/fix-terms-get-slug-resolution.md new file mode 100644 index 000000000..2c585cae3 --- /dev/null +++ b/.changeset/fix-terms-get-slug-resolution.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fixes `GET /_emdash/api/content/{collection}/{id}/terms/{taxonomy}` returning an empty list when the entry is addressed by its slug. Term assignments are stored under the canonical entry ID, and the POST handler already resolves a slug to that ID before writing; the GET handler now performs the same resolution before reading, so a slug-addressed request returns the assigned terms instead of an empty list. diff --git a/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts b/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts index af76d5ee1..b6d1fb812 100644 --- a/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +++ b/packages/core/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts @@ -35,7 +35,14 @@ export const GET: APIRoute = async ({ params, locals }) => { try { const repo = new TaxonomyRepository(emdash.db); - const terms = await repo.getTermsForEntry(collection, id, taxonomy); + // The URL `id` may be a slug. Term rows are keyed by the canonical + // content ULID — the POST handler resolves the slug and stores under + // that ULID, so the read must resolve it too. Without this, a request + // addressed by slug looks up assignments under the slug, finds none, + // and returns an empty list even though the term is assigned (#1045). + const existing = await emdash.handleContentGet(collection, id); + const canonicalId = existing.success ? (existing.data?.item.id ?? id) : id; + const terms = await repo.getTermsForEntry(collection, canonicalId, taxonomy); return apiSuccess({ terms: terms.map((t) => ({ diff --git a/packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts b/packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts new file mode 100644 index 000000000..26b1b8bad --- /dev/null +++ b/packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts @@ -0,0 +1,116 @@ +/** + * Slug resolution on the content-taxonomy association endpoint. + * + * `content_taxonomies` rows are keyed by the canonical content ULID. The POST + * handler resolves the URL `id` segment (which may be a slug) to that ULID via + * `handleContentGet` before writing. The GET handler must perform the same + * resolution before reading — otherwise a request addressed by slug looks up + * assignments under the slug, finds none, and returns an empty list even + * though the term is assigned. + * + * Regression test for #1045 (GET did not resolve slug -> canonical ULID). + */ + +import type { APIContext } from "astro"; +import type { Kysely } from "kysely"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { + GET as getTerms, + POST as postTerms, +} from "../../../src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].js"; +import { TaxonomyRepository } from "../../../src/database/repositories/taxonomy.js"; +import type { Database } from "../../../src/database/types.js"; +import { createTestRuntime, handlersFromRuntime } from "../../utils/mcp-runtime.js"; +import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js"; + +type Handlers = ReturnType; +type TermsBody = { data: { terms: Array<{ slug: string }> } }; + +// RoleLevel 50 = ADMIN — satisfies content:read and content:edit_any. +const ADMIN = { id: "user_admin", email: "admin@example.com", name: "Admin", role: 50 as const }; + +function buildContext(opts: { + emdash: Handlers; + params: { collection: string; id: string; taxonomy: string }; + request: Request; +}): APIContext { + return { + params: opts.params, + url: new URL(opts.request.url), + request: opts.request, + locals: { emdash: opts.emdash, user: ADMIN }, + // eslint-disable-next-line typescript/no-unsafe-type-assertion -- minimal stub for tests + } as unknown as APIContext; +} + +describe("content terms endpoint — slug resolution (#1045)", () => { + let db: Kysely; + let emdash: Handlers; + + beforeEach(async () => { + db = await setupTestDatabaseWithCollections(); + emdash = handlersFromRuntime(createTestRuntime(db)); + }); + + afterEach(async () => { + await teardownTestDatabase(db); + }); + + it("GET returns assigned terms when the entry is addressed by slug", async () => { + const taxRepo = new TaxonomyRepository(db); + const term = await taxRepo.create({ name: "tag", slug: "pakistan", label: "Pakistan" }); + + const created = await emdash.handleContentCreate("post", { + data: { title: "eSIM Pakistan" }, + slug: "esim-pakistan", + }); + expect(created.success).toBe(true); + + const resolved = await emdash.handleContentGet("post", "esim-pakistan"); + const postId = resolved.data?.item.id; + if (typeof postId !== "string") throw new Error("expected created post to have an id"); + + // Assign the term via POST addressed by slug. POST already resolves the + // slug to the ULID, so the row lands under `postId`. + const postRes = await postTerms( + buildContext({ + emdash, + params: { collection: "post", id: "esim-pakistan", taxonomy: "tag" }, + request: new Request( + "http://localhost/_emdash/api/content/post/esim-pakistan/terms/tag", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ termIds: [term.id] }), + }, + ), + }), + ); + expect(postRes.status).toBe(200); + + // Control: GET by canonical ULID resolves trivially and returns the term. + const byId = await getTerms( + buildContext({ + emdash, + params: { collection: "post", id: postId, taxonomy: "tag" }, + request: new Request(`http://localhost/_emdash/api/content/post/${postId}/terms/tag`), + }), + ); + expect(byId.status).toBe(200); + const byIdBody = (await byId.json()) as TermsBody; + expect(byIdBody.data.terms.map((t) => t.slug)).toEqual(["pakistan"]); + + // Regression: GET by slug must resolve to the same ULID and return the term. + const bySlug = await getTerms( + buildContext({ + emdash, + params: { collection: "post", id: "esim-pakistan", taxonomy: "tag" }, + request: new Request("http://localhost/_emdash/api/content/post/esim-pakistan/terms/tag"), + }), + ); + expect(bySlug.status).toBe(200); + const bySlugBody = (await bySlug.json()) as TermsBody; + expect(bySlugBody.data.terms.map((t) => t.slug)).toEqual(["pakistan"]); + }); +}); From a1cde574ea6700b2344d45eddb311cbe35a4528f Mon Sep 17 00:00:00 2001 From: "emdashbot[bot]" Date: Mon, 25 May 2026 02:03:30 +0000 Subject: [PATCH 2/2] style: format --- .../astro/content-terms-slug-resolution.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts b/packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts index 26b1b8bad..abee88e54 100644 --- a/packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts +++ b/packages/core/tests/integration/astro/content-terms-slug-resolution.test.ts @@ -77,14 +77,11 @@ describe("content terms endpoint — slug resolution (#1045)", () => { buildContext({ emdash, params: { collection: "post", id: "esim-pakistan", taxonomy: "tag" }, - request: new Request( - "http://localhost/_emdash/api/content/post/esim-pakistan/terms/tag", - { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ termIds: [term.id] }), - }, - ), + request: new Request("http://localhost/_emdash/api/content/post/esim-pakistan/terms/tag", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ termIds: [term.id] }), + }), }), ); expect(postRes.status).toBe(200);