Skip to content

Commit 4ae056c

Browse files
Ripwordsclaude
andcommitted
refactor(comments): extract addReportComment service function
Mirror the triage.ts pattern: addReportComment(tx, args) runs the report-existence check, reportComments insert, and comment_added reportEvents insert inside the caller's transaction; actorClientId is threaded through both inserts. addReportCommentSideEffects handles enqueueCommentUpsert + publishReportStream post-commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5ba753b commit 4ae056c

2 files changed

Lines changed: 127 additions & 38 deletions

File tree

apps/dashboard/server/api/projects/[id]/reports/[reportId]/comments/index.post.ts

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@ import {
66
readValidatedBody,
77
setResponseStatus,
88
} from "h3"
9-
import { eq } from "drizzle-orm"
109
import { z } from "zod"
1110
import { db } from "../../../../../../db"
12-
import { reportComments } from "../../../../../../db/schema/report-comments"
13-
import { reports } from "../../../../../../db/schema/reports"
14-
import { githubIntegrations } from "../../../../../../db/schema/github-integrations"
11+
import {
12+
addReportComment,
13+
addReportCommentSideEffects,
14+
} from "../../../../../../lib/comments-service"
1515
import { requireProjectRole } from "../../../../../../lib/permissions"
16-
import { publishReportStream } from "../../../../../../lib/report-events-bus"
17-
import { enqueueCommentUpsert } from "../../../../../../lib/enqueue-sync"
1816

1917
const CreateCommentBody = z.object({
2018
body: z.string().min(1).max(65_536),
@@ -26,41 +24,25 @@ export default defineEventHandler(async (event) => {
2624
if (!projectId || !reportId) throw createError({ statusCode: 400, statusMessage: "Missing ids" })
2725

2826
const { session } = await requireProjectRole(event, projectId, "manager")
29-
const body = await readValidatedBody(event, (b) => CreateCommentBody.parse(b))
30-
31-
const [report] = await db.select().from(reports).where(eq(reports.id, reportId)).limit(1)
32-
if (!report || report.projectId !== projectId) {
33-
throw createError({ statusCode: 404, statusMessage: "Report not found" })
34-
}
27+
const { body } = await readValidatedBody(event, (b) => CreateCommentBody.parse(b))
3528

36-
const [inserted] = await db
37-
.insert(reportComments)
38-
.values({
29+
const result = await db.transaction(async (tx) =>
30+
addReportComment(tx, {
31+
projectId,
3932
reportId,
40-
userId: session.userId,
41-
body: body.body,
42-
source: "dashboard",
43-
})
44-
.returning()
45-
46-
// Enqueue sync if report is linked to a GitHub issue and integration is connected
47-
if (report.githubIssueNumber !== null) {
48-
const [integration] = await db
49-
.select({ status: githubIntegrations.status })
50-
.from(githubIntegrations)
51-
.where(eq(githubIntegrations.projectId, projectId))
52-
.limit(1)
53-
54-
if (integration?.status === "connected") {
55-
await enqueueCommentUpsert(reportId, inserted.id)
56-
}
57-
}
58-
59-
publishReportStream(reportId, {
60-
kind: "comment_added",
61-
payload: { commentId: inserted.id },
33+
actorId: session.userId,
34+
actorClientId: null,
35+
body,
36+
}),
37+
)
38+
39+
await addReportCommentSideEffects({
40+
projectId,
41+
reportId,
42+
commentId: result.comment.id,
43+
githubIssueNumber: result.githubIssueNumber,
6244
})
6345

6446
setResponseStatus(event, 201)
65-
return { comment: inserted }
47+
return { comment: result.comment }
6648
})
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// apps/dashboard/server/lib/comments-service.ts
2+
import { eq } from "drizzle-orm"
3+
import { createError } from "h3"
4+
import type { DB } from "../db"
5+
import { db } from "../db"
6+
import { reportComments } from "../db/schema/report-comments"
7+
import { reports } from "../db/schema/reports"
8+
import { githubIntegrations } from "../db/schema/github-integrations"
9+
import { reportEvents } from "../db/schema/report-events"
10+
import { enqueueCommentUpsert } from "./enqueue-sync"
11+
import { publishReportStream } from "./report-events-bus"
12+
13+
// Mirror the DbTransaction type pattern from triage.ts
14+
type DbTransaction = Parameters<DB["transaction"]>[0] extends (tx: infer T) => unknown ? T : never
15+
16+
export interface AddReportCommentArgs {
17+
projectId: string
18+
reportId: string
19+
actorId: string
20+
actorClientId: string | null
21+
body: string
22+
}
23+
24+
export interface AddReportCommentResult {
25+
comment: typeof reportComments.$inferSelect
26+
/** Forwarded to addReportCommentSideEffects to avoid a second DB lookup. */
27+
githubIssueNumber: number | null
28+
}
29+
30+
/**
31+
* Insert a dashboard-source comment on a report. Used by:
32+
* - The dashboard POST endpoint (actorClientId: null)
33+
* - MCP repro_add_comment (actorClientId: OAuth client_id)
34+
*
35+
* In-tx: validates the report belongs to the project, inserts the
36+
* report_comments row (source: "dashboard", actorClientId), inserts a
37+
* report_events row of kind "comment_added" (with actorClientId).
38+
*
39+
* Throws 404 if the report doesn't exist or doesn't match projectId.
40+
*/
41+
export async function addReportComment(
42+
tx: DbTransaction,
43+
args: AddReportCommentArgs,
44+
): Promise<AddReportCommentResult> {
45+
const [report] = await tx.select().from(reports).where(eq(reports.id, args.reportId)).limit(1)
46+
if (!report || report.projectId !== args.projectId) {
47+
throw createError({ statusCode: 404, statusMessage: "Report not found" })
48+
}
49+
50+
const [inserted] = await tx
51+
.insert(reportComments)
52+
.values({
53+
reportId: args.reportId,
54+
userId: args.actorId,
55+
actorClientId: args.actorClientId,
56+
body: args.body,
57+
source: "dashboard",
58+
})
59+
.returning()
60+
61+
await tx.insert(reportEvents).values({
62+
reportId: args.reportId,
63+
projectId: args.projectId,
64+
actorId: args.actorId,
65+
actorClientId: args.actorClientId,
66+
kind: "comment_added",
67+
payload: { commentId: inserted.id },
68+
})
69+
70+
return {
71+
comment: inserted,
72+
githubIssueNumber: report.githubIssueNumber,
73+
}
74+
}
75+
76+
export interface AddReportCommentSideEffectArgs {
77+
projectId: string
78+
reportId: string
79+
commentId: string
80+
githubIssueNumber: number | null
81+
}
82+
83+
/**
84+
* Run the side effects that must happen AFTER the transaction commits:
85+
* - enqueueCommentUpsert (push the comment to GitHub if integration is connected)
86+
* - publishReportStream (SSE event for dashboard live-updates)
87+
*/
88+
export async function addReportCommentSideEffects(
89+
args: AddReportCommentSideEffectArgs,
90+
): Promise<void> {
91+
if (args.githubIssueNumber !== null) {
92+
const [integration] = await db
93+
.select({ status: githubIntegrations.status })
94+
.from(githubIntegrations)
95+
.where(eq(githubIntegrations.projectId, args.projectId))
96+
.limit(1)
97+
98+
if (integration?.status === "connected") {
99+
await enqueueCommentUpsert(args.reportId, args.commentId)
100+
}
101+
}
102+
103+
publishReportStream(args.reportId, {
104+
kind: "comment_added",
105+
payload: { commentId: args.commentId },
106+
})
107+
}

0 commit comments

Comments
 (0)