Skip to content

Commit

Permalink
Onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
aliicezhao committed Oct 4, 2024
1 parent dd11596 commit 7d39693
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 0 deletions.
21 changes: 21 additions & 0 deletions backend/typescript/middlewares/validators/teamMemberValidators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Request, Response, NextFunction } from "express";
import { getApiValidationError, validatePrimitive } from "./util";

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable-next-line import/prefer-default-export */
export const createTeamMemberDtoValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
if (!validatePrimitive(req.body.firstName, "string")) {
return res.status(400).send(getApiValidationError("firstName", "string"));
}
if (!validatePrimitive(req.body.lastName, "string")) {
return res.status(400).send(getApiValidationError("lastName", "string"));
}
if (!validatePrimitive(req.body.teamRole, "string")) {
return res.status(400).send(getApiValidationError("teamRole", "string"));
}
return next();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { DataType } from "sequelize-typescript";

import { Migration } from "../umzug";
import { teamRoleValues } from "../types";

const TABLE_NAME = "team_members";

export const up: Migration = async ({ context: sequelize }) => {
await sequelize.getQueryInterface().createTable(TABLE_NAME, {
id: {
type: DataType.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
first_name: {
type: DataType.STRING,
allowNull: false,
},
last_name: {
type: DataType.STRING,
allowNull: false,
},
team_role: {
type: DataType.ENUM,
values: teamRoleValues,
allowNull: false,
},
});
};

export const down: Migration = async ({ context: sequelize }) => {
await sequelize.getQueryInterface().dropTable(TABLE_NAME);
};
21 changes: 21 additions & 0 deletions backend/typescript/models/teamMember.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
Column,
DataType,
Model,
Table,
AllowNull,
} from "sequelize-typescript";
import { TeamRole, teamRoleValues } from "../types";

@Table({ timestamps: false, tableName: "team_members" })
export default class TeamMember extends Model {
@Column({ type: DataType.STRING, allowNull: false })
first_name!: string;

@Column({ type: DataType.STRING, allowNull: false })
last_name!: string;

@AllowNull(false)
@Column({ type: DataType.ENUM, values: teamRoleValues, allowNull: false })
team_role!: TeamRole;
}
29 changes: 29 additions & 0 deletions backend/typescript/rest/teamMemberRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Router } from "express";
import TeamMemberService from "../services/implementations/teamMemberService";
import { getErrorMessage } from "../utilities/errorUtils";
import { CreateTeamMemberDTO } from "../types";
import { createTeamMemberDtoValidator } from "../middlewares/validators/teamMemberValidators";

const teamMemberRouter: Router = Router();
const teamMemberService = new TeamMemberService();

teamMemberRouter.get("/", async (req, res) => {
try {
const teamMembers = await teamMemberService.getTeamMembers();
res.status(200).json(teamMembers);
} catch (error: unknown) {
res.status(500).json({ error: getErrorMessage(error) });
}
});

teamMemberRouter.post("/", createTeamMemberDtoValidator, async (req, res) => {
const data: CreateTeamMemberDTO = req.body;
try {
const newTeamMember = await teamMemberService.createTeamMember(data);
res.status(201).json(newTeamMember);
} catch (error: unknown) {
res.status(500).json({ error: getErrorMessage(error) });
}
});

export default teamMemberRouter;
2 changes: 2 additions & 0 deletions backend/typescript/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import entityRouter from "./rest/entityRoutes";
import petRouter from "./rest/petRoutes";
import simpleEntityRouter from "./rest/simpleEntityRoutes";
import userRouter from "./rest/userRoutes";
import teamMemberRouter from "./rest/teamMemberRoutes";

const CORS_ALLOW_LIST = [
"http://localhost:3000",
Expand Down Expand Up @@ -44,6 +45,7 @@ app.use("/pets", petRouter);
app.use("/simple-entities", simpleEntityRouter);
app.use("/users", userRouter);
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.use("/team-member", teamMemberRouter);

sequelize.authenticate();

Expand Down
53 changes: 53 additions & 0 deletions backend/typescript/services/implementations/teamMemberService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import PgTeamMember from "../../models/teamMember.model";
import ITeamMemberService from "../interfaces/teamMemberService";
import { TeamMemberDTO, CreateTeamMemberDTO } from "../../types";
import { getErrorMessage } from "../../utilities/errorUtils";
import logger from "../../utilities/logger";

const Logger = logger(__filename);

class TeamMemberService implements ITeamMemberService {
/* eslint-disable class-methods-use-this */
async getTeamMembers(): Promise<TeamMemberDTO[]> {
try {
const teamMembers: Array<PgTeamMember> = await PgTeamMember.findAll();
return teamMembers.map((teamMember) => ({
id: String(teamMember.id),
firstName: teamMember.first_name,
lastName: teamMember.last_name,
teamRole: teamMember.team_role,
}));
} catch (error: unknown) {
Logger.error(
`Failed to get team members. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
}

async createTeamMember(
teamMember: CreateTeamMemberDTO,
): Promise<TeamMemberDTO> {
let newTeamMember: PgTeamMember | null;
try {
newTeamMember = await PgTeamMember.create({
first_name: teamMember.firstName,
last_name: teamMember.lastName,
team_role: teamMember.teamRole,
});
} catch (error: unknown) {
Logger.error(
`Failed to create team member. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
return {
id: String(newTeamMember.id),
firstName: newTeamMember.first_name,
lastName: newTeamMember.last_name,
teamRole: newTeamMember.team_role,
};
}
}

export default TeamMemberService;
20 changes: 20 additions & 0 deletions backend/typescript/services/interfaces/teamMemberService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CreateTeamMemberDTO, TeamMemberDTO } from "../../types";

interface ITeamMemberService {
/**
* Get all team member information
* @returns array of TeamMemberDTO
* @throws Error if team member retrieval fails
*/
getTeamMembers(): Promise<TeamMemberDTO[]>;

/**
* Create a team member
* @param teamMember the team member to be created
* @returns a TeamMemberDTO with the created team member's information
* @throws Error if team member creation fails
*/
createTeamMember(teamMember: CreateTeamMemberDTO): Promise<TeamMemberDTO>;
}

export default ITeamMemberService;
12 changes: 12 additions & 0 deletions backend/typescript/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,15 @@ export type NodemailerConfig = {
};

export type SignUpMethod = "PASSWORD" | "GOOGLE";

export const teamRoleValues = ["PM", "DESIGNER", "PL", "DEVELOPER"] as const;
export type TeamRole = typeof teamRoleValues[number];

export type TeamMemberDTO = {
id: string;
firstName: string;
lastName: string;
teamRole: TeamRole;
};

export type CreateTeamMemberDTO = Omit<TeamMemberDTO, "id">;
30 changes: 30 additions & 0 deletions frontend/src/APIClients/TeamMembersAPIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TeamMember, TeamRole } from "../types/TeamMembersTypes";
import baseAPIClient from "./BaseAPIClient";

const get = async (): Promise<TeamMember[] | null> => {
try {
const { data } = await baseAPIClient.get("team-members/");
return data;
} catch (error: unknown) {
return null;
}
};

const create = async (
firstName: string,
lastName: string,
teamRole: TeamRole,
): Promise<TeamMember[] | null> => {
try {
const { data } = await baseAPIClient.post("team-members/", {
firstName,
lastName,
teamRole,
});
return data;
} catch (error: unknown) {
return null;
}
};

export default { get, create };
6 changes: 6 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import EditTeamInfoPage from "./components/pages/EditTeamPage";
import HooksDemo from "./components/pages/HooksDemo";

import { AuthenticatedUser } from "./types/AuthTypes";
import TeamMembersPage from "./components/pages/TeamMembersPage";

const App = (): React.ReactElement => {
const currentUser: AuthenticatedUser = getLocalStorageObj<AuthenticatedUser>(
Expand Down Expand Up @@ -118,6 +119,11 @@ const App = (): React.ReactElement => {
allowedRoles={AuthConstants.ALL_ROLES}
/>
<Route exact path="*" component={NotFound} />
<Route
exact
path={Routes.TEAM_MEMBERS}
component={TeamMembersPage}
/>
</Switch>
</Router>
</AuthContext.Provider>
Expand Down
65 changes: 65 additions & 0 deletions frontend/src/components/pages/TeamMembersPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useEffect, useState } from "react";
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
VStack,
Button,
} from "@chakra-ui/react";
import TeamMembersAPIClient from "../../APIClients/TeamMembersAPIClient";
import { TeamMember } from "../../types/TeamMembersTypes";

const TeamMembersPage = (): React.ReactElement => {
const [teamMembers, setTeamMembers] = useState<TeamMember[]>([]);

const getTeamMembers = async () => {
const teamMembersData = await TeamMembersAPIClient.get();
if (teamMembersData) {
setTeamMembers(teamMembersData);
}
};

const addTeamMember = async () => {
await TeamMembersAPIClient.create("Jerry", "Cheng", "PL");
await getTeamMembers(); // updates table with latest data
};

useEffect(() => {
getTeamMembers();
}, []);

return (
<VStack spacing="24px" style={{ margin: "24px auto" }}>
<h1>Team Members Page</h1>
<TableContainer>
<Table colorScheme="blue">
<Thead>
<Tr>
<Th>First Name</Th>
<Th>Last Name</Th>
<Th>Team Role</Th>
</Tr>
</Thead>
<Tbody>
{teamMembers.map((teamMember, index) => (
<Tr key={index}>
<Td>{teamMember.firstName}</Td>
<Td>{teamMember.lastName}</Td>
<Td>{teamMember.teamRole}</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<Button colorScheme="blue" onClick={addTeamMember}>
+ Add a Jerry
</Button>
</VStack>
);
};

export default TeamMembersPage;
2 changes: 2 additions & 0 deletions frontend/src/constants/Routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ export const UPDATE_SIMPLE_ENTITY_PAGE = "/simpleEntity/update";
export const HOOKS_PAGE = "/hooks";

export const DEV_UTILITY_PAGE = "/dev-utility"; // TODO: This is only here for development purposes

export const TEAM_MEMBERS = "/team-members";
8 changes: 8 additions & 0 deletions frontend/src/types/TeamMembersTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type TeamRole = "PM" | "DESIGNER" | "PL" | "DEVELOPER";

export type TeamMember = {
id: string;
firstName: string;
lastName: string;
teamRole: TeamRole;
};

0 comments on commit 7d39693

Please sign in to comment.