fix controller architecture
This commit is contained in:
1 parent
ab9e722caa
commit
74a12754b6
4 files changed
+107
-45
No files matched your search
@@ -348,7 +348,7 @@ export const initializeServices = async ({
|
||||
incidentsRepository,
|
||||
});
|
||||
|
||||
const statusPageService = new StatusPageService(statusPagesRepository);
|
||||
const statusPageService = new StatusPageService(statusPagesRepository, settingsService);
|
||||
|
||||
const services = {
|
||||
settingsService,
|
||||
|
||||
@@ -12,7 +12,6 @@ import { IStatusPageService } from "@/service/business/statusPageService.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { NormalizeData } from "@/utils/dataUtils.js";
|
||||
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, type StatusPage } from "@/types/statusPage.js";
|
||||
|
||||
const SERVICE_NAME = "statusPageController";
|
||||
|
||||
@@ -40,19 +39,6 @@ class StatusPageController implements IStatusPageController {
|
||||
return StatusPageController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private withoutThemeFields = (body: Record<string, unknown>): Record<string, unknown> => {
|
||||
const rest = { ...body };
|
||||
delete rest.theme;
|
||||
delete rest.themeMode;
|
||||
return rest;
|
||||
};
|
||||
|
||||
private applyDefaultTheme = (statusPage: StatusPage): StatusPage => ({
|
||||
...statusPage,
|
||||
theme: DEFAULT_STATUS_PAGE_THEME,
|
||||
themeMode: DEFAULT_STATUS_PAGE_THEME_MODE,
|
||||
});
|
||||
|
||||
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
createStatusPageBodyValidation.parse(req.body);
|
||||
@@ -60,16 +46,14 @@ class StatusPageController implements IStatusPageController {
|
||||
imageValidation.parse(req.file);
|
||||
}
|
||||
|
||||
const themesEnabled = this.settingsService.areStatusPageThemesEnabled();
|
||||
const body = themesEnabled ? req.body : this.withoutThemeFields(req.body);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const userId = requireUserId(req?.user?.id);
|
||||
const statusPage = await this.statusPageService.createStatusPage(userId, teamId, req.file, body);
|
||||
const statusPage = await this.statusPageService.createStatusPage(userId, teamId, req.file, req.body);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page created successfully",
|
||||
data: themesEnabled ? statusPage : this.applyDefaultTheme(statusPage),
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -83,21 +67,19 @@ class StatusPageController implements IStatusPageController {
|
||||
imageValidation.parse(req.file);
|
||||
}
|
||||
|
||||
const themesEnabled = this.settingsService.areStatusPageThemesEnabled();
|
||||
const body = themesEnabled ? req.body : this.withoutThemeFields(req.body);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const statusPageId = req.params.id as string;
|
||||
if (!statusPageId) {
|
||||
throw new AppError({ message: "Status page ID is required", status: 400 });
|
||||
}
|
||||
const statusPage = await this.statusPageService.updateStatusPage(statusPageId, teamId, req.file, body);
|
||||
const statusPage = await this.statusPageService.updateStatusPage(statusPageId, teamId, req.file, req.body);
|
||||
if (statusPage === null) {
|
||||
throw new AppError({ message: "Status page not found", status: 404 });
|
||||
}
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page updated successfully",
|
||||
data: themesEnabled ? statusPage : this.applyDefaultTheme(statusPage),
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -143,12 +125,11 @@ class StatusPageController implements IStatusPageController {
|
||||
}
|
||||
return { ...monitor, checks: normalizedChecks };
|
||||
});
|
||||
const themesEnabled = this.settingsService.areStatusPageThemesEnabled();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status page retrieved successfully",
|
||||
data: {
|
||||
statusPage: themesEnabled ? statusPage : this.applyDefaultTheme(statusPage),
|
||||
statusPage,
|
||||
monitors: normalizedMonitors,
|
||||
},
|
||||
});
|
||||
@@ -161,11 +142,10 @@ class StatusPageController implements IStatusPageController {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const statusPages = await this.statusPageService.getStatusPagesByTeamId(teamId);
|
||||
|
||||
const themesEnabled = this.settingsService.areStatusPageThemesEnabled();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Status pages retrieved successfully",
|
||||
data: themesEnabled ? statusPages : statusPages.map((sp) => this.applyDefaultTheme(sp)),
|
||||
data: statusPages,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { type IStatusPagesRepository } from "@/repositories/index.js";
|
||||
import { StatusPage } from "@/types/index.js";
|
||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE, StatusPage } from "@/types/index.js";
|
||||
|
||||
export interface IStatusPageService {
|
||||
createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
|
||||
getStatusPageByUrl(url: string): Promise<StatusPage>;
|
||||
@@ -11,29 +13,52 @@ export interface IStatusPageService {
|
||||
|
||||
export class StatusPageService implements IStatusPageService {
|
||||
private statusPagesRepository: IStatusPagesRepository;
|
||||
constructor(statusPagesRepository: IStatusPagesRepository) {
|
||||
private settingsService: ISettingsService;
|
||||
constructor(statusPagesRepository: IStatusPagesRepository, settingsService: ISettingsService) {
|
||||
this.statusPagesRepository = statusPagesRepository;
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
private withoutThemeFields = (data: Partial<StatusPage>): Partial<StatusPage> => {
|
||||
const { theme: _theme, themeMode: _themeMode, ...rest } = data;
|
||||
return rest;
|
||||
};
|
||||
|
||||
private applyDefaultTheme = (statusPage: StatusPage): StatusPage => ({
|
||||
...statusPage,
|
||||
theme: DEFAULT_STATUS_PAGE_THEME,
|
||||
themeMode: DEFAULT_STATUS_PAGE_THEME_MODE,
|
||||
});
|
||||
|
||||
private normalizeTheme = (statusPage: StatusPage): StatusPage =>
|
||||
this.settingsService.areStatusPageThemesEnabled() ? statusPage : this.applyDefaultTheme(statusPage);
|
||||
|
||||
private normalizeInput = (data: Partial<StatusPage>): Partial<StatusPage> =>
|
||||
this.settingsService.areStatusPageThemesEnabled() ? data : this.withoutThemeFields(data);
|
||||
|
||||
createStatusPage = async (
|
||||
userId: string,
|
||||
teamId: string,
|
||||
image: Express.Multer.File | undefined,
|
||||
data: Partial<StatusPage>
|
||||
): Promise<StatusPage> => {
|
||||
return await this.statusPagesRepository.create(userId, teamId, image, data);
|
||||
const created = await this.statusPagesRepository.create(userId, teamId, image, this.normalizeInput(data));
|
||||
return this.normalizeTheme(created);
|
||||
};
|
||||
|
||||
getStatusPageByUrl = async (url: string): Promise<StatusPage> => {
|
||||
return await this.statusPagesRepository.findByUrl(url);
|
||||
const statusPage = await this.statusPagesRepository.findByUrl(url);
|
||||
return this.normalizeTheme(statusPage);
|
||||
};
|
||||
|
||||
getStatusPagesByTeamId = async (teamId: string): Promise<StatusPage[]> => {
|
||||
return await this.statusPagesRepository.findByTeamId(teamId);
|
||||
const statusPages = await this.statusPagesRepository.findByTeamId(teamId);
|
||||
return statusPages.map((sp) => this.normalizeTheme(sp));
|
||||
};
|
||||
|
||||
updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
|
||||
return await this.statusPagesRepository.updateById(id, teamId, image, data);
|
||||
const updated = await this.statusPagesRepository.updateById(id, teamId, image, this.normalizeInput(data));
|
||||
return this.normalizeTheme(updated);
|
||||
};
|
||||
|
||||
deleteStatusPage = async (statusPageId: string, teamId: string): Promise<StatusPage> => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it, jest } from "@jest/globals";
|
||||
import { StatusPageService } from "../../../src/service/business/statusPageService.ts";
|
||||
import type { IStatusPagesRepository } from "../../../src/repositories/status-pages/IStatusPagesRepository.ts";
|
||||
import type { ISettingsService } from "../../../src/service/system/settingsService.ts";
|
||||
import type { StatusPage } from "../../../src/types/index.ts";
|
||||
import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE } from "../../../src/types/index.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -13,6 +15,8 @@ const makeStatusPage = (overrides?: Partial<StatusPage>): StatusPage =>
|
||||
url: "my-status-page",
|
||||
companyName: "Test Co",
|
||||
monitors: ["mon-1"],
|
||||
theme: "modern",
|
||||
themeMode: "dark",
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
...overrides,
|
||||
@@ -28,18 +32,24 @@ const createRepo = () =>
|
||||
removeMonitorFromStatusPages: jest.fn().mockResolvedValue(1),
|
||||
}) as unknown as jest.Mocked<IStatusPagesRepository>;
|
||||
|
||||
const createService = () => {
|
||||
const createSettingsService = (themesEnabled: boolean) =>
|
||||
({
|
||||
areStatusPageThemesEnabled: jest.fn().mockReturnValue(themesEnabled),
|
||||
}) as unknown as jest.Mocked<ISettingsService>;
|
||||
|
||||
const createService = (themesEnabled = true) => {
|
||||
const repo = createRepo();
|
||||
const service = new StatusPageService(repo);
|
||||
return { service, repo };
|
||||
const settingsService = createSettingsService(themesEnabled);
|
||||
const service = new StatusPageService(repo, settingsService);
|
||||
return { service, repo, settingsService };
|
||||
};
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("StatusPageService", () => {
|
||||
describe("createStatusPage", () => {
|
||||
it("delegates to repository with all parameters", async () => {
|
||||
const { service, repo } = createService();
|
||||
it("delegates to repository with all parameters when themes enabled", async () => {
|
||||
const { service, repo } = createService(true);
|
||||
const data = { companyName: "New Co" };
|
||||
const file = { originalname: "logo.png" } as Express.Multer.File;
|
||||
|
||||
@@ -50,39 +60,75 @@ describe("StatusPageService", () => {
|
||||
});
|
||||
|
||||
it("passes undefined image when not provided", async () => {
|
||||
const { service, repo } = createService();
|
||||
const { service, repo } = createService(true);
|
||||
|
||||
await service.createStatusPage("user-1", "team-1", undefined, {});
|
||||
|
||||
expect(repo.create).toHaveBeenCalledWith("user-1", "team-1", undefined, {});
|
||||
});
|
||||
|
||||
it("strips theme fields from input and applies defaults to result when themes disabled", async () => {
|
||||
const { service, repo } = createService(false);
|
||||
const data = { companyName: "New Co", theme: "bold" as const, themeMode: "light" as const };
|
||||
|
||||
const result = await service.createStatusPage("user-1", "team-1", undefined, data);
|
||||
|
||||
expect(repo.create).toHaveBeenCalledWith("user-1", "team-1", undefined, { companyName: "New Co" });
|
||||
expect(result.theme).toBe(DEFAULT_STATUS_PAGE_THEME);
|
||||
expect(result.themeMode).toBe(DEFAULT_STATUS_PAGE_THEME_MODE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatusPageByUrl", () => {
|
||||
it("delegates to repository", async () => {
|
||||
const { service, repo } = createService();
|
||||
const { service, repo } = createService(true);
|
||||
|
||||
const result = await service.getStatusPageByUrl("my-status-page");
|
||||
|
||||
expect(repo.findByUrl).toHaveBeenCalledWith("my-status-page");
|
||||
expect(result).toEqual(makeStatusPage());
|
||||
});
|
||||
|
||||
it("applies default theme when themes disabled", async () => {
|
||||
const { service } = createService(false);
|
||||
|
||||
const result = await service.getStatusPageByUrl("my-status-page");
|
||||
|
||||
expect(result.theme).toBe(DEFAULT_STATUS_PAGE_THEME);
|
||||
expect(result.themeMode).toBe(DEFAULT_STATUS_PAGE_THEME_MODE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStatusPagesByTeamId", () => {
|
||||
it("delegates to repository", async () => {
|
||||
const { service, repo } = createService();
|
||||
const { service, repo } = createService(true);
|
||||
|
||||
const result = await service.getStatusPagesByTeamId("team-1");
|
||||
|
||||
expect(repo.findByTeamId).toHaveBeenCalledWith("team-1");
|
||||
expect(result).toEqual([makeStatusPage()]);
|
||||
});
|
||||
|
||||
it("applies default theme to every result when themes disabled", async () => {
|
||||
const { service, repo } = createService(false);
|
||||
(repo.findByTeamId as jest.Mock).mockResolvedValue([
|
||||
makeStatusPage({ id: "sp-1", theme: "bold" }),
|
||||
makeStatusPage({ id: "sp-2", theme: "editorial" }),
|
||||
]);
|
||||
|
||||
const result = await service.getStatusPagesByTeamId("team-1");
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
for (const sp of result) {
|
||||
expect(sp.theme).toBe(DEFAULT_STATUS_PAGE_THEME);
|
||||
expect(sp.themeMode).toBe(DEFAULT_STATUS_PAGE_THEME_MODE);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateStatusPage", () => {
|
||||
it("delegates to repository with all parameters", async () => {
|
||||
const { service, repo } = createService();
|
||||
const { service, repo } = createService(true);
|
||||
const updated = makeStatusPage({ companyName: "Updated Co" });
|
||||
(repo.updateById as jest.Mock).mockResolvedValue(updated);
|
||||
const file = { originalname: "new-logo.png" } as Express.Multer.File;
|
||||
@@ -93,19 +139,30 @@ describe("StatusPageService", () => {
|
||||
expect(result).toBe(updated);
|
||||
});
|
||||
|
||||
it("passes theme and themeMode through to the repository", async () => {
|
||||
const { service, repo } = createService();
|
||||
it("passes theme and themeMode through to the repository when themes enabled", async () => {
|
||||
const { service, repo } = createService(true);
|
||||
const patch = { theme: "modern" as const, themeMode: "dark" as const };
|
||||
|
||||
await service.updateStatusPage("sp-1", "team-1", undefined, patch);
|
||||
|
||||
expect(repo.updateById).toHaveBeenCalledWith("sp-1", "team-1", undefined, patch);
|
||||
});
|
||||
|
||||
it("strips theme fields from input and applies defaults to result when themes disabled", async () => {
|
||||
const { service, repo } = createService(false);
|
||||
const patch = { companyName: "Updated Co", theme: "modern" as const, themeMode: "dark" as const };
|
||||
|
||||
const result = await service.updateStatusPage("sp-1", "team-1", undefined, patch);
|
||||
|
||||
expect(repo.updateById).toHaveBeenCalledWith("sp-1", "team-1", undefined, { companyName: "Updated Co" });
|
||||
expect(result.theme).toBe(DEFAULT_STATUS_PAGE_THEME);
|
||||
expect(result.themeMode).toBe(DEFAULT_STATUS_PAGE_THEME_MODE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteStatusPage", () => {
|
||||
it("delegates to repository and returns deleted page", async () => {
|
||||
const { service, repo } = createService();
|
||||
const { service, repo } = createService(true);
|
||||
|
||||
const result = await service.deleteStatusPage("sp-1", "team-1");
|
||||
|
||||
|
||||
Reference in new issue
Block a user