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 { CategoriesController } from "./categories.controller";
import { CategoriesService } from "./categories.service";
import { CategoriesRepository } from "./repositories/categories.repository";
@Module({
imports: [DatabaseModule],
controllers: [CategoriesController],
providers: [CategoriesService],
providers: [CategoriesService, CategoriesRepository],
exports: [CategoriesService],
})
export class CategoriesModule {}

View File

@@ -1,25 +1,24 @@
import { Test, TestingModule } from "@nestjs/testing";
import { DatabaseService } from "../database/database.service";
import { categories } from "../database/schemas";
import { CACHE_MANAGER } from "@nestjs/cache-manager";
import { CategoriesService } from "./categories.service";
import { CategoriesRepository } from "./repositories/categories.repository";
import { CreateCategoryDto } from "./dto/create-category.dto";
import { UpdateCategoryDto } from "./dto/update-category.dto";
describe("CategoriesService", () => {
let service: CategoriesService;
let repository: CategoriesRepository;
const mockDb = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue([]),
orderBy: jest.fn().mockResolvedValue([]),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
returning: jest.fn().mockResolvedValue([]),
const mockCategoriesRepository = {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
const mockCacheManager = {
del: jest.fn(),
};
beforeEach(async () => {
@@ -28,15 +27,15 @@ describe("CategoriesService", () => {
providers: [
CategoriesService,
{
provide: DatabaseService,
useValue: {
db: mockDb,
},
provide: CategoriesRepository,
useValue: mockCategoriesRepository,
},
{ provide: CACHE_MANAGER, useValue: mockCacheManager },
],
}).compile();
service = module.get<CategoriesService>(CategoriesService);
repository = module.get<CategoriesRepository>(CategoriesRepository);
});
it("should be defined", () => {
@@ -46,28 +45,28 @@ describe("CategoriesService", () => {
describe("findAll", () => {
it("should return all categories ordered by name", async () => {
const mockCategories = [{ name: "A" }, { name: "B" }];
mockDb.orderBy.mockResolvedValue(mockCategories);
mockCategoriesRepository.findAll.mockResolvedValue(mockCategories);
const result = await service.findAll();
expect(result).toEqual(mockCategories);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalledWith(categories);
expect(repository.findAll).toHaveBeenCalled();
});
});
describe("findOne", () => {
it("should return a category by id", async () => {
const mockCategory = { id: "1", name: "Cat" };
mockDb.limit.mockResolvedValue([mockCategory]);
mockCategoriesRepository.findOne.mockResolvedValue(mockCategory);
const result = await service.findOne("1");
expect(result).toEqual(mockCategory);
expect(repository.findOne).toHaveBeenCalledWith("1");
});
it("should return null if category not found", async () => {
mockDb.limit.mockResolvedValue([]);
mockCategoriesRepository.findOne.mockResolvedValue(null);
const result = await service.findOne("999");
expect(result).toBeNull();
});
@@ -76,12 +75,11 @@ describe("CategoriesService", () => {
describe("create", () => {
it("should create a category and generate slug", async () => {
const dto: CreateCategoryDto = { name: "Test Category" };
mockDb.returning.mockResolvedValue([{ ...dto, slug: "test-category" }]);
mockCategoriesRepository.create.mockResolvedValue([{ ...dto, slug: "test-category" }]);
const result = await service.create(dto);
expect(mockDb.insert).toHaveBeenCalledWith(categories);
expect(mockDb.values).toHaveBeenCalledWith({
expect(repository.create).toHaveBeenCalledWith({
name: "Test Category",
slug: "test-category",
});
@@ -93,12 +91,12 @@ describe("CategoriesService", () => {
it("should update a category and regenerate slug", async () => {
const id = "1";
const dto: UpdateCategoryDto = { name: "New Name" };
mockDb.returning.mockResolvedValue([{ id, ...dto, slug: "new-name" }]);
mockCategoriesRepository.update.mockResolvedValue([{ id, ...dto, slug: "new-name" }]);
const result = await service.update(id, dto);
expect(mockDb.update).toHaveBeenCalledWith(categories);
expect(mockDb.set).toHaveBeenCalledWith(
expect(repository.update).toHaveBeenCalledWith(
id,
expect.objectContaining({
name: "New Name",
slug: "new-name",
@@ -111,11 +109,11 @@ describe("CategoriesService", () => {
describe("remove", () => {
it("should remove a category", async () => {
const id = "1";
mockDb.returning.mockResolvedValue([{ id }]);
mockCategoriesRepository.remove.mockResolvedValue([{ id }]);
const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalledWith(categories);
expect(repository.remove).toHaveBeenCalledWith(id);
expect(result).toEqual([{ id }]);
});
});

View File

@@ -1,9 +1,7 @@
import { Injectable, Logger, Inject } from "@nestjs/common";
import { CACHE_MANAGER } from "@nestjs/cache-manager";
import { Cache } from "cache-manager";
import { eq } from "drizzle-orm";
import { DatabaseService } from "../database/database.service";
import { categories } from "../database/schemas";
import { CategoriesRepository } from "./repositories/categories.repository";
import { CreateCategoryDto } from "./dto/create-category.dto";
import { UpdateCategoryDto } from "./dto/update-category.dto";
@@ -12,7 +10,7 @@ export class CategoriesService {
private readonly logger = new Logger(CategoriesService.name);
constructor(
private readonly databaseService: DatabaseService,
private readonly categoriesRepository: CategoriesRepository,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
@@ -22,20 +20,11 @@ export class CategoriesService {
}
async findAll() {
return await this.databaseService.db
.select()
.from(categories)
.orderBy(categories.name);
return await this.categoriesRepository.findAll();
}
async findOne(id: string) {
const result = await this.databaseService.db
.select()
.from(categories)
.where(eq(categories.id, id))
.limit(1);
return result[0] || null;
return await this.categoriesRepository.findOne(id);
}
async create(data: CreateCategoryDto) {
@@ -44,10 +33,7 @@ export class CategoriesService {
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^\w-]/g, "");
const result = await this.databaseService.db
.insert(categories)
.values({ ...data, slug })
.returning();
const result = await this.categoriesRepository.create({ ...data, slug });
await this.clearCategoriesCache();
return result;
@@ -65,11 +51,7 @@ export class CategoriesService {
.replace(/[^\w-]/g, "")
: undefined,
};
const result = await this.databaseService.db
.update(categories)
.set(updateData)
.where(eq(categories.id, id))
.returning();
const result = await this.categoriesRepository.update(id, updateData);
await this.clearCategoriesCache();
return result;
@@ -77,10 +59,7 @@ export class CategoriesService {
async remove(id: string) {
this.logger.log(`Removing category: ${id}`);
const result = await this.databaseService.db
.delete(categories)
.where(eq(categories.id, id))
.returning();
const result = await this.categoriesRepository.remove(id);
await this.clearCategoriesCache();
return result;

View File

@@ -0,0 +1,50 @@
import { Injectable } from "@nestjs/common";
import { eq } from "drizzle-orm";
import { DatabaseService } from "../../database/database.service";
import { categories } from "../../database/schemas";
import type { CreateCategoryDto } from "../dto/create-category.dto";
import type { UpdateCategoryDto } from "../dto/update-category.dto";
@Injectable()
export class CategoriesRepository {
constructor(private readonly databaseService: DatabaseService) {}
async findAll() {
return await this.databaseService.db
.select()
.from(categories)
.orderBy(categories.name);
}
async findOne(id: string) {
const result = await this.databaseService.db
.select()
.from(categories)
.where(eq(categories.id, id))
.limit(1);
return result[0] || null;
}
async create(data: CreateCategoryDto & { slug: string }) {
return await this.databaseService.db
.insert(categories)
.values(data)
.returning();
}
async update(id: string, data: UpdateCategoryDto & { slug?: string; updatedAt: Date }) {
return await this.databaseService.db
.update(categories)
.set(data)
.where(eq(categories.id, id))
.returning();
}
async remove(id: string) {
return await this.databaseService.db
.delete(categories)
.where(eq(categories.id, id))
.returning();
}
}