From d2b14c39868e27e01ace0176192f977914adf54e Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Wed, 17 Jul 2024 10:39:09 -0500 Subject: [PATCH] refactor: :recycle: Clean up code initializing --- src/api/healthCheck/healthCheckRouter.ts | 30 +++++------ src/api/user/__tests__/userService.test.ts | 50 +++++++++--------- src/api/user/userRepository.ts | 30 ++++++++--- src/api/user/userRouter.ts | 59 ++++++++++------------ src/api/user/userService.ts | 44 ++++++++++------ src/common/models/serviceResponse.ts | 26 ++++++---- src/common/utils/httpHandlers.ts | 13 +++-- 7 files changed, 139 insertions(+), 113 deletions(-) diff --git a/src/api/healthCheck/healthCheckRouter.ts b/src/api/healthCheck/healthCheckRouter.ts index 610ac9a..66ac17e 100644 --- a/src/api/healthCheck/healthCheckRouter.ts +++ b/src/api/healthCheck/healthCheckRouter.ts @@ -1,28 +1,22 @@ import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; import express, { type Request, type Response, type Router } from "express"; -import { StatusCodes } from "http-status-codes"; import { z } from "zod"; import { createApiResponse } from "@/api-docs/openAPIResponseBuilders"; -import { ResponseStatus, ServiceResponse } from "@/common/models/serviceResponse"; +import { ServiceResponse } from "@/common/models/serviceResponse"; import { handleServiceResponse } from "@/common/utils/httpHandlers"; export const healthCheckRegistry = new OpenAPIRegistry(); +export const healthCheckRouter: Router = express.Router(); -export const healthCheckRouter: Router = (() => { - const router = express.Router(); +healthCheckRegistry.registerPath({ + method: "get", + path: "/health-check", + tags: ["Health Check"], + responses: createApiResponse(z.null(), "Success"), +}); - healthCheckRegistry.registerPath({ - method: "get", - path: "/health-check", - tags: ["Health Check"], - responses: createApiResponse(z.null(), "Success"), - }); - - router.get("/", (_req: Request, res: Response) => { - const serviceResponse = new ServiceResponse(ResponseStatus.Success, "Service is healthy", null, StatusCodes.OK); - handleServiceResponse(serviceResponse, res); - }); - - return router; -})(); +healthCheckRouter.get("/", (_req: Request, res: Response) => { + const serviceResponse = ServiceResponse.success("Service is healthy", null); + return handleServiceResponse(serviceResponse, res); +}); diff --git a/src/api/user/__tests__/userService.test.ts b/src/api/user/__tests__/userService.test.ts index 744acfc..5490445 100644 --- a/src/api/user/__tests__/userService.test.ts +++ b/src/api/user/__tests__/userService.test.ts @@ -6,59 +6,61 @@ import { userRepository } from "@/api/user/userRepository"; import { userService } from "@/api/user/userService"; vi.mock("@/api/user/userRepository"); -vi.mock("@/server", () => ({ - ...vi.importActual("@/server"), - logger: { - error: vi.fn(), - }, -})); describe("userService", () => { + let userServiceInstance: userService; + let userRepositoryInstance: userRepository; + const mockUsers: User[] = [ { id: 1, name: "Alice", email: "alice@example.com", age: 42, createdAt: new Date(), updatedAt: new Date() }, { id: 2, name: "Bob", email: "bob@example.com", age: 21, createdAt: new Date(), updatedAt: new Date() }, ]; + beforeEach(() => { + userRepositoryInstance = new userRepository(); + userServiceInstance = new userService(userRepositoryInstance); + }); + describe("findAll", () => { it("return all users", async () => { // Arrange - (userRepository.findAllAsync as Mock).mockReturnValue(mockUsers); + (userRepositoryInstance.findAllAsync as Mock).mockReturnValue(mockUsers); // Act - const result = await userService.findAll(); + const result = await userServiceInstance.findAll(); // Assert expect(result.statusCode).toEqual(StatusCodes.OK); expect(result.success).toBeTruthy(); - expect(result.message).toContain("Users found"); + expect(result.message).equals("Users found"); expect(result.responseObject).toEqual(mockUsers); }); it("returns a not found error for no users found", async () => { // Arrange - (userRepository.findAllAsync as Mock).mockReturnValue(null); + (userRepositoryInstance.findAllAsync as Mock).mockReturnValue(null); // Act - const result = await userService.findAll(); + const result = await userServiceInstance.findAll(); // Assert expect(result.statusCode).toEqual(StatusCodes.NOT_FOUND); expect(result.success).toBeFalsy(); - expect(result.message).toContain("No Users found"); + expect(result.message).equals("No Users found"); expect(result.responseObject).toBeNull(); }); it("handles errors for findAllAsync", async () => { // Arrange - (userRepository.findAllAsync as Mock).mockRejectedValue(new Error("Database error")); + (userRepositoryInstance.findAllAsync as Mock).mockRejectedValue(new Error("Database error")); // Act - const result = await userService.findAll(); + const result = await userServiceInstance.findAll(); // Assert expect(result.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR); expect(result.success).toBeFalsy(); - expect(result.message).toContain("Error finding all users"); + expect(result.message).equals("An error occurred while retrieving users."); expect(result.responseObject).toBeNull(); }); }); @@ -68,45 +70,45 @@ describe("userService", () => { // Arrange const testId = 1; const mockUser = mockUsers.find((user) => user.id === testId); - (userRepository.findByIdAsync as Mock).mockReturnValue(mockUser); + (userRepositoryInstance.findByIdAsync as Mock).mockReturnValue(mockUser); // Act - const result = await userService.findById(testId); + const result = await userServiceInstance.findById(testId); // Assert expect(result.statusCode).toEqual(StatusCodes.OK); expect(result.success).toBeTruthy(); - expect(result.message).toContain("User found"); + expect(result.message).equals("User found"); expect(result.responseObject).toEqual(mockUser); }); it("handles errors for findByIdAsync", async () => { // Arrange const testId = 1; - (userRepository.findByIdAsync as Mock).mockRejectedValue(new Error("Database error")); + (userRepositoryInstance.findByIdAsync as Mock).mockRejectedValue(new Error("Database error")); // Act - const result = await userService.findById(testId); + const result = await userServiceInstance.findById(testId); // Assert expect(result.statusCode).toEqual(StatusCodes.INTERNAL_SERVER_ERROR); expect(result.success).toBeFalsy(); - expect(result.message).toContain(`Error finding user with id ${testId}`); + expect(result.message).equals("An error occurred while finding user."); expect(result.responseObject).toBeNull(); }); it("returns a not found error for non-existent ID", async () => { // Arrange const testId = 1; - (userRepository.findByIdAsync as Mock).mockReturnValue(null); + (userRepositoryInstance.findByIdAsync as Mock).mockReturnValue(null); // Act - const result = await userService.findById(testId); + const result = await userServiceInstance.findById(testId); // Assert expect(result.statusCode).toEqual(StatusCodes.NOT_FOUND); expect(result.success).toBeFalsy(); - expect(result.message).toContain("User not found"); + expect(result.message).equals("User not found"); expect(result.responseObject).toBeNull(); }); }); diff --git a/src/api/user/userRepository.ts b/src/api/user/userRepository.ts index 28f5a1a..15bc7d7 100644 --- a/src/api/user/userRepository.ts +++ b/src/api/user/userRepository.ts @@ -1,16 +1,30 @@ import type { User } from "@/api/user/userModel"; export const users: User[] = [ - { id: 1, name: "Alice", email: "alice@example.com", age: 42, createdAt: new Date(), updatedAt: new Date() }, - { id: 2, name: "Bob", email: "bob@example.com", age: 21, createdAt: new Date(), updatedAt: new Date() }, + { + id: 1, + name: "Alice", + email: "alice@example.com", + age: 42, + createdAt: new Date(), + updatedAt: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days later + }, + { + id: 2, + name: "Robert", + email: "Robert@example.com", + age: 21, + createdAt: new Date(), + updatedAt: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days later + }, ]; -export const userRepository = { - findAllAsync: async (): Promise => { +export class userRepository { + async findAllAsync(): Promise { return users; - }, + } - findByIdAsync: async (id: number): Promise => { + async findByIdAsync(id: number): Promise { return users.find((user) => user.id === id) || null; - }, -}; + } +} diff --git a/src/api/user/userRouter.ts b/src/api/user/userRouter.ts index 9b5b759..22300bc 100644 --- a/src/api/user/userRouter.ts +++ b/src/api/user/userRouter.ts @@ -4,41 +4,36 @@ import { z } from "zod"; import { createApiResponse } from "@/api-docs/openAPIResponseBuilders"; import { GetUserSchema, UserSchema } from "@/api/user/userModel"; -import { userService } from "@/api/user/userService"; +import { userServiceInstance } from "@/api/user/userService"; import { handleServiceResponse, validateRequest } from "@/common/utils/httpHandlers"; export const userRegistry = new OpenAPIRegistry(); +export const userRouter: Router = express.Router(); userRegistry.register("User", UserSchema); -export const userRouter: Router = (() => { - const router = express.Router(); - - userRegistry.registerPath({ - method: "get", - path: "/users", - tags: ["User"], - responses: createApiResponse(z.array(UserSchema), "Success"), - }); - - router.get("/", async (_req: Request, res: Response) => { - const serviceResponse = await userService.findAll(); - handleServiceResponse(serviceResponse, res); - }); - - userRegistry.registerPath({ - method: "get", - path: "/users/{id}", - tags: ["User"], - request: { params: GetUserSchema.shape.params }, - responses: createApiResponse(UserSchema, "Success"), - }); - - router.get("/:id", validateRequest(GetUserSchema), async (req: Request, res: Response) => { - const id = Number.parseInt(req.params.id as string, 10); - const serviceResponse = await userService.findById(id); - handleServiceResponse(serviceResponse, res); - }); - - return router; -})(); +userRegistry.registerPath({ + method: "get", + path: "/users", + tags: ["User"], + responses: createApiResponse(z.array(UserSchema), "Success"), +}); + +userRouter.get("/", async (_req: Request, res: Response) => { + const serviceResponse = await userServiceInstance.findAll(); + return handleServiceResponse(serviceResponse, res); +}); + +userRegistry.registerPath({ + method: "get", + path: "/users/{id}", + tags: ["User"], + request: { params: GetUserSchema.shape.params }, + responses: createApiResponse(UserSchema, "Success"), +}); + +userRouter.get("/:id", validateRequest(GetUserSchema), async (req: Request, res: Response) => { + const id = Number.parseInt(req.params.id as string, 10); + const serviceResponse = await userServiceInstance.findById(id); + return handleServiceResponse(serviceResponse, res); +}); diff --git a/src/api/user/userService.ts b/src/api/user/userService.ts index 188121e..652741a 100644 --- a/src/api/user/userService.ts +++ b/src/api/user/userService.ts @@ -2,37 +2,49 @@ import { StatusCodes } from "http-status-codes"; import type { User } from "@/api/user/userModel"; import { userRepository } from "@/api/user/userRepository"; -import { ResponseStatus, ServiceResponse } from "@/common/models/serviceResponse"; +import { ServiceResponse } from "@/common/models/serviceResponse"; import { logger } from "@/server"; -export const userService = { +export class userService { + private userRepository: userRepository; + + constructor(repository: userRepository = new userRepository()) { + this.userRepository = repository; + } + // Retrieves all users from the database - findAll: async (): Promise> => { + async findAll(): Promise> { try { - const users = await userRepository.findAllAsync(); - if (!users) { - return new ServiceResponse(ResponseStatus.Failed, "No Users found", null, StatusCodes.NOT_FOUND); + const users = await this.userRepository.findAllAsync(); + if (!users || users.length === 0) { + return ServiceResponse.failure("No Users found", null, StatusCodes.NOT_FOUND); } - return new ServiceResponse(ResponseStatus.Success, "Users found", users, StatusCodes.OK); + return ServiceResponse.success("Users found", users); } catch (ex) { const errorMessage = `Error finding all users: $${(ex as Error).message}`; logger.error(errorMessage); - return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); + return ServiceResponse.failure( + "An error occurred while retrieving users.", + null, + StatusCodes.INTERNAL_SERVER_ERROR, + ); } - }, + } // Retrieves a single user by their ID - findById: async (id: number): Promise> => { + async findById(id: number): Promise> { try { - const user = await userRepository.findByIdAsync(id); + const user = await this.userRepository.findByIdAsync(id); if (!user) { - return new ServiceResponse(ResponseStatus.Failed, "User not found", null, StatusCodes.NOT_FOUND); + return ServiceResponse.failure("User not found", null, StatusCodes.NOT_FOUND); } - return new ServiceResponse(ResponseStatus.Success, "User found", user, StatusCodes.OK); + return ServiceResponse.success("User found", user); } catch (ex) { const errorMessage = `Error finding user with id ${id}:, ${(ex as Error).message}`; logger.error(errorMessage); - return new ServiceResponse(ResponseStatus.Failed, errorMessage, null, StatusCodes.INTERNAL_SERVER_ERROR); + return ServiceResponse.failure("An error occurred while finding user.", null, StatusCodes.INTERNAL_SERVER_ERROR); } - }, -}; + } +} + +export const userServiceInstance = new userService(); diff --git a/src/common/models/serviceResponse.ts b/src/common/models/serviceResponse.ts index 585f9e5..344a104 100644 --- a/src/common/models/serviceResponse.ts +++ b/src/common/models/serviceResponse.ts @@ -1,22 +1,26 @@ +import { StatusCodes } from "http-status-codes"; import { z } from "zod"; -export enum ResponseStatus { - Success = 0, - Failed = 1, -} - export class ServiceResponse { - success: boolean; - message: string; - responseObject: T; - statusCode: number; + readonly success: boolean; + readonly message: string; + readonly responseObject: T; + readonly statusCode: number; - constructor(status: ResponseStatus, message: string, responseObject: T, statusCode: number) { - this.success = status === ResponseStatus.Success; + private constructor(success: boolean, message: string, responseObject: T, statusCode: number) { + this.success = success; this.message = message; this.responseObject = responseObject; this.statusCode = statusCode; } + + static success(message: string, responseObject: T, statusCode: number = StatusCodes.OK) { + return new ServiceResponse(true, message, responseObject, statusCode); + } + + static failure(message: string, responseObject: T, statusCode: number = StatusCodes.BAD_REQUEST) { + return new ServiceResponse(false, message, responseObject, statusCode); + } } export const ServiceResponseSchema = (dataSchema: T) => diff --git a/src/common/utils/httpHandlers.ts b/src/common/utils/httpHandlers.ts index 9b92722..f6364df 100644 --- a/src/common/utils/httpHandlers.ts +++ b/src/common/utils/httpHandlers.ts @@ -2,7 +2,7 @@ import type { NextFunction, Request, Response } from "express"; import { StatusCodes } from "http-status-codes"; import type { ZodError, ZodSchema } from "zod"; -import { ResponseStatus, ServiceResponse } from "@/common/models/serviceResponse"; +import { ServiceResponse } from "@/common/models/serviceResponse"; export const handleServiceResponse = (serviceResponse: ServiceResponse, response: Response) => { return response.status(serviceResponse.statusCode).send(serviceResponse); @@ -13,8 +13,13 @@ export const validateRequest = (schema: ZodSchema) => (req: Request, res: Respon schema.parse({ body: req.body, query: req.query, params: req.params }); next(); } catch (err) { - const errorMessage = `Invalid input: ${(err as ZodError).errors.map((e) => e.message).join(", ")}`; - const statusCode = StatusCodes.BAD_REQUEST; - res.status(statusCode).send(new ServiceResponse(ResponseStatus.Failed, errorMessage, null, statusCode)); + if (err as ZodError) { + const errorMessage = `Invalid input: ${(err as ZodError).errors.map((e) => e.message).join(", ")}`; + const statusCode = StatusCodes.BAD_REQUEST; + const serviceResponse = ServiceResponse.failure(errorMessage, null, statusCode); + return handleServiceResponse(serviceResponse, res); + } + const serviceResponse = ServiceResponse.failure("Invalid input", null, StatusCodes.BAD_REQUEST); + return handleServiceResponse(serviceResponse, res); } };