feat: Persist review comments to database

Review comments on phase diffs now survive page reloads and phase
switches. Adds review_comments table (migration 0028), repository
port/adapter (13th repo), tRPC procedures (listReviewComments,
createReviewComment, resolveReviewComment, unresolveReviewComment),
and replaces useState-based comments in ReviewTab with tRPC queries
and mutations.
This commit is contained in:
Lukas May
2026-03-05 11:16:54 +01:00
parent 69d2543995
commit 173c7f7916
14 changed files with 293 additions and 27 deletions

View File

@@ -17,3 +17,4 @@ export { DrizzleChangeSetRepository } from './change-set.js';
export { DrizzleLogChunkRepository } from './log-chunk.js';
export { DrizzleConversationRepository } from './conversation.js';
export { DrizzleChatSessionRepository } from './chat-session.js';
export { DrizzleReviewCommentRepository } from './review-comment.js';

View File

@@ -0,0 +1,78 @@
/**
* Drizzle Review Comment Repository Adapter
*
* Implements ReviewCommentRepository interface using Drizzle ORM.
*/
import { eq, asc } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import type { DrizzleDatabase } from '../../index.js';
import { reviewComments, type ReviewComment } from '../../schema.js';
import type { ReviewCommentRepository, CreateReviewCommentData } from '../review-comment-repository.js';
export class DrizzleReviewCommentRepository implements ReviewCommentRepository {
constructor(private db: DrizzleDatabase) {}
async create(data: CreateReviewCommentData): Promise<ReviewComment> {
const now = new Date();
const id = nanoid();
await this.db.insert(reviewComments).values({
id,
phaseId: data.phaseId,
filePath: data.filePath,
lineNumber: data.lineNumber,
lineType: data.lineType,
body: data.body,
author: data.author ?? 'you',
resolved: false,
createdAt: now,
updatedAt: now,
});
const rows = await this.db
.select()
.from(reviewComments)
.where(eq(reviewComments.id, id))
.limit(1);
return rows[0]!;
}
async findByPhaseId(phaseId: string): Promise<ReviewComment[]> {
return this.db
.select()
.from(reviewComments)
.where(eq(reviewComments.phaseId, phaseId))
.orderBy(asc(reviewComments.createdAt));
}
async resolve(id: string): Promise<ReviewComment | null> {
await this.db
.update(reviewComments)
.set({ resolved: true, updatedAt: new Date() })
.where(eq(reviewComments.id, id));
const rows = await this.db
.select()
.from(reviewComments)
.where(eq(reviewComments.id, id))
.limit(1);
return rows[0] ?? null;
}
async unresolve(id: string): Promise<ReviewComment | null> {
await this.db
.update(reviewComments)
.set({ resolved: false, updatedAt: new Date() })
.where(eq(reviewComments.id, id));
const rows = await this.db
.select()
.from(reviewComments)
.where(eq(reviewComments.id, id))
.limit(1);
return rows[0] ?? null;
}
async delete(id: string): Promise<void> {
await this.db
.delete(reviewComments)
.where(eq(reviewComments.id, id));
}
}

View File

@@ -78,3 +78,8 @@ export type {
CreateChatSessionData,
CreateChatMessageData,
} from './chat-session-repository.js';
export type {
ReviewCommentRepository,
CreateReviewCommentData,
} from './review-comment-repository.js';

View File

@@ -0,0 +1,24 @@
/**
* Review Comment Repository Port Interface
*
* Port for persisting inline review comments on phase diffs.
*/
import type { ReviewComment } from '../schema.js';
export interface CreateReviewCommentData {
phaseId: string;
filePath: string;
lineNumber: number;
lineType: 'added' | 'removed' | 'context';
body: string;
author?: string;
}
export interface ReviewCommentRepository {
create(data: CreateReviewCommentData): Promise<ReviewComment>;
findByPhaseId(phaseId: string): Promise<ReviewComment[]>;
resolve(id: string): Promise<ReviewComment | null>;
unresolve(id: string): Promise<ReviewComment | null>;
delete(id: string): Promise<void>;
}