chore: first commit
This commit is contained in:
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user