Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions lib/db/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>`(
select CAST(count(*) AS INTEGER) from tasks where tasks.code_plan_id = code_plans.id
Expand Down Expand Up @@ -479,6 +483,10 @@ export async function getCodePlan(id: string, userId: string): Promise<CodePlanD
endDate: plan.endDate ?? undefined,
deadline: plan.deadline ?? undefined,
creatorId: plan.creatorId,
createdById: plan.createdById,
createdByKind: plan.createdByKind,
updatedById: plan.updatedById,
updatedByKind: plan.updatedByKind,
assigneeIds: resolvedAssignees.map((u) => u.id),
taskCount,
completedTaskCount,
Expand All @@ -503,6 +511,10 @@ export async function getCodePlan(id: string, userId: string): Promise<CodePlanD
endDate: t.endDate ?? undefined,
estimatedEffort: t.estimatedEffort ?? undefined,
actualEffort: t.actualEffort ?? undefined,
createdById: t.createdById,
createdByKind: t.createdByKind,
updatedById: t.updatedById,
updatedByKind: t.updatedByKind,
createdAt: t.createdAt.toISOString(),
updatedAt: t.updatedAt.toISOString(),
})),
Expand Down Expand Up @@ -573,6 +585,10 @@ export async function getTasks(userId: string, filters: TaskFilters = {}): Promi
externalUrl: tasks.externalUrl,
createdAt: tasks.createdAt,
updatedAt: tasks.updatedAt,
createdById: tasks.createdById,
createdByKind: tasks.createdByKind,
updatedById: tasks.updatedById,
updatedByKind: tasks.updatedByKind,
planTitle: codePlans.title,
planStatus: codePlans.status,
assetName: assets.name,
Expand Down Expand Up @@ -605,6 +621,10 @@ export async function getTasks(userId: string, filters: TaskFilters = {}): Promi
externalUrl: r.externalUrl ?? undefined,
createdAt: r.createdAt.toISOString(),
updatedAt: r.updatedAt.toISOString(),
createdById: r.createdById,
createdByKind: r.createdByKind,
updatedById: r.updatedById,
updatedByKind: r.updatedByKind,
planTitle: r.planTitle,
planStatus: r.planStatus,
assetName: r.assetName,
Expand Down Expand Up @@ -653,6 +673,10 @@ type WorkItemRow = {
externalUrl: string | null
createdAt: Date
updatedAt: Date
createdById: string | null
createdByKind: string | null
updatedById: string | null
updatedByKind: string | null
productName: string
productSlug: string
assetName: string | null
Expand Down Expand Up @@ -683,6 +707,10 @@ function mapWorkItemRow(
externalUrl: r.externalUrl ?? undefined,
createdAt: r.createdAt.toISOString(),
updatedAt: r.updatedAt.toISOString(),
createdById: r.createdById,
createdByKind: r.createdByKind,
updatedById: r.updatedById,
updatedByKind: r.updatedByKind,
productName: r.productName,
productSlug: r.productSlug,
assetName: r.assetName,
Expand Down Expand Up @@ -793,6 +821,10 @@ function workItemColumns() {
externalUrl: workItems.externalUrl,
createdAt: workItems.createdAt,
updatedAt: workItems.updatedAt,
createdById: workItems.createdById,
createdByKind: workItems.createdByKind,
updatedById: workItems.updatedById,
updatedByKind: workItems.updatedByKind,
}
}

Expand Down Expand Up @@ -1860,6 +1892,10 @@ function releaseRow(r: typeof releases.$inferSelect): Release {
shippedAt: r.shippedAt?.toISOString(),
tags: r.tags,
creatorId: r.creatorId,
createdById: r.createdById,
createdByKind: r.createdByKind,
updatedById: r.updatedById,
updatedByKind: r.updatedByKind,
createdAt: r.createdAt.toISOString(),
updatedAt: r.updatedAt.toISOString(),
}
Expand Down
17 changes: 13 additions & 4 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ export type WorkItemStatus = 'open' | 'planned' | 'in_progress' | 'resolved' | '
export type WorkItemSeverity = 'low' | 'medium' | 'high' | 'critical'
export type ItemSource = 'native' | 'github' | 'gitlab' | 'jira' | 'asana' | 'linear'

/** Who created/last-edited a record. Null means unattributed (predates the
* attribution columns, or the mutation didn't have an actor). */
export interface Attribution {
createdById: string | null
createdByKind: string | null
updatedById: string | null
updatedByKind: string | null
}

export interface User {
id: string
email: string
Expand Down Expand Up @@ -127,7 +136,7 @@ export interface ReleaseAssetRow {
}

/** Delivery grouping above code plans: what ships together. */
export interface Release {
export interface Release extends Attribution {
id: string
productId: string
name: string
Expand All @@ -151,7 +160,7 @@ export interface PlanAsset {
notes?: string
}

export interface CodePlan {
export interface CodePlan extends Attribution {
id: string
title: string
description: string
Expand Down Expand Up @@ -179,7 +188,7 @@ export interface CodePlan {
updatedAt: string
}

export interface Task {
export interface Task extends Attribution {
id: string
codePlanId: string
assetId?: string
Expand All @@ -206,7 +215,7 @@ export interface Task {
* tech-debt item. Native by default; when source ≠ native it mirrors an item
* in an external tracker and its mirrored fields are read-only here.
*/
export interface WorkItem {
export interface WorkItem extends Attribution {
id: string
productId: string
assetId?: string
Expand Down
22 changes: 22 additions & 0 deletions tests/lib/db/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,28 @@ describe('getCodePlan', () => {
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')
})
})

// ---------------------------------------------------------------------------
Expand Down
12 changes: 12 additions & 0 deletions tests/lib/db/releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
10 changes: 10 additions & 0 deletions tests/lib/db/work-items.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
Loading