Update dependencies & fix linter errors
This commit is contained in:
parent
9fc2e48bc4
commit
f06a4b8926
@ -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 . .
|
||||
|
||||
@ -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
|
||||
|
||||
27
package.json
27
package.json
@ -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"
|
||||
}
|
||||
|
||||
2025
pnpm-lock.yaml
2025
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user