From 3c151d968951bcc55b17d8a67391eecf9cbaeafe Mon Sep 17 00:00:00 2001 From: Sai Prakash Date: Sat, 12 Sep 2026 17:33:21 -0400 Subject: [PATCH] Expose createdById/updatedById in the query layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found during v0.5.1 post-deploy verification: created_by_id is correctly backfilled in the database, but no GET/LIST query or MCP tool response ever projected it out — only raw mutation responses (create_*/update_*, which return the inserted/updated row directly) happened to show it. Confirmed live against production: list_code_plans and get_code_plan never showed it despite the column being populated. Adds a shared Attribution interface (createdById/createdByKind/ updatedById/updatedByKind) to CodePlan, Release, Task, and WorkItem, and threads the columns through the corresponding lib/db/queries.ts projections (getCodePlans/getCodePlan, getTasks, getReleases/getRelease, getWorkItems/getWorkItem, and the task list nested inside getCodePlan). Since the MCP tools return these same query-layer objects directly, this fixes both the UI and MCP consumers in one place — no route.ts changes needed. Deliberately keeps this to raw IDs, matching what was asked, rather than resolving to display names — lib/db/wiki.ts already has a working precedent (batch users lookup + fallback to legacy creator/reporter fields) if that's wanted as a follow-up. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016ZM1Fxzj2sqge7EyGQ8LXb --- lib/db/queries.ts | 36 +++++++++++++++++++++++++++++++++ lib/types.ts | 17 ++++++++++++---- tests/lib/db/queries.test.ts | 22 ++++++++++++++++++++ tests/lib/db/releases.test.ts | 12 +++++++++++ tests/lib/db/work-items.test.ts | 10 +++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) diff --git a/lib/db/queries.ts b/lib/db/queries.ts index 04574d0..f6f7c3b 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -342,6 +342,10 @@ export async function getCodePlans(userId: string, filters: PlanFilters = {}): P releaseId: codePlans.releaseId, createdAt: codePlans.createdAt, updatedAt: codePlans.updatedAt, + createdById: codePlans.createdById, + createdByKind: codePlans.createdByKind, + updatedById: codePlans.updatedById, + updatedByKind: codePlans.updatedByKind, productName: products.name, taskCount: sql`( select CAST(count(*) AS INTEGER) from tasks where tasks.code_plan_id = code_plans.id @@ -479,6 +483,10 @@ export async function getCodePlan(id: string, userId: string): Promise u.id), taskCount, completedTaskCount, @@ -503,6 +511,10 @@ export async function getCodePlan(id: string, userId: string): Promise { expect(t1.assigneeId).toBe(F.bob) expect(t1.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/) }) + + it('exposes createdById/updatedById on the plan and its tasks (null for unattributed fixture rows)', async () => { + // seedFixtures never stamps createdById directly — this asserts the field + // is actually projected out (previously it was silently dropped even + // though the column existed and was populated in real data). + const plan = await getCodePlan(F.planActive, F.alice) + expect(plan).toHaveProperty('createdById', null) + expect(plan).toHaveProperty('createdByKind', null) + const t1 = plan!.tasks.find((t) => t.id === F.task1)! + expect(t1).toHaveProperty('createdById', null) + }) + + it('exposes a real createdById for a plan created through the normal mutation path', async () => { + const { createCodePlan } = await import('@/lib/db/mutations') + const created = await createCodePlan( + { title: 'Attributed plan', description: '', productId: F.productShared, type: 'feature', tags: [], targetAssetIds: [] }, + F.bob, + ) + const plan = await getCodePlan(created.id, F.alice) + expect(plan!.createdById).toBe(F.bob) + expect(plan!.createdByKind).toBe('user') + }) }) // --------------------------------------------------------------------------- diff --git a/tests/lib/db/releases.test.ts b/tests/lib/db/releases.test.ts index 68fb3f7..f06ac8b 100644 --- a/tests/lib/db/releases.test.ts +++ b/tests/lib/db/releases.test.ts @@ -83,6 +83,18 @@ describe('releases mutations', () => { }) describe('getReleases / getRelease', () => { + it('exposes createdById on both the list and detail projections', async () => { + const release = await seedRelease() + const detail = await getRelease(release.id, F.alice) + expect(detail!.createdById).toBe(F.alice) + expect(detail!.createdByKind).toBe('user') + + const rows = await getReleases(F.alice, { productId: F.productShared }) + const row = rows.find((r) => r.id === release.id)! + expect(row.createdById).toBe(F.alice) + }) + + it('rolls up plan counts and derived work items through attached plans', async () => { const release = await seedRelease() await (db as any).insert(workItems).values([ diff --git a/tests/lib/db/work-items.test.ts b/tests/lib/db/work-items.test.ts index 6ebca70..a3260ab 100644 --- a/tests/lib/db/work-items.test.ts +++ b/tests/lib/db/work-items.test.ts @@ -111,6 +111,16 @@ describe('getWorkItems', () => { expect(await getWorkItems(F.carol)).toHaveLength(0) }) + it('exposes createdById on both the list and detail projections', async () => { + const item = await createFixtureItem() + const items = await getWorkItems(F.alice) + expect(items[0].createdById).toBe(F.alice) + expect(items[0].createdByKind).toBe('user') + + const found = await getWorkItem(item.id, F.alice) + expect(found!.createdById).toBe(F.alice) + }) + it('filters by type and status', async () => { await createFixtureItem() await createFixtureItem({ type: 'tech_debt', title: 'Legacy ORM calls', severity: 'medium' })