From 3a7be9f11ce09dd2780215d7428d1d05d58a8165 Mon Sep 17 00:00:00 2001 From: soorq Date: Wed, 29 Apr 2026 23:21:12 +0300 Subject: [PATCH 1/2] refactor(user): migrate to DDD hexagonal architecture and update cross-module imports --- src/app.module.ts | 2 +- .../confirm-reset-password.use-case.ts | 6 +- .../use-cases/refresh-tokens.use-case.ts | 6 +- .../use-cases/reset-password.use-case.ts | 6 +- .../application/use-cases/sign-in.use-case.ts | 6 +- .../use-cases/sign-up-verify.use-case.ts | 6 +- .../application/use-cases/sign-up.use-case.ts | 6 +- src/auth/auth.module.ts | 2 +- .../infrastructure/security/token.service.ts | 2 +- .../teams/repository/teams.repository.ts | 2 +- src/modules/user/commands/find-one.command.ts | 34 ----- src/modules/user/commands/index.ts | 3 - src/modules/user/controller/index.ts | 2 - .../user/controller/settings.controller.ts | 18 --- src/modules/user/index.ts | 4 - src/modules/user/repository/index.ts | 1 - src/modules/user/services/index.ts | 2 - src/modules/user/services/user.service.ts | 123 ------------------ src/modules/user/user.module.ts | 22 ---- src/shared/entities/index.ts | 2 +- src/user/application/controller/index.ts | 2 + .../controller/settings/controller.ts | 16 +++ .../controller/settings/swagger.ts | 23 ++++ .../controller/user/controller.ts} | 25 ++-- .../application/controller/user/swagger.ts} | 20 +-- .../user => user/application}/dtos/index.ts | 0 .../application}/dtos/user.dto.ts | 0 .../use-cases/find-profile.query.ts | 25 ++++ .../application/use-cases/find-user.query.ts | 24 ++++ .../use-cases/get-activity.query.ts | 30 +++++ src/user/application/use-cases/index.ts | 8 ++ .../use-cases/register-user.use-case.ts} | 29 ++--- .../update-notifications.use-case.ts} | 37 +++--- .../use-cases/update-password.use-case.ts} | 23 ++-- .../use-cases/update-profile.use-case.ts | 32 +++++ .../use-cases/upload-avatar.use-case.ts | 29 +++++ src/user/application/user.facade.ts | 41 ++++++ src/user/domain/entities/index.ts | 1 + .../domain}/entities/user.domain.ts | 7 +- src/user/domain/repository/index.ts | 1 + .../repository/user.repository.interface.ts | 0 src/user/index.ts | 3 + .../persistence/models}/index.ts | 0 .../persistence/models}/user.entity.ts | 0 .../persistence/repositories/index.ts | 1 + .../repositories}/user.repository.ts | 6 +- src/user/user.module.ts | 38 ++++++ 47 files changed, 354 insertions(+), 322 deletions(-) delete mode 100644 src/modules/user/commands/find-one.command.ts delete mode 100644 src/modules/user/commands/index.ts delete mode 100644 src/modules/user/controller/index.ts delete mode 100644 src/modules/user/controller/settings.controller.ts delete mode 100644 src/modules/user/index.ts delete mode 100644 src/modules/user/repository/index.ts delete mode 100644 src/modules/user/services/index.ts delete mode 100644 src/modules/user/services/user.service.ts delete mode 100644 src/modules/user/user.module.ts create mode 100644 src/user/application/controller/index.ts create mode 100644 src/user/application/controller/settings/controller.ts create mode 100644 src/user/application/controller/settings/swagger.ts rename src/{modules/user/controller/user.controller.ts => user/application/controller/user/controller.ts} (57%) rename src/{modules/user/controller/user.swagger.ts => user/application/controller/user/swagger.ts} (84%) rename src/{modules/user => user/application}/dtos/index.ts (100%) rename src/{modules/user => user/application}/dtos/user.dto.ts (100%) create mode 100644 src/user/application/use-cases/find-profile.query.ts create mode 100644 src/user/application/use-cases/find-user.query.ts create mode 100644 src/user/application/use-cases/get-activity.query.ts create mode 100644 src/user/application/use-cases/index.ts rename src/{modules/user/commands/create.command.ts => user/application/use-cases/register-user.use-case.ts} (65%) rename src/{modules/user/services/settings.service.ts => user/application/use-cases/update-notifications.use-case.ts} (67%) rename src/{modules/user/commands/update-pass.command.ts => user/application/use-cases/update-password.use-case.ts} (57%) create mode 100644 src/user/application/use-cases/update-profile.use-case.ts create mode 100644 src/user/application/use-cases/upload-avatar.use-case.ts create mode 100644 src/user/application/user.facade.ts create mode 100644 src/user/domain/entities/index.ts rename src/{modules/user => user/domain}/entities/user.domain.ts (86%) create mode 100644 src/user/domain/repository/index.ts rename src/{modules/user => user/domain}/repository/user.repository.interface.ts (100%) create mode 100644 src/user/index.ts rename src/{modules/user/entities => user/infrastructure/persistence/models}/index.ts (100%) rename src/{modules/user/entities => user/infrastructure/persistence/models}/user.entity.ts (100%) create mode 100644 src/user/infrastructure/persistence/repositories/index.ts rename src/{modules/user/repository => user/infrastructure/persistence/repositories}/user.repository.ts (97%) create mode 100644 src/user/user.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 696bb75..404fdf8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,7 +7,7 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { ZodValidationPipe } from 'nestjs-zod'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { HealthModule } from '@libs/health'; -import { UserModule } from './modules/user'; +import { UserModule } from './user'; import { GlobalExceptionFilter } from '@shared/error'; import { AuthModule } from './auth/auth.module'; import { BullBoardModule } from '@bull-board/nestjs'; diff --git a/src/auth/application/use-cases/confirm-reset-password.use-case.ts b/src/auth/application/use-cases/confirm-reset-password.use-case.ts index b6be75a..cdbf3b6 100644 --- a/src/auth/application/use-cases/confirm-reset-password.use-case.ts +++ b/src/auth/application/use-cases/confirm-reset-password.use-case.ts @@ -2,16 +2,16 @@ import { InjectRedis } from '@nestjs-modules/ioredis'; import { HttpStatus, Injectable } from '@nestjs/common'; import * as argon from 'argon2'; import Redis from 'ioredis'; -import { UpdatePassUserCommand } from '@core/modules/user'; import { BaseException } from '@shared/error'; import { PasswordResetConfirmDto } from '../dtos'; +import { UpdatePasswordUseCase } from '@core/user'; @Injectable() export class ConfirmResetPasswordUseCase { constructor( @InjectRedis() private readonly redis: Redis, - private readonly updateUserPass: UpdatePassUserCommand, + private readonly updatePasswordUserUseCase: UpdatePasswordUseCase, ) {} async execute(dto: PasswordResetConfirmDto) { @@ -43,7 +43,7 @@ export class ConfirmResetPasswordUseCase { } const hashed = await argon.hash(dto.password); - const isUpdated = await this.updateUserPass.execute(dto.email, hashed); + const isUpdated = await this.updatePasswordUserUseCase.execute(dto.email, hashed); if (!isUpdated) { throw new BaseException( diff --git a/src/auth/application/use-cases/refresh-tokens.use-case.ts b/src/auth/application/use-cases/refresh-tokens.use-case.ts index 32dc367..390c752 100644 --- a/src/auth/application/use-cases/refresh-tokens.use-case.ts +++ b/src/auth/application/use-cases/refresh-tokens.use-case.ts @@ -1,9 +1,9 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { FindOneUserCommand } from '@core/modules/user'; import { BaseException } from '@shared/error'; import { ISessionRepository } from '../../domain/repository'; import { TokenService } from '../../infrastructure/security'; import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta'; +import { FindUserQuery } from '@core/user'; @Injectable() export class RefreshTokensUseCase { @@ -11,7 +11,7 @@ export class RefreshTokensUseCase { @Inject('ISessionRepository') private readonly sessionRepo: ISessionRepository, private readonly tokenService: TokenService, - private readonly findUserCommand: FindOneUserCommand, + private readonly findUserQuery: FindUserQuery, ) {} async execute(token: string, metadata: DeviceMetadata) { @@ -39,7 +39,7 @@ export class RefreshTokensUseCase { ); } - const { user } = await this.findUserCommand.execute({ id: session.userId }); + const { user } = await this.findUserQuery.execute({ id: session.userId }); if (!user) { await this.sessionRepo.revoke(session.id); diff --git a/src/auth/application/use-cases/reset-password.use-case.ts b/src/auth/application/use-cases/reset-password.use-case.ts index 82046f8..f930b56 100644 --- a/src/auth/application/use-cases/reset-password.use-case.ts +++ b/src/auth/application/use-cases/reset-password.use-case.ts @@ -4,11 +4,11 @@ import { HttpStatus, Injectable } from '@nestjs/common'; import { Queue } from 'bullmq'; import Redis from 'ioredis'; import { generate, generateSecret } from 'otplib'; -import { FindOneUserCommand } from '@core/modules/user'; import { BaseException } from '@shared/error'; import { AuthMailJobs, AuthQueues } from '../../domain/enums'; import { ResetPasswordEvent } from '../../domain/events'; import { ResetPasswordDto } from '../dtos'; +import { FindUserQuery } from '@core/user'; @Injectable() export class ResetPasswordUseCase { @@ -17,11 +17,11 @@ export class ResetPasswordUseCase { private readonly redis: Redis, @InjectQueue(AuthQueues.AUTH_MAIL) private readonly mailQueue: Queue, - private readonly findUserCommand: FindOneUserCommand, + private readonly findUserQuery: FindUserQuery, ) {} async execute(dto: ResetPasswordDto) { - const entity = await this.findUserCommand.execute({ email: dto.email }); + const entity = await this.findUserQuery.execute({ email: dto.email }); if (!entity.user) { throw new BaseException( diff --git a/src/auth/application/use-cases/sign-in.use-case.ts b/src/auth/application/use-cases/sign-in.use-case.ts index 30ff23d..05c4d04 100644 --- a/src/auth/application/use-cases/sign-in.use-case.ts +++ b/src/auth/application/use-cases/sign-in.use-case.ts @@ -1,11 +1,11 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import * as argon from 'argon2'; -import { FindOneUserCommand } from '@core/modules/user'; import { BaseException } from '@shared/error'; import { ISessionRepository } from '../../domain/repository'; import { TokenService } from '../../infrastructure/security'; import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta'; import { SignInDto } from '../dtos'; +import { FindUserQuery } from '@core/user'; @Injectable() export class SignInUseCase { @@ -13,11 +13,11 @@ export class SignInUseCase { @Inject('ISessionRepository') private readonly sessionRepo: ISessionRepository, private readonly tokenService: TokenService, - private readonly findUserCommand: FindOneUserCommand, + private readonly findUserQuery: FindUserQuery, ) {} async execute(dto: SignInDto, meta: DeviceMetadata) { - const entities = await this.findUserCommand.execute({ email: dto.email }); + const entities = await this.findUserQuery.execute({ email: dto.email }); if (!entities?.user || !entities?.security) { throw new BaseException( diff --git a/src/auth/application/use-cases/sign-up-verify.use-case.ts b/src/auth/application/use-cases/sign-up-verify.use-case.ts index 9afb00c..6046c73 100644 --- a/src/auth/application/use-cases/sign-up-verify.use-case.ts +++ b/src/auth/application/use-cases/sign-up-verify.use-case.ts @@ -2,7 +2,7 @@ import { InjectRedis } from '@nestjs-modules/ioredis'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import Redis from 'ioredis'; import { verify as verifyOTP } from 'otplib'; -import { CreateUserCommand } from '@core/modules/user'; +import { RegisterUserUseCase } from '@core/user'; import { BaseException } from '@shared/error'; import { ISessionRepository } from '../../domain/repository'; import { TokenService } from '../../infrastructure/security'; @@ -17,7 +17,7 @@ export class SignUpVerifyUseCase { @Inject('ISessionRepository') private readonly sessionRepo: ISessionRepository, private readonly tokenService: TokenService, - private readonly createUserCommand: CreateUserCommand, + private readonly registerUserUseCase: RegisterUserUseCase, ) {} async execute(dto: VerifyDto, meta: DeviceMetadata) { @@ -58,7 +58,7 @@ export class SignUpVerifyUseCase { ); } - const user = await this.createUserCommand.execute({ + const user = await this.registerUserUseCase.execute({ ...userData.user, password: userData.password, }); diff --git a/src/auth/application/use-cases/sign-up.use-case.ts b/src/auth/application/use-cases/sign-up.use-case.ts index 1217fd6..5550c06 100644 --- a/src/auth/application/use-cases/sign-up.use-case.ts +++ b/src/auth/application/use-cases/sign-up.use-case.ts @@ -5,7 +5,7 @@ import * as argon from 'argon2'; import { Queue } from 'bullmq'; import Redis from 'ioredis'; import { generate, generateSecret } from 'otplib'; -import { FindOneUserCommand } from '@core/modules/user'; +import { FindUserQuery } from '@core/user'; import { BaseException } from '@shared/error'; import { AuthQueues, AuthMailJobs } from '../../domain/enums'; import { RegisterCodeEvent } from '../../domain/events'; @@ -18,7 +18,7 @@ export class SignUpUseCase { private readonly redis: Redis, @InjectQueue(AuthQueues.AUTH_MAIL) private readonly mailQueue: Queue, - private readonly findUserCommand: FindOneUserCommand, + private readonly findUserQuery: FindUserQuery, ) {} async execute(dto: SignUpDto) { @@ -37,7 +37,7 @@ export class SignUpUseCase { ); } - const isExists = await this.findUserCommand.execute({ email: dto.email }); + const isExists = await this.findUserQuery.execute({ email: dto.email }); if (isExists) { throw new BaseException( diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 98ac4ac..e79f42a 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -5,7 +5,7 @@ import { BullModule } from '@nestjs/bullmq'; import { Module, forwardRef } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; -import { UserModule } from '@core/modules/user'; +import { UserModule } from '@core/user'; import { AuthController, AuthRecoveryController } from './application/controller'; import { AuthFacade } from './application/auth.facade'; import { diff --git a/src/auth/infrastructure/security/token.service.ts b/src/auth/infrastructure/security/token.service.ts index 72930b1..a3f2480 100644 --- a/src/auth/infrastructure/security/token.service.ts +++ b/src/auth/infrastructure/security/token.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import type { JwtPayload } from '@shared/types'; -import type { User } from '@core/modules/user'; +import type { User } from '@core/user'; @Injectable() export class TokenService { diff --git a/src/modules/teams/repository/teams.repository.ts b/src/modules/teams/repository/teams.repository.ts index b880554..59f078b 100644 --- a/src/modules/teams/repository/teams.repository.ts +++ b/src/modules/teams/repository/teams.repository.ts @@ -2,7 +2,7 @@ import { Inject, Logger } from '@nestjs/common'; import { ITeamsRepository } from './teams.repository.interface'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import * as schema from '../entities'; -import * as scUsers from '@core/modules/user/entities'; +import * as scUsers from '@core/user/infrastructure/persistence/models'; import { and, asc, count, desc, eq, ilike, inArray, isNull, sql } from 'drizzle-orm'; export class TeamsRepository implements ITeamsRepository { diff --git a/src/modules/user/commands/find-one.command.ts b/src/modules/user/commands/find-one.command.ts deleted file mode 100644 index 8a78e1f..0000000 --- a/src/modules/user/commands/find-one.command.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { IUserRepository } from '../repository/user.repository.interface'; -import type { UserWithSecurity } from '../entities/user.domain'; -import { BaseException } from '@shared/error'; - -@Injectable() -export class FindOneUserCommand { - constructor( - @Inject('IUserRepository') - private readonly repository: IUserRepository, - ) {} - - async execute(params: { email: string }): Promise; - async execute(params: { id: string }): Promise; - async execute(params: { email?: string; id?: string }): Promise { - const { email, id } = params; - - if (email) { - return this.repository.findByEmail(email); - } - - if (id) { - return this.repository.findById(id); - } - - throw new BaseException( - { - code: 'COMMAND_PARAMS_MISSING', - message: 'Критическая ошибка: не указаны параметры поиска пользователя', - }, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } -} diff --git a/src/modules/user/commands/index.ts b/src/modules/user/commands/index.ts deleted file mode 100644 index 7a59139..0000000 --- a/src/modules/user/commands/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { CreateUserCommand } from './create.command'; -export { FindOneUserCommand } from './find-one.command'; -export { UpdatePassUserCommand } from './update-pass.command'; diff --git a/src/modules/user/controller/index.ts b/src/modules/user/controller/index.ts deleted file mode 100644 index beaad40..0000000 --- a/src/modules/user/controller/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { UserController } from './user.controller'; -export { UserSettingsController } from './settings.controller'; diff --git a/src/modules/user/controller/settings.controller.ts b/src/modules/user/controller/settings.controller.ts deleted file mode 100644 index e5aa8f4..0000000 --- a/src/modules/user/controller/settings.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Body, Patch, UseGuards } from '@nestjs/common'; -import { UserSettingsService } from '../services'; -import { PatchMeNotificationsSwagger } from './user.swagger'; -import type { UpdateNotificationsDto } from '../dtos'; -import { ApiBaseController, GetUserId } from '../../../shared/decorators'; -import { BearerAuthGuard } from '@shared/guards'; - -@ApiBaseController('users/me', 'Account Settings') -@UseGuards(BearerAuthGuard) -export class UserSettingsController { - constructor(private readonly facade: UserSettingsService) {} - - @Patch('notifications') - @PatchMeNotificationsSwagger() - async updateNotifications(@Body() settings: UpdateNotificationsDto, @GetUserId() id: string) { - return this.facade.updateNotifications(id, settings); - } -} diff --git a/src/modules/user/index.ts b/src/modules/user/index.ts deleted file mode 100644 index 9871038..0000000 --- a/src/modules/user/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { UserModule } from './user.module'; -export { UserRepository } from './repository/user.repository'; -export { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from './commands'; -export { User } from './entities/user.domain'; diff --git a/src/modules/user/repository/index.ts b/src/modules/user/repository/index.ts deleted file mode 100644 index 3e89261..0000000 --- a/src/modules/user/repository/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {} from './user.repository'; diff --git a/src/modules/user/services/index.ts b/src/modules/user/services/index.ts deleted file mode 100644 index b547819..0000000 --- a/src/modules/user/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { UserSettingsService } from './settings.service'; -export { UserService } from './user.service'; diff --git a/src/modules/user/services/user.service.ts b/src/modules/user/services/user.service.ts deleted file mode 100644 index 2d95e6d..0000000 --- a/src/modules/user/services/user.service.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { IUserRepository } from '../repository/user.repository.interface'; -import type { UpdateProfileDto } from '../dtos'; -import { createId } from '@paralleldrive/cuid2'; -import { IUserMedia, USER_MEDIA_TOKEN, type FileUploadDto } from '../../media'; -import { BaseException } from '@shared/error'; - -@Injectable() -export class UserService { - constructor( - @Inject('IUserRepository') - private readonly userRepo: IUserRepository, - @Inject(USER_MEDIA_TOKEN) - private readonly mediaService: IUserMedia, - ) {} - - private throwUserNotFound() { - throw new BaseException( - { - code: 'USER_NOT_FOUND', - message: 'Пользователь не найден в системе', - }, - HttpStatus.NOT_FOUND, - ); - } - - public getProfile = async (userId: string) => { - const { user, notifications, security } = await this.userRepo.findProfile(userId); - if (!user) this.throwUserNotFound(); - const { id, email, ...profile } = user; - - return { - id, - email, - profile, - security, - notifications, - }; - }; - - public updateProfile = async (id: string, dto: UpdateProfileDto) => { - try { - const isUpdated = await this.userRepo.updateProfile(id, dto); - - if (!isUpdated) { - throw new BaseException( - { - code: 'PROFILE_UPDATE_FAILED', - message: 'Не удалось обновить данные профиля', - }, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - - await this.userRepo.logActivity({ - id: createId(), - userId: id, - eventType: 'PROFILE_UPDATED', - }); - - return { - success: true, - message: 'Профиль успешно обновлен', - }; - } catch (error) { - if (error instanceof BaseException) { - throw error; - } - - throw new BaseException( - { - code: 'PROFILE_SERVICE_ERROR', - message: 'Произошла ошибка при обновлении профиля', - details: [ - { - reason: - error instanceof Error ? error.message : 'Unknown database error', - }, - ], - }, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - }; - - public getActivity = async (id: string, page: number, limit: number) => { - const safeLimit = Math.min(limit, 50); - const offset = (page - 1) * safeLimit; - - const { items, total } = await this.userRepo.findActivityByUser(id, { - limit: safeLimit, - offset, - }); - - return { - items, - meta: { - total, - page, - limit: safeLimit, - totalPages: Math.ceil(total / safeLimit), - }, - }; - }; - - public uploadAvatar = async (userId: string, fileDto: FileUploadDto) => { - const { url } = await this.mediaService.uploadUserAvatar(userId, fileDto, (url) => - this.userRepo.updateAvatar(userId, url), - ); - - await this.userRepo.logActivity({ - id: createId(), - userId, - eventType: 'AVATAR_CHANGED', - metadata: { url }, - }); - - return { - success: true, - url, - }; - }; -} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts deleted file mode 100644 index cfaef81..0000000 --- a/src/modules/user/user.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserController, UserSettingsController } from './controller'; -import { UserService } from './services/user.service'; -import { UserRepository } from './repository/user.repository'; -import { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from './commands'; -import { MediaModule } from '../media'; -import { UserSettingsService } from './services'; - -const REPOSITORY = { - provide: 'IUserRepository', - useClass: UserRepository, -}; - -const COMMANDS = [CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand]; - -@Module({ - imports: [MediaModule], - controllers: [UserController, UserSettingsController], - providers: [...COMMANDS, REPOSITORY, UserService, UserSettingsService], - exports: [...COMMANDS], -}) -export class UserModule {} diff --git a/src/shared/entities/index.ts b/src/shared/entities/index.ts index b618226..61f7880 100644 --- a/src/shared/entities/index.ts +++ b/src/shared/entities/index.ts @@ -1,5 +1,5 @@ export { baseSchema } from './schema'; -export * from '../../modules/user/entities'; +export * from '../../user/infrastructure/persistence/models'; export * from '../../auth/infrastructure/persistence/models'; export * from '../../modules/teams/entities'; export * from '../../modules/projects/entities'; diff --git a/src/user/application/controller/index.ts b/src/user/application/controller/index.ts new file mode 100644 index 0000000..0ea2e27 --- /dev/null +++ b/src/user/application/controller/index.ts @@ -0,0 +1,2 @@ +export { UserController } from './user/controller'; +export { UserSettingsController } from './settings/controller'; diff --git a/src/user/application/controller/settings/controller.ts b/src/user/application/controller/settings/controller.ts new file mode 100644 index 0000000..66a174c --- /dev/null +++ b/src/user/application/controller/settings/controller.ts @@ -0,0 +1,16 @@ +import { Body, Patch } from '@nestjs/common'; +import { UserFacade } from '../../user.facade'; +import { PatchMeNotificationsSwagger } from './swagger'; +import { ApiBaseController, GetUserId } from '@shared/decorators'; +import { UpdateNotificationsDto } from '../../dtos'; + +@ApiBaseController('users/me', 'Account Settings', true) +export class UserSettingsController { + constructor(private readonly facade: UserFacade) {} + + @Patch('notifications') + @PatchMeNotificationsSwagger() + async updateNotifications(@Body() settings: UpdateNotificationsDto, @GetUserId() id: string) { + return this.facade.updateNotifications(id, settings); + } +} diff --git a/src/user/application/controller/settings/swagger.ts b/src/user/application/controller/settings/swagger.ts new file mode 100644 index 0000000..956284f --- /dev/null +++ b/src/user/application/controller/settings/swagger.ts @@ -0,0 +1,23 @@ +import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; +import { ApiUnauthorized, ApiValidationError } from '@shared/error'; +import { ActionResponse } from '@shared/dtos'; +import { UpdateNotificationsDto } from '../../dtos'; + +export const PatchMeNotificationsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить настройки уведомлений', + description: 'Частичное обновление настроек email и push уведомлений.', + }), + ApiBody({ + type: UpdateNotificationsDto.Output, + }), + ApiResponse({ + status: 200, + description: 'Настройки успешно сохранены.', + type: ActionResponse.Output, + }), + ApiValidationError('Некорректный формат настроек'), + ApiUnauthorized(), + ); diff --git a/src/modules/user/controller/user.controller.ts b/src/user/application/controller/user/controller.ts similarity index 57% rename from src/modules/user/controller/user.controller.ts rename to src/user/application/controller/user/controller.ts index 29dfde7..ec6fd3d 100644 --- a/src/modules/user/controller/user.controller.ts +++ b/src/user/application/controller/user/controller.ts @@ -1,21 +1,14 @@ -import { Body, Get, Patch, Post, Query, UseGuards } from '@nestjs/common'; -import { UserService } from '../services'; -import { - GetMeActivitySwagger, - GetMeSwagger, - PatchMeSwagger, - PostMeAvatarSwagger, -} from './user.swagger'; -import type { UpdateProfileDto } from '../dtos'; -import { ApiBaseController, ExtractFastifyFile, GetUserId } from '../../../shared/decorators'; -import { BearerAuthGuard } from '@shared/guards'; -import type { PaginationDto } from '../../../shared/dtos'; -import type { FileUploadDto } from '../../media'; +import { Body, Get, Patch, Post, Query } from '@nestjs/common'; +import { GetMeActivitySwagger, GetMeSwagger, PatchMeSwagger, PostMeAvatarSwagger } from './swagger'; +import { UpdateProfileDto } from '../../dtos'; +import { ApiBaseController, ExtractFastifyFile, GetUserId } from '@shared/decorators'; +import { UserFacade } from '../../user.facade'; +import { PaginationDto } from '@shared/dtos'; +import { FileUploadDto } from '@core/modules/media'; -@ApiBaseController('users/me', 'Account Profile') -@UseGuards(BearerAuthGuard) +@ApiBaseController('users/me', 'Account Profile', true) export class UserController { - constructor(private readonly facade: UserService) {} + constructor(private readonly facade: UserFacade) {} @Get() @GetMeSwagger() diff --git a/src/modules/user/controller/user.swagger.ts b/src/user/application/controller/user/swagger.ts similarity index 84% rename from src/modules/user/controller/user.swagger.ts rename to src/user/application/controller/user/swagger.ts index 2418daf..568fb14 100644 --- a/src/modules/user/controller/user.swagger.ts +++ b/src/user/application/controller/user/swagger.ts @@ -6,7 +6,7 @@ import { ApiQuery, ApiResponse, } from '@nestjs/swagger'; -import { UpdateNotificationsDto, UpdateProfileDto, UserResponse } from '../dtos'; +import { UpdateProfileDto, UserResponse } from '../../dtos'; import { applyDecorators } from '@nestjs/common'; import { ApiBadRequest, ApiUnauthorized, ApiValidationError } from '@shared/error'; import { ActionResponse } from '@shared/dtos'; @@ -49,24 +49,6 @@ export const PatchMeSwagger = () => ApiUnauthorized(), ); -export const PatchMeNotificationsSwagger = () => - applyDecorators( - ApiOperation({ - summary: 'Обновить настройки уведомлений', - description: 'Частичное обновление настроек email и push уведомлений.', - }), - ApiBody({ - type: UpdateNotificationsDto.Output, - }), - ApiResponse({ - status: 200, - description: 'Настройки успешно сохранены.', - type: ActionResponse.Output, - }), - ApiValidationError('Некорректный формат настроек'), - ApiUnauthorized(), - ); - export const GetMeActivitySwagger = () => applyDecorators( ApiOperation({ diff --git a/src/modules/user/dtos/index.ts b/src/user/application/dtos/index.ts similarity index 100% rename from src/modules/user/dtos/index.ts rename to src/user/application/dtos/index.ts diff --git a/src/modules/user/dtos/user.dto.ts b/src/user/application/dtos/user.dto.ts similarity index 100% rename from src/modules/user/dtos/user.dto.ts rename to src/user/application/dtos/user.dto.ts diff --git a/src/user/application/use-cases/find-profile.query.ts b/src/user/application/use-cases/find-profile.query.ts new file mode 100644 index 0000000..df2972c --- /dev/null +++ b/src/user/application/use-cases/find-profile.query.ts @@ -0,0 +1,25 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class FindProfileQuery { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + async execute(userId: string) { + const { user, notifications, security } = await this.userRepo.findProfile(userId); + + if (!user) { + throw new BaseException( + { code: 'USER_NOT_FOUND', message: 'Пользователь не найден' }, + HttpStatus.NOT_FOUND, + ); + } + + const { id, email, ...profile } = user; + return { id, email, profile, security, notifications }; + } +} diff --git a/src/user/application/use-cases/find-user.query.ts b/src/user/application/use-cases/find-user.query.ts new file mode 100644 index 0000000..a83ec65 --- /dev/null +++ b/src/user/application/use-cases/find-user.query.ts @@ -0,0 +1,24 @@ +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { IUserRepository } from '@core/user/domain/repository'; +import { BaseException } from '@shared/error'; + +@Injectable() +export class FindUserQuery { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + async execute(params: { email?: string; id?: string }) { + if (params.email) return this.repository.findByEmail(params.email); + if (params.id) return this.repository.findById(params.id); + + throw new BaseException( + { + code: 'QUERY_PARAMS_MISSING', + message: 'Не указаны параметры поиска', + }, + HttpStatus.BAD_REQUEST, + ); + } +} diff --git a/src/user/application/use-cases/get-activity.query.ts b/src/user/application/use-cases/get-activity.query.ts new file mode 100644 index 0000000..5921bd6 --- /dev/null +++ b/src/user/application/use-cases/get-activity.query.ts @@ -0,0 +1,30 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { Inject, Injectable } from '@nestjs/common'; + +@Injectable() +export class GetActivityQuery { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + async execute(id: string, page: number, limit: number) { + const safeLimit = Math.min(limit, 50); + const offset = (page - 1) * safeLimit; + + const { items, total } = await this.userRepo.findActivityByUser(id, { + limit: safeLimit, + offset, + }); + + return { + items, + meta: { + total, + page, + limit: safeLimit, + totalPages: Math.ceil(total / safeLimit), + }, + }; + } +} diff --git a/src/user/application/use-cases/index.ts b/src/user/application/use-cases/index.ts new file mode 100644 index 0000000..fa08830 --- /dev/null +++ b/src/user/application/use-cases/index.ts @@ -0,0 +1,8 @@ +export { FindProfileQuery } from './find-profile.query'; +export { FindUserQuery } from './find-user.query'; +export { GetActivityQuery } from './get-activity.query'; +export { RegisterUserUseCase } from './register-user.use-case'; +export { UpdateNotificationsUseCase } from './update-notifications.use-case'; +export { UpdatePasswordUseCase } from './update-password.use-case'; +export { UpdateProfileUseCase } from './update-profile.use-case'; +export { UploadAvatarUseCase } from './upload-avatar.use-case'; diff --git a/src/modules/user/commands/create.command.ts b/src/user/application/use-cases/register-user.use-case.ts similarity index 65% rename from src/modules/user/commands/create.command.ts rename to src/user/application/use-cases/register-user.use-case.ts index 97861b4..7e150f8 100644 --- a/src/modules/user/commands/create.command.ts +++ b/src/user/application/use-cases/register-user.use-case.ts @@ -1,11 +1,11 @@ +import type { NewUser } from '@core/user/domain/entities'; +import { IUserRepository } from '@core/user/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { IUserRepository } from '../repository/user.repository.interface'; -import { NewUser } from '../entities/user.domain'; import { createId } from '@paralleldrive/cuid2'; import { BaseException } from '@shared/error'; @Injectable() -export class CreateUserCommand { +export class RegisterUserUseCase { constructor( @Inject('IUserRepository') private readonly repository: IUserRepository, @@ -14,7 +14,7 @@ export class CreateUserCommand { async execute(dto: NewUser & { password: string }) { const existingUser = await this.repository.findByEmail(dto.email); - if (existingUser) { + if (existingUser?.user) { throw new BaseException( { code: 'USER_ALREADY_EXISTS', @@ -28,23 +28,22 @@ export class CreateUserCommand { try { const user = await this.repository.create(dto); - await this.repository.logActivity({ - eventType: 'registered', - userId: user.id, - id: createId(), - }); - - await this.repository.updatePasswordHash(user.id, dto.password); + await Promise.all([ + this.repository.logActivity({ + eventType: 'registered', + userId: user.id, + id: createId(), + }), + this.repository.updatePasswordHash(user.id, dto.password), + ]); return user; } catch (error) { throw new BaseException( { code: 'USER_REGISTRATION_FAILED', - message: 'Не удалось завершить регистрацию пользователя', - details: [ - { reason: error instanceof Error ? error.message : 'Database error' }, - ], + message: 'Не удалось завершить регистрацию', + details: [{ reason: error instanceof Error ? error.message : 'DB error' }], }, HttpStatus.INTERNAL_SERVER_ERROR, ); diff --git a/src/modules/user/services/settings.service.ts b/src/user/application/use-cases/update-notifications.use-case.ts similarity index 67% rename from src/modules/user/services/settings.service.ts rename to src/user/application/use-cases/update-notifications.use-case.ts index c4931c9..022e251 100644 --- a/src/modules/user/services/settings.service.ts +++ b/src/user/application/use-cases/update-notifications.use-case.ts @@ -1,29 +1,25 @@ +import { IUserRepository } from '@core/user/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { IUserRepository } from '../repository/user.repository.interface'; -import type { UpdateNotificationsDto } from '../dtos'; -import { createId } from '@paralleldrive/cuid2'; import { BaseException } from '@shared/error'; +import { UpdateNotificationsDto } from '../dtos'; +import { createId } from '@paralleldrive/cuid2'; @Injectable() -export class UserSettingsService { +export class UpdateNotificationsUseCase { constructor( @Inject('IUserRepository') private readonly userRepo: IUserRepository, ) {} - private throwUserNotFound() { - throw new BaseException( - { - code: 'USER_NOT_FOUND', - message: 'Пользователь не найден в системе', - }, - HttpStatus.NOT_FOUND, - ); - } - - public updateNotifications = async (id: string, dto: UpdateNotificationsDto) => { + async execute(id: string, dto: UpdateNotificationsDto) { const user = await this.userRepo.findById(id); - if (!user) this.throwUserNotFound(); + + if (!user) { + throw new BaseException( + { code: 'USER_NOT_FOUND', message: 'Пользователь не найден' }, + HttpStatus.NOT_FOUND, + ); + } try { const isUpdated = await this.userRepo.updateNotifications(id, { @@ -52,9 +48,7 @@ export class UserSettingsService { message: 'Настройки уведомлений обновлены', }; } catch (error) { - if (error instanceof BaseException) { - throw error; - } + if (error instanceof BaseException) throw error; throw new BaseException( { @@ -62,13 +56,12 @@ export class UserSettingsService { message: 'Ошибка при сохранении настроек пользователя', details: [ { - reason: - error instanceof Error ? error.message : 'Unknown database error', + reason: error instanceof Error ? error.message : 'Database error', }, ], }, HttpStatus.INTERNAL_SERVER_ERROR, ); } - }; + } } diff --git a/src/modules/user/commands/update-pass.command.ts b/src/user/application/use-cases/update-password.use-case.ts similarity index 57% rename from src/modules/user/commands/update-pass.command.ts rename to src/user/application/use-cases/update-password.use-case.ts index 6fc61dd..c718ae3 100644 --- a/src/modules/user/commands/update-pass.command.ts +++ b/src/user/application/use-cases/update-password.use-case.ts @@ -1,36 +1,35 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { IUserRepository } from '../repository/user.repository.interface'; +import { IUserRepository } from '@core/user/domain/repository'; +import { Injectable, Inject, HttpStatus } from '@nestjs/common'; import { BaseException } from '@shared/error'; @Injectable() -export class UpdatePassUserCommand { +export class UpdatePasswordUseCase { constructor( @Inject('IUserRepository') private readonly repository: IUserRepository, ) {} async execute(email: string, password: string) { - const { user } = await this.repository.findByEmail(email); + const result = await this.repository.findByEmail(email); - if (!user) { + if (!result?.user) { throw new BaseException( { code: 'USER_NOT_FOUND', message: 'Пользователь для обновления пароля не найден', - details: [{ target: 'email', value: email }], }, HttpStatus.NOT_FOUND, ); } try { - const isUpdated = await this.repository.updatePasswordHash(user.id, password); + const isUpdated = await this.repository.updatePasswordHash(result.user.id, password); if (!isUpdated) { throw new BaseException( { code: 'PASSWORD_UPDATE_FAILED', - message: 'Не удалось обновить пароль. Запись не была изменена.', + message: 'Запись не была изменена', }, HttpStatus.INTERNAL_SERVER_ERROR, ); @@ -41,12 +40,8 @@ export class UpdatePassUserCommand { throw new BaseException( { code: 'DATABASE_ERROR', - message: 'Произошла критическая ошибка при работе с базой данных', - details: [ - { - reason: error instanceof Error ? error.message : 'Unknown DB error', - }, - ], + message: 'Ошибка при работе с БД', + details: [{ reason: error instanceof Error ? error.message : 'Unknown' }], }, HttpStatus.INTERNAL_SERVER_ERROR, ); diff --git a/src/user/application/use-cases/update-profile.use-case.ts b/src/user/application/use-cases/update-profile.use-case.ts new file mode 100644 index 0000000..4a43377 --- /dev/null +++ b/src/user/application/use-cases/update-profile.use-case.ts @@ -0,0 +1,32 @@ +import { IUserRepository } from '@core/user/domain/repository'; +import { Injectable, Inject, HttpStatus } from '@nestjs/common'; +import { UpdateProfileDto } from '../dtos'; +import { BaseException } from '@shared/error'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class UpdateProfileUseCase { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + async execute(id: string, dto: UpdateProfileDto) { + const isUpdated = await this.userRepo.updateProfile(id, dto); + + if (!isUpdated) { + throw new BaseException( + { code: 'PROFILE_UPDATE_FAILED', message: 'Не удалось обновить данные' }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'PROFILE_UPDATED', + }); + + return { success: true }; + } +} diff --git a/src/user/application/use-cases/upload-avatar.use-case.ts b/src/user/application/use-cases/upload-avatar.use-case.ts new file mode 100644 index 0000000..12da3d0 --- /dev/null +++ b/src/user/application/use-cases/upload-avatar.use-case.ts @@ -0,0 +1,29 @@ +import { FileUploadDto, IUserMedia, USER_MEDIA_TOKEN } from '@core/modules/media'; +import { IUserRepository } from '@core/user/domain/repository'; +import { Inject, Injectable } from '@nestjs/common'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class UploadAvatarUseCase { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + @Inject(USER_MEDIA_TOKEN) + private readonly mediaService: IUserMedia, + ) {} + + async execute(userId: string, fileDto: FileUploadDto) { + const { url } = await this.mediaService.uploadUserAvatar(userId, fileDto, (url) => + this.userRepo.updateAvatar(userId, url), + ); + + await this.userRepo.logActivity({ + id: createId(), + userId, + eventType: 'AVATAR_CHANGED', + metadata: { url }, + }); + + return { success: true, url }; + } +} diff --git a/src/user/application/user.facade.ts b/src/user/application/user.facade.ts new file mode 100644 index 0000000..e49cd8f --- /dev/null +++ b/src/user/application/user.facade.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { + FindProfileQuery, + GetActivityQuery, + UpdateNotificationsUseCase, + UpdateProfileUseCase, + UploadAvatarUseCase, +} from './use-cases'; +import { UpdateProfileDto, UpdateNotificationsDto } from './dtos'; +import { FileUploadDto } from '@core/modules/media'; + +@Injectable() +export class UserFacade { + constructor( + private readonly findProfileQuery: FindProfileQuery, + private readonly getActivityQuery: GetActivityQuery, + private readonly updateNotificationsUC: UpdateNotificationsUseCase, + private readonly updateProfileUC: UpdateProfileUseCase, + private readonly uploadAvatarUC: UploadAvatarUseCase, + ) {} + + public async getProfile(userId: string) { + return this.findProfileQuery.execute(userId); + } + + public async getActivity(userId: string, page: number, limit: number) { + return this.getActivityQuery.execute(userId, page, limit); + } + + public async updateProfile(userId: string, dto: UpdateProfileDto) { + return this.updateProfileUC.execute(userId, dto); + } + + public async updateNotifications(userId: string, dto: UpdateNotificationsDto) { + return this.updateNotificationsUC.execute(userId, dto); + } + + public async uploadAvatar(userId: string, file: FileUploadDto) { + return this.uploadAvatarUC.execute(userId, file); + } +} diff --git a/src/user/domain/entities/index.ts b/src/user/domain/entities/index.ts new file mode 100644 index 0000000..54f31af --- /dev/null +++ b/src/user/domain/entities/index.ts @@ -0,0 +1 @@ +export type * from './user.domain'; diff --git a/src/modules/user/entities/user.domain.ts b/src/user/domain/entities/user.domain.ts similarity index 86% rename from src/modules/user/entities/user.domain.ts rename to src/user/domain/entities/user.domain.ts index 0721065..2bf467e 100644 --- a/src/modules/user/entities/user.domain.ts +++ b/src/user/domain/entities/user.domain.ts @@ -1,5 +1,10 @@ import { InferSelectModel, InferInsertModel } from 'drizzle-orm'; -import { users, userSecurity, userNotifications, userActivity } from './user.entity'; +import { + users, + userSecurity, + userNotifications, + userActivity, +} from '../../infrastructure/persistence/models/user.entity'; export type User = InferSelectModel; export type NewUser = InferInsertModel; diff --git a/src/user/domain/repository/index.ts b/src/user/domain/repository/index.ts new file mode 100644 index 0000000..a9419f8 --- /dev/null +++ b/src/user/domain/repository/index.ts @@ -0,0 +1 @@ +export { IUserRepository } from './user.repository.interface'; diff --git a/src/modules/user/repository/user.repository.interface.ts b/src/user/domain/repository/user.repository.interface.ts similarity index 100% rename from src/modules/user/repository/user.repository.interface.ts rename to src/user/domain/repository/user.repository.interface.ts diff --git a/src/user/index.ts b/src/user/index.ts new file mode 100644 index 0000000..4e472d9 --- /dev/null +++ b/src/user/index.ts @@ -0,0 +1,3 @@ +export { UserModule } from './user.module'; +export { RegisterUserUseCase, FindUserQuery, UpdatePasswordUseCase } from './application/use-cases'; +export { User } from './domain/entities/user.domain'; diff --git a/src/modules/user/entities/index.ts b/src/user/infrastructure/persistence/models/index.ts similarity index 100% rename from src/modules/user/entities/index.ts rename to src/user/infrastructure/persistence/models/index.ts diff --git a/src/modules/user/entities/user.entity.ts b/src/user/infrastructure/persistence/models/user.entity.ts similarity index 100% rename from src/modules/user/entities/user.entity.ts rename to src/user/infrastructure/persistence/models/user.entity.ts diff --git a/src/user/infrastructure/persistence/repositories/index.ts b/src/user/infrastructure/persistence/repositories/index.ts new file mode 100644 index 0000000..c9c59cf --- /dev/null +++ b/src/user/infrastructure/persistence/repositories/index.ts @@ -0,0 +1 @@ +export { UserRepository } from './user.repository'; diff --git a/src/modules/user/repository/user.repository.ts b/src/user/infrastructure/persistence/repositories/user.repository.ts similarity index 97% rename from src/modules/user/repository/user.repository.ts rename to src/user/infrastructure/persistence/repositories/user.repository.ts index 757958b..b036891 100644 --- a/src/modules/user/repository/user.repository.ts +++ b/src/user/infrastructure/persistence/repositories/user.repository.ts @@ -1,10 +1,10 @@ -import * as sc from '../entities'; +import { IUserRepository } from '@core/user/domain/repository'; +import * as sc from '../models'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; -import { IUserRepository } from './user.repository.interface'; import { Inject, Injectable } from '@nestjs/common'; -import type { NewUser, NewUserActivity, User, UserNotifications } from '../entities/user.domain'; import { createId } from '@paralleldrive/cuid2'; import { desc, eq, count } from 'drizzle-orm'; +import type { NewUser, NewUserActivity, User, UserNotifications } from '@core/user/domain/entities'; @Injectable() export class UserRepository implements IUserRepository { diff --git a/src/user/user.module.ts b/src/user/user.module.ts new file mode 100644 index 0000000..df7d473 --- /dev/null +++ b/src/user/user.module.ts @@ -0,0 +1,38 @@ +import { Module } from '@nestjs/common'; +import { MediaModule } from '@core/modules/media'; +import { UserRepository } from './infrastructure/persistence/repositories'; +import { UserController, UserSettingsController } from './application/controller'; +import { UserFacade } from './application/user.facade'; +import { + FindProfileQuery, + FindUserQuery, + GetActivityQuery, + RegisterUserUseCase, + UpdateNotificationsUseCase, + UpdatePasswordUseCase, + UpdateProfileUseCase, + UploadAvatarUseCase, +} from './application/use-cases'; + +const REPOSITORY = { + provide: 'IUserRepository', + useClass: UserRepository, +}; + +const USE_CASES = [ + UploadAvatarUseCase, + UpdateProfileUseCase, + UpdateNotificationsUseCase, + FindProfileQuery, + GetActivityQuery, +]; + +const EXTERNAL_USE_CASES = [RegisterUserUseCase, UpdatePasswordUseCase, FindUserQuery]; + +@Module({ + imports: [MediaModule], + controllers: [UserController, UserSettingsController], + providers: [...USE_CASES, ...EXTERNAL_USE_CASES, REPOSITORY, UserFacade], + exports: [...EXTERNAL_USE_CASES], +}) +export class UserModule {} From ef00017fb676df9a4fb05dcc310a54c194f19ce1 Mon Sep 17 00:00:00 2001 From: soorq Date: Wed, 29 Apr 2026 23:25:21 +0300 Subject: [PATCH 2/2] refactor(tests): resolve conflicts test e2e --- test/app.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index cc08a04..733a1f5 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { AppModule } from '../src/modules/app/app.module'; +import { AppModule } from '../src/app.module'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; describe('App (e2e)', () => {