prettier
Gitea Actions Demo / Deploy (push) Successful in 1m34s

This commit is contained in:
vadim committed 2026-10-01 12:04:21 +03:00
1 parent 4b6ecfbf5d
commit 5cfc8986d7
22 files changed
+145 -44

No files matched your search

+1 -1
View File
@@ -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';
@@ -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';
@@ -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';
@@ -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',
@@ -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';
@@ -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;
@@ -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>;
} }
@@ -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>;