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; \
fi
RUN pnpm add -g @nestjs/cli
RUN pnpm add -g @nestjs/cli@11
COPY . .

View File

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

View File

@ -26,18 +26,18 @@
},
"dependencies": {
"@nestjs/bullmq": "^11.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/common": "^11.1.13",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.13",
"@nestjs/jwt": "^11.0.2",
"@nestjs/mapped-types": "^2.0.5",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-express": "^11.1.13",
"@nestjs/swagger": "^11.1.4",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/typeorm": "^11.0.0",
"bcrypt": "^5.1.1",
"bullmq": "^5.49.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"class-validator": "^0.14.3",
"handlebars": "^4.7.8",
"helmet": "^7.1.0",
"nodemailer": "^6.10.1",
@ -49,13 +49,13 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@nestjs/cli": "^11.0.16",
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.13",
"@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17",
"@types/express": "^5.0.6",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/node": "^25.2.3",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
@ -89,7 +89,8 @@
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
"testEnvironment": "node",
"passWithNoTests": true
},
"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 { 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';
export type EntityFields<T> = (keyof T)[];
export interface IFiltering {
[field: string]: string;
}
export type FilteringResult = Record<string, unknown>;
interface IFilteringParams {
field: string;
@ -52,53 +63,99 @@ export const getWhere = (filter: IFilteringParams) => {
if (filter.rule == FilterRule.LIKE) return ILike(`%${filter.value}%`);
if (filter.rule == FilterRule.NOT_LIKE) return Not(ILike(`%${filter.value}%`));
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) : {};
}
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)) : {};
}
if (filter.rule == FilterRule.BETWEEN) return Between(...(filter.value.split(',') as [string, string]));
}
export const FilteringParams = createParamDecorator((data, ctx: ExecutionContext): IFiltering => {
const req: Request = ctx.switchToHttp().getRequest();
const queryFilters = req.query.filters as string[];
if (!queryFilters || !Array.isArray(queryFilters)) return null;
if (typeof data !== 'object') throw new BadRequestException(ErrorCode.InvalidFilterParams);
let filters: { [field: string]: any } = {};
for (const filter of queryFilters) {
const [fieldPath, rule, value] = filter.split(':');
const fieldParts = fieldPath.split('.');
const field = fieldParts.pop();
let nestedFilters = filters;
for (const part of fieldParts) {
nestedFilters[part] = nestedFilters[part] || {};
nestedFilters = nestedFilters[part];
}
if (!data.includes(fieldPath)) throw new BadRequestException(`${ErrorCode.FilterFieldNotAllowed}:${field}`);
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)
&& (!value || value.trim() === '')) {
continue;
}
const whereClause = getWhere({ field, rule, value });
if (whereClause && (typeof whereClause === 'object' && Object.keys(whereClause).length === 0)) {
continue;
}
nestedFilters[field] = whereClause;
if (filter.rule == FilterRule.BETWEEN) {
const [from, to] = filter.value.split(',') as [string?, string?];
if (!from || !to) return {};
return Between(from, to);
}
return filters;
});
return {};
}
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 rawFilters = req.query.filters;
const queryFilters = Array.isArray(rawFilters)
? rawFilters
: typeof rawFilters === 'string'
? [rawFilters]
: null;
if (!queryFilters) return null;
if (!Array.isArray(data)) throw new BadRequestException(ErrorCode.InvalidFilterParams);
const filters: FilteringResult = {};
for (const filter of queryFilters) {
if (typeof filter !== 'string') {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
const [fieldPath, rule, value] = filter.split(':');
if (!fieldPath || !rule || value === undefined) {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
const fieldParts = fieldPath.split('.');
const field = fieldParts.pop();
if (!field) {
throw new BadRequestException(ErrorCode.InvalidFilterParams);
}
let nestedFilters: Record<string, unknown> = filters;
for (const part of fieldParts) {
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;
}
const whereClause = getWhere({ field, rule, value });
if (ensureObjectRecord(whereClause) && Object.keys(whereClause).length === 0) {
continue;
}
nestedFilters[field] = whereClause;
}
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());
return next.handle().pipe(
map((data: any) => {
map((data: T) => {
const meta = data as unknown as { statusCode?: number; message?: string };
return {
statusCode: data?.statusCode || httpResponse.statusCode || 200,
message: data?.message || customMessage || 'success',
statusCode: meta?.statusCode || httpResponse.statusCode || 200,
message: meta?.message || customMessage || 'success',
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
private clearResponseData(data: any) {
if (data?.statusCode) {
data.statusCode = undefined;
}
if (data?.message) {
data.message = undefined;
private clearResponseData(data: T): T {
if (data && typeof data === 'object') {
const record = data as unknown as Record<string, unknown>;
if ('statusCode' in record) record.statusCode = undefined;
if ('message' in record) record.message = undefined;
}
return data;

View File

@ -1,5 +1,5 @@
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';
const DEFAULT_ISOLATION_LEVEL: IsolationLevel = 'READ COMMITTED';

View File

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