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:
Mathis HERRIOT
2026-01-14 12:11:39 +01:00
parent 9c45bf11e4
commit 514bd354bf
64 changed files with 1801 additions and 1295 deletions

View File

@@ -2,11 +2,12 @@ import { Module } from "@nestjs/common";
import { DatabaseModule } from "../database/database.module";
import { FavoritesController } from "./favorites.controller";
import { FavoritesService } from "./favorites.service";
import { FavoritesRepository } from "./repositories/favorites.repository";
@Module({
imports: [DatabaseModule],
controllers: [FavoritesController],
providers: [FavoritesService],
providers: [FavoritesService, FavoritesRepository],
exports: [FavoritesService],
})
export class FavoritesModule {}

View File

@@ -1,54 +1,31 @@
import { ConflictException, NotFoundException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { FavoritesService } from "./favorites.service";
import { FavoritesRepository } from "./repositories/favorites.repository";
describe("FavoritesService", () => {
let service: FavoritesService;
let repository: FavoritesRepository;
const mockDb = {
select: jest.fn(),
from: jest.fn(),
where: jest.fn(),
limit: jest.fn(),
offset: jest.fn(),
innerJoin: jest.fn(),
insert: jest.fn(),
values: jest.fn(),
delete: jest.fn(),
returning: jest.fn(),
const mockFavoritesRepository = {
findContentById: jest.fn(),
add: jest.fn(),
remove: jest.fn(),
findByUserId: jest.fn(),
};
beforeEach(async () => {
jest.clearAllMocks();
const chain = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
offset: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
returning: jest.fn().mockReturnThis(),
};
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: [
FavoritesService,
{ provide: DatabaseService, useValue: { db: mockDb } },
{ provide: FavoritesRepository, useValue: mockFavoritesRepository },
],
}).compile();
service = module.get<FavoritesService>(FavoritesService);
repository = module.get<FavoritesRepository>(FavoritesRepository);
});
it("should be defined", () => {
@@ -57,26 +34,27 @@ describe("FavoritesService", () => {
describe("addFavorite", () => {
it("should add a favorite", async () => {
mockDb.limit.mockResolvedValue([{ id: "content1" }]);
mockDb.returning.mockResolvedValue([
mockFavoritesRepository.findContentById.mockResolvedValue({ id: "content1" });
mockFavoritesRepository.add.mockResolvedValue([
{ userId: "u1", contentId: "content1" },
]);
const result = await service.addFavorite("u1", "content1");
expect(result).toEqual([{ userId: "u1", contentId: "content1" }]);
expect(repository.add).toHaveBeenCalledWith("u1", "content1");
});
it("should throw NotFoundException if content does not exist", async () => {
mockDb.limit.mockResolvedValue([]);
mockFavoritesRepository.findContentById.mockResolvedValue(null);
await expect(service.addFavorite("u1", "invalid")).rejects.toThrow(
NotFoundException,
);
});
it("should throw ConflictException on duplicate favorite", async () => {
mockDb.limit.mockResolvedValue([{ id: "content1" }]);
mockDb.returning.mockRejectedValue(new Error("Duplicate"));
mockFavoritesRepository.findContentById.mockResolvedValue({ id: "content1" });
mockFavoritesRepository.add.mockRejectedValue(new Error("Duplicate"));
await expect(service.addFavorite("u1", "content1")).rejects.toThrow(
ConflictException,
);
@@ -85,13 +63,14 @@ describe("FavoritesService", () => {
describe("removeFavorite", () => {
it("should remove a favorite", async () => {
mockDb.returning.mockResolvedValue([{ userId: "u1", contentId: "c1" }]);
mockFavoritesRepository.remove.mockResolvedValue([{ userId: "u1", contentId: "c1" }]);
const result = await service.removeFavorite("u1", "c1");
expect(result).toEqual({ userId: "u1", contentId: "c1" });
expect(repository.remove).toHaveBeenCalledWith("u1", "c1");
});
it("should throw NotFoundException if favorite not found", async () => {
mockDb.returning.mockResolvedValue([]);
mockFavoritesRepository.remove.mockResolvedValue([]);
await expect(service.removeFavorite("u1", "c1")).rejects.toThrow(
NotFoundException,
);

View File

@@ -4,46 +4,32 @@ import {
Logger,
NotFoundException,
} from "@nestjs/common";
import { and, eq } from "drizzle-orm";
import { DatabaseService } from "../database/database.service";
import { contents, favorites } from "../database/schemas";
import { FavoritesRepository } from "./repositories/favorites.repository";
@Injectable()
export class FavoritesService {
private readonly logger = new Logger(FavoritesService.name);
constructor(private readonly databaseService: DatabaseService) {}
constructor(private readonly favoritesRepository: FavoritesRepository) {}
async addFavorite(userId: string, contentId: string) {
this.logger.log(`Adding favorite: user ${userId}, content ${contentId}`);
// Vérifier si le contenu existe
const content = await this.databaseService.db
.select()
.from(contents)
.where(eq(contents.id, contentId))
.limit(1);
if (content.length === 0) {
const content = await this.favoritesRepository.findContentById(contentId);
if (!content) {
throw new NotFoundException("Content not found");
}
try {
return await this.databaseService.db
.insert(favorites)
.values({ userId, contentId })
.returning();
return await this.favoritesRepository.add(userId, contentId);
} catch (_error) {
// Probablement une violation de clé primaire (déjà en favori)
throw new ConflictException("Content already in favorites");
}
}
async removeFavorite(userId: string, contentId: string) {
this.logger.log(`Removing favorite: user ${userId}, content ${contentId}`);
const result = await this.databaseService.db
.delete(favorites)
.where(and(eq(favorites.userId, userId), eq(favorites.contentId, contentId)))
.returning();
const result = await this.favoritesRepository.remove(userId, contentId);
if (result.length === 0) {
throw new NotFoundException("Favorite not found");
@@ -53,16 +39,6 @@ export class FavoritesService {
}
async getUserFavorites(userId: string, limit: number, offset: number) {
const data = await this.databaseService.db
.select({
content: contents,
})
.from(favorites)
.innerJoin(contents, eq(favorites.contentId, contents.id))
.where(eq(favorites.userId, userId))
.limit(limit)
.offset(offset);
return data.map((item) => item.content);
return await this.favoritesRepository.findByUserId(userId, limit, offset);
}
}

View File

@@ -0,0 +1,46 @@
import { Injectable } from "@nestjs/common";
import { and, eq } from "drizzle-orm";
import { DatabaseService } from "../../database/database.service";
import { contents, favorites } from "../../database/schemas";
@Injectable()
export class FavoritesRepository {
constructor(private readonly databaseService: DatabaseService) {}
async findContentById(contentId: string) {
const result = await this.databaseService.db
.select()
.from(contents)
.where(eq(contents.id, contentId))
.limit(1);
return result[0] || null;
}
async add(userId: string, contentId: string) {
return await this.databaseService.db
.insert(favorites)
.values({ userId, contentId })
.returning();
}
async remove(userId: string, contentId: string) {
return await this.databaseService.db
.delete(favorites)
.where(and(eq(favorites.userId, userId), eq(favorites.contentId, contentId)))
.returning();
}
async findByUserId(userId: string, limit: number, offset: number) {
const data = await this.databaseService.db
.select({
content: contents,
})
.from(favorites)
.innerJoin(contents, eq(favorites.contentId, contents.id))
.where(eq(favorites.userId, userId))
.limit(limit)
.offset(offset);
return data.map((item) => item.content);
}
}