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:
@@ -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 {}
|
||||
|
||||
@@ -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 }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
50
backend/src/categories/repositories/categories.repository.ts
Normal file
50
backend/src/categories/repositories/categories.repository.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user