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..9752b6d1 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,205 @@ +// @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( + { + ignores: ['node_modules', 'dist', '**/*.js', '**/*.d.ts', 'infra', 'migrations'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { + functional, + perfectionist: pluginPerfectionist, + unicorn: pluginUnicorn, + sonarjs: pluginSonarjs, + jsdoc: pluginJsdoc, + }, + 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', + + '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', + { + 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', + }, + }, + { + 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/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..7f3c6c20 100644 --- a/libs/bootstrap/src/setups/cors.ts +++ b/libs/bootstrap/src/setups/cors.ts @@ -1,7 +1,8 @@ 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 +25,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..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 = { /** @@ -129,95 +134,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/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 48e55bfc..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); @@ -23,7 +26,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/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/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/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 71a782a7..a0618d8b 100644 --- a/libs/health/src/controller/health.controlller.spec.ts +++ b/libs/health/src/controller/health.controlller.spec.ts @@ -1,10 +1,11 @@ +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; - let healthServiceMock: { getHealthData: ReturnType }; + let healthServiceMock: { readonly getHealthData: ReturnType }; const SERVICE_NAME = 'MyService'; @@ -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 c0c7639e..1d3eae53 100644 --- a/libs/health/src/health.module-definition.ts +++ b/libs/health/src/health.module-definition.ts @@ -1,5 +1,6 @@ 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/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/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.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 bcfc28d9..affdcfb0 100644 --- a/libs/imagor/src/imagor.service.ts +++ b/libs/imagor/src/imagor.service.ts @@ -1,26 +1,29 @@ +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 { - 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, ) {} /** * Выполняет 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(/\/+$/, ''); @@ -28,21 +31,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); - }), - ), - ); - - return Buffer.from(response.data); - } catch (error) { - throw 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); } private buildTransformPath(path: string, presetOrFilters?: string | Filters): string { @@ -59,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/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/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/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..e91f27b0 100644 --- a/libs/s3/src/s3.module-definition.ts +++ b/libs/s3/src/s3.module-definition.ts @@ -1,5 +1,6 @@ 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.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 c44341b7..5e8e4699 100644 --- a/libs/s3/src/s3.service.ts +++ b/libs/s3/src/s3.service.ts @@ -1,19 +1,25 @@ +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() 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 +34,7 @@ export class S3Service { }), ); return true; - } catch (error) { + } catch { return false; } } diff --git a/package.json b/package.json index 959c2516..276012fa 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,24 @@ "@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-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", "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..8e10d5fa 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,26 @@ 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-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 @@ -222,6 +240,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)) @@ -440,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'} @@ -563,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' @@ -1025,13 +1058,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 +1144,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 +1587,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'} @@ -1691,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'} @@ -2030,9 +2083,15 @@ 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==} + '@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==} @@ -2090,9 +2149,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 +2161,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 - - '@typescript-eslint/scope-manager@6.21.0': - resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} - engines: {node: ^16.0.0 || >=18.0.0} + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.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/utils@6.21.0': - resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/types@8.61.0': + resolution: {integrity: sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.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==} @@ -2347,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'} @@ -2360,10 +2418,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 +2479,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} @@ -2453,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'} @@ -2479,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==} @@ -2494,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'} @@ -2579,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==} @@ -2613,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==} @@ -2667,6 +2744,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'} @@ -2693,18 +2774,14 @@ 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'} - 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 +2898,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 +2989,76 @@ 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-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: ^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==} 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 +3112,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 +3158,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==} @@ -3058,13 +3174,17 @@ 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'} - 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 +3219,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} @@ -3110,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.*} @@ -3134,10 +3254,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,21 +3265,13 @@ 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'} + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} @@ -3172,9 +3280,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'} @@ -3196,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==} @@ -3219,6 +3327,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,9 +3342,9 @@ 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. + indent-string@5.0.0: + resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} + engines: {node: '>=12'} inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -3252,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'} @@ -3268,6 +3384,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 +3402,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'} @@ -3339,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==} @@ -3372,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==} @@ -3588,10 +3719,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 +3751,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==} @@ -3657,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==} @@ -3705,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==} @@ -3749,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'} @@ -3784,10 +3920,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 +4067,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==} @@ -3968,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'} @@ -3979,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'} @@ -4009,19 +4158,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==} @@ -4038,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'} @@ -4053,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==} @@ -4061,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==} @@ -4085,10 +4238,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'} @@ -4123,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'} @@ -4180,9 +4338,9 @@ 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'} + strip-indent@4.1.1: + resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} + engines: {node: '>=12'} strnum@2.2.3: resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} @@ -4237,9 +4395,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'} @@ -4270,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'} @@ -4290,11 +4449,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.2.0' + typescript: '>=4.8.4' + + ts-declaration-location@1.0.7: + resolution: {integrity: sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==} + peerDependencies: + typescript: '>=4.0.0' ts-loader@9.5.7: resolution: {integrity: sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==} @@ -4327,6 +4491,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'} @@ -5095,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 @@ -5261,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 @@ -5493,28 +5676,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 +5804,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 +6207,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 @@ -6127,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 @@ -6570,8 +6758,12 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 20.19.39 @@ -6656,8 +6848,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 +6861,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: @@ -6969,6 +7162,8 @@ snapshots: ansis@4.2.0: {} + are-docs-informative@0.0.2: {} + argon2@0.44.0: dependencies: '@phc/format': 1.0.0 @@ -6982,8 +7177,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 +7243,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 @@ -7084,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 @@ -7096,6 +7289,8 @@ snapshots: transitivePeerDependencies: - supports-color + bytes@3.1.2: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -7115,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: @@ -7126,6 +7323,8 @@ snapshots: chrome-trace-event@1.0.4: {} + ci-info@4.4.0: {} + cli-boxes@2.2.1: optional: true @@ -7200,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 @@ -7228,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): @@ -7276,6 +7481,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge-ts@7.1.5: {} + deepmerge@4.3.1: {} defaults@1.0.4: @@ -7292,15 +7499,9 @@ snapshots: detect-europe-js@0.1.2: {} - detect-libc@2.1.2: {} - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 + detect-indent@7.0.2: {} - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 + detect-libc@2.1.2: {} dot-prop@5.3.0: dependencies: @@ -7330,11 +7531,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 +7688,149 @@ 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-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: 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 +7866,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 +7927,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: @@ -7679,16 +7950,17 @@ 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 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,13 +8001,13 @@ snapshots: fs-monkey@1.1.0: {} - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true 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: {} @@ -7770,10 +8042,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 +8054,16 @@ 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 + globals@17.6.0: {} gopd@1.2.0: {} graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - handlebars@4.7.9: dependencies: minimist: 1.2.8 @@ -7839,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: @@ -7859,6 +8107,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7868,10 +8118,7 @@ snapshots: imurmurhash@0.1.4: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 + indent-string@5.0.0: {} inherits@2.0.4: {} @@ -7895,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: {} @@ -7907,14 +8158,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: {} @@ -7958,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: {} @@ -7995,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 @@ -8188,8 +8453,6 @@ snapshots: merge-stream@2.0.0: {} - merge2@1.4.1: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -8215,10 +8478,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: {} @@ -8247,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): @@ -8285,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: {} @@ -8347,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 @@ -8354,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 @@ -8387,8 +8656,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 +8801,6 @@ snapshots: punycode@2.3.1: {} - queue-microtask@1.2.3: {} - quick-format-unescaped@4.0.4: {} readable-stream@2.3.8: @@ -8572,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: {} @@ -8600,10 +8882,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 +8903,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 @@ -8645,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: {} @@ -8662,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: {} @@ -8682,8 +8968,6 @@ snapshots: signal-exit@4.1.0: {} - slash@3.0.0: {} - slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -8713,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: {} @@ -8764,7 +9057,7 @@ snapshots: strip-bom@3.0.0: {} - strip-json-comments@3.1.1: {} + strip-indent@4.1.1: {} strnum@2.2.3: {} @@ -8812,8 +9105,6 @@ snapshots: text-hex@1.0.0: {} - text-table@0.2.0: {} - thread-stream@4.0.0: dependencies: real-require: 0.2.0 @@ -8843,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: @@ -8858,8 +9154,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 +9199,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/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 9aefea0f..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 type { 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/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..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( @@ -37,25 +38,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 +104,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, @@ -160,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/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/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 8bc1b530..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() @@ -111,7 +113,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..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() @@ -46,59 +48,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..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 { @@ -22,7 +25,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/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 35de0687..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 { @@ -22,7 +25,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 +42,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/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/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..4c601266 100644 --- a/src/auth/application/strategies/index.ts +++ b/src/auth/application/strategies/index.ts @@ -1,8 +1,11 @@ +// 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 { 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 41a69428..77cd5c88 100644 --- a/src/auth/application/strategies/resend-code.strategy.ts +++ b/src/auth/application/strategies/resend-code.strategy.ts @@ -1,11 +1,13 @@ -import { 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 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..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 { 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 { 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 = 'Повторный код для восстановления пароля отправлен на вашу почту'; @@ -20,7 +22,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..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 { SignUpCacheData } from '@core/auth/application/interfaces'; import { EMAIL_CODE_TTL_SECONDS, SIGNUP_CACHE_KEY } from '@core/auth/infrastructure/constants'; -import { 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 = 'Повторный код подтверждения отправлен на вашу почту'; @@ -16,7 +18,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/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 134e0a39..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 { @@ -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/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/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/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/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/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 787f9efc..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( @@ -11,7 +12,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 +22,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 +30,13 @@ export class IdentitiyRepository implements IIdentityRepository { return result.count.valueOf() > 0; }; - public 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 findByProvider = async ( + public readonly findByProvider = async ( provider: 'google' | 'yandex' | 'github', providerUserId: string, ) => { 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/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..e7d2d189 100644 --- a/src/auth/infrastructure/strategies/vkontakte.strategy.ts +++ b/src/auth/infrastructure/strategies/vkontakte.strategy.ts @@ -1,83 +1,83 @@ +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 { - 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() @@ -111,12 +111,12 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau }); } - async validate( + validate( _req: never, _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', @@ -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); @@ -216,23 +218,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', @@ -242,35 +241,23 @@ export class VkontakteStrategy extends PassportStrategy(Strategy, 'vkontakte-oau familyName: json.last_name || '', givenName: json.first_name || '', }, - gender: 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 +269,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..8ebbdba3 100644 --- a/src/auth/infrastructure/strategies/yandex.strategy.ts +++ b/src/auth/infrastructure/strategies/yandex.strategy.ts @@ -1,44 +1,44 @@ +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 { - 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() @@ -67,12 +67,12 @@ export class YandexStrategy extends PassportStrategy(Strategy, 'yandex-oauth') { }); } - async validate( + validate( _req: never, _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..34e259ec 100644 --- a/src/auth/infrastructure/utils/get-device-meta.ts +++ b/src/auth/infrastructure/utils/get-device-meta.ts @@ -1,12 +1,13 @@ -import type { FastifyRequest } from 'fastify'; import { UAParser } from 'ua-parser-js'; +import type { FastifyRequest } from 'fastify'; + 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 { @@ -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 3e4a926d..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 { @@ -46,7 +48,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 +60,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..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) @@ -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/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/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/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 22d123ca..9fbbe3db 100644 --- a/src/projects/application/project.facade.ts +++ b/src/projects/application/project.facade.ts @@ -1,13 +1,19 @@ 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, 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 127dd670..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 { @@ -26,7 +27,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/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 cc0dadf6..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 type { 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 220a4456..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 { @@ -43,7 +45,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 +93,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..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 type { 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 539ab571..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 type { 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( @@ -41,18 +42,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 +74,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..8d01cde5 100644 --- a/src/projects/domain/entities/member.domain.ts +++ b/src/projects/domain/entities/member.domain.ts @@ -1,5 +1,5 @@ +import type { projectMembers } from '@shared/entities'; import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; -import { 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..e39b8da6 100644 --- a/src/projects/domain/entities/project.domain.ts +++ b/src/projects/domain/entities/project.domain.ts @@ -1,5 +1,8 @@ +import type { + projects, + projectShares, +} from '../../infrastructure/persistence/models/project.model'; import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'; -import { 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..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 { @@ -64,7 +66,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) { @@ -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/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/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 42ae2da8..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 { @@ -12,7 +14,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 +27,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,13 +37,12 @@ export class MemberRepository implements IMemberRepository { return result ?? null; }; - public 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 @@ -58,7 +59,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 +73,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 +88,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 +97,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 +107,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..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() @@ -12,7 +14,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 +40,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 +60,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 +80,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 +95,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 +110,13 @@ export class ProjectRepository implements IProjectRepository { return project || null; }; - public 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 createShare = async (data: NewProjectShare) => { + public readonly createShare = async (data: NewProjectShare) => { const [result] = await this.db .insert(schema.projectShares) .values(data) @@ -127,7 +132,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 +151,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 +160,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/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 89f72cf6..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); } @@ -26,8 +29,13 @@ export class RedisCacheAdapter implements ICacheService { await this.redis.set(key, value, 'EX', ttlSeconds); } - async setMany(items: { key: string; value: string }[], ttlSeconds: number = this.defaultTtl) { - if (!items.length) return; + async setMany( + items: readonly { readonly key: string; readonly value: string }[], + ttlSeconds: number = this.defaultTtl, + ) { + if (!items.length) { + return; + } const pipeline = this.redis.pipeline(); @@ -43,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() @@ -57,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); } @@ -66,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); } @@ -110,7 +124,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); } @@ -124,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; @@ -136,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; } @@ -147,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/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..e62e050e 100644 --- a/src/shared/adapters/mail/adapter.ts +++ b/src/shared/adapters/mail/adapter.ts @@ -1,16 +1,18 @@ +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() 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'); @@ -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 640645fa..87bb640b 100644 --- a/src/shared/error/exception.ts +++ b/src/shared/error/exception.ts @@ -1,14 +1,14 @@ -import { HttpException, HttpStatus } from '@nestjs/common'; +import { HttpException, type HttpStatus } 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..0ae8b902 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -6,18 +6,20 @@ 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); - 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,7 +41,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { return this.handleUnknownError(exception, host); } - private parseZodValidation = async (exception: ZodValidationException, host: ArgumentsHost) => { + private parseZodValidation = (exception: ZodValidationException, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); @@ -61,7 +63,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private parseDatabase = async (exception: DrizzleQueryError, host: ArgumentsHost) => { + private parseDatabase = (exception: DrizzleQueryError, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const error = @@ -101,7 +103,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private parseHttp = async (exception: BaseException, host: ArgumentsHost) => { + private parseHttp = (exception: BaseException, host: ArgumentsHost) => { const { request, response } = this.getCtxBase(host); const status = exception.getStatus(); @@ -123,7 +125,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { ); }; - private 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(); @@ -174,7 +176,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/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 b3a8a47a..4ca24cf1 100644 --- a/src/shared/error/swagger.ts +++ b/src/shared/error/swagger.ts @@ -1,12 +1,17 @@ +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, 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 +54,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 +63,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..1542ce8f 100644 --- a/src/shared/guards/bearer.guard.ts +++ b/src/shared/guards/bearer.guard.ts @@ -3,18 +3,22 @@ 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') { - constructor(private reflector: Reflector) { + constructor(private readonly reflector: Reflector) { 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; @@ -51,7 +55,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/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/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/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 dd8a8970..6e11740a 100644 --- a/src/shared/interceptors/zod-validation.interceptor.ts +++ b/src/shared/interceptors/zod-validation.interceptor.ts @@ -6,20 +6,20 @@ 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'; @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/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 fa935ad6..48ca9279 100644 --- a/src/shared/media/decorators/extract-media-req.decorator.ts +++ b/src/shared/media/decorators/extract-media-req.decorator.ts @@ -1,12 +1,16 @@ 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 ( - { allowedMimetypes = IMAGE_MIME_TYPES }: { allowedMimetypes?: string[] } = {}, + { + allowedMimetypes = IMAGE_MIME_TYPES, + }: { readonly allowedMimetypes?: readonly string[] } = {}, ctx: ExecutionContext, ) => { const maxFileSize = 5 * 1024 * 1024; @@ -48,8 +52,10 @@ export const ExtractMediaReq = createParamDecorator( const fields: Record = {}; - for (const key in file.fields) { - if (key === 'file') continue; + for (const key of Object.keys(file.fields)) { + if (key === 'file') { + continue; + } const field = file.fields[key]; if (field && !Array.isArray(field) && 'value' in field) { @@ -66,9 +72,8 @@ export const ExtractMediaReq = createParamDecorator( ...fields, }; } catch (e) { - const hasCode = (err: unknown): err is { 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 349fe0f0..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 type { 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 { @@ -17,7 +20,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); @@ -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 79bc0506..d10cf3ed 100644 --- a/src/shared/media/strategies/team-media.strategy.ts +++ b/src/shared/media/strategies/team-media.strategy.ts @@ -1,16 +1,23 @@ -import type { UploadMediaDto } from '../dtos'; -import type { UpdateMediaTeam } from '../interfaces/media.interface'; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import { UploadMediaDto } from '../dtos'; import { MEDIA_JOBS } from '../media.constant'; -import { MediaDispatchStrategy } from './media.strategy'; + +import type { UpdateMediaTeam } from '../interfaces/media.interface'; +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..94447aa6 100644 --- a/src/shared/media/strategies/user-avatar.strategy.ts +++ b/src/shared/media/strategies/user-avatar.strategy.ts @@ -1,10 +1,12 @@ +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 { 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..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 { @@ -14,8 +16,12 @@ export class MediaProcessor extends WorkerHost { super(); } - async process(job: Job<{ original: string; context: string; userId: string }>) { - if (job.name !== MEDIA_JOBS.RESIZE_IMAGES) return; + 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; @@ -30,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 1c2f70af..cff59b2e 100644 --- a/src/shared/schemas/sorting.schema.ts +++ b/src/shared/schemas/sorting.schema.ts @@ -1,11 +1,11 @@ import { z } from 'zod/v4'; -export const createSortingSchema = ( +export const createSortingSchema = ( fields: T, defaultField?: T[number], defaultOrder: 'asc' | 'desc' = 'asc', -) => { - return z.object({ +) => + z.object({ sortBy: z .enum(fields) .optional() @@ -25,4 +25,3 @@ export const createSortingSchema = ( .default(() => defaultOrder) .describe('Направление сортировки: asc - по возрастанию, desc - по убыванию'), }); -}; 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/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/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/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 b3e8ece2..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 type { UpdateMemberDto } from '../../dtos/member.dto'; + +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 fe19a55c..23b93af7 100644 --- a/src/teams/application/dtos/invitation.dto.ts +++ b/src/teams/application/dtos/invitation.dto.ts @@ -1,7 +1,8 @@ -import { z } from 'zod/v4'; -import { createZodDto } from 'nestjs-zod'; -import { roleEnum, TeamRole } 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 @@ -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/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 530bc0a3..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; @@ -22,7 +23,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)); } @@ -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 e07445ef..50df66d6 100644 --- a/src/teams/application/team.facade.ts +++ b/src/teams/application/team.facade.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; -import * as UC from './use-cases'; -import type { + +import { CreateTeamDto, InviteMemberDto, UpdateInvitationDto, 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 253e7972..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,8 +1,9 @@ import { Inject, Injectable, HttpStatus } from '@nestjs/common'; -import { ITeamsRepository } from '../../../domain/repository'; -import type { UpdateTeamDto } from '../../dtos'; import { BaseException } from '@shared/error'; +import { ITeamsRepository } from '../../../domain/repository'; +import { UpdateTeamDto } from '../../dtos'; + @Injectable() export class UpdateTeamUseCase { constructor( @@ -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 07588df9..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 @@ -132,7 +139,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..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,12 +1,14 @@ +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 { 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 { UpdateInvitationDto } from '../../dtos'; +import { TeamInvite } from '../../dtos/invitation.dto'; + +import type { TeamRole } from '../../../infrastructure/persistence/models'; @Injectable() export class UpdateInvitationUseCase { @@ -28,8 +30,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/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 e4d97cb3..4ee5e26b 100644 --- a/src/teams/domain/entities/teams.domain.ts +++ b/src/teams/domain/entities/teams.domain.ts @@ -1,5 +1,5 @@ +import type { teams, teamMembers } from '../../infrastructure/persistence/models'; import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; -import { 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/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/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/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 a2582b50..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( @@ -12,7 +14,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,8 +25,8 @@ export class TeamsRepository implements ITeamsRepository { return (result?.count ?? 0) > 0; }; - public 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 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,9 +67,8 @@ export class TeamsRepository implements ITeamsRepository { teamId: team.teamId, }; }); - }; - 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,14 @@ export class TeamsRepository implements ITeamsRepository { return member || null; }; - public 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 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 +127,15 @@ 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; + 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 +145,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..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 { @@ -35,7 +36,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/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-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..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() @@ -9,9 +9,13 @@ export class FindUserQuery { private readonly repository: IUserRepository, ) {} - async execute(params: { email?: string; id?: string }) { - if (params.email) return this.repository.findByEmail(params.email); - if (params.id) return this.repository.findById(params.id); + 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); + } 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 8b5e6461..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( @@ -11,7 +12,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/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 ad6c9e18..53918888 100644 --- a/src/user/domain/entities/user.domain.ts +++ b/src/user/domain/entities/user.domain.ts @@ -1,11 +1,11 @@ -import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; -import { +import type { users, userSecurity, userNotifications, userActivity, userPreferences, } from '../../infrastructure/persistence/models/user.entity'; +import type { InferSelectModel, InferInsertModel } from 'drizzle-orm'; export type User = InferSelectModel; export type NewUser = InferInsertModel; @@ -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/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 6ba5e3f7..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') @@ -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..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, @@ -27,7 +29,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,15 +63,19 @@ export class UserRepository implements IUserRepository { }; }; - public findByIds = async (ids: string[]) => { - if (ids.length === 0) return []; + 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; + if (!row || !row.user_security) { + return null; + } return { user: row.users, security: { @@ -78,9 +84,11 @@ 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; + if (!row || !row.user_security) { + return null; + } return { user: row.users, security: { @@ -89,7 +97,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,8 +105,8 @@ export class UserRepository implements IUserRepository { return result || null; }; - public 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,9 +119,8 @@ export class UserRepository implements IUserRepository { return newUser; }); - }; - public updateProfile = async ( + public readonly updateProfile = async ( id: string, user: Partial, preferences?: Partial, @@ -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) @@ -200,7 +209,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/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 733a1f5e..8c5f6905 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,6 +1,7 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppModule } from '../src/app.module'; import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; +import { Test, type TestingModule } from '@nestjs/testing'; + +import { AppModule } from '../src/app.module'; describe('App (e2e)', () => { let app: NestFastifyApplication; 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/*"],