Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
liya-zhu committed Nov 14, 2024
1 parent 0d18e06 commit 9f04ff8
Show file tree
Hide file tree
Showing 6 changed files with 397 additions and 2 deletions.
46 changes: 46 additions & 0 deletions backend/typescript/middlewares/validators/activityValidators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from "express";
import { getApiValidationError, validateDate, validatePrimitive,} from "./util";

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable-next-line import/prefer-default-export */
export const activityRequestDtoValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const { body } = req;

if (!validatePrimitive(body.activityId, "integer")) {
return res.status(400).send(getApiValidationError("activityId", "integer"));
}

if (!validatePrimitive(body.userId, "integer")) {
return res.status(400).send(getApiValidationError("userId", "integer"));
}

if (!validatePrimitive(body.petId, "integer")) {
return res.status(400).send(getApiValidationError("petId", "integer"));
}

if (!validatePrimitive(body.activityTypeId, "integer")) {
return res.status(400).send(getApiValidationError("activityTypeId", "integer"));
}

if (!validateDate(body.scheduledStartTime)) {
return res.status(400).send(getApiValidationError("scheduledStartTime", "integer"));
}

if (!validateDate(body.startTime)) {
return res.status(400).send(getApiValidationError("startTime", "integer"));
}

if (!validateDate(body.endTime)) {
return res.status(400).send(getApiValidationError("endTime", "integer"));
}

if (!validatePrimitive(body.notes, "string")) {
return res.status(400).send(getApiValidationError("notes", "string"));
}

return next();
};
6 changes: 5 additions & 1 deletion backend/typescript/middlewares/validators/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Type = "string" | "integer" | "boolean" | "decimal" | "PetStatus" | "Sex";
type Type = "string" | "integer" | "boolean" | "decimal" | "PetStatus" | "Sex" | "date";

const allowableContentTypes = new Set([
"text/plain",
Expand Down Expand Up @@ -50,6 +50,10 @@ export const validateFileType = (mimetype: string): boolean => {
return allowableContentTypes.has(mimetype);
};

export const validateDate = (value: any): boolean => {
return value instanceof Date && !isNaN(value.getTime());
};

export const getApiValidationError = (
fieldName: string,
type: Type,
Expand Down
121 changes: 121 additions & 0 deletions backend/typescript/rest/activityRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Router } from "express";
import { getAccessToken, isAuthorizedByRole } from "../middlewares/auth";
import { activityRequestDtoValidator } from "../middlewares/validators/activityValidators";
import ActivityService from "../services/implementations/activityService";
import {
ActivityResponseDTO,
IActivityService,
} from "../services/interfaces/activityService";
import { getErrorMessage, INTERNAL_SERVER_ERROR_MESSAGE, NotFoundError } from "../utilities/errorUtils";
import { sendResponseByMimeType } from "../utilities/responseUtil";
import { Role } from "../types";

const activityRouter: Router = Router();
// activityRouter.use(isAuthorizedByRole((new Set([Role.ADMINISTRATOR, Role.ANIMAL_BEHAVIOURIST]))));
const activityService: IActivityService = new ActivityService();

/* Get all Activities */
activityRouter.get("/", async (req, res) => {
const contentType = req.headers["content-type"];
try {
const activities = await activityService.getActivities();
await sendResponseByMimeType<ActivityResponseDTO>(res, 200, contentType, activities);
} catch (e: unknown) {
await sendResponseByMimeType(res, 500, contentType, [
{
error: INTERNAL_SERVER_ERROR_MESSAGE,
},
]);
}
});

/* Get Activity by id */
activityRouter.get("/:id", async (req, res) => {
const { id } = req.params;
try {
const activity = await activityService.getActivity(id);
res.status(200).json(activity);
} catch (error: unknown) {
if (error instanceof NotFoundError) {
res.status(404).send(getErrorMessage(error));
} else {
res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE);
}
}
});

/* Create Activity */
activityRouter.post("/", activityRequestDtoValidator, async (req, res) => {
const accessToken = getAccessToken(req);
if (!accessToken) {
res.status(404).json({ error: "Access token not found" });
return;
}

const isBehaviourist = await authService.isAuthorizedByRole(
accessToken,
new Set([Role.ANIMAL_BEHAVIOURIST]),
);

const isAdministrator = await authService.isAuthorizedByRole(
accessToken,
new Set([Role.ADMINISTRATOR]),
);

try {

const { body } = req;
const newActivity = await activityService.createActivity({
activityId: body.activityId,
userId: body.userId,
petId: body.petId,
activityTypeId: body.activityTypeId,
scheduledStartTime: body.scheduledStartTime,
startTime: body.startTime,
endTime: body.endTime,
notes: body.notes,
});
res.status(201).json(newActivity);
} catch (e: unknown) {
res.status(500).send(INTERNAL_SERVER_ERROR_MESSAGE);
}
},
);



/* Update Activity by id */
activityRouter.put(
"/:id",
activityRequestDtoValidator,
async (req, res) => {
const { id } = req.params;
try {
const { body } = req;
const Activity = await activityService.updateActivity(id, {
stringField: body.stringField,
intField: body.intField,
enumField: body.enumField,
stringArrayField: body.stringArrayField,
boolField: body.boolField,
});
res.status(200).json(Activity);
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
},
);

/* Delete Activity by id */
activityRouter.delete("/:id", async (req, res) => {
const { id } = req.params;

try {
const deletedId = await activityService.deleteActivity(id);
res.status(200).json({ id: deletedId });
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
});

export default activityRouter;
157 changes: 157 additions & 0 deletions backend/typescript/services/implementations/activityService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import PgActivity from "../../models/Activity.model";
import {
IActivityService,
ActivityRequestDTO,
ActivityResponseDTO,
} from "../interfaces/activityService";
import { getErrorMessage, NotFoundError } from "../../utilities/errorUtils";
import logger from "../../utilities/logger";

const Logger = logger(__filename);

class ActivityService implements IActivityService {
/* eslint-disable class-methods-use-this */
async getActivity(id: string): Promise<ActivityResponseDTO> {
let activity: PgActivity | null;
try {
activity = await PgActivity.findByPk(id, { raw: true });
if (!activity) {
throw new NotFoundError(`Activity id ${id} not found`);
}
} catch (error: unknown) {
Logger.error(`Failed to get activity. Reason = ${getErrorMessage(error)}`);
throw error;
}

return {
id: activity.id,
activityId: activity.activity_id,
userId: activity.user_id,
petId: activity.pet_id,
activityTypeId: activity.activity_type_id,
scheduledStartTime: activity.scheduled_start_time,
startTime: activity.start_time,
endTime: activity.end_time,
notes: activity.notes,
};
}

async getActivities(): Promise<ActivityResponseDTO[]> {
try {
const activities: Array<PgActivity> = await PgActivity.findAll({ raw: true });
return activities.map((activity) => ({
id: activity.id,
activityId: activity.activity_id,
userId: activity.user_id,
petId: activity.pet_id,
activityTypeId: activity.activity_type_id,
scheduledStartTime: activity.scheduled_start_time,
startTime: activity.start_time,
endTime: activity.end_time,
notes: activity.notes,
}));
} catch (error: unknown) {
Logger.error(
`Failed to get activites. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
}

async createActivity(
activity: ActivityRequestDTO,
): Promise<ActivityResponseDTO> {
let newActivity: PgActivity | null;
try {
newActivity = await PgActivity.create({
activity_id: activity.activityId,
user_id: activity.userId,
pet_id: activity.petId,
activity_type_id: activity.activityTypeId,
scheduled_start_time: activity.scheduledStartTime,
start_time: activity.startTime,
end_time: activity.endTime,
notes: activity.notes,
});
} catch (error: unknown) {
Logger.error(
`Failed to create activity. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
return {
id: newActivity.id,
activityId: newActivity.activity_id,
userId: newActivity.user_id,
petId: newActivity.pet_id,
activityTypeId: newActivity.activity_type_id,
scheduledStartTime: newActivity.scheduled_start_time,
startTime: newActivity.start_time,
endTime: newActivity.end_time,
notes: newActivity.notes,
};
}

async updateActivity(
id: string,
activity: ActivityRequestDTO,
): Promise<ActivityResponseDTO | null> {
let resultingActivity: PgActivity | null;
let updateResult: [number, PgActivity[]] | null;
try {
updateResult = await PgActivity.update(
{
activity_id: activity.activityId,
user_id: activity.userId,
pet_id: activity.petId,
activity_type_id: activity.activityTypeId,
scheduled_start_time: activity.scheduledStartTime,
start_time: activity.startTime,
end_time: activity.endTime,
notes: activity.notes,
},
{ where: { id }, returning: true },
);

if (!updateResult[0]) {
throw new NotFoundError(`Activity id ${id} not found`);
}
[, [resultingActivity]] = updateResult;
} catch (error: unknown) {
Logger.error(
`Failed to update activity. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
return {
id: resultingActivity.id,
activityId: resultingActivity.activity_id,
userId: resultingActivity.user_id,
petId: resultingActivity.pet_id,
activityTypeId: resultingActivity.activity_type_id,
scheduledStartTime: resultingActivity.scheduled_start_time,
startTime: resultingActivity.start_time,
endTime: resultingActivity.end_time,
notes: resultingActivity.notes,
};
}

async deleteActivity(id: string): Promise<string> {
try {
const deleteResult: number | null = await PgActivity.destroy({
where: { id },
});
if (!deleteResult) {
throw new NotFoundError(`Activity id ${id} not found`);
}
return id;
} catch (error: unknown) {
Logger.error(
`Failed to delete activity. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
}
}

export default ActivityService;
2 changes: 1 addition & 1 deletion backend/typescript/services/implementations/petService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class PetService implements IPetService {
photo: pet.photo,
careInfo: {
id: pet.petCareInfo?.id,
safetyInfo: pet.petCareInfo?.safety_info ?? null,
safetyInfo: pet.petCareInfo?. ?? null,
medicalInfo: pet.petCareInfo?.medical_info ?? null,
managementInfo: pet.petCareInfo?.management_info ?? null,
},
Expand Down
Loading

0 comments on commit 9f04ff8

Please sign in to comment.