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,10 +4,11 @@ import { CryptoModule } from "../crypto/crypto.module";
|
||||
import { DatabaseModule } from "../database/database.module";
|
||||
import { ReportsController } from "./reports.controller";
|
||||
import { ReportsService } from "./reports.service";
|
||||
import { ReportsRepository } from "./repositories/reports.repository";
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, AuthModule, CryptoModule],
|
||||
controllers: [ReportsController],
|
||||
providers: [ReportsService],
|
||||
providers: [ReportsService, ReportsRepository],
|
||||
})
|
||||
export class ReportsModule {}
|
||||
|
||||
@@ -1,56 +1,29 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { DatabaseService } from "../database/database.service";
|
||||
import { CreateReportDto } from "./dto/create-report.dto";
|
||||
import { ReportsService } from "./reports.service";
|
||||
import { ReportsRepository } from "./repositories/reports.repository";
|
||||
|
||||
describe("ReportsService", () => {
|
||||
let service: ReportsService;
|
||||
let repository: ReportsRepository;
|
||||
|
||||
const mockDb = {
|
||||
insert: jest.fn(),
|
||||
values: jest.fn(),
|
||||
returning: jest.fn(),
|
||||
select: jest.fn(),
|
||||
from: jest.fn(),
|
||||
orderBy: jest.fn(),
|
||||
limit: jest.fn(),
|
||||
offset: jest.fn(),
|
||||
update: jest.fn(),
|
||||
set: jest.fn(),
|
||||
where: jest.fn(),
|
||||
const mockReportsRepository = {
|
||||
create: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
updateStatus: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const chain = {
|
||||
insert: jest.fn().mockReturnThis(),
|
||||
values: jest.fn().mockReturnThis(),
|
||||
returning: jest.fn().mockReturnThis(),
|
||||
select: jest.fn().mockReturnThis(),
|
||||
from: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
limit: jest.fn().mockReturnThis(),
|
||||
offset: jest.fn().mockReturnThis(),
|
||||
update: jest.fn().mockReturnThis(),
|
||||
set: jest.fn().mockReturnThis(),
|
||||
where: 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: [
|
||||
ReportsService,
|
||||
{ provide: DatabaseService, useValue: { db: mockDb } },
|
||||
{ provide: ReportsRepository, useValue: mockReportsRepository },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ReportsService>(ReportsService);
|
||||
repository = module.get<ReportsRepository>(ReportsRepository);
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
@@ -60,29 +33,31 @@ describe("ReportsService", () => {
|
||||
describe("create", () => {
|
||||
it("should create a report", async () => {
|
||||
const reporterId = "u1";
|
||||
const data: CreateReportDto = { contentId: "c1", reason: "spam" };
|
||||
mockDb.returning.mockResolvedValue([{ id: "r1", ...data, reporterId }]);
|
||||
const data = { contentId: "c1", reason: "spam" };
|
||||
mockReportsRepository.create.mockResolvedValue({ id: "r1", ...data, reporterId });
|
||||
|
||||
const result = await service.create(reporterId, data);
|
||||
|
||||
expect(result.id).toBe("r1");
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(repository.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should return reports", async () => {
|
||||
mockDb.offset.mockResolvedValue([{ id: "r1" }]);
|
||||
mockReportsRepository.findAll.mockResolvedValue([{ id: "r1" }]);
|
||||
const result = await service.findAll(10, 0);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(repository.findAll).toHaveBeenCalledWith(10, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateStatus", () => {
|
||||
it("should update report status", async () => {
|
||||
mockDb.returning.mockResolvedValue([{ id: "r1", status: "resolved" }]);
|
||||
mockReportsRepository.updateStatus.mockResolvedValue([{ id: "r1", status: "resolved" }]);
|
||||
const result = await service.updateStatus("r1", "resolved");
|
||||
expect(result[0].status).toBe("resolved");
|
||||
expect(repository.updateStatus).toHaveBeenCalledWith("r1", "resolved");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { DatabaseService } from "../database/database.service";
|
||||
import { reports } from "../database/schemas";
|
||||
import { ReportsRepository } from "./repositories/reports.repository";
|
||||
import { CreateReportDto } from "./dto/create-report.dto";
|
||||
|
||||
@Injectable()
|
||||
export class ReportsService {
|
||||
private readonly logger = new Logger(ReportsService.name);
|
||||
|
||||
constructor(private readonly databaseService: DatabaseService) {}
|
||||
constructor(private readonly reportsRepository: ReportsRepository) {}
|
||||
|
||||
async create(reporterId: string, data: CreateReportDto) {
|
||||
this.logger.log(`Creating report from user ${reporterId}`);
|
||||
const [newReport] = await this.databaseService.db
|
||||
.insert(reports)
|
||||
.values({
|
||||
reporterId,
|
||||
contentId: data.contentId,
|
||||
tagId: data.tagId,
|
||||
reason: data.reason,
|
||||
description: data.description,
|
||||
})
|
||||
.returning();
|
||||
return newReport;
|
||||
return await this.reportsRepository.create({
|
||||
reporterId,
|
||||
contentId: data.contentId,
|
||||
tagId: data.tagId,
|
||||
reason: data.reason,
|
||||
description: data.description,
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(limit: number, offset: number) {
|
||||
return await this.databaseService.db
|
||||
.select()
|
||||
.from(reports)
|
||||
.orderBy(desc(reports.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
return await this.reportsRepository.findAll(limit, offset);
|
||||
}
|
||||
|
||||
async updateStatus(
|
||||
@@ -39,10 +28,6 @@ export class ReportsService {
|
||||
status: "pending" | "reviewed" | "resolved" | "dismissed",
|
||||
) {
|
||||
this.logger.log(`Updating report ${id} status to ${status}`);
|
||||
return await this.databaseService.db
|
||||
.update(reports)
|
||||
.set({ status, updatedAt: new Date() })
|
||||
.where(eq(reports.id, id))
|
||||
.returning();
|
||||
return await this.reportsRepository.updateStatus(id, status);
|
||||
}
|
||||
}
|
||||
|
||||
50
backend/src/reports/repositories/reports.repository.ts
Normal file
50
backend/src/reports/repositories/reports.repository.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { desc, eq, lte } from "drizzle-orm";
|
||||
import { DatabaseService } from "../../database/database.service";
|
||||
import { reports } from "../../database/schemas";
|
||||
|
||||
@Injectable()
|
||||
export class ReportsRepository {
|
||||
constructor(private readonly databaseService: DatabaseService) {}
|
||||
|
||||
async create(data: {
|
||||
reporterId: string;
|
||||
contentId?: string;
|
||||
tagId?: string;
|
||||
reason: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const [newReport] = await this.databaseService.db
|
||||
.insert(reports)
|
||||
.values(data)
|
||||
.returning();
|
||||
return newReport;
|
||||
}
|
||||
|
||||
async findAll(limit: number, offset: number) {
|
||||
return await this.databaseService.db
|
||||
.select()
|
||||
.from(reports)
|
||||
.orderBy(desc(reports.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
}
|
||||
|
||||
async updateStatus(
|
||||
id: string,
|
||||
status: "pending" | "reviewed" | "resolved" | "dismissed",
|
||||
) {
|
||||
return await this.databaseService.db
|
||||
.update(reports)
|
||||
.set({ status, updatedAt: new Date() })
|
||||
.where(eq(reports.id, id))
|
||||
.returning();
|
||||
}
|
||||
|
||||
async purgeObsolete(now: Date) {
|
||||
return await this.databaseService.db
|
||||
.delete(reports)
|
||||
.where(lte(reports.expiresAt, now))
|
||||
.returning();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user