Update dependencies & fix linter errors

This commit is contained in:
Денис 2026-02-17 15:22:41 +03:00
parent 9fc2e48bc4
commit f06a4b8926
8 changed files with 1323 additions and 923 deletions

View File

@ -18,6 +18,6 @@ RUN \
else echo "pnpm-lock.yaml not found." && exit 1; \ else echo "pnpm-lock.yaml not found." && exit 1; \
fi fi
RUN pnpm add -g @nestjs/cli RUN pnpm add -g @nestjs/cli@11
COPY . . COPY . .

View File

@ -14,7 +14,7 @@ A NestJS starter template with authentication, user management, email service, a
## Tech Stack ## Tech Stack
- NestJS 10 - NestJS 11
- TypeScript 5 - TypeScript 5
- PostgreSQL 17 + TypeORM - PostgreSQL 17 + TypeORM
- Redis + BullMQ - Redis + BullMQ

View File

@ -26,18 +26,18 @@
}, },
"dependencies": { "dependencies": {
"@nestjs/bullmq": "^11.0.2", "@nestjs/bullmq": "^11.0.2",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^11.1.13",
"@nestjs/config": "^3.2.0", "@nestjs/config": "^4.0.3",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^11.1.13",
"@nestjs/jwt": "^10.2.0", "@nestjs/jwt": "^11.0.2",
"@nestjs/mapped-types": "^2.0.5", "@nestjs/mapped-types": "^2.0.5",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^11.1.13",
"@nestjs/swagger": "^11.1.4", "@nestjs/swagger": "^11.1.4",
"@nestjs/typeorm": "^10.0.2", "@nestjs/typeorm": "^11.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bullmq": "^5.49.1", "bullmq": "^5.49.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.3",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"nodemailer": "^6.10.1", "nodemailer": "^6.10.1",
@ -49,13 +49,13 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^11.0.16",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^11.1.13",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17", "@types/express": "^5.0.6",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^25.2.3",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
@ -89,7 +89,8 @@
"**/*.(t|j)s" "**/*.(t|j)s"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node",
"passWithNoTests": true
}, },
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a" "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,24 @@
import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common'; import {
BadRequestException,
createParamDecorator,
ExecutionContext,
} from '@nestjs/common';
import { Request } from 'express'; import { Request } from 'express';
import { Between, ILike, In, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm"; import {
Between,
ILike,
In,
IsNull,
LessThan,
LessThanOrEqual,
MoreThan,
MoreThanOrEqual,
Not,
} from 'typeorm';
import { ErrorCode } from '../enums/error-code.enum'; import { ErrorCode } from '../enums/error-code.enum';
export type EntityFields<T> = (keyof T)[]; export type EntityFields<T> = (keyof T)[];
export type FilteringResult = Record<string, unknown>;
export interface IFiltering {
[field: string]: string;
}
interface IFilteringParams { interface IFilteringParams {
field: string; field: string;
@ -52,48 +63,93 @@ export const getWhere = (filter: IFilteringParams) => {
if (filter.rule == FilterRule.LIKE) return ILike(`%${filter.value}%`); if (filter.rule == FilterRule.LIKE) return ILike(`%${filter.value}%`);
if (filter.rule == FilterRule.NOT_LIKE) return Not(ILike(`%${filter.value}%`)); if (filter.rule == FilterRule.NOT_LIKE) return Not(ILike(`%${filter.value}%`));
if (filter.rule == FilterRule.IN) { if (filter.rule == FilterRule.IN) {
const values = filter.value.split(',').filter(v => v.trim() !== ''); const values = filter.value.split(',').filter((v) => v.trim() !== '');
return values.length > 0 ? In(values) : {}; return values.length > 0 ? In(values) : {};
} }
if (filter.rule == FilterRule.NOT_IN) { if (filter.rule == FilterRule.NOT_IN) {
const values = filter.value.split(',').filter(v => v.trim() !== ''); const values = filter.value.split(',').filter((v) => v.trim() !== '');
return values.length > 0 ? Not(In(values)) : {}; return values.length > 0 ? Not(In(values)) : {};
} }
if (filter.rule == FilterRule.BETWEEN) return Between(...(filter.value.split(',') as [string, string])); if (filter.rule == FilterRule.BETWEEN) {
const [from, to] = filter.value.split(',') as [string?, string?];
if (!from || !to) return {};
return Between(from, to);
}
return {};
} }
export const FilteringParams = createParamDecorator((data, ctx: ExecutionContext): IFiltering => { function ensureObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export const FilteringParams = createParamDecorator(
(data: readonly string[], ctx: ExecutionContext): FilteringResult | null => {
const req: Request = ctx.switchToHttp().getRequest(); const req: Request = ctx.switchToHttp().getRequest();
const queryFilters = req.query.filters as string[]; const rawFilters = req.query.filters;
const queryFilters = Array.isArray(rawFilters)
? rawFilters
: typeof rawFilters === 'string'
? [rawFilters]
: null;
if (!queryFilters || !Array.isArray(queryFilters)) return null; if (!queryFilters) return null;
if (!Array.isArray(data)) throw new BadRequestException(ErrorCode.InvalidFilterParams);
if (typeof data !== 'object') throw new BadRequestException(ErrorCode.InvalidFilterParams); const filters: FilteringResult = {};
let filters: { [field: string]: any } = {};
for (const filter of queryFilters) { for (const filter of queryFilters) {
if (typeof filter !== 'string') {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
const [fieldPath, rule, value] = filter.split(':'); const [fieldPath, rule, value] = filter.split(':');
if (!fieldPath || !rule || value === undefined) {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
const fieldParts = fieldPath.split('.'); const fieldParts = fieldPath.split('.');
const field = fieldParts.pop(); const field = fieldParts.pop();
let nestedFilters = filters;
for (const part of fieldParts) { if (!field) {
nestedFilters[part] = nestedFilters[part] || {}; throw new BadRequestException(ErrorCode.InvalidFilterParams);
nestedFilters = nestedFilters[part];
} }
if (!data.includes(fieldPath)) throw new BadRequestException(`${ErrorCode.FilterFieldNotAllowed}:${field}`); let nestedFilters: Record<string, unknown> = filters;
if (!Object.values(FilterRule).includes(rule as FilterRule)) throw new BadRequestException(`${ErrorCode.InvalidFilterParams}:${rule}`);
if ((rule === FilterRule.EQUALS || rule === FilterRule.NOT_EQUALS || rule === FilterRule.IN || rule === FilterRule.NOT_IN) for (const part of fieldParts) {
&& (!value || value.trim() === '')) { const existing = nestedFilters[part];
if (!ensureObjectRecord(existing)) {
nestedFilters[part] = {};
}
nestedFilters = nestedFilters[part] as Record<string, unknown>;
}
if (!data.includes(fieldPath)) {
throw new BadRequestException(`${ErrorCode.FilterFieldNotAllowed}:${field}`);
}
if (!Object.values(FilterRule).includes(rule as FilterRule)) {
throw new BadRequestException(`${ErrorCode.InvalidFilterParams}:${rule}`);
}
const isEmptyValueRule = [
FilterRule.EQUALS,
FilterRule.NOT_EQUALS,
FilterRule.IN,
FilterRule.NOT_IN,
].includes(rule as FilterRule) && (!value || value.trim() === '');
if (isEmptyValueRule) {
continue; continue;
} }
const whereClause = getWhere({ field, rule, value }); const whereClause = getWhere({ field, rule, value });
if (whereClause && (typeof whereClause === 'object' && Object.keys(whereClause).length === 0)) { if (ensureObjectRecord(whereClause) && Object.keys(whereClause).length === 0) {
continue; continue;
} }
@ -101,4 +157,5 @@ export const FilteringParams = createParamDecorator((data, ctx: ExecutionContext
} }
return filters; return filters;
}); },
);

View File

@ -23,10 +23,11 @@ export class ResponseInterceptor<T> implements NestInterceptor<T, IResponse<T>>
const customMessage = this.reflector.get<string>('response_message', context.getHandler()); const customMessage = this.reflector.get<string>('response_message', context.getHandler());
return next.handle().pipe( return next.handle().pipe(
map((data: any) => { map((data: T) => {
const meta = data as unknown as { statusCode?: number; message?: string };
return { return {
statusCode: data?.statusCode || httpResponse.statusCode || 200, statusCode: meta?.statusCode || httpResponse.statusCode || 200,
message: data?.message || customMessage || 'success', message: meta?.message || customMessage || 'success',
data: this.clearResponseData(data), data: this.clearResponseData(data),
}; };
}), }),
@ -34,13 +35,11 @@ export class ResponseInterceptor<T> implements NestInterceptor<T, IResponse<T>>
} }
// NOTE: If we want to set custom statusCode and message in response it will set values from data to response then remove // NOTE: If we want to set custom statusCode and message in response it will set values from data to response then remove
private clearResponseData(data: any) { private clearResponseData(data: T): T {
if (data?.statusCode) { if (data && typeof data === 'object') {
data.statusCode = undefined; const record = data as unknown as Record<string, unknown>;
} if ('statusCode' in record) record.statusCode = undefined;
if ('message' in record) record.message = undefined;
if (data?.message) {
data.message = undefined;
} }
return data; return data;

View File

@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, QueryRunner, EntityManager, Repository, DeepPartial, EntityTarget, ObjectLiteral, FindOptionsWhere, DeleteResult, UpdateResult, ObjectId } from 'typeorm'; import { DataSource, QueryRunner, EntityManager, DeepPartial, FindOptionsWhere, DeleteResult, UpdateResult, ObjectId } from 'typeorm';
import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel';
const DEFAULT_ISOLATION_LEVEL: IsolationLevel = 'READ COMMITTED'; const DEFAULT_ISOLATION_LEVEL: IsolationLevel = 'READ COMMITTED';

View File

@ -1,5 +1,5 @@
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { createTransport } from 'nodemailer'; import { createTransport, type Transporter } from 'nodemailer';
import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq'; import { Job } from 'bullmq';
import { MailQueueJobName } from './enums/mail-queue-job-name.enum'; import { MailQueueJobName } from './enums/mail-queue-job-name.enum';
@ -8,10 +8,14 @@ import * as handlebars from 'handlebars';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
type MailJobData =
| { email: string; resetUrl: string }
| { email: string; code: string };
@Processor('email') @Processor('email')
export class MailProcessor extends WorkerHost { export class MailProcessor extends WorkerHost {
private readonly _logger = new Logger(MailProcessor.name); private readonly _logger = new Logger(MailProcessor.name);
private transporter; private transporter: Transporter;
constructor(private configService: ConfigService) { constructor(private configService: ConfigService) {
super(); super();
@ -31,15 +35,15 @@ export class MailProcessor extends WorkerHost {
this._logger.debug('Mail transport initialized', transportConfig); this._logger.debug('Mail transport initialized', transportConfig);
} }
async process(job: Job): Promise<any> { async process(job: Job<MailJobData>): Promise<void> {
switch (job.name) { switch (job.name) {
case MailQueueJobName.SendPasswordResetEmail: { case MailQueueJobName.SendPasswordResetEmail: {
const { email, resetUrl } = job.data; const { email, resetUrl } = job.data as { email: string; resetUrl: string };
await this.sendPasswordResetEmail(email, resetUrl); await this.sendPasswordResetEmail(email, resetUrl);
break; break;
} }
case MailQueueJobName.SendVerificationCodeEmail: { case MailQueueJobName.SendVerificationCodeEmail: {
const { email, code } = job.data; const { email, code } = job.data as { email: string; code: string };
await this.sendVerifyUserEmail(email, code); await this.sendVerifyUserEmail(email, code);
break; break;
} }