Skip to content

Commit

Permalink
Onboarding task completed
Browse files Browse the repository at this point in the history
  • Loading branch information
vips11 committed Oct 10, 2024
1 parent 434e6f4 commit f8811bd
Show file tree
Hide file tree
Showing 15 changed files with 427 additions and 73 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 { validatePrimitive, getApiValidationError } 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("role", "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 { teamRoleEnum } 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: teamRoleEnum,
allowNull: false,
},
});
};

export const down: Migration = async ({ context: sequelize }) => {
await sequelize.getQueryInterface().dropTable(TABLE_NAME);
};
24 changes: 24 additions & 0 deletions backend/typescript/models/teamMember.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
Column,
DataType,
Model,
Table,
AllowNull,
} from "sequelize-typescript";
import { TeamRole, teamRoleEnum } 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(...Object.values(teamRoleEnum)),
allowNull: false,
})
team_role!: TeamRole;
}
2 changes: 1 addition & 1 deletion backend/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"pg": "^8.5.1",
"reflect-metadata": "^0.1.13",
"sequelize": "^6.5.0",
"sequelize-typescript": "^2.1.0",
"sequelize-typescript": "^2.1.6",
"swagger-ui-express": "^4.1.6",
"ts-node": "^10.0.0",
"umzug": "^3.0.0-beta.16",
Expand Down
32 changes: 32 additions & 0 deletions backend/typescript/rest/teamMemberRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Router } from "express";
import ITeamMemberService from "../services/interfaces/teamMemberService";
import TeamMemberService from "../services/implementations/teamMemberService";
import { CreateTeamMemberDTO, TeamMemberDTO, UserDTO } from "../types";
import { getErrorMessage } from "../utilities/errorUtils";
import { createTeamMemberDtoValidator } from "../middlewares/validators/teamMemberValidators";

const teamMemberRouter: Router = Router();

const teamMemberService: ITeamMemberService = new TeamMemberService();

/* Get all team members */
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;
54 changes: 54 additions & 0 deletions backend/typescript/services/implementations/teamMemberService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import PgTeamMember from "../../models/teamMember.model";
import { CreateTeamMemberDTO, TeamMemberDTO } from "../../types";
import { getErrorMessage, NotFoundError } from "../../utilities/errorUtils";
import logger from "../../utilities/logger";
import ITeamMemberService from "../interfaces/teamMemberService";

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 a list of all the team members in the database
* @returns a TeamMemberDTO with a team member's information
* @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;
15 changes: 15 additions & 0 deletions backend/typescript/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ export type UserDTO = {
phoneNumber?: string | null;
};

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

export type CreateUserDTO = Omit<UserDTO, "id"> & { password: string };

export type CreateTeamMemberDTO = Omit<TeamMemberDTO, "id">;

export type UpdateUserDTO = Omit<UserDTO, "id">;

export type RegisterUserDTO = Omit<CreateUserDTO, "role">;
Expand All @@ -33,6 +42,12 @@ export type AuthDTO = Token & UserDTO;

export type Letters = "A" | "B" | "C" | "D";

const teamRoleValues = ["PM", "DESIGNER", "PL", "DEVELOPER"] as const;

export const teamRoleEnum: TeamRole[] = [...teamRoleValues];

export type TeamRole = typeof teamRoleValues[number];

const sexValues = ["M", "F"] as const;

export const sexEnum: Sex[] = [...sexValues];
Expand Down
58 changes: 21 additions & 37 deletions backend/typescript/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -561,15 +561,6 @@
camel-case "4.1.2"
tslib "~2.1.0"

"@graphql-tools/utils@^7.6.0":
version "7.10.0"
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-7.10.0.tgz#07a4cb5d1bec1ff1dc1d47a935919ee6abd38699"
integrity sha512-d334r6bo9mxdSqZW6zWboEnnOOFRrAPVQJ7LkU8/6grglrbcu6WhwCLzHb90E94JI3TD3ricC3YGbUqIi9Xg0w==
dependencies:
"@ardatan/aggregate-error" "0.0.6"
camel-case "4.1.2"
tslib "~2.2.0"

"@grpc/grpc-js@~1.2.0":
version "1.2.10"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.10.tgz#f316d29a45fcc324e923d593cb849d292b1ed598"
Expand Down Expand Up @@ -3416,10 +3407,10 @@ glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
dependencies:
is-glob "^4.0.1"

glob@7.1.6, glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
glob@7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
Expand All @@ -3440,6 +3431,18 @@ glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"

glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"

global-dirs@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d"
Expand Down Expand Up @@ -3569,25 +3572,6 @@ graphql-middleware@^6.0.6:
"@graphql-tools/delegate" "^7.1.1"
"@graphql-tools/schema" "^7.1.3"

graphql-rate-limit@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/graphql-rate-limit/-/graphql-rate-limit-3.3.0.tgz#241e3f6dc4cc3cbd139d63d40e2ce613f8485646"
integrity sha512-mbbEv5z3SjkDLvVVdHi0XrVLavw2Mwo93GIqgQB/fx8dhcNSEv3eYI1OGdp8mhsm/MsZm7hjrRlwQMVRKBVxhA==
dependencies:
"@graphql-tools/utils" "^7.6.0"
graphql-shield "^7.5.0"
lodash.get "^4.4.2"
ms "^2.1.3"

graphql-shield@^7.5.0:
version "7.5.0"
resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.5.0.tgz#aa3af226946946dfadac33eccc6cbe7fec6e9000"
integrity sha512-T1A6OreOe/dHDk/1Qg3AHCrKLmTkDJ3fPFGYpSOmUbYXyDnjubK4J5ab5FjHdKHK5fWQRZNTvA0SrBObYsyfaw==
dependencies:
"@types/yup" "0.29.11"
object-hash "^2.0.3"
yup "^0.31.0"

graphql-subscriptions@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d"
Expand Down Expand Up @@ -6008,12 +5992,12 @@ sequelize-pool@^6.0.0:
resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-6.1.0.tgz#caaa0c1e324d3c2c3a399fed2c7998970925d668"
integrity sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg==

sequelize-typescript@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/sequelize-typescript/-/sequelize-typescript-2.1.0.tgz#7d42dac368f32829a736acc4f0c9f3b79fc089bb"
integrity sha512-wwPxydBQ/wIZ92pFxDQEAhW8uRHqwFZGm6JkPmpsCjrODWrH8TANZiOCjwGouygFMgBwCNK91RNwLe5TYoy5pg==
sequelize-typescript@^2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/sequelize-typescript/-/sequelize-typescript-2.1.6.tgz#9476c8a2510114ed1c3a26b424c47e05c2e6284e"
integrity sha512-Vc2N++3en346RsbGjL3h7tgAl2Y7V+2liYTAOZ8XL0KTw3ahFHsyAUzOwct51n+g70I1TOUDgs06Oh6+XGcFkQ==
dependencies:
glob "7.1.6"
glob "7.2.0"

sequelize@^6.5.0:
version "6.5.0"
Expand Down
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/TeamMemberTypes";
import baseAPIClient from "./BaseAPIClient";

const get = async (): Promise<TeamMember[]> => {
try {
const { data } = await baseAPIClient.get("team-members/");
return data;
} catch (error) {
throw new Error(`Failed to get team members: ${error}`);
}
};

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 };
7 changes: 7 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 @@ -117,6 +118,12 @@ const App = (): React.ReactElement => {
component={Default}
allowedRoles={AuthConstants.ALL_ROLES}
/>
<PrivateRoute
exact
path={Routes.TEAM_MEMBERS_PAGE}
component={TeamMembersPage}
allowedRoles={AuthConstants.ALL_ROLES}
/>
<Route exact path="*" component={NotFound} />
</Switch>
</Router>
Expand Down
Loading

0 comments on commit f8811bd

Please sign in to comment.