diff --git a/backend/typescript/graphql/index.ts b/backend/typescript/graphql/index.ts index 7b758379..149febc9 100644 --- a/backend/typescript/graphql/index.ts +++ b/backend/typescript/graphql/index.ts @@ -17,6 +17,8 @@ import userResolvers from "./resolvers/userResolvers"; import userType from "./types/userType"; import reviewDashboardResolvers from "./resolvers/reviewDashboardResolvers"; import reviewDashboardType from "./types/reviewDashboardType"; +import reviewedApplicantRecordTypes from "./types/reviewedApplicantRecordTypes"; +import reviewedApplicantRecordResolvers from "./resolvers/reviewedApplicantRecordResolver"; import adminCommentResolvers from "./resolvers/adminCommentsResolvers"; import adminCommentType from "./types/adminCommentsType"; import reviewPageType from "./types/reviewPageType"; @@ -43,6 +45,7 @@ const executableSchema = makeExecutableSchema({ simpleEntityType, userType, reviewDashboardType, + reviewedApplicantRecordTypes, adminCommentType, reviewPageType, ], @@ -52,6 +55,7 @@ const executableSchema = makeExecutableSchema({ simpleEntityResolvers, userResolvers, reviewDashboardResolvers, + reviewedApplicantRecordResolvers, adminCommentResolvers, reviewPageResolvers, ), @@ -79,6 +83,10 @@ const graphQLMiddlewares = { createSimpleEntity: authorizedByAllRoles(), updateSimpleEntity: authorizedByAllRoles(), deleteSimpleEntity: authorizedByAllRoles(), + createReviewedApplicantRecord: authorizedByAllRoles(), + bulkCreateReviewedApplicantRecord: authorizedByAllRoles(), + deleteReviewedApplicantRecord: authorizedByAllRoles(), + bulkDeleteReviewedApplicantRecord: authorizedByAllRoles(), createUser: authorizedByAdmin(), updateUser: authorizedByAdmin(), deleteUserById: authorizedByAdmin(), diff --git a/backend/typescript/graphql/resolvers/reviewedApplicantRecordResolver.ts b/backend/typescript/graphql/resolvers/reviewedApplicantRecordResolver.ts new file mode 100644 index 00000000..afa3a96c --- /dev/null +++ b/backend/typescript/graphql/resolvers/reviewedApplicantRecordResolver.ts @@ -0,0 +1,67 @@ +import ReviewedApplicantRecordService from "../../services/implementations/reviewedApplicantRecordService"; +import { + ReviewedApplicantRecordDTO, + CreateReviewedApplicantRecordDTO, + DeleteReviewedApplicantRecordDTO, +} from "../../types"; +import { getErrorMessage } from "../../utilities/errorUtils"; + +const reviewedApplicantRecordService = new ReviewedApplicantRecordService(); + +const reviewedApplicantRecordResolvers = { + Mutation: { + createReviewedApplicantRecord: async ( + _parent: undefined, + args: { input: CreateReviewedApplicantRecordDTO }, + ): Promise => { + try { + return await reviewedApplicantRecordService.createReviewedApplicantRecord( + args.input, + ); + } catch (error) { + throw new Error(getErrorMessage(error)); + } + }, + + bulkCreateReviewedApplicantRecord: async ( + _parent: undefined, + args: { inputs: CreateReviewedApplicantRecordDTO[] }, + ): Promise => { + try { + return await reviewedApplicantRecordService.bulkCreateReviewedApplicantRecord( + args.inputs, + ); + } catch (error) { + throw new Error(getErrorMessage(error)); + } + }, + + deleteReviewedApplicantRecord: async ( + _parent: undefined, + args: { input: DeleteReviewedApplicantRecordDTO }, + ): Promise => { + try { + return await reviewedApplicantRecordService.deleteReviewedApplicantRecord( + args.input, + ); + } catch (error) { + throw new Error(getErrorMessage(error)); + } + }, + + bulkDeleteReviewedApplicantRecord: async ( + _parent: undefined, + args: { inputs: DeleteReviewedApplicantRecordDTO[] }, + ): Promise => { + try { + return await reviewedApplicantRecordService.bulkDeleteReviewedApplicantRecord( + args.inputs, + ); + } catch (error) { + throw new Error(getErrorMessage(error)); + } + }, + }, +}; + +export default reviewedApplicantRecordResolvers; diff --git a/backend/typescript/graphql/types/reviewedApplicantRecordTypes.ts b/backend/typescript/graphql/types/reviewedApplicantRecordTypes.ts new file mode 100644 index 00000000..c7acaa8c --- /dev/null +++ b/backend/typescript/graphql/types/reviewedApplicantRecordTypes.ts @@ -0,0 +1,66 @@ +import { gql } from "apollo-server-express"; + +const reviewedApplicantRecordTypes = gql` + enum SkillCategory { + JUNIOR + INTERMEDIATE + SENIOR + } + + type Review { + passionFSG: Int + teamPlayer: Int + desireToLearn: Int + skill: Int + skillCategory: SkillCategory + } + + input ReviewInput { + passionFSG: Int + teamPlayer: Int + desireToLearn: Int + skill: Int + skillCategory: SkillCategory + } + + type ReviewedApplicantRecord { + applicantRecordId: ID! + reviewerId: Int! + review: Review + status: String + score: Int + reviewerHasConflict: Boolean + } + + input CreateReviewedApplicantRecordInput { + applicantRecordId: ID! + reviewerId: Int! + review: ReviewInput + status: String + } + + input DeleteReviewedApplicantRecord { + applicantRecordId: ID! + reviewerId: Int! + } + + extend type Mutation { + createReviewedApplicantRecord( + input: CreateReviewedApplicantRecordInput! + ): ReviewedApplicantRecord! + + bulkCreateReviewedApplicantRecord( + inputs: [CreateReviewedApplicantRecordInput!]! + ): [ReviewedApplicantRecord!]! + + deleteReviewedApplicantRecord( + input: DeleteReviewedApplicantRecord! + ): ReviewedApplicantRecord! + + bulkDeleteReviewedApplicantRecord( + inputs: [DeleteReviewedApplicantRecord!]! + ): [ReviewedApplicantRecord!]! + } +`; + +export default reviewedApplicantRecordTypes; diff --git a/backend/typescript/migrations/20251112023004-add-createdat-updatedat-to-reviewed-application.ts b/backend/typescript/migrations/20251112023004-add-createdat-updatedat-to-reviewed-application.ts new file mode 100644 index 00000000..f091e88b --- /dev/null +++ b/backend/typescript/migrations/20251112023004-add-createdat-updatedat-to-reviewed-application.ts @@ -0,0 +1,23 @@ +import { DataType } from "sequelize-typescript"; +import { Migration } from "../umzug"; + +const TABLE_NAME = "reviewed_applicant_records"; + +export const up: Migration = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().addColumn(TABLE_NAME, "createdAt", { + type: DataType.DATE, + allowNull: false, + defaultValue: DataType.NOW, + }); + + await sequelize.getQueryInterface().addColumn(TABLE_NAME, "updatedAt", { + type: DataType.DATE, + allowNull: false, + defaultValue: DataType.NOW, + }); +}; + +export const down: Migration = async ({ context: sequelize }) => { + await sequelize.getQueryInterface().removeColumn(TABLE_NAME, "createdAt"); + await sequelize.getQueryInterface().removeColumn(TABLE_NAME, "updatedAt"); +}; diff --git a/backend/typescript/services/implementations/reviewedApplicantRecordService.ts b/backend/typescript/services/implementations/reviewedApplicantRecordService.ts new file mode 100644 index 00000000..4b0f8444 --- /dev/null +++ b/backend/typescript/services/implementations/reviewedApplicantRecordService.ts @@ -0,0 +1,126 @@ +import { sequelize } from "../../models"; +import ReviewedApplicantRecord from "../../models/reviewedApplicantRecord.model"; +import { + ReviewedApplicantRecordDTO, + CreateReviewedApplicantRecordDTO, + DeleteReviewedApplicantRecordDTO, +} from "../../types"; +import { getErrorMessage } from "../../utilities/errorUtils"; +import logger from "../../utilities/logger"; +import IReviewApplicantRecordService from "../interfaces/IReviewedApplicantRecordService"; + +const Logger = logger(__filename); + +class ReviewedApplicantRecordService implements IReviewApplicantRecordService { + /* eslint-disable class-methods-use-this */ + async createReviewedApplicantRecord( + dto: CreateReviewedApplicantRecordDTO, + ): Promise { + try { + const record = await ReviewedApplicantRecord.create(dto); + return record.toJSON() as ReviewedApplicantRecordDTO; + } catch (error: unknown) { + Logger.error( + `Failed to create reviewed applicant record. Reason = ${getErrorMessage( + error, + )}`, + ); + throw error; + } + } + + async bulkCreateReviewedApplicantRecord( + createReviewedApplicantRecordDTOs: CreateReviewedApplicantRecordDTO[], + ): Promise { + try { + const reviewedApplicantRecords = await sequelize.transaction( + async (t) => { + const records = await ReviewedApplicantRecord.bulkCreate( + createReviewedApplicantRecordDTOs, + { transaction: t }, + ); + return records; + }, + ); + + return reviewedApplicantRecords.map( + (record) => record.toJSON() as ReviewedApplicantRecordDTO, + ); + } catch (error: unknown) { + Logger.error( + `Failed to bulk create reviewed applicant records. Reason = ${getErrorMessage( + error, + )}`, + ); + throw error; + } + } + + async deleteReviewedApplicantRecord( + deleteReviewedApplicantRecord: DeleteReviewedApplicantRecordDTO, + ): Promise { + try { + const { applicantRecordId } = deleteReviewedApplicantRecord; + const { reviewerId } = deleteReviewedApplicantRecord; + const record = await ReviewedApplicantRecord.findOne({ + where: { applicantRecordId, reviewerId }, + }); + + if (!record) { + throw new Error("ReviewedApplicantRecord not found, delete failed"); + } + + await record.destroy(); + return record.toJSON() as ReviewedApplicantRecordDTO; + } catch (error: unknown) { + Logger.error( + `Failed to delete reviewed applicant records. Reason = ${getErrorMessage( + error, + )}`, + ); + throw error; + } + } + + async bulkDeleteReviewedApplicantRecord( + deleteReviewedApplicantRecords: DeleteReviewedApplicantRecordDTO[], + ): Promise { + try { + const deletedRecords = await sequelize.transaction(async (t) => { + const records = await Promise.all( + deleteReviewedApplicantRecords.map( + ({ applicantRecordId, reviewerId }) => + ReviewedApplicantRecord.findOne({ + where: { applicantRecordId, reviewerId }, + transaction: t, + }), + ), + ); + + if (records.some((r) => !r)) { + throw new Error("Not all records were found, bulk delete failed"); + } + + const existingRecords = records as ReviewedApplicantRecord[]; + await Promise.all( + existingRecords.map((r) => r.destroy({ transaction: t })), + ); + + return existingRecords; + }); + + return deletedRecords.map( + (r) => r.toJSON() as ReviewedApplicantRecordDTO, + ); + } catch (error: unknown) { + Logger.error( + `Failed to bulk delete reviewed applicant records. Reason = ${getErrorMessage( + error, + )}`, + ); + throw error; + } + } +} + +export default ReviewedApplicantRecordService; diff --git a/backend/typescript/services/interfaces/IReviewedApplicantRecordService.ts b/backend/typescript/services/interfaces/IReviewedApplicantRecordService.ts new file mode 100644 index 00000000..646ffc67 --- /dev/null +++ b/backend/typescript/services/interfaces/IReviewedApplicantRecordService.ts @@ -0,0 +1,42 @@ +import { + ReviewedApplicantRecordDTO, + CreateReviewedApplicantRecordDTO, + DeleteReviewedApplicantRecordDTO, +} from "../../types"; + +interface IReviewApplicantRecordService { + /** + * Creates a single reviewed applicant record entry + * @Param createReviewedApplicantRecordDTO data to create reviewed applicant record + */ + createReviewedApplicantRecord( + createReviewedApplicantRecordDTO: CreateReviewedApplicantRecordDTO, + ): Promise; + + /** + * Creates multiple reviewed applicant record entries in bulk + * @Param createReviewedApplicantRecordDTOs array of data to create reviewed applicant records + */ + bulkCreateReviewedApplicantRecord( + createReviewedApplicantRecordDTOs: CreateReviewedApplicantRecordDTO[], + ): Promise; + + /** + * Deletes a single reviewed applicant record entry + * @Param applicantRecordId the ID of applicant record to delete + * @Param reviewerId the ID of the reviewer + */ + deleteReviewedApplicantRecord( + deleteReviewedApplicantRecord: DeleteReviewedApplicantRecordDTO, + ): Promise; + + /** + * Deletes multiple reviewed applicant record entries in bulk + * @Param deleteReviewedApplicantRecord array of data to delete reviewed applicant records + */ + bulkDeleteReviewedApplicantRecord( + deleteReviewedApplicantRecords: DeleteReviewedApplicantRecordDTO[], + ): Promise; +} + +export default IReviewApplicantRecordService; diff --git a/backend/typescript/types.ts b/backend/typescript/types.ts index b423dc77..7a838d29 100644 --- a/backend/typescript/types.ts +++ b/backend/typescript/types.ts @@ -208,6 +208,18 @@ export type ReviewedApplicantRecordDTO = { reviewerHasConflict: boolean; }; +export type CreateReviewedApplicantRecordDTO = { + applicantRecordId: string; + reviewerId: number; + review?: Review; + reviewerHasConflict?: boolean; +}; + +export type DeleteReviewedApplicantRecordDTO = { + applicantRecordId: string; + reviewerId: number; +}; + export type AdminCommentDTO = { id: string; userId: number;