Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { type Kysely } from 'kysely';
2import { v1 as uuidv1 } from 'uuid';
3
4import { makeNotFoundError } from '../../../utils/errors.js';
5import { isForeignKeyViolationError } from '../../../utils/kysely.js';
6import { type ManualReviewToolServicePg } from '../dbTypes.js';
7
8export type ManualReviewJobComment = {
9 id: string;
10 commentText: string;
11 authorId: string;
12 createdAt: Date;
13};
14
15const manualReviewCommentDbSelection = [
16 'id',
17 'comment_text as commentText',
18 'author_id as authorId',
19 'created_at as createdAt',
20] as const;
21
22export default class CommentOperations {
23 constructor(private readonly pgQuery: Kysely<ManualReviewToolServicePg>) {}
24
25 private async getRelatedJobIds(opts: { orgId: string; jobId: string }): Promise<string[]> {
26 const { orgId, jobId } = opts;
27
28 // First get the item identifiers for the current job
29 const currentJob = await this.pgQuery
30 .selectFrom('manual_review_tool.job_creations')
31 .select(['item_id', 'item_type_id'])
32 .where('org_id', '=', orgId)
33 .where('id', '=', jobId as any)
34 .executeTakeFirst();
35
36 if (!currentJob) {
37 // Fallback to single job if current job not found in job_creations
38 return [jobId];
39 }
40
41 // Get all job IDs for the same item across all queues
42 const relatedJobIds = await this.pgQuery
43 .selectFrom('manual_review_tool.job_creations')
44 .select(['id'])
45 .where('org_id', '=', orgId)
46 .where('item_id', '=', currentJob.item_id)
47 .where('item_type_id', '=', currentJob.item_type_id)
48 .execute();
49
50 return relatedJobIds.map(row => row.id);
51 }
52
53 async getComments(opts: { orgId: string; jobId: string }) {
54 const { orgId } = opts;
55 const jobIds = await this.getRelatedJobIds(opts);
56
57 const comments = await this.pgQuery
58 .selectFrom('manual_review_tool.job_comments')
59 .select(manualReviewCommentDbSelection)
60 .where('org_id', '=', orgId)
61 .where('job_id', 'in', jobIds as any[])
62 .orderBy('created_at', 'asc')
63 .execute();
64
65 return comments;
66 }
67
68 async getCommentCount(opts: { orgId: string; jobId: string }) {
69 const { orgId } = opts;
70 const jobIds = await this.getRelatedJobIds(opts);
71
72 const result = await this.pgQuery
73 .selectFrom('manual_review_tool.job_comments')
74 .select((eb) => eb.fn.count('id').as('count'))
75 .where('org_id', '=', orgId)
76 .where('job_id', 'in', jobIds as any[])
77 .executeTakeFirst();
78
79 return result?.count ? Number(result.count) : 0;
80 }
81
82 async addComment(opts: {
83 orgId: string;
84 jobId: string;
85 commentText: string;
86 authorId: string;
87 }) {
88 const { orgId, jobId, commentText, authorId } = opts;
89 try {
90 const comment = await this.pgQuery
91 .insertInto('manual_review_tool.job_comments')
92 .returning(manualReviewCommentDbSelection)
93 .values([
94 {
95 id: uuidv1(),
96 org_id: orgId,
97 job_id: jobId,
98 comment_text: commentText,
99 author_id: authorId,
100 },
101 ])
102 .executeTakeFirst();
103
104 return comment!;
105 } catch (e) {
106 if (isForeignKeyViolationError(e)) {
107 throw makeNotFoundError('Job not found', { shouldErrorSpan: true });
108 }
109
110 throw e;
111 }
112 }
113
114 async deleteComment(opts: {
115 orgId: string;
116 jobId: string;
117 userId: string;
118 commentId: string;
119 }) {
120 const { orgId, jobId, userId, commentId } = opts;
121 const result = await this.pgQuery
122 .deleteFrom('manual_review_tool.job_comments')
123 .where('org_id', '=', orgId)
124 .where('job_id', '=', jobId)
125 .where('author_id', '=', userId)
126 .where('id', '=', commentId)
127 .executeTakeFirst();
128
129 return result.numDeletedRows === 1n;
130 }
131}