feat: add modular services and repositories for improved code organization
Introduce repository pattern across multiple services, including `favorites`, `tags`, `sessions`, `reports`, `auth`, and more. Decouple crypto functionalities into modular services like `HashingService`, `JwtService`, and `EncryptionService`. Improve testability and maintainability by simplifying dependencies and consolidating utility logic.
This commit is contained in:
4
backend/src/common/interfaces/mail.interface.ts
Normal file
4
backend/src/common/interfaces/mail.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IMailService {
|
||||
sendEmailValidation(email: string, token: string): Promise<void>;
|
||||
sendPasswordReset(email: string, token: string): Promise<void>;
|
||||
}
|
||||
25
backend/src/common/interfaces/media.interface.ts
Normal file
25
backend/src/common/interfaces/media.interface.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface MediaProcessingResult {
|
||||
buffer: Buffer;
|
||||
mimeType: string;
|
||||
extension: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ScanResult {
|
||||
isInfected: boolean;
|
||||
virusName?: string;
|
||||
}
|
||||
|
||||
export interface IMediaService {
|
||||
scanFile(buffer: Buffer, filename: string): Promise<ScanResult>;
|
||||
processImage(
|
||||
buffer: Buffer,
|
||||
format?: "webp" | "avif",
|
||||
): Promise<MediaProcessingResult>;
|
||||
processVideo(
|
||||
buffer: Buffer,
|
||||
format?: "webm" | "av1",
|
||||
): Promise<MediaProcessingResult>;
|
||||
}
|
||||
39
backend/src/common/interfaces/storage.interface.ts
Normal file
39
backend/src/common/interfaces/storage.interface.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Readable } from "node:stream";
|
||||
|
||||
export interface IStorageService {
|
||||
uploadFile(
|
||||
fileName: string,
|
||||
file: Buffer,
|
||||
mimeType: string,
|
||||
metaData?: Record<string, string>,
|
||||
bucketName?: string,
|
||||
): Promise<string>;
|
||||
|
||||
getFile(
|
||||
fileName: string,
|
||||
bucketName?: string,
|
||||
): Promise<Readable>;
|
||||
|
||||
getFileUrl(
|
||||
fileName: string,
|
||||
expiry?: number,
|
||||
bucketName?: string,
|
||||
): Promise<string>;
|
||||
|
||||
getUploadUrl(
|
||||
fileName: string,
|
||||
expiry?: number,
|
||||
bucketName?: string,
|
||||
): Promise<string>;
|
||||
|
||||
deleteFile(fileName: string, bucketName?: string): Promise<void>;
|
||||
|
||||
getFileInfo(fileName: string, bucketName?: string): Promise<any>;
|
||||
|
||||
moveFile(
|
||||
sourceFileName: string,
|
||||
destinationFileName: string,
|
||||
sourceBucketName?: string,
|
||||
destinationBucketName?: string,
|
||||
): Promise<string>;
|
||||
}
|
||||
@@ -1,38 +1,31 @@
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { DatabaseService } from "../../database/database.service";
|
||||
import { ContentsRepository } from "../../contents/repositories/contents.repository";
|
||||
import { ReportsRepository } from "../../reports/repositories/reports.repository";
|
||||
import { SessionsRepository } from "../../sessions/repositories/sessions.repository";
|
||||
import { UsersRepository } from "../../users/repositories/users.repository";
|
||||
import { PurgeService } from "./purge.service";
|
||||
|
||||
describe("PurgeService", () => {
|
||||
let service: PurgeService;
|
||||
|
||||
const mockDb = {
|
||||
delete: jest.fn(),
|
||||
where: jest.fn(),
|
||||
returning: jest.fn(),
|
||||
};
|
||||
const mockSessionsRepository = { purgeExpired: jest.fn().mockResolvedValue([]) };
|
||||
const mockReportsRepository = { purgeObsolete: jest.fn().mockResolvedValue([]) };
|
||||
const mockUsersRepository = { purgeDeleted: jest.fn().mockResolvedValue([]) };
|
||||
const mockContentsRepository = { purgeSoftDeleted: jest.fn().mockResolvedValue([]) };
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(Logger.prototype, "error").mockImplementation(() => {});
|
||||
jest.spyOn(Logger.prototype, "log").mockImplementation(() => {});
|
||||
|
||||
const chain = {
|
||||
delete: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
const mockImplementation = () => Object.assign(Promise.resolve([]), chain);
|
||||
for (const mock of Object.values(chain)) {
|
||||
mock.mockImplementation(mockImplementation);
|
||||
}
|
||||
Object.assign(mockDb, chain);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PurgeService,
|
||||
{ provide: DatabaseService, useValue: { db: mockDb } },
|
||||
{ provide: SessionsRepository, useValue: mockSessionsRepository },
|
||||
{ provide: ReportsRepository, useValue: mockReportsRepository },
|
||||
{ provide: UsersRepository, useValue: mockUsersRepository },
|
||||
{ provide: ContentsRepository, useValue: mockContentsRepository },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -44,23 +37,22 @@ describe("PurgeService", () => {
|
||||
});
|
||||
|
||||
describe("purgeExpiredData", () => {
|
||||
it("should purge data", async () => {
|
||||
mockDb.returning
|
||||
.mockResolvedValueOnce([{ id: "s1" }]) // sessions
|
||||
.mockResolvedValueOnce([{ id: "r1" }]) // reports
|
||||
.mockResolvedValueOnce([{ id: "u1" }]) // users
|
||||
.mockResolvedValueOnce([{ id: "c1" }]); // contents
|
||||
it("should purge data using repositories", async () => {
|
||||
mockSessionsRepository.purgeExpired.mockResolvedValue([{ id: "s1" }]);
|
||||
mockReportsRepository.purgeObsolete.mockResolvedValue([{ id: "r1" }]);
|
||||
mockUsersRepository.purgeDeleted.mockResolvedValue([{ id: "u1" }]);
|
||||
mockContentsRepository.purgeSoftDeleted.mockResolvedValue([{ id: "c1" }]);
|
||||
|
||||
await service.purgeExpiredData();
|
||||
|
||||
expect(mockDb.delete).toHaveBeenCalledTimes(4);
|
||||
expect(mockDb.returning).toHaveBeenCalledTimes(4);
|
||||
expect(mockSessionsRepository.purgeExpired).toHaveBeenCalled();
|
||||
expect(mockReportsRepository.purgeObsolete).toHaveBeenCalled();
|
||||
expect(mockUsersRepository.purgeDeleted).toHaveBeenCalled();
|
||||
expect(mockContentsRepository.purgeSoftDeleted).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle errors", async () => {
|
||||
mockDb.delete.mockImplementation(() => {
|
||||
throw new Error("Db error");
|
||||
});
|
||||
mockSessionsRepository.purgeExpired.mockRejectedValue(new Error("Db error"));
|
||||
await expect(service.purgeExpiredData()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Cron, CronExpression } from "@nestjs/schedule";
|
||||
import { and, eq, isNotNull, lte } from "drizzle-orm";
|
||||
import { DatabaseService } from "../../database/database.service";
|
||||
import { contents, reports, sessions, users } from "../../database/schemas";
|
||||
import { ContentsRepository } from "../../contents/repositories/contents.repository";
|
||||
import { ReportsRepository } from "../../reports/repositories/reports.repository";
|
||||
import { SessionsRepository } from "../../sessions/repositories/sessions.repository";
|
||||
import { UsersRepository } from "../../users/repositories/users.repository";
|
||||
|
||||
@Injectable()
|
||||
export class PurgeService {
|
||||
private readonly logger = new Logger(PurgeService.name);
|
||||
|
||||
constructor(private readonly databaseService: DatabaseService) {}
|
||||
constructor(
|
||||
private readonly sessionsRepository: SessionsRepository,
|
||||
private readonly reportsRepository: ReportsRepository,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly contentsRepository: ContentsRepository,
|
||||
) {}
|
||||
|
||||
// Toutes les nuits à minuit
|
||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||
@@ -19,40 +25,26 @@ export class PurgeService {
|
||||
const now = new Date();
|
||||
|
||||
// 1. Purge des sessions expirées
|
||||
const deletedSessions = await this.databaseService.db
|
||||
.delete(sessions)
|
||||
.where(lte(sessions.expiresAt, now))
|
||||
.returning();
|
||||
const deletedSessions = await this.sessionsRepository.purgeExpired(now);
|
||||
this.logger.log(`Purged ${deletedSessions.length} expired sessions.`);
|
||||
|
||||
// 2. Purge des signalements obsolètes
|
||||
const deletedReports = await this.databaseService.db
|
||||
.delete(reports)
|
||||
.where(lte(reports.expiresAt, now))
|
||||
.returning();
|
||||
const deletedReports = await this.reportsRepository.purgeObsolete(now);
|
||||
this.logger.log(`Purged ${deletedReports.length} obsolete reports.`);
|
||||
|
||||
// 3. Purge des utilisateurs supprimés (Soft Delete > 30 jours)
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const deletedUsers = await this.databaseService.db
|
||||
.delete(users)
|
||||
.where(
|
||||
and(eq(users.status, "deleted"), lte(users.deletedAt, thirtyDaysAgo)),
|
||||
)
|
||||
.returning();
|
||||
const deletedUsers = await this.usersRepository.purgeDeleted(thirtyDaysAgo);
|
||||
this.logger.log(
|
||||
`Purged ${deletedUsers.length} users marked for deletion more than 30 days ago.`,
|
||||
);
|
||||
|
||||
// 4. Purge des contenus supprimés (Soft Delete > 30 jours)
|
||||
const deletedContents = await this.databaseService.db
|
||||
.delete(contents)
|
||||
.where(
|
||||
and(isNotNull(contents.deletedAt), lte(contents.deletedAt, thirtyDaysAgo)),
|
||||
)
|
||||
.returning();
|
||||
const deletedContents = await this.contentsRepository.purgeSoftDeleted(
|
||||
thirtyDaysAgo,
|
||||
);
|
||||
this.logger.log(
|
||||
`Purged ${deletedContents.length} contents marked for deletion more than 30 days ago.`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user