chore: first commit

This commit is contained in:
mahyargdz
2025-06-24 11:26:06 +03:30
commit ccbf743ecb
186 changed files with 22258 additions and 0 deletions
@@ -0,0 +1,42 @@
import { BadRequestException, CallHandler, ExecutionContext, ForbiddenException, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { FastifyRequest } from "fastify";
import { Observable } from "rxjs";
import { BusinessMessage } from "../../common/enums/message.enum";
import { Business } from "../../modules/businesses/entities/business.entity";
import { BusinessesService } from "../../modules/businesses/services/businesses.service";
declare module "fastify" {
interface FastifyRequest {
business?: Business;
}
}
@Injectable()
export class BusinessInterceptor implements NestInterceptor {
private readonly headerName = "x-business-id";
private readonly logger = new Logger(BusinessInterceptor.name);
constructor(private readonly businessesService: BusinessesService) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const businessId = request.headers[this.headerName.toLocaleLowerCase()] as string;
if (businessId) {
try {
const business = await this.businessesService.getBusinessByDanakSubscriptionId(businessId);
request.business = business;
} catch (error) {
this.logger.error(error);
throw new BadRequestException(BusinessMessage.NOT_FOUND);
}
} else {
throw new ForbiddenException(BusinessMessage.BUSINESS_ID_REQUIRED);
}
return next.handle();
}
}
@@ -0,0 +1,79 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { Observable } from "rxjs";
import { catchError, tap } from "rxjs/operators";
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const { method, url, body, query, params, headers } = request;
const startTime = Date.now();
const requestId = this.generateRequestId();
// Log request
this.logger.log({
message: "Incoming request",
requestId,
method,
url,
query,
params,
userAgent: headers["user-agent"],
ip: request.ip,
// Don't log sensitive data
body: this.sanitizeRequestBody(body),
});
return next.handle().pipe(
tap((data) => {
const duration = Date.now() - startTime;
this.logger.log({
message: "Request completed",
requestId,
method,
url,
statusCode: response.statusCode,
duration: `${duration}ms`,
responseSize: JSON.stringify(data).length,
});
}),
catchError((error) => {
const duration = Date.now() - startTime;
this.logger.error({
message: "Request failed",
requestId,
method,
url,
statusCode: response.statusCode,
duration: `${duration}ms`,
error: error.message,
stack: error.stack,
});
throw error;
}),
);
}
private generateRequestId(): string {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private sanitizeRequestBody(body: any): any {
if (!body || typeof body !== "object") return body;
const sanitized = { ...body };
const sensitiveFields = ["password", "token", "authorization", "secret"];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = "***REDACTED***";
}
}
return sanitized;
}
}
@@ -0,0 +1,84 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
interface RequestMetrics {
method: string;
url: string;
statusCode: number;
responseTime: number;
timestamp: string;
userAgent?: string;
ip?: string;
}
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
private readonly logger = new Logger(MetricsInterceptor.name);
private static metrics: Map<string, any> = new Map();
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const startTime = Date.now();
return next.handle().pipe(
tap(() => {
const responseTime = Date.now() - startTime;
const metrics: RequestMetrics = {
method: request.method,
url: request.url,
statusCode: response.statusCode,
responseTime,
timestamp: new Date().toISOString(),
userAgent: request.headers["user-agent"],
ip: request.ip,
};
this.collectMetrics(metrics);
// Log slow requests
if (responseTime > 1000) {
this.logger.warn(`Slow request detected: ${request.method} ${request.url} - ${responseTime}ms`);
}
}),
);
}
private collectMetrics(metrics: RequestMetrics): void {
const key = `${metrics.method}:${metrics.url}`;
if (!MetricsInterceptor.metrics.has(key)) {
MetricsInterceptor.metrics.set(key, {
count: 0,
totalResponseTime: 0,
avgResponseTime: 0,
maxResponseTime: 0,
minResponseTime: Infinity,
errorCount: 0,
});
}
const existing = MetricsInterceptor.metrics.get(key);
existing.count++;
existing.totalResponseTime += metrics.responseTime;
existing.avgResponseTime = existing.totalResponseTime / existing.count;
existing.maxResponseTime = Math.max(existing.maxResponseTime, metrics.responseTime);
existing.minResponseTime = Math.min(existing.minResponseTime, metrics.responseTime);
if (metrics.statusCode >= 400) {
existing.errorCount++;
}
MetricsInterceptor.metrics.set(key, existing);
}
static getMetrics(): Map<string, any> {
return MetricsInterceptor.metrics;
}
static resetMetrics(): void {
MetricsInterceptor.metrics.clear();
}
}
+97
View File
@@ -0,0 +1,97 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from "@nestjs/common";
import { FastifyRequest } from "fastify";
import { Observable, map } from "rxjs";
import { IPageFormat, PaginatedResponse, PaginationQuery } from "../../common/interfaces/IPagination";
@Injectable()
export class PaginationInterceptor implements NestInterceptor {
private readonly logger = new Logger(PaginationInterceptor.name);
private readonly DEFAULT_PAGE = 1;
private readonly DEFAULT_LIMIT = 10;
private readonly MAX_LIMIT = 100;
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest<FastifyRequest>();
const { page, limit } = this.extractPaginationParams(request.query as PaginationQuery);
const { className, handlerName } = this.getContextInfo(context);
return next.handle().pipe(
map((data: PaginatedResponse) => {
if (!this.isPaginatedResponse(data)) {
return data;
}
this.logger.log(`Paginating response from ${className}.${handlerName}`);
const { count, paginate, ...response } = data;
const pager = this.formatPage(page, limit, count, request);
return {
pager,
...response,
};
}),
);
}
//******* Extract Pagination Params *******//
private extractPaginationParams(query: PaginationQuery): { page: number; limit: number } {
const page = this.validateNumber(query.page, this.DEFAULT_PAGE);
const limit = this.validateNumber(query.limit, this.DEFAULT_LIMIT);
return {
page: Math.max(1, page),
limit: Math.min(Math.max(1, limit), this.MAX_LIMIT),
};
}
//******* Validate Number *******//
private validateNumber(value: string | undefined, defaultValue: number): number {
const parsed = parseInt(value || "", 10);
return isNaN(parsed) ? defaultValue : parsed;
}
//******* Get Context Info *******//
private getContextInfo(context: ExecutionContext): { className: string; handlerName: string } {
return {
className: context.getClass().name,
handlerName: context.getHandler().name,
};
}
//******* Check if Response is Paginated *******//
private isPaginatedResponse(data: PaginatedResponse): data is PaginatedResponse {
return data && (data.paginate || typeof data.count === "number");
}
//******* Format Page *******//
private formatPage(page: number, limit: number, totalItems: number | undefined, request: FastifyRequest): IPageFormat {
const count = totalItems || 0;
const totalPages = Math.ceil(count / limit);
const prevPage = page > 1 ? page - 1 : false;
const nextPage = page < totalPages ? page + 1 : false;
return {
page,
limit,
totalItems: count,
totalPages,
prevPage: this.generatePageLink(prevPage, limit, request),
nextPage: this.generatePageLink(nextPage, limit, request),
};
}
//******* Generate Page Link *******//
private generatePageLink(page: number | boolean, limit: number, request: FastifyRequest): string | boolean {
if (!page) return false;
const { protocol, hostname, url } = request;
const baseUrl = url.split("?")[0];
const queryParams = new URLSearchParams({
page: page.toString(),
limit: limit.toString(),
});
return `${protocol}://${hostname}${baseUrl}?${queryParams.toString()}`;
}
}
+30
View File
@@ -0,0 +1,30 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { FastifyReply } from "fastify";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
//
intercept(context: ExecutionContext, next: CallHandler<Record<string, unknown>>): Observable<unknown> {
const ctx = context.switchToHttp().getResponse<FastifyReply>();
const statusCode = ctx.statusCode;
return next.handle().pipe(
map((data) => {
if (data && data.data !== undefined) {
return {
statusCode,
success: true,
data: data.data,
};
}
return {
statusCode,
success: true,
data,
};
}),
);
}
}