Skip to content

Commit

Permalink
refactor: ♻️ Clean up code initializing
Browse files Browse the repository at this point in the history
  • Loading branch information
edwinhern committed Jul 17, 2024
1 parent a1ccfc1 commit d2b14c3
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 113 deletions.
30 changes: 12 additions & 18 deletions src/api/healthCheck/healthCheckRouter.ts
Original file line number Diff line number Diff line change
@@ -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);
});
50 changes: 26 additions & 24 deletions src/api/user/__tests__/userService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]", age: 42, createdAt: new Date(), updatedAt: new Date() },
{ id: 2, name: "Bob", email: "[email protected]", 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();
});
});
Expand All @@ -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();
});
});
Expand Down
30 changes: 22 additions & 8 deletions src/api/user/userRepository.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import type { User } from "@/api/user/userModel";

export const users: User[] = [
{ id: 1, name: "Alice", email: "[email protected]", age: 42, createdAt: new Date(), updatedAt: new Date() },
{ id: 2, name: "Bob", email: "[email protected]", age: 21, createdAt: new Date(), updatedAt: new Date() },
{
id: 1,
name: "Alice",
email: "[email protected]",
age: 42,
createdAt: new Date(),
updatedAt: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days later
},
{
id: 2,
name: "Robert",
email: "[email protected]",
age: 21,
createdAt: new Date(),
updatedAt: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000), // 5 days later
},
];

export const userRepository = {
findAllAsync: async (): Promise<User[]> => {
export class userRepository {
async findAllAsync(): Promise<User[]> {
return users;
},
}

findByIdAsync: async (id: number): Promise<User | null> => {
async findByIdAsync(id: number): Promise<User | null> {
return users.find((user) => user.id === id) || null;
},
};
}
}
59 changes: 27 additions & 32 deletions src/api/user/userRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
44 changes: 28 additions & 16 deletions src/api/user/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServiceResponse<User[] | null>> => {
async findAll(): Promise<ServiceResponse<User[] | null>> {
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<User[]>(ResponseStatus.Success, "Users found", users, StatusCodes.OK);
return ServiceResponse.success<User[]>("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<ServiceResponse<User | null>> => {
async findById(id: number): Promise<ServiceResponse<User | null>> {
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<User>(ResponseStatus.Success, "User found", user, StatusCodes.OK);
return ServiceResponse.success<User>("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();
26 changes: 15 additions & 11 deletions src/common/models/serviceResponse.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import { StatusCodes } from "http-status-codes";
import { z } from "zod";

export enum ResponseStatus {
Success = 0,
Failed = 1,
}

export class ServiceResponse<T = null> {
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<T>(message: string, responseObject: T, statusCode: number = StatusCodes.OK) {
return new ServiceResponse(true, message, responseObject, statusCode);
}

static failure<T>(message: string, responseObject: T, statusCode: number = StatusCodes.BAD_REQUEST) {
return new ServiceResponse(false, message, responseObject, statusCode);
}
}

export const ServiceResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
Expand Down
Loading

0 comments on commit d2b14c3

Please sign in to comment.