1 parent
4b6ecfbf5d
commit
5cfc8986d7
22 files changed
+145
-44
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"printWidth": 140
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,14 @@ function isPaginatedResult<T>(value: unknown): value is PaginatedResult<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TransformResponseInterceptor<T> implements NestInterceptor<T, T | ResponseEnvelope<T> | PaginatedResponseEnvelope<T>> {
|
export class TransformResponseInterceptor<T> implements NestInterceptor<
|
||||||
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<T | ResponseEnvelope<T> | PaginatedResponseEnvelope<T>> {
|
T,
|
||||||
|
T | ResponseEnvelope<T> | PaginatedResponseEnvelope<T>
|
||||||
|
> {
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler<T>,
|
||||||
|
): Observable<T | ResponseEnvelope<T> | PaginatedResponseEnvelope<T>> {
|
||||||
if (context.getType() !== 'http') {
|
if (context.getType() !== 'http') {
|
||||||
return next.handle();
|
return next.handle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { COMMENT_REPOSITORY, type ICommentRepository } from '../../domain/repositories/comment.repository.interface.js';
|
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 {
|
||||||
|
ISSUE_REPOSITORY,
|
||||||
|
type IIssueRepository,
|
||||||
|
} from '../../../issue/domain/repositories/issue.repository.interface.js';
|
||||||
import { Comment } from '../../domain/entities/comment.entity.js';
|
import { Comment } from '../../domain/entities/comment.entity.js';
|
||||||
import { CommentNotFoundException } from '../../domain/exceptions/comment-not-found.exception.js';
|
import { CommentNotFoundException } from '../../domain/exceptions/comment-not-found.exception.js';
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -49,7 +49,9 @@ describe('CreateCommentUseCase', () => {
|
|||||||
it('throws IssueNotFoundException when the issue is not visible to the user', async () => {
|
it('throws IssueNotFoundException when the issue is not visible to the user', async () => {
|
||||||
vi.mocked(issueRepository.findById).mockResolvedValue(null);
|
vi.mocked(issueRepository.findById).mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(useCase.execute(new CreateCommentCommand(1, 'user-2', 'Hello'))).rejects.toThrow(IssueNotFoundException);
|
await expect(useCase.execute(new CreateCommentCommand(1, 'user-2', 'Hello'))).rejects.toThrow(
|
||||||
|
IssueNotFoundException,
|
||||||
|
);
|
||||||
expect(commentRepository.create).not.toHaveBeenCalled();
|
expect(commentRepository.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { COMMENT_REPOSITORY, type ICommentRepository } from '../../../domain/repositories/comment.repository.interface.js';
|
import {
|
||||||
import { ISSUE_REPOSITORY, type IIssueRepository } from '../../../../issue/domain/repositories/issue.repository.interface.js';
|
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 { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
||||||
import { Comment } from '../../../domain/entities/comment.entity.js';
|
import { Comment } from '../../../domain/entities/comment.entity.js';
|
||||||
import { CreateCommentCommand } from './create-comment.command.js';
|
import { CreateCommentCommand } from './create-comment.command.js';
|
||||||
|
|||||||
+13
-3
@@ -8,7 +8,13 @@ import { CommentEventsService, type CommentEvent } from '../../services/comment-
|
|||||||
import { CommentAccessService } from '../../services/comment-access.service.js';
|
import { CommentAccessService } from '../../services/comment-access.service.js';
|
||||||
import { DeleteCommentUseCase } from './delete-comment.use-case.js';
|
import { DeleteCommentUseCase } from './delete-comment.use-case.js';
|
||||||
import { DeleteCommentCommand } from './delete-comment.command.js';
|
import { DeleteCommentCommand } from './delete-comment.command.js';
|
||||||
import { buildComment, buildIssue, mockAuthorExpander, mockCommentRepository, mockIssueRepository } from '../test-helpers.js';
|
import {
|
||||||
|
buildComment,
|
||||||
|
buildIssue,
|
||||||
|
mockAuthorExpander,
|
||||||
|
mockCommentRepository,
|
||||||
|
mockIssueRepository,
|
||||||
|
} from '../test-helpers.js';
|
||||||
|
|
||||||
describe('DeleteCommentUseCase', () => {
|
describe('DeleteCommentUseCase', () => {
|
||||||
let commentRepository: ICommentRepository;
|
let commentRepository: ICommentRepository;
|
||||||
@@ -46,13 +52,17 @@ describe('DeleteCommentUseCase', () => {
|
|||||||
it('throws CommentForbiddenException for a non-author', async () => {
|
it('throws CommentForbiddenException for a non-author', async () => {
|
||||||
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment());
|
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment());
|
||||||
|
|
||||||
await expect(useCase.execute(new DeleteCommentCommand('comment-1', 'user-2'))).rejects.toThrow(CommentForbiddenException);
|
await expect(useCase.execute(new DeleteCommentCommand('comment-1', 'user-2'))).rejects.toThrow(
|
||||||
|
CommentForbiddenException,
|
||||||
|
);
|
||||||
expect(commentRepository.update).not.toHaveBeenCalled();
|
expect(commentRepository.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws CommentNotFoundException when the comment does not exist', async () => {
|
it('throws CommentNotFoundException when the comment does not exist', async () => {
|
||||||
vi.mocked(commentRepository.findById).mockResolvedValue(null);
|
vi.mocked(commentRepository.findById).mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(useCase.execute(new DeleteCommentCommand('missing', 'user-1'))).rejects.toThrow(CommentNotFoundException);
|
await expect(useCase.execute(new DeleteCommentCommand('missing', 'user-1'))).rejects.toThrow(
|
||||||
|
CommentNotFoundException,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { COMMENT_REPOSITORY, type ICommentRepository } from '../../../domain/repositories/comment.repository.interface.js';
|
import {
|
||||||
|
COMMENT_REPOSITORY,
|
||||||
|
type ICommentRepository,
|
||||||
|
} from '../../../domain/repositories/comment.repository.interface.js';
|
||||||
import { DeleteCommentCommand } from './delete-comment.command.js';
|
import { DeleteCommentCommand } from './delete-comment.command.js';
|
||||||
import { CommentAuthorExpanderService } from '../../services/comment-author-expander.service.js';
|
import { CommentAuthorExpanderService } from '../../services/comment-author-expander.service.js';
|
||||||
import { CommentEventsService } from '../../services/comment-events.service.js';
|
import { CommentEventsService } from '../../services/comment-events.service.js';
|
||||||
|
|||||||
+7
-1
@@ -4,7 +4,13 @@ import type { IIssueRepository } from '../../../../issue/domain/repositories/iss
|
|||||||
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
||||||
import { ListCommentsUseCase } from './list-comments.use-case.js';
|
import { ListCommentsUseCase } from './list-comments.use-case.js';
|
||||||
import { ListCommentsQuery } from './list-comments.query.js';
|
import { ListCommentsQuery } from './list-comments.query.js';
|
||||||
import { buildComment, buildIssue, mockAuthorExpander, mockCommentRepository, mockIssueRepository } from '../test-helpers.js';
|
import {
|
||||||
|
buildComment,
|
||||||
|
buildIssue,
|
||||||
|
mockAuthorExpander,
|
||||||
|
mockCommentRepository,
|
||||||
|
mockIssueRepository,
|
||||||
|
} from '../test-helpers.js';
|
||||||
|
|
||||||
describe('ListCommentsUseCase', () => {
|
describe('ListCommentsUseCase', () => {
|
||||||
let commentRepository: ICommentRepository;
|
let commentRepository: ICommentRepository;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { COMMENT_REPOSITORY, type ICommentRepository } from '../../../domain/repositories/comment.repository.interface.js';
|
import {
|
||||||
import { ISSUE_REPOSITORY, type IIssueRepository } from '../../../../issue/domain/repositories/issue.repository.interface.js';
|
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 { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
||||||
import { ListCommentsQuery } from './list-comments.query.js';
|
import { ListCommentsQuery } from './list-comments.query.js';
|
||||||
import { CommentDto } from '../../dto/comment.dto.js';
|
import { CommentDto } from '../../dto/comment.dto.js';
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { Issue } from '../../../issue/domain/entities/issue.entity.js';
|
|||||||
import { CommentAuthorExpanderService } from '../services/comment-author-expander.service.js';
|
import { CommentAuthorExpanderService } from '../services/comment-author-expander.service.js';
|
||||||
import { CommentApplicationMapper } from '../mappers/comment-application.mapper.js';
|
import { CommentApplicationMapper } from '../mappers/comment-application.mapper.js';
|
||||||
|
|
||||||
export const buildComment = (overrides: Partial<{ id: string; issueId: number; authorId: string; deletedAt: Date | null }> = {}) =>
|
export const buildComment = (
|
||||||
|
overrides: Partial<{ id: string; issueId: number; authorId: string; deletedAt: Date | null }> = {},
|
||||||
|
) =>
|
||||||
Comment.reconstitute({
|
Comment.reconstitute({
|
||||||
id: overrides.id ?? 'comment-1',
|
id: overrides.id ?? 'comment-1',
|
||||||
content: 'Original',
|
content: 'Original',
|
||||||
|
|||||||
+16
-4
@@ -7,7 +7,13 @@ import { CommentEventsService } from '../../services/comment-events.service.js';
|
|||||||
import { CommentAccessService } from '../../services/comment-access.service.js';
|
import { CommentAccessService } from '../../services/comment-access.service.js';
|
||||||
import { UpdateCommentUseCase } from './update-comment.use-case.js';
|
import { UpdateCommentUseCase } from './update-comment.use-case.js';
|
||||||
import { UpdateCommentCommand } from './update-comment.command.js';
|
import { UpdateCommentCommand } from './update-comment.command.js';
|
||||||
import { buildComment, buildIssue, mockAuthorExpander, mockCommentRepository, mockIssueRepository } from '../test-helpers.js';
|
import {
|
||||||
|
buildComment,
|
||||||
|
buildIssue,
|
||||||
|
mockAuthorExpander,
|
||||||
|
mockCommentRepository,
|
||||||
|
mockIssueRepository,
|
||||||
|
} from '../test-helpers.js';
|
||||||
|
|
||||||
describe('UpdateCommentUseCase', () => {
|
describe('UpdateCommentUseCase', () => {
|
||||||
let commentRepository: ICommentRepository;
|
let commentRepository: ICommentRepository;
|
||||||
@@ -54,20 +60,26 @@ describe('UpdateCommentUseCase', () => {
|
|||||||
it('throws CommentForbiddenException for a non-author', async () => {
|
it('throws CommentForbiddenException for a non-author', async () => {
|
||||||
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment());
|
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment());
|
||||||
|
|
||||||
await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-2', 'Changed'))).rejects.toThrow(CommentForbiddenException);
|
await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-2', 'Changed'))).rejects.toThrow(
|
||||||
|
CommentForbiddenException,
|
||||||
|
);
|
||||||
expect(commentRepository.update).not.toHaveBeenCalled();
|
expect(commentRepository.update).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws CommentNotFoundException for a deleted comment', async () => {
|
it('throws CommentNotFoundException for a deleted comment', async () => {
|
||||||
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment({ deletedAt: new Date() }));
|
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment({ deletedAt: new Date() }));
|
||||||
|
|
||||||
await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Changed'))).rejects.toThrow(CommentNotFoundException);
|
await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Changed'))).rejects.toThrow(
|
||||||
|
CommentNotFoundException,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws CommentNotFoundException when the issue is not visible to the user', async () => {
|
it('throws CommentNotFoundException when the issue is not visible to the user', async () => {
|
||||||
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment());
|
vi.mocked(commentRepository.findById).mockResolvedValue(buildComment());
|
||||||
vi.mocked(issueRepository.findById).mockResolvedValue(null);
|
vi.mocked(issueRepository.findById).mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Changed'))).rejects.toThrow(CommentNotFoundException);
|
await expect(useCase.execute(new UpdateCommentCommand('comment-1', 'user-1', 'Changed'))).rejects.toThrow(
|
||||||
|
CommentNotFoundException,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { COMMENT_REPOSITORY, type ICommentRepository } from '../../../domain/repositories/comment.repository.interface.js';
|
import {
|
||||||
|
COMMENT_REPOSITORY,
|
||||||
|
type ICommentRepository,
|
||||||
|
} from '../../../domain/repositories/comment.repository.interface.js';
|
||||||
import { UpdateCommentCommand } from './update-comment.command.js';
|
import { UpdateCommentCommand } from './update-comment.command.js';
|
||||||
import { CommentDto } from '../../dto/comment.dto.js';
|
import { CommentDto } from '../../dto/comment.dto.js';
|
||||||
import { CommentAuthorExpanderService } from '../../services/comment-author-expander.service.js';
|
import { CommentAuthorExpanderService } from '../../services/comment-author-expander.service.js';
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@ import { WatchIssueCommentsUseCase } from './watch-issue-comments.use-case.js';
|
|||||||
import { WatchIssueCommentsQuery } from './watch-issue-comments.query.js';
|
import { WatchIssueCommentsQuery } from './watch-issue-comments.query.js';
|
||||||
import { buildComment, buildIssue, mockIssueRepository } from '../test-helpers.js';
|
import { buildComment, buildIssue, mockIssueRepository } from '../test-helpers.js';
|
||||||
|
|
||||||
const dtoForIssue = (issueId: number) => CommentApplicationMapper.toDto(buildComment({ id: `comment-of-${issueId}`, issueId }), new Map());
|
const dtoForIssue = (issueId: number) =>
|
||||||
|
CommentApplicationMapper.toDto(buildComment({ id: `comment-of-${issueId}`, issueId }), new Map());
|
||||||
|
|
||||||
describe('WatchIssueCommentsUseCase', () => {
|
describe('WatchIssueCommentsUseCase', () => {
|
||||||
let issueRepository: IIssueRepository;
|
let issueRepository: IIssueRepository;
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,9 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { filter, type Observable } from 'rxjs';
|
import { filter, type Observable } from 'rxjs';
|
||||||
import { ISSUE_REPOSITORY, type IIssueRepository } from '../../../../issue/domain/repositories/issue.repository.interface.js';
|
import {
|
||||||
|
ISSUE_REPOSITORY,
|
||||||
|
type IIssueRepository,
|
||||||
|
} from '../../../../issue/domain/repositories/issue.repository.interface.js';
|
||||||
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
import { IssueNotFoundException } from '../../../../issue/domain/exceptions/issue-not-found.exception.js';
|
||||||
import { WatchIssueCommentsQuery } from './watch-issue-comments.query.js';
|
import { WatchIssueCommentsQuery } from './watch-issue-comments.query.js';
|
||||||
import { type CommentEvent, CommentEventsService } from '../../services/comment-events.service.js';
|
import { type CommentEvent, CommentEventsService } from '../../services/comment-events.service.js';
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ export interface ICommentRepository {
|
|||||||
* Комментарии задачи от старых к новым, включая удалённые.
|
* Комментарии задачи от старых к новым, включая удалённые.
|
||||||
* Курсорная пагинация: cursor — id последнего полученного комментария.
|
* Курсорная пагинация: cursor — id последнего полученного комментария.
|
||||||
*/
|
*/
|
||||||
findByIssue(params: { issueId: number; limit: number; cursor?: string }): Promise<{ list: Comment[]; nextCursor: string | null }>;
|
findByIssue(params: {
|
||||||
|
issueId: number;
|
||||||
|
limit: number;
|
||||||
|
cursor?: string;
|
||||||
|
}): Promise<{ list: Comment[]; nextCursor: string | null }>;
|
||||||
create(comment: Comment): Promise<Comment>;
|
create(comment: Comment): Promise<Comment>;
|
||||||
update(comment: Comment): Promise<void>;
|
update(comment: Comment): Promise<void>;
|
||||||
}
|
}
|
||||||
+5
-1
@@ -17,7 +17,11 @@ export class CommentPrismaRepository implements ICommentRepository {
|
|||||||
return record ? CommentPrismaMapper.toDomain(record) : null;
|
return record ? CommentPrismaMapper.toDomain(record) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByIssue(params: { issueId: number; limit: number; cursor?: string }): Promise<{ list: Comment[]; nextCursor: string | null }> {
|
async findByIssue(params: {
|
||||||
|
issueId: number;
|
||||||
|
limit: number;
|
||||||
|
cursor?: string;
|
||||||
|
}): Promise<{ list: Comment[]; nextCursor: string | null }> {
|
||||||
const limit = Math.max(1, params.limit);
|
const limit = Math.max(1, params.limit);
|
||||||
|
|
||||||
// Берём на одну запись больше, чтобы понять, есть ли следующая страница.
|
// Берём на одну запись больше, чтобы понять, есть ли следующая страница.
|
||||||
|
|||||||
@@ -47,15 +47,27 @@ export class CommentController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('issues/:issueId/comments')
|
@Get('issues/:issueId/comments')
|
||||||
list(@Param('issueId', ParseIntPipe) issueId: number, @Query() query: ListCommentsDto, @Session() session: UserSession) {
|
list(
|
||||||
|
@Param('issueId', ParseIntPipe) issueId: number,
|
||||||
|
@Query() query: ListCommentsDto,
|
||||||
|
@Session() session: UserSession,
|
||||||
|
) {
|
||||||
return this.handleErrors(() =>
|
return this.handleErrors(() =>
|
||||||
this.listCommentsUseCase.execute(new ListCommentsQuery(issueId, session.user.id, Number(query.limit), query.cursor)),
|
this.listCommentsUseCase.execute(
|
||||||
|
new ListCommentsQuery(issueId, session.user.id, Number(query.limit), query.cursor),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('issues/:issueId/comments')
|
@Post('issues/:issueId/comments')
|
||||||
create(@Param('issueId', ParseIntPipe) issueId: number, @Body() dto: CreateCommentRequestDto, @Session() session: UserSession) {
|
create(
|
||||||
return this.handleErrors(() => this.createCommentUseCase.execute(new CreateCommentCommand(issueId, session.user.id, dto.content)));
|
@Param('issueId', ParseIntPipe) issueId: number,
|
||||||
|
@Body() dto: CreateCommentRequestDto,
|
||||||
|
@Session() session: UserSession,
|
||||||
|
) {
|
||||||
|
return this.handleErrors(() =>
|
||||||
|
this.createCommentUseCase.execute(new CreateCommentCommand(issueId, session.user.id, dto.content)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,16 +78,23 @@ export class CommentController {
|
|||||||
*/
|
*/
|
||||||
@Sse('issues/:issueId/comments/events')
|
@Sse('issues/:issueId/comments/events')
|
||||||
@UseGuards(IssueVisibleGuard)
|
@UseGuards(IssueVisibleGuard)
|
||||||
events(@Param('issueId', ParseIntPipe) issueId: number, @Session() session: UserSession): Promise<Observable<MessageEvent>> {
|
events(
|
||||||
|
@Param('issueId', ParseIntPipe) issueId: number,
|
||||||
|
@Session() session: UserSession,
|
||||||
|
): Promise<Observable<MessageEvent>> {
|
||||||
return this.handleErrors(async () => {
|
return this.handleErrors(async () => {
|
||||||
const events$ = await this.watchIssueCommentsUseCase.execute(new WatchIssueCommentsQuery(issueId, session.user.id));
|
const events$ = await this.watchIssueCommentsUseCase.execute(
|
||||||
|
new WatchIssueCommentsQuery(issueId, session.user.id),
|
||||||
|
);
|
||||||
return events$.pipe(map((event) => ({ type: event.type, data: event.comment })));
|
return events$.pipe(map((event) => ({ type: event.type, data: event.comment })));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('comments/:id')
|
@Patch('comments/:id')
|
||||||
update(@Param('id') id: string, @Body() dto: UpdateCommentRequestDto, @Session() session: UserSession) {
|
update(@Param('id') id: string, @Body() dto: UpdateCommentRequestDto, @Session() session: UserSession) {
|
||||||
return this.handleErrors(() => this.updateCommentUseCase.execute(new UpdateCommentCommand(id, session.user.id, dto.content)));
|
return this.handleErrors(() =>
|
||||||
|
this.updateCommentUseCase.execute(new UpdateCommentCommand(id, session.user.id, dto.content)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('comments/:id')
|
@Delete('comments/:id')
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export class IssueVisibleGuard implements CanActivate {
|
|||||||
constructor(private readonly commentAccess: CommentAccessService) {}
|
constructor(private readonly commentAccess: CommentAccessService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest<{ params: Record<string, string | undefined>; session?: UserSession }>();
|
const request = context
|
||||||
|
.switchToHttp()
|
||||||
|
.getRequest<{ params: Record<string, string | undefined>; session?: UserSession }>();
|
||||||
|
|
||||||
const userId = request.session?.user.id;
|
const userId = request.session?.user.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { NOTIFY_EVENT, type NotifyEventPayload } from '../../domain/events/notify.event.js';
|
import { NOTIFY_EVENT, type NotifyEventPayload } from '../../domain/events/notify.event.js';
|
||||||
import {
|
import { NOTIFICATION_CHANNEL_SENDERS, type NotificationChannelSender } from '../../domain/channel-sender.interface.js';
|
||||||
NOTIFICATION_CHANNEL_SENDERS,
|
|
||||||
type NotificationChannelSender,
|
|
||||||
} from '../../domain/channel-sender.interface.js';
|
|
||||||
import {
|
import {
|
||||||
NOTIFICATION_PREFERENCE_REPOSITORY,
|
NOTIFICATION_PREFERENCE_REPOSITORY,
|
||||||
type INotificationPreferenceRepository,
|
type INotificationPreferenceRepository,
|
||||||
@@ -39,7 +36,9 @@ export class NotificationDispatcherService {
|
|||||||
/** Каналы из payload, отфильтрованные по подпискам; без явных подписок — все каналы включены по умолчанию */
|
/** Каналы из payload, отфильтрованные по подпискам; без явных подписок — все каналы включены по умолчанию */
|
||||||
private async resolveChannels(payload: NotifyEventPayload): Promise<NotificationChannelValue[]> {
|
private async resolveChannels(payload: NotifyEventPayload): Promise<NotificationChannelValue[]> {
|
||||||
const userPreferences = await this.preferences.findByUserId(payload.userId);
|
const userPreferences = await this.preferences.findByUserId(payload.userId);
|
||||||
const disabledChannels = new Set(userPreferences.filter((preference) => !preference.enabled).map((preference) => preference.channel));
|
const disabledChannels = new Set(
|
||||||
|
userPreferences.filter((preference) => !preference.enabled).map((preference) => preference.channel),
|
||||||
|
);
|
||||||
|
|
||||||
const candidateChannels = payload.channels ?? this.senders.map((sender) => sender.channel);
|
const candidateChannels = payload.channels ?? this.senders.map((sender) => sender.channel);
|
||||||
return candidateChannels.filter((channel) => !disabledChannels.has(channel));
|
return candidateChannels.filter((channel) => !disabledChannels.has(channel));
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
NOTIFICATION_PREFERENCE_REPOSITORY,
|
NOTIFICATION_PREFERENCE_REPOSITORY,
|
||||||
type INotificationPreferenceRepository,
|
type INotificationPreferenceRepository,
|
||||||
} from '../../domain/repositories/notification-preference.repository.interface.js';
|
} from '../../domain/repositories/notification-preference.repository.interface.js';
|
||||||
import { NOTIFICATION_CHANNELS, type NotificationChannelValue } from '../../domain/value-objects/notification-channel.vo.js';
|
import {
|
||||||
|
NOTIFICATION_CHANNELS,
|
||||||
|
type NotificationChannelValue,
|
||||||
|
} from '../../domain/value-objects/notification-channel.vo.js';
|
||||||
|
|
||||||
export interface ResolvedNotificationPreference {
|
export interface ResolvedNotificationPreference {
|
||||||
channel: NotificationChannelValue;
|
channel: NotificationChannelValue;
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import { BadRequestException, Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Post } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
Put,
|
||||||
|
Post,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { Session, type UserSession } from '@thallesp/nestjs-better-auth';
|
import { Session, type UserSession } from '@thallesp/nestjs-better-auth';
|
||||||
import { NotificationsService } from '../../application/services/notifications.service.js';
|
import { NotificationsService } from '../../application/services/notifications.service.js';
|
||||||
import { RegisterDeviceTokenDto } from '../dto/register-device-token.dto.js';
|
import { RegisterDeviceTokenDto } from '../dto/register-device-token.dto.js';
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ export const PROJECT_REPOSITORY = Symbol('PROJECT_REPOSITORY');
|
|||||||
export interface IProjectRepository {
|
export interface IProjectRepository {
|
||||||
/** Проект виден владельцу и участникам проекта (ProjectMember) */
|
/** Проект виден владельцу и участникам проекта (ProjectMember) */
|
||||||
findById(id: string, userId: string): Promise<Project | null>;
|
findById(id: string, userId: string): Promise<Project | null>;
|
||||||
findAll(params: {
|
findAll(params: { limit: number; page: number; userId: string }): Promise<{ list: Project[]; meta: PaginationMeta }>;
|
||||||
limit: number;
|
|
||||||
page: number;
|
|
||||||
userId: string;
|
|
||||||
}): Promise<{ list: Project[]; meta: PaginationMeta }>;
|
|
||||||
create(project: Project): Promise<Project>;
|
create(project: Project): Promise<Project>;
|
||||||
update(project: Project): Promise<void>;
|
update(project: Project): Promise<void>;
|
||||||
delete(id: string): Promise<void>;
|
delete(id: string): Promise<void>;
|
||||||
|
|||||||
Reference in new issue
Block a user