comments implement

This commit is contained in:
vadim committed 2026-10-01 12:02:03 +03:00
1 parent 9e0bef2527
commit 4b6ecfbf5d
41 files changed
+1350 -13

No files matched your search

@@ -0,0 +1,39 @@
import { Inject, Injectable } from '@nestjs/common';
import { COMMENT_REPOSITORY, type ICommentRepository } from '../../domain/repositories/comment.repository.interface.js';
import { ISSUE_REPOSITORY, type IIssueRepository } from '../../../issue/domain/repositories/issue.repository.interface.js';
import { Comment } from '../../domain/entities/comment.entity.js';
import { CommentNotFoundException } from '../../domain/exceptions/comment-not-found.exception.js';
@Injectable()
export class CommentAccessService {
constructor(
@Inject(COMMENT_REPOSITORY)
private readonly commentRepository: ICommentRepository,
@Inject(ISSUE_REPOSITORY)
private readonly issueRepository: IIssueRepository,
) {}
/** Видна ли задача пользователю (то же правило, что в IIssueRepository.findById) */
async isIssueVisible(issueId: number, userId: string): Promise<boolean> {
return (await this.issueRepository.findById(issueId, userId)) !== null;
}
/**
* Возвращает неудалённый комментарий, если его задача видна пользователю.
* Иначе — CommentNotFoundException, чтобы не раскрывать существование
* комментариев к чужим задачам.
*/
async getVisibleComment(id: string, userId: string): Promise<Comment> {
const comment = await this.commentRepository.findById(id);
if (!comment || comment.isDeleted) {
throw new CommentNotFoundException(id);
}
const issue = await this.issueRepository.findById(comment.issueId, userId);
if (!issue) {
throw new CommentNotFoundException(id);
}
return comment;
}
}
@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { Comment } from '../../domain/entities/comment.entity.js';
import { CommentDto } from '../dto/comment.dto.js';
import { CommentApplicationMapper } from '../mappers/comment-application.mapper.js';
import { GetUsersUseCase } from '../../../user/application/use-cases/get-users/get-users.use-case.js';
import { GetUsersQuery } from '../../../user/application/use-cases/get-users/get-users.query.js';
/** Раскрывает автора комментария (или списка комментариев) в полноценного пользователя */
@Injectable()
export class CommentAuthorExpanderService {
constructor(private readonly getUsersUseCase: GetUsersUseCase) {}
async expand(comment: Comment): Promise<CommentDto> {
const [dto] = await this.expandAll([comment]);
return dto!;
}
async expandAll(comments: Comment[]): Promise<CommentDto[]> {
if (comments.length === 0) return [];
const userIds = [...new Set(comments.map((comment) => comment.authorId).filter((id): id is string => Boolean(id)))];
const users = userIds.length > 0 ? await this.getUsersUseCase.execute(new GetUsersQuery(userIds)) : [];
const usersById = new Map(users.map((user) => [user.id, user]));
return comments.map((comment) => CommentApplicationMapper.toDto(comment, usersById));
}
}
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { Observable, Subject } from 'rxjs';
import { CommentDto } from '../dto/comment.dto.js';
export type CommentEventType = 'comment.created' | 'comment.updated' | 'comment.deleted';
export interface CommentEvent {
type: CommentEventType;
comment: CommentDto;
}
/**
* In-process шина событий comment-модуля для SSE-подписчиков
* (по аналогии с IssueEventsService). Работает в пределах одного
* инстанса приложения — для нескольких инстансов понадобится Redis pub/sub.
*/
@Injectable()
export class CommentEventsService {
private readonly events$ = new Subject<CommentEvent>();
emit(type: CommentEventType, comment: CommentDto): void {
this.events$.next({ type, comment });
}
onEvents(): Observable<CommentEvent> {
return this.events$.asObservable();
}
}