This commit is contained in:
2026-04-16 17:21:48 +03:00
parent c8fa4c442d
commit c8e7e4e927
116 changed files with 3720 additions and 4197 deletions
+29 -29
View File
@@ -8,20 +8,20 @@ import {
Body,
HttpCode,
ParseUUIDPipe,
} from '@nestjs/common';
} from "@nestjs/common";
import {
ApiOperation,
ApiOkResponse,
ApiNotFoundResponse,
ApiBadRequestResponse,
} from '@nestjs/swagger';
import { BaseService } from './base.service';
import { PaginationDto } from '../dto/pagination.dto';
} from "@nestjs/swagger";
import { BaseService } from "./base.service";
import { PaginationDto } from "../dto/pagination.dto";
import {
ApiResponse,
createSuccessResponse,
createPaginatedResponse,
} from '../types/api-response.type';
} from "../types/api-response.type";
/**
* Generic base controller with common CRUD endpoints
@@ -37,8 +37,8 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
@Get()
@HttpCode(200)
@ApiOperation({ summary: 'Get all records with pagination' })
@ApiOkResponse({ description: 'Records retrieved successfully' })
@ApiOperation({ summary: "Get all records with pagination" })
@ApiOkResponse({ description: "Records retrieved successfully" })
async findAll(
@Query() pagination: PaginationDto,
): Promise<ApiResponse<{ items: T[]; meta: any }>> {
@@ -52,13 +52,13 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
);
}
@Get(':id')
@Get(":id")
@HttpCode(200)
@ApiOperation({ summary: 'Get a record by ID' })
@ApiOkResponse({ description: 'Record retrieved successfully' })
@ApiNotFoundResponse({ description: 'Record not found' })
@ApiOperation({ summary: "Get a record by ID" })
@ApiOkResponse({ description: "Record retrieved successfully" })
@ApiNotFoundResponse({ description: "Record not found" })
async findOne(
@Param('id', ParseUUIDPipe) id: string,
@Param("id", ParseUUIDPipe) id: string,
): Promise<ApiResponse<T>> {
const result = await this.service.findOne(id);
return createSuccessResponse(
@@ -69,9 +69,9 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
@Post()
@HttpCode(200)
@ApiOperation({ summary: 'Create a new record' })
@ApiOkResponse({ description: 'Record created successfully' })
@ApiBadRequestResponse({ description: 'Validation failed' })
@ApiOperation({ summary: "Create a new record" })
@ApiOkResponse({ description: "Record created successfully" })
@ApiBadRequestResponse({ description: "Validation failed" })
async create(@Body() createDto: CreateDto): Promise<ApiResponse<T>> {
const result = await this.service.create(createDto);
return createSuccessResponse(
@@ -81,13 +81,13 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
);
}
@Put(':id')
@Put(":id")
@HttpCode(200)
@ApiOperation({ summary: 'Update an existing record' })
@ApiOkResponse({ description: 'Record updated successfully' })
@ApiNotFoundResponse({ description: 'Record not found' })
@ApiOperation({ summary: "Update an existing record" })
@ApiOkResponse({ description: "Record updated successfully" })
@ApiNotFoundResponse({ description: "Record not found" })
async update(
@Param('id', ParseUUIDPipe) id: string,
@Param("id", ParseUUIDPipe) id: string,
@Body() updateDto: UpdateDto,
): Promise<ApiResponse<T>> {
const result = await this.service.update(id, updateDto);
@@ -97,13 +97,13 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
);
}
@Delete(':id')
@Delete(":id")
@HttpCode(200)
@ApiOperation({ summary: 'Delete a record (soft delete)' })
@ApiOkResponse({ description: 'Record deleted successfully' })
@ApiNotFoundResponse({ description: 'Record not found' })
@ApiOperation({ summary: "Delete a record (soft delete)" })
@ApiOkResponse({ description: "Record deleted successfully" })
@ApiNotFoundResponse({ description: "Record not found" })
async delete(
@Param('id', ParseUUIDPipe) id: string,
@Param("id", ParseUUIDPipe) id: string,
): Promise<ApiResponse<T>> {
const result = await this.service.delete(id);
return createSuccessResponse(
@@ -112,12 +112,12 @@ export abstract class BaseController<T, CreateDto, UpdateDto> {
);
}
@Post(':id/restore')
@Post(":id/restore")
@HttpCode(200)
@ApiOperation({ summary: 'Restore a soft-deleted record' })
@ApiOkResponse({ description: 'Record restored successfully' })
@ApiOperation({ summary: "Restore a soft-deleted record" })
@ApiOkResponse({ description: "Record restored successfully" })
async restore(
@Param('id', ParseUUIDPipe) id: string,
@Param("id", ParseUUIDPipe) id: string,
): Promise<ApiResponse<T>> {
const result = await this.service.restore(id);
return createSuccessResponse(
+4 -4
View File
@@ -1,7 +1,7 @@
import { NotFoundException, Logger } from '@nestjs/common';
import { PrismaService } from '../../database/prisma.service';
import { PaginationDto } from '../dto/pagination.dto';
import { PaginationMeta } from '../types/api-response.type';
import { NotFoundException, Logger } from "@nestjs/common";
import { PrismaService } from "../../database/prisma.service";
import { PaginationDto } from "../dto/pagination.dto";
import { PaginationMeta } from "../types/api-response.type";
/**
* Generic base service with common CRUD operations
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './base.service';
export * from './base.controller';
export * from "./base.service";
export * from "./base.controller";
+5 -5
View File
@@ -2,7 +2,7 @@ import {
createParamDecorator,
ExecutionContext,
SetMetadata,
} from '@nestjs/common';
} from "@nestjs/common";
/**
* Get the current authenticated user from request
@@ -23,19 +23,19 @@ export const CurrentUser = createParamDecorator(
/**
* Mark a route as public (no authentication required)
*/
export const IS_PUBLIC_KEY = 'isPublic';
export const IS_PUBLIC_KEY = "isPublic";
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
/**
* Require specific roles to access a route
*/
export const ROLES_KEY = 'roles';
export const ROLES_KEY = "roles";
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
/**
* Require specific permissions to access a route
*/
export const PERMISSIONS_KEY = 'permissions';
export const PERMISSIONS_KEY = "permissions";
export const RequirePermissions = (...permissions: string[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
@@ -55,6 +55,6 @@ export const CurrentTenant = createParamDecorator(
export const CurrentLang = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.headers['accept-language'] || 'en';
return request.headers["accept-language"] || "en";
},
);
+15 -15
View File
@@ -1,9 +1,9 @@
import { IsOptional, IsInt, Min, Max, IsString, IsIn } from 'class-validator';
import { Transform } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsInt, Min, Max, IsString, IsIn } from "class-validator";
import { Transform } from "class-transformer";
import { ApiPropertyOptional } from "@nestjs/swagger";
export class PaginationDto {
@ApiPropertyOptional({ default: 1, minimum: 1, description: 'Page number' })
@ApiPropertyOptional({ default: 1, minimum: 1, description: "Page number" })
@IsOptional()
@Transform(({ value }) => parseInt(value, 10))
@IsInt()
@@ -14,7 +14,7 @@ export class PaginationDto {
default: 10,
minimum: 1,
maximum: 100,
description: 'Items per page',
description: "Items per page",
})
@IsOptional()
@Transform(({ value }) => parseInt(value, 10))
@@ -23,21 +23,21 @@ export class PaginationDto {
@Max(100)
limit?: number = 10;
@ApiPropertyOptional({ description: 'Field to sort by' })
@ApiPropertyOptional({ description: "Field to sort by" })
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
sortBy?: string = "createdAt";
@ApiPropertyOptional({
enum: ['asc', 'desc'],
default: 'desc',
description: 'Sort order',
enum: ["asc", "desc"],
default: "desc",
description: "Sort order",
})
@IsOptional()
@IsIn(['asc', 'desc'])
sortOrder?: 'asc' | 'desc' = 'desc';
@IsIn(["asc", "desc"])
sortOrder?: "asc" | "desc" = "desc";
@ApiPropertyOptional({ description: 'Search query' })
@ApiPropertyOptional({ description: "Search query" })
@IsOptional()
@IsString()
search?: string;
@@ -59,7 +59,7 @@ export class PaginationDto {
/**
* Get orderBy object for Prisma
*/
get orderBy(): Record<string, 'asc' | 'desc'> {
return { [this.sortBy || 'createdAt']: this.sortOrder || 'desc' };
get orderBy(): Record<string, "asc" | "desc"> {
return { [this.sortBy || "createdAt"]: this.sortOrder || "desc" };
}
}
+15 -15
View File
@@ -5,10 +5,10 @@ import {
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { I18nService, I18nContext } from 'nestjs-i18n';
import { ApiResponse, createErrorResponse } from '../types/api-response.type';
} from "@nestjs/common";
import { Request, Response } from "express";
import { I18nService, I18nContext } from "nestjs-i18n";
import { ApiResponse, createErrorResponse } from "../types/api-response.type";
/**
* Global exception filter that catches all exceptions
@@ -27,23 +27,23 @@ export class GlobalExceptionFilter implements ExceptionFilter {
// Determine status and message
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let message = "Internal server error";
let errors: string[] = [];
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
if (typeof exceptionResponse === "string") {
message = exceptionResponse;
} else if (typeof exceptionResponse === 'object') {
} else if (typeof exceptionResponse === "object") {
const responseObj = exceptionResponse as Record<string, unknown>;
message = (responseObj.message as string) || exception.message;
// Handle validation errors (class-validator)
if (Array.isArray(responseObj.message)) {
errors = responseObj.message as string[];
message = 'VALIDATION_FAILED';
message = "VALIDATION_FAILED";
}
}
} else if (exception instanceof Error) {
@@ -57,22 +57,22 @@ export class GlobalExceptionFilter implements ExceptionFilter {
let lang = i18nContext?.lang;
if (!lang) {
const acceptLanguage = request.headers['accept-language'];
const xLang = request.headers['x-lang'];
const acceptLanguage = request.headers["accept-language"];
const xLang = request.headers["x-lang"];
if (xLang) {
lang = Array.isArray(xLang) ? xLang[0] : xLang;
} else if (acceptLanguage) {
// Take first preferred language: "tr-TR,en;q=0.9" -> "tr"
lang = acceptLanguage.split(',')[0].split(';')[0].split('-')[0];
lang = acceptLanguage.split(",")[0].split(";")[0].split("-")[0];
}
}
lang = lang || 'en';
lang = lang || "en";
// Translate validation error specially
if (message === 'VALIDATION_FAILED') {
message = this.i18n.translate('errors.VALIDATION_FAILED', { lang });
if (message === "VALIDATION_FAILED") {
message = this.i18n.translate("errors.VALIDATION_FAILED", { lang });
} else {
// Try dynamic translation
const translatedMessage = this.i18n.translate(`errors.${message}`, {
@@ -95,7 +95,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
);
// Build response
const isDevelopment = process.env.NODE_ENV === 'development';
const isDevelopment = process.env.NODE_ENV === "development";
const errorResponse: ApiResponse<null> = createErrorResponse(
message,
status,
+26 -26
View File
@@ -3,16 +3,16 @@ import {
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiResponse, createSuccessResponse } from '../types/api-response.type';
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { ApiResponse, createSuccessResponse } from "../types/api-response.type";
/**
* Response interceptor that wraps all successful responses
* in the standard ApiResponse format
*/
import { I18nService, I18nContext } from 'nestjs-i18n';
import { I18nService, I18nContext } from "nestjs-i18n";
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<
@@ -34,17 +34,17 @@ export class ResponseInterceptor<T> implements NestInterceptor<
let lang = i18nContext?.lang;
if (!lang) {
const acceptLanguage = request.headers['accept-language'];
const xLang = request.headers['x-lang'];
const acceptLanguage = request.headers["accept-language"];
const xLang = request.headers["x-lang"];
if (xLang) {
lang = Array.isArray(xLang) ? xLang[0] : xLang;
} else if (acceptLanguage) {
lang = acceptLanguage.split(',')[0].split(';')[0].split('-')[0];
lang = acceptLanguage.split(",")[0].split(";")[0].split("-")[0];
}
}
lang = lang || 'en';
lang = lang || "en";
// If data is already an ApiResponse, we should still translate its 'data' property
// But first let's just do it directly on 'data' below before returning
@@ -68,7 +68,7 @@ export class ResponseInterceptor<T> implements NestInterceptor<
}
}
const message = this.i18n.translate('common.success', {
const message = this.i18n.translate("common.success", {
lang,
});
@@ -79,7 +79,7 @@ export class ResponseInterceptor<T> implements NestInterceptor<
}
private translateReasons(data: any, lang: string) {
if (!data || typeof data !== 'object') {
if (!data || typeof data !== "object") {
return;
}
@@ -91,44 +91,44 @@ export class ResponseInterceptor<T> implements NestInterceptor<
Object.keys(data).forEach((key) => {
const val = data[key];
if (
(key === 'reasons' ||
key === 'decision_reasons' ||
key === 'reasoning_factors') &&
(key === "reasons" ||
key === "decision_reasons" ||
key === "reasoning_factors") &&
Array.isArray(val)
) {
data[key] = val.map((r: any) => {
if (typeof r !== 'string') return r;
if (typeof r !== "string") return r;
const translationKey = `predictions.reasons.${r}`;
const translated = this.i18n.translate(translationKey, {
lang,
});
return translated === translationKey ? r : translated;
});
} else if (key === 'reason' && typeof val === 'string') {
} else if (key === "reason" && typeof val === "string") {
const translationKey = `predictions.reasons.${val}`;
const translated = this.i18n.translate(translationKey, {
lang,
});
data[key] = translated === translationKey ? val : translated;
} else if (key === 'flags' && Array.isArray(val)) {
} else if (key === "flags" && Array.isArray(val)) {
data[key] = val.map((r: any) => {
if (typeof r !== 'string') return r;
if (typeof r !== "string") return r;
const translationKey = `predictions.flags.${r}`;
const translated = this.i18n.translate(translationKey, {
lang,
});
return translated === translationKey ? r : translated;
});
} else if (key === 'warnings' && Array.isArray(val)) {
} else if (key === "warnings" && Array.isArray(val)) {
data[key] = val.map((r: any) => {
if (typeof r !== 'string') return r;
if (typeof r !== "string") return r;
const translationKey = `predictions.warnings.${r}`;
const translated = this.i18n.translate(translationKey, {
lang,
});
return translated === translationKey ? r : translated;
});
} else if (typeof val === 'object' && val !== null) {
} else if (typeof val === "object" && val !== null) {
this.translateReasons(val, lang);
}
});
@@ -137,11 +137,11 @@ export class ResponseInterceptor<T> implements NestInterceptor<
private isApiResponse(data: unknown): boolean {
return (
data !== null &&
typeof data === 'object' &&
'success' in data &&
'status' in data &&
'message' in data &&
'data' in data
typeof data === "object" &&
"success" in data &&
"status" in data &&
"message" in data &&
"data" in data
);
}
}
@@ -3,8 +3,8 @@ import {
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
} from "@nestjs/common";
import { Observable } from "rxjs";
/**
* Strips HTML/script tags from all string values in the request body.
@@ -15,7 +15,7 @@ export class SanitizeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request = context.switchToHttp().getRequest();
if (request.body && typeof request.body === 'object') {
if (request.body && typeof request.body === "object") {
request.body = this.sanitize(request.body);
}
@@ -23,7 +23,7 @@ export class SanitizeInterceptor implements NestInterceptor {
}
private sanitize(value: unknown): unknown {
if (typeof value === 'string') {
if (typeof value === "string") {
return this.stripTags(value);
}
@@ -31,7 +31,7 @@ export class SanitizeInterceptor implements NestInterceptor {
return value.map((item) => this.sanitize(item));
}
if (value !== null && typeof value === 'object') {
if (value !== null && typeof value === "object") {
const sanitized: Record<string, unknown> = {};
for (const [key, val] of Object.entries(value)) {
sanitized[key] = this.sanitize(val);
@@ -43,6 +43,6 @@ export class SanitizeInterceptor implements NestInterceptor {
}
private stripTags(input: string): string {
return input.replace(/<[^>]*>/g, '');
return input.replace(/<[^>]*>/g, "");
}
}
+7 -7
View File
@@ -1,6 +1,6 @@
import { Module, Global } from '@nestjs/common';
import { BullModule } from '@nestjs/bullmq';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Module, Global } from "@nestjs/common";
import { BullModule } from "@nestjs/bullmq";
import { ConfigModule, ConfigService } from "@nestjs/config";
@Global()
@Module({
@@ -9,14 +9,14 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
connection: {
host: configService.get('redis.host', 'localhost'),
port: configService.get('redis.port', 6379),
password: configService.get('redis.password'),
host: configService.get("redis.host", "localhost"),
port: configService.get("redis.port", 6379),
password: configService.get("redis.password"),
},
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
type: "exponential",
delay: 1000,
},
removeOnComplete: true,
+2 -2
View File
@@ -33,7 +33,7 @@ export interface PaginationMeta {
*/
export function createSuccessResponse<T>(
data: T,
message = 'Success',
message = "Success",
status = 200,
): ApiResponse<T> {
return {
@@ -72,7 +72,7 @@ export function createPaginatedResponse<T>(
total: number,
page: number,
limit: number,
message = 'Success',
message = "Success",
): ApiResponse<PaginatedData<T>> {
const totalPages = Math.ceil(total / limit);
+9 -9
View File
@@ -1,10 +1,10 @@
import { existsSync, createWriteStream, mkdirSync } from 'fs';
import { dirname } from 'path';
import axios from 'axios';
import { Logger } from '@nestjs/common';
import { existsSync, createWriteStream, mkdirSync } from "fs";
import { dirname } from "path";
import axios from "axios";
import { Logger } from "@nestjs/common";
export class ImageUtils {
private static readonly logger = new Logger('ImageUtils');
private static readonly logger = new Logger("ImageUtils");
/**
* Downloads an image from a URL and saves it to a local path.
@@ -26,8 +26,8 @@ export class ImageUtils {
// Download
const response = await axios({
url,
method: 'GET',
responseType: 'stream',
method: "GET",
responseType: "stream",
timeout: 5000,
validateStatus: (status) => status === 200, // Only save if 200 OK
});
@@ -37,8 +37,8 @@ export class ImageUtils {
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', () => resolve(true));
writer.on('error', (err) => {
writer.on("finish", () => resolve(true));
writer.on("error", (err) => {
this.logger.warn(
`Failed to write image to ${localPath}: ${err.message}`,
);