Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 131 lines 3.8 kB view raw
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}