Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ workflows:
only:
- develop
- feat/ai-workflows
- pm-1955
- pm-1957


- 'build-prod':
Expand Down
55 changes: 55 additions & 0 deletions src/api/ai-workflow/ai-workflow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
UpdateAiWorkflowRunDto,
CreateRunItemCommentDto,
UpdateAiWorkflowRunItemDto,
UpdateRunItemCommentDto,
} from '../../dto/aiWorkflow.dto';
import { Scopes } from 'src/shared/decorators/scopes.decorator';
import { UserRole } from 'src/shared/enums/userRole.enum';
Expand All @@ -40,6 +41,60 @@ import { User } from 'src/shared/decorators/user.decorator';
export class AiWorkflowController {
constructor(private readonly aiWorkflowService: AiWorkflowService) {}

@Patch('/:workflowId/runs/:runId/items/:itemId/comments/:commentId')
@Roles(
UserRole.Submitter,
UserRole.Copilot,
UserRole.ProjectManager,
UserRole.Admin,
UserRole.Reviewer,
UserRole.User,
)
@ApiOperation({ summary: 'Update a comment by id' })
@ApiParam({ name: 'workflowId', description: 'Workflow ID' })
@ApiParam({ name: 'runId', description: 'Run ID' })
@ApiParam({ name: 'itemId', description: 'Item ID' })
@ApiParam({ name: 'commentId', description: 'Comment ID' })
@ApiBody({
description: 'Partial comment data to update',
schema: {
type: 'object',
properties: {
content: { type: 'string' },
upVotes: { type: 'number' },
downVotes: { type: 'number' },
},
additionalProperties: false,
},
})
@ApiResponse({
status: 200,
description: 'Comment updated successfully.',
})
@ApiResponse({
status: 403,
description: 'Forbidden. User not comment creator.',
})
@ApiResponse({ status: 404, description: 'Comment not found.' })
async updateRunItemComment(
@Param('workflowId') workflowId: string,
@Param('runId') runId: string,
@Param('itemId') itemId: string,
@Param('commentId') commentId: string,
@Body(new ValidationPipe({ whitelist: true, transform: true }))
body: UpdateRunItemCommentDto,
@User() user: JwtUser,
) {
return this.aiWorkflowService.updateCommentById(
user,
workflowId,
runId,
itemId,
commentId,
body,
);
}

@Post('/:workflowId/runs/:runId/items/:itemId/comments')
@Roles(
UserRole.Submitter,
Expand Down
88 changes: 88 additions & 0 deletions src/api/ai-workflow/ai-workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UpdateAiWorkflowDto,
UpdateAiWorkflowRunDto,
UpdateAiWorkflowRunItemDto,
UpdateRunItemCommentDto,
} from '../../dto/aiWorkflow.dto';
import { ScorecardStatus } from 'src/dto/scorecard.dto';
import { JwtUser } from 'src/shared/modules/global/jwt.service';
Expand All @@ -38,6 +39,93 @@ export class AiWorkflowService {
this.logger = LoggerService.forRoot('AiWorkflowService');
}

async updateCommentById(
user: JwtUser,
workflowId: string,
runId: string,
itemId: string,
commentId: string,
patchData: UpdateRunItemCommentDto,
) {
this.logger.log(
`Updating comment ${commentId} for workflow ${workflowId}, run ${runId}, item ${itemId}`,
);

try {
const workflow = await this.prisma.aiWorkflow.findUnique({
where: { id: workflowId },
});
if (!workflow) {
throw new NotFoundException(
`Workflow with id ${workflowId} not found.`,
);
}

const run = await this.prisma.aiWorkflowRun.findUnique({
where: { id: runId },
});
if (!run || run.workflowId !== workflowId) {
throw new NotFoundException(
`Run with id ${runId} not found or does not belong to workflow ${workflowId}.`,
);
}

const item = await this.prisma.aiWorkflowRunItem.findUnique({
where: { id: itemId },
});
if (!item || item.workflowRunId !== runId) {
throw new NotFoundException(
`Item with id ${itemId} not found or does not belong to run ${runId}.`,
);
}

const comment = await this.prisma.aiWorkflowRunItemComment.findUnique({
where: { id: commentId },
});
if (!comment || comment.workflowRunItemId !== itemId) {
throw new NotFoundException(
`Comment with id ${commentId} not found or does not belong to item ${itemId}.`,
);
}

if (String(comment.userId) !== String(user.userId)) {
throw new ForbiddenException(
'User is not the creator of this comment and cannot update it.',
);
}

const allowedFields = ['content', 'upVotes', 'downVotes'];
const updateData: any = {};
for (const key of allowedFields) {
if (key in patchData) {
updateData[key] = patchData[key];
}
}

if (Object.keys(updateData).length === 0) {
throw new BadRequestException('No valid fields provided for update.');
}

const updatedComment = await this.prisma.aiWorkflowRunItemComment.update({
where: { id: commentId },
data: updateData,
});

return updatedComment;
} catch (error) {
if (
error instanceof NotFoundException ||
error instanceof ForbiddenException ||
error instanceof BadRequestException
) {
throw error;
}

this.logger.error(`Failed to update comment ${commentId}`, error);
throw new InternalServerErrorException('Failed to update comment');
}
}

async createRunItemComment(
workflowId: string,
runId: string,
Expand Down
29 changes: 29 additions & 0 deletions src/dto/aiWorkflow.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,32 @@ export class CreateRunItemCommentDto {
@IsOptional()
parentId?: string;
}

export class UpdateRunItemCommentDto {
@ApiProperty({ required: false })
@IsString()
@IsOptional()
content?: string;

@ApiProperty({ required: false })
@IsInt()
@IsOptional()
upVotes?: number;

@ApiProperty({ required: false })
@IsInt()
@IsOptional()
downVotes?: number;

@ApiHideProperty()
@IsEmpty({ message: 'parentId cannot be updated' })
parentId?: never;

@ApiHideProperty()
@IsEmpty({ message: 'userId cannot be updated' })
userId?: never;

@ApiHideProperty()
@IsEmpty({ message: 'workflowRunItemId cannot be updated' })
workflowRunItemId?: never;
}