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:
@@ -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';
|
||||
|
||||
78
apps/server/db/repositories/drizzle/review-comment.ts
Normal file
78
apps/server/db/repositories/drizzle/review-comment.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -78,3 +78,8 @@ export type {
|
||||
CreateChatSessionData,
|
||||
CreateChatMessageData,
|
||||
} from './chat-session-repository.js';
|
||||
|
||||
export type {
|
||||
ReviewCommentRepository,
|
||||
CreateReviewCommentData,
|
||||
} from './review-comment-repository.js';
|
||||
|
||||
24
apps/server/db/repositories/review-comment-repository.ts
Normal file
24
apps/server/db/repositories/review-comment-repository.ts
Normal 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>;
|
||||
}
|
||||
@@ -604,3 +604,27 @@ export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({
|
||||
|
||||
export type ChatMessage = InferSelectModel<typeof chatMessages>;
|
||||
export type NewChatMessage = InferInsertModel<typeof chatMessages>;
|
||||
|
||||
// ============================================================================
|
||||
// REVIEW COMMENTS
|
||||
// ============================================================================
|
||||
|
||||
export const reviewComments = sqliteTable('review_comments', {
|
||||
id: text('id').primaryKey(),
|
||||
phaseId: text('phase_id')
|
||||
.notNull()
|
||||
.references(() => phases.id, { onDelete: 'cascade' }),
|
||||
filePath: text('file_path').notNull(),
|
||||
lineNumber: integer('line_number').notNull(),
|
||||
lineType: text('line_type', { enum: ['added', 'removed', 'context'] }).notNull(),
|
||||
body: text('body').notNull(),
|
||||
author: text('author').notNull().default('you'),
|
||||
resolved: integer('resolved', { mode: 'boolean' }).notNull().default(false),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
}, (table) => [
|
||||
index('review_comments_phase_id_idx').on(table.phaseId),
|
||||
]);
|
||||
|
||||
export type ReviewComment = InferSelectModel<typeof reviewComments>;
|
||||
export type NewReviewComment = InferInsertModel<typeof reviewComments>;
|
||||
|
||||
Reference in New Issue
Block a user