From bd785b6f981329909c79bc4ddd44668fed119ca4 Mon Sep 17 00:00:00 2001 From: soorq Date: Mon, 15 Jun 2026 15:27:33 +0300 Subject: [PATCH 1/3] refactor: eslint config and fixs errors with new config --- .eslintrc.js | 46 -- .vscode/settings.json | 31 + eslint.config.mjs | 122 +++ .../src/interfaces/options.interface.ts | 40 +- libs/bootstrap/src/setups/cors.ts | 4 +- libs/bootstrap/src/setups/logger.ts | 34 +- libs/config/src/config.module.ts | 2 +- .../src/interfaces/module.interface.ts | 12 +- .../src/controller/health.controlller.spec.ts | 2 +- libs/health/src/health.module-definition.ts | 2 +- .../health/src/interfaces/module.interface.ts | 6 +- libs/imagor/src/imagor.service.ts | 28 +- .../src/interfaces/filters.interface.ts | 72 +- .../imagor/src/interfaces/module.interface.ts | 12 +- libs/s3/src/interfaces/module.interface.ts | 12 +- libs/s3/src/s3.module-definition.ts | 2 +- libs/s3/src/s3.service.ts | 6 +- package.json | 13 +- pnpm-lock.yaml | 756 ++++++++---------- .../use-cases/areas/create.use-case.ts | 2 +- .../use-cases/areas/get-one.query.ts | 4 +- .../use-cases/areas/update.use-case.ts | 83 +- src/area/domain/entities/area.domain.ts | 2 +- src/area/domain/entities/state.domain.ts | 2 +- .../repository/area.repository.interface.ts | 4 +- .../repository/states.repository.interface.ts | 4 +- .../repositories/state.repository.ts | 2 +- src/auth/application/auth.facade.ts | 28 +- .../application/controller/auth/controller.ts | 2 +- .../controller/oauth/controller.ts | 4 +- .../interfaces/cache-data.interface.ts | 1 + src/auth/application/strategies/index.ts | 3 +- .../strategies/resend-code.strategy.ts | 11 +- .../reset-password-resend.strategy.ts | 6 +- .../strategies/sign-up-resend.strategy.ts | 6 +- .../oauth/connect-provider.use-case.ts | 18 +- .../oauth/get-enabled-providers.query.ts | 33 +- .../events/create-user-workspace.event.ts | 4 +- src/auth/domain/events/register-code.event.ts | 6 +- .../domain/events/reset-password.event.ts | 4 +- .../identity.repository.interface.ts | 4 +- .../session.repository.interface.ts | 4 +- .../repositories/identity.repository.ts | 8 +- .../strategies/github.strategy.ts | 14 +- .../strategies/vkontakte.strategy.ts | 182 ++--- .../strategies/yandex.strategy.ts | 62 +- .../infrastructure/utils/get-device-meta.ts | 10 +- .../infrastructure/workers/mail.processor.ts | 4 +- .../infrastructure/workers/user.processor.ts | 2 +- .../application/mappers/member.mapper.ts | 2 +- src/projects/application/project.facade.ts | 2 +- .../use-cases/member/find-all.query.ts | 2 +- .../use-cases/project/create.use-case.ts | 2 +- .../use-cases/project/find-one.query.ts | 4 +- .../project/generate-share-token.use-case.ts | 2 +- .../use-cases/project/update.use-case.ts | 37 +- src/projects/domain/entities/member.domain.ts | 2 +- .../domain/entities/project.domain.ts | 5 +- .../domain/policy/project-access.policy.ts | 2 +- .../repository/member.repository.interface.ts | 4 +- .../project.repository.interface.ts | 7 +- .../repositories/member.repository.ts | 16 +- .../repositories/project.repository.ts | 24 +- .../cache/adapters/redis-cache.adapter.ts | 10 +- .../adapters/cache/ports/static-cache.port.ts | 30 +- src/shared/adapters/mail/adapter.ts | 4 +- src/shared/error/exception.ts | 13 +- src/shared/error/filter.ts | 23 +- src/shared/error/swagger.ts | 10 +- src/shared/guards/bearer.guard.ts | 4 +- src/shared/guards/oauth.guard.ts | 18 +- .../zod-validation.interceptor.ts | 4 +- .../decorators/extract-media-req.decorator.ts | 8 +- src/shared/media/media.service.ts | 4 +- .../media/strategies/team-media.strategy.ts | 14 +- .../media/strategies/user-avatar.strategy.ts | 5 +- src/shared/media/workers/media.worker.ts | 4 +- src/shared/schemas/sorting.schema.ts | 2 +- src/shared/types/jwt-payload.ts | 12 +- src/shared/utils/remove-undefined.util.ts | 10 +- .../controller/members/controller.ts | 2 +- src/teams/application/dtos/invitation.dto.ts | 21 +- .../application/mappers/member.mapper.ts | 2 +- src/teams/application/team.facade.ts | 2 +- .../use-cases/base/update-team.use-case.ts | 2 +- .../invitions/send-invitation.use-case.ts | 2 +- .../invitions/update-invitation.use-case.ts | 10 +- src/teams/domain/entities/teams.domain.ts | 4 +- .../domain/events/team-invitation.event.ts | 6 +- .../repository/teams.repository.interface.ts | 40 +- .../repositories/teams.repository.ts | 26 +- .../infrastructure/workers/mail.processor.ts | 2 +- .../use-cases/find-by-ids.query.ts | 2 +- .../application/use-cases/find-user.query.ts | 2 +- .../use-cases/register-user.use-case.ts | 2 +- src/user/domain/entities/user.domain.ts | 20 +- .../repository/user.repository.interface.ts | 12 +- .../persistence/models/user.entity.ts | 8 +- .../repositories/user.repository.ts | 19 +- test/app.e2e-spec.ts | 2 +- tsconfig.json | 3 +- 101 files changed, 1115 insertions(+), 1081 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 .vscode/settings.json create mode 100644 eslint.config.mjs diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 14746fb4..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - sourceType: 'module', - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended'], - root: true, - env: { - node: true, - }, - globals: { - describe: 'readonly', - it: 'readonly', - expect: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - vi: 'readonly', - }, - ignorePatterns: [ - '.eslintrc.js', - '*.config.{js,ts}', - 'migrations', - 'infra', - '.github', - 'dist', - 'node_modules', - ], - rules: { - 'prettier/prettier': 'off', - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - }, - ], - }, -}; diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ba92e259 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,31 @@ +{ + "eslint.enable": true, + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], + "eslint.options": { + "overrideConfigFile": "eslint.config.mjs" + }, + "eslint.format.enable": true, + "eslint.lintTask.enable": true, + "eslint.run": "onSave", + "eslint.problems.shortenToSingleLine": false, + + "eslint.trace.server": "off", + "eslint.debug": false, + + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + + "js/ts.preferences.importModuleSpecifier": "relative", + "js/ts.preferences.preferTypeOnlyAutoImports": false, + + "prettier.enable": true, + "prettier.requireConfig": true, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..b9b74d46 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,122 @@ +// @ts-check +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import functional from 'eslint-plugin-functional'; + +export default tseslint.config( + { + ignores: ['node_modules', 'dist', '**/*.js', '**/*.d.ts', 'infra', 'migrations'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { + functional, + }, + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + args: 'after-used', + ignoreRestSiblings: true, + }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/prefer-readonly': 'off', + '@typescript-eslint/prefer-readonly-parameter-types': 'off', + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + disallowTypeAnnotations: false, + fixStyle: 'separate-type-imports', + }, + ], + 'functional/prefer-readonly-type': 'off', + 'functional/no-conditional-statements': 'off', + 'functional/no-return-void': 'off', + 'functional/immutable-data': 'warn', + 'functional/no-let': 'off', + 'functional/no-expression-statements': 'off', + + 'no-restricted-syntax': [ + 'error', + { + selector: `ImportDeclaration[importKind=type] > ImportSpecifier[imported.name=/.*Dto/]`, + message: + 'DTO с декораторами должны использовать обычный импорт, а не import type', + }, + { + selector: `ImportDeclaration[importKind=type] > ImportSpecifier[imported.name=/.*Validation/]`, + message: + 'Классы валидации должны использовать обычный импорт, а не import type', + }, + { + selector: `ImportDeclaration[importKind=type] > ImportSpecifier[imported.name=/.*Entity/]`, + message: + 'Entity с декораторами должны использовать обычный импорт, а не import type', + }, + { + selector: 'WhileStatement', + message: + '⚠️ Цикл while может быть бесконечным. Используйте for или for...of с явным счётчиком', + }, + { + selector: 'DoWhileStatement', + message: '⚠️ Цикл do-while сложнее читать. Используйте for или for...of', + }, + { + selector: 'ForInStatement', + message: 'Цикл for-in включает прототип. Используйте Object.keys() + for...of', + }, + { + selector: 'LabeledStatement', + message: 'Метки делают код нечитаемым. Используйте функции с return', + }, + { + selector: 'SwitchStatement[cases.length>10]', + message: '⚠️ Switch с более чем 10 кейсами. Используйте Map или объект', + }, + { + selector: 'SwitchStatement:not([cases.length<=5])', + message: + '⚠️ Большой switch statement. Рассмотрите использование Map или реестра стратегий', + }, + ], + 'no-restricted-properties': [ + 'error', + { + object: 'Array', + property: 'pop', + message: 'Используйте slice/spread вместо pop', + }, + { + object: 'Array', + property: 'splice', + message: 'Используйте filter/slice вместо splice', + }, + { + object: 'Object', + property: 'assign', + message: 'Используйте spread оператор: {...obj, newProp} вместо Object.assign', + }, + ], + }, + }, + { + files: ['infra/**/*.ts', '**/migrations/**/*.ts', '**/*.config.ts', 'libs/**/*.ts'], + rules: { + 'functional/no-conditional-statements': 'off', + 'functional/immutable-data': 'off', + }, + }, +); diff --git a/libs/bootstrap/src/interfaces/options.interface.ts b/libs/bootstrap/src/interfaces/options.interface.ts index 3d42a761..7f2492f3 100644 --- a/libs/bootstrap/src/interfaces/options.interface.ts +++ b/libs/bootstrap/src/interfaces/options.interface.ts @@ -4,33 +4,33 @@ import type { NestFastifyApplication } from '@nestjs/platform-fastify'; import type { ThrottlerModuleOptions } from '@nestjs/throttler'; export interface SwaggerMetadata { - title?: string; - description?: string; - version?: string; - path?: string; + readonly title?: string; + readonly description?: string; + readonly version?: string; + readonly path?: string; } export interface SwaggerInfrastructure { - server?: { - port?: string | number; - domain?: string; - stage?: string; + readonly server?: { + readonly port?: string | number; + readonly domain?: string; + readonly stage?: string; }; - services?: { name: string; port: number }[]; + readonly services?: readonly { readonly name: string; readonly port: number }[]; } export interface SwaggerOptions extends SwaggerMetadata, SwaggerInfrastructure {} export interface BootstrapOptions { - apiPrefix?: string; - version?: string; - appModule: Type; - defaultPort?: number; - portEnvKey?: keyof Config; - serviceName: string; - setupApp?: (app: NestFastifyApplication) => Promise | void; - swaggerOptions?: SwaggerMetadata; - throttlerOptions?: ThrottlerModuleOptions; - useCookieParser?: boolean; - useCors?: boolean; + readonly apiPrefix?: string; + readonly version?: string; + readonly appModule: Type; + readonly defaultPort?: number; + readonly portEnvKey?: keyof Config; + readonly serviceName: string; + readonly setupApp?: (app: NestFastifyApplication) => Promise | void; + readonly swaggerOptions?: SwaggerMetadata; + readonly throttlerOptions?: ThrottlerModuleOptions; + readonly useCookieParser?: boolean; + readonly useCors?: boolean; } diff --git a/libs/bootstrap/src/setups/cors.ts b/libs/bootstrap/src/setups/cors.ts index 59a79594..3993f49d 100644 --- a/libs/bootstrap/src/setups/cors.ts +++ b/libs/bootstrap/src/setups/cors.ts @@ -1,7 +1,7 @@ import fastifyCors from '@fastify/cors'; import type { NestFastifyApplication } from '@nestjs/platform-fastify'; -export function setupCors(app: NestFastifyApplication, origins: string[]) { +export function setupCors(app: NestFastifyApplication, origins: readonly string[]) { app.getHttpAdapter() .getInstance() .register(fastifyCors, { @@ -24,7 +24,7 @@ export function setupCors(app: NestFastifyApplication, origins: string[]) { } callback(new Error('Not allowed by CORS'), false); - } catch (e) { + } catch { callback(new Error('Invalid origin format'), false); } }, diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts index df7dc9a2..c5403c94 100644 --- a/libs/bootstrap/src/setups/logger.ts +++ b/libs/bootstrap/src/setups/logger.ts @@ -129,95 +129,95 @@ export type TLog = { * Used by Grafana to color-code rows and for alerting. * @type {'info' | 'error' | 'warn'} */ - level: 'info' | 'error' | 'warn'; + readonly level: 'info' | 'error' | 'warn'; /** * Human-readable summary of the event. * @example 'Request completed POST /v1/auth/sign-in | 200 | 145ms' * @type {string} */ - message: string; + readonly message: string; /** * Event occurrence time in ISO 8601 format. * @example '2026-05-09T01:17:29.000Z' * @type {string} */ - timestamp: string; + readonly timestamp: string; /** * Unique identifier for the HTTP request (e.g., UUID, NanoID). * Used to correlate all logs produced within a single request lifecycle. * @type {string} */ - request_id: string; + readonly request_id: string; /** * The system component that triggered the log entry. * @type {'interceptor' | 'filter_exception' | 'guard' | 'service'} */ - triggered_by: 'interceptor' | 'filter_exception' | 'guard' | 'service'; + readonly triggered_by: 'interceptor' | 'filter_exception' | 'guard' | 'service'; /** * The logical type of the event within the request/response flow. * @type {'request' | 'response' | 'error' | 'system'} */ - type: 'request' | 'response' | 'error' | 'system'; + readonly type: 'request' | 'response' | 'error' | 'system'; /** * The HTTP method used for the request. * @type {'POST' | 'GET' | 'DELETE' | 'PATCH' | 'PUT' | 'OPTIONS' | 'HEAD'} */ - method: 'POST' | 'GET' | 'DELETE' | 'PATCH' | 'PUT' | 'OPTIONS' | 'HEAD'; + readonly method: 'POST' | 'GET' | 'DELETE' | 'PATCH' | 'PUT' | 'OPTIONS' | 'HEAD'; /** * The full URL of the request, including query parameters. * @example '/v1/auth/sign-in?source=mobile' * @type {string} */ - url: string; + readonly url: string; /** * The sanitized API path, including versioning but excluding query parameters. * Ideal for aggregating statistics per endpoint. * @example '/v1/auth/sign-in' * @type {string} */ - path: string; + readonly path: string; /** * The HTTP status code returned to the client. * @example 200 * @type {number} */ - status_code: number; + readonly status_code: number; /** * Request processing time in milliseconds. * Note: Typically undefined for entries with type 'request'. * @type {number} */ - delay_num?: number; + readonly delay_num?: number; /** * The client's IP address. * @type {string} */ - ip: string; + readonly ip: string; /** * The client's application or browser identification string. * @type {string} */ - user_agent: string; + readonly user_agent: string; /** * The name of the NestJS controller handling the request. * @example 'AuthController' * @type {string} */ - controller: string; + readonly controller: string; /** * The name of the specific controller method (handler). * @example 'signIn' * @type {string} */ - handler: string; + readonly handler: string; /** * The error stack trace. Only populated when level is 'error'. * @type {string} */ - stack?: string; + readonly stack?: string; /** * Additional contextual data for debugging (e.g., Zod validation issues, DB error details). * @type {any} */ - error_details?: any; + readonly error_details?: any; }; diff --git a/libs/config/src/config.module.ts b/libs/config/src/config.module.ts index 48e55bfc..d6bf1de1 100644 --- a/libs/config/src/config.module.ts +++ b/libs/config/src/config.module.ts @@ -23,7 +23,7 @@ const validateConfig = (config: Record) => { console.groupEnd(); - throw new Error('Invalid environment configuration'); + throw new Error('Invalid environment configuration', { cause: error }); } throw error; } diff --git a/libs/database/src/interfaces/module.interface.ts b/libs/database/src/interfaces/module.interface.ts index bbe40216..fd9499a2 100644 --- a/libs/database/src/interfaces/module.interface.ts +++ b/libs/database/src/interfaces/module.interface.ts @@ -8,14 +8,14 @@ export interface DatabaseModuleOptions { * @default 'public' * @example 'auth_service' */ - schemaName?: string; + readonly schemaName?: string; /** * Объект схемы Drizzle, содержащий определения таблиц и связей. * * Рекомендуется импортировать целиком: `import * as schema from './schema'`. * @example schema */ - schema: Record; + readonly schema: Record; /** * Настройки драйвера `postgres.js`. @@ -24,27 +24,27 @@ export interface DatabaseModuleOptions { * @see https://github.com/porsager/postgres#options * @example { max: 20, idle_timeout: 30, connect_timeout: 5 } */ - pool?: Options; + readonly pool?: Options; /** * Включение или выключение логирования SQL-запросов в консоль через NestJS Logger. * @default false */ - logging?: boolean; + readonly logging?: boolean; /** * Флаг для автоматического запуска миграций при старте приложения. * * Полезно для локальной разработки и стейджинга. * @default true */ - runMigrations?: boolean; + readonly runMigrations?: boolean; /** * Абсолютный путь к директории с файлами миграций (SQL или JS/TS). * * Если не указано, используется путь `./migrations` от корня проекта. * @default path.resolve(process.cwd(), 'migrations') */ - migrationsPath?: string; + readonly migrationsPath?: string; } /** diff --git a/libs/health/src/controller/health.controlller.spec.ts b/libs/health/src/controller/health.controlller.spec.ts index 71a782a7..4d624a65 100644 --- a/libs/health/src/controller/health.controlller.spec.ts +++ b/libs/health/src/controller/health.controlller.spec.ts @@ -4,7 +4,7 @@ import { HttpStatus, Logger } from '@nestjs/common'; describe('HealthController', () => { let controller: HealthController; - let healthServiceMock: { getHealthData: ReturnType }; + let healthServiceMock: { readonly getHealthData: ReturnType }; const SERVICE_NAME = 'MyService'; diff --git a/libs/health/src/health.module-definition.ts b/libs/health/src/health.module-definition.ts index c0c7639e..8a374e8f 100644 --- a/libs/health/src/health.module-definition.ts +++ b/libs/health/src/health.module-definition.ts @@ -1,5 +1,5 @@ import { ConfigurableModuleBuilder } from '@nestjs/common'; -import { HealthModuleOptions } from './interfaces'; +import type { HealthModuleOptions } from './interfaces'; export const { ASYNC_OPTIONS_TYPE, ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = new ConfigurableModuleBuilder() diff --git a/libs/health/src/interfaces/module.interface.ts b/libs/health/src/interfaces/module.interface.ts index 054caaf1..f0c08c76 100644 --- a/libs/health/src/interfaces/module.interface.ts +++ b/libs/health/src/interfaces/module.interface.ts @@ -3,7 +3,7 @@ export type HealthIndicatorKey = HealthIndicatorsServices | (string & NonNullabl export type HealthIndicatorFn = () => boolean | Promise; export interface HealthModuleOptions { - serviceName: string; - version?: string; - indicators?: Partial>; + readonly serviceName: string; + readonly version?: string; + readonly indicators?: Partial>; } diff --git a/libs/imagor/src/imagor.service.ts b/libs/imagor/src/imagor.service.ts index bcfc28d9..ab2de8aa 100644 --- a/libs/imagor/src/imagor.service.ts +++ b/libs/imagor/src/imagor.service.ts @@ -9,11 +9,11 @@ import { AxiosError } from 'axios'; @Injectable() export class ImagorService { - private logger = new Logger(ImagorService.name); + private readonly logger = new Logger(ImagorService.name); constructor( @Inject(MODULE_OPTIONS_TOKEN) - private options: ImagorModuleOptions, + private readonly options: ImagorModuleOptions, private readonly http: HttpService, ) {} @@ -28,21 +28,17 @@ export class ImagorService { const signature = this.getFullSignedPath(transformPath); const url = `${host}/${signature}`; - try { - this.logger.debug(url); - const response = await firstValueFrom( - this.http.get(url, { responseType: 'arraybuffer' }).pipe( - catchError((error: AxiosError) => { - console.error('Imagor Get Error:', error.response?.data || error.message); - return throwError(() => error); - }), - ), - ); + this.logger.debug(url); + const response = await firstValueFrom( + this.http.get(url, { responseType: 'arraybuffer' }).pipe( + catchError((error: AxiosError) => { + console.error('Imagor Get Error:', error.response?.data || error.message); + return throwError(() => error); + }), + ), + ); - return Buffer.from(response.data); - } catch (error) { - throw error; - } + return Buffer.from(response.data); } private buildTransformPath(path: string, presetOrFilters?: string | Filters): string { diff --git a/libs/imagor/src/interfaces/filters.interface.ts b/libs/imagor/src/interfaces/filters.interface.ts index 1fe3471d..f13c4e3f 100644 --- a/libs/imagor/src/interfaces/filters.interface.ts +++ b/libs/imagor/src/interfaces/filters.interface.ts @@ -18,64 +18,64 @@ export interface Filters { * Ширина выходного изображения в пикселях. * Используйте 'orig', чтобы сохранить исходную ширину. */ - width?: number | 'orig'; + readonly width?: number | 'orig'; /** * Высота выходного изображения в пикселях. * Используйте 'orig', чтобы сохранить исходную высоту. */ - height?: number | 'orig'; + readonly height?: number | 'orig'; /** * Включает умную обрезку (Smart Cropping). * Imagor попытается найти наиболее важные области (лица, контрастные объекты) и сфокусироваться на них. */ - smart?: boolean; + readonly smart?: boolean; /** * Режим вписывания. * Если не указан, по умолчанию используется обрезка (Crop) для заполнения всей области. */ - fit?: Fit; + readonly fit?: Fit; /** * Устанавливает качество выходного изображения. * @param {number} quality Число от 0 до 100. */ - quality?: number; + readonly quality?: number; /** * Принудительно устанавливает формат выходного изображения. * WebP и AVIF рекомендуются для лучшего сжатия. */ - format?: Format; + readonly format?: Format; /** * Если true, автоматически конвертирует изображения с прозрачностью в JPEG, * заменяя прозрачные области фоном (белым по умолчанию). */ - autojpg?: boolean; + readonly autojpg?: boolean; /** Удаляет EXIF метаданные из выходного изображения. Полезно для приватности и уменьшения размера. */ - strip_exif?: boolean; + readonly strip_exif?: boolean; /** Удаляет ICC профили цвета. */ - strip_icc?: boolean; + readonly strip_icc?: boolean; /** * Регулирует яркость изображения. * @param {number} brightness Число от -100 до 100. Положительные — ярче, отрицательные — темнее. */ - brightness?: number; + readonly brightness?: number; /** * Регулирует контрастность изображения. * @param {number} contrast Число от -100 до 100. */ - contrast?: number; + readonly contrast?: number; /** Преобразует изображение в черно-белое (grayscale). */ - grayscale?: boolean; + readonly grayscale?: boolean; /** * Настройка цветовых каналов RGB. @@ -83,19 +83,19 @@ export interface Filters { * @property {number} g Зеленый (-100 до 100) * @property {number} b Синий (-100 до 100) */ - rgb?: { r: number; g: number; b: number }; + readonly rgb?: { readonly r: number; readonly g: number; readonly b: number }; /** * Изменяет общую насыщенность цветов. * @param {number} proportion Число от 0 до 100. */ - proportion?: number; + readonly proportion?: number; /** * Применяет размытие Гаусса. * Можно передать число (радиус) или объект для более точной настройки сигмы. */ - blur?: number | { radius: number; sigma?: number }; + readonly blur?: number | { readonly radius: number; readonly sigma?: number }; /** * Повышает резкость изображения. @@ -103,71 +103,71 @@ export interface Filters { * @property {number} radius Радиус фильтра. * @property {number} threshold Порог срабатывания. */ - sharpen?: { - amount: number; - radius: number; - threshold: number; + readonly sharpen?: { + readonly amount: number; + readonly radius: number; + readonly threshold: number; }; /** * Добавляет шум на изображение. * @param {number} noise Уровень шума от 0 до 100. */ - noise?: number; + readonly noise?: number; /** Поворачивает изображение на заданный угол по часовой стрелке. */ - rotate?: 90 | 180 | 270; + readonly rotate?: 90 | 180 | 270; /** * Определяет цвет заполнения пустых областей при использовании режима 'fit-in'. * @example 'ff0000' (hex), 'white' (name) или 'auto' (главный цвет изображения). */ - fill?: string; + readonly fill?: string; /** Устанавливает цвет фона для прозрачных изображений (например, PNG). */ - background_color?: string; + readonly background_color?: string; /** * Наложение водяного знака поверх основного изображения. */ - watermark?: { + readonly watermark?: { /** Путь к файлу водяного знака в хранилище. */ - image: string; + readonly image: string; /** Позиция по горизонтали или смещение в пикселях. */ - x?: number | 'center' | 'left' | 'right'; + readonly x?: number | 'center' | 'left' | 'right'; /** Позиция по вертикали или смещение в пикселях. */ - y?: number | 'center' | 'top' | 'bottom'; + readonly y?: number | 'center' | 'top' | 'bottom'; /** Прозрачность водяного знака (0 - прозрачный, 100 - непрозрачный). */ - alpha?: number; + readonly alpha?: number; /** Относительная ширина знака в процентах (0.0 - 1.0) от основного изображения. */ - w_ratio?: number; + readonly w_ratio?: number; /** Относительная высота знака в процентах (0.0 - 1.0). */ - h_ratio?: number; + readonly h_ratio?: number; }; /** * Указывает точку фокуса для кропа. * Полезно, если вы знаете координаты лица или важного объекта. */ - focal?: { x: number; y: number }; + readonly focal?: { readonly x: number; readonly y: number }; /** * Скругление углов изображения. * @property {number} radius Радиус скругления в пикселях. * @property {string} color Цвет заливки углов (например, 'transparent' или 'ffffff'). */ - round_corner?: { - radius: number; - color?: string; + readonly round_corner?: { + readonly radius: number; + readonly color?: string; }; /** * Ограничивает размер файла (в байтах). Imagor будет снижать качество, пока не впишется в лимит. */ - max_bytes?: number; + readonly max_bytes?: number; /** * Запрещает увеличивать изображение, если его исходные размеры меньше запрошенных (width/height). */ - no_upscale?: boolean; + readonly no_upscale?: boolean; } diff --git a/libs/imagor/src/interfaces/module.interface.ts b/libs/imagor/src/interfaces/module.interface.ts index 5f77397a..7338255b 100644 --- a/libs/imagor/src/interfaces/module.interface.ts +++ b/libs/imagor/src/interfaces/module.interface.ts @@ -5,23 +5,23 @@ import type { Filters } from './filters.interface'; */ export interface ImagorModuleOptions { /** Базовый URL вашего инстанса Imagor (например, https://imagor.example.com) */ - url: string; + readonly url: string; /** Секретный ключ для генерации HMAC подписи (безопасные URL) */ - secret?: string; + readonly secret?: string; /** Глобальные фильтры, которые будут применяться ко всем изображениям по умолчанию */ - filters?: Filters; + readonly filters?: Filters; /** Базовый путь в S3/хранилище (например, 'products/') */ - storageRoot?: string; + readonly storageRoot?: string; /** * Именованные пресеты для часто используемых трансформаций. * @example { 'thumb': { width: 150, height: 150, smart: true } } */ - presets?: Record; + readonly presets?: Record; /** Включает логирование процесса генерации URL для отладки */ - debug?: boolean; + readonly debug?: boolean; } diff --git a/libs/s3/src/interfaces/module.interface.ts b/libs/s3/src/interfaces/module.interface.ts index 05bcdf17..c62cd030 100644 --- a/libs/s3/src/interfaces/module.interface.ts +++ b/libs/s3/src/interfaces/module.interface.ts @@ -1,14 +1,14 @@ import type { S3ClientConfig } from '@aws-sdk/client-s3'; export interface S3Connection extends Pick { - endpoint: string; - region: string; + readonly endpoint: string; + readonly region: string; } -export interface S3Config extends Omit {} +export type S3Config = Omit; export interface S3ModuleOptions { - connection: S3Connection; - bucket: string; - config?: S3Config; + readonly connection: S3Connection; + readonly bucket: string; + readonly config?: S3Config; } diff --git a/libs/s3/src/s3.module-definition.ts b/libs/s3/src/s3.module-definition.ts index 3648deb4..f4d9e9bd 100644 --- a/libs/s3/src/s3.module-definition.ts +++ b/libs/s3/src/s3.module-definition.ts @@ -1,5 +1,5 @@ import { ConfigurableModuleBuilder } from '@nestjs/common'; -import { S3ModuleOptions } from './interfaces'; +import type { S3ModuleOptions } from './interfaces'; export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = new ConfigurableModuleBuilder() diff --git a/libs/s3/src/s3.service.ts b/libs/s3/src/s3.service.ts index c44341b7..17a2c8bd 100644 --- a/libs/s3/src/s3.service.ts +++ b/libs/s3/src/s3.service.ts @@ -11,9 +11,9 @@ import { MODULE_OPTIONS_TOKEN } from './s3.module-definition'; export class S3Service { constructor( @Inject(S3_CLIENT) - private s3Client: S3Client, + private readonly s3Client: S3Client, @Inject(MODULE_OPTIONS_TOKEN) - private options: S3ModuleOptions, + private readonly options: S3ModuleOptions, ) {} private get bucket(): string { @@ -28,7 +28,7 @@ export class S3Service { }), ); return true; - } catch (error) { + } catch { return false; } } diff --git a/package.json b/package.json index 959c2516..3c54f688 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start:prod": "nest start", "start:dev": "nest start -w", "start:debug": "nest start -d -w", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "test": "vitest run", "test:w": "vitest", "test:c": "vitest run --coverage", @@ -62,7 +62,6 @@ "bullmq": "^5.73.4", "dotenv": "^17.4.2", "drizzle-orm": "^0.45.2", - "drizzle-zod": "^0.8.3", "fastify": "^5.8.4", "handlebars": "^4.7.9", "ioredis": "^5.10.1", @@ -87,6 +86,7 @@ "devDependencies": { "@commitlint/cli": "^20.5.0", "@commitlint/config-conventional": "^20.5.0", + "@eslint/js": "^10.0.1", "@nestjs/cli": "^11.0.19", "@nestjs/schematics": "^11.0.10", "@nestjs/testing": "^11.1.18", @@ -97,17 +97,20 @@ "@types/passport-jwt": "^4.0.1", "@types/passport-oauth2": "^1.8.0", "@types/ua-parser-js": "^0.7.39", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.61.0", + "@typescript-eslint/parser": "^8.61.0", "@vitest/coverage-v8": "^4.1.4", "drizzle-kit": "^0.31.10", - "eslint": "^8.42.0", + "eslint": "^10.5.0", + "eslint-plugin-functional": "^10.0.0", + "eslint-plugin-no-loops": "^0.4.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", "prettier": "^3.0.0", "ts-loader": "^9.4.3", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3", + "typescript-eslint": "^8.61.0", "vitest": "^4.1.4" }, "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa34e7bc..3784f377 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,9 +89,6 @@ importers: drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9) - drizzle-zod: - specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9))(zod@4.3.6) fastify: specifier: ^5.8.4 version: 5.8.4 @@ -159,6 +156,9 @@ importers: '@commitlint/config-conventional': specifier: ^20.5.0 version: 20.5.0 + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.5.0(jiti@2.6.1)) '@nestjs/cli': specifier: ^11.0.19 version: 11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7) @@ -190,11 +190,11 @@ importers: specifier: ^0.7.39 version: 0.7.39 '@typescript-eslint/eslint-plugin': - specifier: ^6.0.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + specifier: ^8.61.0 + version: 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': - specifier: ^6.0.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + specifier: ^8.61.0 + version: 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) '@vitest/coverage-v8': specifier: ^4.1.4 version: 4.1.4(vitest@4.1.4) @@ -202,8 +202,14 @@ importers: specifier: ^0.31.10 version: 0.31.10 eslint: - specifier: ^8.42.0 - version: 8.57.1 + specifier: ^10.5.0 + version: 10.5.0(jiti@2.6.1) + eslint-plugin-functional: + specifier: ^10.0.0 + version: 10.0.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-no-loops: + specifier: ^0.4.0 + version: 0.4.0(eslint@10.5.0(jiti@2.6.1)) husky: specifier: ^9.1.7 version: 9.1.7 @@ -222,6 +228,9 @@ importers: typescript: specifier: ^5.1.3 version: 5.9.3 + typescript-eslint: + specifier: ^8.61.0 + version: 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) vitest: specifier: ^4.1.4 version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -1025,13 +1034,34 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -1090,18 +1120,25 @@ packages: '@fastify/view@11.1.1': resolution: {integrity: sha512-GiHqT3R2eKJgWmy0s45eELTC447a4+lTM2o+8fSWeKwBe9VToeePuHJcKtOEXPrKGSddGO0RsNayULiS3aeHeQ==} - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} @@ -1526,18 +1563,6 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@nuxt/opencollective@0.4.1': resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} @@ -2030,6 +2055,9 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2090,9 +2118,6 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -2105,66 +2130,64 @@ packages: '@types/ua-parser-js@0.7.39': resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} - '@typescript-eslint/eslint-plugin@6.21.0': - resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/eslint-plugin@8.61.0': + resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.61.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@6.21.0': - resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/parser@8.61.0': + resolution: {integrity: sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} - - '@typescript-eslint/type-utils@6.21.0': - resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/project-service@8.61.0': + resolution: {integrity: sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@6.21.0': - resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/scope-manager@8.61.0': + resolution: {integrity: sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@6.21.0': - resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/tsconfig-utils@8.61.0': + resolution: {integrity: sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.61.0': + resolution: {integrity: sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.61.0': + resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/typescript-estree@8.61.0': + resolution: {integrity: sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@6.21.0': - resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/utils@8.61.0': + resolution: {integrity: sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@typescript-eslint/visitor-keys@8.61.0': + resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@4.1.4': resolution: {integrity: sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==} @@ -2360,10 +2383,6 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2425,9 +2444,6 @@ packages: brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - brace-expansion@2.0.3: - resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -2667,6 +2683,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -2697,14 +2717,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -2821,12 +2833,6 @@ packages: sqlite3: optional: true - drizzle-zod@0.8.3: - resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} - peerDependencies: - drizzle-orm: '>=0.36.0' - zod: ^3.25.0 || ^4.0.0 - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2918,27 +2924,54 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-plugin-functional@10.0.0: + resolution: {integrity: sha512-D/BWdGUvPz5uIC+kZM1BWrELhjHpDiYxIYjzF7a6TZ62KINajjKgOiSYGbGv4fpMT1enEC9yG+0kOaIRTZzpTg==} + engines: {node: '>=v20.0.0'} + peerDependencies: + eslint: ^9.0.0 || ^10.0.0 + typescript: '>=4.7.4' + peerDependenciesMeta: + typescript: + optional: true + + eslint-plugin-no-loops@0.4.0: + resolution: {integrity: sha512-cudfSMy9KLnyqQ5QPBuzuxtBqGvJOZ4SeeUuKbGMYc2opJzCUnkKKfUYI4bMErdgTwoJeguYscSWaWI5/2A8aA==} + peerDependencies: + eslint: '>=9' + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.5.0: + resolution: {integrity: sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} @@ -2992,10 +3025,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3042,9 +3071,9 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} file-type@21.3.4: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} @@ -3062,9 +3091,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} @@ -3099,9 +3128,6 @@ packages: fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3134,10 +3160,6 @@ packages: engines: {node: '>=18'} hasBin: true - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -3149,22 +3171,10 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3172,9 +3182,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - handlebars@4.7.9: resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} engines: {node: '>=0.4.7'} @@ -3219,6 +3226,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -3230,10 +3241,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3268,6 +3275,12 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-immutable-type@5.0.4: + resolution: {integrity: sha512-tDf0vB9dJJt/1USS2nvHJb8UsIAhs+pF6z8UZLNdhrzB+PKMrdcN45je9jhuFpaJOjJpoRhueFmFrRs5g/TNMA==} + peerDependencies: + eslint: '*' + typescript: '>=4.7.4' + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -3280,10 +3293,6 @@ packages: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3519,9 +3528,6 @@ packages: lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} @@ -3588,10 +3594,6 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3624,10 +3626,6 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3784,10 +3782,6 @@ packages: resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3935,9 +3929,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -4009,19 +4000,11 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rolldown@1.0.0-rc.15: resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} @@ -4085,10 +4068,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} @@ -4180,10 +4159,6 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} @@ -4237,9 +4212,6 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - thread-stream@4.0.0: resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} engines: {node: '>=20'} @@ -4290,11 +4262,16 @@ packages: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} peerDependencies: - typescript: '>=4.2.0' + typescript: '>=4.0.0' ts-loader@9.5.7: resolution: {integrity: sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==} @@ -4327,6 +4304,13 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + typescript-eslint@8.61.0: + resolution: {integrity: sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5493,28 +5477,39 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@2.6.1))': dependencies: - eslint: 8.57.1 + eslint: 10.5.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/eslintrc@2.1.4': + '@eslint/config-array@0.23.5': dependencies: - ajv: 6.14.0 + '@eslint/object-schema': 3.0.5 debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color - '@eslint/js@8.57.1': {} + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.5.0(jiti@2.6.1))': + optionalDependencies: + eslint: 10.5.0(jiti@2.6.1) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 '@fastify/accept-negotiator@2.0.1': {} @@ -5610,17 +5605,21 @@ snapshots: toad-cache: 3.7.1 optional: true - '@humanwhocodes/config-array@0.13.0': + '@humanfs/core@0.19.2': dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.4.3': {} '@inquirer/ansi@1.0.2': {} @@ -6009,18 +6008,6 @@ snapshots: '@noble/hashes@2.0.1': {} - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - '@nuxt/opencollective@0.4.1': dependencies: consola: 3.4.2 @@ -6570,6 +6557,8 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} '@types/express-serve-static-core@5.1.1': @@ -6656,8 +6645,6 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/semver@7.7.1': {} - '@types/send@1.2.1': dependencies: '@types/node': 20.19.39 @@ -6671,93 +6658,96 @@ snapshots: '@types/ua-parser-js@0.7.39': {} - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.61.0 + '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.0 + eslint: 10.5.0(jiti@2.6.1) + ignore: 7.0.5 natural-compare: 1.4.0 - semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/scope-manager': 8.61.0 + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.61.0 + debug: 4.4.3 + eslint: 10.5.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.61.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) + '@typescript-eslint/types': 8.61.0 debug: 4.4.3 - eslint: 8.57.1 - optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@6.21.0': + '@typescript-eslint/scope-manager@8.61.0': dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/visitor-keys': 8.61.0 - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.61.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.9.3) + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + eslint: 10.5.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@6.21.0': {} + '@typescript-eslint/types@8.61.0': {} - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.61.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/visitor-keys': 6.21.0 + '@typescript-eslint/project-service': 8.61.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.61.0(typescript@5.9.3) + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/visitor-keys': 8.61.0 debug: 4.4.3 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.3 + minimatch: 10.2.5 semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@5.9.3) - optionalDependencies: + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@types/json-schema': 7.0.15 - '@types/semver': 7.7.1 - '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/types': 6.21.0 - '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.9.3) - eslint: 8.57.1 - semver: 7.7.4 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.61.0 + '@typescript-eslint/types': 8.61.0 + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + eslint: 10.5.0(jiti@2.6.1) + typescript: 5.9.3 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@6.21.0': + '@typescript-eslint/visitor-keys@8.61.0': dependencies: - '@typescript-eslint/types': 6.21.0 - eslint-visitor-keys: 3.4.3 - - '@ungap/structured-clone@1.3.0': {} + '@typescript-eslint/types': 8.61.0 + eslint-visitor-keys: 5.0.1 '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': dependencies: @@ -6982,8 +6972,6 @@ snapshots: array-timsort@1.0.3: {} - array-union@2.1.0: {} - assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.0: @@ -7050,10 +7038,6 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.3: - dependencies: - balanced-match: 1.0.2 - brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -7276,6 +7260,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge-ts@7.1.5: {} + deepmerge@4.3.1: {} defaults@1.0.4: @@ -7294,14 +7280,6 @@ snapshots: detect-libc@2.1.2: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -7330,11 +7308,6 @@ snapshots: pg: 8.20.0 postgres: 3.4.9 - drizzle-zod@0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9))(zod@4.3.6): - dependencies: - drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0)(postgres@3.4.9) - zod: 4.3.6 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -7492,66 +7465,84 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + + eslint-plugin-functional@10.0.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + deepmerge-ts: 7.1.5 + escape-string-regexp: 5.0.0 + eslint: 10.5.0(jiti@2.6.1) + is-immutable-type: 5.0.4(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) + ts-declaration-location: 1.0.7(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + eslint-plugin-no-loops@0.4.0(eslint@10.5.0(jiti@2.6.1)): + dependencies: + eslint: 10.5.0(jiti@2.6.1) + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 - eslint-scope@7.2.2: + eslint-scope@9.1.2: dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint@8.57.1: + eslint-visitor-keys@5.0.1: {} + + eslint@10.5.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.0 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 ajv: 6.14.0 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 - doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color - espree@9.6.1: + espree@11.2.0: dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.3 + eslint-visitor-keys: 5.0.1 esprima@4.0.1: {} @@ -7587,14 +7578,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-json-stringify@6.3.0: @@ -7656,9 +7639,9 @@ snapshots: fecha@4.2.3: {} - file-entry-cache@6.0.1: + file-entry-cache@8.0.0: dependencies: - flat-cache: 3.2.0 + flat-cache: 4.0.1 file-type@21.3.4: dependencies: @@ -7684,11 +7667,10 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@3.2.0: + flat-cache@4.0.1: dependencies: flatted: 3.4.2 keyv: 4.5.4 - rimraf: 3.0.2 flatted@3.4.2: {} @@ -7729,8 +7711,6 @@ snapshots: fs-monkey@1.1.0: {} - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true @@ -7770,10 +7750,6 @@ snapshots: - conventional-commits-filter - conventional-commits-parser - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -7786,38 +7762,14 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - global-directory@4.0.1: dependencies: ini: 4.1.1 - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - handlebars@4.7.9: dependencies: minimist: 1.2.8 @@ -7859,6 +7811,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7868,11 +7822,6 @@ snapshots: imurmurhash@0.1.4: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} ini@4.1.1: {} @@ -7907,14 +7856,22 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-immutable-type@5.0.4(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/type-utils': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.5.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + ts-declaration-location: 1.0.7(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + is-interactive@1.0.0: {} is-number@7.0.0: {} is-obj@2.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@4.1.0: {} is-standalone-pwa@0.1.1: {} @@ -8120,8 +8077,6 @@ snapshots: lodash.kebabcase@4.1.1: {} - lodash.merge@4.6.2: {} - lodash.mergewith@4.6.2: {} lodash.once@4.1.1: {} @@ -8188,8 +8143,6 @@ snapshots: merge-stream@2.0.0: {} - merge2@1.4.1: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8215,10 +8168,6 @@ snapshots: dependencies: brace-expansion: 1.1.13 - minimatch@9.0.3: - dependencies: - brace-expansion: 2.0.3 - minimist@1.2.8: {} minipass@7.1.3: {} @@ -8387,8 +8336,6 @@ snapshots: path-expression-matcher@1.5.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-scurry@2.0.2: @@ -8534,8 +8481,6 @@ snapshots: punycode@2.3.1: {} - queue-microtask@1.2.3: {} - quick-format-unescaped@4.0.4: {} readable-stream@2.3.8: @@ -8600,10 +8545,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rolldown@1.0.0-rc.15: dependencies: '@oxc-project/types': 0.124.0 @@ -8625,10 +8566,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - rxjs@7.8.1: dependencies: tslib: 2.8.1 @@ -8682,8 +8619,6 @@ snapshots: signal-exit@4.1.0: {} - slash@3.0.0: {} - slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -8764,8 +8699,6 @@ snapshots: strip-bom@3.0.0: {} - strip-json-comments@3.1.1: {} - strnum@2.2.3: {} strtok3@10.3.5: @@ -8812,8 +8745,6 @@ snapshots: text-hex@1.0.0: {} - text-table@0.2.0: {} - thread-stream@4.0.0: dependencies: real-require: 0.2.0 @@ -8858,8 +8789,13 @@ snapshots: triple-beam@1.4.1: {} - ts-api-utils@1.4.3(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-declaration-location@1.0.7(typescript@5.9.3): dependencies: + picomatch: 4.0.4 typescript: 5.9.3 ts-loader@9.5.7(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): @@ -8898,7 +8834,19 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.20.2: {} + type-fest@0.20.2: + optional: true + + typescript-eslint@8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.61.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.5.0(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color typescript@5.9.3: {} diff --git a/src/area/application/use-cases/areas/create.use-case.ts b/src/area/application/use-cases/areas/create.use-case.ts index 9aefea0f..4e702200 100644 --- a/src/area/application/use-cases/areas/create.use-case.ts +++ b/src/area/application/use-cases/areas/create.use-case.ts @@ -1,6 +1,6 @@ import { IAreaRepository } from '@core/area/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import type { CreateAreaDto } from '../../dtos'; +import { CreateAreaDto } from '../../dtos'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; import slugify from 'slugify'; import { BaseException } from '@shared/error'; diff --git a/src/area/application/use-cases/areas/get-one.query.ts b/src/area/application/use-cases/areas/get-one.query.ts index c715f6ea..cc883e27 100644 --- a/src/area/application/use-cases/areas/get-one.query.ts +++ b/src/area/application/use-cases/areas/get-one.query.ts @@ -4,7 +4,9 @@ import { ProjectAccessPolicy } from '@core/projects/domain/policy'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; -export type GetOneAreaParams = { projectSlug: string; key: string } | { key: string }; +export type GetOneAreaParams = + | { readonly projectSlug: string; readonly key: string } + | { readonly key: string }; @Injectable() export class GetAreaQuery { diff --git a/src/area/application/use-cases/areas/update.use-case.ts b/src/area/application/use-cases/areas/update.use-case.ts index 15479526..7182820e 100644 --- a/src/area/application/use-cases/areas/update.use-case.ts +++ b/src/area/application/use-cases/areas/update.use-case.ts @@ -37,25 +37,30 @@ export class UpdateAreaUseCase { const updateData: any = { updatedAt: new Date().toISOString(), updatedBy: userId, + ...(dto.title && dto.title !== area.title && { title: dto.title.trim() }), + ...(dto.description && + dto.description !== area.description && { + description: dto.description?.trim() || null, + }), + ...(dto.descriptionHtml && + dto.descriptionHtml !== area.descriptionHtml && { + descriptionHtml: dto.descriptionHtml?.trim() || null, + }), + ...(dto.color && dto.color !== area.color && { color: dto.color || null }), + ...(dto.icon && dto.icon !== area.icon && { icon: dto.icon || null }), + ...(dto.defaultView && + dto.defaultView !== area.defaultView && { defaultView: dto.defaultView }), + ...(dto.position && + dto.position !== area.position && + dto.position >= 0 && { position: dto.position }), + ...(dto.maxTasksLimit && + dto.maxTasksLimit !== area.maxTasksLimit && + dto.maxTasksLimit > 0 && { maxTasksLimit: dto.maxTasksLimit }), + ...(dto.isLocked && dto.isLocked !== area.isLocked && { isLocked: dto.isLocked }), }; let hasChanges = false; - if (dto.title && dto.title !== area.title) { - updateData.title = dto.title.trim(); - hasChanges = true; - } - - if (dto.description && dto.description !== area.description) { - updateData.description = dto.description?.trim() || null; - hasChanges = true; - } - - if (dto.descriptionHtml && dto.descriptionHtml !== area.descriptionHtml) { - updateData.descriptionHtml = dto.descriptionHtml?.trim() || null; - hasChanges = true; - } - if (dto.slug && dto.slug !== area.slug) { let newSlug = dto.slug; @@ -98,54 +103,6 @@ export class UpdateAreaUseCase { hasChanges = true; } - if (dto.color && dto.color !== area.color) { - updateData.color = dto.color || null; - hasChanges = true; - } - - if (dto.icon && dto.icon !== area.icon) { - updateData.icon = dto.icon || null; - hasChanges = true; - } - - if (dto.defaultView && dto.defaultView !== area.defaultView) { - updateData.defaultView = dto.defaultView; - hasChanges = true; - } - - if (dto.position && dto.position !== area.position) { - if (dto.position < 0) { - throw new BaseException( - { - code: AreaErrorCodes.POSITION_INVALID, - message: AreaErrorMessages[AreaErrorCodes.POSITION_INVALID], - }, - HttpStatus.BAD_REQUEST, - ); - } - updateData.position = dto.position; - hasChanges = true; - } - - if (dto.maxTasksLimit && dto.maxTasksLimit !== area.maxTasksLimit) { - if (dto.maxTasksLimit !== null && dto.maxTasksLimit <= 0) { - throw new BaseException( - { - code: AreaErrorCodes.MAX_TASKS_LIMIT_INVALID, - message: AreaErrorMessages[AreaErrorCodes.MAX_TASKS_LIMIT_INVALID], - }, - HttpStatus.BAD_REQUEST, - ); - } - updateData.maxTasksLimit = dto.maxTasksLimit; - hasChanges = true; - } - - if (dto.isLocked && dto.isLocked !== area.isLocked) { - updateData.isLocked = dto.isLocked; - hasChanges = true; - } - if (!hasChanges) { return { success: true, diff --git a/src/area/domain/entities/area.domain.ts b/src/area/domain/entities/area.domain.ts index cb00441c..07e61368 100644 --- a/src/area/domain/entities/area.domain.ts +++ b/src/area/domain/entities/area.domain.ts @@ -1,4 +1,4 @@ -import { areas } from '@core/area/infrastructure/persistence/models'; +import type { areas } from '@core/area/infrastructure/persistence/models'; import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; export type Area = InferSelectModel; diff --git a/src/area/domain/entities/state.domain.ts b/src/area/domain/entities/state.domain.ts index 7f953024..69438b3c 100644 --- a/src/area/domain/entities/state.domain.ts +++ b/src/area/domain/entities/state.domain.ts @@ -1,4 +1,4 @@ -import { states } from '@core/area/infrastructure/persistence/models'; +import type { states } from '@core/area/infrastructure/persistence/models'; import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; export type State = InferSelectModel; diff --git a/src/area/domain/repository/area.repository.interface.ts b/src/area/domain/repository/area.repository.interface.ts index dc21dc19..59f4c075 100644 --- a/src/area/domain/repository/area.repository.interface.ts +++ b/src/area/domain/repository/area.repository.interface.ts @@ -1,11 +1,11 @@ import type { Area, NewArea } from '../entities'; export interface IAreaRepository { - create(dto: NewArea): Promise<{ slug: string }>; + create(dto: NewArea): Promise<{ readonly slug: string }>; update(projectId: string, areaId: string, dto: Partial): Promise; delete(projectId: string, areaId: string): Promise; findOne(projectId: string, areaId: string, includeDeleted?: boolean): Promise; - findAll(projectId: string, includeDeleted?: boolean): Promise; + findAll(projectId: string, includeDeleted?: boolean): Promise; findBySlug(slug: string, projectId?: string): Promise; countByProject(projectId: string): Promise; diff --git a/src/area/domain/repository/states.repository.interface.ts b/src/area/domain/repository/states.repository.interface.ts index 798b9444..93e35a11 100644 --- a/src/area/domain/repository/states.repository.interface.ts +++ b/src/area/domain/repository/states.repository.interface.ts @@ -1,11 +1,11 @@ import type { NewState, State } from '../entities'; export interface IStateRepository { - create(dto: NewState): Promise<{ id: string }>; + create(dto: NewState): Promise<{ readonly id: string }>; update(areaId: string, stateId: string, dto: Partial): Promise; delete(areaId: string, stateId: string): Promise; findOne(areaId: string, stateId: string, deleted?: boolean): Promise; - find(areaId: string, query?: unknown): Promise; + find(areaId: string, query?: unknown): Promise; findByTitle(areaId: string, title: string): Promise; findByType( areaId: string, diff --git a/src/area/infrastructure/persistence/repositories/state.repository.ts b/src/area/infrastructure/persistence/repositories/state.repository.ts index 8bc1b530..2ba2d2f8 100644 --- a/src/area/infrastructure/persistence/repositories/state.repository.ts +++ b/src/area/infrastructure/persistence/repositories/state.repository.ts @@ -111,7 +111,7 @@ export class StateRepository implements IStateRepository { return result ?? null; } - public countByArea = async (areaId: string) => { + public readonly countByArea = async (areaId: string) => { const [result] = await this.db .select({ count: count() }) .from(schema.states) diff --git a/src/auth/application/auth.facade.ts b/src/auth/application/auth.facade.ts index 839ef46d..2d23fa8e 100644 --- a/src/auth/application/auth.facade.ts +++ b/src/auth/application/auth.facade.ts @@ -46,59 +46,59 @@ export class AuthFacade { private readonly resendCodeUseCase: ResendCodeUseCase, ) {} - async signIn(dto: SignInDto, device: DeviceMetadata) { + public async signIn(dto: SignInDto, device: DeviceMetadata) { return this.signInUseCase.execute(dto, device); } - async signUp(dto: SignUpDto) { + public async signUp(dto: SignUpDto) { return this.signUpUseCase.execute(dto); } - async resendCode(dto: ResendCodeDto) { + public async resendCode(dto: ResendCodeDto) { return this.resendCodeUseCase.execute(dto); } - async verifySignUp(dto: VerifyDto, device: DeviceMetadata) { + public async verifySignUp(dto: VerifyDto, device: DeviceMetadata) { return this.signUpVerifyUseCase.execute(dto, device); } - async signOut(token?: string) { + public async signOut(token?: string) { return this.signOutUseCase.execute(token); } - async refreshTokens(token: string | undefined, device: DeviceMetadata) { + public async refreshTokens(token: string | undefined, device: DeviceMetadata) { return this.refreshTokensUseCase.execute(token, device); } - async sendResetCode(dto: ResetPasswordDto) { + public async sendResetCode(dto: ResetPasswordDto) { return this.resetPasswordUseCase.execute(dto); } - async verifyResetCode(dto: VerifyResetCodeDto) { + public async verifyResetCode(dto: VerifyResetCodeDto) { return this.verifyResetPasswordUseCase.execute(dto); } - async confirmNewPassword(dto: PasswordResetConfirmDto) { + public async confirmNewPassword(dto: PasswordResetConfirmDto) { return this.confirmResetPasswordUseCase.execute(dto); } - async authenticateOAuth(dto: OAuthResponse, device: DeviceMetadata, state?: string) { + public async authenticateOAuth(dto: OAuthResponse, device: DeviceMetadata, state?: string) { return this.authenticateOAuthUseCase.execute(dto, device, state); } - async connectProvider(provider: string, userId: string) { + public async connectProvider(provider: string, userId: string) { return this.connectProviderUseCase.execute(provider, userId); } - async disconnectProvider(provider: string, userId: string) { + public async disconnectProvider(provider: string, userId: string) { return this.disconnectProviderUseCase.execute(provider, userId); } - async getConnectedProviders(userId: string) { + public async getConnectedProviders(userId: string) { return this.getConnectedProvidersQuery.execute(userId); } - async getEnabledProviders() { + public async getEnabledProviders() { return this.getEnabledProvidersQuery.execute(); } } diff --git a/src/auth/application/controller/auth/controller.ts b/src/auth/application/controller/auth/controller.ts index 5c7ffcf8..59155521 100644 --- a/src/auth/application/controller/auth/controller.ts +++ b/src/auth/application/controller/auth/controller.ts @@ -22,7 +22,7 @@ export class AuthController { constructor( private readonly facade: AuthFacade, - private cfg: ConfigService, + private readonly cfg: ConfigService, ) { this.isProduction = this.cfg.get('NODE_ENV') === 'production'; this.domain = this.cfg.get('DOMAIN'); diff --git a/src/auth/application/controller/oauth/controller.ts b/src/auth/application/controller/oauth/controller.ts index 35de0687..46bc7800 100644 --- a/src/auth/application/controller/oauth/controller.ts +++ b/src/auth/application/controller/oauth/controller.ts @@ -22,7 +22,7 @@ export class OAuthController { constructor( private readonly facade: AuthFacade, - private cfg: ConfigService, + private readonly cfg: ConfigService, ) { this.isProduction = this.cfg.get('NODE_ENV') === 'production'; this.domain = this.cfg.get('DOMAIN'); @@ -39,7 +39,7 @@ export class OAuthController { @UseGuards(OAuthGuard) @SkipContract() async oauthCallback( - @Query() query: { code?: string; state?: string }, + @Query() query: { readonly code?: string; readonly state?: string }, @Param('provider') provider: 'google' | 'yandex' | 'github' | 'vkontakte', @Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest, diff --git a/src/auth/application/interfaces/cache-data.interface.ts b/src/auth/application/interfaces/cache-data.interface.ts index 8ba2d183..4e89bc93 100644 --- a/src/auth/application/interfaces/cache-data.interface.ts +++ b/src/auth/application/interfaces/cache-data.interface.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/consistent-type-imports import { SignUpDto } from '@core/auth/application/dtos'; export interface SignUpCacheData { diff --git a/src/auth/application/strategies/index.ts b/src/auth/application/strategies/index.ts index d72156ac..17b7ffa3 100644 --- a/src/auth/application/strategies/index.ts +++ b/src/auth/application/strategies/index.ts @@ -1,6 +1,7 @@ +// eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ResendCodeDto } from '../dtos'; import { ResetPasswordResendStrategy } from './reset-password-resend.strategy'; -import { ResendCodeStrategy } from './resend-code.strategy'; +import type { ResendCodeStrategy } from './resend-code.strategy'; import { SignUpResendStrategy } from './sign-up-resend.strategy'; export const RESEND_CODE_STRATEGIES: Record = { diff --git a/src/auth/application/strategies/resend-code.strategy.ts b/src/auth/application/strategies/resend-code.strategy.ts index 41a69428..593f4bdd 100644 --- a/src/auth/application/strategies/resend-code.strategy.ts +++ b/src/auth/application/strategies/resend-code.strategy.ts @@ -1,11 +1,12 @@ -import { Queue } from 'bullmq'; +import type { Queue } from 'bullmq'; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ResendCodeDto } from '../dtos'; export abstract class ResendCodeStrategy { - abstract readonly context: ResendCodeDto['context']; - abstract readonly successMessage: string; - abstract readonly cacheNotFoundCode: string; - abstract readonly cacheNotFoundMessage: string; + abstract context: ResendCodeDto['context']; + abstract successMessage: string; + abstract cacheNotFoundCode: string; + abstract cacheNotFoundMessage: string; abstract getCacheKey(email: string): string; diff --git a/src/auth/application/strategies/reset-password-resend.strategy.ts b/src/auth/application/strategies/reset-password-resend.strategy.ts index 6d700d44..ab71a34a 100644 --- a/src/auth/application/strategies/reset-password-resend.strategy.ts +++ b/src/auth/application/strategies/reset-password-resend.strategy.ts @@ -1,11 +1,11 @@ import { AuthMailJobs } from '@core/auth/domain/enums'; import { ResetPasswordEvent } from '@core/auth/domain/events'; -import { ResetPasswordCacheData } from '@core/auth/application/interfaces'; +import type { ResetPasswordCacheData } from '@core/auth/application/interfaces'; import { EMAIL_CODE_TTL_SECONDS, RESET_PASSWORD_CACHE_KEY, } from '@core/auth/infrastructure/constants'; -import { Queue } from 'bullmq'; +import type { Queue } from 'bullmq'; import { generate, generateSecret } from 'otplib'; import { ResendCodeStrategy } from './resend-code.strategy'; @@ -20,7 +20,7 @@ export class ResetPasswordResendStrategy extends ResendCodeStrategy { + async generateOtp(): Promise<{ readonly token: string; readonly secret: string }> { const secret = generateSecret(); const token = await generate({ secret, diff --git a/src/auth/application/strategies/sign-up-resend.strategy.ts b/src/auth/application/strategies/sign-up-resend.strategy.ts index e614f862..1e323fa9 100644 --- a/src/auth/application/strategies/sign-up-resend.strategy.ts +++ b/src/auth/application/strategies/sign-up-resend.strategy.ts @@ -1,8 +1,8 @@ import { AuthMailJobs } from '@core/auth/domain/enums'; import { RegisterCodeEvent } from '@core/auth/domain/events'; -import { SignUpCacheData } from '@core/auth/application/interfaces'; +import type { SignUpCacheData } from '@core/auth/application/interfaces'; import { EMAIL_CODE_TTL_SECONDS, SIGNUP_CACHE_KEY } from '@core/auth/infrastructure/constants'; -import { Queue } from 'bullmq'; +import type { Queue } from 'bullmq'; import { generate, generateSecret } from 'otplib'; import { ResendCodeStrategy } from './resend-code.strategy'; @@ -16,7 +16,7 @@ export class SignUpResendStrategy extends ResendCodeStrategy { return SIGNUP_CACHE_KEY(email); } - async generateOtp(): Promise<{ token: string; secret: string }> { + async generateOtp(): Promise<{ readonly token: string; readonly secret: string }> { const secret = generateSecret(); const token = await generate({ secret, diff --git a/src/auth/application/use-cases/oauth/connect-provider.use-case.ts b/src/auth/application/use-cases/oauth/connect-provider.use-case.ts index 134e0a39..2ff7646e 100644 --- a/src/auth/application/use-cases/oauth/connect-provider.use-case.ts +++ b/src/auth/application/use-cases/oauth/connect-provider.use-case.ts @@ -93,22 +93,15 @@ export class ConnectProviderUseCase { const minutesLeft = Math.floor(timeLeft / 60); const secondsLeft = timeLeft % 60; - let timeMessage = ''; - if (minutesLeft > 0) { - timeMessage = `${minutesLeft} мин ${secondsLeft} сек`; - } else { - timeMessage = `${secondsLeft} сек`; - } + const timeMessage = + minutesLeft > 0 ? `${minutesLeft} мин ${secondsLeft} сек` : `${secondsLeft} сек`; const isSameProvider = activeSession.provider === newProvider; const providerName = this.getProviderName(activeSession.provider); - let message = ''; - if (isSameProvider) { - message = `У вас уже есть активный процесс авторизации через ${providerName}. Подождите ${timeMessage} или завершите его в другом окне.`; - } else { - message = `У вас уже есть активный процесс авторизации через ${providerName}. Дождитесь его завершения (${timeMessage}) или отмените, чтобы начать через ${this.getProviderName(newProvider)}.`; - } + const message = isSameProvider + ? `У вас уже есть активный процесс авторизации через ${providerName}. Подождите ${timeMessage} или завершите его в другом окне.` + : `У вас уже есть активный процесс авторизации через ${providerName}. Дождитесь его завершения (${timeMessage}) или отмените, чтобы начать через ${this.getProviderName(newProvider)}.`; throw new BaseException( { @@ -121,7 +114,6 @@ export class ConnectProviderUseCase { isSameProvider, timeLeftSeconds: timeLeft, expiresAt: activeSession.expiresAt, - stateCode: activeSession.stateCode, }, ], }, diff --git a/src/auth/application/use-cases/oauth/get-enabled-providers.query.ts b/src/auth/application/use-cases/oauth/get-enabled-providers.query.ts index 8957aeb9..111c6db9 100644 --- a/src/auth/application/use-cases/oauth/get-enabled-providers.query.ts +++ b/src/auth/application/use-cases/oauth/get-enabled-providers.query.ts @@ -7,24 +7,19 @@ export class GetEnabledProvidersQuery { constructor(private readonly cfg: ConfigService) {} async execute() { - const providers = []; - - if (this.cfg.get('GOOGLE_CLIENT_ID') && this.cfg.get('GOOGLE_CLIENT_SECRET')) { - providers.push(OAuthAssets.google); - } - - if (this.cfg.get('GITHUB_CLIENT_ID') && this.cfg.get('GITHUB_CLIENT_SECRET')) { - providers.push(OAuthAssets.github); - } - - if (this.cfg.get('YANDEX_CLIENT_ID') && this.cfg.get('YANDEX_CLIENT_SECRET')) { - providers.push(OAuthAssets.yandex); - } - - if (this.cfg.get('VKONTAKTE_CLIENT_ID') && this.cfg.get('VKONTAKTE_CLIENT_SECRET')) { - providers.push(OAuthAssets.yandex); - } - - return providers; + return [ + ...(this.cfg.get('GOOGLE_CLIENT_ID') && this.cfg.get('GOOGLE_CLIENT_SECRET') + ? [OAuthAssets.google] + : []), + ...(this.cfg.get('GITHUB_CLIENT_ID') && this.cfg.get('GITHUB_CLIENT_SECRET') + ? [OAuthAssets.github] + : []), + ...(this.cfg.get('YANDEX_CLIENT_ID') && this.cfg.get('YANDEX_CLIENT_SECRET') + ? [OAuthAssets.yandex] + : []), + ...(this.cfg.get('VKONTAKTE_CLIENT_ID') && this.cfg.get('VKONTAKTE_CLIENT_SECRET') + ? [OAuthAssets.vkontakte] + : []), + ]; } } diff --git a/src/auth/domain/events/create-user-workspace.event.ts b/src/auth/domain/events/create-user-workspace.event.ts index cad0e710..2b3b6bb7 100644 --- a/src/auth/domain/events/create-user-workspace.event.ts +++ b/src/auth/domain/events/create-user-workspace.event.ts @@ -1,6 +1,6 @@ export class CreateUserWorkspaceEvent { constructor( - public userId: string, - public username: string, + public readonly userId: string, + public readonly username: string, ) {} } diff --git a/src/auth/domain/events/register-code.event.ts b/src/auth/domain/events/register-code.event.ts index df87ca8b..c0f9cfe9 100644 --- a/src/auth/domain/events/register-code.event.ts +++ b/src/auth/domain/events/register-code.event.ts @@ -1,7 +1,7 @@ export class RegisterCodeEvent { constructor( - public email: string, - public name: string, - public otp: string, + public readonly email: string, + public readonly name: string, + public readonly otp: string, ) {} } diff --git a/src/auth/domain/events/reset-password.event.ts b/src/auth/domain/events/reset-password.event.ts index 1f50e09c..992b232e 100644 --- a/src/auth/domain/events/reset-password.event.ts +++ b/src/auth/domain/events/reset-password.event.ts @@ -1,6 +1,6 @@ export class ResetPasswordEvent { constructor( - public email: string, - public otp: string, + public readonly email: string, + public readonly otp: string, ) {} } diff --git a/src/auth/domain/repository/identity.repository.interface.ts b/src/auth/domain/repository/identity.repository.interface.ts index 55393944..5f4f1145 100644 --- a/src/auth/domain/repository/identity.repository.interface.ts +++ b/src/auth/domain/repository/identity.repository.interface.ts @@ -1,4 +1,4 @@ -import { userIdentities } from '../../infrastructure/persistence/models/identity.model'; +import type { userIdentities } from '../../infrastructure/persistence/models/identity.model'; export type IdentitiyInsert = typeof userIdentities.$inferInsert; export type IdentitiySelect = typeof userIdentities.$inferSelect; @@ -9,6 +9,6 @@ export interface IIdentityRepository { provider: 'google' | 'yandex' | 'github', providerUserId: string, ): Promise; - findAllByUserId(userId: string): Promise; + findAllByUserId(userId: string): Promise; delete(id: string): Promise; } diff --git a/src/auth/domain/repository/session.repository.interface.ts b/src/auth/domain/repository/session.repository.interface.ts index e83a682b..cd546290 100644 --- a/src/auth/domain/repository/session.repository.interface.ts +++ b/src/auth/domain/repository/session.repository.interface.ts @@ -1,4 +1,4 @@ -import { sessions } from '../../infrastructure/persistence/models/session.model'; +import type { sessions } from '../../infrastructure/persistence/models/session.model'; export type SessionInsert = typeof sessions.$inferInsert; export type SessionSelect = typeof sessions.$inferSelect; @@ -6,7 +6,7 @@ export type SessionSelect = typeof sessions.$inferSelect; export interface ISessionRepository { create(data: SessionInsert): Promise; findById(id: string): Promise; - findAllByUserId(userId: string): Promise; + findAllByUserId(userId: string): Promise; revoke(id: string): Promise; revokeAllByUserId(userId: string, exceptSessionId?: string): Promise; deleteExpired(): Promise; diff --git a/src/auth/infrastructure/persistence/repositories/identity.repository.ts b/src/auth/infrastructure/persistence/repositories/identity.repository.ts index 787f9efc..de56f08f 100644 --- a/src/auth/infrastructure/persistence/repositories/identity.repository.ts +++ b/src/auth/infrastructure/persistence/repositories/identity.repository.ts @@ -11,7 +11,7 @@ export class IdentitiyRepository implements IIdentityRepository { private readonly db: DatabaseService, ) {} - public create = async (data: typeof schema.userIdentities.$inferInsert) => { + public readonly create = async (data: typeof schema.userIdentities.$inferInsert) => { const [result] = await this.db.insert(schema.userIdentities).values(data).returning(); if (!result) { @@ -21,7 +21,7 @@ export class IdentitiyRepository implements IIdentityRepository { return result; }; - public delete = async (id: string) => { + public readonly delete = async (id: string) => { const result = await this.db .delete(schema.userIdentities) .where(eq(schema.userIdentities.id, id)); @@ -29,14 +29,14 @@ export class IdentitiyRepository implements IIdentityRepository { return result.count.valueOf() > 0; }; - public findAllByUserId = async (userId: string) => { + public readonly findAllByUserId = async (userId: string) => { return this.db .select() .from(schema.userIdentities) .where(eq(schema.userIdentities.userId, userId)); }; - public findByProvider = async ( + public readonly findByProvider = async ( provider: 'google' | 'yandex' | 'github', providerUserId: string, ) => { diff --git a/src/auth/infrastructure/strategies/github.strategy.ts b/src/auth/infrastructure/strategies/github.strategy.ts index ee7404db..e89e6a0e 100644 --- a/src/auth/infrastructure/strategies/github.strategy.ts +++ b/src/auth/infrastructure/strategies/github.strategy.ts @@ -4,12 +4,12 @@ import { PassportStrategy } from '@nestjs/passport'; import { Strategy, type Profile } from 'passport-github'; interface GitHubJsonProfile { - login: string; - id: number; - avatar_url: string; - name: string | null; - email: string | null; - bio: string | null; + readonly login: string; + readonly id: number; + readonly avatar_url: string; + readonly name: string | null; + readonly email: string | null; + readonly bio: string | null; } @Injectable() @@ -38,7 +38,7 @@ export class GithubStrategy extends PassportStrategy(Strategy, 'github-oauth') { _at: string, _rt: string, profile: Profile, - done: (...args: unknown[]) => void, + done: (...args: readonly unknown[]) => void, ) { const json = profile._json as unknown as GitHubJsonProfile; diff --git a/src/auth/infrastructure/strategies/vkontakte.strategy.ts b/src/auth/infrastructure/strategies/vkontakte.strategy.ts index cc4d39da..7874fb4d 100644 --- a/src/auth/infrastructure/strategies/vkontakte.strategy.ts +++ b/src/auth/infrastructure/strategies/vkontakte.strategy.ts @@ -7,77 +7,77 @@ import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; export interface IVKUserInfo { - id: number; - first_name: string; - last_name: string; - screen_name: string; - sex: 0 | 1 | 2; - photo_50?: string; - photo_100?: string; - photo_200?: string; - photo_200_orig?: string; - photo_400_orig?: string; - photo_max?: string; - photo_max_orig?: string; - city?: { id: number; title: string }; - country?: { id: number; title: string }; - bdate?: string; - about?: string; - activities?: string; - interests?: string; - music?: string; - movies?: string; - tv?: string; - books?: string; - games?: string; - status?: string; - online?: number; - domain?: string; - has_mobile?: number; - mobile_phone?: string; - home_phone?: string; - can_post?: number; - can_see_all_posts?: number; - can_see_audio?: number; - contacts?: { - mobile_phone?: string; - home_phone?: string; + readonly id: number; + readonly first_name: string; + readonly last_name: string; + readonly screen_name: string; + readonly sex: 0 | 1 | 2; + readonly photo_50?: string; + readonly photo_100?: string; + readonly photo_200?: string; + readonly photo_200_orig?: string; + readonly photo_400_orig?: string; + readonly photo_max?: string; + readonly photo_max_orig?: string; + readonly city?: { readonly id: number; readonly title: string }; + readonly country?: { readonly id: number; readonly title: string }; + readonly bdate?: string; + readonly about?: string; + readonly activities?: string; + readonly interests?: string; + readonly music?: string; + readonly movies?: string; + readonly tv?: string; + readonly books?: string; + readonly games?: string; + readonly status?: string; + readonly online?: number; + readonly domain?: string; + readonly has_mobile?: number; + readonly mobile_phone?: string; + readonly home_phone?: string; + readonly can_post?: number; + readonly can_see_all_posts?: number; + readonly can_see_audio?: number; + readonly contacts?: { + readonly mobile_phone?: string; + readonly home_phone?: string; }; - site?: string; - education?: { - university?: number; - university_name?: string; - faculty?: number; - faculty_name?: string; - graduation?: number; + readonly site?: string; + readonly education?: { + readonly university?: number; + readonly university_name?: string; + readonly faculty?: number; + readonly faculty_name?: string; + readonly graduation?: number; }; - universities?: Array<{ - id: number; - name: string; - faculty: number; - faculty_name: string; - graduation: number; + readonly universities?: ReadonlyArray<{ + readonly id: number; + readonly name: string; + readonly faculty: number; + readonly faculty_name: string; + readonly graduation: number; }>; } export interface IVKProfile { - provider: 'vkontakte'; - id: string; - displayName: string; - name: { - familyName: string; - givenName: string; + readonly provider: 'vkontakte'; + readonly id: string; + readonly displayName: string; + readonly name: { + readonly familyName: string; + readonly givenName: string; }; - gender: 'male' | 'female' | undefined; - emails?: Array<{ value: string }>; - photos: Array<{ value: string }>; - city?: string; - country?: string; - birthday?: string; - about?: string; - _raw: string; - _json: IVKUserInfo; - [key: string]: unknown; + readonly gender: 'male' | 'female' | undefined; + readonly emails?: ReadonlyArray<{ readonly value: string }>; + readonly photos: ReadonlyArray<{ readonly value: string }>; + readonly city?: string; + readonly country?: string; + readonly birthday?: string; + readonly about?: string; + readonly _raw: string; + readonly _json: IVKUserInfo; + readonly [key: string]: unknown; } @Injectable() @@ -116,7 +116,7 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau _at: never, _rt: never, profile: IVKProfile, - done: (...args: unknown[]) => void, + done: (...args: readonly unknown[]) => void, ) { const user = { id: profile.id, @@ -134,7 +134,7 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau done(null, user); } - private async getUserProfile(accessToken: string): Promise { + private async getUserProfile(accessToken: string) { try { const fields = [ 'uid', @@ -216,23 +216,20 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau } private parseProfile(json: IVKUserInfo): IVKProfile { - let gender: 'male' | 'female' | undefined; - if (json.sex === 2) gender = 'male'; - else if (json.sex === 1) gender = 'female'; + const gender: 'male' | 'female' | undefined = json.sex === 2 ? 'male' : 'female'; - const photos: Array<{ value: string }> = []; const photoSizes = ['photo_50', 'photo_100', 'photo_200', 'photo_400_orig', 'photo_max']; - for (const size of photoSizes) { + const photos = photoSizes.reduce<{ value: string }[]>((acc, size) => { const photoUrl = json[size as keyof IVKUserInfo]; if (photoUrl && typeof photoUrl === 'string') { - photos.push({ value: photoUrl }); + return [...acc, { value: photoUrl }]; } - } + return acc; + }, []); - if (photos.length === 0 && json.photo_max) { - photos.push({ value: json.photo_max }); - } + const finalPhotos = + photos.length === 0 && json.photo_max ? [...photos, { value: json.photo_max }] : photos; const profile: IVKProfile = { provider: 'vkontakte', @@ -244,33 +241,21 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau }, gender: gender, emails: [], - photos: photos, + photos: finalPhotos, _raw: JSON.stringify(json), _json: json, + ...(json.city?.title && { city: json.city.title }), + ...(json.country?.title && { country: json.country.title }), + ...(json.bdate && { birthday: json.bdate }), + ...(json.about && { about: json.about }), }; - if (json.city && json.city.title) { - profile.city = json.city.title; - } - - if (json.country && json.country.title) { - profile.country = json.country.title; - } - - if (json.bdate) { - profile.birthday = json.bdate; - } - - if (json.about) { - profile.about = json.about; - } - return profile; } override userProfile( accessToken: string, - done: (err?: Error | null, profile?: any) => void, + done: (err?: Error | null, profile?: unknown) => void, ): void { this.getUserProfile(accessToken) .then((profile) => done(null, profile)) @@ -282,10 +267,9 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau ): Record { const params: Record = {}; - if (options.display) { - params['display'] = options.display; - } - - return params; + return { + ...params, + ...(options.display && { display: options.display }), + }; } } diff --git a/src/auth/infrastructure/strategies/yandex.strategy.ts b/src/auth/infrastructure/strategies/yandex.strategy.ts index bab171af..593c8c5f 100644 --- a/src/auth/infrastructure/strategies/yandex.strategy.ts +++ b/src/auth/infrastructure/strategies/yandex.strategy.ts @@ -7,38 +7,38 @@ import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; export interface IUserInfo { - id: string; - login: string; - client_id: string; - display_name: string; - real_name: string; - first_name: string; - last_name: string; - sex: 'male' | 'female'; - default_email: string; - emails: string[]; - birthday: string; - default_avatar_id: string; - is_avatar_empty: false; - default_phone: { id: number; number: string }; - psuid: string; + readonly id: string; + readonly login: string; + readonly client_id: string; + readonly display_name: string; + readonly real_name: string; + readonly first_name: string; + readonly last_name: string; + readonly sex: 'male' | 'female'; + readonly default_email: string; + readonly emails: readonly string[]; + readonly birthday: string; + readonly default_avatar_id: string; + readonly is_avatar_empty: false; + readonly default_phone: { readonly id: number; readonly number: string }; + readonly psuid: string; } export interface IYandexProfile { - provider: 'yandex'; - id: string; - displayName: string; - username: string; - emails: [{ value: string }]; - name: { - familyName: string; - givenName: string; + readonly provider: 'yandex'; + readonly id: string; + readonly displayName: string; + readonly username: string; + readonly emails: readonly [{ readonly value: string }]; + readonly name: { + readonly familyName: string; + readonly givenName: string; }; - gender: 'female' | 'male' | undefined; - photos: [{ value: string }]; - _raw: string; - _json: IUserInfo; - [key: string]: unknown; + readonly gender: 'female' | 'male' | undefined; + readonly photos: readonly [{ readonly value: string }]; + readonly _raw: string; + readonly _json: IUserInfo; + readonly [key: string]: unknown; } @Injectable() @@ -72,7 +72,7 @@ export class YandexStrategy extends PassportStrategy(Strategy, 'yandex-oauth') { _at: string, _rt: string, profile: IYandexProfile, - done: (...args: unknown[]) => void, + done: (...args: readonly unknown[]) => void, ) { const json = profile._json; @@ -90,7 +90,7 @@ export class YandexStrategy extends PassportStrategy(Strategy, 'yandex-oauth') { done(null, user); } - private async getUserProfile(accessToken: string): Promise { + private async getUserProfile(accessToken: string) { try { const response = await firstValueFrom( this.http.get('https://login.yandex.ru/info', { @@ -142,7 +142,7 @@ export class YandexStrategy extends PassportStrategy(Strategy, 'yandex-oauth') { override userProfile( accessToken: string, - done: (err?: Error | null, profile?: any) => void, + done: (err?: Error | null, profile?: unknown) => void, ): void { this.getUserProfile(accessToken) .then((profile) => done(null, profile)) diff --git a/src/auth/infrastructure/utils/get-device-meta.ts b/src/auth/infrastructure/utils/get-device-meta.ts index b37f69e1..408a7a8e 100644 --- a/src/auth/infrastructure/utils/get-device-meta.ts +++ b/src/auth/infrastructure/utils/get-device-meta.ts @@ -2,11 +2,11 @@ import type { FastifyRequest } from 'fastify'; import { UAParser } from 'ua-parser-js'; export interface DeviceMetadata { - ip: string; - userAgent: string; - browser: string; - os: string; - deviceType: 'mobile' | 'desktop' | 'tablet'; + readonly ip: string; + readonly userAgent: string; + readonly browser: string; + readonly os: string; + readonly deviceType: 'mobile' | 'desktop' | 'tablet'; } export function getDeviceMeta(req: FastifyRequest): DeviceMetadata { diff --git a/src/auth/infrastructure/workers/mail.processor.ts b/src/auth/infrastructure/workers/mail.processor.ts index 3e4a926d..09e2b01d 100644 --- a/src/auth/infrastructure/workers/mail.processor.ts +++ b/src/auth/infrastructure/workers/mail.processor.ts @@ -46,7 +46,7 @@ export class MailProcessor extends WorkerHost { } } - private sendRegisterCode = async (job: Job) => { + private readonly sendRegisterCode = async (job: Job) => { const { email, name, otp } = job.data; await job.log(`Sending registration code to: ${email}`); @@ -58,7 +58,7 @@ export class MailProcessor extends WorkerHost { await job.updateProgress(100); }; - private sendResetPassCode = async (job: Job) => { + private readonly sendResetPassCode = async (job: Job) => { const { email, otp } = job.data; await job.log(`Sending password reset to: ${email}`); diff --git a/src/auth/infrastructure/workers/user.processor.ts b/src/auth/infrastructure/workers/user.processor.ts index 174eb126..cd615af1 100644 --- a/src/auth/infrastructure/workers/user.processor.ts +++ b/src/auth/infrastructure/workers/user.processor.ts @@ -38,7 +38,7 @@ export class UserProcessor extends WorkerHost { } } - private createWorkspace = async (job: Job) => { + private readonly createWorkspace = async (job: Job) => { const { userId, username } = job.data; await job.log(`Start creating a workspace for ${username}`); diff --git a/src/projects/application/mappers/member.mapper.ts b/src/projects/application/mappers/member.mapper.ts index 650ec077..06751fdb 100644 --- a/src/projects/application/mappers/member.mapper.ts +++ b/src/projects/application/mappers/member.mapper.ts @@ -16,7 +16,7 @@ export class MemberMapper { }; } - public static toMemberListResponse(members: MemberWithUser[]) { + public static toMemberListResponse(members: readonly MemberWithUser[]) { const items = members.map(MemberMapper.toMemberResponse); return { diff --git a/src/projects/application/project.facade.ts b/src/projects/application/project.facade.ts index 22d123ca..783889a3 100644 --- a/src/projects/application/project.facade.ts +++ b/src/projects/application/project.facade.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import type { ProjectStatus } from '../domain/entities'; import { CheckSlugAvailabilityQuery } from './use-cases/project/check-slug.use-case'; -import type { +import { AddProjectMemberDto, CreateProjectDto, CreateShareTokenDto, diff --git a/src/projects/application/use-cases/member/find-all.query.ts b/src/projects/application/use-cases/member/find-all.query.ts index 127dd670..ca2a416f 100644 --- a/src/projects/application/use-cases/member/find-all.query.ts +++ b/src/projects/application/use-cases/member/find-all.query.ts @@ -26,7 +26,7 @@ export class FindAllProjectMembersQuery { user: map.get(m.userId), })) .filter( - (item): item is typeof item & { user: NonNullable } => + (item): item is typeof item & { readonly user: NonNullable } => item.user !== undefined, ); diff --git a/src/projects/application/use-cases/project/create.use-case.ts b/src/projects/application/use-cases/project/create.use-case.ts index cc0dadf6..cad2ae1b 100644 --- a/src/projects/application/use-cases/project/create.use-case.ts +++ b/src/projects/application/use-cases/project/create.use-case.ts @@ -1,5 +1,5 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import type { CreateProjectDto } from '../../dtos'; +import { CreateProjectDto } from '../../dtos'; import { IProjectRepository } from '@core/projects/domain/repository'; import { PROJECT_STATUSES } from '@core/projects/domain/entities'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; diff --git a/src/projects/application/use-cases/project/find-one.query.ts b/src/projects/application/use-cases/project/find-one.query.ts index 220a4456..9e2693ae 100644 --- a/src/projects/application/use-cases/project/find-one.query.ts +++ b/src/projects/application/use-cases/project/find-one.query.ts @@ -43,7 +43,7 @@ export class FindProjectQuery { return this.findPrivate(project, teamId, userId, minRole); } - private findPrivate = async ( + private readonly findPrivate = async ( project: Project, teamId: string, userId?: string, @@ -91,7 +91,7 @@ export class FindProjectQuery { return { project, member, team }; }; - private findPublic = async (project: Project, token: string) => { + private readonly findPublic = async (project: Project, token: string) => { if (project.visibility !== 'public') { throw new BaseException( { code: 'PROJECT_NOT_PUBLIC', message: 'Публичный доступ к проекту ограничен' }, diff --git a/src/projects/application/use-cases/project/generate-share-token.use-case.ts b/src/projects/application/use-cases/project/generate-share-token.use-case.ts index c7e60dea..3e849110 100644 --- a/src/projects/application/use-cases/project/generate-share-token.use-case.ts +++ b/src/projects/application/use-cases/project/generate-share-token.use-case.ts @@ -1,5 +1,5 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import type { CreateShareTokenDto } from '../../dtos'; +import { CreateShareTokenDto } from '../../dtos'; import { createHash, randomBytes } from 'crypto'; import { BaseException } from '@shared/error'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; diff --git a/src/projects/application/use-cases/project/update.use-case.ts b/src/projects/application/use-cases/project/update.use-case.ts index 539ab571..d8261b0e 100644 --- a/src/projects/application/use-cases/project/update.use-case.ts +++ b/src/projects/application/use-cases/project/update.use-case.ts @@ -1,5 +1,5 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import type { UpdateProjectDto } from '../../dtos'; +import { UpdateProjectDto } from '../../dtos'; import { BaseException } from '@shared/error'; import { IProjectRepository } from '@core/projects/domain/repository'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; @@ -41,18 +41,18 @@ export class UpdateProjectUseCase { } } - const data: Record = {}; - - if (dto.slug) data['slug'] = slugify(dto.slug, { lower: true, strict: true }); - if (dto.name) data['name'] = dto.name.trim(); - if (dto.description !== undefined) data['description'] = dto.description?.trim() || null; - if (dto.descriptionHtml !== undefined) { - data['descriptionHtml'] = dto.descriptionHtml?.trim() || null; - } - if (dto.icon !== undefined) data['icon'] = dto.icon || null; - if (dto.color !== undefined) data['color'] = dto.color || null; - if (dto.sequence !== undefined) data['sequence'] = dto.sequence; - if (dto.visibility) data['visibility'] = dto.visibility; + const data = { + ...(dto.slug && { slug: slugify(dto.slug, { lower: true, strict: true }) }), + ...(dto.name && { name: dto.name.trim() }), + ...(dto.description !== undefined && { description: dto.description?.trim() || null }), + ...(dto.descriptionHtml !== undefined && { + descriptionHtml: dto.descriptionHtml?.trim() || null, + }), + ...(dto.icon !== undefined && { icon: dto.icon || null }), + ...(dto.color !== undefined && { color: dto.color || null }), + ...(dto.sequence !== undefined && { sequence: dto.sequence }), + ...(dto.visibility && { visibility: dto.visibility }), + }; if (Object.keys(data).length === 0 && !dto.settings) { return { @@ -73,17 +73,6 @@ export class UpdateProjectUseCase { ); } - if (!result) { - throw new BaseException( - { - code: 'UPDATE_FAILED', - message: - 'Изменения не были применены. Возможно, данные идентичны текущим или проект недоступен', - }, - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - // if (dto.settings) { // await this.settingsRepo.update(project.id, dto.settings); // } diff --git a/src/projects/domain/entities/member.domain.ts b/src/projects/domain/entities/member.domain.ts index 838a0c87..0d5c956e 100644 --- a/src/projects/domain/entities/member.domain.ts +++ b/src/projects/domain/entities/member.domain.ts @@ -1,5 +1,5 @@ import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; -import { projectMembers } from '@shared/entities'; +import type { projectMembers } from '@shared/entities'; export type Member = InferSelectModel; export type NewMember = InferInsertModel; diff --git a/src/projects/domain/entities/project.domain.ts b/src/projects/domain/entities/project.domain.ts index b28530af..3af75f44 100644 --- a/src/projects/domain/entities/project.domain.ts +++ b/src/projects/domain/entities/project.domain.ts @@ -1,5 +1,8 @@ import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; -import { projects, projectShares } from '../../infrastructure/persistence/models/project.model'; +import type { + projects, + projectShares, +} from '../../infrastructure/persistence/models/project.model'; export type Project = InferSelectModel; export type NewProject = InferInsertModel; diff --git a/src/projects/domain/policy/project-access.policy.ts b/src/projects/domain/policy/project-access.policy.ts index 409777ee..8d6cd2f1 100644 --- a/src/projects/domain/policy/project-access.policy.ts +++ b/src/projects/domain/policy/project-access.policy.ts @@ -64,7 +64,7 @@ export class ProjectAccessPolicy { public async ensureProjectAccess( slug: string, userId: string, - minRoles: MemberRole[] = ['viewer'], + minRoles: readonly MemberRole[] = ['viewer'], ) { const project = await this.projectRepo.findBySlug(slug); if (!project) { diff --git a/src/projects/domain/repository/member.repository.interface.ts b/src/projects/domain/repository/member.repository.interface.ts index 558197a5..bd3aae95 100644 --- a/src/projects/domain/repository/member.repository.interface.ts +++ b/src/projects/domain/repository/member.repository.interface.ts @@ -1,13 +1,13 @@ import type { Member, MemberRole, NewMember } from '../entities'; export interface IMemberRepository { - create(data: NewMember): Promise<{ id: string }>; + create(data: NewMember): Promise<{ readonly id: string }>; updateRole(memberId: string, role: MemberRole): Promise; delete(memberId: string): Promise; findById(memberId: string): Promise; findByProjectAndUser(projectId: string, userId: string): Promise; - findByProject(projectId: string): Promise; + findByProject(projectId: string): Promise; isMember(projectId: string, userId: string): Promise; getUserRole(projectId: string, userId: string): Promise; diff --git a/src/projects/domain/repository/project.repository.interface.ts b/src/projects/domain/repository/project.repository.interface.ts index 95c9d10d..319fa6b6 100644 --- a/src/projects/domain/repository/project.repository.interface.ts +++ b/src/projects/domain/repository/project.repository.interface.ts @@ -1,11 +1,14 @@ import type { NewProject, NewProjectShare, Project } from '../entities'; export interface IProjectRepository { - create(userId: string, data: NewProject): Promise<{ result: boolean; slug: string }>; + create( + userId: string, + data: NewProject, + ): Promise<{ readonly result: boolean; readonly slug: string }>; update(teamId: string, projectId: string, data: Partial): Promise; delete(teamId: string, projectId: string): Promise; findOne(projectId: string, teamId?: string): Promise; - findByTeam(teamId: string): Promise; + findByTeam(teamId: string): Promise; createShare(data: NewProjectShare): Promise; findBySlug(slug: string, teamId?: string): Promise; diff --git a/src/projects/infrastructure/persistence/repositories/member.repository.ts b/src/projects/infrastructure/persistence/repositories/member.repository.ts index 42ae2da8..266af47a 100644 --- a/src/projects/infrastructure/persistence/repositories/member.repository.ts +++ b/src/projects/infrastructure/persistence/repositories/member.repository.ts @@ -12,7 +12,7 @@ export class MemberRepository implements IMemberRepository { private readonly db: DatabaseService, ) {} - public create = async (data: typeof schema.projectMembers.$inferInsert) => { + public readonly create = async (data: typeof schema.projectMembers.$inferInsert) => { const [result] = await this.db .insert(schema.projectMembers) .values(data) @@ -25,7 +25,7 @@ export class MemberRepository implements IMemberRepository { return { id: result?.id }; }; - public findById = async (memberId: string) => { + public readonly findById = async (memberId: string) => { const [result] = await this.db .select() .from(schema.projectMembers) @@ -35,7 +35,7 @@ export class MemberRepository implements IMemberRepository { return result ?? null; }; - public findByProject = async (projectId: string) => { + public readonly findByProject = async (projectId: string) => { return this.db .select() .from(schema.projectMembers) @@ -58,7 +58,7 @@ export class MemberRepository implements IMemberRepository { return result !== undefined; } - public findByProjectAndUser = async (projectId: string, userId: string) => { + public readonly findByProjectAndUser = async (projectId: string, userId: string) => { const [result] = await this.db .select() .from(schema.projectMembers) @@ -72,7 +72,7 @@ export class MemberRepository implements IMemberRepository { return result || null; }; - public getUserRole = async (projectId: string, userId: string) => { + public readonly getUserRole = async (projectId: string, userId: string) => { const [result] = await this.db .select({ role: schema.projectMembers.role }) .from(schema.projectMembers) @@ -87,7 +87,7 @@ export class MemberRepository implements IMemberRepository { return (result?.role as MemberRole) ?? null; }; - public countByProject = async (projectId: string) => { + public readonly countByProject = async (projectId: string) => { const [result] = await this.db .select({ count: sql`count(*)` }) .from(schema.projectMembers) @@ -96,7 +96,7 @@ export class MemberRepository implements IMemberRepository { return result?.count ?? 0; }; - public updateRole = async (memberId: string, role: MemberRole) => { + public readonly updateRole = async (memberId: string, role: MemberRole) => { const [result] = await this.db .update(schema.projectMembers) .set({ role }) @@ -106,7 +106,7 @@ export class MemberRepository implements IMemberRepository { return result ?? null; }; - public delete = async (memberId: string) => { + public readonly delete = async (memberId: string) => { const [result] = await this.db .delete(schema.projectMembers) .where(eq(schema.projectMembers.id, memberId)) diff --git a/src/projects/infrastructure/persistence/repositories/project.repository.ts b/src/projects/infrastructure/persistence/repositories/project.repository.ts index f53a1602..dbe0280b 100644 --- a/src/projects/infrastructure/persistence/repositories/project.repository.ts +++ b/src/projects/infrastructure/persistence/repositories/project.repository.ts @@ -12,7 +12,7 @@ export class ProjectRepository implements IProjectRepository { private readonly db: DatabaseService, ) {} - public create = async (userId: string, data: NewProject) => { + public readonly create = async (userId: string, data: NewProject) => { const result = await this.db.transaction(async (tx) => { const project = await tx .insert(schema.projects) @@ -38,7 +38,11 @@ export class ProjectRepository implements IProjectRepository { return result; }; - public update = async (teamId: string, projectId: string, data: Partial) => { + public readonly update = async ( + teamId: string, + projectId: string, + data: Partial, + ) => { const result = await this.db .update(schema.projects) .set({ ...data, updatedAt: new Date().toISOString() }) @@ -54,7 +58,7 @@ export class ProjectRepository implements IProjectRepository { return result.length > 0; }; - public delete = async (teamId: string, projectId: string) => { + public readonly delete = async (teamId: string, projectId: string) => { const result = await this.db .update(schema.projects) .set({ @@ -74,7 +78,7 @@ export class ProjectRepository implements IProjectRepository { return result.length > 0; }; - public findOne = async (id: string, teamId?: string) => { + public readonly findOne = async (id: string, teamId?: string) => { const [project] = await this.db .select() .from(schema.projects) @@ -89,7 +93,7 @@ export class ProjectRepository implements IProjectRepository { return project || null; }; - public findBySlug = async (slug: string, teamId?: string) => { + public readonly findBySlug = async (slug: string, teamId?: string) => { const [project] = await this.db .select() .from(schema.projects) @@ -104,14 +108,14 @@ export class ProjectRepository implements IProjectRepository { return project || null; }; - public findByTeam = async (teamId: string) => { + public readonly findByTeam = async (teamId: string) => { return this.db .select() .from(schema.projects) .where(and(eq(schema.projects.teamId, teamId), isNull(schema.projects.deletedAt))); }; - public createShare = async (data: NewProjectShare) => { + public readonly createShare = async (data: NewProjectShare) => { const [result] = await this.db .insert(schema.projectShares) .values(data) @@ -127,7 +131,7 @@ export class ProjectRepository implements IProjectRepository { return !!result; }; - public hasValidShareToken = async (id: string, token: string) => { + public readonly hasValidShareToken = async (id: string, token: string) => { const [result] = await this.db .select() .from(schema.projectShares) @@ -146,7 +150,7 @@ export class ProjectRepository implements IProjectRepository { return !!result; }; - public revokeAllShares = async (projectId: string) => { + public readonly revokeAllShares = async (projectId: string) => { const result = await this.db .delete(schema.projectShares) .where(eq(schema.projectShares.projectId, projectId)) @@ -155,7 +159,7 @@ export class ProjectRepository implements IProjectRepository { return result.length > 0; }; - public countByTeam = async (teamId: string) => { + public readonly countByTeam = async (teamId: string) => { const [result] = await this.db .select({ count: count() }) .from(schema.projects) diff --git a/src/shared/adapters/cache/adapters/redis-cache.adapter.ts b/src/shared/adapters/cache/adapters/redis-cache.adapter.ts index 89f72cf6..2102fc0f 100644 --- a/src/shared/adapters/cache/adapters/redis-cache.adapter.ts +++ b/src/shared/adapters/cache/adapters/redis-cache.adapter.ts @@ -26,7 +26,10 @@ export class RedisCacheAdapter implements ICacheService { await this.redis.set(key, value, 'EX', ttlSeconds); } - async setMany(items: { key: string; value: string }[], ttlSeconds: number = this.defaultTtl) { + async setMany( + items: readonly { readonly key: string; readonly value: string }[], + ttlSeconds: number = this.defaultTtl, + ) { if (!items.length) return; const pipeline = this.redis.pipeline(); @@ -110,7 +113,10 @@ class RedisTransaction implements ICacheTransaction { return this; } - setMany(items: { key: string; value: string }[], ttlSeconds: number = this.defaultTtl): this { + setMany( + items: readonly { readonly key: string; readonly value: string }[], + ttlSeconds: number = this.defaultTtl, + ): this { for (const item of items) { this.multi.set(item.key, item.value, 'EX', ttlSeconds); } diff --git a/src/shared/adapters/cache/ports/static-cache.port.ts b/src/shared/adapters/cache/ports/static-cache.port.ts index e982fbba..b4da0840 100644 --- a/src/shared/adapters/cache/ports/static-cache.port.ts +++ b/src/shared/adapters/cache/ports/static-cache.port.ts @@ -1,22 +1,27 @@ export interface ICacheService { getOne(key: string): Promise; - getMany(keys: string[]): Promise<(string | null)[]>; - getCollection(collectionKey: string): Promise; + getMany(keys: readonly string[]): Promise; + getCollection(collectionKey: string): Promise; setOne(key: string, value: string, ttlSeconds?: number): Promise; - setMany(items: { key: string; value: string }[], ttlSeconds?: number): Promise; + setMany( + items: readonly { readonly key: string; readonly value: string }[], + ttlSeconds?: number, + ): Promise; addOneToCollection(key: string, value: string, ttlSeconds?: number): Promise; - addManyToCollection(key: string, values: string[], ttlSeconds?: number): Promise; + addManyToCollection(key: string, values: readonly string[], ttlSeconds?: number): Promise; removeOne(key: string): Promise; - removeMany(keys: string[]): Promise; + removeMany(keys: readonly string[]): Promise; removeOneFromCollection(key: string, value: string): Promise; - removeManyFromCollection(key: string, values: string[]): Promise; + removeManyFromCollection(key: string, values: readonly string[]): Promise; getTtl(key: string): Promise; - getOneWithTtl(key: string): Promise<{ value: string | null; ttlSeconds: number }>; + getOneWithTtl( + key: string, + ): Promise<{ readonly value: string | null; readonly ttlSeconds: number }>; transaction(): ICacheTransaction; @@ -25,16 +30,19 @@ export interface ICacheService { export interface ICacheTransaction { setOne(key: string, value: string, ttlSeconds?: number): this; - setMany(items: { key: string; value: string }[], ttlSeconds?: number): this; + setMany( + items: readonly { readonly key: string; readonly value: string }[], + ttlSeconds?: number, + ): this; addOneToCollection(key: string, value: string, ttlSeconds?: number): this; - addManyToCollection(key: string, values: string[], ttlSeconds?: number): this; + addManyToCollection(key: string, values: readonly string[], ttlSeconds?: number): this; removeOne(key: string): this; - removeMany(keys: string[]): this; + removeMany(keys: readonly string[]): this; removeOneFromCollection(key: string, value: string): this; - removeManyFromCollection(key: string, values: string[]): this; + removeManyFromCollection(key: string, values: readonly string[]): this; execute(): Promise; } diff --git a/src/shared/adapters/mail/adapter.ts b/src/shared/adapters/mail/adapter.ts index 6996131b..b34a19fc 100644 --- a/src/shared/adapters/mail/adapter.ts +++ b/src/shared/adapters/mail/adapter.ts @@ -8,9 +8,9 @@ import { IMailPort } from './port'; @Injectable() export class MailAdapter implements IMailPort { - private transporter: nodemailer.Transporter; + private readonly transporter: nodemailer.Transporter; - constructor(private cfg: ConfigService) { + constructor(private readonly cfg: ConfigService) { const port = this.cfg.get('MAIL_PORT'); const mode = this.cfg.get('NODE_ENV'); diff --git a/src/shared/error/exception.ts b/src/shared/error/exception.ts index 640645fa..20d9a1f0 100644 --- a/src/shared/error/exception.ts +++ b/src/shared/error/exception.ts @@ -1,14 +1,15 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; +import type { HttpStatus } from '@nestjs/common'; +import { HttpException } from '@nestjs/common'; interface IDetailsOptions { - target?: string; - [key: string]: any; + readonly target?: string; + readonly [key: string]: any; } export interface IErrorOptions { - code: string; - message: string; - details?: IDetailsOptions[]; + readonly code: string; + readonly message: string; + readonly details?: readonly IDetailsOptions[]; } export class BaseException extends HttpException { diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index 5e1fd965..5d7fcbeb 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -17,7 +17,7 @@ import { DATABASE_ERRORS } from './swagger'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(GlobalExceptionFilter.name); - private isDev = process.env['NODE_ENV'] === 'development'; + private readonly isDev = process.env['NODE_ENV'] === 'development'; catch(exception: unknown, host: ArgumentsHost) { if (exception instanceof ZodValidationException) { @@ -39,12 +39,15 @@ export class GlobalExceptionFilter implements ExceptionFilter { return this.handleUnknownError(exception, host); } - private parseZodValidation = async (exception: ZodValidationException, host: ArgumentsHost) => { + private readonly parseZodValidation = async ( + exception: ZodValidationException, + host: ArgumentsHost, + ) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); const zodError = exception.getZodError() as ZodError; - const issues: ZodIssue[] = zodError.issues || []; + const issues: readonly ZodIssue[] = zodError.issues || []; this.log(exception, host, status, { validationIssues: issues, @@ -61,7 +64,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private parseDatabase = async (exception: DrizzleQueryError, host: ArgumentsHost) => { + private readonly parseDatabase = async (exception: DrizzleQueryError, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const error = @@ -101,7 +104,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private parseHttp = async (exception: BaseException, host: ArgumentsHost) => { + private readonly parseHttp = async (exception: BaseException, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); @@ -123,7 +126,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private parseNestHttp = async (exception: HttpException, host: ArgumentsHost) => { + private readonly parseNestHttp = async (exception: HttpException, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); const res = exception.getResponse(); @@ -174,7 +177,13 @@ export class GlobalExceptionFilter implements ExceptionFilter { private formatErrorResponse( request: FastifyRequest, status: number, - data: { code: string; message: string; details: any[]; stack?: string; service?: string }, + data: { + readonly code: string; + readonly message: string; + readonly details: readonly any[]; + readonly stack?: string; + readonly service?: string; + }, ) { const requestId = request.id ?? request.headers['x-request-id']; diff --git a/src/shared/error/swagger.ts b/src/shared/error/swagger.ts index b3a8a47a..aec47cd8 100644 --- a/src/shared/error/swagger.ts +++ b/src/shared/error/swagger.ts @@ -6,7 +6,11 @@ export const ApiErrorResponse = ( status: number, bizCode: string, description: string, - details?: { field: string; message: string; code: string }[], + details?: readonly { + readonly field: string; + readonly message: string; + readonly code: string; + }[], ) => ApiResponse({ status, @@ -49,7 +53,7 @@ export const ApiNotFound = (description: string = 'Ресурс не найде export const ApiValidationError = ( description: string = 'Ошибка валидации входных данных', - fields: any[] = [], + fields: readonly any[] = [], ) => applyDecorators(ApiErrorResponse(400, 'VALIDATION_FAILED', description, fields)); export const ApiConflict = (description: string = 'Ресурс уже существует') => @@ -58,7 +62,7 @@ export const ApiConflict = (description: string = 'Ресурс уже суще export const ApiTooManyRequests = (description: string = 'Слишком много попыток') => applyDecorators(ApiErrorResponse(429, 'TOO_MANY_REQUESTS', description)); -export const DATABASE_ERRORS: Record = { +export const DATABASE_ERRORS: Record = { '23505': { code: 409, msg: 'Запись с таким значением уже существует (дубликат).' }, '23503': { code: 409, msg: 'Ошибка внешнего ключа: связанная запись не найдена.' }, '22P02': { code: 400, msg: 'Неверный формат данных (например, некорректный UUID).' }, diff --git a/src/shared/guards/bearer.guard.ts b/src/shared/guards/bearer.guard.ts index 25926b92..d5708abc 100644 --- a/src/shared/guards/bearer.guard.ts +++ b/src/shared/guards/bearer.guard.ts @@ -8,7 +8,7 @@ import type { FastifyRequest } from 'fastify'; @Injectable() export class BearerAuthGuard extends AuthGuard('bearer') { - constructor(private reflector: Reflector) { + constructor(private readonly reflector: Reflector) { super(); } @@ -51,7 +51,7 @@ export class BearerAuthGuard extends AuthGuard('bearer') { private isPublicOrHasToken(context: ExecutionContext): boolean { const { query } = context .switchToHttp() - .getRequest>(); + .getRequest>(); const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), diff --git a/src/shared/guards/oauth.guard.ts b/src/shared/guards/oauth.guard.ts index 0e806edf..bfef6ac7 100644 --- a/src/shared/guards/oauth.guard.ts +++ b/src/shared/guards/oauth.guard.ts @@ -28,16 +28,14 @@ export class OAuthGuard implements CanActivate { const GuardClass = this.guardClasses[provider]; - const passportOptions: Record = { session: false }; - - if (query) { - passportOptions['state'] = query; - } - - if (provider === 'google') { - passportOptions['accessType'] = 'offline'; - passportOptions['prompt'] = 'consent'; - } + const passportOptions: Record = { + session: false, + ...(query && { state: query }), + ...(provider === 'google' && { + accessType: 'offline', + prompt: 'consent', + }), + }; const targetGuard = new GuardClass(passportOptions); diff --git a/src/shared/interceptors/zod-validation.interceptor.ts b/src/shared/interceptors/zod-validation.interceptor.ts index dd8a8970..175efc6e 100644 --- a/src/shared/interceptors/zod-validation.interceptor.ts +++ b/src/shared/interceptors/zod-validation.interceptor.ts @@ -15,11 +15,11 @@ export const ZOD_RESPONSE_TOKEN = 'ZOD_RESPONSE_TOKEN'; @Injectable() export class ZodValidationInterceptor implements NestInterceptor { - constructor(private reflector: Reflector) {} + constructor(private readonly reflector: Reflector) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const handler = context.getHandler(); - const metadata = this.reflector.get<{ schema: z.ZodTypeAny } | undefined>( + const metadata = this.reflector.get<{ readonly schema: z.ZodTypeAny } | undefined>( ZOD_RESPONSE_TOKEN, handler, ); diff --git a/src/shared/media/decorators/extract-media-req.decorator.ts b/src/shared/media/decorators/extract-media-req.decorator.ts index fa935ad6..622b92d3 100644 --- a/src/shared/media/decorators/extract-media-req.decorator.ts +++ b/src/shared/media/decorators/extract-media-req.decorator.ts @@ -6,7 +6,9 @@ import { formatBytes } from '@shared/utils/format-bytes.util'; export const ExtractMediaReq = createParamDecorator( async ( - { allowedMimetypes = IMAGE_MIME_TYPES }: { allowedMimetypes?: string[] } = {}, + { + allowedMimetypes = IMAGE_MIME_TYPES, + }: { readonly allowedMimetypes?: readonly string[] } = {}, ctx: ExecutionContext, ) => { const maxFileSize = 5 * 1024 * 1024; @@ -48,7 +50,7 @@ export const ExtractMediaReq = createParamDecorator( const fields: Record = {}; - for (const key in file.fields) { + for (const key of Object.keys(file.fields)) { if (key === 'file') continue; const field = file.fields[key]; @@ -66,7 +68,7 @@ export const ExtractMediaReq = createParamDecorator( ...fields, }; } catch (e) { - const hasCode = (err: unknown): err is { code: string } => { + const hasCode = (err: unknown): err is { readonly code: string } => { return err !== null && typeof err === 'object' && 'code' in err; }; diff --git a/src/shared/media/media.service.ts b/src/shared/media/media.service.ts index 349fe0f0..21c7bf9e 100644 --- a/src/shared/media/media.service.ts +++ b/src/shared/media/media.service.ts @@ -1,6 +1,6 @@ import { HttpStatus, Injectable } from '@nestjs/common'; import { S3Service } from '@libs/s3'; -import type { UploadMediaDto } from './dtos'; +import { UploadMediaDto } from './dtos'; import { BaseException } from '@shared/error'; import { FlowProducer } from 'bullmq'; import { InjectFlowProducer } from '@nestjs/bullmq'; @@ -17,7 +17,7 @@ export class MediaService { private readonly s3: S3Service, ) {} - public upload = async (dto: UploadMediaDto, userId: string) => { + public readonly upload = async (dto: UploadMediaDto, userId: string) => { const { context, file } = dto; const strategy = this.getStrategy(context); diff --git a/src/shared/media/strategies/team-media.strategy.ts b/src/shared/media/strategies/team-media.strategy.ts index 79bc0506..4e380fb4 100644 --- a/src/shared/media/strategies/team-media.strategy.ts +++ b/src/shared/media/strategies/team-media.strategy.ts @@ -1,16 +1,22 @@ -import type { UploadMediaDto } from '../dtos'; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import { UploadMediaDto } from '../dtos'; import type { UpdateMediaTeam } from '../interfaces/media.interface'; import { MEDIA_JOBS } from '../media.constant'; -import { MediaDispatchStrategy } from './media.strategy'; +import type { MediaDispatchStrategy } from './media.strategy'; export class TeamMediaStrategy implements MediaDispatchStrategy { - jobName: string = MEDIA_JOBS.UPDATE_TEAM_MEDIA; + readonly jobName: string = MEDIA_JOBS.UPDATE_TEAM_MEDIA; createPayload(dto: UploadMediaDto, userId: string, path: string): UpdateMediaTeam { + const type = dto.context.split('.').pop(); + if (type !== 'avatar' && type !== 'banner') { + throw new Error(`Invalid media type: ${type}`); + } + return { entity: { type: 'team', id: dto.teamId! }, - type: dto.context.split('.').pop() as 'avatar' | 'banner', initiatorId: userId, + type, path, }; } diff --git a/src/shared/media/strategies/user-avatar.strategy.ts b/src/shared/media/strategies/user-avatar.strategy.ts index 20ccdc7f..ed1abc1c 100644 --- a/src/shared/media/strategies/user-avatar.strategy.ts +++ b/src/shared/media/strategies/user-avatar.strategy.ts @@ -1,10 +1,11 @@ +// eslint-disable-next-line no-restricted-syntax import type { UploadMediaDto } from '../dtos'; import type { UpdateMediaUser } from '../interfaces/media.interface'; import { MEDIA_JOBS } from '../media.constant'; -import { MediaDispatchStrategy } from './media.strategy'; +import type { MediaDispatchStrategy } from './media.strategy'; export class UserAvatarStrategy implements MediaDispatchStrategy { - jobName: string = MEDIA_JOBS.UPDATE_USER_AVATAR; + readonly jobName: string = MEDIA_JOBS.UPDATE_USER_AVATAR; createPayload(_d: UploadMediaDto, userId: string, path: string): UpdateMediaUser { return { diff --git a/src/shared/media/workers/media.worker.ts b/src/shared/media/workers/media.worker.ts index 144f046b..0cd8c4ec 100644 --- a/src/shared/media/workers/media.worker.ts +++ b/src/shared/media/workers/media.worker.ts @@ -14,7 +14,9 @@ export class MediaProcessor extends WorkerHost { super(); } - async process(job: Job<{ original: string; context: string; userId: string }>) { + async process( + job: Job<{ readonly original: string; readonly context: string; readonly userId: string }>, + ) { if (job.name !== MEDIA_JOBS.RESIZE_IMAGES) return; const { original: originalFilePath, context } = job.data; diff --git a/src/shared/schemas/sorting.schema.ts b/src/shared/schemas/sorting.schema.ts index 1c2f70af..216f1d5c 100644 --- a/src/shared/schemas/sorting.schema.ts +++ b/src/shared/schemas/sorting.schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod/v4'; -export const createSortingSchema = ( +export const createSortingSchema = ( fields: T, defaultField?: T[number], defaultOrder: 'asc' | 'desc' = 'asc', diff --git a/src/shared/types/jwt-payload.ts b/src/shared/types/jwt-payload.ts index c7886981..7cb5ddcb 100644 --- a/src/shared/types/jwt-payload.ts +++ b/src/shared/types/jwt-payload.ts @@ -1,8 +1,8 @@ export interface JwtPayload { - sub: string; - email: string; - role: string; - iss: string; - aud: string; - jti: string; + readonly sub: string; + readonly email: string; + readonly role: string; + readonly iss: string; + readonly aud: string; + readonly jti: string; } diff --git a/src/shared/utils/remove-undefined.util.ts b/src/shared/utils/remove-undefined.util.ts index 5f3e954f..0c226856 100644 --- a/src/shared/utils/remove-undefined.util.ts +++ b/src/shared/utils/remove-undefined.util.ts @@ -1,9 +1,5 @@ export function removeUndefined>(obj: T): Partial { - const result: Partial = {}; - for (const key in obj) { - if (obj[key] !== undefined) { - result[key] = obj[key]; - } - } - return result; + return Object.fromEntries( + Object.entries(obj).filter(([_, value]) => value !== undefined), + ) as Partial; } diff --git a/src/teams/application/controller/members/controller.ts b/src/teams/application/controller/members/controller.ts index b3e8ece2..f2a0276d 100644 --- a/src/teams/application/controller/members/controller.ts +++ b/src/teams/application/controller/members/controller.ts @@ -1,7 +1,7 @@ import { Body, Delete, Get, Param, Patch } from '@nestjs/common'; import { ApiBaseController, GetUserId } from '@shared/decorators'; import { GetMembersSwagger, RemoveMemberSwagger, UpdateMemberSwagger } from './swagger'; -import type { UpdateMemberDto } from '../../dtos/member.dto'; +import { UpdateMemberDto } from '../../dtos'; import { TeamsFacade } from '../../team.facade'; @ApiBaseController('teams/:teamId', 'Teams Members', true) diff --git a/src/teams/application/dtos/invitation.dto.ts b/src/teams/application/dtos/invitation.dto.ts index fe19a55c..f332fa59 100644 --- a/src/teams/application/dtos/invitation.dto.ts +++ b/src/teams/application/dtos/invitation.dto.ts @@ -1,6 +1,7 @@ import { z } from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; -import { roleEnum, TeamRole } from '../../infrastructure/persistence/models/enums'; +import type { TeamRole } from '../../infrastructure/persistence/models/enums'; +import { roleEnum } from '../../infrastructure/persistence/models/enums'; import { createPaginationSchema } from '@shared/schemas'; export const UpdateInvitationSchema = z.object({ @@ -41,13 +42,13 @@ export class TeamInvitationsResponse extends createZodDto( ) {} export interface TeamInvite { - teamId: string; - teamName: string; - teamAvatar: string | null; - email: string; - role: TeamRole; - inviterId: string; - inviterName: string; - createdAt: string; - expiresAt: string; + readonly teamId: string; + readonly teamName: string; + readonly teamAvatar: string | null; + readonly email: string; + readonly role: TeamRole; + readonly inviterId: string; + readonly inviterName: string; + readonly createdAt: string; + readonly expiresAt: string; } diff --git a/src/teams/application/mappers/member.mapper.ts b/src/teams/application/mappers/member.mapper.ts index 530bc0a3..bd8fc5ad 100644 --- a/src/teams/application/mappers/member.mapper.ts +++ b/src/teams/application/mappers/member.mapper.ts @@ -22,7 +22,7 @@ export class TeamMemberMapper { }; } - public static toList(rows: RawMemberRow[], cdn: string) { + public static toList(rows: readonly RawMemberRow[], cdn: string) { return rows.map((row) => this.toDetail(row, cdn)); } diff --git a/src/teams/application/team.facade.ts b/src/teams/application/team.facade.ts index e07445ef..6016e478 100644 --- a/src/teams/application/team.facade.ts +++ b/src/teams/application/team.facade.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import * as UC from './use-cases'; -import type { +import { CreateTeamDto, InviteMemberDto, UpdateInvitationDto, diff --git a/src/teams/application/use-cases/base/update-team.use-case.ts b/src/teams/application/use-cases/base/update-team.use-case.ts index 253e7972..84db12aa 100644 --- a/src/teams/application/use-cases/base/update-team.use-case.ts +++ b/src/teams/application/use-cases/base/update-team.use-case.ts @@ -1,6 +1,6 @@ import { Inject, Injectable, HttpStatus } from '@nestjs/common'; import { ITeamsRepository } from '../../../domain/repository'; -import type { UpdateTeamDto } from '../../dtos'; +import { UpdateTeamDto } from '../../dtos'; import { BaseException } from '@shared/error'; @Injectable() diff --git a/src/teams/application/use-cases/invitions/send-invitation.use-case.ts b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts index 07588df9..1554fb7d 100644 --- a/src/teams/application/use-cases/invitions/send-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts @@ -132,7 +132,7 @@ export class SendInvitationUseCase { } private async sendEmailNotification(code: string, teamName: string, email: string) { - const origins = this.cfg.get('CORS_ALLOWED_ORIGINS') || []; + const origins = this.cfg.get('CORS_ALLOWED_ORIGINS') || []; const url = `${origins[0]}/invites/accept?code=${code}`; const event = new TeamInvitationEvent(email, teamName, url); diff --git a/src/teams/application/use-cases/invitions/update-invitation.use-case.ts b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts index 8ccceb03..6c6b6da6 100644 --- a/src/teams/application/use-cases/invitions/update-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts @@ -4,9 +4,9 @@ import { UpdateInvitationDto } from '../../dtos'; import { BaseException } from '@shared/error'; import { TeamInvite } from '../../dtos/invitation.dto'; import { TeamMemberPolicy } from '@core/teams/domain/policy'; -import { TeamRole } from '@shared/entities'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import type { TeamRole } from '../../../infrastructure/persistence/models'; @Injectable() export class UpdateInvitationUseCase { @@ -28,8 +28,12 @@ export class UpdateInvitationUseCase { this.validateInviteOwnership(invite, team.id); this.validatePolicy(member.role as TeamRole, invite.role as TeamRole, dto.role as TeamRole); - invite.role = dto.role as TeamRole; - await this.cacheService.setOne(key, JSON.stringify(invite), ttlSeconds); + const updatedInvite = { + ...invite, + role: dto.role as TeamRole, + }; + + await this.cacheService.setOne(key, JSON.stringify(updatedInvite), ttlSeconds); return { success: true, diff --git a/src/teams/domain/entities/teams.domain.ts b/src/teams/domain/entities/teams.domain.ts index e4d97cb3..1c8dcd38 100644 --- a/src/teams/domain/entities/teams.domain.ts +++ b/src/teams/domain/entities/teams.domain.ts @@ -1,5 +1,5 @@ import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; -import { teams, teamMembers } from '../../infrastructure/persistence/models'; +import type { teams, teamMembers } from '../../infrastructure/persistence/models'; export type Team = InferSelectModel; export type NewTeam = InferInsertModel; @@ -8,5 +8,5 @@ export type TeamMember = InferSelectModel; export type NewTeamMember = InferInsertModel; export type TeamWithMembers = Team & { - members: TeamMember[]; + readonly members: readonly TeamMember[]; }; diff --git a/src/teams/domain/events/team-invitation.event.ts b/src/teams/domain/events/team-invitation.event.ts index 5dc9d67f..aeac2d83 100644 --- a/src/teams/domain/events/team-invitation.event.ts +++ b/src/teams/domain/events/team-invitation.event.ts @@ -1,7 +1,7 @@ export class TeamInvitationEvent { constructor( - public email: string, - public teamName: string, - public inviteUrl: string, + public readonly email: string, + public readonly teamName: string, + public readonly inviteUrl: string, ) {} } diff --git a/src/teams/domain/repository/teams.repository.interface.ts b/src/teams/domain/repository/teams.repository.interface.ts index c4bf29bc..b7dee49e 100644 --- a/src/teams/domain/repository/teams.repository.interface.ts +++ b/src/teams/domain/repository/teams.repository.interface.ts @@ -1,26 +1,26 @@ import type { Team, NewTeam, NewTeamMember } from '../entities'; -type TResponse = { success: boolean; teamId: string }; +type TResponse = { readonly success: boolean; readonly teamId: string }; export type RawMemberRow = { - userId: string; - role: string; - status: string; - joinedAt: string | null; - firstName: string | null; - lastName: string | null; - middleName: string | null; - avatarUrl: string | null; - email?: string; + readonly userId: string; + readonly role: string; + readonly status: string; + readonly joinedAt: string | null; + readonly firstName: string | null; + readonly lastName: string | null; + readonly middleName: string | null; + readonly avatarUrl: string | null; + readonly email?: string; }; export type RawMemberTeams = { - id: string; - name: string; - description: string | null; - avatarUrl: string | null; - role: string; - joinedAt: string | null; + readonly id: string; + readonly name: string; + readonly description: string | null; + readonly avatarUrl: string | null; + readonly role: string; + readonly joinedAt: string | null; }; export interface ITeamsRepository { @@ -29,13 +29,13 @@ export interface ITeamsRepository { remove(id: string, userId: string): Promise; findMember(teamId: string, userId: string): Promise; - findMembers(teamId: string): Promise; + findMembers(teamId: string): Promise; findById(teamId: string): Promise; findByUser( userId: string, // TODO: ADD ZOD QUERY - pagination: { search?: string; limit?: number; offset?: number }, - ): Promise; + pagination: { readonly search?: string; readonly limit?: number; readonly offset?: number }, + ): Promise; updateTeamAvatar(teamId: string, url: string): Promise; updateTeamBanner(teamId: string, url: string): Promise; @@ -44,7 +44,7 @@ export interface ITeamsRepository { updateMember( teamId: string, userId: string, - dto: { role?: string; status?: string }, + dto: { readonly role?: string; readonly status?: string }, ): Promise; removeMember(teamId: string, userId: string): Promise; } diff --git a/src/teams/infrastructure/persistence/repositories/teams.repository.ts b/src/teams/infrastructure/persistence/repositories/teams.repository.ts index a2582b50..ac4059be 100644 --- a/src/teams/infrastructure/persistence/repositories/teams.repository.ts +++ b/src/teams/infrastructure/persistence/repositories/teams.repository.ts @@ -12,7 +12,7 @@ export class TeamsRepository implements ITeamsRepository { private readonly db: DatabaseService, ) {} - public addMember = async (dto: NewTeamMember) => { + public readonly addMember = async (dto: NewTeamMember) => { const result = await this.db .insert(schema.teamMembers) .values(dto) @@ -23,7 +23,7 @@ export class TeamsRepository implements ITeamsRepository { return (result?.count ?? 0) > 0; }; - public create = async (ownerId: string, dto: NewTeam) => { + public readonly create = async (ownerId: string, dto: NewTeam) => { return this.db.transaction(async (tx) => { const [team] = await tx .insert(schema.teams) @@ -49,7 +49,7 @@ export class TeamsRepository implements ITeamsRepository { }); }; - public update = async (id: string, dto: Partial) => { + public readonly update = async (id: string, dto: Partial) => { return this.db.transaction(async (tx) => { const [team] = await tx .update(schema.teams) @@ -68,7 +68,7 @@ export class TeamsRepository implements ITeamsRepository { }); }; - public remove = async (teamId: string, userId: string) => { + public readonly remove = async (teamId: string, userId: string) => { const result = await this.db .update(schema.teams) .set({ @@ -79,7 +79,7 @@ export class TeamsRepository implements ITeamsRepository { return (result?.count ?? 0) > 0; }; - public findMember = async (teamId: string, userId: string) => { + public readonly findMember = async (teamId: string, userId: string) => { const [member] = await this.membersQuery.where( and(eq(schema.teamMembers.teamId, teamId), eq(schema.teamMembers.userId, userId)), ); @@ -87,15 +87,15 @@ export class TeamsRepository implements ITeamsRepository { return member || null; }; - public findMembers = async (teamId: string) => { + public readonly findMembers = async (teamId: string) => { return this.membersQuery .where(eq(schema.teamMembers.teamId, teamId)) .orderBy(desc(schema.teamMembers.joinedAt)); }; - public findByUser = async ( + public readonly findByUser = async ( userId: string, - pagination: { search?: string; limit?: number; offset?: number }, + pagination: { readonly search?: string; readonly limit?: number; readonly offset?: number }, ) => { const { search, limit = 10, offset = 0 } = pagination; @@ -128,13 +128,13 @@ export class TeamsRepository implements ITeamsRepository { return query; }; - public findById = async (teamId: string) => { + public readonly findById = async (teamId: string) => { const [team] = await this.db.select().from(schema.teams).where(eq(schema.teams.id, teamId)); if (!team) return null; return team; }; - public removeMember = async (teamId: string, userId: string) => { + public readonly removeMember = async (teamId: string, userId: string) => { const result = await this.db .delete(schema.teamMembers) .where( @@ -144,7 +144,11 @@ export class TeamsRepository implements ITeamsRepository { return (result?.count ?? 0) > 0; }; - public updateMember = async (teamId: string, userId: string, dto: Partial) => { + public readonly updateMember = async ( + teamId: string, + userId: string, + dto: Partial, + ) => { const { role, status } = dto; const data = { diff --git a/src/teams/infrastructure/workers/mail.processor.ts b/src/teams/infrastructure/workers/mail.processor.ts index 34320b92..d2be68fe 100644 --- a/src/teams/infrastructure/workers/mail.processor.ts +++ b/src/teams/infrastructure/workers/mail.processor.ts @@ -35,7 +35,7 @@ export class MailProcessor extends WorkerHost { } } - private sendTeamInvitation = async (job: Job) => { + private readonly sendTeamInvitation = async (job: Job) => { const { email, teamName, inviteUrl } = job.data; await job.log(`Sending team(${teamName}) invitation link to: ${email}`); diff --git a/src/user/application/use-cases/find-by-ids.query.ts b/src/user/application/use-cases/find-by-ids.query.ts index 74cbf0c2..5dc1dcf5 100644 --- a/src/user/application/use-cases/find-by-ids.query.ts +++ b/src/user/application/use-cases/find-by-ids.query.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; export class FindByIdsQuery { constructor(@Inject('IUserRepository') private readonly userRepo: IUserRepository) {} - async execute(ids: string[]) { + async execute(ids: readonly string[]) { return this.userRepo.findByIds(ids); } } diff --git a/src/user/application/use-cases/find-user.query.ts b/src/user/application/use-cases/find-user.query.ts index a83ec657..e820e178 100644 --- a/src/user/application/use-cases/find-user.query.ts +++ b/src/user/application/use-cases/find-user.query.ts @@ -9,7 +9,7 @@ export class FindUserQuery { private readonly repository: IUserRepository, ) {} - async execute(params: { email?: string; id?: string }) { + async execute(params: { readonly email?: string; readonly id?: string }) { if (params.email) return this.repository.findByEmail(params.email); if (params.id) return this.repository.findById(params.id); diff --git a/src/user/application/use-cases/register-user.use-case.ts b/src/user/application/use-cases/register-user.use-case.ts index 8b5e6461..50aa9664 100644 --- a/src/user/application/use-cases/register-user.use-case.ts +++ b/src/user/application/use-cases/register-user.use-case.ts @@ -11,7 +11,7 @@ export class RegisterUserUseCase { private readonly repository: IUserRepository, ) {} - async execute(dto: NewUser & { password: string | null }) { + async execute(dto: NewUser & { readonly password: string | null }) { const existingUser = await this.repository.findByEmail(dto.email); if (existingUser?.user) { diff --git a/src/user/domain/entities/user.domain.ts b/src/user/domain/entities/user.domain.ts index ad6c9e18..4b8b40da 100644 --- a/src/user/domain/entities/user.domain.ts +++ b/src/user/domain/entities/user.domain.ts @@ -1,5 +1,5 @@ import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; -import { +import type { users, userSecurity, userNotifications, @@ -23,18 +23,18 @@ export type UserActivity = InferSelectModel; export type NewUserActivity = InferInsertModel; export type UserProfile = { - user: User; - security: { - lastPasswordChange: string | null; - is2faEnabled: boolean; + readonly user: User; + readonly security: { + readonly lastPasswordChange: string | null; + readonly is2faEnabled: boolean; }; - preferences: UserPreferences | null; - notifications: NotificationSettings; + readonly preferences: UserPreferences | null; + readonly notifications: NotificationSettings; }; export type UserWithSecurity = { - user: User; - security: { - passwordHash: string | null; + readonly user: User; + readonly security: { + readonly passwordHash: string | null; }; }; diff --git a/src/user/domain/repository/user.repository.interface.ts b/src/user/domain/repository/user.repository.interface.ts index 333f16b8..c27abc5e 100644 --- a/src/user/domain/repository/user.repository.interface.ts +++ b/src/user/domain/repository/user.repository.interface.ts @@ -9,22 +9,20 @@ import type { UserWithSecurity, } from '../entities'; -type DeepPartial = { - [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; -}; +type DeepPartial = { readonly [P in keyof T]?: T[P] extends object ? DeepPartial : T[P] }; export interface IUserRepository { create(data: NewUser): Promise; findById(id: string): Promise; - findByIds(ids: string[]): Promise; + findByIds(ids: readonly string[]): Promise; findByEmail(email: string): Promise; findProfile(id: string): Promise; findActivityByUser( userId: string, - options: { limit: number; offset: number }, + options: { readonly limit: number; readonly offset: number }, ): Promise<{ - items: UserActivity[]; - total: number; + readonly items: readonly UserActivity[]; + readonly total: number; }>; updateAvatar(id: string, url: string): Promise; updateProfile( diff --git a/src/user/infrastructure/persistence/models/user.entity.ts b/src/user/infrastructure/persistence/models/user.entity.ts index 6ba5e3f7..7f918a62 100644 --- a/src/user/infrastructure/persistence/models/user.entity.ts +++ b/src/user/infrastructure/persistence/models/user.entity.ts @@ -67,8 +67,12 @@ export const userNotifications = baseSchema.table('user_notifications', { .references(() => users.id, { onDelete: 'cascade' }), settings: jsonb('settings') .$type<{ - email: { task_assigned: boolean; mentions: boolean; daily_summary: boolean }; - push: { task_assigned: boolean; reminders: boolean }; + readonly email: { + readonly task_assigned: boolean; + readonly mentions: boolean; + readonly daily_summary: boolean; + }; + readonly push: { readonly task_assigned: boolean; readonly reminders: boolean }; }>() .default({ email: { task_assigned: true, mentions: true, daily_summary: false }, diff --git a/src/user/infrastructure/persistence/repositories/user.repository.ts b/src/user/infrastructure/persistence/repositories/user.repository.ts index 3f5a1966..a35c034a 100644 --- a/src/user/infrastructure/persistence/repositories/user.repository.ts +++ b/src/user/infrastructure/persistence/repositories/user.repository.ts @@ -27,7 +27,7 @@ export class UserRepository implements IUserRepository { .leftJoin(sc.userNotifications, eq(sc.users.id, sc.userNotifications.userId)); } - public findProfile = async (id: string) => { + public readonly findProfile = async (id: string) => { const [rows] = await this.fullUserQuery .leftJoin(sc.userPreferences, eq(sc.users.id, sc.userPreferences.userId)) .where(eq(sc.users.id, id)); @@ -61,13 +61,13 @@ export class UserRepository implements IUserRepository { }; }; - public findByIds = async (ids: string[]) => { + public readonly findByIds = async (ids: readonly string[]) => { if (ids.length === 0) return []; return this.db.select().from(sc.users).where(inArray(sc.users.id, ids)); }; - public findById = async (id: string) => { + public readonly findById = async (id: string) => { const [row] = await this.fullUserQuery.where(eq(sc.users.id, id)); if (!row || !row.user_security) return null; return { @@ -78,7 +78,7 @@ export class UserRepository implements IUserRepository { }; }; - public findByEmail = async (email: string) => { + public readonly findByEmail = async (email: string) => { const [row] = await this.fullUserQuery.where(eq(sc.users.email, email.toLowerCase())); if (!row || !row.user_security) return null; return { @@ -89,7 +89,7 @@ export class UserRepository implements IUserRepository { }; }; - public findSecurityByUserId = async (userId: string) => { + public readonly findSecurityByUserId = async (userId: string) => { const [result] = await this.db .select() .from(sc.userSecurity) @@ -97,7 +97,7 @@ export class UserRepository implements IUserRepository { return result || null; }; - public create = async (data: NewUser) => { + public readonly create = async (data: NewUser) => { return this.db.transaction(async (tx) => { const [newUser] = await tx.insert(sc.users).values(data).returning(); @@ -113,7 +113,7 @@ export class UserRepository implements IUserRepository { }); }; - public updateProfile = async ( + public readonly updateProfile = async ( id: string, user: Partial, preferences?: Partial, @@ -200,7 +200,10 @@ export class UserRepository implements IUserRepository { return (result?.count ?? 0) > 0; } - async findActivityByUser(userId: string, options: { limit: number; offset: number }) { + async findActivityByUser( + userId: string, + options: { readonly limit: number; readonly offset: number }, + ) { const [totalResult, items] = await Promise.all([ this.db .select({ value: count() }) diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 733a1f5e..b11d3937 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,4 +1,4 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import { Test, type TestingModule } from '@nestjs/testing'; import { AppModule } from '../src/app.module'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; diff --git a/tsconfig.json b/tsconfig.json index 9fa039b4..d175afda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "ES2021", + "target": "ES2022", "resolveJsonModule": true, "esModuleInterop": true, "sourceMap": true, @@ -31,6 +31,7 @@ "noImplicitOverride": true, "exactOptionalPropertyTypes": false, "forceConsistentCasingInFileNames": true, + "lib": ["ES2022"], "paths": { "@libs/bootstrap": ["./libs/bootstrap/src"], "@libs/bootstrap/*": ["./libs/bootstrap/src/*"], From b50cefde5c099e749502a76faaf4abcce3ec990a Mon Sep 17 00:00:00 2001 From: soorq Date: Mon, 15 Jun 2026 15:37:01 +0300 Subject: [PATCH 2/3] feat: eslint rules --- eslint.config.mjs | 67 ++++++++ package.json | 6 +- pnpm-lock.yaml | 379 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 444 insertions(+), 8 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index b9b74d46..eb0455f2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,11 @@ // @ts-check import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; +import pluginJsdoc from 'eslint-plugin-jsdoc'; +import pluginPerfectionist from 'eslint-plugin-perfectionist'; import functional from 'eslint-plugin-functional'; +import pluginUnicorn from 'eslint-plugin-unicorn'; +import pluginSonarjs from 'eslint-plugin-sonarjs'; export default tseslint.config( { @@ -12,6 +16,10 @@ export default tseslint.config( { plugins: { functional, + perfectionist: pluginPerfectionist, + unicorn: pluginUnicorn, + sonarjs: pluginSonarjs, + jsdoc: pluginJsdoc, }, languageOptions: { parserOptions: { @@ -48,6 +56,65 @@ export default tseslint.config( 'functional/no-let': 'off', 'functional/no-expression-statements': 'off', + 'perfectionist/sort-imports': [ + 'error', + { + type: 'natural', + order: 'asc', + groups: [ + 'builtin', // node:fs + 'external', // @nestjs, rxjs + 'internal', // @core, @shared + 'parent', // ../ + 'sibling', // ./ + 'index', // index.ts + 'type', // type imports + ], + }, + ], + 'no-duplicate-imports': 'error', + + 'unicorn/filename-case': [ + 'error', + { + case: 'kebabCase', + ignore: ['index.ts', '\\.d\\.ts$'], + }, + ], + 'unicorn/prefer-node-protocol': 'error', + 'unicorn/no-array-method-this-argument': 'warn', + 'unicorn/prefer-structured-clone': 'error', + 'unicorn/no-useless-undefined': 'error', + 'unicorn/prefer-export-from': 'error', + 'unicorn/prefer-spread': 'warn', + 'unicorn/no-array-reduce': 'warn', + 'unicorn/no-array-push-push': 'warn', + + 'sonarjs/cognitive-complexity': ['error', 15], + 'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }], + 'sonarjs/no-identical-functions': 'error', + 'sonarjs/no-collapsible-if': 'error', + 'sonarjs/no-unused-collection': 'error', + + 'jsdoc/require-description': ['warn', { descriptionStyle: 'body' }], + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-types': 'error', + 'jsdoc/require-param-type': 'error', + 'jsdoc/require-returns-type': 'error', + + eqeqeq: ['error', 'always'], + curly: ['error', 'all'], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'no-return-await': 'error', + 'require-await': 'error', + 'no-var': 'error', + 'prefer-const': 'error', + 'prefer-template': 'error', + 'object-shorthand': 'error', + 'arrow-body-style': ['error', 'as-needed'], + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + 'no-restricted-syntax': [ 'error', { diff --git a/package.json b/package.json index 3c54f688..276012fa 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,11 @@ "drizzle-kit": "^0.31.10", "eslint": "^10.5.0", "eslint-plugin-functional": "^10.0.0", - "eslint-plugin-no-loops": "^0.4.0", + "eslint-plugin-jsdoc": "^63.0.2", + "eslint-plugin-perfectionist": "^5.9.0", + "eslint-plugin-security": "^4.0.1", + "eslint-plugin-sonarjs": "^4.0.3", + "eslint-plugin-unicorn": "^66.0.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", "prettier": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3784f377..8e10d5fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,9 +207,21 @@ importers: eslint-plugin-functional: specifier: ^10.0.0 version: 10.0.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-no-loops: - specifier: ^0.4.0 - version: 0.4.0(eslint@10.5.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^63.0.2 + version: 63.0.2(eslint@10.5.0(jiti@2.6.1)) + eslint-plugin-perfectionist: + specifier: ^5.9.0 + version: 5.9.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + eslint-plugin-security: + specifier: ^4.0.1 + version: 4.0.1 + eslint-plugin-sonarjs: + specifier: ^4.0.3 + version: 4.0.3(eslint@10.5.0(jiti@2.6.1)) + eslint-plugin-unicorn: + specifier: ^66.0.0 + version: 66.0.0(eslint@10.5.0(jiti@2.6.1)) husky: specifier: ^9.1.7 version: 9.1.7 @@ -449,6 +461,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.2': resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} @@ -572,6 +588,14 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@es-joy/jsdoccomment@0.87.0': + resolution: {integrity: sha512-mFXZloZMzuJZXSHUmAFu/pXTk0ZJTJBluuAkrvbzidpTN8W6F2bpRFuedSH+85kbdlRLJqc+gfN+kD3JOLJK5g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@es-joy/resolve.exports@1.2.0': + resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} + engines: {node: '>=10'} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -1716,6 +1740,10 @@ packages: resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} engines: {node: '>=18'} + '@sindresorhus/base62@1.0.0': + resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} + engines: {node: '>=18'} + '@smithy/chunked-blob-reader-native@4.2.3': resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} engines: {node: '>=18.0.0'} @@ -2061,6 +2089,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@5.1.1': resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} @@ -2370,6 +2401,10 @@ packages: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} + are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + argon2@0.44.0: resolution: {integrity: sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==} engines: {node: '>=16.17.0'} @@ -2469,9 +2504,21 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + builtin-modules@5.2.0: + resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} + engines: {node: '>=18.20'} + bullmq@5.73.4: resolution: {integrity: sha512-Q+NeFLtdKSD3GDPYSX4pH+Mc9E4OZVKimXwrnZ5WmndNy31COMy4vQV9zfhgfHGSUFrlpsBicfKYbSjx9FbO+A==} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2495,6 +2542,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -2510,6 +2560,10 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + cli-boxes@2.2.1: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} @@ -2595,6 +2649,10 @@ packages: resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} engines: {node: '>= 6'} + comment-parser@1.4.7: + resolution: {integrity: sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==} + engines: {node: '>= 12.0.0'} + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -2629,6 +2687,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -2713,6 +2774,10 @@ packages: detect-europe-js@0.1.2: resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} + detect-indent@7.0.2: + resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} + engines: {node: '>=12.20'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2938,10 +3003,32 @@ packages: typescript: optional: true - eslint-plugin-no-loops@0.4.0: - resolution: {integrity: sha512-cudfSMy9KLnyqQ5QPBuzuxtBqGvJOZ4SeeUuKbGMYc2opJzCUnkKKfUYI4bMErdgTwoJeguYscSWaWI5/2A8aA==} + eslint-plugin-jsdoc@63.0.2: + resolution: {integrity: sha512-0TchoK1uS4VxHSo3P4CyWQ31Lm+6zsT+xkHMC5KbFKwgOf8YrXPf1Bl8EP7kpgw1wfe/Ui5jz5mSX7ou8WAVuw==} + engines: {node: ^22.13.0 || >=24} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-perfectionist@5.9.0: + resolution: {integrity: sha512-8TWzg02zmnBdZwCkWLi8jhzqXI+fE7Z/RwV8SL6xD45tJ8Bp3wGuYL2XtQgfe/Wd0eBqOUX+s6ey73IyszvKTA==} + engines: {node: ^20.0.0 || >=22.0.0} + peerDependencies: + eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-security@4.0.1: + resolution: {integrity: sha512-/lZCkOxPOWaf1jXAqgICrS8St3BMBccIPvhOSUYuV6VCr1o5nFVG998FnTLt6w2Nxb8Uo0nM8fzmnhp+GY/aEg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-plugin-sonarjs@4.0.3: + resolution: {integrity: sha512-5drkJKLC9qQddIiaATV0e8+ygbUc7b0Ti6VB7M2d3jmKNh3X0RaiIJYTs3dr9xnlhlrxo+/s1FoO3Jgv6O/c7g==} peerDependencies: - eslint: '>=9' + eslint: ^8.0.0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-unicorn@66.0.0: + resolution: {integrity: sha512-+ywdy8T3foyZ2t3nRBujGa3vfOVMobHIi5iLB0L+fogdVO3EiUJ4BAyIacogWytnweLw3hgT70LQL9KoKTY/kA==} + engines: {node: '>=22'} + peerDependencies: + eslint: '>=10.4' eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} @@ -3087,6 +3174,10 @@ packages: resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} engines: {node: '>=20'} + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -3136,6 +3227,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -3175,6 +3269,10 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3203,6 +3301,9 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -3241,6 +3342,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3259,6 +3364,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-builtin-module@5.0.0: + resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} + engines: {node: '>=18.20'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3348,6 +3457,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdoc-type-pratt-parser@7.2.0: + resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} + engines: {node: '>=20.0.0'} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -3381,6 +3499,10 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} + jsx-ast-utils-x@0.1.0: + resolution: {integrity: sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -3528,6 +3650,9 @@ packages: lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} @@ -3655,6 +3780,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -3703,6 +3832,9 @@ packages: oauth@0.10.2: resolution: {integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==} + object-deep-merge@2.0.1: + resolution: {integrity: sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -3747,10 +3879,16 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-imports-exports@0.2.4: + resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-statements@1.0.11: + resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} + passport-github@1.1.0: resolution: {integrity: sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==} engines: {node: '>= 0.4.0'} @@ -3959,9 +4097,25 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + regjsparser@0.13.2: + resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} + hasBin: true + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3970,6 +4124,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4021,6 +4179,9 @@ packages: resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} hasBin: true + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -4036,6 +4197,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -4044,6 +4209,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.4: + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -4102,6 +4272,15 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@4.0.0: + resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -4159,6 +4338,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} + strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} @@ -4242,6 +4425,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + to-valid-identifier@1.0.0: + resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} + engines: {node: '>=20'} + toad-cache@3.7.0: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} @@ -5079,6 +5266,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 @@ -5245,6 +5434,16 @@ snapshots: '@epic-web/invariant@1.0.0': {} + '@es-joy/jsdoccomment@0.87.0': + dependencies: + '@types/estree': 1.0.9 + '@typescript-eslint/types': 8.61.0 + comment-parser: 1.4.7 + esquery: 1.7.0 + jsdoc-type-pratt-parser: 7.2.0 + + '@es-joy/resolve.exports@1.2.0': {} + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -6114,6 +6313,8 @@ snapshots: '@simple-libs/stream-utils@1.2.0': {} + '@sindresorhus/base62@1.0.0': {} + '@smithy/chunked-blob-reader-native@4.2.3': dependencies: '@smithy/util-base64': 4.3.2 @@ -6561,6 +6762,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 20.19.39 @@ -6959,6 +7162,8 @@ snapshots: ansis@4.2.0: {} + are-docs-informative@0.0.2: {} + argon2@0.44.0: dependencies: '@phc/format': 1.0.0 @@ -7068,6 +7273,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + builtin-modules@3.3.0: {} + + builtin-modules@5.2.0: {} + bullmq@5.73.4: dependencies: cron-parser: 4.9.0 @@ -7080,6 +7289,8 @@ snapshots: transitivePeerDependencies: - supports-color + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -7099,6 +7310,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + change-case@5.4.4: {} + chardet@2.1.1: {} check-disk-space@3.4.0: @@ -7110,6 +7323,8 @@ snapshots: chrome-trace-event@1.0.4: {} + ci-info@4.4.0: {} + cli-boxes@2.2.1: optional: true @@ -7184,6 +7399,8 @@ snapshots: array-timsort: 1.0.3 esprima: 4.0.1 + comment-parser@1.4.7: {} + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -7212,6 +7429,10 @@ snapshots: cookie@1.1.1: {} + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.2 + core-util-is@1.0.3: {} cosmiconfig-typescript-loader@6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): @@ -7278,6 +7499,8 @@ snapshots: detect-europe-js@0.1.2: {} + detect-indent@7.0.2: {} + detect-libc@2.1.2: {} dot-prop@5.3.0: @@ -7481,9 +7704,74 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-no-loops@0.4.0(eslint@10.5.0(jiti@2.6.1)): + eslint-plugin-jsdoc@63.0.2(eslint@10.5.0(jiti@2.6.1)): dependencies: + '@es-joy/jsdoccomment': 0.87.0 + '@es-joy/resolve.exports': 1.2.0 + are-docs-informative: 0.0.2 + comment-parser: 1.4.7 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint: 10.5.0(jiti@2.6.1) + espree: 11.2.0 + esquery: 1.7.0 + html-entities: 2.6.0 + object-deep-merge: 2.0.1 + parse-imports-exports: 0.2.4 + semver: 7.8.4 + spdx-expression-parse: 4.0.0 + to-valid-identifier: 1.0.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-perfectionist@5.9.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/utils': 8.61.0(eslint@10.5.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.5.0(jiti@2.6.1) + natural-orderby: 5.0.0 + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-security@4.0.1: + dependencies: + safe-regex: 2.1.1 + + eslint-plugin-sonarjs@4.0.3(eslint@10.5.0(jiti@2.6.1)): + dependencies: + '@eslint-community/regexpp': 4.12.2 + builtin-modules: 3.3.0 + bytes: 3.1.2 + eslint: 10.5.0(jiti@2.6.1) + functional-red-black-tree: 1.0.1 + globals: 17.6.0 + jsx-ast-utils-x: 0.1.0 + lodash.merge: 4.6.2 + minimatch: 10.2.5 + scslre: 0.3.0 + semver: 7.7.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + + eslint-plugin-unicorn@66.0.0(eslint@10.5.0(jiti@2.6.1)): + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.6.1)) + browserslist: 4.28.2 + change-case: 5.4.4 + ci-info: 4.4.0 + core-js-compat: 3.49.0 + detect-indent: 7.0.2 eslint: 10.5.0(jiti@2.6.1) + find-up-simple: 1.0.1 + globals: 17.6.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regjsparser: 0.13.2 + semver: 7.8.4 + strip-indent: 4.1.1 eslint-scope@5.1.1: dependencies: @@ -7662,6 +7950,8 @@ snapshots: fast-querystring: 1.1.2 safe-regex2: 5.1.0 + find-up-simple@1.0.1: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -7716,6 +8006,8 @@ snapshots: function-bind@1.1.2: {} + functional-red-black-tree@1.0.1: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.5.0: {} @@ -7766,6 +8058,8 @@ snapshots: dependencies: ini: 4.1.1 + globals@17.6.0: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -7791,6 +8085,8 @@ snapshots: dependencies: function-bind: 1.1.2 + html-entities@2.6.0: {} + html-escaper@2.0.2: {} http-errors@2.0.1: @@ -7822,6 +8118,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@5.0.0: {} + inherits@2.0.4: {} ini@4.1.1: {} @@ -7844,6 +8142,10 @@ snapshots: is-arrayish@0.2.1: {} + is-builtin-module@5.0.0: + dependencies: + builtin-modules: 5.2.0 + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -7915,6 +8217,10 @@ snapshots: dependencies: argparse: 2.0.1 + jsdoc-type-pratt-parser@7.2.0: {} + + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -7952,6 +8258,8 @@ snapshots: ms: 2.1.3 semver: 7.7.4 + jsx-ast-utils-x@0.1.0: {} + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -8077,6 +8385,8 @@ snapshots: lodash.kebabcase@4.1.1: {} + lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} lodash.once@4.1.1: {} @@ -8196,6 +8506,8 @@ snapshots: natural-compare@1.4.0: {} + natural-orderby@5.0.0: {} + neo-async@2.6.2: {} nest-winston@1.10.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.19.0): @@ -8234,6 +8546,8 @@ snapshots: oauth@0.10.2: {} + object-deep-merge@2.0.1: {} + obug@2.1.1: {} on-exit-leak-free@2.1.2: {} @@ -8296,6 +8610,10 @@ snapshots: dependencies: callsites: 3.1.0 + parse-imports-exports@0.2.4: + dependencies: + parse-statements: 1.0.11 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -8303,6 +8621,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-statements@1.0.11: {} + passport-github@1.1.0: dependencies: passport-oauth2: 1.8.0 @@ -8517,12 +8837,29 @@ snapshots: dependencies: redis-errors: 1.2.0 + refa@0.12.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + reflect-metadata@0.2.2: {} + regexp-ast-analysis@0.7.1: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + + regexp-tree@0.1.27: {} + + regjsparser@0.13.2: + dependencies: + jsesc: 3.1.0 + require-directory@2.1.1: {} require-from-string@2.0.2: {} + reserved-identifiers@1.2.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -8582,6 +8919,10 @@ snapshots: dependencies: ret: 0.5.0 + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -8599,10 +8940,18 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + secure-json-parse@4.1.0: {} semver@7.7.4: {} + semver@7.8.4: {} + set-cookie-parser@2.7.2: {} setprototypeof@1.2.0: {} @@ -8648,6 +8997,15 @@ snapshots: source-map@0.7.6: {} + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@4.0.0: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + split2@4.2.0: {} stack-trace@0.0.10: {} @@ -8699,6 +9057,8 @@ snapshots: strip-bom@3.0.0: {} + strip-indent@4.1.1: {} + strnum@2.2.3: {} strtok3@10.3.5: @@ -8774,6 +9134,11 @@ snapshots: dependencies: is-number: 7.0.0 + to-valid-identifier@1.0.0: + dependencies: + '@sindresorhus/base62': 1.0.0 + reserved-identifiers: 1.2.0 + toad-cache@3.7.0: {} toad-cache@3.7.1: From 10374e974a94ab5fa84368465e39ac712fa20aa1 Mon Sep 17 00:00:00 2001 From: soorq Date: Mon, 15 Jun 2026 16:29:31 +0300 Subject: [PATCH 3/3] chore: fix code style issues to comply with new ESLint config --- eslint.config.mjs | 16 ++++ libs/bootstrap/src/bootstrap.ts | 40 +++++---- libs/bootstrap/src/configs/throttler.ts | 1 + libs/bootstrap/src/setups/cors.ts | 1 + libs/bootstrap/src/setups/logger.ts | 19 +++-- libs/bootstrap/src/setups/swagger.ts | 8 +- libs/bootstrap/src/setups/throttler.ts | 3 +- libs/config/src/config.module.ts | 7 +- libs/config/src/config.schema.ts | 1 + libs/database/src/database-health.service.ts | 2 +- .../src/database.module-definition.ts | 1 + libs/database/src/database.module.ts | 8 +- libs/database/src/migration.service.ts | 7 +- .../src/controller/health.controller.ts | 6 +- .../src/controller/health.controlller.spec.ts | 5 +- libs/health/src/controller/health.swagger.ts | 3 +- libs/health/src/health.module-definition.ts | 1 + libs/health/src/health.module.ts | 3 +- libs/health/src/health.service.spec.ts | 12 +-- libs/health/src/health.service.ts | 7 +- libs/imagor/src/imagor.module-definition.ts | 1 + libs/imagor/src/imagor.module.ts | 1 + libs/imagor/src/imagor.service.ts | 29 ++++--- libs/imagor/src/utils/imagor-path-builder.ts | 84 ++++++++++++++----- libs/metrics/src/metrics.controller.ts | 4 +- libs/metrics/src/metrics.module.ts | 5 +- libs/s3/src/s3.module-definition.ts | 1 + libs/s3/src/s3.module.ts | 16 ++-- libs/s3/src/s3.service.ts | 14 +++- src/app.module.ts | 56 ++++++------- src/area/application/area.facade.ts | 19 +++-- .../controllers/area/controller.ts | 4 +- .../application/controllers/area/swagger.ts | 1 + .../controllers/state/controller.ts | 6 +- .../application/controllers/state/swagger.ts | 3 +- src/area/application/dtos/area.dto.ts | 4 +- src/area/application/dtos/states.dto.ts | 6 +- .../use-cases/areas/create.use-case.ts | 15 ++-- .../use-cases/areas/delete.use-case.ts | 4 +- .../use-cases/areas/update.use-case.ts | 11 ++- .../use-cases/states/create.use-case.ts | 11 ++- .../use-cases/states/delete.use-case.ts | 7 +- .../use-cases/states/get-all.query.ts | 1 + .../use-cases/states/get-one.query.ts | 1 + .../application/use-cases/states/index.ts | 2 +- .../use-cases/states/reorder.use-case.ts | 9 +- .../use-cases/states/restore.use-state.ts | 5 +- .../use-cases/states/update.use-case.ts | 9 +- src/area/area.module.ts | 7 +- .../persistence/models/area.model.ts | 4 +- .../persistence/models/state.model.ts | 9 +- .../repositories/area.repository.ts | 10 ++- .../persistence/repositories/index.ts | 2 +- .../repositories/state.repository.ts | 8 +- src/auth/application/auth.facade.ts | 22 ++--- .../application/controller/auth/controller.ts | 15 ++-- .../application/controller/auth/swagger.ts | 5 +- .../controller/oauth/controller.ts | 13 +-- .../application/controller/oauth/swagger.ts | 5 +- .../controller/recovery/controller.ts | 8 +- .../controller/recovery/swagger.ts | 5 +- src/auth/application/dtos/2fa.dto.ts | 2 +- src/auth/application/dtos/oauth.dto.ts | 2 +- src/auth/application/dtos/session.dto.ts | 2 +- src/auth/application/strategies/index.ts | 4 +- .../strategies/resend-code.strategy.ts | 3 +- .../reset-password-resend.strategy.ts | 6 +- .../strategies/sign-up-resend.strategy.ts | 6 +- .../confirm-reset-password.use-case.ts | 9 +- src/auth/application/use-cases/index.ts | 59 +++++++------ .../oauth/authenticate-oauth.use-case.ts | 6 +- .../oauth/connect-oauth-provider.use-case.ts | 3 +- .../oauth/connect-provider.use-case.ts | 4 +- .../oauth/disconnect-provider.use-case.ts | 2 +- .../oauth/oauth-orchestrator.use-case.ts | 14 ++-- .../oauth/process-oauth-login.use-case.ts | 5 +- .../process-oauth-registration.use-case.ts | 9 +- .../use-cases/refresh-tokens.use-case.ts | 5 +- .../use-cases/resend-code.use-case.ts | 15 ++-- .../use-cases/reset-password.use-case.ts | 19 +++-- .../application/use-cases/sign-in.use-case.ts | 7 +- .../use-cases/sign-out.use-case.ts | 1 + .../use-cases/sign-up-verify.use-case.ts | 25 +++--- .../application/use-cases/sign-up.use-case.ts | 13 +-- .../verify-reset-password.use-case.ts | 7 +- src/auth/auth.module.ts | 17 ++-- .../persistence/models/identity.model.ts | 2 +- .../persistence/models/session.model.ts | 2 +- .../repositories/identity.repository.ts | 10 +-- .../repositories/session.repository.ts | 5 +- .../infrastructure/security/token.service.ts | 5 +- .../strategies/bearer.strategy.ts | 3 +- .../strategies/cookie.strategy.ts | 9 +- .../strategies/vkontakte.strategy.ts | 12 +-- .../strategies/yandex.strategy.ts | 6 +- .../infrastructure/utils/get-device-meta.ts | 11 ++- .../infrastructure/workers/mail.processor.ts | 8 +- .../infrastructure/workers/user.processor.ts | 8 +- src/main.ts | 4 + src/projects/application/controller/index.ts | 2 +- .../controller/members/controller.ts | 4 +- .../application/controller/members/swagger.ts | 1 + .../controller/projects/controller.ts | 8 +- .../controller/projects/swagger.ts | 11 +-- src/projects/application/dtos/member.dto.ts | 4 +- src/projects/application/dtos/project.dto.ts | 9 +- .../application/mappers/project.mapper.ts | 2 +- src/projects/application/project.facade.ts | 20 +++-- .../use-cases/member/add.use-case.ts | 7 +- .../use-cases/member/find-all.query.ts | 3 +- .../use-cases/member/update.use-case.ts | 24 +++--- .../use-cases/project/create.use-case.ts | 11 +-- .../use-cases/project/find-by-team.query.ts | 5 +- .../use-cases/project/find-one.query.ts | 12 +-- .../project/generate-share-token.use-case.ts | 12 +-- .../use-cases/project/get-detail.query.ts | 2 + .../application/use-cases/project/index.ts | 8 +- .../use-cases/project/update.use-case.ts | 9 +- src/projects/domain/entities/member.domain.ts | 2 +- .../domain/entities/project.domain.ts | 2 +- .../domain/policy/project-access.policy.ts | 18 ++-- .../infrastructure/persistence/models/enum.ts | 2 +- .../persistence/models/project.model.ts | 7 +- .../repositories/member.repository.ts | 11 +-- .../repositories/project.repository.ts | 11 +-- src/projects/projects.module.ts | 7 +- .../cache/adapters/redis-cache.adapter.ts | 35 ++++++-- src/shared/adapters/cache/module.ts | 5 +- src/shared/adapters/mail/adapter.ts | 12 +-- src/shared/adapters/mail/module.ts | 1 + src/shared/constants/roles.constant.ts | 4 +- .../decorators/api-controller.decorator.ts | 1 + src/shared/decorators/user.decorator.ts | 7 +- src/shared/dtos/pagination.dto.ts | 2 +- src/shared/dtos/response.dto.ts | 2 +- src/shared/error/exception.ts | 3 +- src/shared/error/filter.ts | 21 +++-- src/shared/error/schema.ts | 2 +- src/shared/error/swagger.ts | 3 +- src/shared/guards/bearer.guard.ts | 8 +- src/shared/guards/cookie.guard.ts | 1 + .../interceptors/http-metrics.interceptor.ts | 9 +- .../zod-validation.interceptor.ts | 4 +- src/shared/media/controller/index.ts | 6 +- src/shared/media/controller/swagger.ts | 5 +- .../decorators/extract-media-req.decorator.ts | 15 ++-- src/shared/media/media.module.ts | 9 +- src/shared/media/media.service.ts | 17 ++-- src/shared/media/strategies/index.ts | 2 +- .../media/strategies/team-media.strategy.ts | 3 +- .../media/strategies/user-avatar.strategy.ts | 3 +- src/shared/media/workers/media.worker.ts | 16 ++-- src/shared/schemas/pagination.schema.ts | 5 +- src/shared/schemas/sorting.schema.ts | 5 +- src/shared/utils/format-bytes.util.ts | 6 +- src/shared/utils/image-builder.util.ts | 6 +- .../controller/invitations/controller.ts | 7 +- .../controller/invitations/swagger.ts | 3 +- .../application/controller/me/controller.ts | 7 +- .../application/controller/me/swagger.ts | 3 +- .../controller/members/controller.ts | 4 +- .../application/controller/members/swagger.ts | 3 +- .../controller/teams/controller.ts | 6 +- .../application/controller/teams/swagger.ts | 5 +- src/teams/application/dtos/invitation.dto.ts | 8 +- src/teams/application/dtos/member.dto.ts | 4 +- src/teams/application/dtos/team.dto.ts | 6 +- .../application/mappers/member.mapper.ts | 9 +- src/teams/application/team.facade.ts | 3 +- .../use-cases/base/create-team.use-case.ts | 7 +- .../use-cases/base/delete-team.use-case.ts | 4 +- .../use-cases/base/find-team.query.ts | 1 + .../use-cases/base/get-my-teams.use-case.ts | 2 +- .../use-cases/base/update-team.use-case.ts | 7 +- src/teams/application/use-cases/index.ts | 58 +++++++------ .../invitions/accept-invitation.use-case.ts | 5 +- .../invitions/decline-invitation.use-case.ts | 8 +- .../invitions/get-invitation.query.ts | 19 +++-- .../invitions/get-invitations.query.ts | 12 ++- .../invitions/get-my-invites.use-case.ts | 7 +- .../invitions/send-invitation.use-case.ts | 29 ++++--- .../invitions/update-invitation.use-case.ts | 10 ++- .../members/find-team-member.query.ts | 1 + .../members/get-team-members.query.ts | 4 +- .../members/remove-team-member.use-case.ts | 7 +- .../members/update-team-member.use-case.ts | 47 +++++------ src/teams/domain/entities/teams.domain.ts | 2 +- src/teams/domain/policy/team-member.policy.ts | 33 ++++++-- .../listeners/update-media.listener.ts | 11 ++- .../persistence/models/teams.model.ts | 5 +- .../repositories/teams.repository.ts | 29 +++---- .../infrastructure/workers/mail.processor.ts | 9 +- src/teams/teams.module.ts | 9 +- .../controller/settings/controller.ts | 6 +- .../controller/settings/swagger.ts | 7 +- .../application/controller/user/controller.ts | 8 +- .../application/controller/user/swagger.ts | 7 +- src/user/application/dtos/user.dto.ts | 2 +- .../application/use-cases/find-user.query.ts | 10 ++- src/user/application/use-cases/index.ts | 9 +- .../use-cases/register-user.use-case.ts | 3 +- .../update-notifications.use-case.ts | 9 +- .../use-cases/update-profile.use-case.ts | 5 +- src/user/application/user.facade.ts | 3 +- src/user/domain/entities/user.domain.ts | 2 +- .../listeners/update-avatar.listener.ts | 4 +- .../persistence/models/user.entity.ts | 2 +- .../repositories/user.repository.ts | 25 ++++-- src/user/user.module.ts | 5 +- test/app.e2e-spec.ts | 3 +- 210 files changed, 1124 insertions(+), 735 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index eb0455f2..9752b6d1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -186,4 +186,20 @@ export default tseslint.config( 'functional/immutable-data': 'off', }, }, + { + files: [ + '**/*.{facade,repository,service,controller,query,use-case,adapter}.ts', + '**/controller.ts', + '**/adapter.ts', + ], + rules: { + 'require-await': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'no-useless-constructor': 'off', + 'sonarjs/cognitive-complexity': 'off', + 'unicorn/no-useless-undefined': 'off', + 'unicorn/prefer-export-from': 'off', + 'functional/immutable-data': 'off', + }, + }, ); diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts index 5131df08..0cb22423 100644 --- a/libs/bootstrap/src/bootstrap.ts +++ b/libs/bootstrap/src/bootstrap.ts @@ -1,25 +1,25 @@ +import fastifyCompress from '@fastify/compress'; +import fastifyCookie from '@fastify/cookie'; +import fastifyCsrf from '@fastify/csrf-protection'; +import fastifyMultipart from '@fastify/multipart'; import { Logger, VersioningType } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; +import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; +import { createId } from '@paralleldrive/cuid2'; + import { DEFAULT_THROTTLER_OPTIONS } from './configs/throttler'; import { setupCors, setupLogger, setupThrottler, setupSwagger } from './setups'; -import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; + import type { BootstrapOptions } from './interfaces/options.interface'; -import fastifyCookie from '@fastify/cookie'; -import fastifyCompress from '@fastify/compress'; -import fastifyMultipart from '@fastify/multipart'; -import fastifyCsrf from '@fastify/csrf-protection'; -import { createId } from '@paralleldrive/cuid2'; -import type { IncomingMessage } from 'http'; +import type { IncomingMessage } from 'node:http'; export async function bootstrapApp(options: BootstrapOptions) { const startTime = performance.now(); const adapter = new FastifyAdapter({ requestIdHeader: 'x-request-id', requestIdLogLabel: 'request', - genReqId: (req: IncomingMessage) => { - return (req.headers['x-request-id'] as string) || createId(); - }, + genReqId: (req: IncomingMessage) => (req.headers['x-request-id'] as string) || createId(), }); const { @@ -56,7 +56,7 @@ export async function bootstrapApp(options: BootstrapOptions) { app.getHttpAdapter() .getInstance() - .addHook('onSend', async (request, reply, payload) => { + .addHook('onSend', (request, reply, payload) => { reply.header('x-request-id', request.id); return payload; }) @@ -82,7 +82,7 @@ export async function bootstrapApp(options: BootstrapOptions) { done(); }); - await setupLogger(app, options.serviceName); + setupLogger(app, options.serviceName); await app.register(fastifyCompress, { global: true, @@ -97,7 +97,9 @@ export async function bootstrapApp(options: BootstrapOptions) { }, }); - if (apiPrefix) app.setGlobalPrefix(apiPrefix); + if (apiPrefix) { + app.setGlobalPrefix(apiPrefix); + } if (version) { const hasV = version.startsWith('v'); @@ -107,7 +109,9 @@ export async function bootstrapApp(options: BootstrapOptions) { defaultVersion: hasV ? version.slice(1) : version, }); } - if (useCors) setupCors(app, origins); + if (useCors) { + setupCors(app, origins); + } if (swaggerOptions) { const { path = 'docs', ...metadata } = swaggerOptions; @@ -138,13 +142,15 @@ export async function bootstrapApp(options: BootstrapOptions) { }, }); } - if (setupApp) setupApp(app); + if (setupApp) { + await setupApp(app); + } await app.listen(port, '0.0.0.0', (_err, address) => { const prefix = [apiPrefix, version].filter(Boolean).join('/'); - const baseUrl = `${address}${prefix ? '/' + prefix : ''}`; + const baseUrl = `${address}${prefix ? `/${prefix}` : ''}`; - const swaggerBase = `${address}${apiPrefix ? '/' + apiPrefix : ''}`; + const swaggerBase = `${address}${apiPrefix ? `/${apiPrefix}` : ''}`; const swaggerPath = swaggerOptions?.path ?? 'docs'; if (_err) { diff --git a/libs/bootstrap/src/configs/throttler.ts b/libs/bootstrap/src/configs/throttler.ts index b186fcbf..0fb96936 100644 --- a/libs/bootstrap/src/configs/throttler.ts +++ b/libs/bootstrap/src/configs/throttler.ts @@ -1,4 +1,5 @@ import 'dotenv/config'; + import type { ThrottlerModuleOptions } from '@nestjs/throttler'; export const DEFAULT_THROTTLER_OPTIONS: ThrottlerModuleOptions = [ diff --git a/libs/bootstrap/src/setups/cors.ts b/libs/bootstrap/src/setups/cors.ts index 3993f49d..7f3c6c20 100644 --- a/libs/bootstrap/src/setups/cors.ts +++ b/libs/bootstrap/src/setups/cors.ts @@ -1,4 +1,5 @@ import fastifyCors from '@fastify/cors'; + import type { NestFastifyApplication } from '@nestjs/platform-fastify'; export function setupCors(app: NestFastifyApplication, origins: readonly string[]) { diff --git a/libs/bootstrap/src/setups/logger.ts b/libs/bootstrap/src/setups/logger.ts index c5403c94..f13211a8 100644 --- a/libs/bootstrap/src/setups/logger.ts +++ b/libs/bootstrap/src/setups/logger.ts @@ -5,13 +5,14 @@ import { type CallHandler, Logger, } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { WinstonModule, utilities } from 'nest-winston'; import { Observable, throwError } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; -import type { FastifyRequest } from 'fastify'; -import { WinstonModule, utilities } from 'nest-winston'; import { format, transports } from 'winston'; + import type { NestFastifyApplication } from '@nestjs/platform-fastify'; -import { ConfigService } from '@nestjs/config'; +import type { FastifyRequest } from 'fastify'; export function setupLogger(app: NestFastifyApplication, service: string) { const cfg = app.get(ConfigService); @@ -95,10 +96,14 @@ export class LoggingInterceptor implements NestInterceptor { } private sanitize(data: T): T { - if (!data || typeof data !== 'object') return data; - if (Array.isArray(data)) return data.map((v) => this.sanitize(v)) as T; + if (!data || typeof data !== 'object') { + return data; + } + if (Array.isArray(data)) { + return data.map((v) => this.sanitize(v)) as T; + } - const cleanData = JSON.parse(JSON.stringify(data)) as Record; + const cleanData = structuredClone(data) as Record; return Object.keys(cleanData).reduce>((acc, key) => { const isSensitive = this.sensitiveFields.some((field) => @@ -121,7 +126,7 @@ export class LoggingInterceptor implements NestInterceptor { * Represents a structured application log payload for Grafana Loki. * This object is flattened to ensure each property is indexed as a top-level label/column. * - * @typedef {Object} TLog + * @typedef {object} TLog */ export type TLog = { /** diff --git a/libs/bootstrap/src/setups/swagger.ts b/libs/bootstrap/src/setups/swagger.ts index d4347cf1..46e060dd 100644 --- a/libs/bootstrap/src/setups/swagger.ts +++ b/libs/bootstrap/src/setups/swagger.ts @@ -1,9 +1,11 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { GlobalErrorResponse } from '@shared/error/schema'; import { cleanupOpenApiDoc } from 'nestjs-zod'; -import type { NestFastifyApplication } from '@nestjs/platform-fastify'; -import type { SwaggerOptions } from '../interfaces'; + import { SWAGGER_DEFAULTS } from '../configs/swagger'; -import { GlobalErrorResponse } from '@shared/error/schema'; + +import type { SwaggerOptions } from '../interfaces'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; async function getCustomCSS() { const rawUrl = 'https://gist.githubusercontent.com/soorq/f745e5c44cfe27aa928048d6d4ccb18a/raw'; diff --git a/libs/bootstrap/src/setups/throttler.ts b/libs/bootstrap/src/setups/throttler.ts index 29f683b4..a3456b04 100644 --- a/libs/bootstrap/src/setups/throttler.ts +++ b/libs/bootstrap/src/setups/throttler.ts @@ -1,7 +1,6 @@ import { Module, type Type } from '@nestjs/common'; -import type { ThrottlerModuleOptions } from '@nestjs/throttler'; import { APP_GUARD } from '@nestjs/core'; -import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; +import { ThrottlerGuard, ThrottlerModule, type ThrottlerModuleOptions } from '@nestjs/throttler'; export function setupThrottler(module: Type, options: ThrottlerModuleOptions) { @Module({ diff --git a/libs/config/src/config.module.ts b/libs/config/src/config.module.ts index d6bf1de1..be8e560f 100644 --- a/libs/config/src/config.module.ts +++ b/libs/config/src/config.module.ts @@ -1,9 +1,12 @@ +/* eslint-disable no-console */ +import * as path from 'node:path'; + import { Module } from '@nestjs/common'; import { ConfigModule as NestConfigModule } from '@nestjs/config'; -import * as path from 'path'; -import { ConfigSchema } from './config.schema'; import { ZodError } from 'zod/v4'; +import { ConfigSchema } from './config.schema'; + const validateConfig = (config: Record) => { try { return ConfigSchema.parse(config); diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 9098513e..f7539b00 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -1,4 +1,5 @@ import { z } from 'zod/v4'; + import { jwtSecretValidation } from './helpers/jwt-secren-validation'; const timeStringSchema = z.string().regex(/^[0-9]+[smhdw]$/, { diff --git a/libs/database/src/database-health.service.ts b/libs/database/src/database-health.service.ts index c8698061..82211d81 100644 --- a/libs/database/src/database-health.service.ts +++ b/libs/database/src/database-health.service.ts @@ -1,5 +1,5 @@ -import { Inject, Injectable } from '@nestjs/common'; import { SQL_CLIENT } from '@libs/database/constants'; +import { Inject, Injectable } from '@nestjs/common'; import { Sql } from 'postgres'; @Injectable() diff --git a/libs/database/src/database.module-definition.ts b/libs/database/src/database.module-definition.ts index a9cbc335..bb742bc8 100644 --- a/libs/database/src/database.module-definition.ts +++ b/libs/database/src/database.module-definition.ts @@ -1,4 +1,5 @@ import { ConfigurableModuleBuilder } from '@nestjs/common'; + import type { DatabaseModuleOptions } from './interfaces'; export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = diff --git a/libs/database/src/database.module.ts b/libs/database/src/database.module.ts index 1b82c43f..4c7f11c7 100644 --- a/libs/database/src/database.module.ts +++ b/libs/database/src/database.module.ts @@ -1,15 +1,16 @@ +import { DatabaseHealthService } from '@libs/database/database-health.service'; import { Inject, Logger, Module, OnApplicationShutdown } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; + import { DATABASE_SERVICE, SQL_CLIENT } from './constants'; -import { MigrationService } from './migration.service'; import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, } from './database.module-definition'; -import { DatabaseHealthService } from '@libs/database/database-health.service'; +import { MigrationService } from './migration.service'; @Module({ providers: [ @@ -54,8 +55,9 @@ import { DatabaseHealthService } from '@libs/database/database-health.service'; ? { logQuery(query, params) { logger.debug(`SQL: ${query}`); - if (params?.length) + if (params?.length) { logger.debug(`Params: ${JSON.stringify(params)}`); + } }, } : false, diff --git a/libs/database/src/migration.service.ts b/libs/database/src/migration.service.ts index 2aeefdc6..ca261ad1 100644 --- a/libs/database/src/migration.service.ts +++ b/libs/database/src/migration.service.ts @@ -1,10 +1,13 @@ +import * as path from 'node:path'; + import { Inject, Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { migrate } from 'drizzle-orm/postgres-js/migrator'; + import { DATABASE_SERVICE } from './constants'; -import type { DatabaseService } from './interfaces'; -import * as path from 'path'; import { MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } from './database.module-definition'; +import type { DatabaseService } from './interfaces'; + @Injectable() export class MigrationService implements OnModuleInit { private readonly logger = new Logger(MigrationService.name); diff --git a/libs/health/src/controller/health.controller.ts b/libs/health/src/controller/health.controller.ts index 31b17546..dc00b2ab 100644 --- a/libs/health/src/controller/health.controller.ts +++ b/libs/health/src/controller/health.controller.ts @@ -1,9 +1,11 @@ import { Controller, Get, HttpStatus } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { SkipThrottle } from '@nestjs/throttler'; +import { BaseException } from '@shared/error'; + import { HealthService } from '../health.service'; + import { GetHealthSwagger, GetPingSwagger } from './health.swagger'; -import { ApiTags } from '@nestjs/swagger'; -import { BaseException } from '@shared/error'; @SkipThrottle() @Controller() diff --git a/libs/health/src/controller/health.controlller.spec.ts b/libs/health/src/controller/health.controlller.spec.ts index 4d624a65..a0618d8b 100644 --- a/libs/health/src/controller/health.controlller.spec.ts +++ b/libs/health/src/controller/health.controlller.spec.ts @@ -1,6 +1,7 @@ +import { HttpStatus, Logger } from '@nestjs/common'; import { describe, it, expect, vi, beforeEach } from 'vitest'; + import { HealthController } from './health.controller'; -import { HttpStatus, Logger } from '@nestjs/common'; describe('HealthController', () => { let controller: HealthController; @@ -15,7 +16,7 @@ describe('HealthController', () => { controller = new HealthController(healthServiceMock as any); - vi.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); + vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {}); }); it('should throw SERVICE_UNAVAILABLE when service status is false (down)', async () => { diff --git a/libs/health/src/controller/health.swagger.ts b/libs/health/src/controller/health.swagger.ts index 4ea96e60..f2ee9663 100644 --- a/libs/health/src/controller/health.swagger.ts +++ b/libs/health/src/controller/health.swagger.ts @@ -1,8 +1,9 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { HealthResponse, HealthDetailedResponse } from '../dtos'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; +import { HealthResponse, HealthDetailedResponse } from '../dtos'; + export const GetHealthSwagger = () => applyDecorators( ApiOperation({ diff --git a/libs/health/src/health.module-definition.ts b/libs/health/src/health.module-definition.ts index 8a374e8f..1d3eae53 100644 --- a/libs/health/src/health.module-definition.ts +++ b/libs/health/src/health.module-definition.ts @@ -1,4 +1,5 @@ import { ConfigurableModuleBuilder } from '@nestjs/common'; + import type { HealthModuleOptions } from './interfaces'; export const { ASYNC_OPTIONS_TYPE, ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } = diff --git a/libs/health/src/health.module.ts b/libs/health/src/health.module.ts index 32067586..91b7e89e 100644 --- a/libs/health/src/health.module.ts +++ b/libs/health/src/health.module.ts @@ -1,7 +1,8 @@ import { Module } from '@nestjs/common'; + import { HealthController } from './controller/health.controller'; -import { HealthService } from './health.service'; import { ConfigurableModuleClass } from './health.module-definition'; +import { HealthService } from './health.service'; @Module({ controllers: [HealthController], diff --git a/libs/health/src/health.service.spec.ts b/libs/health/src/health.service.spec.ts index 802b89fa..03867bf7 100644 --- a/libs/health/src/health.service.spec.ts +++ b/libs/health/src/health.service.spec.ts @@ -1,14 +1,16 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { HealthService } from './health.service'; + +import type { HealthModuleOptions } from './interfaces'; + vi.mock('os', async () => { - const actual = await vi.importActual('os'); + const actual = await vi.importActual('os'); return { ...actual, loadavg: () => [1.23, 0.5, 0.1], }; }); -import { HealthService } from './health.service'; -import type { HealthModuleOptions } from './interfaces'; describe('HealthService', () => { const BASE_TIME = new Date('2026-05-15T10:00:00.000Z'); @@ -30,7 +32,7 @@ describe('HealthService', () => { version: 'v2.0.0', indicators: { database: () => true, - redis: async () => true, + redis: () => true, }, }; @@ -75,7 +77,7 @@ describe('HealthService', () => { const options: HealthModuleOptions = { serviceName: 'MyService', indicators: { - http: () => new Promise(() => undefined), + http: () => new Promise(() => {}), }, }; diff --git a/libs/health/src/health.service.ts b/libs/health/src/health.service.ts index b1299377..210f427b 100644 --- a/libs/health/src/health.service.ts +++ b/libs/health/src/health.service.ts @@ -1,6 +1,9 @@ +import * as os from 'node:os'; + import { Inject, Injectable } from '@nestjs/common'; -import * as os from 'os'; + import { MODULE_OPTIONS_TOKEN } from './health.module-definition'; + import type { HealthModuleOptions } from './interfaces'; @Injectable() @@ -60,7 +63,7 @@ export class HealthService { now: new Date().toISOString(), startedAt: this.startTime.toISOString(), uptime: this.formatUptime(uptimeSeconds), - uptimeSeconds: uptimeSeconds, + uptimeSeconds, }, loaded: loaded?.toFixed(2), }; diff --git a/libs/imagor/src/imagor.module-definition.ts b/libs/imagor/src/imagor.module-definition.ts index b958a9b5..9a15c496 100644 --- a/libs/imagor/src/imagor.module-definition.ts +++ b/libs/imagor/src/imagor.module-definition.ts @@ -1,4 +1,5 @@ import { ConfigurableModuleBuilder } from '@nestjs/common'; + import type { ImagorModuleOptions } from './interfaces'; export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = diff --git a/libs/imagor/src/imagor.module.ts b/libs/imagor/src/imagor.module.ts index 763626a8..e5b3b277 100644 --- a/libs/imagor/src/imagor.module.ts +++ b/libs/imagor/src/imagor.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; + import { ConfigurableModuleClass } from './imagor.module-definition'; import { ImagorService } from './imagor.service'; diff --git a/libs/imagor/src/imagor.service.ts b/libs/imagor/src/imagor.service.ts index ab2de8aa..affdcfb0 100644 --- a/libs/imagor/src/imagor.service.ts +++ b/libs/imagor/src/imagor.service.ts @@ -1,11 +1,14 @@ +import { createHmac } from 'node:crypto'; + +import { HttpService } from '@nestjs/axios'; import { Inject, Injectable, Logger } from '@nestjs/common'; +import { AxiosError } from 'axios'; +import { catchError, firstValueFrom, throwError } from 'rxjs'; + import { MODULE_OPTIONS_TOKEN } from './imagor.module-definition'; -import type { ImagorModuleOptions, Filters } from './interfaces'; -import { createHmac } from 'crypto'; -import { HttpService } from '@nestjs/axios'; import { ImagorPathBuilder } from './utils'; -import { catchError, firstValueFrom, throwError } from 'rxjs'; -import { AxiosError } from 'axios'; + +import type { ImagorModuleOptions, Filters } from './interfaces'; @Injectable() export class ImagorService { @@ -19,8 +22,8 @@ export class ImagorService { /** * Выполняет GET запрос к Imagor с применением фильтров и пресетов - * @param path Путь к исходному файлу в хранилище - * @param presetOrFilters Название пресета или объект с фильтрами (width, height, smart и т.д.) + * @param {string} path - Путь к исходному файлу в хранилище + * @param {string | Filters} [presetOrFilters] - Название пресета или объект с фильтрами (width, height, smart и т.д.) */ async get(path: string, presetOrFilters?: string | Filters): Promise { const host = this.options.url.replace(/\/+$/, ''); @@ -55,9 +58,15 @@ export class ImagorService { const merged = { ...globalFilters, ...localFilters }; - if (merged.width || merged.height) builder.resize(merged.width ?? 0, merged.height ?? 0); - if (merged.smart) builder.smart(true); - if (merged.fit) builder.fit(merged.fit); + if (merged.width || merged.height) { + builder.resize(merged.width ?? 0, merged.height ?? 0); + } + if (merged.smart) { + builder.smart(true); + } + if (merged.fit) { + builder.fit(merged.fit); + } builder.applyFilters(merged); diff --git a/libs/imagor/src/utils/imagor-path-builder.ts b/libs/imagor/src/utils/imagor-path-builder.ts index 811950d2..ce0df82b 100644 --- a/libs/imagor/src/utils/imagor-path-builder.ts +++ b/libs/imagor/src/utils/imagor-path-builder.ts @@ -36,16 +36,22 @@ export class ImagorPathBuilder { build(): string { const parts: string[] = []; - if (this._fitMode) parts.push(this._fitMode); + if (this._fitMode) { + parts.push(this._fitMode); + } if (this._width || this._height) { parts.push(`${this._width}x${this._height}`); } - if (this._isSmart) parts.push('smart'); + if (this._isSmart) { + parts.push('smart'); + } const filterString = this.serializeAllFilters(this._filters); - if (filterString) parts.push(filterString); + if (filterString) { + parts.push(filterString); + } const fullPath = this.storageRoot ? `${this.storageRoot}/${this.path}`.replace(/\/+/g, '/') @@ -56,20 +62,42 @@ export class ImagorPathBuilder { return parts.join('/'); } + // TODO will fix that shit + // eslint-disable-next-line sonarjs/cognitive-complexity private serializeAllFilters(f: Filters): string { const s: string[] = []; - if (f.quality) s.push(`quality(${f.quality})`); - if (f.format) s.push(`format(${f.format})`); - if (f.autojpg) s.push('autojpg()'); - if (f.strip_exif) s.push('strip_exif()'); - if (f.strip_icc) s.push('strip_icc()'); + if (f.quality) { + s.push(`quality(${f.quality})`); + } + if (f.format) { + s.push(`format(${f.format})`); + } + if (f.autojpg) { + s.push('autojpg()'); + } + if (f.strip_exif) { + s.push('strip_exif()'); + } + if (f.strip_icc) { + s.push('strip_icc()'); + } - if (f.brightness !== undefined) s.push(`brightness(${f.brightness})`); - if (f.contrast !== undefined) s.push(`contrast(${f.contrast})`); - if (f.grayscale) s.push('grayscale()'); - if (f.proportion !== undefined) s.push(`proportion(${f.proportion})`); - if (f.rgb) s.push(`rgb(${f.rgb.r},${f.rgb.g},${f.rgb.b})`); + if (f.brightness !== undefined) { + s.push(`brightness(${f.brightness})`); + } + if (f.contrast !== undefined) { + s.push(`contrast(${f.contrast})`); + } + if (f.grayscale) { + s.push('grayscale()'); + } + if (f.proportion !== undefined) { + s.push(`proportion(${f.proportion})`); + } + if (f.rgb) { + s.push(`rgb(${f.rgb.r},${f.rgb.g},${f.rgb.b})`); + } if (f.blur) { const b = f.blur; @@ -78,11 +106,19 @@ export class ImagorPathBuilder { if (f.sharpen) { s.push(`sharpen(${f.sharpen.amount},${f.sharpen.radius},${f.sharpen.threshold})`); } - if (f.noise) s.push(`noise(${f.noise})`); - if (f.rotate) s.push(`rotate(${f.rotate})`); + if (f.noise) { + s.push(`noise(${f.noise})`); + } + if (f.rotate) { + s.push(`rotate(${f.rotate})`); + } - if (f.fill) s.push(`fill(${f.fill})`); - if (f.background_color) s.push(`background_color(${f.background_color})`); + if (f.fill) { + s.push(`fill(${f.fill})`); + } + if (f.background_color) { + s.push(`background_color(${f.background_color})`); + } if (f.watermark) { const w = f.watermark; @@ -97,15 +133,21 @@ export class ImagorPathBuilder { s.push(`watermark(${params.join(',')})`); } - if (f.focal) s.push(`focal(${f.focal.x}x${f.focal.y})`); + if (f.focal) { + s.push(`focal(${f.focal.x}x${f.focal.y})`); + } if (f.round_corner) { s.push( - `round_corner(${f.round_corner.radius}${f.round_corner.color ? ',' + f.round_corner.color : ''})`, + `round_corner(${f.round_corner.radius}${f.round_corner.color ? `,${f.round_corner.color}` : ''})`, ); } - if (f.max_bytes) s.push(`max_bytes(${f.max_bytes})`); - if (f.no_upscale) s.push('no_upscale()'); + if (f.max_bytes) { + s.push(`max_bytes(${f.max_bytes})`); + } + if (f.no_upscale) { + s.push('no_upscale()'); + } return s.length ? `filters:${s.join(':')}` : ''; } diff --git a/libs/metrics/src/metrics.controller.ts b/libs/metrics/src/metrics.controller.ts index 7dfce547..180ff9c2 100644 --- a/libs/metrics/src/metrics.controller.ts +++ b/libs/metrics/src/metrics.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Res } from '@nestjs/common'; -import * as client from 'prom-client'; -import { FastifyReply } from 'fastify'; import { SkipContract } from '@shared/decorators'; +import { FastifyReply } from 'fastify'; +import * as client from 'prom-client'; @Controller('metrics') export class MetricsController { diff --git a/libs/metrics/src/metrics.module.ts b/libs/metrics/src/metrics.module.ts index 2e50db29..c5c8a395 100644 --- a/libs/metrics/src/metrics.module.ts +++ b/libs/metrics/src/metrics.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { HttpMetricsInterceptor } from '@shared/interceptors'; import { makeHistogramProvider, PrometheusModule } from '@willsoto/nestjs-prometheus'; + import { MetricsController } from './metrics.controller'; -import { HttpMetricsInterceptor } from '@shared/interceptors'; -import { APP_INTERCEPTOR } from '@nestjs/core'; @Module({ imports: [ diff --git a/libs/s3/src/s3.module-definition.ts b/libs/s3/src/s3.module-definition.ts index f4d9e9bd..e91f27b0 100644 --- a/libs/s3/src/s3.module-definition.ts +++ b/libs/s3/src/s3.module-definition.ts @@ -1,4 +1,5 @@ import { ConfigurableModuleBuilder } from '@nestjs/common'; + import type { S3ModuleOptions } from './interfaces'; export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } = diff --git a/libs/s3/src/s3.module.ts b/libs/s3/src/s3.module.ts index e47a5589..68f89c46 100644 --- a/libs/s3/src/s3.module.ts +++ b/libs/s3/src/s3.module.ts @@ -1,9 +1,11 @@ +import { S3Client } from '@aws-sdk/client-s3'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; -import type { S3ModuleOptions } from './interfaces'; -import { S3Service } from './s3.service'; -import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from './s3.module-definition'; + import { S3_CLIENT } from './constants'; -import { S3Client } from '@aws-sdk/client-s3'; +import { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } from './s3.module-definition'; +import { S3Service } from './s3.service'; + +import type { S3ModuleOptions } from './interfaces'; @Module({ providers: [ @@ -28,7 +30,9 @@ export class S3Module extends ConfigurableModuleClass implements OnApplicationSh super(); } - async onApplicationShutdown() { - this.client.destroy(); + onApplicationShutdown() { + if (this.client.destroy) { + this.client.destroy(); + } } } diff --git a/libs/s3/src/s3.service.ts b/libs/s3/src/s3.service.ts index 17a2c8bd..5e8e4699 100644 --- a/libs/s3/src/s3.service.ts +++ b/libs/s3/src/s3.service.ts @@ -1,10 +1,16 @@ +import { randomUUID } from 'node:crypto'; +import { extname } from 'node:path'; + +import { + DeleteObjectCommand, + HeadBucketCommand, + S3Client, + PutObjectCommand, +} from '@aws-sdk/client-s3'; import { Inject, Injectable } from '@nestjs/common'; -import { DeleteObjectCommand, HeadBucketCommand, S3Client } from '@aws-sdk/client-s3'; + import { S3_CLIENT } from './constants'; import { S3ModuleOptions } from './interfaces'; -import { PutObjectCommand } from '@aws-sdk/client-s3'; -import { randomUUID } from 'crypto'; -import { extname } from 'path'; import { MODULE_OPTIONS_TOKEN } from './s3.module-definition'; @Injectable() diff --git a/src/app.module.ts b/src/app.module.ts index be2f4dbd..43c307ef 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,28 +1,28 @@ -import { Module } from '@nestjs/common'; import { ConfigModule } from '@libs/config'; -import { DatabaseModule } from '@libs/database'; -import { ConfigService } from '@nestjs/config'; -import * as schema from './shared/entities'; -import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; -import { ZodValidationPipe } from 'nestjs-zod'; +import { DatabaseModule, DatabaseHealthService } from '@libs/database'; import { HealthModule } from '@libs/health'; -import { UserModule } from './user'; -import { GlobalExceptionFilter } from '@shared/error'; -import { AuthModule } from './auth/auth.module'; -import { BullModule } from '@nestjs/bullmq'; -import { MailModule } from '@shared/adapters/mail'; -import { TeamsModule } from './teams'; -import { ProjectsModule } from './projects'; -import { HttpModule } from '@nestjs/axios'; -import { MediaModule } from '@shared/media'; -import { CacheModule } from '@shared/adapters/cache/module'; +import { MetricsModule } from '@libs/metrics'; import { S3Service } from '@libs/s3'; +import { HttpModule } from '@nestjs/axios'; +import { BullModule } from '@nestjs/bullmq'; +import { Module } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { CacheModule } from '@shared/adapters/cache/module'; import { ICacheService } from '@shared/adapters/cache/ports'; -import { DatabaseHealthService } from '@libs/database'; +import { MailModule } from '@shared/adapters/mail'; +import { GlobalExceptionFilter } from '@shared/error'; import { ZodValidationInterceptor } from '@shared/interceptors'; +import { MediaModule } from '@shared/media'; +import { ZodValidationPipe } from 'nestjs-zod'; + import { AreaModule } from './area'; -import { MetricsModule } from '@libs/metrics'; +import { AuthModule } from './auth/auth.module'; +import { ProjectsModule } from './projects'; +import * as schema from './shared/entities'; +import { TeamsModule } from './teams'; +import { UserModule } from './user'; @Module({ imports: [ @@ -30,14 +30,12 @@ import { MetricsModule } from '@libs/metrics'; DatabaseModule.registerAsync({ global: true, inject: [ConfigService], - useFactory: (cfg: ConfigService) => { - return { - schema, - schemaName: cfg.getOrThrow('DB_SCHEMA'), - logging: true, - // runMigrations: false, - }; - }, + useFactory: (cfg: ConfigService) => ({ + schema, + schemaName: cfg.getOrThrow('DB_SCHEMA'), + logging: true, + // runMigrations: false, + }), }), BullModule.forRootAsync({ inject: [ConfigService], @@ -68,9 +66,9 @@ import { MetricsModule } from '@libs/metrics'; serviceName: 'gateway', version, indicators: { - database: async () => db.isAlive(), - cache: async () => cache.isAlive(), - storage: async () => s3.isAlive(), + database: () => db.isAlive(), + cache: () => cache.isAlive(), + storage: () => s3.isAlive(), }, }; }, diff --git a/src/area/application/area.facade.ts b/src/area/application/area.facade.ts index 5a721c79..b33a2466 100644 --- a/src/area/application/area.facade.ts +++ b/src/area/application/area.facade.ts @@ -1,13 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { - CreateStateUseCase, - DeleteStateUseCase, - GetStateQuery, - GetStatesQuery, - ReorderStateUseCase, - RestoreStateUseCase, - UpdateStateUseCase, -} from './use-cases/states'; + import { CreateStateDto, UpdateStateDto, @@ -22,6 +14,15 @@ import { GetAreasQuery, UpdateAreaUseCase, } from './use-cases'; +import { + CreateStateUseCase, + DeleteStateUseCase, + GetStateQuery, + GetStatesQuery, + ReorderStateUseCase, + RestoreStateUseCase, + UpdateStateUseCase, +} from './use-cases/states'; @Injectable() export class AreaFacade { diff --git a/src/area/application/controllers/area/controller.ts b/src/area/application/controllers/area/controller.ts index dbd38eb2..37a5a8e2 100644 --- a/src/area/application/controllers/area/controller.ts +++ b/src/area/application/controllers/area/controller.ts @@ -1,7 +1,9 @@ +import { Post, Body, Get, Query, Param, Delete, Put } from '@nestjs/common'; import { ApiBaseController, GetUserId } from '@shared/decorators'; + import { AreaFacade } from '../../area.facade'; -import { Post, Body, Get, Query, Param, Delete, Put } from '@nestjs/common'; import { CreateAreaDto, UpdateAreaDto } from '../../dtos'; + import { CreateAreaSwagger, DeleteAreaSwagger, diff --git a/src/area/application/controllers/area/swagger.ts b/src/area/application/controllers/area/swagger.ts index 772b9a70..8e3fcc26 100644 --- a/src/area/application/controllers/area/swagger.ts +++ b/src/area/application/controllers/area/swagger.ts @@ -9,6 +9,7 @@ import { ApiConflict, } from '@shared/error'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { CreateAreaDto, UpdateAreaDto, AreaResponse, AreasResponse } from '../../dtos'; export const CreateAreaSwagger = () => diff --git a/src/area/application/controllers/state/controller.ts b/src/area/application/controllers/state/controller.ts index 75f58bfd..40d18654 100644 --- a/src/area/application/controllers/state/controller.ts +++ b/src/area/application/controllers/state/controller.ts @@ -1,5 +1,9 @@ import { Body, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiBaseController, GetUserId, Public } from '@shared/decorators'; + +import { AreaFacade } from '../../area.facade'; +import { CreateStateDto, ReordersStatesDto, UpdateStateDto } from '../../dtos'; + import { CreateStateSwagger, FindAllStatesSwagger, @@ -9,8 +13,6 @@ import { RestoreStateSwagger, UpdateStateSwagger, } from './swagger'; -import { CreateStateDto, ReordersStatesDto, UpdateStateDto } from '../../dtos'; -import { AreaFacade } from '../../area.facade'; @ApiBaseController('area/:slug/states', 'Area States', true) export class StateController { diff --git a/src/area/application/controllers/state/swagger.ts b/src/area/application/controllers/state/swagger.ts index e0b62c27..c3872ac1 100644 --- a/src/area/application/controllers/state/swagger.ts +++ b/src/area/application/controllers/state/swagger.ts @@ -1,5 +1,6 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiBody } from '@nestjs/swagger'; +import { ApiListQuery } from '@shared/decorators'; import { ActionResponse } from '@shared/dtos'; import { ApiUnauthorized, @@ -9,6 +10,7 @@ import { ApiConflict, } from '@shared/error'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { CreateStateDto, UpdateStateDto, @@ -17,7 +19,6 @@ import { StateResponse, StatesResponse, } from '../../dtos'; -import { ApiListQuery } from '@shared/decorators'; export const FindAllStatesSwagger = () => applyDecorators( diff --git a/src/area/application/dtos/area.dto.ts b/src/area/application/dtos/area.dto.ts index fa66d03f..5490c0a6 100644 --- a/src/area/application/dtos/area.dto.ts +++ b/src/area/application/dtos/area.dto.ts @@ -1,6 +1,6 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; import { DEFAULT_VIEWS } from '@core/area/domain/entities'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const DefaultViewSchema = z .enum(DEFAULT_VIEWS) diff --git a/src/area/application/dtos/states.dto.ts b/src/area/application/dtos/states.dto.ts index 353e1e07..c6c937a6 100644 --- a/src/area/application/dtos/states.dto.ts +++ b/src/area/application/dtos/states.dto.ts @@ -1,7 +1,7 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; -import { ActionResponseSchema } from '@shared/dtos'; import { STATE_CATEGORIES, STATE_TYPES } from '@core/area/domain/entities'; +import { ActionResponseSchema } from '@shared/dtos'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const StateTypeSchema = z .enum(STATE_TYPES) diff --git a/src/area/application/use-cases/areas/create.use-case.ts b/src/area/application/use-cases/areas/create.use-case.ts index 4e702200..43b54d77 100644 --- a/src/area/application/use-cases/areas/create.use-case.ts +++ b/src/area/application/use-cases/areas/create.use-case.ts @@ -1,11 +1,12 @@ +import { AreaErrorCodes, AreaErrorMessages } from '@core/area/domain/errors'; import { IAreaRepository } from '@core/area/domain/repository'; -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { CreateAreaDto } from '../../dtos'; +import { MAX_AREAS_PER_PROJECT } from '@core/area/infrastructure/constants'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; -import slugify from 'slugify'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; -import { AreaErrorCodes, AreaErrorMessages } from '@core/area/domain/errors'; -import { MAX_AREAS_PER_PROJECT } from '@core/area/infrastructure/constants'; +import slugify from 'slugify'; + +import { CreateAreaDto } from '../../dtos'; @Injectable() export class CreateAreaUseCase { @@ -66,7 +67,9 @@ export class CreateAreaUseCase { slug: result.slug, }; } catch (e) { - if (e instanceof BaseException) throw e; + if (e instanceof BaseException) { + throw e; + } throw new BaseException( { diff --git a/src/area/application/use-cases/areas/delete.use-case.ts b/src/area/application/use-cases/areas/delete.use-case.ts index 88d24d7e..f53747c4 100644 --- a/src/area/application/use-cases/areas/delete.use-case.ts +++ b/src/area/application/use-cases/areas/delete.use-case.ts @@ -46,7 +46,9 @@ export class DeleteAreaUseCase { message: `Пространство ${area.title} успешно удалено.`, }; } catch (e) { - if (e instanceof BaseException) throw e; + if (e instanceof BaseException) { + throw e; + } throw new BaseException( { diff --git a/src/area/application/use-cases/areas/update.use-case.ts b/src/area/application/use-cases/areas/update.use-case.ts index 7182820e..1497a1b0 100644 --- a/src/area/application/use-cases/areas/update.use-case.ts +++ b/src/area/application/use-cases/areas/update.use-case.ts @@ -1,11 +1,12 @@ +import { AreaErrorCodes, AreaErrorMessages } from '@core/area/domain/errors'; import { IAreaRepository } from '@core/area/domain/repository'; -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { UpdateAreaDto } from '../../dtos'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; -import { AreaErrorCodes, AreaErrorMessages } from '@core/area/domain/errors'; import slugify from 'slugify'; +import { UpdateAreaDto } from '../../dtos'; + @Injectable() export class UpdateAreaUseCase { constructor( @@ -117,7 +118,9 @@ export class UpdateAreaUseCase { message: `Пространство ${dto.title || area.title} успешно обновлено`, }; } catch (e) { - if (e instanceof BaseException) throw e; + if (e instanceof BaseException) { + throw e; + } throw new BaseException( { diff --git a/src/area/application/use-cases/states/create.use-case.ts b/src/area/application/use-cases/states/create.use-case.ts index c8aae071..c6105468 100644 --- a/src/area/application/use-cases/states/create.use-case.ts +++ b/src/area/application/use-cases/states/create.use-case.ts @@ -1,10 +1,11 @@ +import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; +import { IStateRepository } from '@core/area/domain/repository'; +import { MAX_STATES_PER_PROJECT } from '@core/area/infrastructure/constants'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; -import { IStateRepository } from '@core/area/domain/repository'; + import { CreateStateDto } from '../../dtos'; -import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; import { GetAreaQuery } from '../areas'; -import { MAX_STATES_PER_PROJECT } from '@core/area/infrastructure/constants'; @Injectable() export class CreateStateUseCase { @@ -72,7 +73,9 @@ export class CreateStateUseCase { stateId: result.id, }; } catch (err) { - if (err instanceof BaseException) throw err; + if (err instanceof BaseException) { + throw err; + } throw new BaseException( { diff --git a/src/area/application/use-cases/states/delete.use-case.ts b/src/area/application/use-cases/states/delete.use-case.ts index 855f1f67..56abf254 100644 --- a/src/area/application/use-cases/states/delete.use-case.ts +++ b/src/area/application/use-cases/states/delete.use-case.ts @@ -2,6 +2,7 @@ import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; import { IStateRepository } from '@core/area/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; + import { GetAreaQuery } from '../areas'; @Injectable() @@ -60,8 +61,6 @@ export class DeleteStateUseCase { // ); // } - console.log(area, state); - const result = await this.stateRepo.delete(area.id, state.id); return { @@ -71,7 +70,9 @@ export class DeleteStateUseCase { : 'Не удалось удалить состояние: запись не найдена или уже удалена', }; } catch (err) { - if (err instanceof BaseException) throw err; + if (err instanceof BaseException) { + throw err; + } throw new BaseException( { diff --git a/src/area/application/use-cases/states/get-all.query.ts b/src/area/application/use-cases/states/get-all.query.ts index 46af6a71..ce632176 100644 --- a/src/area/application/use-cases/states/get-all.query.ts +++ b/src/area/application/use-cases/states/get-all.query.ts @@ -1,5 +1,6 @@ import { IStateRepository } from '@core/area/domain/repository'; import { Inject, Injectable } from '@nestjs/common'; + import { GetAreaQuery } from '../areas'; @Injectable() diff --git a/src/area/application/use-cases/states/get-one.query.ts b/src/area/application/use-cases/states/get-one.query.ts index c24ba85f..2e14a08e 100644 --- a/src/area/application/use-cases/states/get-one.query.ts +++ b/src/area/application/use-cases/states/get-one.query.ts @@ -2,6 +2,7 @@ import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; import { IStateRepository } from '@core/area/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; + import { GetAreaQuery } from '../areas'; @Injectable() diff --git a/src/area/application/use-cases/states/index.ts b/src/area/application/use-cases/states/index.ts index 584da7ab..21b070a3 100644 --- a/src/area/application/use-cases/states/index.ts +++ b/src/area/application/use-cases/states/index.ts @@ -1,7 +1,7 @@ import { CreateStateUseCase } from './create.use-case'; import { DeleteStateUseCase } from './delete.use-case'; -import { GetStateQuery } from './get-one.query'; import { GetStatesQuery } from './get-all.query'; +import { GetStateQuery } from './get-one.query'; import { ReorderStateUseCase } from './reorder.use-case'; import { RestoreStateUseCase } from './restore.use-state'; import { UpdateStateUseCase } from './update.use-case'; diff --git a/src/area/application/use-cases/states/reorder.use-case.ts b/src/area/application/use-cases/states/reorder.use-case.ts index 063f439f..f053663d 100644 --- a/src/area/application/use-cases/states/reorder.use-case.ts +++ b/src/area/application/use-cases/states/reorder.use-case.ts @@ -1,8 +1,9 @@ +import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; +import { IStateRepository } from '@core/area/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; + import { ReordersStatesDto } from '../../dtos'; -import { IStateRepository } from '@core/area/domain/repository'; -import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; import { GetAreaQuery } from '../areas'; @Injectable() @@ -38,7 +39,9 @@ export class ReorderStateUseCase { : 'Не удалось восстановить состояние: запись не найдена или уже активна', }; } catch (err) { - if (err instanceof BaseException) throw err; + if (err instanceof BaseException) { + throw err; + } throw new BaseException( { diff --git a/src/area/application/use-cases/states/restore.use-state.ts b/src/area/application/use-cases/states/restore.use-state.ts index 288cecb5..4b402357 100644 --- a/src/area/application/use-cases/states/restore.use-state.ts +++ b/src/area/application/use-cases/states/restore.use-state.ts @@ -2,6 +2,7 @@ import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; import { IStateRepository } from '@core/area/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; + import { GetAreaQuery } from '../areas'; @Injectable() @@ -39,7 +40,9 @@ export class RestoreStateUseCase { : 'Не удалось восстановить состояние: запись не найдена или уже активна', }; } catch (err) { - if (err instanceof BaseException) throw err; + if (err instanceof BaseException) { + throw err; + } throw new BaseException( { diff --git a/src/area/application/use-cases/states/update.use-case.ts b/src/area/application/use-cases/states/update.use-case.ts index 44b34d78..7d88bff3 100644 --- a/src/area/application/use-cases/states/update.use-case.ts +++ b/src/area/application/use-cases/states/update.use-case.ts @@ -1,8 +1,9 @@ +import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; +import { IStateRepository } from '@core/area/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; -import { IStateRepository } from '@core/area/domain/repository'; + import { UpdateStateDto } from '../../dtos'; -import { StateErrorCodes, StateErrorMessages } from '@core/area/domain/errors'; import { GetAreaQuery } from '../areas'; @Injectable() @@ -58,7 +59,9 @@ export class UpdateStateUseCase { : 'Не удалось обновить состояние: запись не найдена', }; } catch (err) { - if (err instanceof BaseException) throw err; + if (err instanceof BaseException) { + throw err; + } throw new BaseException( { diff --git a/src/area/area.module.ts b/src/area/area.module.ts index 184f1a62..147ddb30 100644 --- a/src/area/area.module.ts +++ b/src/area/area.module.ts @@ -1,9 +1,10 @@ +import { ProjectsModule } from '@core/projects'; import { forwardRef, Module } from '@nestjs/common'; -import { REPOSITORIES } from './infrastructure/persistence/repositories'; + import { AreaFacade } from './application/area.facade'; -import { AreasUseCases, StatesUseCases } from './application/use-cases'; import { CONTROLLERS } from './application/controllers'; -import { ProjectsModule } from '@core/projects'; +import { AreasUseCases, StatesUseCases } from './application/use-cases'; +import { REPOSITORIES } from './infrastructure/persistence/repositories'; @Module({ imports: [forwardRef(() => ProjectsModule)], diff --git a/src/area/infrastructure/persistence/models/area.model.ts b/src/area/infrastructure/persistence/models/area.model.ts index 8341d432..a7cc3e7e 100644 --- a/src/area/infrastructure/persistence/models/area.model.ts +++ b/src/area/infrastructure/persistence/models/area.model.ts @@ -1,7 +1,7 @@ -import { text, boolean, varchar, timestamp, integer, index } from 'drizzle-orm/pg-core'; import { createId } from '@paralleldrive/cuid2'; -import { isNotNull, isNull } from 'drizzle-orm'; import { baseSchema, projects, users } from '@shared/entities'; +import { isNotNull, isNull } from 'drizzle-orm'; +import { text, boolean, varchar, timestamp, integer, index } from 'drizzle-orm/pg-core'; export const areas = baseSchema.table( 'areas', diff --git a/src/area/infrastructure/persistence/models/state.model.ts b/src/area/infrastructure/persistence/models/state.model.ts index 44a6508b..93ba3f42 100644 --- a/src/area/infrastructure/persistence/models/state.model.ts +++ b/src/area/infrastructure/persistence/models/state.model.ts @@ -1,3 +1,6 @@ +import { createId } from '@paralleldrive/cuid2'; +import { baseSchema, users } from '@shared/entities'; +import { isNotNull, isNull } from 'drizzle-orm'; import { text, boolean, @@ -7,11 +10,9 @@ import { uniqueIndex, index, } from 'drizzle-orm/pg-core'; -import { createId } from '@paralleldrive/cuid2'; -import { isNotNull, isNull } from 'drizzle-orm'; -import { stateCategoryEnum, stateTypeEnum } from './enum'; -import { baseSchema, users } from '@shared/entities'; + import { areas } from './area.model'; +import { stateCategoryEnum, stateTypeEnum } from './enum'; export const states = baseSchema.table( 'states', diff --git a/src/area/infrastructure/persistence/repositories/area.repository.ts b/src/area/infrastructure/persistence/repositories/area.repository.ts index 5a095cdf..3aa05927 100644 --- a/src/area/infrastructure/persistence/repositories/area.repository.ts +++ b/src/area/infrastructure/persistence/repositories/area.repository.ts @@ -1,10 +1,12 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { IAreaRepository } from '@core/area/domain/repository'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; -import * as schema from '../models'; +import { Inject, Injectable } from '@nestjs/common'; import { and, count, eq, isNotNull, isNull } from 'drizzle-orm'; -import { IAreaRepository } from '@core/area/domain/repository'; -import type { NewArea } from '@core/area/domain/entities'; + import { DEFAULT_STATES } from '../../constants'; +import * as schema from '../models'; + +import type { NewArea } from '@core/area/domain/entities'; @Injectable() export class AreaRepository implements IAreaRepository { diff --git a/src/area/infrastructure/persistence/repositories/index.ts b/src/area/infrastructure/persistence/repositories/index.ts index fb5a38fd..f0606fa7 100644 --- a/src/area/infrastructure/persistence/repositories/index.ts +++ b/src/area/infrastructure/persistence/repositories/index.ts @@ -1,5 +1,5 @@ -import { StateRepository } from './state.repository'; import { AreaRepository } from './area.repository'; +import { StateRepository } from './state.repository'; export const REPOSITORIES = [ { diff --git a/src/area/infrastructure/persistence/repositories/state.repository.ts b/src/area/infrastructure/persistence/repositories/state.repository.ts index 2ba2d2f8..261e67a2 100644 --- a/src/area/infrastructure/persistence/repositories/state.repository.ts +++ b/src/area/infrastructure/persistence/repositories/state.repository.ts @@ -1,8 +1,10 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { IStateRepository } from '@core/area/domain/repository'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; -import * as schema from '../models'; +import { Inject, Injectable } from '@nestjs/common'; import { and, count, eq, isNotNull, isNull } from 'drizzle-orm'; -import { IStateRepository } from '@core/area/domain/repository'; + +import * as schema from '../models'; + import type { NewState } from '@core/area/domain/entities'; @Injectable() diff --git a/src/auth/application/auth.facade.ts b/src/auth/application/auth.facade.ts index 2d23fa8e..817f28bb 100644 --- a/src/auth/application/auth.facade.ts +++ b/src/auth/application/auth.facade.ts @@ -1,4 +1,15 @@ import { Injectable } from '@nestjs/common'; + +import { + OAuthResponse, + PasswordResetConfirmDto, + ResendCodeDto, + ResetPasswordDto, + SignInDto, + SignUpDto, + VerifyDto, + VerifyResetCodeDto, +} from './dtos'; import { SignInUseCase, SignUpUseCase, @@ -15,16 +26,7 @@ import { GetEnabledProvidersQuery, ResendCodeUseCase, } from './use-cases'; -import { - OAuthResponse, - PasswordResetConfirmDto, - ResendCodeDto, - ResetPasswordDto, - SignInDto, - SignUpDto, - VerifyDto, - VerifyResetCodeDto, -} from './dtos'; + import type { DeviceMetadata } from '../infrastructure/utils'; @Injectable() diff --git a/src/auth/application/controller/auth/controller.ts b/src/auth/application/controller/auth/controller.ts index 59155521..25d76a1b 100644 --- a/src/auth/application/controller/auth/controller.ts +++ b/src/auth/application/controller/auth/controller.ts @@ -1,4 +1,12 @@ +import { getDeviceMeta } from '@core/auth/infrastructure/utils'; import { Body, HttpCode, HttpStatus, Post, Req, Res, UseGuards } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiBaseController } from '@shared/decorators'; +import { BearerAuthGuard, CookieAuthGuard } from '@shared/guards'; + +import { AuthFacade } from '../../auth.facade'; +import { ResendCodeDto, SignInDto, SignUpDto, VerifyDto } from '../../dtos'; + import { PostLoginSwagger, PostLogoutSwagger, @@ -7,13 +15,8 @@ import { PostSignUpConfirmSwagger, ResendCodeSwagger, } from './swagger'; -import { ResendCodeDto, SignInDto, SignUpDto, VerifyDto } from '../../dtos'; + import type { FastifyReply, FastifyRequest } from 'fastify'; -import { BearerAuthGuard, CookieAuthGuard } from '@shared/guards'; -import { AuthFacade } from '../../auth.facade'; -import { getDeviceMeta } from '@core/auth/infrastructure/utils'; -import { ApiBaseController } from '@shared/decorators'; -import { ConfigService } from '@nestjs/config'; @ApiBaseController('auth', 'Auth') export class AuthController { diff --git a/src/auth/application/controller/auth/swagger.ts b/src/auth/application/controller/auth/swagger.ts index 08c85817..1ed906a6 100644 --- a/src/auth/application/controller/auth/swagger.ts +++ b/src/auth/application/controller/auth/swagger.ts @@ -1,5 +1,6 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; import { ApiBadRequest, ApiConflict, @@ -9,6 +10,8 @@ import { ApiUnauthorized, ApiValidationError, } from '@shared/error'; +import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { SignInDto, SignResponse, @@ -19,8 +22,6 @@ import { ResendCodeDto, ResendCodeResponse, } from '../../dtos'; -import { ActionResponse } from '@shared/dtos'; -import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; export const PostRegisterSwagger = () => applyDecorators( diff --git a/src/auth/application/controller/oauth/controller.ts b/src/auth/application/controller/oauth/controller.ts index 46bc7800..20c43062 100644 --- a/src/auth/application/controller/oauth/controller.ts +++ b/src/auth/application/controller/oauth/controller.ts @@ -1,4 +1,11 @@ +import { getDeviceMeta } from '@core/auth/infrastructure/utils'; import { Delete, Get, Param, Post, Query, Req, Res, UseGuards } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiBaseController, GetUserId, SkipContract } from '@shared/decorators'; +import { BearerAuthGuard, OAuthGuard } from '@shared/guards'; + +import { AuthFacade } from '../../auth.facade'; + import { DisconnectOAuthProviderSwagger, GetConnectedProvidersSwagger, @@ -7,13 +14,9 @@ import { OAuthCallbackSwagger, OAuthLoginSwagger, } from './swagger'; + import type { TOAuthResponse } from '../../dtos'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import { BearerAuthGuard, OAuthGuard } from '@shared/guards'; -import { AuthFacade } from '../../auth.facade'; -import { getDeviceMeta } from '@core/auth/infrastructure/utils'; -import { ApiBaseController, GetUserId, SkipContract } from '@shared/decorators'; -import { ConfigService } from '@nestjs/config'; @ApiBaseController('auth/oauth', 'OAuth') export class OAuthController { diff --git a/src/auth/application/controller/oauth/swagger.ts b/src/auth/application/controller/oauth/swagger.ts index 4fab6c7d..d02e06fc 100644 --- a/src/auth/application/controller/oauth/swagger.ts +++ b/src/auth/application/controller/oauth/swagger.ts @@ -1,6 +1,7 @@ import { OAuthProvider } from '@core/auth/infrastructure/constants'; import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; import { ApiBadRequest, ApiConflict, @@ -8,9 +9,9 @@ import { ApiUnauthorized, ApiValidationError, } from '@shared/error'; -import { ConnectedProviders, ConnectProviderResponse, ProvidersResponse } from '../../dtos'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; -import { ActionResponse } from '@shared/dtos'; + +import { ConnectedProviders, ConnectProviderResponse, ProvidersResponse } from '../../dtos'; export const OAuthLoginSwagger = () => applyDecorators( diff --git a/src/auth/application/controller/recovery/controller.ts b/src/auth/application/controller/recovery/controller.ts index e274427e..3c3a8976 100644 --- a/src/auth/application/controller/recovery/controller.ts +++ b/src/auth/application/controller/recovery/controller.ts @@ -1,12 +1,14 @@ -import { ApiBaseController } from '@shared/decorators'; import { Body, Post } from '@nestjs/common'; +import { ApiBaseController } from '@shared/decorators'; + +import { AuthFacade } from '../../auth.facade'; +import { PasswordResetConfirmDto, ResetPasswordDto, VerifyResetCodeDto } from '../../dtos'; + import { PostPasswordResetConfirmSwagger, PostPasswordResetSwagger, PostPasswordResetVerifySwagger, } from './swagger'; -import { PasswordResetConfirmDto, ResetPasswordDto, VerifyResetCodeDto } from '../../dtos'; -import { AuthFacade } from '../../auth.facade'; @ApiBaseController('auth', 'Auth Recovery') export class AuthRecoveryController { diff --git a/src/auth/application/controller/recovery/swagger.ts b/src/auth/application/controller/recovery/swagger.ts index 64fa9089..01a25159 100644 --- a/src/auth/application/controller/recovery/swagger.ts +++ b/src/auth/application/controller/recovery/swagger.ts @@ -1,5 +1,6 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ActionResponse } from '@shared/dtos'; import { ApiBadRequest, ApiErrorResponse, @@ -8,6 +9,8 @@ import { ApiUnauthorized, ApiValidationError, } from '@shared/error'; +import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { ChangePasswordDto, Confirm2FaDto, @@ -19,8 +22,6 @@ import { SessionsResponse, SessionResponse, } from '../../dtos'; -import { ActionResponse } from '@shared/dtos'; -import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; export const PostPasswordResetSwagger = () => applyDecorators( diff --git a/src/auth/application/dtos/2fa.dto.ts b/src/auth/application/dtos/2fa.dto.ts index 36f28f7d..e254451b 100644 --- a/src/auth/application/dtos/2fa.dto.ts +++ b/src/auth/application/dtos/2fa.dto.ts @@ -1,5 +1,5 @@ -import z from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; +import z from 'zod/v4'; export const Confirm2FaSchema = z .object({ diff --git a/src/auth/application/dtos/oauth.dto.ts b/src/auth/application/dtos/oauth.dto.ts index e1c6fbb4..3188ef95 100644 --- a/src/auth/application/dtos/oauth.dto.ts +++ b/src/auth/application/dtos/oauth.dto.ts @@ -1,5 +1,5 @@ -import { z } from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; const OAuthResponseSchema = z.object({ id: z.string(), diff --git a/src/auth/application/dtos/session.dto.ts b/src/auth/application/dtos/session.dto.ts index fc4d95f5..f1b7a5f2 100644 --- a/src/auth/application/dtos/session.dto.ts +++ b/src/auth/application/dtos/session.dto.ts @@ -1,5 +1,5 @@ -import { z } from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const SessionResponseSchema = z .object({ diff --git a/src/auth/application/strategies/index.ts b/src/auth/application/strategies/index.ts index 17b7ffa3..4c601266 100644 --- a/src/auth/application/strategies/index.ts +++ b/src/auth/application/strategies/index.ts @@ -1,9 +1,11 @@ // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ResendCodeDto } from '../dtos'; + import { ResetPasswordResendStrategy } from './reset-password-resend.strategy'; -import type { ResendCodeStrategy } from './resend-code.strategy'; import { SignUpResendStrategy } from './sign-up-resend.strategy'; +import type { ResendCodeStrategy } from './resend-code.strategy'; + export const RESEND_CODE_STRATEGIES: Record = { 'sign-up': new SignUpResendStrategy(), 'reset-password': new ResetPasswordResendStrategy(), diff --git a/src/auth/application/strategies/resend-code.strategy.ts b/src/auth/application/strategies/resend-code.strategy.ts index 593f4bdd..77cd5c88 100644 --- a/src/auth/application/strategies/resend-code.strategy.ts +++ b/src/auth/application/strategies/resend-code.strategy.ts @@ -1,7 +1,8 @@ -import type { Queue } from 'bullmq'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { ResendCodeDto } from '../dtos'; +import type { Queue } from 'bullmq'; + export abstract class ResendCodeStrategy { abstract context: ResendCodeDto['context']; abstract successMessage: string; diff --git a/src/auth/application/strategies/reset-password-resend.strategy.ts b/src/auth/application/strategies/reset-password-resend.strategy.ts index ab71a34a..6c93b3ec 100644 --- a/src/auth/application/strategies/reset-password-resend.strategy.ts +++ b/src/auth/application/strategies/reset-password-resend.strategy.ts @@ -1,14 +1,16 @@ import { AuthMailJobs } from '@core/auth/domain/enums'; import { ResetPasswordEvent } from '@core/auth/domain/events'; -import type { ResetPasswordCacheData } from '@core/auth/application/interfaces'; import { EMAIL_CODE_TTL_SECONDS, RESET_PASSWORD_CACHE_KEY, } from '@core/auth/infrastructure/constants'; -import type { Queue } from 'bullmq'; import { generate, generateSecret } from 'otplib'; + import { ResendCodeStrategy } from './resend-code.strategy'; +import type { ResetPasswordCacheData } from '@core/auth/application/interfaces'; +import type { Queue } from 'bullmq'; + export class ResetPasswordResendStrategy extends ResendCodeStrategy { readonly context = 'reset-password' as const; readonly successMessage = 'Повторный код для восстановления пароля отправлен на вашу почту'; diff --git a/src/auth/application/strategies/sign-up-resend.strategy.ts b/src/auth/application/strategies/sign-up-resend.strategy.ts index 1e323fa9..e66f8c04 100644 --- a/src/auth/application/strategies/sign-up-resend.strategy.ts +++ b/src/auth/application/strategies/sign-up-resend.strategy.ts @@ -1,11 +1,13 @@ import { AuthMailJobs } from '@core/auth/domain/enums'; import { RegisterCodeEvent } from '@core/auth/domain/events'; -import type { SignUpCacheData } from '@core/auth/application/interfaces'; import { EMAIL_CODE_TTL_SECONDS, SIGNUP_CACHE_KEY } from '@core/auth/infrastructure/constants'; -import type { Queue } from 'bullmq'; import { generate, generateSecret } from 'otplib'; + import { ResendCodeStrategy } from './resend-code.strategy'; +import type { SignUpCacheData } from '@core/auth/application/interfaces'; +import type { Queue } from 'bullmq'; + export class SignUpResendStrategy extends ResendCodeStrategy { readonly context = 'sign-up' as const; readonly successMessage = 'Повторный код подтверждения отправлен на вашу почту'; 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 ffb36460..1c7dd7b8 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 @@ -1,10 +1,11 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import * as argon from 'argon2'; -import { BaseException } from '@shared/error'; -import { PasswordResetConfirmDto } from '../dtos'; import { UpdatePasswordUseCase } from '@core/user'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; +import * as argon from 'argon2'; + +import { PasswordResetConfirmDto } from '../dtos'; @Injectable() export class ConfirmResetPasswordUseCase { diff --git a/src/auth/application/use-cases/index.ts b/src/auth/application/use-cases/index.ts index 330cd522..917ffe51 100644 --- a/src/auth/application/use-cases/index.ts +++ b/src/auth/application/use-cases/index.ts @@ -1,42 +1,21 @@ import { ConfirmResetPasswordUseCase } from './confirm-reset-password.use-case'; -import { VerifyResetPasswordUseCase } from './verify-reset-password.use-case'; -import { DisconnectProviderUseCase } from './oauth/disconnect-provider.use-case'; import { AuthenticateOAuthUseCase } from './oauth/authenticate-oauth.use-case'; +import { ConnectOAuthProviderUseCase } from './oauth/connect-oauth-provider.use-case'; import { ConnectProviderUseCase } from './oauth/connect-provider.use-case'; -import { RefreshTokensUseCase } from './refresh-tokens.use-case'; -import { ResetPasswordUseCase } from './reset-password.use-case'; -import { SignUpVerifyUseCase } from './sign-up-verify.use-case'; -import { SignInUseCase } from './sign-in.use-case'; -import { SignOutUseCase } from './sign-out.use-case'; -import { SignUpUseCase } from './sign-up.use-case'; +import { DisconnectProviderUseCase } from './oauth/disconnect-provider.use-case'; import { GetConnectedProvidersQuery } from './oauth/get-connected-providers.query'; import { GetEnabledProvidersQuery } from './oauth/get-enabled-providers.query'; import { OAuthOrchestratorUseCase } from './oauth/oauth-orchestrator.use-case'; import { ProcessOAuthLoginUseCase } from './oauth/process-oauth-login.use-case'; import { ProcessOAuthRegistrationUseCase } from './oauth/process-oauth-registration.use-case'; -import { ConnectOAuthProviderUseCase } from './oauth/connect-oauth-provider.use-case'; +import { RefreshTokensUseCase } from './refresh-tokens.use-case'; import { ResendCodeUseCase } from './resend-code.use-case'; - -export { - ConfirmResetPasswordUseCase, - VerifyResetPasswordUseCase, - GetConnectedProvidersQuery, - DisconnectProviderUseCase, - AuthenticateOAuthUseCase, - ConnectProviderUseCase, - RefreshTokensUseCase, - ResetPasswordUseCase, - SignUpVerifyUseCase, - GetEnabledProvidersQuery, - OAuthOrchestratorUseCase, - ProcessOAuthLoginUseCase, - ProcessOAuthRegistrationUseCase, - ConnectOAuthProviderUseCase, - SignInUseCase, - SignOutUseCase, - SignUpUseCase, - ResendCodeUseCase, -}; +import { ResetPasswordUseCase } from './reset-password.use-case'; +import { SignInUseCase } from './sign-in.use-case'; +import { SignOutUseCase } from './sign-out.use-case'; +import { SignUpVerifyUseCase } from './sign-up-verify.use-case'; +import { SignUpUseCase } from './sign-up.use-case'; +import { VerifyResetPasswordUseCase } from './verify-reset-password.use-case'; export const AuthUseCases = [ ConfirmResetPasswordUseCase, @@ -59,3 +38,23 @@ export const AuthUseCases = [ SignUpUseCase, ResendCodeUseCase, ]; + +export { ConfirmResetPasswordUseCase } from './confirm-reset-password.use-case'; +export { VerifyResetPasswordUseCase } from './verify-reset-password.use-case'; +export { GetConnectedProvidersQuery } from './oauth/get-connected-providers.query'; +export { DisconnectProviderUseCase } from './oauth/disconnect-provider.use-case'; +export { AuthenticateOAuthUseCase } from './oauth/authenticate-oauth.use-case'; +export { ConnectProviderUseCase } from './oauth/connect-provider.use-case'; +export { RefreshTokensUseCase } from './refresh-tokens.use-case'; +export { ResetPasswordUseCase } from './reset-password.use-case'; +export { SignUpVerifyUseCase } from './sign-up-verify.use-case'; +export { GetEnabledProvidersQuery } from './oauth/get-enabled-providers.query'; + +export { OAuthOrchestratorUseCase } from './oauth/oauth-orchestrator.use-case'; +export { ProcessOAuthLoginUseCase } from './oauth/process-oauth-login.use-case'; +export { ProcessOAuthRegistrationUseCase } from './oauth/process-oauth-registration.use-case'; +export { ConnectOAuthProviderUseCase } from './oauth/connect-oauth-provider.use-case'; +export { SignInUseCase } from './sign-in.use-case'; +export { SignOutUseCase } from './sign-out.use-case'; +export { SignUpUseCase } from './sign-up.use-case'; +export { ResendCodeUseCase } from './resend-code.use-case'; diff --git a/src/auth/application/use-cases/oauth/authenticate-oauth.use-case.ts b/src/auth/application/use-cases/oauth/authenticate-oauth.use-case.ts index 72688903..deeaebf6 100644 --- a/src/auth/application/use-cases/oauth/authenticate-oauth.use-case.ts +++ b/src/auth/application/use-cases/oauth/authenticate-oauth.use-case.ts @@ -1,11 +1,13 @@ import { ISessionRepository } from '@core/auth/domain/repository'; import { TokenService } from '@core/auth/infrastructure/security'; import { Inject, Injectable } from '@nestjs/common'; -import type { OAuthResponse } from '../../dtos'; -import type { DeviceMetadata } from '@core/auth/infrastructure/utils'; import { createId } from '@paralleldrive/cuid2'; + import { OAuthOrchestratorUseCase } from './oauth-orchestrator.use-case'; +import type { OAuthResponse } from '../../dtos'; +import type { DeviceMetadata } from '@core/auth/infrastructure/utils'; + @Injectable() export class AuthenticateOAuthUseCase { constructor( diff --git a/src/auth/application/use-cases/oauth/connect-oauth-provider.use-case.ts b/src/auth/application/use-cases/oauth/connect-oauth-provider.use-case.ts index d1347a71..29f4c7ca 100644 --- a/src/auth/application/use-cases/oauth/connect-oauth-provider.use-case.ts +++ b/src/auth/application/use-cases/oauth/connect-oauth-provider.use-case.ts @@ -3,9 +3,10 @@ import { FindUserQuery } from '@core/user'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; -import { OAuthResponse } from '../../dtos'; import { BaseException } from '@shared/error'; +import { OAuthResponse } from '../../dtos'; + @Injectable() export class ConnectOAuthProviderUseCase { constructor( diff --git a/src/auth/application/use-cases/oauth/connect-provider.use-case.ts b/src/auth/application/use-cases/oauth/connect-provider.use-case.ts index 2ff7646e..9d8115f9 100644 --- a/src/auth/application/use-cases/oauth/connect-provider.use-case.ts +++ b/src/auth/application/use-cases/oauth/connect-provider.use-case.ts @@ -1,10 +1,10 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { IIdentityRepository } from '@core/auth/domain/repository'; import { FindUserQuery } from '@core/user'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { createId } from '@paralleldrive/cuid2'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; import { BaseException } from '@shared/error'; -import { IIdentityRepository } from '@core/auth/domain/repository'; @Injectable() export class ConnectProviderUseCase { diff --git a/src/auth/application/use-cases/oauth/disconnect-provider.use-case.ts b/src/auth/application/use-cases/oauth/disconnect-provider.use-case.ts index f35adf3e..4b914530 100644 --- a/src/auth/application/use-cases/oauth/disconnect-provider.use-case.ts +++ b/src/auth/application/use-cases/oauth/disconnect-provider.use-case.ts @@ -1,6 +1,6 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { IIdentityRepository } from '@core/auth/domain/repository'; import { FindUserQuery } from '@core/user'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; @Injectable() diff --git a/src/auth/application/use-cases/oauth/oauth-orchestrator.use-case.ts b/src/auth/application/use-cases/oauth/oauth-orchestrator.use-case.ts index bbf12fe1..e8fa0999 100644 --- a/src/auth/application/use-cases/oauth/oauth-orchestrator.use-case.ts +++ b/src/auth/application/use-cases/oauth/oauth-orchestrator.use-case.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; +import { BaseException, type IErrorOptions } from '@shared/error'; + +import { OAuthResponse } from '../../dtos'; + +import { ConnectOAuthProviderUseCase } from './connect-oauth-provider.use-case'; import { ProcessOAuthLoginUseCase } from './process-oauth-login.use-case'; import { ProcessOAuthRegistrationUseCase } from './process-oauth-registration.use-case'; -import { ConnectOAuthProviderUseCase } from './connect-oauth-provider.use-case'; -import { OAuthResponse } from '../../dtos'; -import { BaseException, type IErrorOptions } from '@shared/error'; // TODO: ADD TO GLOBAL function isBaseException(error: unknown): error is BaseException { @@ -25,7 +27,7 @@ export class OAuthOrchestratorUseCase { async execute(dto: OAuthResponse, state?: string) { if (state) { try { - return await this.connectProvider.execute(dto, state); + return this.connectProvider.execute(dto, state); } catch (error) { if (!isBaseExceptionWithCode(error, 'INVALID_ACTION')) { throw error; @@ -34,13 +36,13 @@ export class OAuthOrchestratorUseCase { } try { - return await this.processLogin.execute(dto); + return this.processLogin.execute(dto); } catch (error) { if (!isBaseExceptionWithCode(error, 'OAUTH_LOGIN_NOT_FOUND')) { throw error; } } - return await this.processRegistration.execute(dto); + return this.processRegistration.execute(dto); } } diff --git a/src/auth/application/use-cases/oauth/process-oauth-login.use-case.ts b/src/auth/application/use-cases/oauth/process-oauth-login.use-case.ts index 18af65b7..455ae945 100644 --- a/src/auth/application/use-cases/oauth/process-oauth-login.use-case.ts +++ b/src/auth/application/use-cases/oauth/process-oauth-login.use-case.ts @@ -1,9 +1,10 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { OAuthResponse } from '../../dtos'; import { IIdentityRepository } from '@core/auth/domain/repository'; import { FindUserQuery } from '@core/user'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; +import { OAuthResponse } from '../../dtos'; + @Injectable() export class ProcessOAuthLoginUseCase { constructor( diff --git a/src/auth/application/use-cases/oauth/process-oauth-registration.use-case.ts b/src/auth/application/use-cases/oauth/process-oauth-registration.use-case.ts index 461d114f..0131cb32 100644 --- a/src/auth/application/use-cases/oauth/process-oauth-registration.use-case.ts +++ b/src/auth/application/use-cases/oauth/process-oauth-registration.use-case.ts @@ -1,12 +1,13 @@ +import { AuthQueues, AuthUserJobs } from '@core/auth/domain/enums'; +import { CreateUserWorkspaceEvent } from '@core/auth/domain/events'; import { IIdentityRepository } from '@core/auth/domain/repository'; import { FindUserQuery, RegisterUserUseCase } from '@core/user'; +import { InjectQueue } from '@nestjs/bullmq'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { OAuthResponse } from '../../dtos'; import { BaseException } from '@shared/error'; -import { InjectQueue } from '@nestjs/bullmq'; -import { AuthQueues, AuthUserJobs } from '@core/auth/domain/enums'; import { Queue } from 'bullmq'; -import { CreateUserWorkspaceEvent } from '@core/auth/domain/events'; + +import { OAuthResponse } from '../../dtos'; @Injectable() export class ProcessOAuthRegistrationUseCase { 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 a65ccdc6..70de6f9f 100644 --- a/src/auth/application/use-cases/refresh-tokens.use-case.ts +++ b/src/auth/application/use-cases/refresh-tokens.use-case.ts @@ -1,10 +1,11 @@ +import { FindUserQuery } from '@core/user'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { createId } from '@paralleldrive/cuid2'; 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'; -import { createId } from '@paralleldrive/cuid2'; @Injectable() export class RefreshTokensUseCase { diff --git a/src/auth/application/use-cases/resend-code.use-case.ts b/src/auth/application/use-cases/resend-code.use-case.ts index d94b2913..f5546ff3 100644 --- a/src/auth/application/use-cases/resend-code.use-case.ts +++ b/src/auth/application/use-cases/resend-code.use-case.ts @@ -1,11 +1,5 @@ -import { HttpStatus, Inject, Injectable, Logger } from '@nestjs/common'; -import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; -import { ICacheService } from '@shared/adapters/cache/ports'; -import { BaseException } from '@shared/error'; -import { InjectQueue } from '@nestjs/bullmq'; -import { AuthQueues } from '@core/auth/domain/enums'; -import { Queue } from 'bullmq'; import { ResendCodeDto } from '@core/auth/application/dtos'; +import { AuthQueues } from '@core/auth/domain/enums'; import { EMAIL_CODE_TTL_SECONDS, MAX_ATTEMPTS, @@ -13,6 +7,13 @@ import { RESEND_COOLDOWN_KEY, SECONDS_BETWEEN_ATTEMPTS, } from '@core/auth/infrastructure/constants'; +import { InjectQueue } from '@nestjs/bullmq'; +import { HttpStatus, Inject, Injectable, Logger } from '@nestjs/common'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; +import { Queue } from 'bullmq'; + import { RESEND_CODE_STRATEGIES, ResendCodeStrategy } from '../strategies'; @Injectable() 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 cbc410e8..b5a8f559 100644 --- a/src/auth/application/use-cases/reset-password.use-case.ts +++ b/src/auth/application/use-cases/reset-password.use-case.ts @@ -1,19 +1,20 @@ +import { ResetPasswordCacheData } from '@core/auth/application/interfaces'; +import { + EMAIL_CODE_TTL_SECONDS, + RESET_PASSWORD_CACHE_KEY, +} from '@core/auth/infrastructure/constants'; +import { FindUserQuery } from '@core/user'; import { InjectQueue } from '@nestjs/bullmq'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; import { Queue } from 'bullmq'; import { generate, generateSecret } from 'otplib'; -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'; -import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; -import { ICacheService } from '@shared/adapters/cache/ports'; -import { - EMAIL_CODE_TTL_SECONDS, - RESET_PASSWORD_CACHE_KEY, -} from '@core/auth/infrastructure/constants'; -import { ResetPasswordCacheData } from '@core/auth/application/interfaces'; @Injectable() export class ResetPasswordUseCase { 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 07be802d..51d67cf7 100644 --- a/src/auth/application/use-cases/sign-in.use-case.ts +++ b/src/auth/application/use-cases/sign-in.use-case.ts @@ -1,12 +1,13 @@ +import { FindUserQuery } from '@core/user'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import * as argon from 'argon2'; +import { createId } from '@paralleldrive/cuid2'; import { BaseException } from '@shared/error'; +import * as argon from 'argon2'; + 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'; -import { createId } from '@paralleldrive/cuid2'; @Injectable() export class SignInUseCase { diff --git a/src/auth/application/use-cases/sign-out.use-case.ts b/src/auth/application/use-cases/sign-out.use-case.ts index 437d6a19..301a5fe6 100644 --- a/src/auth/application/use-cases/sign-out.use-case.ts +++ b/src/auth/application/use-cases/sign-out.use-case.ts @@ -1,5 +1,6 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; + import { ISessionRepository } from '../../domain/repository'; import { TokenService } from '../../infrastructure/security'; 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 ba41c8ee..3b43dd98 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 @@ -1,21 +1,22 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { verify as verifyOTP } from 'otplib'; +import { SignUpCacheData } from '@core/auth/application/interfaces'; +import { AuthQueues } from '@core/auth/domain/enums'; +import { AuthUserJobs } from '@core/auth/domain/enums/auth-jobs.enum'; +import { CreateUserWorkspaceEvent } from '@core/auth/domain/events/create-user-workspace.event'; +import { SIGNUP_CACHE_KEY } from '@core/auth/infrastructure/constants'; import { RegisterUserUseCase } from '@core/user'; +import { InjectQueue } from '@nestjs/bullmq'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { createId } from '@paralleldrive/cuid2'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; import { BaseException } from '@shared/error'; +import { Queue } from 'bullmq'; +import { verify as verifyOTP } from 'otplib'; + import { ISessionRepository } from '../../domain/repository'; import { TokenService } from '../../infrastructure/security'; import { DeviceMetadata } from '../../infrastructure/utils/get-device-meta'; import { VerifyDto } from '../dtos'; -import { createId } from '@paralleldrive/cuid2'; -import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; -import { ICacheService } from '@shared/adapters/cache/ports'; -import { SIGNUP_CACHE_KEY } from '@core/auth/infrastructure/constants'; -import { SignUpCacheData } from '@core/auth/application/interfaces'; -import { InjectQueue } from '@nestjs/bullmq'; -import { AuthQueues } from '@core/auth/domain/enums'; -import { Queue } from 'bullmq'; -import { CreateUserWorkspaceEvent } from '@core/auth/domain/events/create-user-workspace.event'; -import { AuthUserJobs } from '@core/auth/domain/enums/auth-jobs.enum'; @Injectable() export class SignUpVerifyUseCase { 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 03febe49..2e338f52 100644 --- a/src/auth/application/use-cases/sign-up.use-case.ts +++ b/src/auth/application/use-cases/sign-up.use-case.ts @@ -1,17 +1,18 @@ +import { SignUpCacheData } from '@core/auth/application/interfaces'; +import { EMAIL_CODE_TTL_SECONDS, SIGNUP_CACHE_KEY } from '@core/auth/infrastructure/constants'; +import { FindUserQuery } from '@core/user'; import { InjectQueue } from '@nestjs/bullmq'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; import * as argon from 'argon2'; import { Queue } from 'bullmq'; import { generate, generateSecret } from 'otplib'; -import { FindUserQuery } from '@core/user'; -import { BaseException } from '@shared/error'; + import { AuthQueues, AuthMailJobs } from '../../domain/enums'; import { RegisterCodeEvent } from '../../domain/events'; import { SignUpDto } from '../dtos'; -import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; -import { ICacheService } from '@shared/adapters/cache/ports'; -import { EMAIL_CODE_TTL_SECONDS, SIGNUP_CACHE_KEY } from '@core/auth/infrastructure/constants'; -import { SignUpCacheData } from '@core/auth/application/interfaces'; @Injectable() export class SignUpUseCase { diff --git a/src/auth/application/use-cases/verify-reset-password.use-case.ts b/src/auth/application/use-cases/verify-reset-password.use-case.ts index a17a7e6c..fb35deeb 100644 --- a/src/auth/application/use-cases/verify-reset-password.use-case.ts +++ b/src/auth/application/use-cases/verify-reset-password.use-case.ts @@ -1,9 +1,10 @@ import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { verify as verifyOTP } from 'otplib'; -import { BaseException } from '@shared/error'; -import { VerifyResetCodeDto } from '../dtos'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; +import { verify as verifyOTP } from 'otplib'; + +import { VerifyResetCodeDto } from '../dtos'; @Injectable() export class VerifyResetPasswordUseCase { diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 6677e994..946c9ebf 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -1,19 +1,20 @@ +import { ProjectsModule } from '@core/projects'; +import { TeamsModule } from '@core/teams'; +import { UserModule } from '@core/user'; import { BullModule } from '@nestjs/bullmq'; import { forwardRef, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtModule } from '@nestjs/jwt'; -import { UserModule } from '@core/user'; -import { CONTROLLERS } from './application/controller'; +import { MailAdapter } from '@shared/adapters/mail'; + import { AuthFacade } from './application/auth.facade'; +import { CONTROLLERS } from './application/controller'; import { AuthUseCases } from './application/use-cases'; import { AuthQueues } from './domain/enums'; +import { REPOSITORIES } from './infrastructure/persistence/repositories'; import { TokenService } from './infrastructure/security'; -import { MailProcessor, UserProcessor } from './infrastructure/workers'; -import { MailAdapter } from '@shared/adapters/mail'; import { STRATEGIES } from './infrastructure/strategies'; -import { REPOSITORIES } from './infrastructure/persistence/repositories'; -import { TeamsModule } from '@core/teams'; -import { ProjectsModule } from '@core/projects'; +import { MailProcessor, UserProcessor } from './infrastructure/workers'; const WORKERS = [MailProcessor, UserProcessor]; @@ -21,7 +22,7 @@ const WORKERS = [MailProcessor, UserProcessor]; imports: [ JwtModule.registerAsync({ inject: [ConfigService], - useFactory: async (cfg: ConfigService) => ({ + useFactory: (cfg: ConfigService) => ({ secret: cfg.get('JWT_ACCESS_SECRET'), signOptions: { /** diff --git a/src/auth/infrastructure/persistence/models/identity.model.ts b/src/auth/infrastructure/persistence/models/identity.model.ts index 12e14ead..bdeeb174 100644 --- a/src/auth/infrastructure/persistence/models/identity.model.ts +++ b/src/auth/infrastructure/persistence/models/identity.model.ts @@ -1,6 +1,6 @@ import { createId } from '@paralleldrive/cuid2'; -import { text, timestamp, varchar, unique } from 'drizzle-orm/pg-core'; import { baseSchema, users } from '@shared/entities'; +import { text, timestamp, varchar, unique } from 'drizzle-orm/pg-core'; export const userIdentities = baseSchema.table( 'user_identities', diff --git a/src/auth/infrastructure/persistence/models/session.model.ts b/src/auth/infrastructure/persistence/models/session.model.ts index 56a32079..98495f7f 100644 --- a/src/auth/infrastructure/persistence/models/session.model.ts +++ b/src/auth/infrastructure/persistence/models/session.model.ts @@ -1,6 +1,6 @@ import { createId } from '@paralleldrive/cuid2'; -import { text, timestamp, varchar, boolean } from 'drizzle-orm/pg-core'; import { baseSchema, users } from '@shared/entities'; +import { text, timestamp, varchar, boolean } from 'drizzle-orm/pg-core'; export const sessions = baseSchema.table('sessions', { id: text('id') diff --git a/src/auth/infrastructure/persistence/repositories/identity.repository.ts b/src/auth/infrastructure/persistence/repositories/identity.repository.ts index de56f08f..311f44a7 100644 --- a/src/auth/infrastructure/persistence/repositories/identity.repository.ts +++ b/src/auth/infrastructure/persistence/repositories/identity.repository.ts @@ -1,9 +1,10 @@ +import { IIdentityRepository } from '@core/auth/domain/repository'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; -import * as schema from '../models/identity.model'; import { Inject, Injectable } from '@nestjs/common'; -import { IIdentityRepository } from '@core/auth/domain/repository'; import { and, eq } from 'drizzle-orm'; +import * as schema from '../models/identity.model'; + @Injectable() export class IdentitiyRepository implements IIdentityRepository { constructor( @@ -29,12 +30,11 @@ export class IdentitiyRepository implements IIdentityRepository { return result.count.valueOf() > 0; }; - public readonly findAllByUserId = async (userId: string) => { - return this.db + public readonly findAllByUserId = async (userId: string) => + this.db .select() .from(schema.userIdentities) .where(eq(schema.userIdentities.userId, userId)); - }; public readonly findByProvider = async ( provider: 'google' | 'yandex' | 'github', diff --git a/src/auth/infrastructure/persistence/repositories/session.repository.ts b/src/auth/infrastructure/persistence/repositories/session.repository.ts index b5445199..13c9996b 100644 --- a/src/auth/infrastructure/persistence/repositories/session.repository.ts +++ b/src/auth/infrastructure/persistence/repositories/session.repository.ts @@ -1,8 +1,9 @@ +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import { Inject, Injectable } from '@nestjs/common'; import { eq, and, ne, lt, desc } from 'drizzle-orm'; -import * as schema from '../models/session.model'; -import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; + import { ISessionRepository, type SessionInsert } from '../../../domain/repository'; +import * as schema from '../models/session.model'; @Injectable() export class SessionRepository implements ISessionRepository { diff --git a/src/auth/infrastructure/security/token.service.ts b/src/auth/infrastructure/security/token.service.ts index 5cbe5527..dae16d8c 100644 --- a/src/auth/infrastructure/security/token.service.ts +++ b/src/auth/infrastructure/security/token.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; -import type { JwtPayload } from '@shared/types'; +import { JwtService } from '@nestjs/jwt'; + import type { User } from '@core/user'; +import type { JwtPayload } from '@shared/types'; @Injectable() export class TokenService { diff --git a/src/auth/infrastructure/strategies/bearer.strategy.ts b/src/auth/infrastructure/strategies/bearer.strategy.ts index 06e735d2..713af0a6 100644 --- a/src/auth/infrastructure/strategies/bearer.strategy.ts +++ b/src/auth/infrastructure/strategies/bearer.strategy.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; -import type { JwtPayload } from '@shared/types'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy, ExtractJwt } from 'passport-jwt'; +import type { JwtPayload } from '@shared/types'; + @Injectable() export class BearerStrategy extends PassportStrategy(Strategy, 'bearer') { constructor(cfg: ConfigService) { diff --git a/src/auth/infrastructure/strategies/cookie.strategy.ts b/src/auth/infrastructure/strategies/cookie.strategy.ts index c7b65b9f..96b89688 100644 --- a/src/auth/infrastructure/strategies/cookie.strategy.ts +++ b/src/auth/infrastructure/strategies/cookie.strategy.ts @@ -1,10 +1,11 @@ -import { PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; import { HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import type { FastifyRequest } from 'fastify'; -import type { JwtPayload } from '@shared/types'; +import { PassportStrategy } from '@nestjs/passport'; import { BaseException } from '@shared/error'; +import { ExtractJwt, Strategy } from 'passport-jwt'; + +import type { JwtPayload } from '@shared/types'; +import type { FastifyRequest } from 'fastify'; @Injectable() export class CookieStrategy extends PassportStrategy(Strategy, 'cookie') { diff --git a/src/auth/infrastructure/strategies/vkontakte.strategy.ts b/src/auth/infrastructure/strategies/vkontakte.strategy.ts index 7874fb4d..e7d2d189 100644 --- a/src/auth/infrastructure/strategies/vkontakte.strategy.ts +++ b/src/auth/infrastructure/strategies/vkontakte.strategy.ts @@ -1,9 +1,9 @@ +import { HttpService } from '@nestjs/axios'; import { HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; -import { Strategy } from 'passport-oauth2'; import { BaseException } from '@shared/error'; -import { HttpService } from '@nestjs/axios'; +import { Strategy } from 'passport-oauth2'; import { firstValueFrom } from 'rxjs'; export interface IVKUserInfo { @@ -111,7 +111,7 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau }); } - async validate( + validate( _req: never, _at: never, _rt: never, @@ -200,7 +200,9 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau return this.parseProfile(data.response[0]); } catch (error) { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } console.error('Failed to get VK user info:', error); @@ -239,7 +241,7 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau familyName: json.last_name || '', givenName: json.first_name || '', }, - gender: gender, + gender, emails: [], photos: finalPhotos, _raw: JSON.stringify(json), diff --git a/src/auth/infrastructure/strategies/yandex.strategy.ts b/src/auth/infrastructure/strategies/yandex.strategy.ts index 593c8c5f..8ebbdba3 100644 --- a/src/auth/infrastructure/strategies/yandex.strategy.ts +++ b/src/auth/infrastructure/strategies/yandex.strategy.ts @@ -1,9 +1,9 @@ +import { HttpService } from '@nestjs/axios'; import { HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; -import { Strategy } from 'passport-oauth2'; import { BaseException } from '@shared/error'; -import { HttpService } from '@nestjs/axios'; +import { Strategy } from 'passport-oauth2'; import { firstValueFrom } from 'rxjs'; export interface IUserInfo { @@ -67,7 +67,7 @@ export class YandexStrategy extends PassportStrategy(Strategy, 'yandex-oauth') { }); } - async validate( + validate( _req: never, _at: string, _rt: string, diff --git a/src/auth/infrastructure/utils/get-device-meta.ts b/src/auth/infrastructure/utils/get-device-meta.ts index 408a7a8e..34e259ec 100644 --- a/src/auth/infrastructure/utils/get-device-meta.ts +++ b/src/auth/infrastructure/utils/get-device-meta.ts @@ -1,6 +1,7 @@ -import type { FastifyRequest } from 'fastify'; import { UAParser } from 'ua-parser-js'; +import type { FastifyRequest } from 'fastify'; + export interface DeviceMetadata { readonly ip: string; readonly userAgent: string; @@ -17,8 +18,12 @@ export function getDeviceMeta(req: FastifyRequest): DeviceMetadata { const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.ip || '0.0.0.0'; let deviceType: 'mobile' | 'desktop' | 'tablet' = 'desktop'; - if (res.device.type === 'mobile') deviceType = 'mobile'; - if (res.device.type === 'tablet') deviceType = 'tablet'; + if (res.device.type === 'mobile') { + deviceType = 'mobile'; + } + if (res.device.type === 'tablet') { + deviceType = 'tablet'; + } return { ip, diff --git a/src/auth/infrastructure/workers/mail.processor.ts b/src/auth/infrastructure/workers/mail.processor.ts index 09e2b01d..9277a8d8 100644 --- a/src/auth/infrastructure/workers/mail.processor.ts +++ b/src/auth/infrastructure/workers/mail.processor.ts @@ -1,9 +1,11 @@ import { Processor, WorkerHost } from '@nestjs/bullmq'; -import type { Job } from 'bullmq'; -import { IMailPort } from '@shared/adapters/mail'; import { Inject } from '@nestjs/common'; -import { RegisterCodeEvent, ResetPasswordEvent } from '../../domain/events'; +import { IMailPort } from '@shared/adapters/mail'; + import { AuthMailJobs, AuthQueues } from '../../domain/enums'; +import { RegisterCodeEvent, ResetPasswordEvent } from '../../domain/events'; + +import type { Job } from 'bullmq'; @Processor(AuthQueues.AUTH_MAIL) export class MailProcessor extends WorkerHost { diff --git a/src/auth/infrastructure/workers/user.processor.ts b/src/auth/infrastructure/workers/user.processor.ts index cd615af1..7fe27669 100644 --- a/src/auth/infrastructure/workers/user.processor.ts +++ b/src/auth/infrastructure/workers/user.processor.ts @@ -1,10 +1,10 @@ -import { Processor, WorkerHost } from '@nestjs/bullmq'; import { AuthQueues } from '@core/auth/domain/enums'; -import { Job } from 'bullmq'; -import { CreateTeamUseCase } from '@core/teams/application/use-cases'; -import { CreateProjectUseCase } from '@core/projects/application/use-cases'; import { AuthUserJobs } from '@core/auth/domain/enums/auth-jobs.enum'; import { CreateUserWorkspaceEvent } from '@core/auth/domain/events'; +import { CreateProjectUseCase } from '@core/projects/application/use-cases'; +import { CreateTeamUseCase } from '@core/teams/application/use-cases'; +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { Job } from 'bullmq'; import slugify from 'slugify'; @Processor(AuthQueues.AUTH_USER) diff --git a/src/main.ts b/src/main.ts index 009bef90..bcd8ca0a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ import { bootstrapApp } from '@libs/bootstrap'; + import { AppModule } from './app.module'; bootstrapApp({ @@ -22,4 +23,7 @@ RESTful API сервиса управления задачами (Task Tracker). }, useCors: true, useCookieParser: true, +}).catch((error) => { + console.error('Failed to bootstrap app:', error); + process.exit(1); }); diff --git a/src/projects/application/controller/index.ts b/src/projects/application/controller/index.ts index ab511967..a8d00bab 100644 --- a/src/projects/application/controller/index.ts +++ b/src/projects/application/controller/index.ts @@ -1,4 +1,4 @@ -import { ProjectsController } from './projects/controller'; import { ProjectMembersController } from './members/controller'; +import { ProjectsController } from './projects/controller'; export const CONTROLLERS = [ProjectsController, ProjectMembersController]; diff --git a/src/projects/application/controller/members/controller.ts b/src/projects/application/controller/members/controller.ts index d1ba6b6a..070524bf 100644 --- a/src/projects/application/controller/members/controller.ts +++ b/src/projects/application/controller/members/controller.ts @@ -1,7 +1,9 @@ import { Body, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiBaseController, GetUserId, SkipContract } from '@shared/decorators'; -import { ProjectFacade } from '../../project.facade'; + import { AddProjectMemberDto, UpdateProjectMemberDto } from '../../dtos'; +import { ProjectFacade } from '../../project.facade'; + import { AddMemberSwagger, FindAllMembersSwagger, diff --git a/src/projects/application/controller/members/swagger.ts b/src/projects/application/controller/members/swagger.ts index eff57362..b00efa0c 100644 --- a/src/projects/application/controller/members/swagger.ts +++ b/src/projects/application/controller/members/swagger.ts @@ -3,6 +3,7 @@ import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/ import { ActionResponse } from '@shared/dtos'; import { ApiForbidden, ApiNotFound, ApiUnauthorized, ApiValidationError } from '@shared/error'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { AddProjectMemberDto, ListMembersResponse, UpdateProjectMemberDto } from '../../dtos'; export const FindAllMembersSwagger = () => diff --git a/src/projects/application/controller/projects/controller.ts b/src/projects/application/controller/projects/controller.ts index 0410059c..e48dacd4 100644 --- a/src/projects/application/controller/projects/controller.ts +++ b/src/projects/application/controller/projects/controller.ts @@ -1,5 +1,9 @@ -import { ApiBaseController, GetUserId, Public } from '@shared/decorators'; import { Body, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { ApiBaseController, GetUserId, Public } from '@shared/decorators'; + +import { CreateProjectDto, CreateShareTokenDto, UpdateProjectDto } from '../../dtos'; +import { ProjectFacade } from '../../project.facade'; + import { ArchiveProjectSwagger, CheckSlugSwagger, @@ -10,8 +14,6 @@ import { RemoveProjectSwagger, UpdateProjectSwagger, } from './swagger'; -import { CreateProjectDto, CreateShareTokenDto, UpdateProjectDto } from '../../dtos'; -import { ProjectFacade } from '../../project.facade'; @ApiBaseController('teams/:teamId/projects', 'Projects', true) export class ProjectsController { diff --git a/src/projects/application/controller/projects/swagger.ts b/src/projects/application/controller/projects/swagger.ts index 8f367212..bc56a0d1 100644 --- a/src/projects/application/controller/projects/swagger.ts +++ b/src/projects/application/controller/projects/swagger.ts @@ -1,3 +1,7 @@ +import { + CheckSlugResponse, + CreateShareTokenResponse, +} from '@core/projects/application/dtos/project.dto'; import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiOperation, ApiBody, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; import { ActionResponse } from '@shared/dtos'; @@ -8,6 +12,8 @@ import { ApiNotFound, ApiConflict, } from '@shared/error'; +import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { CreateProjectDto, CreateProjectResponse, @@ -16,11 +22,6 @@ import { ProjectListResponse, ProjectDetailResponse, } from '../../dtos'; -import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; -import { - CheckSlugResponse, - CreateShareTokenResponse, -} from '@core/projects/application/dtos/project.dto'; export const CreateProjectSwagger = () => applyDecorators( diff --git a/src/projects/application/dtos/member.dto.ts b/src/projects/application/dtos/member.dto.ts index aab5ae96..d0b89141 100644 --- a/src/projects/application/dtos/member.dto.ts +++ b/src/projects/application/dtos/member.dto.ts @@ -1,6 +1,6 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; import { createPaginationSchema } from '@shared/schemas'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const ProjectMemberRoleSchema = z.enum(['owner', 'admin', 'member', 'viewer']); export type ProjectMemberRole = z.infer; diff --git a/src/projects/application/dtos/project.dto.ts b/src/projects/application/dtos/project.dto.ts index d5f74fa5..4167926b 100644 --- a/src/projects/application/dtos/project.dto.ts +++ b/src/projects/application/dtos/project.dto.ts @@ -1,10 +1,11 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; +import { PROJECT_STATUSES, PROJECT_VISIBILITIES } from '@core/projects/domain/entities'; import { ActionResponseSchema } from '@shared/dtos'; import { createPaginationSchema } from '@shared/schemas'; -import { PROJECT_STATUSES, PROJECT_VISIBILITIES } from '@core/projects/domain/entities'; -import { CreateProjectSettingsSchema, ProjectSettingsSchema } from './settings.dto'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + import { ProjectMemberRoleSchema } from './member.dto'; +import { CreateProjectSettingsSchema, ProjectSettingsSchema } from './settings.dto'; export const ProjectStatusSchema = z.enum(PROJECT_STATUSES); export const ProjectVisibilitySchema = z.enum(PROJECT_VISIBILITIES); diff --git a/src/projects/application/mappers/project.mapper.ts b/src/projects/application/mappers/project.mapper.ts index b568a19b..7580a42a 100644 --- a/src/projects/application/mappers/project.mapper.ts +++ b/src/projects/application/mappers/project.mapper.ts @@ -1,5 +1,5 @@ -import type { RawMemberRow } from '@core/teams/domain/repository'; import type { Project } from '@core/projects/domain/entities'; +import type { RawMemberRow } from '@core/teams/domain/repository'; export class ProjectMapper { public static toDetailResponse(project: Project, member?: RawMemberRow | null, token?: string) { diff --git a/src/projects/application/project.facade.ts b/src/projects/application/project.facade.ts index 783889a3..9fbbe3db 100644 --- a/src/projects/application/project.facade.ts +++ b/src/projects/application/project.facade.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import type { ProjectStatus } from '../domain/entities'; -import { CheckSlugAvailabilityQuery } from './use-cases/project/check-slug.use-case'; + import { AddProjectMemberDto, CreateProjectDto, @@ -8,6 +7,13 @@ import { UpdateProjectDto, UpdateProjectMemberDto, } from './dtos'; +import { + AddProjectMemberUseCase, + DeleteProjectMemberUseCase, + FindAllProjectMembersQuery, + GetAvailableTeamMemberQuery, + UpdateProjectMemberUseCase, +} from './use-cases'; import { CreateProjectUseCase, DeleteProjectUseCase, @@ -17,13 +23,9 @@ import { FindProjectsByTeamQuery, GetProjectDetailQuery, } from './use-cases/project'; -import { - AddProjectMemberUseCase, - DeleteProjectMemberUseCase, - FindAllProjectMembersQuery, - GetAvailableTeamMemberQuery, - UpdateProjectMemberUseCase, -} from './use-cases'; +import { CheckSlugAvailabilityQuery } from './use-cases/project/check-slug.use-case'; + +import type { ProjectStatus } from '../domain/entities'; @Injectable() export class ProjectFacade { diff --git a/src/projects/application/use-cases/member/add.use-case.ts b/src/projects/application/use-cases/member/add.use-case.ts index 1f2ee28a..e8788d1f 100644 --- a/src/projects/application/use-cases/member/add.use-case.ts +++ b/src/projects/application/use-cases/member/add.use-case.ts @@ -1,11 +1,12 @@ +import { MemberErrorCodes, MemberErrorMessages } from '@core/projects/domain/errors'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; import { IMemberRepository } from '@core/projects/domain/repository'; +import { MAX_MEMBERS_PER_PROJECT } from '@core/projects/infrastructure/constants'; +import { FindTeamMemberQuery } from '@core/teams'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; + import { AddProjectMemberDto } from '../../dtos'; -import { MemberErrorCodes, MemberErrorMessages } from '@core/projects/domain/errors'; -import { FindTeamMemberQuery } from '@core/teams'; -import { MAX_MEMBERS_PER_PROJECT } from '@core/projects/infrastructure/constants'; @Injectable() export class AddProjectMemberUseCase { diff --git a/src/projects/application/use-cases/member/find-all.query.ts b/src/projects/application/use-cases/member/find-all.query.ts index ca2a416f..e60ec9d8 100644 --- a/src/projects/application/use-cases/member/find-all.query.ts +++ b/src/projects/application/use-cases/member/find-all.query.ts @@ -1,8 +1,9 @@ import { ProjectAccessPolicy } from '@core/projects/domain/policy'; import { IMemberRepository } from '@core/projects/domain/repository'; +import { FindByIdsQuery } from '@core/user/application/use-cases'; import { Inject, Injectable } from '@nestjs/common'; + import { MemberMapper } from '../../mappers/member.mapper'; -import { FindByIdsQuery } from '@core/user/application/use-cases'; @Injectable() export class FindAllProjectMembersQuery { diff --git a/src/projects/application/use-cases/member/update.use-case.ts b/src/projects/application/use-cases/member/update.use-case.ts index ca66d6a1..eb7815c4 100644 --- a/src/projects/application/use-cases/member/update.use-case.ts +++ b/src/projects/application/use-cases/member/update.use-case.ts @@ -1,9 +1,10 @@ +import { MemberErrorCodes, MemberErrorMessages } from '@core/projects/domain/errors'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; import { IMemberRepository } from '@core/projects/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; + import { UpdateProjectMemberDto } from '../../dtos'; -import { MemberErrorCodes, MemberErrorMessages } from '@core/projects/domain/errors'; @Injectable() export class UpdateProjectMemberUseCase { @@ -40,16 +41,17 @@ export class UpdateProjectMemberUseCase { ); } - if (targetMember.role === 'admin' || dto.role === 'admin') { - if (currentMember.role !== 'owner') { - throw new BaseException( - { - code: MemberErrorCodes.ADMIN_CHANGE_FORBIDDEN, - message: MemberErrorMessages[MemberErrorCodes.ADMIN_CHANGE_FORBIDDEN], - }, - HttpStatus.FORBIDDEN, - ); - } + if ( + (targetMember.role === 'admin' || dto.role === 'admin') && + currentMember.role !== 'owner' + ) { + throw new BaseException( + { + code: MemberErrorCodes.ADMIN_CHANGE_FORBIDDEN, + message: MemberErrorMessages[MemberErrorCodes.ADMIN_CHANGE_FORBIDDEN], + }, + HttpStatus.FORBIDDEN, + ); } if (targetMember.role === dto.role) { diff --git a/src/projects/application/use-cases/project/create.use-case.ts b/src/projects/application/use-cases/project/create.use-case.ts index cad2ae1b..dbf687b5 100644 --- a/src/projects/application/use-cases/project/create.use-case.ts +++ b/src/projects/application/use-cases/project/create.use-case.ts @@ -1,12 +1,13 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { CreateProjectDto } from '../../dtos'; -import { IProjectRepository } from '@core/projects/domain/repository'; import { PROJECT_STATUSES } from '@core/projects/domain/entities'; +import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; +import { IProjectRepository } from '@core/projects/domain/repository'; +import { MAX_PROJECTS_PER_TEAM } from '@core/projects/infrastructure/constants'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; -import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; import slugify from 'slugify'; -import { MAX_PROJECTS_PER_TEAM } from '@core/projects/infrastructure/constants'; + +import { CreateProjectDto } from '../../dtos'; @Injectable() export class CreateProjectUseCase { diff --git a/src/projects/application/use-cases/project/find-by-team.query.ts b/src/projects/application/use-cases/project/find-by-team.query.ts index cacf374e..b90b5db2 100644 --- a/src/projects/application/use-cases/project/find-by-team.query.ts +++ b/src/projects/application/use-cases/project/find-by-team.query.ts @@ -1,7 +1,8 @@ +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; +import { IProjectRepository } from '@core/projects/domain/repository'; import { Inject, Injectable } from '@nestjs/common'; + import { ProjectMapper } from '../../mappers'; -import { IProjectRepository } from '@core/projects/domain/repository'; -import { ProjectAccessPolicy } from '@core/projects/domain/policy'; @Injectable() export class FindProjectsByTeamQuery { diff --git a/src/projects/application/use-cases/project/find-one.query.ts b/src/projects/application/use-cases/project/find-one.query.ts index 9e2693ae..c92ee10a 100644 --- a/src/projects/application/use-cases/project/find-one.query.ts +++ b/src/projects/application/use-cases/project/find-one.query.ts @@ -1,11 +1,13 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { createHash } from 'node:crypto'; + +import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; +import { IProjectRepository } from '@core/projects/domain/repository'; import { FindTeamMemberQuery, FindTeamQuery } from '@core/teams'; -import { createHash } from 'crypto'; -import { BaseException } from '@shared/error'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { isTeamRole, ROLE_PRIORITY } from '@shared/constants'; -import { IProjectRepository } from '@core/projects/domain/repository'; +import { BaseException } from '@shared/error'; + import type { Project } from '@core/projects/domain/entities'; -import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; @Injectable() export class FindProjectQuery { diff --git a/src/projects/application/use-cases/project/generate-share-token.use-case.ts b/src/projects/application/use-cases/project/generate-share-token.use-case.ts index 3e849110..566b2d10 100644 --- a/src/projects/application/use-cases/project/generate-share-token.use-case.ts +++ b/src/projects/application/use-cases/project/generate-share-token.use-case.ts @@ -1,15 +1,17 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { CreateShareTokenDto } from '../../dtos'; -import { createHash, randomBytes } from 'crypto'; -import { BaseException } from '@shared/error'; +import { createHash, randomBytes } from 'node:crypto'; + +import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; import { ProjectAccessPolicy } from '@core/projects/domain/policy'; import { IProjectRepository } from '@core/projects/domain/repository'; -import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; import { SHARE_LINK_LENGTH, SHARE_LINK_PREFIX, SHARE_LINK_TTL_MONTHS, } from '@core/projects/infrastructure/constants'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + +import { CreateShareTokenDto } from '../../dtos'; @Injectable() export class GenerateShareTokenUseCase { diff --git a/src/projects/application/use-cases/project/get-detail.query.ts b/src/projects/application/use-cases/project/get-detail.query.ts index a6e560eb..b69915db 100644 --- a/src/projects/application/use-cases/project/get-detail.query.ts +++ b/src/projects/application/use-cases/project/get-detail.query.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; + import { ProjectMapper } from '../../mappers'; + import { FindProjectQuery } from './find-one.query'; @Injectable() diff --git a/src/projects/application/use-cases/project/index.ts b/src/projects/application/use-cases/project/index.ts index b2f0e0df..3039bda0 100644 --- a/src/projects/application/use-cases/project/index.ts +++ b/src/projects/application/use-cases/project/index.ts @@ -1,12 +1,12 @@ +import { CheckSlugAvailabilityQuery } from './check-slug.use-case'; import { CreateProjectUseCase } from './create.use-case'; import { DeleteProjectUseCase } from './delete.use-case'; +import { FindProjectsByTeamQuery } from './find-by-team.query'; +import { FindProjectQuery } from './find-one.query'; import { GenerateShareTokenUseCase } from './generate-share-token.use-case'; +import { GetProjectDetailQuery } from './get-detail.query'; import { SetProjectStatusUseCase } from './set-status.use-case'; import { UpdateProjectUseCase } from './update.use-case'; -import { FindProjectsByTeamQuery } from './find-by-team.query'; -import { GetProjectDetailQuery } from './get-detail.query'; -import { FindProjectQuery } from './find-one.query'; -import { CheckSlugAvailabilityQuery } from './check-slug.use-case'; export * from './find-by-team.query'; export * from './find-one.query'; diff --git a/src/projects/application/use-cases/project/update.use-case.ts b/src/projects/application/use-cases/project/update.use-case.ts index d8261b0e..bda09200 100644 --- a/src/projects/application/use-cases/project/update.use-case.ts +++ b/src/projects/application/use-cases/project/update.use-case.ts @@ -1,11 +1,12 @@ +import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; +import { ProjectAccessPolicy } from '@core/projects/domain/policy'; +import { IProjectRepository } from '@core/projects/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { UpdateProjectDto } from '../../dtos'; import { BaseException } from '@shared/error'; -import { IProjectRepository } from '@core/projects/domain/repository'; -import { ProjectAccessPolicy } from '@core/projects/domain/policy'; -import { ProjectErrorCodes, ProjectErrorMessages } from '@core/projects/domain/errors'; import slugify from 'slugify'; +import { UpdateProjectDto } from '../../dtos'; + @Injectable() export class UpdateProjectUseCase { constructor( diff --git a/src/projects/domain/entities/member.domain.ts b/src/projects/domain/entities/member.domain.ts index 0d5c956e..8d01cde5 100644 --- a/src/projects/domain/entities/member.domain.ts +++ b/src/projects/domain/entities/member.domain.ts @@ -1,5 +1,5 @@ -import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; import type { projectMembers } from '@shared/entities'; +import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; export type Member = InferSelectModel; export type NewMember = InferInsertModel; diff --git a/src/projects/domain/entities/project.domain.ts b/src/projects/domain/entities/project.domain.ts index 3af75f44..e39b8da6 100644 --- a/src/projects/domain/entities/project.domain.ts +++ b/src/projects/domain/entities/project.domain.ts @@ -1,8 +1,8 @@ -import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; import type { projects, projectShares, } from '../../infrastructure/persistence/models/project.model'; +import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; export type Project = InferSelectModel; export type NewProject = InferInsertModel; diff --git a/src/projects/domain/policy/project-access.policy.ts b/src/projects/domain/policy/project-access.policy.ts index 8d6cd2f1..d11a4234 100644 --- a/src/projects/domain/policy/project-access.policy.ts +++ b/src/projects/domain/policy/project-access.policy.ts @@ -1,12 +1,14 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { IMemberRepository, IProjectRepository } from '../repository'; -import { BaseException } from '@shared/error'; import { FindTeamMemberQuery, FindTeamQuery } from '@core/teams'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ROLE_PRIORITY, PROJECT_ROLE_PRIORITY } from '@shared/constants'; -import type { MemberRole } from '../entities'; -import { MemberErrorCodes, MemberErrorMessages } from '../errors/member.errors'; -import { ProjectErrorCodes, ProjectErrorMessages } from '../errors'; +import { BaseException } from '@shared/error'; + import { isTeamRole } from '../../../shared/constants/roles.constant'; +import { ProjectErrorCodes, ProjectErrorMessages } from '../errors'; +import { MemberErrorCodes, MemberErrorMessages } from '../errors/member.errors'; +import { IMemberRepository, IProjectRepository } from '../repository'; + +import type { MemberRole } from '../entities'; @Injectable() export class ProjectAccessPolicy { @@ -89,7 +91,9 @@ export class ProjectAccessPolicy { } const hasRole = minRoles.some((role) => { - if (!isTeamRole(member.role) || !isTeamRole(role)) return false; + if (!isTeamRole(member.role) || !isTeamRole(role)) { + return false; + } const memberPriority = PROJECT_ROLE_PRIORITY[member.role] ?? -1; const rolePriority = PROJECT_ROLE_PRIORITY[role] ?? -1; diff --git a/src/projects/infrastructure/persistence/models/enum.ts b/src/projects/infrastructure/persistence/models/enum.ts index 626c10e3..c03295f1 100644 --- a/src/projects/infrastructure/persistence/models/enum.ts +++ b/src/projects/infrastructure/persistence/models/enum.ts @@ -1,5 +1,5 @@ -import { baseSchema } from '@shared/entities'; import { LAYOUTS, PROJECT_STATUSES, PROJECT_VISIBILITIES } from '@core/projects/domain/entities'; +import { baseSchema } from '@shared/entities'; export const projectStatusEnum = baseSchema.enum('project_status', PROJECT_STATUSES); export const projectVisibilityEnum = baseSchema.enum('project_visibility', PROJECT_VISIBILITIES); diff --git a/src/projects/infrastructure/persistence/models/project.model.ts b/src/projects/infrastructure/persistence/models/project.model.ts index e3aa4819..bd5c9adf 100644 --- a/src/projects/infrastructure/persistence/models/project.model.ts +++ b/src/projects/infrastructure/persistence/models/project.model.ts @@ -1,3 +1,6 @@ +import { createId } from '@paralleldrive/cuid2'; +import { baseSchema, teams, users } from '@shared/entities'; +import { isNull } from 'drizzle-orm'; import { text, varchar, @@ -7,9 +10,7 @@ import { uniqueIndex, index, } from 'drizzle-orm/pg-core'; -import { baseSchema, teams, users } from '@shared/entities'; -import { createId } from '@paralleldrive/cuid2'; -import { isNull } from 'drizzle-orm'; + import { layoutEnum, projectStatusEnum, projectVisibilityEnum } from './enum'; export const projects = baseSchema.table( diff --git a/src/projects/infrastructure/persistence/repositories/member.repository.ts b/src/projects/infrastructure/persistence/repositories/member.repository.ts index 266af47a..4ee6bc46 100644 --- a/src/projects/infrastructure/persistence/repositories/member.repository.ts +++ b/src/projects/infrastructure/persistence/repositories/member.repository.ts @@ -1,9 +1,11 @@ +import { IMemberRepository } from '@core/projects/domain/repository'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import { Inject, Injectable } from '@nestjs/common'; -import * as schema from '../models'; import { and, eq, sql } from 'drizzle-orm'; + +import * as schema from '../models'; + import type { MemberRole } from '@core/projects/domain/entities'; -import { IMemberRepository } from '@core/projects/domain/repository'; @Injectable() export class MemberRepository implements IMemberRepository { @@ -35,13 +37,12 @@ export class MemberRepository implements IMemberRepository { return result ?? null; }; - public readonly findByProject = async (projectId: string) => { - return this.db + public readonly findByProject = async (projectId: string) => + this.db .select() .from(schema.projectMembers) .where(eq(schema.projectMembers.projectId, projectId)) .orderBy(schema.projectMembers.createdAt); - }; async isMember(projectId: string, userId: string) { const [result] = await this.db diff --git a/src/projects/infrastructure/persistence/repositories/project.repository.ts b/src/projects/infrastructure/persistence/repositories/project.repository.ts index dbe0280b..92395b78 100644 --- a/src/projects/infrastructure/persistence/repositories/project.repository.ts +++ b/src/projects/infrastructure/persistence/repositories/project.repository.ts @@ -1,8 +1,10 @@ import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import { Injectable, Inject } from '@nestjs/common'; -import * as schema from '../models'; -import { IProjectRepository } from '../../../domain/repository'; import { and, count, eq, gt, isNull, or } from 'drizzle-orm'; + +import { IProjectRepository } from '../../../domain/repository'; +import * as schema from '../models'; + import type { NewProject, NewProjectShare } from '@core/projects/domain/entities'; @Injectable() @@ -108,12 +110,11 @@ export class ProjectRepository implements IProjectRepository { return project || null; }; - public readonly findByTeam = async (teamId: string) => { - return this.db + public readonly findByTeam = async (teamId: string) => + this.db .select() .from(schema.projects) .where(and(eq(schema.projects.teamId, teamId), isNull(schema.projects.deletedAt))); - }; public readonly createShare = async (data: NewProjectShare) => { const [result] = await this.db diff --git a/src/projects/projects.module.ts b/src/projects/projects.module.ts index b219976d..3ad18d3d 100644 --- a/src/projects/projects.module.ts +++ b/src/projects/projects.module.ts @@ -1,11 +1,12 @@ -import { forwardRef, Module } from '@nestjs/common'; import { TeamsModule } from '@core/teams'; +import { UserModule } from '@core/user'; +import { forwardRef, Module } from '@nestjs/common'; + import { CONTROLLERS } from './application/controller'; +import { ProjectFacade } from './application/project.facade'; import { CreateProjectUseCase, FindProjectQuery, USE_CASES } from './application/use-cases'; import { POLICIES, ProjectAccessPolicy } from './domain/policy'; -import { ProjectFacade } from './application/project.facade'; import { REPOSITORIES } from './infrastructure/persistence/repositories'; -import { UserModule } from '@core/user'; @Module({ imports: [UserModule, forwardRef(() => TeamsModule)], diff --git a/src/shared/adapters/cache/adapters/redis-cache.adapter.ts b/src/shared/adapters/cache/adapters/redis-cache.adapter.ts index 2102fc0f..41eeb1e2 100644 --- a/src/shared/adapters/cache/adapters/redis-cache.adapter.ts +++ b/src/shared/adapters/cache/adapters/redis-cache.adapter.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@nestjs/common'; import { InjectRedis } from '@nestjs-modules/ioredis'; +import { Injectable } from '@nestjs/common'; import Redis, { ChainableCommander } from 'ioredis'; + import { ICacheService, ICacheTransaction } from '../ports'; @Injectable() @@ -14,7 +15,9 @@ export class RedisCacheAdapter implements ICacheService { } async getMany(keys: string[]): Promise<(string | null)[]> { - if (keys.length === 0) return []; + if (keys.length === 0) { + return []; + } return this.redis.mget(keys); } @@ -30,7 +33,9 @@ export class RedisCacheAdapter implements ICacheService { items: readonly { readonly key: string; readonly value: string }[], ttlSeconds: number = this.defaultTtl, ) { - if (!items.length) return; + if (!items.length) { + return; + } const pipeline = this.redis.pipeline(); @@ -46,7 +51,9 @@ export class RedisCacheAdapter implements ICacheService { } async addManyToCollection(key: string, values: string[], ttlSeconds: number = this.defaultTtl) { - if (!values.length) return; + if (!values.length) { + return; + } await this.redis .pipeline() @@ -60,7 +67,9 @@ export class RedisCacheAdapter implements ICacheService { } async removeMany(keys: string[]) { - if (!keys.length) return; + if (!keys.length) { + return; + } await this.redis.del(keys); } @@ -69,7 +78,9 @@ export class RedisCacheAdapter implements ICacheService { } async removeManyFromCollection(key: string, values: string[]) { - if (!values.length) return; + if (!values.length) { + return; + } await this.redis.srem(key, ...values); } @@ -130,7 +141,9 @@ class RedisTransaction implements ICacheTransaction { } addManyToCollection(key: string, values: string[], ttlSeconds: number = this.defaultTtl): this { - if (!values.length) return this; + if (!values.length) { + return this; + } this.multi.sadd(key, ...values); this.multi.expire(key, ttlSeconds); return this; @@ -142,7 +155,9 @@ class RedisTransaction implements ICacheTransaction { } removeMany(keys: string[]): this { - if (!keys.length) return this; + if (!keys.length) { + return this; + } this.multi.del(keys); return this; } @@ -153,7 +168,9 @@ class RedisTransaction implements ICacheTransaction { } removeManyFromCollection(collectionKey: string, values: string[]): this { - if (!values.length) return this; + if (!values.length) { + return this; + } this.multi.srem(collectionKey, ...values); return this; } diff --git a/src/shared/adapters/cache/module.ts b/src/shared/adapters/cache/module.ts index de8c5813..2e476557 100644 --- a/src/shared/adapters/cache/module.ts +++ b/src/shared/adapters/cache/module.ts @@ -1,6 +1,7 @@ -import { Global, Module } from '@nestjs/common'; import { RedisModule } from '@nestjs-modules/ioredis'; +import { Global, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; + import { RedisCacheAdapter } from './adapters'; import { CACHE_SERVICE } from './constants'; @@ -9,7 +10,7 @@ import { CACHE_SERVICE } from './constants'; imports: [ RedisModule.forRootAsync({ inject: [ConfigService], - useFactory: async (cfg: ConfigService) => { + useFactory: (cfg: ConfigService) => { const host = cfg.getOrThrow('REDIS_HOST'); const port = cfg.get('REDIS_PORT'); const password = cfg.get('REDIS_PASSWORD'); diff --git a/src/shared/adapters/mail/adapter.ts b/src/shared/adapters/mail/adapter.ts index b34a19fc..e62e050e 100644 --- a/src/shared/adapters/mail/adapter.ts +++ b/src/shared/adapters/mail/adapter.ts @@ -1,9 +1,11 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import * as nodemailer from 'nodemailer'; import * as hbs from 'handlebars'; -import * as fs from 'fs'; -import * as path from 'path'; +import * as nodemailer from 'nodemailer'; + import { IMailPort } from './port'; @Injectable() @@ -31,7 +33,7 @@ export class MailAdapter implements IMailPort { }); } - private async sendMail(to: string, subject: string, templateName: string, context: any) { + private sendMail(to: string, subject: string, templateName: string, context: any) { const templatePath = path.join(process.cwd(), 'templates', `${templateName}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); @@ -43,7 +45,7 @@ export class MailAdapter implements IMailPort { const template = hbs.compile(templateSource); const html = template(contextWithYear); - return await this.transporter.sendMail({ + return this.transporter.sendMail({ from: `"${this.cfg.get('MAIL_FROM_NAME')}" <${this.cfg.get('MAIL_FROM_EMAIL')}>`, to, subject, diff --git a/src/shared/adapters/mail/module.ts b/src/shared/adapters/mail/module.ts index d70c47c3..1271ed39 100644 --- a/src/shared/adapters/mail/module.ts +++ b/src/shared/adapters/mail/module.ts @@ -1,4 +1,5 @@ import { Global, Module } from '@nestjs/common'; + import { MailAdapter } from './adapter'; @Global() diff --git a/src/shared/constants/roles.constant.ts b/src/shared/constants/roles.constant.ts index b81d36e0..b50e0b4f 100644 --- a/src/shared/constants/roles.constant.ts +++ b/src/shared/constants/roles.constant.ts @@ -10,9 +10,7 @@ export const ROLE_PRIORITY: Record = { viewer: 0, }; -export const isTeamRole = (role: string): role is TeamRole => { - return TEAM_ROLES.includes(role as TeamRole); -}; +export const isTeamRole = (role: string): role is TeamRole => TEAM_ROLES.includes(role as TeamRole); export const PROJECT_ROLE_PRIORITY: Record = { owner: 4, diff --git a/src/shared/decorators/api-controller.decorator.ts b/src/shared/decorators/api-controller.decorator.ts index 72e75813..4899cfcc 100644 --- a/src/shared/decorators/api-controller.decorator.ts +++ b/src/shared/decorators/api-controller.decorator.ts @@ -1,6 +1,7 @@ import { Controller, UseGuards, applyDecorators } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiErrorResponse } from '@shared/error'; + import { BearerAuthGuard } from '../guards'; export const ApiBaseController = (path: string, tag: string, hasJWTGuard?: boolean) => { diff --git a/src/shared/decorators/user.decorator.ts b/src/shared/decorators/user.decorator.ts index 938bc37c..61cc71d5 100644 --- a/src/shared/decorators/user.decorator.ts +++ b/src/shared/decorators/user.decorator.ts @@ -1,12 +1,15 @@ import { createParamDecorator, type ExecutionContext } from '@nestjs/common'; -import type { FastifyRequest } from 'fastify'; + import type { JwtPayload } from '@shared/types'; +import type { FastifyRequest } from 'fastify'; export const GetUser = createParamDecorator( (data: keyof JwtPayload | undefined, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; - if (!user) return null; + if (!user) { + return null; + } return data ? user[data] : user; }, ); diff --git a/src/shared/dtos/pagination.dto.ts b/src/shared/dtos/pagination.dto.ts index d0e8d388..7636a1b8 100644 --- a/src/shared/dtos/pagination.dto.ts +++ b/src/shared/dtos/pagination.dto.ts @@ -1,5 +1,5 @@ -import { z } from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const PaginationSchema = z.object({ page: z.coerce.number().int().min(1).default(1), diff --git a/src/shared/dtos/response.dto.ts b/src/shared/dtos/response.dto.ts index 325f7195..08b0786f 100644 --- a/src/shared/dtos/response.dto.ts +++ b/src/shared/dtos/response.dto.ts @@ -1,5 +1,5 @@ -import { z } from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const ActionResponseSchema = z.object({ success: z.boolean().describe('Статус операции'), diff --git a/src/shared/error/exception.ts b/src/shared/error/exception.ts index 20d9a1f0..87bb640b 100644 --- a/src/shared/error/exception.ts +++ b/src/shared/error/exception.ts @@ -1,5 +1,4 @@ -import type { HttpStatus } from '@nestjs/common'; -import { HttpException } from '@nestjs/common'; +import { HttpException, type HttpStatus } from '@nestjs/common'; interface IDetailsOptions { readonly target?: string; diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index 5d7fcbeb..0ae8b902 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -6,14 +6,16 @@ import { HttpStatus, Logger, } from '@nestjs/common'; +import { DrizzleQueryError } from 'drizzle-orm'; import { ZodValidationException } from 'nestjs-zod'; -import type { FastifyReply, FastifyRequest } from 'fastify'; import { PostgresError } from 'postgres'; + import { BaseException, type IErrorOptions } from './exception'; -import { DrizzleQueryError } from 'drizzle-orm'; -import type { ZodError, ZodIssue } from 'zod/v4'; import { DATABASE_ERRORS } from './swagger'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import type { ZodError, ZodIssue } from 'zod/v4'; + @Catch() export class GlobalExceptionFilter implements ExceptionFilter { private readonly logger = new Logger(GlobalExceptionFilter.name); @@ -39,15 +41,12 @@ export class GlobalExceptionFilter implements ExceptionFilter { return this.handleUnknownError(exception, host); } - private readonly parseZodValidation = async ( - exception: ZodValidationException, - host: ArgumentsHost, - ) => { + private parseZodValidation = (exception: ZodValidationException, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); const zodError = exception.getZodError() as ZodError; - const issues: readonly ZodIssue[] = zodError.issues || []; + const issues: ZodIssue[] = zodError.issues || []; this.log(exception, host, status, { validationIssues: issues, @@ -64,7 +63,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private readonly parseDatabase = async (exception: DrizzleQueryError, host: ArgumentsHost) => { + private parseDatabase = (exception: DrizzleQueryError, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const error = @@ -104,7 +103,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private readonly parseHttp = async (exception: BaseException, host: ArgumentsHost) => { + private parseHttp = (exception: BaseException, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); @@ -126,7 +125,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private readonly parseNestHttp = async (exception: HttpException, host: ArgumentsHost) => { + private parseNestHttp = (exception: HttpException, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); const res = exception.getResponse(); diff --git a/src/shared/error/schema.ts b/src/shared/error/schema.ts index 734c9263..e9ace4bf 100644 --- a/src/shared/error/schema.ts +++ b/src/shared/error/schema.ts @@ -1,5 +1,5 @@ -import { z } from 'zod/v4'; import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; const ErrorDetailSchema = z.object({ field: z.string().describe('Путь к полю (например, "user.email")'), diff --git a/src/shared/error/swagger.ts b/src/shared/error/swagger.ts index aec47cd8..4ca24cf1 100644 --- a/src/shared/error/swagger.ts +++ b/src/shared/error/swagger.ts @@ -1,6 +1,7 @@ +import { applyDecorators } from '@nestjs/common'; import { ApiResponse, getSchemaPath } from '@nestjs/swagger'; + import { GlobalErrorResponse } from './schema'; -import { applyDecorators } from '@nestjs/common'; export const ApiErrorResponse = ( status: number, diff --git a/src/shared/guards/bearer.guard.ts b/src/shared/guards/bearer.guard.ts index d5708abc..1542ce8f 100644 --- a/src/shared/guards/bearer.guard.ts +++ b/src/shared/guards/bearer.guard.ts @@ -3,8 +3,10 @@ import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { IS_PUBLIC_KEY } from '@shared/decorators'; import { BaseException } from '@shared/error'; + import type { JwtPayload } from '@shared/types'; import type { FastifyRequest } from 'fastify'; +import type { Observable } from 'rxjs'; @Injectable() export class BearerAuthGuard extends AuthGuard('bearer') { @@ -12,9 +14,11 @@ export class BearerAuthGuard extends AuthGuard('bearer') { super(); } - override async canActivate(context: ExecutionContext): Promise { + override canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { try { - return super.canActivate(context) as Promise; + return super.canActivate(context); } catch (e) { if (this.isPublicOrHasToken(context)) { return true; diff --git a/src/shared/guards/cookie.guard.ts b/src/shared/guards/cookie.guard.ts index 3b7df53c..50d98e4a 100644 --- a/src/shared/guards/cookie.guard.ts +++ b/src/shared/guards/cookie.guard.ts @@ -1,6 +1,7 @@ import { HttpStatus, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { BaseException } from '@shared/error'; + import type { JwtPayload } from '@shared/types'; @Injectable() diff --git a/src/shared/interceptors/http-metrics.interceptor.ts b/src/shared/interceptors/http-metrics.interceptor.ts index 347ad2d8..7b047da2 100644 --- a/src/shared/interceptors/http-metrics.interceptor.ts +++ b/src/shared/interceptors/http-metrics.interceptor.ts @@ -4,10 +4,11 @@ import { Injectable, NestInterceptor, } from '@nestjs/common'; +import { InjectMetric } from '@willsoto/nestjs-prometheus'; +import { Histogram } from 'prom-client'; import { Observable, throwError } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; -import { Histogram } from 'prom-client'; -import { InjectMetric } from '@willsoto/nestjs-prometheus'; + import type { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() @@ -43,7 +44,9 @@ export class HttpMetricsInterceptor implements NestInterceptor { ) { const route = req.routeOptions?.url || req.url; - if (route === '/metrics') return; + if (route === '/metrics') { + return; + } const statusCode = err ? err.status || err.statusCode || 500 : res.statusCode; diff --git a/src/shared/interceptors/zod-validation.interceptor.ts b/src/shared/interceptors/zod-validation.interceptor.ts index 175efc6e..6e11740a 100644 --- a/src/shared/interceptors/zod-validation.interceptor.ts +++ b/src/shared/interceptors/zod-validation.interceptor.ts @@ -6,10 +6,10 @@ import { NestInterceptor, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { map, Observable } from 'rxjs'; +import { SKIP_CONTRACT } from '@shared/decorators'; import { BaseException } from '@shared/error'; +import { map, Observable } from 'rxjs'; import { z } from 'zod/v4'; -import { SKIP_CONTRACT } from '@shared/decorators'; export const ZOD_RESPONSE_TOKEN = 'ZOD_RESPONSE_TOKEN'; diff --git a/src/shared/media/controller/index.ts b/src/shared/media/controller/index.ts index e61a17af..2d6d82f2 100644 --- a/src/shared/media/controller/index.ts +++ b/src/shared/media/controller/index.ts @@ -1,9 +1,11 @@ import { Post } from '@nestjs/common'; import { ApiBaseController, GetUserId } from '@shared/decorators'; + +import { ExtractMediaReq } from '../decorators'; import { UploadMediaDto } from '../dtos'; import { MediaService } from '../media.service'; + import { UploadMediaSwagger } from './swagger'; -import { ExtractMediaReq } from '../decorators'; @ApiBaseController('upload', 'Upload Media', true) export class MediaController { @@ -11,7 +13,7 @@ export class MediaController { @Post() @UploadMediaSwagger() - async upload(@ExtractMediaReq() dto: UploadMediaDto, @GetUserId() userId: string) { + upload(@ExtractMediaReq() dto: UploadMediaDto, @GetUserId() userId: string) { return this.service.upload(dto, userId); } } diff --git a/src/shared/media/controller/swagger.ts b/src/shared/media/controller/swagger.ts index 7c5b71b8..0d416d9d 100644 --- a/src/shared/media/controller/swagger.ts +++ b/src/shared/media/controller/swagger.ts @@ -1,10 +1,11 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { UploadMediaDto } from '../dtos'; -import { ApiUnauthorized, ApiValidationError } from '@shared/error'; import { ActionResponse } from '@shared/dtos'; +import { ApiUnauthorized, ApiValidationError } from '@shared/error'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; +import { UploadMediaDto } from '../dtos'; + export const UploadMediaSwagger = () => applyDecorators( ApiOperation({ diff --git a/src/shared/media/decorators/extract-media-req.decorator.ts b/src/shared/media/decorators/extract-media-req.decorator.ts index 622b92d3..48ca9279 100644 --- a/src/shared/media/decorators/extract-media-req.decorator.ts +++ b/src/shared/media/decorators/extract-media-req.decorator.ts @@ -1,9 +1,11 @@ import { createParamDecorator, type ExecutionContext, HttpStatus } from '@nestjs/common'; -import type { FastifyRequest } from 'fastify'; -import { IMAGE_MIME_TYPES } from '../../constants'; import { BaseException } from '@shared/error'; import { formatBytes } from '@shared/utils/format-bytes.util'; +import { IMAGE_MIME_TYPES } from '../../constants'; + +import type { FastifyRequest } from 'fastify'; + export const ExtractMediaReq = createParamDecorator( async ( { @@ -51,7 +53,9 @@ export const ExtractMediaReq = createParamDecorator( const fields: Record = {}; for (const key of Object.keys(file.fields)) { - if (key === 'file') continue; + if (key === 'file') { + continue; + } const field = file.fields[key]; if (field && !Array.isArray(field) && 'value' in field) { @@ -68,9 +72,8 @@ export const ExtractMediaReq = createParamDecorator( ...fields, }; } catch (e) { - const hasCode = (err: unknown): err is { readonly code: string } => { - return err !== null && typeof err === 'object' && 'code' in err; - }; + const hasCode = (err: unknown): err is { readonly code: string } => + err !== null && typeof err === 'object' && 'code' in err; if (hasCode(e) && e?.code === 'FST_REQ_FILE_TOO_LARGE') { throw new BaseException( diff --git a/src/shared/media/media.module.ts b/src/shared/media/media.module.ts index 69ae8c56..f72c79bb 100644 --- a/src/shared/media/media.module.ts +++ b/src/shared/media/media.module.ts @@ -1,11 +1,12 @@ -import { Module } from '@nestjs/common'; -import { MediaService } from './media.service'; +import { ImagorModule } from '@libs/imagor'; import { S3Module } from '@libs/s3'; +import { BullModule } from '@nestjs/bullmq'; +import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; + import { MediaController } from './controller'; import { MEDIA_FLOW, MEDIA_QUEUES } from './media.constant'; -import { BullModule } from '@nestjs/bullmq'; -import { ImagorModule } from '@libs/imagor'; +import { MediaService } from './media.service'; import { MediaProcessor } from './workers/media.worker'; @Module({ diff --git a/src/shared/media/media.service.ts b/src/shared/media/media.service.ts index 21c7bf9e..b2eb22cb 100644 --- a/src/shared/media/media.service.ts +++ b/src/shared/media/media.service.ts @@ -1,13 +1,16 @@ -import { HttpStatus, Injectable } from '@nestjs/common'; +import { extname } from 'node:path'; + import { S3Service } from '@libs/s3'; -import { UploadMediaDto } from './dtos'; +import { InjectFlowProducer } from '@nestjs/bullmq'; +import { HttpStatus, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; import { FlowProducer } from 'bullmq'; -import { InjectFlowProducer } from '@nestjs/bullmq'; -import { MEDIA_STRATEGIES, MediaStrategyKey } from './strategies'; + +import { UploadMediaDto } from './dtos'; import { MEDIA_FLOW, MEDIA_JOBS, MEDIA_QUEUES } from './media.constant'; +import { MEDIA_STRATEGIES, MediaStrategyKey } from './strategies'; + import type { MediaDispatchStrategy } from './strategies/media.strategy'; -import { extname } from 'path'; @Injectable() export class MediaService { @@ -99,7 +102,9 @@ export class MediaService { } private handleError(error: unknown): never { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } throw new BaseException( { diff --git a/src/shared/media/strategies/index.ts b/src/shared/media/strategies/index.ts index 5a832fe3..42eef8c9 100644 --- a/src/shared/media/strategies/index.ts +++ b/src/shared/media/strategies/index.ts @@ -1,5 +1,5 @@ -import { UserAvatarStrategy } from './user-avatar.strategy'; import { TeamMediaStrategy } from './team-media.strategy'; +import { UserAvatarStrategy } from './user-avatar.strategy'; export const MEDIA_STRATEGIES = { 'user.avatar': new UserAvatarStrategy(), diff --git a/src/shared/media/strategies/team-media.strategy.ts b/src/shared/media/strategies/team-media.strategy.ts index 4e380fb4..d10cf3ed 100644 --- a/src/shared/media/strategies/team-media.strategy.ts +++ b/src/shared/media/strategies/team-media.strategy.ts @@ -1,7 +1,8 @@ // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { UploadMediaDto } from '../dtos'; -import type { UpdateMediaTeam } from '../interfaces/media.interface'; import { MEDIA_JOBS } from '../media.constant'; + +import type { UpdateMediaTeam } from '../interfaces/media.interface'; import type { MediaDispatchStrategy } from './media.strategy'; export class TeamMediaStrategy implements MediaDispatchStrategy { diff --git a/src/shared/media/strategies/user-avatar.strategy.ts b/src/shared/media/strategies/user-avatar.strategy.ts index ed1abc1c..94447aa6 100644 --- a/src/shared/media/strategies/user-avatar.strategy.ts +++ b/src/shared/media/strategies/user-avatar.strategy.ts @@ -1,7 +1,8 @@ +import { MEDIA_JOBS } from '../media.constant'; + // eslint-disable-next-line no-restricted-syntax import type { UploadMediaDto } from '../dtos'; import type { UpdateMediaUser } from '../interfaces/media.interface'; -import { MEDIA_JOBS } from '../media.constant'; import type { MediaDispatchStrategy } from './media.strategy'; export class UserAvatarStrategy implements MediaDispatchStrategy { diff --git a/src/shared/media/workers/media.worker.ts b/src/shared/media/workers/media.worker.ts index 0cd8c4ec..02b8c213 100644 --- a/src/shared/media/workers/media.worker.ts +++ b/src/shared/media/workers/media.worker.ts @@ -1,9 +1,11 @@ +import { dirname } from 'node:path'; + import { ImagorService } from '@libs/imagor'; +import { S3Service } from '@libs/s3'; import { Processor, WorkerHost } from '@nestjs/bullmq'; -import { MEDIA_JOBS, MEDIA_QUEUES, MEDIA_SPECS } from '../media.constant'; import { Job } from 'bullmq'; -import { S3Service } from '@libs/s3'; -import { dirname } from 'path'; + +import { MEDIA_JOBS, MEDIA_QUEUES, MEDIA_SPECS } from '../media.constant'; @Processor(MEDIA_QUEUES.RESIZE) export class MediaProcessor extends WorkerHost { @@ -17,7 +19,9 @@ export class MediaProcessor extends WorkerHost { async process( job: Job<{ readonly original: string; readonly context: string; readonly userId: string }>, ) { - if (job.name !== MEDIA_JOBS.RESIZE_IMAGES) return; + if (job.name !== MEDIA_JOBS.RESIZE_IMAGES) { + return; + } const { original: originalFilePath, context } = job.data; @@ -32,7 +36,9 @@ export class MediaProcessor extends WorkerHost { for (let i = 0; i < resizeSpecs.length; i++) { const spec = resizeSpecs[i]; - if (!spec) continue; + if (!spec) { + continue; + } const { name, ...dimensions } = spec; const targetFileName = `${name}.webp`; diff --git a/src/shared/schemas/pagination.schema.ts b/src/shared/schemas/pagination.schema.ts index 400d5c10..5fb63611 100644 --- a/src/shared/schemas/pagination.schema.ts +++ b/src/shared/schemas/pagination.schema.ts @@ -58,9 +58,8 @@ export const paginationResponseSchema = z.object({ limit: z.number().int().positive().describe('Количество элементов на одну страницу.'), }); -export const createPaginationSchema = (itemSchema: T) => { - return z.object({ +export const createPaginationSchema = (itemSchema: T) => + z.object({ items: z.array(itemSchema), meta: paginationResponseSchema, }); -}; diff --git a/src/shared/schemas/sorting.schema.ts b/src/shared/schemas/sorting.schema.ts index 216f1d5c..cff59b2e 100644 --- a/src/shared/schemas/sorting.schema.ts +++ b/src/shared/schemas/sorting.schema.ts @@ -4,8 +4,8 @@ export const createSortingSchema = { - return z.object({ +) => + z.object({ sortBy: z .enum(fields) .optional() @@ -25,4 +25,3 @@ export const createSortingSchema = defaultOrder) .describe('Направление сортировки: asc - по возрастанию, desc - по убыванию'), }); -}; diff --git a/src/shared/utils/format-bytes.util.ts b/src/shared/utils/format-bytes.util.ts index 7e6f9055..34320a31 100644 --- a/src/shared/utils/format-bytes.util.ts +++ b/src/shared/utils/format-bytes.util.ts @@ -1,7 +1,9 @@ export const formatBytes = (bytes: number): string => { - if (bytes === 0) return '0 Bytes'; + if (bytes === 0) { + return '0 Bytes'; + } const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; }; diff --git a/src/shared/utils/image-builder.util.ts b/src/shared/utils/image-builder.util.ts index 77e83ce9..328b112a 100644 --- a/src/shared/utils/image-builder.util.ts +++ b/src/shared/utils/image-builder.util.ts @@ -1,8 +1,10 @@ -import { dirname } from 'path'; +import { dirname } from 'node:path'; export class ImageHelper { public static buildResponsiveUrls(cdn: string, path?: string | null) { - if (!path) return null; + if (!path) { + return null; + } const folder = dirname(path); const base = `${cdn}/${folder}`; diff --git a/src/teams/application/controller/invitations/controller.ts b/src/teams/application/controller/invitations/controller.ts index 0a422472..5cedc7e7 100644 --- a/src/teams/application/controller/invitations/controller.ts +++ b/src/teams/application/controller/invitations/controller.ts @@ -1,5 +1,9 @@ import { Body, Get, Param, Delete, Patch, Post } from '@nestjs/common'; import { ApiBaseController, GetUser, GetUserId } from '@shared/decorators'; + +import { InviteMemberDto, UpdateInvitationDto } from '../../dtos'; +import { TeamsFacade } from '../../team.facade'; + import { AcceptInviteSwagger, DeleteTeamInvitationSwagger, @@ -8,9 +12,8 @@ import { InviteMemberSwagger, UpdateTeamInvitationSwagger, } from './swagger'; + import type { JwtPayload } from '@shared/types'; -import { InviteMemberDto, UpdateInvitationDto } from '../../dtos'; -import { TeamsFacade } from '../../team.facade'; @ApiBaseController('teams/:teamId/invitations', 'Teams Invitations', true) export class TeamsInvitationsController { diff --git a/src/teams/application/controller/invitations/swagger.ts b/src/teams/application/controller/invitations/swagger.ts index 365c3434..9ebcf3ea 100644 --- a/src/teams/application/controller/invitations/swagger.ts +++ b/src/teams/application/controller/invitations/swagger.ts @@ -9,6 +9,8 @@ import { ApiUnauthorized, ApiValidationError, } from '@shared/error'; +import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { InviteMemberDto, TeamInvitationResponse, @@ -16,7 +18,6 @@ import { TeamInvitationsResponse, UserInvitesResponse, } from '../../dtos'; -import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; export const FindInvitesSwagger = () => applyDecorators( diff --git a/src/teams/application/controller/me/controller.ts b/src/teams/application/controller/me/controller.ts index 7c1098bd..cc478375 100644 --- a/src/teams/application/controller/me/controller.ts +++ b/src/teams/application/controller/me/controller.ts @@ -1,8 +1,11 @@ -import { ApiBaseController, GetUser, GetUserId } from '@shared/decorators'; import { Get, Query } from '@nestjs/common'; +import { ApiBaseController, GetUser, GetUserId } from '@shared/decorators'; + +import { TeamsFacade } from '../../team.facade'; + import { FindInvitesSwagger, FindTeamsSwagger } from './swagger'; + import type { JwtPayload } from '@shared/types'; -import { TeamsFacade } from '../../team.facade'; @ApiBaseController('users/me', 'Account Teams', true) export class MeController { diff --git a/src/teams/application/controller/me/swagger.ts b/src/teams/application/controller/me/swagger.ts index 15bed7ab..fe316ad4 100644 --- a/src/teams/application/controller/me/swagger.ts +++ b/src/teams/application/controller/me/swagger.ts @@ -1,9 +1,10 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiOperation, ApiResponse } from '@nestjs/swagger'; import { ApiUnauthorized } from '@shared/error'; -import { UserTeamsResponse, UserInvitesResponse } from '../../dtos'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; +import { UserTeamsResponse, UserInvitesResponse } from '../../dtos'; + export const FindTeamsSwagger = () => applyDecorators( ApiOperation({ diff --git a/src/teams/application/controller/members/controller.ts b/src/teams/application/controller/members/controller.ts index f2a0276d..a558bd8a 100644 --- a/src/teams/application/controller/members/controller.ts +++ b/src/teams/application/controller/members/controller.ts @@ -1,9 +1,11 @@ import { Body, Delete, Get, Param, Patch } from '@nestjs/common'; import { ApiBaseController, GetUserId } from '@shared/decorators'; -import { GetMembersSwagger, RemoveMemberSwagger, UpdateMemberSwagger } from './swagger'; + import { UpdateMemberDto } from '../../dtos'; import { TeamsFacade } from '../../team.facade'; +import { GetMembersSwagger, RemoveMemberSwagger, UpdateMemberSwagger } from './swagger'; + @ApiBaseController('teams/:teamId', 'Teams Members', true) export class TeamsMembersController { constructor(private readonly facade: TeamsFacade) {} diff --git a/src/teams/application/controller/members/swagger.ts b/src/teams/application/controller/members/swagger.ts index b955087c..cf24ad2b 100644 --- a/src/teams/application/controller/members/swagger.ts +++ b/src/teams/application/controller/members/swagger.ts @@ -2,13 +2,14 @@ import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ActionResponse } from '@shared/dtos'; import { ApiForbidden, ApiNotFound, ApiUnauthorized } from '@shared/error'; +import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; + import { UpdateMemberDto, TeamMembersResponse, UserTeamsResponse, UserInvitesResponse, } from '../../dtos'; -import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; export const FindTeamsSwagger = () => applyDecorators( diff --git a/src/teams/application/controller/teams/controller.ts b/src/teams/application/controller/teams/controller.ts index c1182a00..b3e872d4 100644 --- a/src/teams/application/controller/teams/controller.ts +++ b/src/teams/application/controller/teams/controller.ts @@ -1,13 +1,15 @@ import { Body, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post } from '@nestjs/common'; import { ApiBaseController, GetUserId } from '@shared/decorators'; + +import { CreateTeamDto, UpdateTeamDto } from '../../dtos'; +import { TeamsFacade } from '../../team.facade'; + import { CreateTeamSwagger, FindOneTeamSwagger, RemoveTeamSwagger, UpdateTeamSwagger, } from './swagger'; -import { CreateTeamDto, UpdateTeamDto } from '../../dtos'; -import { TeamsFacade } from '../../team.facade'; @ApiBaseController('teams', 'Teams', true) export class TeamsController { diff --git a/src/teams/application/controller/teams/swagger.ts b/src/teams/application/controller/teams/swagger.ts index a5fb6fc3..87b07545 100644 --- a/src/teams/application/controller/teams/swagger.ts +++ b/src/teams/application/controller/teams/swagger.ts @@ -1,10 +1,11 @@ +import { CreateTeamResponse } from '@core/teams/application/dtos/team.dto'; import { applyDecorators, SetMetadata } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ActionResponse } from '@shared/dtos'; import { ApiForbidden, ApiNotFound, ApiUnauthorized, ApiValidationError } from '@shared/error'; -import { CreateTeamDto, UpdateTeamDto, TeamResponse } from '../../dtos'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; -import { CreateTeamResponse } from '@core/teams/application/dtos/team.dto'; + +import { CreateTeamDto, UpdateTeamDto, TeamResponse } from '../../dtos'; export const CreateTeamSwagger = () => applyDecorators( diff --git a/src/teams/application/dtos/invitation.dto.ts b/src/teams/application/dtos/invitation.dto.ts index f332fa59..23b93af7 100644 --- a/src/teams/application/dtos/invitation.dto.ts +++ b/src/teams/application/dtos/invitation.dto.ts @@ -1,8 +1,8 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; -import type { TeamRole } from '../../infrastructure/persistence/models/enums'; -import { roleEnum } from '../../infrastructure/persistence/models/enums'; import { createPaginationSchema } from '@shared/schemas'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +import { roleEnum, type TeamRole } from '../../infrastructure/persistence/models/enums'; export const UpdateInvitationSchema = z.object({ role: z diff --git a/src/teams/application/dtos/member.dto.ts b/src/teams/application/dtos/member.dto.ts index 6cf7ca0a..9c2e6a0d 100644 --- a/src/teams/application/dtos/member.dto.ts +++ b/src/teams/application/dtos/member.dto.ts @@ -1,7 +1,7 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; import { roleEnum } from '@core/teams/infrastructure/persistence/models'; import { AvatarResponseSchema, createPaginationSchema } from '@shared/schemas'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const InviteMemberSchema = z.object({ email: z.string().email().describe('Email пользователя, которого нужно пригласить'), diff --git a/src/teams/application/dtos/team.dto.ts b/src/teams/application/dtos/team.dto.ts index 9dc36d5f..58a59554 100644 --- a/src/teams/application/dtos/team.dto.ts +++ b/src/teams/application/dtos/team.dto.ts @@ -1,7 +1,7 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; -import { AvatarResponseSchema } from '@shared/schemas'; import { ActionResponseSchema } from '@shared/dtos'; +import { AvatarResponseSchema } from '@shared/schemas'; +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; export const CreateTeamSchema = z.object({ name: z.string().min(2).max(100).describe('Название команды, отображаемое в интерфейсе'), diff --git a/src/teams/application/mappers/member.mapper.ts b/src/teams/application/mappers/member.mapper.ts index bd8fc5ad..97bcfc0e 100644 --- a/src/teams/application/mappers/member.mapper.ts +++ b/src/teams/application/mappers/member.mapper.ts @@ -1,6 +1,7 @@ -import type { RawMemberRow, RawMemberTeams } from '../../domain/repository'; import { ImageHelper } from '@shared/utils'; +import type { RawMemberRow, RawMemberTeams } from '../../domain/repository'; + export class TeamMemberMapper { public static toDetail(row: RawMemberRow, cdn: string) { const { firstName, lastName, middleName, avatarUrl, userId, ...rest } = row; @@ -36,7 +37,7 @@ export class TeamMemberMapper { name: row.name, description: row.description, avatar, - role: role, + role, joinedAt: row.joinedAt, permissions: { canEdit: ['owner', 'admin'].includes(role), @@ -49,7 +50,9 @@ export class TeamMemberMapper { } public static toPublicInvite(raw: string | null, code: string) { - if (!raw) return null; + if (!raw) { + return null; + } try { const p = JSON.parse(raw); return { diff --git a/src/teams/application/team.facade.ts b/src/teams/application/team.facade.ts index 6016e478..50df66d6 100644 --- a/src/teams/application/team.facade.ts +++ b/src/teams/application/team.facade.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import * as UC from './use-cases'; + import { CreateTeamDto, InviteMemberDto, @@ -7,6 +7,7 @@ import { UpdateMemberDto, UpdateTeamDto, } from './dtos'; +import * as UC from './use-cases'; @Injectable() export class TeamsFacade { diff --git a/src/teams/application/use-cases/base/create-team.use-case.ts b/src/teams/application/use-cases/base/create-team.use-case.ts index 00b41d2a..4fb0b55d 100644 --- a/src/teams/application/use-cases/base/create-team.use-case.ts +++ b/src/teams/application/use-cases/base/create-team.use-case.ts @@ -1,8 +1,9 @@ import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { CreateTeamDto } from '../../dtos'; import { BaseException } from '@shared/error'; +import { CreateTeamDto } from '../../dtos'; + @Injectable() export class CreateTeamUseCase { constructor( @@ -19,7 +20,9 @@ export class CreateTeamUseCase { message: 'Команда успешно создана', }; } catch (error) { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } throw new BaseException( { diff --git a/src/teams/application/use-cases/base/delete-team.use-case.ts b/src/teams/application/use-cases/base/delete-team.use-case.ts index 0855390b..f0cb9a02 100644 --- a/src/teams/application/use-cases/base/delete-team.use-case.ts +++ b/src/teams/application/use-cases/base/delete-team.use-case.ts @@ -43,7 +43,9 @@ export class DeleteTeamUseCase { message: 'Команда успешно удалена', }; } catch (error) { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } throw new BaseException( { diff --git a/src/teams/application/use-cases/base/find-team.query.ts b/src/teams/application/use-cases/base/find-team.query.ts index b2d0b1c7..8b025d7f 100644 --- a/src/teams/application/use-cases/base/find-team.query.ts +++ b/src/teams/application/use-cases/base/find-team.query.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; + import { ITeamsRepository } from '../../../domain/repository'; @Injectable() diff --git a/src/teams/application/use-cases/base/get-my-teams.use-case.ts b/src/teams/application/use-cases/base/get-my-teams.use-case.ts index e8277fc0..6ed57a52 100644 --- a/src/teams/application/use-cases/base/get-my-teams.use-case.ts +++ b/src/teams/application/use-cases/base/get-my-teams.use-case.ts @@ -1,5 +1,5 @@ -import { ITeamsRepository } from '@core/teams/domain/repository'; import { TeamMemberMapper } from '@core/teams/application/mappers'; +import { ITeamsRepository } from '@core/teams/domain/repository'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; diff --git a/src/teams/application/use-cases/base/update-team.use-case.ts b/src/teams/application/use-cases/base/update-team.use-case.ts index 84db12aa..2cdd00bd 100644 --- a/src/teams/application/use-cases/base/update-team.use-case.ts +++ b/src/teams/application/use-cases/base/update-team.use-case.ts @@ -1,7 +1,8 @@ import { Inject, Injectable, HttpStatus } from '@nestjs/common'; +import { BaseException } from '@shared/error'; + import { ITeamsRepository } from '../../../domain/repository'; import { UpdateTeamDto } from '../../dtos'; -import { BaseException } from '@shared/error'; @Injectable() export class UpdateTeamUseCase { @@ -45,7 +46,9 @@ export class UpdateTeamUseCase { message: 'Данные команды успешно обновлены', }; } catch (error) { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } throw new BaseException( { diff --git a/src/teams/application/use-cases/index.ts b/src/teams/application/use-cases/index.ts index e5849ba6..ac91205d 100644 --- a/src/teams/application/use-cases/index.ts +++ b/src/teams/application/use-cases/index.ts @@ -1,39 +1,19 @@ +import { CreateTeamUseCase } from './base/create-team.use-case'; +import { DeleteTeamUseCase } from './base/delete-team.use-case'; import { FindTeamQuery } from './base/find-team.query'; -import { FindTeamMemberQuery } from './members/find-team-member.query'; +import { GetMyTeamsUseCase } from './base/get-my-teams.use-case'; +import { UpdateTeamUseCase } from './base/update-team.use-case'; +import { AcceptInvitationUseCase } from './invitions/accept-invitation.use-case'; +import { DeclineInvitationUseCase } from './invitions/decline-invitation.use-case'; import { GetInvitationQuery } from './invitions/get-invitation.query'; import { GetInvitationsQuery } from './invitions/get-invitations.query'; -import { GetTeamMembersQuery } from './members/get-team-members.query'; import { GetMyInvitesUseCase } from './invitions/get-my-invites.use-case'; -import { GetMyTeamsUseCase } from './base/get-my-teams.use-case'; - -import { AcceptInvitationUseCase } from './invitions/accept-invitation.use-case'; -import { CreateTeamUseCase } from './base/create-team.use-case'; -import { DeleteTeamUseCase } from './base/delete-team.use-case'; -import { RemoveTeamMemberUseCase } from './members/remove-team-member.use-case'; import { SendInvitationUseCase } from './invitions/send-invitation.use-case'; -import { UpdateTeamUseCase } from './base/update-team.use-case'; -import { UpdateTeamMemberUseCase } from './members/update-team-member.use-case'; import { UpdateInvitationUseCase } from './invitions/update-invitation.use-case'; -import { DeclineInvitationUseCase } from './invitions/decline-invitation.use-case'; - -export { - FindTeamQuery, - FindTeamMemberQuery, - GetInvitationQuery, - GetInvitationsQuery, - GetTeamMembersQuery, - GetMyInvitesUseCase, - GetMyTeamsUseCase, - AcceptInvitationUseCase, - CreateTeamUseCase, - DeleteTeamUseCase, - RemoveTeamMemberUseCase, - SendInvitationUseCase, - UpdateTeamUseCase, - UpdateTeamMemberUseCase, - UpdateInvitationUseCase, - DeclineInvitationUseCase, -}; +import { FindTeamMemberQuery } from './members/find-team-member.query'; +import { GetTeamMembersQuery } from './members/get-team-members.query'; +import { RemoveTeamMemberUseCase } from './members/remove-team-member.use-case'; +import { UpdateTeamMemberUseCase } from './members/update-team-member.use-case'; export const TeamQueries = [ FindTeamQuery, @@ -59,3 +39,21 @@ export const TeamUseCases = [ export const TEAM_EXTERNAL_QUERIES = [FindTeamQuery, FindTeamMemberQuery]; export const TEAM_EXTERNAL_COMMANDS = [CreateTeamUseCase]; + +export { FindTeamQuery } from './base/find-team.query'; +export { FindTeamMemberQuery } from './members/find-team-member.query'; +export { GetInvitationQuery } from './invitions/get-invitation.query'; +export { GetInvitationsQuery } from './invitions/get-invitations.query'; +export { GetTeamMembersQuery } from './members/get-team-members.query'; +export { GetMyInvitesUseCase } from './invitions/get-my-invites.use-case'; +export { GetMyTeamsUseCase } from './base/get-my-teams.use-case'; +export { AcceptInvitationUseCase } from './invitions/accept-invitation.use-case'; +export { CreateTeamUseCase } from './base/create-team.use-case'; +export { DeleteTeamUseCase } from './base/delete-team.use-case'; + +export { RemoveTeamMemberUseCase } from './members/remove-team-member.use-case'; +export { SendInvitationUseCase } from './invitions/send-invitation.use-case'; +export { UpdateTeamUseCase } from './base/update-team.use-case'; +export { UpdateTeamMemberUseCase } from './members/update-team-member.use-case'; +export { UpdateInvitationUseCase } from './invitions/update-invitation.use-case'; +export { DeclineInvitationUseCase } from './invitions/decline-invitation.use-case'; diff --git a/src/teams/application/use-cases/invitions/accept-invitation.use-case.ts b/src/teams/application/use-cases/invitions/accept-invitation.use-case.ts index 328dafc9..94e9a308 100644 --- a/src/teams/application/use-cases/invitions/accept-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/accept-invitation.use-case.ts @@ -1,9 +1,10 @@ import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; +import { ICacheService } from '@shared/adapters/cache/ports'; import { BaseException } from '@shared/error'; + import type { TeamInvite } from '../../dtos/invitation.dto'; -import { ICacheService } from '@shared/adapters/cache/ports'; -import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; @Injectable() export class AcceptInvitationUseCase { diff --git a/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts b/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts index 1a9abaa0..4ded7102 100644 --- a/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/decline-invitation.use-case.ts @@ -1,9 +1,10 @@ import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { BaseException } from '@shared/error'; -import type { TeamInvite } from '../../dtos/invitation.dto'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; + +import type { TeamInvite } from '../../dtos/invitation.dto'; @Injectable() export class DeclineInvitationUseCase { @@ -58,11 +59,12 @@ export class DeclineInvitationUseCase { private async getTeamOrThrow(teamId: string) { const team = await this.teamsRepo.findById(teamId); - if (!team) + if (!team) { throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, HttpStatus.NOT_FOUND, ); + } return team; } diff --git a/src/teams/application/use-cases/invitions/get-invitation.query.ts b/src/teams/application/use-cases/invitions/get-invitation.query.ts index bb7ee3c3..43a8c528 100644 --- a/src/teams/application/use-cases/invitions/get-invitation.query.ts +++ b/src/teams/application/use-cases/invitions/get-invitation.query.ts @@ -1,9 +1,10 @@ import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { BaseException } from '@shared/error'; -import type { TeamInvite } from '../../dtos/invitation.dto'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; + +import type { TeamInvite } from '../../dtos/invitation.dto'; @Injectable() export class GetInvitationQuery { @@ -26,21 +27,23 @@ export class GetInvitationQuery { private async getTeamOrThrow(teamId: string) { const team = await this.teamsRepo.findById(teamId); - if (!team) + if (!team) { throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, HttpStatus.NOT_FOUND, ); + } return team; } private async getInviteOrThrow(code: string) { const raw = await this.cacheService.getOne(this.INVITES_KEY(code)); - if (!raw) + if (!raw) { throw new BaseException( { code: 'INVITE_EXPIRED', message: 'Срок действия приглашения истек' }, HttpStatus.NOT_FOUND, ); + } return JSON.parse(raw) as TeamInvite; } @@ -59,10 +62,14 @@ export class GetInvitationQuery { currentUserEmail: string, inviteEmail: string, ) { - if (currentUserEmail.toLowerCase() === inviteEmail.toLowerCase()) return; + if (currentUserEmail.toLowerCase() === inviteEmail.toLowerCase()) { + return; + } const member = await this.teamsRepo.findMember(teamId, userId); - if (member && (member.role === 'owner' || member.role === 'admin')) return; + if (member && (member.role === 'owner' || member.role === 'admin')) { + return; + } throw new BaseException( { code: 'INSUFFICIENT_PERMISSIONS', message: 'У вас нет прав просмотра' }, diff --git a/src/teams/application/use-cases/invitions/get-invitations.query.ts b/src/teams/application/use-cases/invitions/get-invitations.query.ts index fd60d132..66b9b7e3 100644 --- a/src/teams/application/use-cases/invitions/get-invitations.query.ts +++ b/src/teams/application/use-cases/invitions/get-invitations.query.ts @@ -1,8 +1,8 @@ import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { BaseException } from '@shared/error'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; @Injectable() export class GetInvitationsQuery { @@ -20,7 +20,7 @@ export class GetInvitationsQuery { const teamKey = this.TEAM_INVITES_KEY(team.id); const codes = await this.cacheService.getCollection(teamKey); - if (!codes.length) + if (!codes.length) { return { // TODO: реализовать полноценную пагинацию для инвайтов команды. items: [], @@ -33,13 +33,16 @@ export class GetInvitationsQuery { hasNextPage: false, }, }; + } const results = await this.cacheService.getMany(codes.map(this.INVITES_KEY)); const { active, expired } = results.reduce( (acc: { active: any[]; expired: string[] }, raw, i) => { const code = codes[i]; - if (!code) return acc; + if (!code) { + return acc; + } if (raw) { acc.active.push({ code, ...JSON.parse(raw) }); @@ -73,11 +76,12 @@ export class GetInvitationsQuery { private async getTeamOrThrow(teamId: string) { const team = await this.teamsRepo.findById(teamId); - if (!team) + if (!team) { throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, HttpStatus.NOT_FOUND, ); + } return team; } diff --git a/src/teams/application/use-cases/invitions/get-my-invites.use-case.ts b/src/teams/application/use-cases/invitions/get-my-invites.use-case.ts index 5555831c..6c176699 100644 --- a/src/teams/application/use-cases/invitions/get-my-invites.use-case.ts +++ b/src/teams/application/use-cases/invitions/get-my-invites.use-case.ts @@ -14,7 +14,7 @@ export class GetMyInvitesUseCase { const userKey = `user:invites:${email.toLowerCase()}`; const codes = await this.cacheService.getCollection(userKey); - if (!codes.length) + if (!codes.length) { return { // TODO: реализовать полноценную пагинацию для инвайтов пользователя. items: [], @@ -27,13 +27,16 @@ export class GetMyInvitesUseCase { hasNextPage: false, }, }; + } const inviteKeys = codes.map((c) => `inv:code:${c}`); const results = await this.cacheService.getMany(inviteKeys); const { active, expired } = results.reduce( (acc: { active: any[]; expired: string[] }, raw, i) => { const code = codes[i]; - if (!code) return acc; + if (!code) { + return acc; + } if (raw) { acc.active.push(TeamMemberMapper.toPublicInvite(raw, code)); diff --git a/src/teams/application/use-cases/invitions/send-invitation.use-case.ts b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts index 1554fb7d..875f3191 100644 --- a/src/teams/application/use-cases/invitions/send-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/send-invitation.use-case.ts @@ -1,18 +1,20 @@ import { TeamMailJobs, TeamQueues } from '@core/teams/domain/enums'; +import { TeamInvitationEvent } from '@core/teams/domain/events'; +import { TeamMemberPolicy } from '@core/teams/domain/policy'; import { ITeamsRepository } from '@core/teams/domain/repository'; import { InjectQueue } from '@nestjs/bullmq'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Queue } from 'bullmq'; -import { InviteMemberDto, type TeamInvite } from '../../dtos'; -import { BaseException } from '@shared/error'; -import { generateSecret } from 'otplib'; -import { TeamInvitationEvent } from '@core/teams/domain/events'; -import { TeamMemberPolicy } from '@core/teams/domain/policy'; -import type { TeamRole } from '@shared/entities'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; import { ImageHelper } from '@shared/utils'; +import { Queue } from 'bullmq'; +import { generateSecret } from 'otplib'; + +import { InviteMemberDto, type TeamInvite } from '../../dtos'; + +import type { TeamRole } from '@shared/entities'; @Injectable() export class SendInvitationUseCase { @@ -49,21 +51,23 @@ export class SendInvitationUseCase { private async getTeamOrThrow(teamId: string) { const team = await this.teamsRepo.findById(teamId); - if (!team) + if (!team) { throw new BaseException( { code: 'TEAM_NOT_FOUND', message: 'Команда не найдена' }, HttpStatus.NOT_FOUND, ); + } return team; } private async getInviterOrThrow(teamId: string, userId: string) { const inviter = await this.teamsRepo.findMember(teamId, userId); - if (!inviter) + if (!inviter) { throw new BaseException( { code: 'NOT_A_MEMBER', message: 'Вы не член команды' }, HttpStatus.FORBIDDEN, ); + } return inviter; } @@ -78,16 +82,19 @@ export class SendInvitationUseCase { private async ensureNotAlreadyMember(teamId: string, email: string) { const member = await this.teamsRepo.findMember(teamId, email); // Тут лучше искать по email в репо - if (member) + if (member) { throw new BaseException( { code: 'ALREADY_MEMBER', message: 'Уже в команде' }, HttpStatus.BAD_REQUEST, ); + } } private async ensureNoPendingInvite(teamId: string, email: string) { const activeCodes = await this.cacheService.getCollection(this.USER_INVITES_KEY(email)); - if (activeCodes.length === 0) return; + if (activeCodes.length === 0) { + return; + } const invitesData = await this.cacheService.getMany(activeCodes.map(this.INVITES_KEY)); const hasDuplicate = invitesData diff --git a/src/teams/application/use-cases/invitions/update-invitation.use-case.ts b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts index 6c6b6da6..aeaebdda 100644 --- a/src/teams/application/use-cases/invitions/update-invitation.use-case.ts +++ b/src/teams/application/use-cases/invitions/update-invitation.use-case.ts @@ -1,11 +1,13 @@ +import { TeamMemberPolicy } from '@core/teams/domain/policy'; import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { UpdateInvitationDto } from '../../dtos'; -import { BaseException } from '@shared/error'; -import { TeamInvite } from '../../dtos/invitation.dto'; -import { TeamMemberPolicy } from '@core/teams/domain/policy'; import { CACHE_SERVICE } from '@shared/adapters/cache/constants'; import { ICacheService } from '@shared/adapters/cache/ports'; +import { BaseException } from '@shared/error'; + +import { UpdateInvitationDto } from '../../dtos'; +import { TeamInvite } from '../../dtos/invitation.dto'; + import type { TeamRole } from '../../../infrastructure/persistence/models'; @Injectable() diff --git a/src/teams/application/use-cases/members/find-team-member.query.ts b/src/teams/application/use-cases/members/find-team-member.query.ts index 291ce9f5..5f82597e 100644 --- a/src/teams/application/use-cases/members/find-team-member.query.ts +++ b/src/teams/application/use-cases/members/find-team-member.query.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; + import { ITeamsRepository } from '../../../domain/repository'; @Injectable() diff --git a/src/teams/application/use-cases/members/get-team-members.query.ts b/src/teams/application/use-cases/members/get-team-members.query.ts index 39194302..ccf02a81 100644 --- a/src/teams/application/use-cases/members/get-team-members.query.ts +++ b/src/teams/application/use-cases/members/get-team-members.query.ts @@ -1,8 +1,8 @@ -import { ITeamsRepository } from '@core/teams/domain/repository'; import { TeamMemberMapper } from '@core/teams/application/mappers'; +import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { BaseException } from '@shared/error'; import { ConfigService } from '@nestjs/config'; +import { BaseException } from '@shared/error'; @Injectable() export class GetTeamMembersQuery { diff --git a/src/teams/application/use-cases/members/remove-team-member.use-case.ts b/src/teams/application/use-cases/members/remove-team-member.use-case.ts index aabab45a..d5a875a9 100644 --- a/src/teams/application/use-cases/members/remove-team-member.use-case.ts +++ b/src/teams/application/use-cases/members/remove-team-member.use-case.ts @@ -1,9 +1,10 @@ import { TeamMemberPolicy } from '@core/teams/domain/policy'; import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import type { TeamRole } from '@shared/entities'; import { BaseException } from '@shared/error'; +import type { TeamRole } from '@shared/entities'; + @Injectable() export class RemoveTeamMemberUseCase { constructor( @@ -69,7 +70,9 @@ export class RemoveTeamMemberUseCase { : `Участник успешно исключен из команды ${team.name}`, }; } catch (error) { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } throw new BaseException( { code: 'MEMBER_REMOVAL_FAILED', message: 'Ошибка при удалении участника' }, diff --git a/src/teams/application/use-cases/members/update-team-member.use-case.ts b/src/teams/application/use-cases/members/update-team-member.use-case.ts index 1084ffdc..9b8eb5c8 100644 --- a/src/teams/application/use-cases/members/update-team-member.use-case.ts +++ b/src/teams/application/use-cases/members/update-team-member.use-case.ts @@ -1,9 +1,10 @@ +import { TeamMemberPolicy } from '@core/teams/domain/policy'; import { ITeamsRepository } from '@core/teams/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { UpdateMemberDto } from '../../dtos'; -import { BaseException } from '@shared/error'; -import { TeamMemberPolicy } from '@core/teams/domain/policy'; import { TeamRole } from '@shared/entities'; +import { BaseException } from '@shared/error'; + +import { UpdateMemberDto } from '../../dtos'; @Injectable() export class UpdateTeamMemberUseCase { @@ -63,28 +64,24 @@ export class UpdateTeamMemberUseCase { ); } - if (dto.role) { - if (!this.teamMemberPolicy.canAssignRole(issuerRole, targetRole, dto.role)) { - throw new BaseException( - { - code: 'INVALID_ROLE_ASSIGNMENT', - message: 'У вас нет прав назначить выбранную роль', - }, - HttpStatus.FORBIDDEN, - ); - } + if (dto.role && !this.teamMemberPolicy.canAssignRole(issuerRole, targetRole, dto.role)) { + throw new BaseException( + { + code: 'INVALID_ROLE_ASSIGNMENT', + message: 'У вас нет прав назначить выбранную роль', + }, + HttpStatus.FORBIDDEN, + ); } - if (dto.status) { - if (!this.teamMemberPolicy.canChangeStatus(issuerRole, targetRole)) { - throw new BaseException( - { - code: 'INVALID_STATUS_CHANGE', - message: 'Вы не можете менять статус этого участника', - }, - HttpStatus.FORBIDDEN, - ); - } + if (dto.status && !this.teamMemberPolicy.canChangeStatus(issuerRole, targetRole)) { + throw new BaseException( + { + code: 'INVALID_STATUS_CHANGE', + message: 'Вы не можете менять статус этого участника', + }, + HttpStatus.FORBIDDEN, + ); } try { @@ -94,7 +91,9 @@ export class UpdateTeamMemberUseCase { message: `Данные участника команды ${team.name} успешно обновлены`, }; } catch (error) { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } throw new BaseException( { diff --git a/src/teams/domain/entities/teams.domain.ts b/src/teams/domain/entities/teams.domain.ts index 1c8dcd38..4ee5e26b 100644 --- a/src/teams/domain/entities/teams.domain.ts +++ b/src/teams/domain/entities/teams.domain.ts @@ -1,5 +1,5 @@ -import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; import type { teams, teamMembers } from '../../infrastructure/persistence/models'; +import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; export type Team = InferSelectModel; export type NewTeam = InferInsertModel; diff --git a/src/teams/domain/policy/team-member.policy.ts b/src/teams/domain/policy/team-member.policy.ts index c849508b..2a726e24 100644 --- a/src/teams/domain/policy/team-member.policy.ts +++ b/src/teams/domain/policy/team-member.policy.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ROLE_PRIORITY } from '@shared/constants'; + import type { TeamRole } from '@shared/entities'; @Injectable() @@ -15,7 +16,9 @@ export class TeamMemberPolicy { */ public canManage(issuerRole: TeamRole, targetRole: TeamRole): boolean { // Минимальный порог для управления — администратор - if (this.getPriority(issuerRole) < (ROLE_PRIORITY['admin'] ?? 3)) return false; + if (this.getPriority(issuerRole) < (ROLE_PRIORITY['admin'] ?? 3)) { + return false; + } // Нельзя редактировать того, кто равен или выше по рангу return this.getPriority(issuerRole) > this.getPriority(targetRole); @@ -30,10 +33,14 @@ export class TeamMemberPolicy { newRole: TeamRole, ): boolean { // 1. Проверка прав на управление целью - if (!this.canManage(issuerRole, targetCurrentRole)) return false; + if (!this.canManage(issuerRole, targetCurrentRole)) { + return false; + } // 2. Роль Owner неприкосновенна (нельзя снять и нельзя назначить через обычный Update) - if (targetCurrentRole === 'owner' || newRole === 'owner') return false; + if (targetCurrentRole === 'owner' || newRole === 'owner') { + return false; + } // 3. Нельзя назначить роль выше своей или равную своей (если ты не владелец) if (issuerRole !== 'owner' && this.getPriority(newRole) >= this.getPriority(issuerRole)) { @@ -48,7 +55,9 @@ export class TeamMemberPolicy { */ public canChangeStatus(issuerRole: TeamRole, targetRole: TeamRole): boolean { // Владельца нельзя забанить или деактивировать - if (targetRole === 'owner') return false; + if (targetRole === 'owner') { + return false; + } // В остальном работают стандартные правила иерархии return this.canManage(issuerRole, targetRole); @@ -76,13 +85,19 @@ export class TeamMemberPolicy { const newRolePrio = this.getPriority(newMemberRole); // Только админы и выше могут приглашать - if (issuerPrio < (ROLE_PRIORITY['admin'] ?? 3)) return false; + if (issuerPrio < (ROLE_PRIORITY['admin'] ?? 3)) { + return false; + } // Нельзя пригласить кого-то на роль выше или равную своей (кроме owner) - if (issuerRole !== 'owner' && newRolePrio >= issuerPrio) return false; + if (issuerRole !== 'owner' && newRolePrio >= issuerPrio) { + return false; + } // Нельзя пригласить на роль owner через обычный инвайт - if (newMemberRole === 'owner') return false; + if (newMemberRole === 'owner') { + return false; + } return true; } @@ -93,8 +108,8 @@ export class TeamMemberPolicy { * @remarks * Логика базируется на приоритете ролей. Минимально допустимая роль — Модератор. * - * @param issuerRole - Роль участника, инициирующего обновление. - * @returns `true`, если приоритет роли равен или выше приоритета модератора, иначе `false`. + * @param {TeamRole} issuerRole - Роль участника, инициирующего обновление. + * @returns {boolean} `true`, если приоритет роли равен или выше приоритета модератора, иначе `false`. * * @example * const canUpdate = policy.canUpdateMedia('admin'); // true diff --git a/src/teams/infrastructure/listeners/update-media.listener.ts b/src/teams/infrastructure/listeners/update-media.listener.ts index 02c45158..fb53079c 100644 --- a/src/teams/infrastructure/listeners/update-media.listener.ts +++ b/src/teams/infrastructure/listeners/update-media.listener.ts @@ -1,9 +1,10 @@ -import { Inject } from '@nestjs/common'; +import { TeamMemberPolicy } from '@core/teams/domain/policy'; import { ITeamsRepository } from '@core/teams/domain/repository'; import { Processor, WorkerHost } from '@nestjs/bullmq'; -import { type Job, UnrecoverableError } from 'bullmq'; +import { Inject } from '@nestjs/common'; import { MEDIA_JOBS, MEDIA_QUEUES, type UpdateMediaTeam } from '@shared/media'; -import { TeamMemberPolicy } from '@core/teams/domain/policy'; +import { type Job, UnrecoverableError } from 'bullmq'; + import type { TeamRole } from '@shared/entities'; @Processor(MEDIA_QUEUES.SAVE_ENTITY) @@ -17,7 +18,9 @@ export class UpdateTeamMediaListener extends WorkerHost { } async process(job: Job): Promise { - if (job.name !== MEDIA_JOBS.UPDATE_TEAM_MEDIA) return; + if (job.name !== MEDIA_JOBS.UPDATE_TEAM_MEDIA) { + return; + } const { initiatorId, entity, type, path } = job.data; diff --git a/src/teams/infrastructure/persistence/models/teams.model.ts b/src/teams/infrastructure/persistence/models/teams.model.ts index 751f983e..9a78f6f1 100644 --- a/src/teams/infrastructure/persistence/models/teams.model.ts +++ b/src/teams/infrastructure/persistence/models/teams.model.ts @@ -1,7 +1,8 @@ -import { primaryKey, timestamp, text, varchar, index } from 'drizzle-orm/pg-core'; import { createId } from '@paralleldrive/cuid2'; -import { roleEnum, statusEnum } from './enums'; import { baseSchema, users } from '@shared/entities'; +import { primaryKey, timestamp, text, varchar, index } from 'drizzle-orm/pg-core'; + +import { roleEnum, statusEnum } from './enums'; export const teams = baseSchema.table( 'teams', diff --git a/src/teams/infrastructure/persistence/repositories/teams.repository.ts b/src/teams/infrastructure/persistence/repositories/teams.repository.ts index ac4059be..0c3ed2b5 100644 --- a/src/teams/infrastructure/persistence/repositories/teams.repository.ts +++ b/src/teams/infrastructure/persistence/repositories/teams.repository.ts @@ -1,10 +1,12 @@ -import { Inject } from '@nestjs/common'; -import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; -import * as schema from '../models'; +import { ITeamsRepository } from '@core/teams/domain/repository'; import * as scUsers from '@core/user/infrastructure/persistence/models'; +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; +import { Inject } from '@nestjs/common'; import { and, desc, eq, ilike, isNull } from 'drizzle-orm'; + +import * as schema from '../models'; + import type { NewTeam, NewTeamMember, Team, TeamMember } from '@core/teams/domain/entities'; -import { ITeamsRepository } from '@core/teams/domain/repository'; export class TeamsRepository implements ITeamsRepository { constructor( @@ -23,8 +25,8 @@ export class TeamsRepository implements ITeamsRepository { return (result?.count ?? 0) > 0; }; - public readonly create = async (ownerId: string, dto: NewTeam) => { - return this.db.transaction(async (tx) => { + public readonly create = async (ownerId: string, dto: NewTeam) => + this.db.transaction(async (tx) => { const [team] = await tx .insert(schema.teams) .values({ ...dto, ownerId }) @@ -47,10 +49,9 @@ export class TeamsRepository implements ITeamsRepository { teamId: team.teamId, }; }); - }; - public readonly update = async (id: string, dto: Partial) => { - return this.db.transaction(async (tx) => { + public readonly update = async (id: string, dto: Partial) => + this.db.transaction(async (tx) => { const [team] = await tx .update(schema.teams) .set(dto) @@ -66,7 +67,6 @@ export class TeamsRepository implements ITeamsRepository { teamId: team.teamId, }; }); - }; public readonly remove = async (teamId: string, userId: string) => { const result = await this.db @@ -87,11 +87,10 @@ export class TeamsRepository implements ITeamsRepository { return member || null; }; - public readonly findMembers = async (teamId: string) => { - return this.membersQuery + public readonly findMembers = async (teamId: string) => + this.membersQuery .where(eq(schema.teamMembers.teamId, teamId)) .orderBy(desc(schema.teamMembers.joinedAt)); - }; public readonly findByUser = async ( userId: string, @@ -130,7 +129,9 @@ export class TeamsRepository implements ITeamsRepository { public readonly findById = async (teamId: string) => { const [team] = await this.db.select().from(schema.teams).where(eq(schema.teams.id, teamId)); - if (!team) return null; + if (!team) { + return null; + } return team; }; diff --git a/src/teams/infrastructure/workers/mail.processor.ts b/src/teams/infrastructure/workers/mail.processor.ts index d2be68fe..ec6c0655 100644 --- a/src/teams/infrastructure/workers/mail.processor.ts +++ b/src/teams/infrastructure/workers/mail.processor.ts @@ -1,9 +1,10 @@ +import { TeamQueues } from '@core/teams/domain/enums'; +import { TeamInvitationEvent } from '@core/teams/domain/events'; import { Processor, WorkerHost } from '@nestjs/bullmq'; -import type { Job } from 'bullmq'; -import { IMailPort } from '@shared/adapters/mail'; import { Inject } from '@nestjs/common'; -import { TeamInvitationEvent } from '@core/teams/domain/events'; -import { TeamQueues } from '@core/teams/domain/enums'; +import { IMailPort } from '@shared/adapters/mail'; + +import type { Job } from 'bullmq'; @Processor(TeamQueues.TEAM_MAIL) export class MailProcessor extends WorkerHost { diff --git a/src/teams/teams.module.ts b/src/teams/teams.module.ts index 34593b87..92b39574 100644 --- a/src/teams/teams.module.ts +++ b/src/teams/teams.module.ts @@ -1,13 +1,13 @@ +import { MailProcessor } from '@core/teams/infrastructure/workers'; +import { BullModule } from '@nestjs/bullmq'; import { Module } from '@nestjs/common'; + import { TeamsInvitationsController, TeamsMembersController, TeamsController, MeController, } from './application/controller'; -import { BullModule } from '@nestjs/bullmq'; -import { TeamsRepository } from './infrastructure/persistence/repositories'; -import { TeamQueues } from './domain/enums'; import { TeamsFacade } from './application/team.facade'; import { TeamQueries, @@ -15,9 +15,10 @@ import { TEAM_EXTERNAL_QUERIES, TEAM_EXTERNAL_COMMANDS, } from './application/use-cases'; +import { TeamQueues } from './domain/enums'; import { TeamMemberPolicy } from './domain/policy'; -import { MailProcessor } from '@core/teams/infrastructure/workers'; import { LISTENERS } from './infrastructure/listeners'; +import { TeamsRepository } from './infrastructure/persistence/repositories'; const REPOSITORY = { provide: 'ITeamsRepository', useClass: TeamsRepository }; diff --git a/src/user/application/controller/settings/controller.ts b/src/user/application/controller/settings/controller.ts index 66a174c9..5263602f 100644 --- a/src/user/application/controller/settings/controller.ts +++ b/src/user/application/controller/settings/controller.ts @@ -1,8 +1,10 @@ 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'; +import { UserFacade } from '../../user.facade'; + +import { PatchMeNotificationsSwagger } from './swagger'; @ApiBaseController('users/me', 'Account Settings', true) export class UserSettingsController { diff --git a/src/user/application/controller/settings/swagger.ts b/src/user/application/controller/settings/swagger.ts index ad4c4999..4a478df9 100644 --- a/src/user/application/controller/settings/swagger.ts +++ b/src/user/application/controller/settings/swagger.ts @@ -1,10 +1,11 @@ -import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { applyDecorators, SetMetadata } from '@nestjs/common'; -import { ApiUnauthorized, ApiValidationError } from '@shared/error'; +import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { ActionResponse } from '@shared/dtos'; -import { UpdateNotificationsDto } from '../../dtos'; +import { ApiUnauthorized, ApiValidationError } from '@shared/error'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; +import { UpdateNotificationsDto } from '../../dtos'; + export const PatchMeNotificationsSwagger = () => applyDecorators( ApiOperation({ diff --git a/src/user/application/controller/user/controller.ts b/src/user/application/controller/user/controller.ts index d60790ef..ee47c00a 100644 --- a/src/user/application/controller/user/controller.ts +++ b/src/user/application/controller/user/controller.ts @@ -1,10 +1,12 @@ import { Body, Get, Patch, Query } from '@nestjs/common'; -import { GetMeActivitySwagger, GetMeSwagger, PatchMeSwagger } from './swagger'; -import { UpdateProfileDto } from '../../dtos'; import { ApiBaseController, GetUserId } from '@shared/decorators'; -import { UserFacade } from '../../user.facade'; import { PaginationDto } from '@shared/dtos'; +import { UpdateProfileDto } from '../../dtos'; +import { UserFacade } from '../../user.facade'; + +import { GetMeActivitySwagger, GetMeSwagger, PatchMeSwagger } from './swagger'; + @ApiBaseController('users/me', 'Account Profile', true) export class UserController { constructor(private readonly facade: UserFacade) {} diff --git a/src/user/application/controller/user/swagger.ts b/src/user/application/controller/user/swagger.ts index 16a5edbf..922411df 100644 --- a/src/user/application/controller/user/swagger.ts +++ b/src/user/application/controller/user/swagger.ts @@ -1,10 +1,11 @@ -import { ApiBody, ApiExtraModels, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { UpdateProfileDto, UserActivityResponse, UserResponse } from '../../dtos'; import { applyDecorators, SetMetadata } from '@nestjs/common'; -import { ApiUnauthorized, ApiValidationError } from '@shared/error'; +import { ApiBody, ApiExtraModels, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; import { ActionResponse } from '@shared/dtos'; +import { ApiUnauthorized, ApiValidationError } from '@shared/error'; import { ZOD_RESPONSE_TOKEN } from '@shared/interceptors'; +import { UpdateProfileDto, UserActivityResponse, UserResponse } from '../../dtos'; + export const GetMeSwagger = () => applyDecorators( ApiExtraModels(UserResponse.Output), diff --git a/src/user/application/dtos/user.dto.ts b/src/user/application/dtos/user.dto.ts index a0f78173..5046d290 100644 --- a/src/user/application/dtos/user.dto.ts +++ b/src/user/application/dtos/user.dto.ts @@ -1,6 +1,6 @@ +import { AvatarResponseSchema, createPaginationSchema } from '@shared/schemas'; import { createZodDto } from 'nestjs-zod'; import { z } from 'zod/v4'; -import { AvatarResponseSchema, createPaginationSchema } from '@shared/schemas'; const NotificationsSchema = z .object({ diff --git a/src/user/application/use-cases/find-user.query.ts b/src/user/application/use-cases/find-user.query.ts index e820e178..3ec9225d 100644 --- a/src/user/application/use-cases/find-user.query.ts +++ b/src/user/application/use-cases/find-user.query.ts @@ -1,5 +1,5 @@ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { IUserRepository } from '@core/user/domain/repository'; +import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { BaseException } from '@shared/error'; @Injectable() @@ -10,8 +10,12 @@ export class FindUserQuery { ) {} async execute(params: { readonly email?: string; readonly id?: string }) { - if (params.email) return this.repository.findByEmail(params.email); - if (params.id) return this.repository.findById(params.id); + if (params.email) { + return this.repository.findByEmail(params.email); + } + if (params.id) { + return this.repository.findById(params.id); + } throw new BaseException( { diff --git a/src/user/application/use-cases/index.ts b/src/user/application/use-cases/index.ts index ae3ba887..53a5e6f7 100644 --- a/src/user/application/use-cases/index.ts +++ b/src/user/application/use-cases/index.ts @@ -1,14 +1,13 @@ +import { FindByIdsQuery } from './find-by-ids.query'; +import { FindProfileQuery } from './find-profile.query'; +import { FindUserQuery } from './find-user.query'; +import { GetActivityQuery } from './get-activity.query'; import { RegisterUserUseCase } from './register-user.use-case'; import { UpdateNotificationsUseCase } from './update-notifications.use-case'; import { UpdatePasswordUseCase } from './update-password.use-case'; import { UpdateProfileUseCase } from './update-profile.use-case'; import { UploadAvatarUseCase } from './upload-avatar.use-case'; -import { FindProfileQuery } from './find-profile.query'; -import { FindUserQuery } from './find-user.query'; -import { GetActivityQuery } from './get-activity.query'; -import { FindByIdsQuery } from './find-by-ids.query'; - export * from './register-user.use-case'; export * from './update-notifications.use-case'; export * from './update-password.use-case'; diff --git a/src/user/application/use-cases/register-user.use-case.ts b/src/user/application/use-cases/register-user.use-case.ts index 50aa9664..ba5ee612 100644 --- a/src/user/application/use-cases/register-user.use-case.ts +++ b/src/user/application/use-cases/register-user.use-case.ts @@ -1,9 +1,10 @@ -import type { NewUser } from '@core/user/domain/entities'; import { IUserRepository } from '@core/user/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { createId } from '@paralleldrive/cuid2'; import { BaseException } from '@shared/error'; +import type { NewUser } from '@core/user/domain/entities'; + @Injectable() export class RegisterUserUseCase { constructor( diff --git a/src/user/application/use-cases/update-notifications.use-case.ts b/src/user/application/use-cases/update-notifications.use-case.ts index 2c9c55c6..a47e5391 100644 --- a/src/user/application/use-cases/update-notifications.use-case.ts +++ b/src/user/application/use-cases/update-notifications.use-case.ts @@ -1,10 +1,11 @@ import { IUserRepository } from '@core/user/domain/repository'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { BaseException } from '@shared/error'; -import { UpdateNotificationsDto } from '../dtos'; import { createId } from '@paralleldrive/cuid2'; +import { BaseException } from '@shared/error'; import { removeUndefined } from '@shared/utils'; +import { UpdateNotificationsDto } from '../dtos'; + @Injectable() export class UpdateNotificationsUseCase { constructor( @@ -51,7 +52,9 @@ export class UpdateNotificationsUseCase { message: 'Настройки уведомлений обновлены', }; } catch (error) { - if (error instanceof BaseException) throw error; + if (error instanceof BaseException) { + throw error; + } throw new BaseException( { diff --git a/src/user/application/use-cases/update-profile.use-case.ts b/src/user/application/use-cases/update-profile.use-case.ts index 5d7e22f0..d2dd35ee 100644 --- a/src/user/application/use-cases/update-profile.use-case.ts +++ b/src/user/application/use-cases/update-profile.use-case.ts @@ -1,10 +1,11 @@ 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'; +import { BaseException } from '@shared/error'; import { removeUndefined } from '@shared/utils'; +import { UpdateProfileDto } from '../dtos'; + @Injectable() export class UpdateProfileUseCase { constructor( diff --git a/src/user/application/user.facade.ts b/src/user/application/user.facade.ts index b8819693..75cb84d0 100644 --- a/src/user/application/user.facade.ts +++ b/src/user/application/user.facade.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; + +import { UpdateProfileDto, UpdateNotificationsDto } from './dtos'; import { FindProfileQuery, GetActivityQuery, UpdateNotificationsUseCase, UpdateProfileUseCase, } from './use-cases'; -import { UpdateProfileDto, UpdateNotificationsDto } from './dtos'; @Injectable() export class UserFacade { diff --git a/src/user/domain/entities/user.domain.ts b/src/user/domain/entities/user.domain.ts index 4b8b40da..53918888 100644 --- a/src/user/domain/entities/user.domain.ts +++ b/src/user/domain/entities/user.domain.ts @@ -1,4 +1,3 @@ -import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; import type { users, userSecurity, @@ -6,6 +5,7 @@ import type { userActivity, userPreferences, } from '../../infrastructure/persistence/models/user.entity'; +import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; export type User = InferSelectModel; export type NewUser = InferInsertModel; diff --git a/src/user/infrastructure/listeners/update-avatar.listener.ts b/src/user/infrastructure/listeners/update-avatar.listener.ts index 631a916b..2e123681 100644 --- a/src/user/infrastructure/listeners/update-avatar.listener.ts +++ b/src/user/infrastructure/listeners/update-avatar.listener.ts @@ -14,7 +14,9 @@ export class UpdateAvatarListener extends WorkerHost { } async process(job: Job) { - if (job.name !== MEDIA_JOBS.UPDATE_USER_AVATAR) return; + if (job.name !== MEDIA_JOBS.UPDATE_USER_AVATAR) { + return; + } const { entity, path } = job.data; diff --git a/src/user/infrastructure/persistence/models/user.entity.ts b/src/user/infrastructure/persistence/models/user.entity.ts index 7f918a62..1ba0ddc1 100644 --- a/src/user/infrastructure/persistence/models/user.entity.ts +++ b/src/user/infrastructure/persistence/models/user.entity.ts @@ -1,6 +1,6 @@ import { createId } from '@paralleldrive/cuid2'; -import { varchar, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core'; import { baseSchema } from '@shared/entities'; +import { varchar, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core'; export const users = baseSchema.table('users', { id: text('id') diff --git a/src/user/infrastructure/persistence/repositories/user.repository.ts b/src/user/infrastructure/persistence/repositories/user.repository.ts index a35c034a..e4628224 100644 --- a/src/user/infrastructure/persistence/repositories/user.repository.ts +++ b/src/user/infrastructure/persistence/repositories/user.repository.ts @@ -1,9 +1,11 @@ import { IUserRepository } from '@core/user/domain/repository'; -import * as sc from '../models'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import { Inject, Injectable } from '@nestjs/common'; import { createId } from '@paralleldrive/cuid2'; import { desc, eq, count, inArray } from 'drizzle-orm'; + +import * as sc from '../models'; + import type { NewUser, NewUserActivity, @@ -62,14 +64,18 @@ export class UserRepository implements IUserRepository { }; public readonly findByIds = async (ids: readonly string[]) => { - if (ids.length === 0) return []; + if (ids.length === 0) { + return []; + } return this.db.select().from(sc.users).where(inArray(sc.users.id, ids)); }; public readonly findById = async (id: string) => { const [row] = await this.fullUserQuery.where(eq(sc.users.id, id)); - if (!row || !row.user_security) return null; + if (!row || !row.user_security) { + return null; + } return { user: row.users, security: { @@ -80,7 +86,9 @@ export class UserRepository implements IUserRepository { public readonly findByEmail = async (email: string) => { const [row] = await this.fullUserQuery.where(eq(sc.users.email, email.toLowerCase())); - if (!row || !row.user_security) return null; + if (!row || !row.user_security) { + return null; + } return { user: row.users, security: { @@ -97,8 +105,8 @@ export class UserRepository implements IUserRepository { return result || null; }; - public readonly create = async (data: NewUser) => { - return this.db.transaction(async (tx) => { + public readonly create = async (data: NewUser) => + this.db.transaction(async (tx) => { const [newUser] = await tx.insert(sc.users).values(data).returning(); if (!newUser) { @@ -111,7 +119,6 @@ export class UserRepository implements IUserRepository { return newUser; }); - }; public readonly updateProfile = async ( id: string, @@ -127,7 +134,9 @@ export class UserRepository implements IUserRepository { }; private async updateUser(id: string, data: Partial) { - if (Object.keys(data).length === 0) return null; + if (Object.keys(data).length === 0) { + return null; + } const result = await this.db .update(sc.users) diff --git a/src/user/user.module.ts b/src/user/user.module.ts index e473132e..0010dc41 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; -import { UserRepository } from './infrastructure/persistence/repositories'; + import { UserController, UserSettingsController } from './application/controller'; -import { UserFacade } from './application/user.facade'; import { USER_EXTERNAL_USE_CASES, UserQueries, UserUseCases } from './application/use-cases'; +import { UserFacade } from './application/user.facade'; import { LISTENERS } from './infrastructure/listeners'; +import { UserRepository } from './infrastructure/persistence/repositories'; const REPOSITORY = { provide: 'IUserRepository', diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index b11d3937..8c5f6905 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,6 +1,7 @@ +import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; import { Test, type TestingModule } from '@nestjs/testing'; + import { AppModule } from '../src/app.module'; -import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; describe('App (e2e)', () => { let app: NestFastifyApplication;